みーのぺーじ

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

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

前回の記事では,データは固定長という制限がありました.可変長データを扱えるように,Embedding Layerを用いたRecurrent neural network (RNN)で学習してみます.

可変長データの扱い

RNNなので,可変長の入力を扱えますが,学習するためには1個のTensorにまとめなければなりません.そのため,データの長さは最大のものに合わせて,データがない部分はpaddingします.このために,keras.preprocessing.sequence.pad_sequences()を使用します.padding="post"を合わせて指定しておきます*1

Timeseries data preprocessing

Embedding layerのmask_zeroを有効にすれば,0はマスクされます.

Embedding layer

データの準備

単なる数値と+-を解釈できるよう,可変長の文字列76+3254-16を入力したら,それぞれ73838と出力するように学習させます.

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より

*2:Model training APIs