前回の記事では,データは固定長という制限がありました.可変長データを扱えるように,Embedding Layerを用いたRecurrent neural network (RNN)で学習してみます.
可変長データの扱い
RNNなので,可変長の入力を扱えますが,学習するためには1個のTensorにまとめなければなりません.そのため,データの長さは最大のものに合わせて,データがない部分はpaddingします.このために,keras.preprocessing.sequence.pad_sequences()
を使用します.padding="post"
を合わせて指定しておきます*1.
Embedding layerのmask_zero
を有効にすれば,0
はマスクされます.
データの準備
単なる数値と+
,-
を解釈できるよう,可変長の文字列7
,6+32
,54-16
を入力したら,それぞれ7
,38
,38
と出力するように学習させます.
vocab_num
に1を足しているのは,パディング用に0
を使用するため,語彙数が1個増えるからです.
教師データは24896個でした.
d = { # 0: padding "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9, "0": 10, "+": 11, "-": 12, } vocab_num = len(d.keys()) + 1 def str2v(text: str) -> list: return [d[u] for u in text] rd = {v: k for k, v in d.items()} def v2str(input: list[int]) -> str: return "".join(rd.get(u, "") for u in input) # data ymax = 256 data = [] for j in range(0, 128): data.append((str2v(f"{j}"), j)) for i in range(j, 128): data.append((str2v(f"{i}+{j}"), i + j)) data.append((str2v(f"{j}+{i}"), i + j)) data.append((str2v(f"{i}-{j}"), i - j)) random.shuffle(data) print(f"data length = {len(data)}") x = np.array( tf.keras.preprocessing.sequence.pad_sequences( [u for u, _ in data], padding="post", value=0 ) ) y = np.array([v for _, v in data]).astype("float32") / ymax x_train, x_test = train_test_split(x, test_size=0.1, shuffle=False) y_train, y_test = train_test_split(y, test_size=0.1, shuffle=False)
モデル
最初にSimpleRNNで挑戦しましたが,1層のSimpleRNNでは学習が不十分だったので,代わりにLSTMを使用しました.
def build_model(): model = tf.keras.Sequential() model.add( tf.keras.layers.Embedding( input_dim=vocab_num, output_dim=2, mask_zero=True, name="embedding" ) ) model.add( tf.keras.layers.LSTM( units=8, activation="tanh", recurrent_activation="sigmoid", ) ) model.add(tf.keras.layers.Dense(units=1, activation="tanh")) model.compile( loss="mean_squared_error", optimizer=tf.keras.optimizers.Adam(1e-2), ) return model
Embedding Layer 1層とLSTM 1層を含む単純なモデルです.
_________________________________________________________________ Layer (type) Output Shape Param # ================================================================= embedding (Embedding) (None, None, 2) 26 lstm (LSTM) (None, 8) 352 dense (Dense) (None, 1) 9 ================================================================= Total params: 387 Trainable params: 387 Non-trainable params: 0
RNNは可変長データを扱えるので,embedding_inputが(None, None)
になっています.なお,入出力のtensor shapeでNone
は可変長を意味します.
このRNNに学習させたところ,loss= 8.9867×10-6 ,val_loss= 2.4440×10-5となり,問題なく学習できました.
Embedding LayerのWeightsをプロットしたところ,以下のようになりました.
数字は順番に並んでおり,+
と-
が逆の意味であることを学習しているように思われます.
可変長データで予測する
さて,RNNは可変長データを扱えるはずですが,上記の学習においては,パディングしたデータを用いてるので,パディングを含めた固定長データを学習しているかもしれません.もちろんKerasのEmbedding layerのmask_zero=True
は正しく実装されているはずですが,念の為,検証します.
model = tf.keras.models.load_model("rnn-model") tx = [str2v("4"), str2v("4+12"), str2v("16-4"), str2v("94+54")] for input in tx: o = model(tf.expand_dims(input, axis=0), training=False) output = o[0][0] * ymax print("{} = {} (input={})".format(v2str(input), int(output), input))
model
に明示的に可変長データinput
を指定しました*2.なお,expand_dims()
を用いるのは,1個のデータにbatchの次元を追加するためです.output
は,その逆の操作o[0]
で取得します.
予測値は以下のようになり,可変長データを適切に扱えていることが分かりました.
4 = 4 (input=[4]) 4+12 = 15 (input=[4, 11, 1, 2]) 16-4 = 12 (input=[1, 6, 12, 4]) 94+54 = 146 (input=[9, 4, 11, 5, 4])
計算結果の誤差は2程度ありますが,概ね正しい値を予測できました.
まとめ
Embedding Layer 1層とLSTM 1層を含む単純なモデルではありましたが,数値と加算減算を解釈できるように学習することができましたので,楽しかったです.
*1:We recommend using "post" padding when working with RNN layers (in order to be able to use the CuDNN implementation of the layers). keras-io/understanding_masking_and_padding.ipynb at master · keras-team/keras-io · GitHubより