Contents
- 1 TimeDistributedがわからん
- 2 Kerasモデルへの入力の形状
- 3 predictメソッドにわたすデータの形状
- 4 input_shapeに与える形状にはサンプル次元を加えない
- 5 出力が複数ベクトルからなる場合の交差エントロピー誤差
- 6 Kerasでの学習済みモデルの保存と読み込み
- 7 オプティマイザはrmsprop一択
- 8 情報量を徐々に絞っていく層の構成がよさげ
- 9 plot_model
- 10 LSTMの状態について
- 11 Masking層のあとのリカレント層でreturn_sequences=Trueにする場合はプレパディングがいい
- 12 Masking層
- 13 Embedding層のmask_zeroオプション
TimeDistributedがわからん
レイヤーラッパー – Keras Documentationの説明を読んだ。各タイムステップごとに層を適用するにはTimeDistributedをかませないとだめということか。では、まずTimeDistributedをかまさないとどうなるか試してみよう。たぶんなんかエラーになるんだろう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
from keras import models, layers import numpy as np model = models.Sequential() model.add(layers.Dense(3, use_bias=False, weights=[np.ones((2, 3))], trainable=False)) X = np.array([ [[1,2], [3,4]], [[5,6], [7,8]] ]) pred = model.predict(X) print(pred) #[[[ 3. 3. 3.] # [ 7. 7. 7.]] # # [[11. 11. 11.] # [15. 15. 15.]]] |
あれ?エラーでないし、TimeDistributedかまさなくても各タイプステップごとにDense層を適用できてしまった。TimeDistributedいらねーじゃん。
ではTimeDistributedをかましても同じ結果になるのか?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
model = models.Sequential() model.add(layers.wrappers.TimeDistributed(layers.Dense(3, use_bias=False, weights=[np.ones((2, 3))], trainable=False), input_shape=(2, 2))) X = np.array([ [[1,2], [3,4]], [[5,6], [7,8]] ]) pred = model.predict(X) print(pred) #[[[ -1.4608536 0.8944001 -2.6980581] # [ -2.843469 2.2316668 -6.3874865]] # # [[ -4.2260838 3.5689335 -10.076916 ] # [ -5.6087 4.9062004 -13.766344 ]]] |
なんじゃこの結果は????どう解釈したらいいんだ。
ちなみに「詳解ディープラーニング」のp.275の足し算プログラムは、TimeDistributedを入れていても入れなくてもうまく学習できた。結果的にはどちらでもいいんだが、うーん。
参考:Keras Recurrentレイヤーメモ:return_sequences, RepeatVector, TimeDistributed
ちなみに、TimeDistributedを用いず、単にModel.add(Dense)をしてしまうとどうなるのでしょうか…?
パッと思いつきで、「TimeDistributedを用いないと全ての時刻にわたってFull Conncetionしてしまうのでは?」と思っていましたが、特に出力の次元が変わることもなかったので、そうではないようです。今のところ、まだ違いがわかっていないので目下調査中です。
そうなんだよ。次元は変わらない。でも中身は異なる。しかし学習はどちらでもうまくいく。???
Kerasモデルへの入力の形状
Kerasモデルへの入力は基本的にテンソル(ベクトル)の配列。たとえば以下のような入力を2次元ベクトルへ変換するだけのモデルを考える。
1 2 3 4 5 |
from keras import models, layers import numpy as np model = models.Sequential() model.add(layers.Dense(2)) |
このモデルにテンソルではなくスカラーの配列を与えるとエラー。
1 2 3 4 |
x = np.array([1,2,3]) # [1 2 3] pred = model.predict(x) # ValueError: Input 0 is incompatible with layer dense_1: expected min_ndim=2, found ndim=1 |
入力の次元は2以上(つまりテンソル)が期待されるが、1次元(スカラー)が入力されましたーといわれる。
スカラーの配列は受け付けないが、テンソルの配列にすると飲み込んでくれる。
1 2 3 4 5 6 7 8 |
x = np.array([1,2,3]).reshape(-1, 1) # [[1] # [2] # [3]] pred = model.predict(x) # [[-0.19495952 -1.3617309 ] # [-0.38991904 -2.7234619 ] # [-0.58487856 -4.0851927 ]] |
ただし例外的に、入力層のinput_shape(またはinput_dim)オプションで1次元を指定すると、スカラーの配列でも受け付けてくれる。
1 2 3 4 5 6 7 8 9 10 11 |
model = models.Sequential() model.add(layers.Dense(2, input_shape=(1,))) # model.add(layers.Dense(2, input_dim=1)) # おなじ意味 x = np.array([1,2,3]) # [1 2 3] pred = model.predict(x) # [[ 0.85891926 -0.37103486] # [ 1.7178385 -0.7420697 ] # [ 2.576758 -1.1131046 ]] |
まとめ。基本的にテンソル(ベクトル)の配列を入力する。スカラーの配列を入力したい場合はinput_shape(またはinput_dim)で1次元を指定する。
predictメソッドにわたすデータの形状
predictメソッドで予測するとき、データをひとつわたすとエラーになる。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
from keras import models, layers import numpy as np model = models.Sequential() model.add(layers.Dense(2, input_shape=(2, 3))) x = np.array( [[1, 2, 3], [4, 5, 6]] ) pred = model.predict(x) # ValueError: Error when checking input: expected dense_1_input to have 3 dimensions, but got array with shape (2, 3) |
サンプル次元を追加しなければならない。
1 2 3 4 5 6 7 8 9 10 11 |
x = np.array( [[1, 2, 3], [4, 5, 6]] ) x = np.expand_dims(x, axis=0) # [[[1 2 3] # [4 5 6]]] pred = model.predict(x) # [[[ 0.7466643 -0.79373527] # [-1.040453 -2.8780713 ]]] |
np.expand_dimsを使わずにこのようにしてもサンプル次元を追加できる。
1 2 3 |
x = x.reshape(1, 2, 3) # 元の形状(2, 3)のまえに1をつける # [[[1 2 3] # [4 5 6]]] |
fitメソッドにサンプル次元をつけなければならないのは当然だ。学習には複数のデータをまとめて投入する必要があるからだ。
しかしpredictメソッドにはひとつのデータを入れて予測したい場合もあると思う。しかし実際には、predictメソッドへの入力データは複数であることが前提となっているのでサンプル次元が必要。
ちなみにscikit-learnも同様にサンプル次元が必要。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
from sklearn.linear_model import LogisticRegression X = np.array([ [0, 0], [0, 1], [1, 0], [1, 1] ]) y = np.array([ [0], [1], [1], [1] ]) log_reg = LogisticRegression().fit(X, y) log_reg.predict([0, 1]) # ValueError: Expected 2D array, got 1D array instead: array=[0 1]. # Reshape your data either using array.reshape(-1, 1) if your data has a single feature or array.reshape(1, -1) if it contains a single sample. log_reg.predict([[0, 1]]) # array([1]) |
kerasと異なるのは、numpy配列でもいいし配列の配列でもOKということ。kerasはnumpy配列でなければだめ。
input_shapeに与える形状にはサンプル次元を加えない
input_shapeにはひとつひとつのデータの形状を指定する。サンプル次元は加えない。以下ではX.shapeは(4, 2)となる。よってサンプル次元をとりのぞいた(2, )をinput_shapeに指定する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
X = np.array([ [0, 0], [0, 1], [1, 0], [1, 1] ]) y = np.array([ [0], [1], [1], [1] ]) X.shape # (4, 2) import keras model = keras.models.Sequential() model.add(keras.layers.Dense(1, activation='sigmoid', input_shape=(2,))) model.compile(loss='binary_crossentropy', optimizer='rmsprop', metrics=['accuracy']) |
一方、predict, predict_proba, predict_classesに与えるデータにはサンプル次元を加える必要がある。
出力が複数ベクトルからなる場合の交差エントロピー誤差
チュートリアルでよくある分類問題では出力がひとつのベクトルだが、複数のベクトルを出力する問題もある。出力が複数ベクトルからなる場合の交差エントロピー誤差は、個々のベクトルの交差エントロピー誤差の平均であることを確認する。
1 2 3 4 5 6 7 8 9 10 11 12 |
from keras import models, layers import numpy as np weights = np.array([ [1, 2], [1, 2], [1, 2] ]) model = models.Sequential() model.add(layers.Dense(2, use_bias=False, trainable=False, weights=[weights])) # 重みの初期値はリストでわたす model.add(layers.Softmax()) model.compile(loss='categorical_crossentropy', optimizer='rmsprop') |
このようなモデルに対して、まずはデータをひとつずつ流してこんで誤差がいくつになるか確認。
1 2 3 4 5 6 7 8 |
x = np.array([ [1, 2, 3] ]) y = np.array([ [1, 0] ]) model.fit(x, y) # 1/1 [==============================] - 0s 14ms/step - loss: 6.0025 |
誤差は6くらい。次のデータを流し込む。
1 2 3 4 5 6 7 8 |
x = np.array([ [4, 5, 6] ]) y = np.array([ [0, 1] ]) model.fit(x, y) # 1/1 [==============================] - 0s 14ms/step - loss: 2.9802e-07 |
誤差はほぼゼロ。つぎに以上のふたつのデータを同時に流し込んでみる。
1 2 3 4 5 6 7 8 9 10 |
x = np.array([ [[1, 2, 3], [4, 5, 6]] ]) y = np.array([ [[1, 0], [0, 1]] ]) model.fit(x, y) # 1/1 [==============================] - 0s 17ms/step - loss: 3.0012 |
誤差は3。データを同時に流し込んだときの誤差は、個別にデータを流し込んだときの平均であることが確認できた。ソース読まないと確実なことはいえないけど、たぶんまちがいないと思う。
word2vecのskip-gramとか出力がふたつ以上のやつはたぶん複数の交差エントロピー誤差の平均が損失関数なんだと思う。gensimのソースを読むのが億劫なので推測だけしておいておく。なんか矛盾が起こったらしかたなくソース読む。
Kerasでの学習済みモデルの保存と読み込み
1 2 3 4 5 6 7 8 9 10 11 |
# モデル(重みデータを含む)を保存 model.save('model.h5') # 重みデータのみ保存 model.save_weights('weights.h5') from keras.models import load_model # モデルを読込 model = load_model('model.h5') # 重みデータを読込 model.load_weights('weights.h5') |
saveメソッドでモデルの構造と重みデータの両方を保存できる。重みのみ保存するにはsave_weightsメソッドを使う。
当然だがload_weightsメソッドを使う際、モデルの構造と重みデータの形状のつじつまがあっていないとエラーになる。
オプティマイザはrmsprop一択
rmspropオプティマイザは、一般にどのような問題でも十分によい選択である。心配事が1つ減ることになる。(p.78)
「PythonとKerasによるディープラーニング」より。
情報量を徐々に絞っていく層の構成がよさげ
アヤメデータで遊んでみた結果、ニューロン数が多い層から徐々に少ない層を重ねていって、最終的に3つのソフトマックス層で終える構成がいちばん性能がよくなった。情報量を徐々に絞っていって、最終的にラベル数に落とし込む構成がよいのだと思う。途中でニューロン数を多くすると性能が悪化する。
徐々に情報を絞っていく構成でも、層を17層も重ねたら複雑すぎて過学習を起こすかなと思ったら、起こさなかった。
plot_model
Keras.utils.plot_modelを使うとpydotだけインポートしようとし、pydotplusのインポートはトライしないのでtensorflow配下のものを使う(pydotは開発停止していてpython3.5以降では使えないらしいのでpydotplusを使っている)。
1 2 3 |
from tensorflow.python.keras.utils import plot_model plot_model(model, 'test.png') plot_model(model, 'test.png', show_shapes=True, show_layer_names=True) # 形状とレイヤー名を表示 |
ひとつはまった点。graphvizへパスを通したあと、開発環境(PyCharm)を再起動しないとパスが有効にならずエラーが解消されなかった。
LSTMの状態について
layers.SimpleRNNの場合、状態イコール出力のため、return_state=Trueにする意味はないと思う。
LSTMもそうだと誤解していたが、LSTMの状態はふたつある。状態h(=出力)と状態c。長短記憶ということから、たぶん状態hが短期の記憶で、状態cが長期の記憶なんだと思う。
1 2 3 4 5 6 7 8 9 10 11 12 |
from keras import models, layers import numpy as np inputs = layers.Input(shape=(3, 1)) outputs, state_h, state_c = layers.LSTM(1, return_state=True)(inputs) model = models.Model(inputs=inputs, outputs=[outputs, state_h, state_c]) data = np.array([1, 2, 3]).reshape(1,3,1) model.predict(data) # [array([[-0.18946545]], dtype=float32), # LSTMの最後の出力 # array([[-0.18946545]], dtype=float32), # LSTMの最後の状態h(=最後の出力) # array([[-0.69200426]], dtype=float32)] # LSTMの最後の状態c |
return_state=Trueを指定することで、最後の状態hとcを得ることができる。状態hは出力とおなじ。
1 2 3 4 5 6 7 8 9 10 11 |
inputs = layers.Input(shape=(3, 1)) outputs, state_h, state_c = layers.LSTM(1, return_state=True, return_sequences=True)(inputs) model = models.Model(inputs=inputs, outputs=[outputs, state_h, state_c]) data = np.array([1, 2, 3]).reshape(1,3,1) model.predict(data) # [array([[[0.01961198], # [0.05961949], # [0.11981957]]], dtype=float32), # LSTMの最後の出力 # array([[0.11981957]], dtype=float32), # LSTMの最後の状態h(=最後の出力) # array([[0.21112838]], dtype=float32)] # LSTMの最後の状態c |
return_sequences=Trueとすると、最後の出力と状態hが等しいことが確認できる。
initial_stateを指定するには、LSTMの場合はSimpleRNNとちがって状態hと状態cをセットで渡さなければならない。以下はLSTMを2層並べ、最初の層の状態を次の層に引き継いでいる。
1 2 3 |
inputs = layers.Input(shape=(3, 1)) outputs, state_h, state_c = layers.LSTM(1, return_state=True, return_sequences=True)(inputs) last_outputs = layers.LSTM(1)(outputs, initial_state=[state_h, state_c]) |
Masking層のあとのリカレント層でreturn_sequences=Trueにする場合はプレパディングがいい
ポストパディングすると以下のようになる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import numpy as np from keras import models, layers model = models.Sequential() model.add(layers.Masking(input_shape=(None,3))) model.add(layers.SimpleRNN(3, activation='linear', return_sequences=True, use_bias=False, weights=[np.eye(3), np.zeros((3, 3))])) data = np.array([ [[0, 1, 2], [3, 4, 5]], [[6, 7, 8], [0, 0, 0]] # ゼロパディング(ポストパディング) ]) model.predict(data) # [[[0. 1. 2.] # [3. 4. 5.]] # # [[6. 7. 8.] # [6. 7. 8.]]] # ゼロベクトルではなく先頭のデータがリピートされてしまった |
これではこのあとのDense層などで困ったことになる。
プレパディングにすると以下のようになる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
model = models.Sequential() model.add(layers.Masking(input_shape=(None,3))) model.add(layers.SimpleRNN(3, activation='linear', return_sequences=True, use_bias=False, weights=[np.eye(3), np.zeros((3, 3))])) data = np.array([ [[0, 1, 2], [3, 4, 5]], [[0, 0, 0], # ゼロパディング(プレパディング) [6, 7, 8]] ]) model.predict(data) # [[[0. 1. 2.] # [3. 4. 5.]] # # [[0. 0. 0.] # ゼロベクトルになっている # [6. 7. 8.]]] |
Masking層
可変長のデータを扱うさいに使うものと思っていたが、実験してみたらなんかちょっと違う。Masking層はその名のとおり、たんに指定した値をゼロに変換する(マスクする)もののようだ。
1 2 3 4 5 |
import numpy as np from keras import models, layers model = models.Sequential() model.add(layers.Masking(input_shape=(None, 7), mask_value=[10,10,10,10,10,10,10])) |
mask_valueにゼロに変換したい値を指定したMasking層のみのモデル。これに時系列データを流して動作確認していく。
1 2 3 4 5 6 7 |
test_data = np.array([ [[0,1,0,0,0,0,0], [0,0,0,0,0,1,0]] ]) model.predict(test_data)[0] # [[0. 1. 0. 0. 0. 0. 0.] # [0. 0. 0. 0. 0. 1. 0.]] |
mask_valueに指定した値を含まないとそのままのデータが流れてくる。
1 2 3 4 5 6 7 8 9 10 |
test_data = np.array([ [[0,1,0,0,0,0,0], [10,10,10,10,10,10,10], [10,10,10,10,10,10,10]] ]) model.predict(test_data)[0] # [[0. 1. 0. 0. 0. 0. 0.] # [0. 0. 0. 0. 0. 0. 0.] # [0. 0. 0. 0. 0. 0. 0.]] |
mask_valueに指定した値はゼロ埋めされて流れてくる。
1 2 3 4 5 6 7 8 9 |
model = models.Sequential() model.add(layers.Masking(input_shape=(5, 7))) # タイムステップを5に指定 test_data = np.array([ [[0,1,0,0,0,0,0], [0,0,0,0,0,1,0]] ]) model.predict(test_data) # ValueError: Error when checking input: expected masking_1_input to have shape (5, 7) but got array with shape (2, 7) |
ポイントは、可変長のデータを事前に特殊な値でパディングするのは自分ということ。Masking層は特殊な値(mask_value)をゼロ埋めして流してくれる。
ということは、自分でパディングするさいゼロパディングすればMasking層はいらない。
↑は勘違い。Masking層を入れると指定した値をゼロに変換するだけではなく、以降のリカレント層でパディング部分の計算をスキップしてくれる。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
model = models.Sequential() model.add(layers.SimpleRNN(3, activation='linear', use_bias=False, weights=[np.eye(3), np.zeros((3, 3))])) data = np.array([ [[0, 1, 2], [3, 4, 5]], [[6, 7, 8], [0, 0, 0]] # ゼロパディング ]) model.predict(data) # [[3. 4. 5.] # [0. 0. 0.]] |
Masking層を入れないとゼロパディングした部分も計算してしまうため、[6, 7, 8]ではなく最後の[0, 0, 0]が流れてくる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
model = models.Sequential() model.add(layers.Masking(input_shape=(None,3), mask_value=[0, 0, 0])) model.add(layers.SimpleRNN(3, activation='linear', use_bias=False, weights=[np.eye(3), np.zeros((3, 3))])) data = np.array([ [[0, 1, 2], [3, 4, 5]], [[6, 7, 8], [0, 0, 0]] # ゼロパディング ]) model.predict(data) # [[3. 4. 5.] # [6. 7. 8.]] |
Masking層を入れるとゼロパディング部分の計算をスキップしてくれるため、[6, 7, 8]が流れてくる。ちなみにmask_valueを指定しなくてもゼロベクトルをスキップしてくれるようだ。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
model = models.Sequential() model.add(layers.Masking(input_shape=(None,3))) # mask_valueを指定しない model.add(layers.SimpleRNN(3, activation='linear', use_bias=False, weights=[np.eye(3), np.zeros((3, 3))])) data = np.array([ [[0, 1, 2], [3, 4, 5]], [[6, 7, 8], [0, 0, 0]] # ゼロパディング ]) model.predict(data) # [[3. 4. 5.] # [6. 7. 8.]] # mask_value=[0, 0, 0]としなくても[0, 0, 0]をスキップしてくれた |
逆にmask_value=[6, 7, 8]とすると[6, 7, 8]がスキップされて[0, 0, 0]が出力される。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
model = models.Sequential() model.add(layers.Masking(input_shape=(None,3), mask_value=[6, 7, 8])) model.add(layers.SimpleRNN(3, activation='linear', use_bias=False, weights=[np.eye(3), np.zeros((3, 3))])) data = np.array([ [[0, 1, 2], [3, 4, 5]], [[6, 7, 8], [0, 0, 0]] # ゼロパディング ]) model.predict(data) # [[3. 4. 5.] # [0. 0. 0.]] |
結論。パディング部分の計算をスキップするためにMasking層が必要。
個人的に、Masking層を入れるとほかの層(リカレント層)の動作が変わるというのは違和感がある。ほかの層のオプションでスキップする値を指定できたほうが直観的にわかりやすいかな。しかし、たくさん層が積み重なっているネットワークだとぜんぶの層でオプションいじるのが面倒そうだ。Masking層ひとついれるだけでOKというのは合理的なんだろう。
Embedding層のmask_zeroオプション
まずはKerasの日本語ドキュメントから引用。
mask_zero: 真理値.入力の0をパディングのための特別値として扱うかどうか. これは入力の系列長が可変長となりうる変数を入力にもつRecurrentレイヤーに対して有効です. この引数がTrueのとき,以降のレイヤーは全てこのマスクをサポートする必要があり, そうしなければ,例外が起きます. mask_zeroがTrueのとき,index 0は語彙の中で使えません(input_dim は語彙数+1と等しくなるべきです).
わかりそうでわからなかったので実験で確かめていく。
まずはEmbedding層のみで動作確認。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import numpy as np from keras import models, layers embedding_weights = np.arange(5*3).reshape(5, 3) # [[ 0. 1. 2.] # [ 3. 4. 5.] # [ 6. 7. 8.] # [ 9. 10. 11.] # [12. 13. 14.]] model = models.Sequential() model.add(layers.Embedding(5, 3, mask_zero=True, weights=[embedding_weights])) model.predict([0]) # [[[0. 1. 2.]]] # mask_zero=Trueにしているがふつうに0行目が出力される |
mask_zeroオプションを指定していても、Embedding層の出力には変化なし。
つぎにEmbedding層のあとにRNN層を追加してみる。
1 2 3 4 5 6 7 8 9 10 |
model = models.Sequential() model.add(layers.Embedding(5, 3, mask_zero=True, weights=[embedding_weights])) # 入力をそのまま流すRNN。 model.add(layers.SimpleRNN(3, activation='linear', use_bias=False, weights=[np.eye(3), np.zeros((3, 3))])) model.predict([1]) # [[3. 4. 5.]] # 当然1行目が出力される model.predict([0]) # [[0. 0. 0.]] # 0行目ではなくゼロベクトルが出力された |
mask_zero=Trueにすると、そのあとのリカレント層でインデックス0に対応するベクトルがゼロベクトルに変換される。
これってEmbedding層の0行目をゼロベクトルにするのとおなじなのでは?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
embedding_weights = np.arange(5*3).reshape(5, 3) embedding_weights[0] = [0, 0, 0] # 0行目をゼロベクトルにする # [[ 0. 0. 0.] # [ 3. 4. 5.] # [ 6. 7. 8.] # [ 9. 10. 11.] # [12. 13. 14.]] model = models.Sequential() model.add(layers.Embedding(5, 3, weights=[embedding_weights])) # mask_zero=Trueを外す model.add(layers.SimpleRNN(3, activation='linear', use_bias=False, weights=[np.eye(3), np.zeros((3, 3))])) model.predict([0]) # [[0. 0. 0.]] |
やはりおなじでした。mask_zero=Trueを外す代わりに重みの0行目をゼロベクトルにしてもおなじ結果になる。こちらのほうが直観的にはわかりやすい。
けっきょくはMasking層とおなじく、可変長のデータをゼロパディングして長さをそろえるのは別途やらなければならない。
ゼロパディングはkeras.preprocessing.sequence.pad_sequences関数でかんたんにできる。
ゼロパディングしたならmask_zero=Trueにする必要はないと思う。ゼロになにかけてもゼロだから。→と思ったんだが、バイアスをFalseにしてない場合はバイアスが足されてゼロじゃなくなってしまう。バイアスFalseの場合はほぼない。ゆえにやっぱりゼロパディングした場合でもmask_zero=Trueは必要。