word2vecが流行っており,可視化されたデータも公開されています.
http://projector.tensorflow.org/
例えば"town"の近傍を検索すると,以下のような結果となりました.
word | distance |
---|---|
village | 0.355 |
city | 0.366 |
towns | 0.411 |
province | 0.486 |
county | 0.488 |
near | 0.510 |
fort | 0.513 |
castle | 0.521 |
感覚的にも,近い意味の単語が抽出できているようです.これらはEmbedding Layerを駆使しているらしいのですが,自然言語の語彙では少し捉えにくいので,数字の加算と減算を機械学習で扱ってみました.
データの準備
学習の入力に93+84
のような5文字の式を,出力に演算の答え177
を指定しました.
+
や-
などの記号が中央に固定されてしまうと面白くないので,桁が少ない場合はあえてスペースで右寄せして5文字にすることにしました.例えば,32+14
,__1+7
のようになります.
実際のソースコードは以下の通りです.
import random import numpy as np import tensorflow as tf from sklearn.model_selection import train_test_split from matplotlib import pyplot as plt d = { " ": 0, "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()) sentense_len = 5 def str2v(text: str) -> list: rt = " " * (sentense_len - len(text)) + text return [d[u] for u in rt] rd = {v: k for k, v in d.items()} def v2str(input: list[int]) -> str: return "".join(rd[u] for u in input) ymax = 200 data = [] for j in range(0, 100): for i in range(j, 100): 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) x = np.array([u for u, _ in data]).astype("int16") 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)
x_train
の長さは13635個でした*1.
モデルとその結果
Embedding Layerは,可視化しやすいように,outputを2次元にしました.
Dense Layerの数を1層から3層まで変更して学習させたところ,1層ではepoch_lossが下がり止まりましたが,2層以上ではほぼ0になりました.
Dense Layer 1層
epoch_loss ~ 0.13
model = tf.keras.Sequential() model.add(tf.keras.Input(shape=(sentense_len,))) model.add( tf.keras.layers.Embedding( vocab_num, 2, input_length=sentense_len, name="embedding" ) ) model.add(tf.keras.layers.Flatten()) model.add(tf.keras.layers.Dense(units=1, activation="tanh")) model.compile(loss="mean_squared_error", optimizer="adam")
Layer (type) Output Shape Param # ================================================================= embedding (Embedding) (None, 5, 2) 26 flatten (Flatten) (None, 10) 0 dense (Dense) (None, 1) 11 ================================================================= Total params: 37 Trainable params: 37 Non-trainable params: 0
検証データで予測した結果
1+7 = 30 66-26 = 24 0+57 = 49 85+46 = 120 48-7 = 52 73-61 = 37 57+30 = 103 8+84 = 88
近い数字にはなっていますが,誤った答えを返しました.
Embedding Layerのweightをプロットした図
epoch_loss が下がり止まったので,このモデルは複雑さが不足しているようです.それでもEmbedding Layerのweightをプロットしたところ,数字が一直線に並び,記号 (+
, -
, ) は遠い場所に配置されていたので,ある程度は学習できたようです.
Dense Layer 2層
epoch_loss ~ 10-5
model = tf.keras.Sequential() model.add(tf.keras.Input(shape=(sentense_len,))) model.add( tf.keras.layers.Embedding( vocab_num, 2, input_length=sentense_len, name="embedding" ) ) model.add(tf.keras.layers.Flatten()) model.add(tf.keras.layers.Dense(units=16, activation="relu")) model.add(tf.keras.layers.Dense(units=1, activation="tanh")) model.compile(loss="mean_squared_error", optimizer="adam")
_________________________________________________________________ Layer (type) Output Shape Param # ================================================================= embedding (Embedding) (None, 5, 2) 26 flatten (Flatten) (None, 10) 0 dense (Dense) (None, 16) 176 dense_1 (Dense) (None, 1) 17 ================================================================= Total params: 219 Trainable params: 219 Non-trainable params: 0
検証データで予測した結果
24+19 = 42 53+2 = 54 37+5 = 41 30+90 = 122 43+95 = 138 72+70 = 141 0+22 = 21 58-20 = 38
かなりよい精度で計算できていますが,1ぐらいの誤差があります.
Embedding Layerのweightをプロットした図
Dense Layerを2層にしたところ,十分に学習できました.Embedding Layerのweightをプロットすると,数字が一直線上に並んでおり,記号は遠くに配置されていました.また,+
と-
が真逆の部分に配置されており,とても興味深いです.
Dense Layer 3層
epoch_loss ~ 10-5
model = tf.keras.Sequential() model.add(tf.keras.Input(shape=(sentense_len,))) model.add( tf.keras.layers.Embedding( vocab_num, 2, input_length=sentense_len, name="embedding" ) ) model.add(tf.keras.layers.Flatten()) model.add(tf.keras.layers.Dense(units=16, activation="relu")) model.add(tf.keras.layers.Dense(units=16, activation="relu")) model.add(tf.keras.layers.Dense(units=1, activation="tanh")) model.compile(loss="mean_squared_error", optimizer="adam")
_________________________________________________________________ Layer (type) Output Shape Param # ================================================================= embedding (Embedding) (None, 5, 2) 26 flatten (Flatten) (None, 10) 0 dense (Dense) (None, 16) 176 dense_1 (Dense) (None, 16) 272 dense_2 (Dense) (None, 1) 17 ================================================================= Total params: 491 Trainable params: 491 Non-trainable params: 0
検証データで予測した結果
31+54 = 85 63+20 = 83 65+86 = 151 57+66 = 123 86-59 = 27 52+38 = 91 24+4 = 28 66+17 = 84
ほぼ完璧な答えだと思います.
Embedding Layerのweightをプロットした図
先程と概ね同じ結果でした.
Embedding Layerのweightのアニメーション
まとめ
たった13635個の加算減算の結果を学習させるだけで,数字や記号の意味を2次元空間にプロットできたので,楽しかったです.
補足
学習のソースコード
log_dir = "logs/fit/3" tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir=log_dir, histogram_freq=1) model = build_model() model.summary() model.fit( x_train, y_train, batch_size=32, epochs=32, validation_data=(x_test, y_test), callbacks=[tensorboard_callback], ) py = model.predict(x_test) for input, output in list(zip(x_test, np.round(py * ymax)))[0:8]: print(f"{v2str(input)} = {int(output)}") # embedding weight weights = model.get_layer("embedding").get_weights()[0] for i, w in enumerate(weights): plt.plot(w[0], w[1], "ro") plt.text(w[0], w[1], rd[i]) plt.savefig("embedding-weights.png")
*1:計算式は,(100C2+100)×3÷2×0.9=13635