みーのぺーじ

みーが趣味でやっているPCやソフトウェアについて.Python, Javascript, Processing, Unityなど.

加算と減算をEmbeddingで機械学習する

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