【翻訳技術】seq2seqを実装しながら理解してみた

【翻訳技術】seq2seqを実装しながら理解してみた

当ページのリンクには広告が含まれています。



✓目次



本記事の対象者

・ RNNとLSTMを理解している人

・ pythonの基本を理解している人。

・ Kerasでニューラルネットワークを作ったことがある人

RNNの基礎については以下の記事でまとめているので参照してください。

また、実行環境としてはJupyter Notebookになります。以下の記事に従って環境構築していただければと思います。

今回は、Kerasを使ってseq2seqを構築し、コサイン関数をサイン関数に変換(翻訳)してみたいと思います。

seq2seqとはなにか

seq2seqは、シーケンスを受け取り別のシーケンスに変換するモデルのことで、文章などの入力を圧縮するencoderと、出力を展開するdecoderからなります。

「私はペンを持っている」という文章を、「I have a pen」という英文に翻訳することが可能となります。

encoderとdecoderに、それぞれRNNの層が構築され、encoderとdecoderが接続させることによってseq2seqが作られます。

seq2seqの仕組みについては以下の記事が大変分かりやすいので、参考にしてください。こちらでまとめられている絵を見ながら、以降の記事を読んでいただくと、それぞれのコードがどの領域の実装なのかがイメージしやすくなると思います。
https://sinyblog.com/deaplearning/seq2seq-001/#i

少々複雑に聞こえるかもしれませんが、今回は簡単な例としてcos関数をsin関数に翻訳する形で、その構造を理解したいと思います。

訓練用のデータを作成する。

cos関数の値をencoderへの入力、sin関数の値をdecoderへの入力とします。

なお、正解は、sin関数になります。

decoderへの入力というのは正解から1つ後の時刻にずらした値になります。これは、ある時刻におけるdecoderの出力が、次の時刻における入力に近づくように学習をさせるためです。

このように、ある時刻における正解が次の時刻の入力となる手法を教師強制と言います。

まずは、cos関数とsin関数を作成してプロットしてみます。

1
2
3
4
5
6
7
8
9
10
11
12
%matplotlib inline

import numpy as np
import matplotlib.pyplot as plt

x_ax = np.linspace(-2*np.pi, 2*np.pi) # -2πから2πまでの50つの値
cos_data = np.cos(x_ax)
sin_data = np.sin(x_ax)

plt.plot(x_ax, cos_data)
plt.plot(x_ax, sin_data)
plt.show()

cos関数とsin関数を描画

次に、encoderへの入力、decoderへの入力、decoderの正解を作成します。

今回、時系列の数は10に設定しておきます。

zerosメソッドでエンコーダー、デコーダー、正解の形状を作成します。

今回はすべて同じ形状にする。

1
2
3
4
5
6
7
8
9
10
11

n_rnn = 10 # 時系列の数
n_sample = len(x_ax) - n_rnn # サンプル数
x_encoder = np.zeros((n_sample, n_rnn)) # encoderの入力
x_decoder = np.zeros((n_sample, n_rnn)) # decoderの入力
t_decoder = np.zeros((n_sample, n_rnn)) # decoderの正解

print(x_encoder.shape)
print(x_decoder.shape)
print(t_decoder.shape)
# 全部(40, 10)

x_encoderには、コサイン関数の値を入れていき、x_decoderにはサイン関数の値を入れていきます。

この時、x_decoderに入れるサイン関数は、1つ後の時刻にずらす必要があります。時系列で1以降の値に代入するので最初の値は0のままということになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

for i in range(0, n_sample):
x_encoder[i] = cos_data[i:i+n_rnn]
x_decoder[i, 1:] = sin_data[i:i+n_rnn-1]

x_decoder
# array([[ 0.00000000e+00, 2.44929360e-16, 2.53654584e-01,
# 4.90717552e-01, 6.95682551e-01, 8.55142763e-01,
# 9.58667853e-01, 9.99486216e-01, 9.74927912e-01,
# 8.86599306e-01],
# [ 0.00000000e+00, 2.53654584e-01, 4.90717552e-01,
# 6.95682551e-01, 8.55142763e-01, 9.58667853e-01,
# 9.99486216e-01, 9.74927912e-01, 8.86599306e-01,
# 7.40277997e-01],
# [ 0.00000000e+00, 4.90717552e-01, 6.95682551e-01,
# 8.55142763e-01, 9.58667853e-01, 9.99486216e-01,
# 9.74927912e-01, 8.86599306e-01, 7.40277997e-01,
# 5.45534901e-01],

続いて正解データであるt_decoderを作成します。

t_decoderには、サイン関数の値をそのまま入れます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

for j in range(0, n_sample):
t_decoder[j] = sin_data[j:j+n_rnn]

t_decoder
# array([[ 2.44929360e-16, 2.53654584e-01, 4.90717552e-01,
# 6.95682551e-01, 8.55142763e-01, 9.58667853e-01,
# 9.99486216e-01, 9.74927912e-01, 8.86599306e-01,
# 7.40277997e-01],
# [ 2.53654584e-01, 4.90717552e-01, 6.95682551e-01,
# 8.55142763e-01, 9.58667853e-01, 9.99486216e-01,
# 9.74927912e-01, 8.86599306e-01, 7.40277997e-01,
# 5.45534901e-01],
# [ 4.90717552e-01, 6.95682551e-01, 8.55142763e-01,
# 9.58667853e-01, 9.99486216e-01, 9.74927912e-01,
# 8.86599306e-01, 7.40277997e-01, 5.45534901e-01,
# 3.15108218e-01],

KerasにおけるRNNの入力の形状にするために、x_encoder, x_decoder, t_decoderの形状を、サンプル数、時系列の数、入力層のニューロン数という形状に変更しておきます。

1
2
3
x_encoder = x_encoder.reshape(n_sample, n_rnn, 1)  # (サンプル数、時系列の数、入力層のニューロン数)
x_decoder = x_decoder.reshape(n_sample, n_rnn, 1)
t_decoder = t_decoder.reshape(n_sample, n_rnn, 1)

これで、データの準備はOKです。

seq2seqの構築

Kerasを使ってseq2seqを構築していきます。

seq2seqの特徴は、学習用のモデル構築と予測用のモデル構築の2つのモデル構築を別々で行う点にあります。

学習用のモデルと予測用のモデルの中に、それぞれencoderとdecoderが存在します。

この特徴を踏まえ、以下のような流れで作成を進めていきます。

1. 学習用モデル構築(ライブラリのインポートとモデルの設定)

2. 学習用モデル構築(encoderの構築)

3. 学習用モデル構築(decoderの構築)

4. 学習用モデル構築(モデルのコンパイル)

5. 学習用モデル構築(構築した学習用モデルを用いて学習を実施)

6. 学習用モデル構築(学習の推移を確認する。)

7. 予測用モデル構築(encoderのモデルを構築)

8. 予測用モデルの構築(decoderのモデルを構築)

9. 翻訳用の関数を定義

それでは一つ一つ順を追ってseq2seqを構築していきたいと思います。

1. 学習用モデル構築(ライブラリのインポートとモデルの設定)

これまで、RNNやLSTMを構築した際は、Sequentialクラスを使用してましたが、今回は、Modelクラスを使います。

Modelクラスを用いることで、複数の経路の入力を持つニューラルネットワークを構築することが可能だからです。

また状態を渡すことでRNN同士を接続することもできます。

今回のseq2seqのRNN部分にはLSTMを使おうと思います。

各種ライブラリをインポートしていますが、今回はInputというのも導入します。

Inputは、入力層を表すライブラリです。

モデルの設定をしていきます。入力層のニューロン数は1、中間層のニューロン数は20、出力層のニューロン数は入力層のニューロン数と同じにします。

1
2
3
4
5
6
from keras.models import Model
from keras.layers import Dense, LSTM, Input

n_in = 1 # 入力層のニューロン数
n_mid = 20 # 中間層のニューロン数
n_out = n_in # 出力層のニューロン数

こちらのコードで各種ライブラリのインポートと、モデルの設定は完了です。

2. 学習用モデル構築(encoderの構築)

最初にInputを使ってencoderの入力層を設定します。

Inputを使用する際は、入力の形状を設定する必要があります。時系列数, 入力層のニューロン数を指定しますので、n_rnn, n_inとします。

1
encoder_input = Input(shape=(n_rnn, n_in))

続いてencoderにLSTMを設定します。中間層のニューロン数を設定し、return_stateをTrueに設定します。

return_stateをTrueに設定すると、その時刻における出力と共に状態を得ることができます。

LSTMにおける出力\(h_{t}\)と状態であるメモリセルを得ることができるということです。

1
encoder_lstm = LSTM(n_mid, return_state=True)

そして、このencoderのLSTMに、先ほどのencorderの入力を渡します。その結果得られるのは、encoderの出力とencoderの状態\(h\)とencoderの状態\(c\)になります。

LSTMの内部に\(h\)と\(c\)という2つの状態を持っている点については、以下の記事で詳しくまとめらていますので、詳細が知りたい方は参考にしてください。
https://qiita.com/KojiOhki/items/89cd7b69a8a6239d67ca

1
encoder_output, encoder_state_h, encoder_state_c = encoder_lstm(encoder_input)

この2つの状態\(t\)と\(c\)は、リストでまとめておき、encoder_stateとしておきます。

1
encoder_state = [encoder_state_h, encoder_state_c]

これでencoderは完成です。

3. 学習用モデル構築(decoderの構築)

まずInputを用いて、decoderの入力層を作ります。

1
decoder_input = Input(shape=(n_rnn, n_in))

encoderの時と同様、LSTMの層を作ります。こちらも先ほど同様に、return_stateをTrueにしておきますが、return_sequencesもTrueにしておきます。

このようにすることで、すべての時系列の出力を出力として得ることができます。

1
decoder_lstm = LSTM(n_mid, return_sequences=True, return_state=True)

decoderのLSTMには、先ほど作成したdecoder_inputを入れます。その際に初期状態であるinitial_stateに先ほどのencoder_stateに設定します。

その結果返ってくるのが、decoderの出力と2つの状態です。

この段階で、decoderから出力された状態は使わないので、とりあえず_をいれておきます。

1
decoder_output, _, _ = decoder_lstm(decoder_input, initial_state=encoder_state )

LSTM層の次に、全結合層であるDenseを入れます。

Denseには出力層のニューロン数を設定します。活性化関数は、ひとまずlinearとしておきます。linearは恒等関数を意味します。

1
decoder_dense = Dense(n_out, activation='linear')

そして、このdecoder_densedecoder_outputを入れることでdecoderの出力を得ることができます。

1
decoder_output = decoder_dense(decoder_output)

decoderの構築はこれで完了です。

4. 学習用モデル構築(モデルのコンパイル)

まずはseq2seqにおける学習用のモデルを構築していきます。

Modelクラスを用いてモデル構築を行います。

Modelクラスは、全体の入力と出力のみ設定すればOK、という優れものです。

入力が複数存在する場合は、リストを使って設定すればOKです。

入力は、encoder_inputdecoder_inputの2つになります。

そして全体の出力は、decoder_outputになるので、コードとしては以下となります。

1
model = Model([encoder_input, decoder_input], decoder_output)

次にモデルのコンパイルを実施します。

コンパイルの際は、損失関数とと最適化アルゴリズムを指定する必要があります。

損失関数は回帰の場合は、二乗誤差で、分類の場合はクロスエントロピーが提供されるのが一般的です。

今回は、encoderの入力から、学習した結果に基づいて翻訳結果を予測するので、二乗誤差を適用します。

最適化アルゴリズムは、収束しやすいadamでもいいですが、sgdを適用してみたいと思います

これでモデル構築は完了なので、print(model.summary())で、構築したモデルの概要を確認したいと思います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
model.compile(loss="mean_squared_error", optimizer="sgd")
print(model.summary())
# Model: "model_11"
# __________________________________________________________________________________________________
# Layer (type) Output Shape Param # Connected to
# ==================================================================================================
# input_18 (InputLayer) (None, 10, 1) 0
# __________________________________________________________________________________________________
# input_19 (InputLayer) (None, 10, 1) 0
# __________________________________________________________________________________________________
# lstm_9 (LSTM) [(None, 20), (None, 1760 input_18[0][0]
# __________________________________________________________________________________________________
# lstm_10 (LSTM) [(None, 10, 20), (No 1760 input_19[0][0]
# lstm_9[0][1]
# lstm_9[0][2]
# __________________________________________________________________________________________________
# dense_5 (Dense) (None, 10, 1) 21 lstm_10[0][0]
# ==================================================================================================
# Total params: 3,541
# Trainable params: 3,541
# Non-trainable params: 0
# __________________________________________________________________________________________________
# None

encoderの入力とdecoderの入力があります。

また、encoderのLSTMとdecoderのLSTMがあります。decoderの方は、全結合層であるDenseを1つ所持していることが確認できます。

5. 学習用モデル構築(構築した学習用モデルを用いて学習を実施)

RNNの時と同様にfitメソッドを使用して学習を行います。

入力は、x_encoderx_decoderとし、正解は、t_decoderを指定します。

バッチサイズを10とし、エポック数を30に設定します。

1
2
3
history = model.fit([x_encoder, x_decoder], t_decoder,
batch_size=8,
epochs=30)

6. 学習用モデル構築(学習の推移を確認する。)

以下のコードでグラフで誤差の収束具合を確認します。

1
2
3
loss = history.history['loss']
plt.plot(np.arange(len(loss)), loss)
plt.show()

学習の推移を示すグラフ

んー、収束しきってない気がします。

7. 予測用モデル構築(encoderのモデルを構築)

seq2seqでは、訓練用のモデルと、予測用のモデルを別々に構築する必要があります。

予測用モデルは、学習済みのオブジェクトから、encoder, decoderのモデルを構築します。

encoderは入力を受け取り状態を返し、decoderは入力と状態を受け取って出力と状態を返すようにします。

まずはModelクラスを用いてencoderのモデルを構築します。

入力としてencoder_input、出力としてencoder_stateを設定します。

encoder_inputとencoder_stateの間には、encoder_lstmが存在する構造をとっています。このencoder_lstmは先ほどの学習済みのLSTMになります。

これだけで、予測用モデルにおけるencoderは完成します。

1
encoder_model = Model(encoder_input, encoder_state)

8. 予測用モデルの構築(decoderのモデルを構築)

続いて予測用モデルのdecoderを構築します。

入力層は、新規にInputを使って作成します。形状は、時系列の長さ、入力層のニューロン数を設定します。

今回は、時系列の長さは1で入力層のニューロン数はn_inで設定しているので、以下のようになります。なお、時系列の長さが可変である場合は、Noneを設定します。

1
decoder_input = Input(shape=(1, n_in))

LSTMには内部に2つの状態を持つので、状態の入力を2つ作ります。

この2つの状態は、decoder_state_in_hdecoder_state_in_cとします。

それぞれの状態の数は、中間層のニューロンの数と同じになります。

これらは、1つのリストにまとめて、decoder_state_inとしておきます。

1
2
3
decoder_state_in_h = Input(shape=(n_mid,))
decoder_state_in_c = Input(shape=(n_mid,))
decoder_state_in = [decoder_state_in_h, decoder_state_in_c]

次に先ほどのdecoder_lstmdecoder_inputdecoder_state_inを設定します。decoder_state_inは、初期状態として設定するので、initial_stateとしておきます。

decoder_lstmも同様、既存の学習済みのLSTMになります。

1
decoder_output, decoder_state_h, decoder_state_c = decoder_lstm(decoder_input, initial_state=decoder_state_in)

得られたdecoder_state_hdecoder_state_cは、以下のようにリストにし、そのうえで、decoder_densedecoder_outputを入れて、decoderの出力を得ることができます。こちらも、すでに学習済みのDenseになります。

1
2
decoder_state = [decoder_state_h, decoder_state_c]
decoder_output = decoder_dense(decoder_output)

いよいよ、decoderのモデルを構築します。

モデル化の際に、入力として渡すのは、decoder_inputdecoder_state_inです。

出力は、decoder_outputdecoder_stateになります。

それぞれ、リストとして値を保持しており、入力として渡す際は、各リストを結合して渡すので、+でリストを結合しています。

1
decoder_model = Model([decoder_input] + decoder_state_in, [decoder_output] + decoder_state)

9. 翻訳用の関数を定義

コサイン関数を翻訳して、サイン関数に変換するための関数を定義します。

encoderへの入力値であるinput_dataを渡し、予測用モデルに適用した翻訳結果を返す処理を定義します。

1
2
3
4
5
6
7
8
9
10
11
12
13
def translate(input_data):
translated = []
state_value = encoder_model.predict(input_data) # 予測モデルのencoderにinput_dataをわたし、学習済みのLSTMで予測。出力は、内部状態h, cの2つが出力される
y_decoder = np.zeros((1, 1, 1))

for i in range(0, n_rnn):
y, h, c = decoder_model.predict([y_decoder] + state_value) # 1つ前の時刻の出力(最初は空)と、状態(内部状態hとc)をdecoder_modelの学習済みLSTMとDenceで予測、
y = y[0][0][0] # 出力yから翻訳された結果を抽出
translated.append(y)
y_decoder[0][0][0] = y # 次の時刻に渡す値
state_value = [h, c] # 次の時刻に渡す状態

return translated

コサイン関数をサイン関数に翻訳

1
2
3
4
5
6
7
8
9
idices = range(0, 40)
for indice in idices:
x = x_encoder[indice:indice+1] # 入力を一部取り出す
y = translate(x)

plt.plot(x_ax[indice:indice+n_rnn], x.reshape(-1), color="b") # 翻訳前を青い線
plt.plot(x_ax[indice:indice+n_rnn], y, color="r") # 翻訳後は赤い線

plt.show()

翻訳結果(epoc数30)

翻訳の結果としては少々いまいちでした。

epoc数を30ではなく50に増やして再度翻訳してみます。

翻訳結果(epoc数50)

まだ微妙ですね。思い切ってepoc数を100に増やして再度実施してみます。

epoc数100の時の誤差の収束具合)

収束しきっているようなしていないような微妙な感じです。翻訳までさせてみると以下のような感じです。

翻訳結果(epoc数100)

どうやらこの三次元的なコサインカーブはどうにもならないみたいですね。

こんな時は、最適化アルゴリズムをsgdではなく別のものを適用すると改善される可能性が高いです。もしくは、通常のニューラルネットワークとかですと、中間層のニューロン数を増やしたり、層を増やしたりするのもある程度完全が見込めるかもしれません。
最終手段としてはデータの中心化(バッチノーマリゼーション)と言われる処理をするとよくなるパタンが多いそうです。

今回は、KerasのDocumentationにて記載のある最適化アルゴリズムの中で、Adagradという最適化アルゴリズムを適用してみたいと思います。

以下のように、Adagradという風にするだけです。エポック数は100のままにしています。

1
model.compile(loss="mean_squared_error", optimizer="Adagrad")

誤差の収束具合は以下の通りです。(さっきと全然違う)

adagrad適用時の誤差の収束(epoc数100)

肝心の翻訳結果は以下の通りです。すごい、ちゃんとコサインカーブになりました。

adagrad適用時の翻訳結果(epoc数100)

ここまでの知識を活用して、実際に日本語や英語を活用した翻訳にもチャレンジしたい方は、以下のUdemy(オンライン学習プラットフォーム)の講座がおすすめです。視聴期限無しで一流講師への質問も可能です。また講座料金については、30日以内では返金可能ですのでお気軽に試していただければと思います。


>> 自然言語処理とチャットボット: AIによる文章生成と会話エンジン開発

コメント