2017年2月25日土曜日

LSTMで文字列を単語(番号)にマッピング

LSTMを無意味に使ってみました。

目的は、文字列を入力したら単語が出力されるニューラルネットワーク(NN)を作ることです。
単なるマッピングなので、連想配列を使えば解決ですが、無理やりNNで実現してみます。

入力層は、文字のone-hotベクトルとします。
同じく、出力層は単語のone-hotベクトルとします。

学習データはどこからともなく見つけてきた英単語300個。

入力と出力の長さが異なっているので、そろえるためにブランク記号 _ を使います。

例えば、n e u r o n を入力すると、_ _ _ _ _ neuron が出力されるように学習します。
単語ごとに文字列長が異なるので、足りない分は入力側にも _ を使います。

単語リストは

business
company
...
neuron
というふうに、単に1行に1単語が書かれたファイルです。

このファイルを次のスクリプトの第1引数に指定してスクリプトを実行します。
学習用ライブラリにはTensorFlowベースのKerasを利用しました。

ブランク記号 _ には数値で0を、ASCII文字には1から順に整数を割り当てています。
単語についても同じです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import numpy as np
import sys,os
import numpy.random as random
from keras.models import Sequential
from keras.layers import Dense, Activation, LSTM, TimeDistributedDense
from keras.optimizers import RMSprop
import keras.preprocessing.text as pp

if len(sys.argv) != 2:
    print("Usage: seq2seq.py [word list]")
    exit(0)

words_file = sys.argv[1]
if not os.path.isfile(words_file):
    print(words_file+" is not found.")
    exit(0) 

random.seed(123)

# 単語データの読み込み
words = []
max_ch_seq = 0
for line in open(words_file):
    line = line.rstrip('\n')
    chars = [ord(c)-ord('a')+1 for c in list(line)]
    col = np.array(chars)
    max_ch_seq = max(max_ch_seq, len(col))
    words.append([col, line])

# 文字長を揃える
max_ch_seq_orig = max_ch_seq
for w in words:
    while len(w[0]) < max_ch_seq:
        w[0] = np.append(w[0], 0) # 0はブランク

word_id_map = {}
id = 1
for w in words:
    word_id_map[w[1]] = id # 重複がないことが前提
    id += 1

input_vec_len = ord('z') - ord('a') + 2 # +1はブランク入力のため
output_vec_len = len(words) + 1 # +1はブランク出力のため
np.set_printoptions(threshold=np.inf)
for w in words:
    # 入力(=文字)のone-hotベクトルを作成する
    input_vec_seq = np.zeros((len(w[0]), input_vec_len))
    input_vec_seq[np.arange(len(w[0])), w[0]] = 1
    w.append(input_vec_seq)

    # 出力(=単語)のone-hotベクトルを作成する
    x = np.zeros(len(w[0]), dtype=int)
    x[-1] = word_id_map[w[1]] #最後に単語を入れる
    output_vec_seq = np.zeros((len(w[0]),output_vec_len))
    output_vec_seq[np.arange(len(w[0])), x] = 1
    w.append(output_vec_seq)

# この時点で、wordsの各要素は、
#   (文字列(数字), 単語, 文字列のOne-hotベクトル, 単語のOne-hotベクトル)
# となっている。
data_dup = 1  # 何度も同じデータを入れるなら、2以上にする

# 訓練用データを設定する
x_train = np.empty((len(words)*data_dup, max_ch_seq, input_vec_len))
y_train = np.empty((len(words)*data_dup, max_ch_seq, output_vec_len))
for j in range(0, data_dup):
    for i in range(0, len(words)):
        x_train[i+len(words)*j] = words[i][2]
        y_train[i+len(words)*j] = words[i][3]

# 評価用データを設定する
x_val = np.empty((len(words), max_ch_seq, input_vec_len))
y_val = np.empty((len(words), max_ch_seq, output_vec_len))
for i in range(0, len(words)):
    x_val[i] = words[i][2]
    y_val[i] = words[i][3]

# モデルを作る
batch_size = 50
model = Sequential()
model.add(LSTM(16, return_sequences=True,
               batch_input_shape=(batch_size, max_ch_seq, input_vec_len)))
model.add(LSTM(16, return_sequences=True))
model.add(TimeDistributedDense(128))
model.add(TimeDistributedDense(output_vec_len, activation='softmax'))

# 学習する
rmsprop = RMSprop(lr=0.0005, rho=0.9, epsilon=1e-08)
model.compile(loss='categorical_crossentropy', optimizer=rmsprop, metrics=['accuracy'])
model.fit(x_train, y_train, batch_size=batch_size, nb_epoch=5000, validation_data=(x_val, y_val))

# モデルを保存
model.save_weights('model.dat')

# 結果の確認
r = model.predict_classes(batch_size=batch_size, x=x_val)
print(r)
78行目まではデータの読み込みと整形部分です。80行目からがモデルを作ったり学習したりする部分です。 とても短いですね

さて、実行すると

Train on 300 samples, validate on 300 samples
Epoch 1/5000
300/300 [==============================] - 1s - loss: 5.6607 - acc: 0.4613 - val_loss: 5.5990 - val_acc: 0.8738
Epoch 2/5000
300/300 [==============================] - 1s - loss: 5.5510 - acc: 0.9046 - val_loss: 5.4750 - val_acc: 0.9215
Epoch 3/5000
300/300 [==============================] - 1s - loss: 5.4081 - acc: 0.9231 - val_loss: 5.3032 - val_acc: 0.9231
Epoch 4/5000
300/300 [==============================] - 1s - loss: 5.2126 - acc: 0.9231 - val_loss: 5.0716 - val_acc: 0.9231
Epoch 5/5000
300/300 [==============================] - 1s - loss: 4.9520 - acc: 0.9231 - val_loss: 4.7682 - val_acc: 0.9231

...

Epoch 1009/5000
300/300 [==============================] - 0s - loss: 0.2684 - acc: 0.9367 - val_loss: 0.3089 - val_acc: 0.9241
Epoch 1010/5000
300/300 [==============================] - 1s - loss: 0.2687 - acc: 0.9297 - val_loss: 0.2368 - val_acc: 0.9646
Epoch 1011/5000
300/300 [==============================] - 1s - loss: 0.2416 - acc: 0.9479 - val_loss: 0.2385 - val_acc: 0.9559

...

Epoch 1499/5000
300/300 [==============================] - 1s - loss: 0.2082 - acc: 0.9518 - val_loss: 0.2559 - val_acc: 0.9285
Epoch 1500/5000
300/300 [==============================] - 1s - loss: 0.1796 - acc: 0.9649 - val_loss: 0.1584 - val_acc: 0.9869
Epoch 1501/5000
300/300 [==============================] - 1s - loss: 0.1629 - acc: 0.9759 - val_loss: 0.1578 - val_acc: 0.9874

...

Epoch 1999/5000
300/300 [==============================] - 1s - loss: 0.0943 - acc: 0.9967 - val_loss: 0.0892 - val_acc: 0.9990
Epoch 2000/5000
300/300 [==============================] - 1s - loss: 0.0941 - acc: 0.9967 - val_loss: 0.0890 - val_acc: 0.9987
Epoch 2001/5000
300/300 [==============================] - 1s - loss: 0.1502 - acc: 0.9644 - val_loss: 0.1626 - val_acc: 0.9487

...

Epoch 2999/5000
300/300 [==============================] - 1s - loss: 0.0155 - acc: 0.9995 - val_loss: 0.0123 - val_acc: 1.0000
Epoch 3000/5000
300/300 [==============================] - 1s - loss: 0.0134 - acc: 1.0000 - val_loss: 0.0121 - val_acc: 1.0000
Epoch 3001/5000
300/300 [==============================] - 1s - loss: 0.0134 - acc: 1.0000 - val_loss: 0.0120 - val_acc: 1.0000

...

Epoch 4998/5000
300/300 [==============================] - 1s - loss: 0.0800 - acc: 0.9813 - val_loss: 6.6157e-04 - val_acc: 1.0000
Epoch 4999/5000
300/300 [==============================] - 1s - loss: 6.1272e-04 - acc: 1.0000 - val_loss: 5.0139e-04 - val_acc: 1.0000
Epoch 5000/5000
300/300 [==============================] - 1s - loss: 4.9502e-04 - acc: 1.0000 - val_loss: 4.4888e-04 - val_acc: 1.0000
というようなログが出力されます。汎化性能は無視なので、ひたすらlossを小さくしました。やりすぎですね。

99行目のprint文の出力は

[[  0   0   0   0   0   0   0   0   0   0   0   0   1]
 [  0   0   0   0   0   0   0   0   0   0   0   0   2]
 [  0   0   0   0   0   0   0   0   0   0   0   0   3]
 [  0   0   0   0   0   0   0   0   0   0   0   0   4]
 [  0   0   0   0   0   0   0   0   0   0   0   0   5]
 [  0   0   0   0   0   0   0   0   0   0   0   0   6]
 [  0   0   0   0   0   0   0   0   0   0   0   0   7]
 [  0   0   0   0   0   0   0   0   0   0   0   0   8]
 [  0   0   0   0   0   0   0   0   0   0   0   0   9]
 [  0   0   0   0   0   0   0   0   0   0   0   0  10]

...

 [  0   0   0   0   0   0   0   0   0   0   0   0 298]
 [  0   0   0   0   0   0   0   0   0   0   0   0 299]
 [  0   0   0   0   0   0   0   0   0   0   0   0 300]]
となります。きちんと、途中は全てブランク(=0)が出ていて、
単語の番号が最後に正しく出力されているのが分かります。

というわけで、LSTMは結構過去のことを覚えておいてくれることが分かりました。
そして、とても重たい連想配列ができました。

0 件のコメント :