ころがる狸

ころがる狸のデータ解析ブログ

【自然言語処理】単語埋め込みからSelf-Attention、2値分類まで。

こんにちは、Dajiroです。前回の技術記事を書いてからだいぶ日が空きました。本ブログでは機械学習に関する幅広い技術を解説しようと目論んでいるので、まだ扱ったことのない自然言語処理のネタををじっくりコトコト仕込んでいました。本記事では

  • 単語埋め込み
  • 語順の組み込み
  • Self-Attention

に焦点を当てながら、2値分類の一連のワークフローの解説と(若干の)実装をご紹介します!実装はこちらの書籍を参考にしました。2値分類を行うためのTransformerのエンコーダ部分が紹介されています。

Transformerとは?

Transformerとは、2017年に報告された自然言語処理分野におけるニューラルネットワークのモデルです。Self-Attentionという機構を導入することで高速計算・高精度予測を両立し、当時の世界最高性を示したモデルとして知られます。現在ではBertという更に優れたモデルがありますが、これもTransformerをベースとしています。

余談ですが、Transformerと聞くと皆さんなにを思い浮かべますか?私はこちら、超生命体トランスフォーマー・ビーストウォーズです!今から20年以上前に放送されていたCGアニメですが、もろ世代なのでした。Transformerというモデル名は、これを意識したジョークだと思われます。

f:id:Dajiro:20200621164751p:plain
ビーストウォーズ。ライノックスの変身。

仕組みの概要

f:id:Dajiro:20200625223255p:plain
Transformerモデルのアーキテクチャ。本記事ではエンコーダ側のみ実装。

全体の流れ

こちらはTransformerの全体図です。この図の左側がエンコーダと呼ばれるもので、入力される元の文章を何らかの問題を解けるような形式にエンコード(暗号化)します。本ブログではこれを元に文章がポジティブか、ネガティブかの2値分類を行いますが、翻訳タスクではこの暗号化された情報をデコード(暗号復元)して、日本語→英語のように別の言語形式に復元します。

以下では、エンコーダ側の解説を行います。エンコーダの処理ステップは以下のようになります。

エンコーダのワークフロー

  1. 文章のベクトル化
  2. 語順情報を追加
  3. TransformerBlockを複数回計算する
    1. 各単語の関係性の計算(Self-Attention)
    2. 全結合層の計算
  4. (分類タスクの場合)得られた特徴量を元に2値分類タスクを行う

このように書くと意外とさっぱりしていると思われるかもしれません。Transformerを理解するために特に重要な、文章のベクトル化、語順情報の計算 アテンションの計算を順に見ていきましょう。

1. 文章のベクトル化

文章をニューラルネットワークに読み込ませるためには、何らかのベクトル表現に『埋め込む』必要があります。例えば

【To be, or not to be, that is the question.】

という英文があったとき、第一ステップとして単語を分割します。

【"To", "be", ",", "or", "not", "to", "be", ",", "that", "is", "the", "question", "."】

ここでは13語の単語に分割されました(カンマ、ピリオド含む)。それぞれの単語を、適当な次元数を持った特徴量ベクトルで表現します。例えば300次元とすれば、この英文は13×300の配列として表現できるようになります。

単語の特徴量ベクトルを生成するには、CBOW、 skip-gram、fastText等の技術が知られており、ここでは最も一般的であろうskip-gramを説明します。skip-gramでは、特徴量を抽出したい単語のみ1, それ以外を0として文章をベクトル化します(ワンホットベクトル)。それに対し、その周辺の単語を1, それ以外を0としたベクトル(例えば上の英文なら、"is"の特徴量を得たいなら"that", "the"に対応)を教師データとしてニューラルネットワークを訓練することで特徴量を生成します。下の図のようなイメージです。

f:id:Dajiro:20200621190124p:plain
skipgramのイメージ図。
こうして生成されるニューラルネットワークの重みW_{in}の各行には、各単語に対応した任意の次元の特徴量ベクトルが埋め込まれます。入力ベクトルがワンホットベクトルなので、対象となる単語に対応した行以外はゼロになるという寸法です。もちろん入力ベクトルは色々変えながら学習するので、W_{in}の各行には最終的に何らかの数字が書き込まれます。
f:id:Dajiro:20200626101324p:plain
ニューラルネットワークの重みW_{in}に特徴量が書き込まれる
自然言語処理のタスクを行う度に自力でベクトル化するのは大変なので、学習済みのモデルを使うことが多いようです。上記の方法には登録されていない未知の単語に対応できないという短所があるため、それを克服したskip-gramの発展形であるfastTextというモデルに基づいて以下では計算を行います。

2. 語順情報を追加

ここまでで単語の埋め込みが完了したとしましょう。つまりある文章が(単語数×埋め込み次元)のテンソル形式で表されている状態です。TransformerではSelf-Attentionという処理によりタスクを解くための単語の重要度を評価できる一方、RNNのように語順情報を考慮することはできません。そこでPositional Encodingと呼ばれる方法を用いて特徴量ベクトルに語順情報を付加することにします。

Positional Encodingには以下の式を計算します。
 PE_{(pos, 2i)} = sin(pos/10000^{2i/d_{model}})
 PE_{(pos, 2i+1)} = cos(pos/10000^{2i/d_{model}})

posは文章の中で何番目の単語かを表し、iはその単語の特徴量ベクトルのうち何次元目の数値かを表しています。つまりPEは(単語数×埋め込み次元)の形状の配列であり、文章を表す特徴量のテンソルと同じ形状をしています。この2つのテンソルを要素ごとに加算することで、Positional Encodingの処理が完了します。

実装は以下のようになります。Positional Encodingは見慣れない処理で意味分からん感じがしますが、よくよく見てみると単に上式を計算して配列を作るという単純な操作であるとも言えます。

class PositionalEncoder(nn.Module):
    def __init__(self, d_model = 300, max_seq_len = 256):
        super().__init__()
        
        self.d_model = d_model
        pe = torch.zeros(max_seq_len, d_model)
        
        for pos in range(max_seq_len):
            for i in range(0, d_model, 2):
                #Positional Encodingを計算
                pe[pos, i] = math.sin(pos / (10000 ** ((2 * i)/d_model)))
                pe[pos, i + 1] = math.cos(pos /
                                          (10000 ** ((2 * (i + 1))/d_model)))
        #バッチの次元を追加します。
        self.pe = pe.unsqueeze(0)
        self.pe.requires_grad = False
        
    def forward(self, x):
        #PEと元の特徴量の配列を加算
        ret = math.sqrt(self.d_model)*x + self.pe
        return ret
3.アテンションの計算

学習の準備がようやく整いました!ここから、Self-Attentionを用いて単語同士の相関関係を抽出します。RNNでは単語を文頭から逐次的に処理していくイメージでしたが、Self-Attentionでは文中の全単語にそれぞれ重みをかけて、その和として1つの単語を構成します。

f:id:Dajiro:20200624081837p:plain
アテンション係数による単語の表現。
ここでカラーで示したのは、ターゲットとなる単語と特に強く相互作用している単語の重要度です。この重要度をアテンション係数と呼ぶわけですが、それは以下の図のように文章のマトリックス同士の行列計算によって定義されます。これにより各単語ベクトルの全組み合わせの積が計算され、似た単語は大きな値、かけ離れた単語は小さな値が得られるため単語同士の類似度が計算できると期待されます。単語の次元が大きいと係数も大きくなるので、次元数dで除算しています。
f:id:Dajiro:20200624085139p:plain
アテンション係数の求め方。文中単語の全組み合わせの積を取る。
ここで求めた係数を、再び元の文章のマトリックスに掛けることで、アテンション係数の重みつき和がすべて行列形式で表現できるようになります。こうした行列計算の導入により、GPUや並列化を用いた高速計算が可能になったという点がアテンションの強力な長所です。
最後に、アテンション層の処理内容の全体像を振り返ってみましょう。Self-Attentionがすべて行列計算で処理できることが分かると思います。ここでキー、クエリ、バリューと見慣れない単語が出てきますが、Self-Attentionではなかなかイメージしづらいと思うのでとりあえずそういうものとお考え下さい(別種のsource-target attentionではその意味が明確になると思います)。
f:id:Dajiro:20200624085112p:plain
アテンション層における全体の処理内容のイメージ図。

二値分類と結果の解釈

最後に二値分類を行います。ここでは、IMDBという映画レビューのデータセットを用いてその文章が肯定的か、否定的かを分類するタスクを実行します。
上記のアテンション処理はTransformerの核心部分ですが、その前後には更に以下のような操作を行います。

  • Layer normalization処理
  • Self-Attention処理
  • dropout処理
  • Layer normalization処理
  • dropout処理

これらのうちattention以外は全てPyTorchのフレームワークの中の関数として定義されているので実装は容易です。
これを用いて二値分類を行うのにはちょっとしたトリックがあり、文章の先頭にclsという2値判定を行うために使う単語を1つ差し込んでおきます。2値判定を行う際にはこれだけを多層パーセプトロンに通し、最終的な分類結果を得ます。またclsと他の単語とのアテンション係数を見ることによって、2値分類を行う際にどの単語が強調されたのか分かるようになり、都合が良いのです。

学習の結果だけ見てみましょう。5エポックだけ走らせてみましたが、なんという重さでしょうか。CPU環境で計算を走らせると2時間近くかかってしまいました。GPUが欲しくてたまりません。一方で、予測の正解率は85%と中々の精度をたたき出しています。

epoch 1/5 | train | Loss: 0.5229 Acc: 0.7331
epoch 1/5 |  val  | Loss: 0.3959 Acc: 0.8236
epoch 2/5 | train | Loss: 0.4031 Acc: 0.8157
epoch 2/5 |  val  | Loss: 0.3872 Acc: 0.8242
epoch 3/5 | train | Loss: 0.3761 Acc: 0.8329
epoch 3/5 |  val  | Loss: 0.3571 Acc: 0.8404
epoch 4/5 | train | Loss: 0.3613 Acc: 0.8406
epoch 4/5 |  val  | Loss: 0.3573 Acc: 0.8420
epoch 5/5 | train | Loss: 0.3490 Acc: 0.8478
epoch 5/5 |  val  | Loss: 0.3368 Acc: 0.8492
CPU times: user 6h 2min 32s, sys: 2h 2min 1s, total: 8h 4min 33s
Wall time: 1h 51min 8s

続いて、アテンション係数を用いて予測に強く相関する単語を抜き出したのが以下のhtmlです。赤で示した部分が予測に重要な単語で、2つのTransformerBlockの結果を示しています。これは正解positive, 予測positiveの結果ですが、上段を見るとgood, delightful, betterというポジティブな印象がある単語にアテンションが強くかかっていることが分かります!一方で下段ではthe, and, in, toなど中性的な単語にアテンションが掛かっており、正直これを見てもなぜ正解を予測できたのかは分かりません。しかし、上段のような結果を解析することでAIの予測根拠を可視化できるためより良いモデル構築のためのヒントが得られそうです。

f:id:Dajiro:20200625222815p:plain
アテンションの可視化。

所感

自然言語処理の技術を始めて紹介しましたが、解説すべき事項が多すぎて非常に大変でした汗 自然言語処理は数ある機械学習技術の中でも難しい部類に入るのではないでしょうか。特にTransformerのような発展的なモデルではなおさらです。しかしTransformerの核心にあるSelf-Attentionに関しては腑に落ちるレベルでの理解が得られたと思うので、それは大きな収穫でした。いつかBertに関しても動かし、まとめてみたいと思います。