TransformerをPython・Kerasで実装したレビュー分類を行うサンプルコードを解説(1)

前回の記事
https://nine-num-98.blogspot.com/2023/04/imdb-classification-01.html
で紹介したTransformerのサンプルコードについて解説します。

各プログラム等の概要

GitHub公開プログラム
https://github.com/kotetsu99/imdb_classification

(1)01-imdb_train.py: レビュー学習プログラム
・IMDBのレビューを読み込み、高評価/低評価の分類をAIに学習させる。
・AIモデルは、TransformerのEncoder部分を主に使用

(2)02-imdb_test.py: レビュー分類プログラム
・(1)で学習させたAIモデルを使って、IMDBレビューの分類(高評価/低評価)の分類を行う。

レビュー学習プログラム: ライブラリインポート、各設定

(1)01-imdb_train.py: レビュー学習プログラム
についてコードを掲載し、解説していきます。

  1: import tensorflow as tf
  2: from tensorflow import keras
  3: from tensorflow.keras import layers
  4: import sys, os
  5: 
  6: # 出現上位単語数の設定
  7: vocab_size = 20000
  8: # 各レビューで学習の対象とする最初の単語数の上限
  9: maxlen = 200
 10: 
 11: # 各単語のEmbedding(埋め込み)ベクトル次元数
 12: embed_dim = 32
 13: # Multi-Head Attentionのヘッド数
 14: num_heads = 2
 15: # Feed Forward Networkのニューロン数
 16: ff_dim = 32

Transformerを構築するために必要なtensorflow等のライブラリのインポートを行ったあとレビュー文解析の設定を行っています。

imdbレビューに登場するすべての単語を扱うのではなく出現する数の多い上位20000件の単語を選び、残りは学習の対象にはしません。さらにレビュー1件の文の長さを制限200単語に制限しています。

Transformerにおいては、文章をそのままの形で扱うことはできないので計算可能なベクトルの形式に変換して扱います。(埋め込みベクトル)正確には、学習に使用するimdbレビューの単語は、文字ではなくスカラー数値として表現されておりそれをさらにベクトルに変換することを行います。ベクトルの次元数は32としています。

Transformerの肝となる、Attentionの設定ですがAttention層は並列化(マルチヘッド)することが可能でありMulti-Head Attentionの数をここで2として設定しています。

レビュー学習プログラム: main関数

 19: # main関数 
 20: def main():
 21: 
 22:     # モデル名取得。引数にモデル名がなければ、強制終了。
 23:     if not len(sys.argv)==2:
 24:         print('使用法: python 01-imdb_train.py モデルファイル名')
 25:         sys.exit()
 26:     savefile = sys.argv[1]
 27: 
 28:     # 学習済モデルがあれば、読み込み
 29:     if os.path.exists(savefile):
 30:         print('モデル再学習')
 31:         model = keras.models.load_model(savefile)
 32:     # 学習済モデルがなければ、新規作成
 33:     else:
 34:         print('モデル新規作成')
 35:         # モデル作成
 36:         model = transformer_model_maker()
 37:         # モデルの学習設定(コンパイル)
 38:         model.compile(
 39:         optimizer="adam", loss="binary_crossentropy", metrics=["accuracy"]
 40:         )
 41: 
 42:     # imdbレビュー学習用および検証用データをダウンロード
 43:     (x_train, y_train), (x_val, y_val) = keras.datasets.imdb.load_data(num_words=vocab_size)
 44:     print(len(x_train), "学習用imdbレビュー件数")
 45:     print(len(x_val), "検証用imdbレビュー件数")
 46: 
 47:     # 学習用および検証用データを整形。各レビューの文字数をmaxlenに合わせ。短いレビューは不足分を0で埋める
 48:     x_train = keras.preprocessing.sequence.pad_sequences(x_train, maxlen=maxlen)
 49:     x_val = keras.preprocessing.sequence.pad_sequences(x_val, maxlen=maxlen)
 50: 
 51:     # モデル学習 
 52:     history = model.fit(
 53:         x_train, y_train, batch_size=32, epochs=2, validation_data=(x_val, y_val)
 54:     )
 55: 
 56:     # モデル保存
 57:     model.save(savefile)

main関数を定義しています。順に説明しますと

python 01-imdb_train.py imdb_model

のように、プログラム実行時に引数として指定した学習モデルディレクトリ(imdb_model)がすでに存在すれば、それを読み込み、存在しなければ新規にモデルを作成する処理を実行します。後述する transformer_model_maker() 関数によりTransformerモデルの組み立てを行います。

次に、imdbのレビューをダウンロードします。

  (x_train, y_train), (x_val, y_val) =
  keras.datasets.imdb.load_data(num_words=vocab_size)

このkerasライブラリで定義されているメソッドを利用し、学習用および検証用データを(x_train, y_train), (x_val, y_val)として保存します。x_train,x_valには、レビュー文章(各単語は数値で表現)y_train,y_valには、それらの評価の値、すなわち高評価(1)、低評価(0)が格納されています。

レビュー文章の長さは各々バラバラなので以下のkerasのメソッドを用いて長さを統一します。

  x_train = keras.preprocessing.sequence.pad_sequences(x_train, maxlen=maxlen)

学習用および検証用データを整形。各レビューの文字数をmaxlenに合わせ。短いレビューは不足分を0で埋めるという処理です。その後にモデル学習処理を実施、学習が終了したらモデルを保存して終了になります。

main関数の実行は、ブログラムの終盤で命令されています。

152: # main関数実行
153: if __name__ == '__main__':
154:     main()

レビュー学習プログラム: Transformerモデル組み立て関数

 60: # Transformerモデル組み立て
 61: def transformer_model_maker():
 62: 
 63:     # 入力層の定義
 64:     inputs = layers.Input(shape=(maxlen,))
 65:     # Embedding層の初期化
 66:     embedding_layer = TokenAndPositionEmbedding(maxlen, vocab_size, embed_dim)
 67:     # Attention層の初期化
 68:     transformer_block = TransformerBlock(embed_dim, num_heads, ff_dim)
 69: 
 70:     # 入力層、Embedding層、Attenstion層を順に組み立て
 71:     x = embedding_layer(inputs)
 72:     x = transformer_block(x)
 73: 
 74:     # プーリング層(データ圧縮)、ドロップアウト層、全結合層を追加
 75:     x = layers.GlobalAveragePooling1D()(x)
 76:     x = layers.Dropout(0.1)(x)
 77:     x = layers.Dense(20, activation="relu")(x)
 78:     x = layers.Dropout(0.1)(x)
 79: 
 80:     # 出力層。シグモイド関数により0~1の値を出力し、二値分類を行う
 81:     outputs = layers.Dense(1, activation="sigmoid")(x)
 82: 
 83:     # 入力層と出力層を引数に指定してモデル作成
 84:     model = keras.Model(inputs=inputs, outputs=outputs)
 85: 
 86:     return model    

Transformerモデルの組み立てを行う関数です。以下のサイトを参考にしております。
https://keras.io/examples/nlp/text_classification_with_transformer/

(1)入力層
(2)Embedding層
(3)Attention層
(4)プーリング層
(5)ドロップアウト層
(6)全結合層
(7)出力層

の構成になっています。(2),(3)は後述する関数で定義しています。

(1)は、入力層ということで、レビュー文章(各単語は数値で表現)の系列が入ってきます。系列のベクトル長はmaxlen(200)です。
(2)は(1)の系列の各単語のスカラー値をベクトルに変換します。
(3)は、Transformerの目玉機能であるAttention層です。文章中の各単語のお互いの関連度を学習することで、文意を把握しているかのような認識能力を発揮します。

次に(4)(5)(6)と、ディープラーニングでよく使用される層が続いていきます。
このうち GlobalAveragePooling1D のプーリング層は文中単語のベクトルを平均する機能を持ちます。

例えば
I[1,0,0,0] like[0,1,0,0] movies[0,0,1,0]
これらの単語ベクトルの集合文があったとすると、次元ごとに足して平均し、一つのベクトルにまとめます。
[0.3333…, 0.3333…, 0.3333…, 0]

プーリング層の効果としては、単語の関係の情報は失われる代わりデータ圧縮することで後続処理の計算量を減らしたり全体的にみれば、同じ情報を示す系列を、同一のものとして認識させることができます。

CNNなどの画像認識AIにおいても多少のズレ、歪みに振り回されて、同じ画像を別々の画像として誤認してしまう問題があります。それを解決するために、このプーリング層が頻繁に使用されています。

最後の(7)出力層ですが、これはシグモイド関数による二値分類を行っています。0~1の値を出力しますが、これは高評価であることの確率と解釈されますが、0に近づくほど低評価を意味し、1に近づくほど高評価を意味するスコアとも読み取ることもできます。

レビュー学習プログラム: Embedding層定義

 89: # Embedding層定義
 90: class TokenAndPositionEmbedding(layers.Layer):
 91: 
 92:     # 初期化処理
 93:     def __init__(self, maxlen, vocab_size, embed_dim):
 94:         super().__init__()
 95:         # 単語(単語数=vocab_size)を埋め込みベクトル化(次元数=embed_dim)
 96:         self.token_emb = layers.Embedding(input_dim=vocab_size, output_dim=embed_dim)
 97:         # 単語の位置情報を埋め込みベクトル化(次元数=embed_dim)
 98:         self.pos_emb = layers.Embedding(input_dim=maxlen, output_dim=embed_dim)
 99: 
100:     # 呼び出し時の処理
101:     def call(self, x):
102:         # レビュー文の最大単語数
103:         maxlen = tf.shape(x)[-1]
104:         # 位置情報を[0,1,2...maxlen-1]までの整数列で表現
105:         positions = tf.range(start=0, limit=maxlen, delta=1)
106:         # 位置情報を埋め込みベクトル化
107:         positions = self.pos_emb(positions)
108:         # 単語を埋め込みベクトル化
109:         x = self.token_emb(x)
110:         # 単語と位置情報の埋め込みベクトルの和を取ることで、単語に位置情報を持たせる
111:         return x + positions

Embedding層の処理を定義している関数です。以下のサイトを参考にしております。https://keras.io/examples/nlp/text_classification_with_transformer/

 Kerasで定義されているlayers.Layerクラスを継承して、初期化処理(__init__)と呼び出し(実行)処理(call)を再定義しています。 

関数の初期化処理では、単語および位置情報のベクトル化するメソッドをKerasでデフォルト定義されているEmbeddingクラスを呼び出して定義します。 

関数を呼び出した時の処理では、初期処理で定義したメソッドを用いてレビュー文章を、位置情報を持ったベクトルの集合体に変換する処理を行っています。

例えば
I[1] like[2] movies[3] という文があれば、
I[1,0,0,0] like[0,1,0,0] movies[0,0,1,0] という()内のベクトル表現に変換されます。

さらに、自然言語処理では、言葉の順番を意識する必要があるため各単語の順番(位置情報)[1,2,3……]をベクトル表現したものを足し合わせます。これで、位置情報を加味した文章ベクトルができます。

【イメージ】
文章: I [1,0,0,0] like[0,1,0,0] movies[0,0,1,0]
+
位置番号: 1 [0.1, 0,0,0] 2[0, 0.1, 0,0,0] 3[0,0, 0.1, 0]

文章+位置番号: I [1.1, 0,0,0] like[0, 1.1, 0,0,0] movies[0,0, 1.1, 0]

レビュー学習プログラム: Transformerブロック定義

114: # Transformerブロック(Attention層、FFN層)を定義
115: class TransformerBlock(layers.Layer):
116: 
117:     # 初期化
118:     def __init__(self, embed_dim, num_heads, ff_dim, rate=0.1):
119:         super().__init__()
120:         # MultiHead Attention層を定義
121:         self.att = layers.MultiHeadAttention(num_heads=num_heads, key_dim=embed_dim)
122:         # FFN層を定義
123:         self.ffn = keras.Sequential(
124:             [
125:                 layers.Dense(ff_dim, activation="relu"),
126:                 layers.Dense(embed_dim),
127:             ]
128:         )
129:         # 正規化設定
130:         self.layernorm1 = layers.LayerNormalization(epsilon=1e-6)
131:         self.layernorm2 = layers.LayerNormalization(epsilon=1e-6)
132:         # ドロップアウト層を定義
133:         self.dropout1 = layers.Dropout(rate)
134:         self.dropout2 = layers.Dropout(rate)
135: 
136:     # 呼び出し時の処理
137:     def call(self, inputs, training):
138:         # Attention層の出力
139:         attn_output = self.att(inputs, inputs)
140:         # ドロップアウト層を通過
141:         attn_output = self.dropout1(attn_output, training=training)
142:         # Attention出力と入力を加算したものを正規化
143:         out1 = self.layernorm1(inputs + attn_output)
144:         # FFN層を通過
145:         ffn_output = self.ffn(out1)
146:         # ドロップアウト層を通過
147:         ffn_output = self.dropout2(ffn_output, training=training)
148:         # Attention層の出力、FFN層の出力を加算しさらに正規化 
149:         return self.layernorm2(out1 + ffn_output)

Transformerブロック(Attention層、FFN層)を定義する関数です。以下のサイトを参考にしております。
https://keras.io/examples/nlp/text_classification_with_transformer/

Kerasで定義されているlayers.Layerクラスを継承して、初期化処理(__init__)、呼び出し(実行)処理(call)を定義しています。

初期化処理では、Kerasでデフォルト定義されているMultiHeadAttention クラスのインスタンス生成(初期化処理)を行い、Attention層を定義します。その際に、並列するAttention層の数(ヘッド数)、入力するベクトルの次元数を定義します。

次に、後続するFFN(フィードフォワードニューラルネットワーク)層を定義します。全結合層を2層結合しています。

さらに過学習対策のための正規化(LayerNormalization)、ドロップアウト(Dropout)層を定義します。正規化はレイヤー正規化と呼ばれる手法で、ある時点でのレイヤーの各ニューロン出力に対して正規化を行います。


呼び出し(実行)時の処理では、入力文章のベクトルをAttention層に入力します。入力(inputs)が2つ引数に渡されていますが、これはSelf Attentionと言われる処理です。

ある単語が、自分自身の属する文章にある、どの単語と関連があるかを学習するというものです。MultiHeadAttentionには、Key, Valueというパラメータ名が定義されており、これらに、同じinputs(Queryとも呼ぶ)という入力系列を与えると、Self Attentionとなります。ちなみに、それぞれの違う入力系列を与えると、Source-Target Attentionといわれる処理になります。

Attentionでは、以下の図のように入力文章を行列表現したQ(Query)に対して、
同じく入力文章を表現したK(Key)とV(Value)を準備し、それらの行列積(内積)を計算します。これにより、入力された文章の各単語がそれぞれ、文章内のどの単語と関連しているかを把握することができます。



Attention is All you Need より引用


Self-Attentionであれば、Q=K=Vとなります。Source-Target Attentionであれば、Q≠ K=Vとなります。(厳密には、KはQおよびVの転置行列です。これにより行列積で、単語間の内積すなわち関連度が計算できます。)

QKの行列積を計算した後に、SoftMax関数をかけて、最大値1、最小値0の範囲で単語間の関連度を表現し、さらにVをかけて、各単語に関連する単語を取り出すことができます。

Attention層を通過後、ドロップアウト層に入ります。training=training(値はここではNone)という引数が設定されていますが、学習時にドロップアウト有効にし、テスト(推論)時には、ドロップアウトをせずに入力を通過させます。

ドロップアウト層を通過したAttentionの出力と、Attentionの入力を加算する処理が続きます。これは残差結合(残差接続・残差ブロック)と呼ばれる処理です。ResNetと呼ばれる画像認識モデルで採用されたもので、レイヤーの入力と出力を加算させて学習させることで、層を深くしても、学習が進みやすいという効果があるそうです。

AttentionのQKVで考えると、Attentionで最終的に出てきたVにQを足すことで、
「それぞれの単語」 + 「その単語が関連している別の単語」
を表現した行列を出力していると解釈することもできます。

「私  は  コーヒー  が  好き  だ」という文章があった場合
・「私」は「好き」に関連している(主語述語の関係)
・「コーヒー」は「好き」に関連している(動詞と目的語の関係)
という文の構造や文法を捉えることができるといえます。

こうすれば、英訳するときに逐次的に「I am coffee like」などという間違った訳を出さず
「I like cofee」
という元の文構造を正しく認識したうえで、対応する英文を出力することができます。これがAttentionを使うTransformerの強みです。


次に上記をレイヤー正規化したものを入力とするFFN層に入ります。FFN層はシンプルな全結合層でできています。こちらもドロップアウト層、残差結合、レイヤー正規化処理を経ていきます。

上記により、Transformerの出力が得られます。


以上、レビュー学習プログラムの解説でした。このプログラムを用いて、生成した学習モデルを利用するレビュー分類プログラムは、以下に解説を記載しています。

https://nine-num-98.blogspot.com/2023/04/imdb-classification-03.html

スポンサーリンク