KerasのTokenizerの細かい設定

前回の記事の続きで、 Keras の Tokenizer の話です。

Tokenizerのインスタンスを作成する時、前回紹介した引数の filters / lower/ split /char_level のほか、
num_wordsと、oov_token という二つの引数を設定することができます。
これが少し癖があるので確認しておきましょう。

まず、num_wordsですが、こちらは学習する単語の種類数を指定するものです。
ここで注意しないといけないのは、インデックス0がどの単語にも割り当てられないインデックスとして予約されてしまっているので、
Tokenizerが学習してくれる単語は、num_wordsで指定したのよりも一つ少なくなります。
(普通は数千とかの値を渡すのですが動きを確認しやすくするために、)試しにnum_words=7 を渡してみましょう。
なお、前回の記事同様に、 変数 text_data に、テキストデータが入ってるものとします。


# num_words で、学習する語彙数を指定できる。
keras_tokenizer_7 = Tokenizer(
                            num_words=7
                        )

# 文字列から学習する
keras_tokenizer_7.fit_on_texts(text_data)

# 学習した単語とそのindex
print(keras_tokenizer_7.word_index)
# {'the': 1, 'of': 2, 'to': 3, 'and': 4, 'a': 5, 'in': 6, 'is': 7, 'i': 8, 'that': 9, 'it': 10, 'for': 11, 'this': 12,... 

まず、戸惑うところは、word_indexには大量の単語が含まれている点です。でも、実際にトークン化できる単語は頻度が高い7-1=6個だけになります。
試しに高頻度の単語を並べたテキストをトークン化してみましょう。


print(keras_tokenizer_7.texts_to_sequences(["the of to and a in is i that it for"]))
# [[1, 2, 3, 4, 5, 6]]

ちょっとわかりにくいですが、 the から in までの 6単語が変換され、 is 以降は無視されました。

次に、 oov_token です。 これは元々のテキストに含まれていなかったり、num_wordsで指定した上限値によって溢れてしまった単語を、
oov_token で渡した文字列に変換するものです。
それだけなら単純ですが、注意しないといけないのは、oov_tokenがインデックス1を使ってしまうので、
oov_tokenに何か値を指定すると、学習してくれる単語が一つ減ります。

num_words を先ほどと同じように7にしておくと、 the, of, to, and, a, の5単語しか変換しないことがわかります。


keras_tokenizer_oov = Tokenizer(
                            num_words=7,
                            oov_token="未知語"
                        )

# 文字列から学習する
keras_tokenizer_oov.fit_on_texts(text_data)

# 学習した単語とそのindex
print(keras_tokenizer_oov.word_index)
# {'未知語': 1, 'the': 2, 'of': 3, 'to': 4, 'and': 5, 'a': 6, 'in': 7, 'is': 8, 'i': 9, 'that': 10, 'it': 11,...

print(keras_tokenizer_oov.texts_to_sequences(["the of to and a in is i that it for"]))
# [[2, 3, 4, 5, 6, 1, 1, 1, 1, 1, 1]]

oov_token を使うと、元の単語列と、変換後の数列の要素数が維持されますし、
未学習だった単語がどれだけ含まれているのかもわかるのでとても便利ですが、
使える単語が一個減ることなどは注意しないといけませんね。

ちなみに、 oov_token に、 学習対象のドキュメントにある単語を使うと、その単語のインデックスがoov_tokenとして使われ、
1が使われなくなります。
ちょっとややこしいのでできるだけ避けましょう。

KerasのTokenizerの基本的な使い方

自然言語処理において翻訳などのseq2seqモデルやそれ以外でもRNN系のモデルを使う場合、
前処理においてテキストの列を数列に変換(トークン化)することがあります。

そのよな時に、Kerasのユーティリティーに用意されている、Tokenizerが便利なのでその基本的な使い方を紹介します。
今回の主な内容は次の4つです。(その他細かいオプションとか、別の使い側は次回以降の更新で。)
– インスタンスの生成
– テキストを数列化する
– デフォルトパラメーターで生成した時の設定
– 数列をテキストに戻す

ドキュメントはこちらです。

サンプルに何かデータが必要なので、20newsのデータを一部だけ読み込んで使います。


from sklearn.datasets import fetch_20newsgroups

# データの読み込み。少量で良いのでカテゴリも一つに絞る。
remove = ('headers', 'footers', 'quotes')
categorys = [
        "sci.med",
    ]
twenty_news = fetch_20newsgroups(
                                subset='train',
                                remove=remove,
                                categories=categorys
                            )
text_data = twenty_news.data

Tokenizer を使うときはまずはインスタンスを生成し、
テキストデータを学習させる必要があります。
(ここで学習しなかった単語はトークン化できません。)


from tensorflow.keras.preprocessing.text import Tokenizer
# Tokenizer のインスタンス生成
keras_tokenizer = Tokenizer()
# 文字列から学習する
keras_tokenizer.fit_on_texts(text_data)

# 学習した単語とそのindex
print(keras_tokenizer.word_index)
"""
{'the': 1, 'of': 2, 'to': 3, 'and': 4, 'a': 5, 'in': 6, 'is': 7,
 'i': 8, 'that': 9, 'it': 10, 'for': 11, 'this': 12, 'are': 13, ...,
--- 以下略 ---
"""

テキストデータをトークン化するときは、texts_to_sequences に、”テキストデータの配列を”渡します。
テキストを一つだけ渡すと、それを文字単位に分解してしまうので注意してください。


# テキストデータを数列に変更
sequence_data = keras_tokenizer.texts_to_sequences(text_data)
# 一つ目のテキストの変換結果。
print(sequence_data[0])
"""
[780, 3, 1800, 4784, 4785, 3063, 1800, 2596, 10, 41, 130, 24,
15, 4, 148, 388, 2597, 11, 60, 110, 20, 38, 515, 108, 586, 704,
353, 21, 46, 31, 7, 467, 3, 268, 6, 5, 4786, 965, 2223, 43, 2598,
2, 1, 515, 24, 15, 13, 747, 11, 5, 705, 662, 586, 37, 423, 587, 7092,
77, 13, 1490, 3, 130, 16, 5, 2224, 2, 12, 415, 3064, 12, 7, 5, 516,
6, 40, 79, 47, 18, 610, 3732, 1801, 26, 2225, 706, 918, 3065, 2,
1, 1801, 21, 32, 61, 1638, 31, 329, 7, 9, 1, 1802, 966, 1491,
18, 3, 126, 3066, 4, 50, 1352, 3067]
"""

これで目的のトークン化ができました。
今回は、インスタンス化する時に何も引数を渡さず、完全にデフォルトの設定になっているのですが、
一応主な設定を確認しておきましょう。


# デフォルトでは、文字を小文字に揃える。
print(keras_tokenizer.lower)
# True

# デフォルトでは文字単位ではなく、次のsplitで区切った単語単位でトークン化する。
print(keras_tokenizer.char_level)
# False

# デフォルトでは、split に半角スペースが指定されており、スーペースで区切られる。
print(keras_tokenizer.split == " ")

# いくつかの記号は除外され、単語中に含まれている場合はそこで区切られる。
print(keras_tokenizer.filters)
# !"#$%&()*+,-./:;<=>?@[\]^_`{|}~

# 例えば、 dog&cat は &が取り除かれ、 dog と cat が個別にトークン化される。
print(keras_tokenizer.texts_to_sequences(["dog&cat"]))
# [[7316, 2043]]

# & が 半角ペースだった場合と結果は同じ
print(keras_tokenizer.texts_to_sequences(["dog cat"]))
# [[7316, 2043]]

最後に、トークン列をテキストに戻す方法です。
sequences_to_texts を使います。


# 数列をテキストに戻す。
text_data_2 = keras_tokenizer.sequences_to_texts(sequence_data)

print("元のテキスト")
print(text_data[0])
print("\n復元したテキスト")
print(text_data_2[0])

"""
元のテキスト
[reply to keith@actrix.gen.nz (Keith Stewart)]


It would help if you (and anyone else asking for medical information on
some subject) could ask specific questions, as no one is likely to type
in a textbook chapter covering all aspects of the subject.  If you are
looking for a comprehensive review, ask your local hospital librarian.
Most are happy to help with a request of this sort.

Briefly, this is a condition in which patients who have significant
residual weakness from childhood polio notice progression of the
weakness as they get older.  One theory is that the remaining motor
neurons have to work harder and so die sooner.

復元したテキスト
reply to keith actrix gen nz keith stewart it would help if you and anyone
else asking for medical information on some subject could ask specific
questions as no one is likely to type in a textbook chapter covering all
aspects of the subject if you are looking for a comprehensive review ask
your local hospital librarian most are happy to help with a request of this
sort briefly this is a condition in which patients who have significant
residual weakness from childhood polio notice progression of
the weakness as they get older one theory is that the remaining
motor neurons have to work harder and so die sooner
"""

改行のほか、括弧やカンマなどの記号が消えていること、一部の大文字が小文字になっていることなどが確認できます。

kerasのモデルの可視化

kerasでモデルを構築したとき、構築したモデルが意図した構造になっているかどうか可視化して確認する方法です。
Sequentialモデルであれば、 .summary()で十分なことが多いのですが、functional APIを使って複雑なモデルを作る場合に重宝します。

kerasのドキュメントを見ると、そのままズバリな名前で 可視化 のページがあり、plot_modelという関数が説明されています。
可視化 – Keras Documentation

「graphvizを用いて」と書かれている通り、graphvizがインストールされている必要がありますが、
このほか pydot というライブラリも必要なのでpip等でインストールしておきましょう。
(他サイトなどでpydotは開発が止まっていて動かないからpydotplusを使う、といった趣旨の記事を見かけますが、
現在はpydotの開発が再開されているようでpydotで動きます。)

さて、graphvizとpydotが入ったら、早速ちょっとだけ複雑なモデルを作ってみて、可視化してみましょう。
一応 model.summary() の結果も表示してみました。

まずは可視化対象のモデル構築から。


from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input
from tensorflow.keras.layers import Dense
from tensorflow.keras.layers import Concatenate
from tensorflow.keras.layers import Add

i0 = Input(shape=(64, ))
i1 = Input(shape=(64, ))
x0 = Concatenate()([i0, i1])
x1 = Dense(32, activation="tanh")(x0)
x2 = Dense(32, activation="tanh")(x1)
x3 = Add()([x1, x2])
x4 = Dense(1, activation="sigmoid")(x3)
model = Model([i0, i1], x4)

print(model.summary())
# 以下出力結果
"""
Model: "model"
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
input_1 (InputLayer)            [(None, 64)]         0                                            
__________________________________________________________________________________________________
input_2 (InputLayer)            [(None, 64)]         0                                            
__________________________________________________________________________________________________
concatenate (Concatenate)       (None, 128)          0           input_1[0][0]                    
                                                                 input_2[0][0]                    
__________________________________________________________________________________________________
dense (Dense)                   (None, 32)           4128        concatenate[0][0]                
__________________________________________________________________________________________________
dense_1 (Dense)                 (None, 32)           1056        dense[0][0]                      
__________________________________________________________________________________________________
add (Add)                       (None, 32)           0           dense[0][0]                      
                                                                 dense_1[0][0]                    
__________________________________________________________________________________________________
dense_2 (Dense)                 (None, 1)            33          add[0][0]                        
==================================================================================================
Total params: 5,217
Trainable params: 5,217
Non-trainable params: 0
__________________________________________________________________________________________________
None
"""

Connected to に複数レイヤー入っているとぱっと見わかりにくいですね。

次にplot_model使ってみます。
show_shapes オプションを使って、入出力の形も表示してみました。


from tensorflow.keras.utils import plot_model
plot_model(
    model,
    show_shapes=True,
)

出力されたのがこちら。

モデルの形をイメージしやすいですね。

層化K分割交差検証の紹介とPythonで実行する方法

少し前の記事になりますが、 scikit-learnでK-分割交差検証 というのを書きました。
これは、分類のタスクでは目的変数の件数がクラスごとにある程度揃っていたり、データが十分に揃っていればうまく機能します。
しかし、一方で不均衡データなど、目的変数の値の割合が偏っていて特に、一部のクラスのデータが非常に少ないと困ったことになります。

試しに、いつものirisのデータを少し絞り込んで、元々種類ごとに50件ずつあるデータを
setosa: 50個
versicolor: 10個
virginica: 5個
にして試してみます。
(一番少ないクラスのデータ件数が5個なのに5分割するという極端な例ですが、
説明のためなのでご了承ください。)


from sklearn.model_selection import KFold
from sklearn.datasets import load_iris
import numpy as np

# データの読み込み
iris = load_iris()
X = iris.data
y = iris.target

# 実験のため対象を絞り込んで不均衡データにする
index = list(range(50)) + list(range(50, 60)) + list(range(100, 105))
X = X[index]
y = y[index]

for c in range(3):
    print(f"{iris.target_names[c]}: {list(y).count(c)}個")

"""
setosa: 50個
versicolor: 10個
virginica: 5個
"""

# KFoldを用いてK-分割交差検証した時に各グループに含まれるラベル数
kf = KFold(5, shuffle=True)
i = 0
for train_index, test_index in kf.split(X):
    i += 1
    print(f"\n{i}グループの訓練データに含まれるラベル")
    train_y = y[train_index]
    for c in range(3):
        print(f"{iris.target_names[c]}: {list(train_y).count(c)}個")
    print(f"{i}グループのテストデータに含まれるラベル")
    test_y = y[test_index]
    for c in range(3):
        print(f"{iris.target_names[c]}: {list(test_y).count(c)}個")

"""
1グループの訓練データに含まれるラベル
setosa: 41個
versicolor: 6個
virginica: 5個
1グループのテストデータに含まれるラベル
setosa: 9個
versicolor: 4個
virginica: 0個

2グループの訓練データに含まれるラベル
setosa: 42個
versicolor: 7個
virginica: 3個
2グループのテストデータに含まれるラベル
setosa: 8個
versicolor: 3個
virginica: 2個

3グループの訓練データに含まれるラベル
setosa: 39個
versicolor: 9個
virginica: 4個
3グループのテストデータに含まれるラベル
setosa: 11個
versicolor: 1個
virginica: 1個

4グループの訓練データに含まれるラベル
setosa: 40個
versicolor: 8個
virginica: 4個
4グループのテストデータに含まれるラベル
setosa: 10個
versicolor: 2個
virginica: 1個

5グループの訓練データに含まれるラベル
setosa: 38個
versicolor: 10個
virginica: 4個
5グループのテストデータに含まれるラベル
setosa: 12個
versicolor: 0個
virginica: 1個
"""

結果が長くなって恐縮ですが、1グループ目では、テストデータにvirginicaが含まれなくなっていますし、
5グループ目では versicolor がテストデータにありません。
逆に、訓練データでそれらのデータの割合が過剰に高くなっています。
これではモデルの学習もうまくいきませんし、評価も適切に行えません。

このような時、 train_test_splitであれば、stratify引数を使って、ラベルの割合を揃えられます。
参考: scikit-learnのtrain_test_splitで、訓練データとテストデータのラベルの割合を揃える
そして、KFoldには stratify がないのですが代わりに、
タイトルの 層化K分割交差検証(Stratified K-Folds cross-validator)という手法が知られており、それに対応する
StratifiedKFold というクラスが用意されています。

要は、ラベルの割合を揃えながらK分割交差検証する方法です。
使い方はKFoldととても似ていますが、splitするときに、labelも渡してあげる必要がある点だけ注意です。


from sklearn.model_selection import StratifiedKFold

skf = StratifiedKFold(5, shuffle=True)
i = 0
for train_index, test_index in skf.split(X, y):
    i += 1
    print(f"\n{i}グループの訓練データに含まれるラベル")
    train_y = y[train_index]
    for c in range(3):
        print(f"{iris.target_names[c]}: {list(train_y).count(c)}個")
    print(f"{i}グループのテストデータに含まれるラベル")
    test_y = y[test_index]
    for c in range(3):
        print(f"{iris.target_names[c]}: {list(test_y).count(c)}個")

"""
1グループの訓練データに含まれるラベル
setosa: 40個
versicolor: 8個
virginica: 4個
1グループのテストデータに含まれるラベル
setosa: 10個
versicolor: 2個
virginica: 1個

2グループの訓練データに含まれるラベル
setosa: 40個
versicolor: 8個
virginica: 4個
2グループのテストデータに含まれるラベル
setosa: 10個
versicolor: 2個
virginica: 1個

3グループの訓練データに含まれるラベル
setosa: 40個
versicolor: 8個
virginica: 4個
3グループのテストデータに含まれるラベル
setosa: 10個
versicolor: 2個
virginica: 1個

4グループの訓練データに含まれるラベル
setosa: 40個
versicolor: 8個
virginica: 4個
4グループのテストデータに含まれるラベル
setosa: 10個
versicolor: 2個
virginica: 1個

5グループの訓練データに含まれるラベル
setosa: 40個
versicolor: 8個
virginica: 4個
5グループのテストデータに含まれるラベル
setosa: 10個
versicolor: 2個
virginica: 1個
"""

全グループで、訓練データとテストデータの割合が揃っているのを確認できました。

評価値の改善が止まった時に学習率を減らす

今回もkerasの学習率改善のコールバックの話です。
LearningRateScheduler を使って、エポックごとの学習率を変えられることを紹介しましたが、
実際、学習をやってみる前に最適な学習率の変化の計画を立てておくことは非常に困難です。
最初は大きめの値でどんどん学習して、それではうまくいかなくなった段階で徐々に下げるということをやりたくなります。

そして、 kerasにはそのためのコールバックの、ReduceLROnPlateau というのが用意されています。
監視する評価値、何エポック改善しなかったら学習率を落とすか、その変化の割合、最小値などを指定すると、
学習の進みに応じて調整してくれます。

さっそく適当なモデルで試してみましょう。
(今回は着目するのが学習率の変化なので、下のコードのモデルは対して良いものでもないことをご了承ください。)


from tensorflow.keras.datasets import fashion_mnist
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras.layers import Dropout
from tensorflow.keras.layers import Flatten
from tensorflow.keras.layers import Conv2D
from tensorflow.keras.layers import MaxPooling2D
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.callbacks import EarlyStopping
from tensorflow.keras.callbacks import ReduceLROnPlateau
import matplotlib.pyplot as plt
from sklearn.metrics import classification_report

# データの読み込み
(data_train, target_train), (data_test, target_test) = fashion_mnist.load_data()

# Conv2D の inputに合わせて変形
X_train = data_train.reshape(-1, 28, 28, 1)
X_test = data_test.reshape(-1, 28, 28, 1)

# 特徴量を0~1に正規化する
X_train = X_train / 255
X_test = X_test / 255

# ラベルを1 hot 表現に変換
y_train = to_categorical(target_train, 10)
y_test = to_categorical(target_test, 10)

# lr に少し大きめの値を設定しておく (デフォルトは lr =0.001)
adam = Adam(lr=0.01)

# モデルの構築
model = Sequential()
model.add(Conv2D(16, kernel_size=(3, 3),
                 activation='relu',
                 input_shape=(28, 28, 1)))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(32, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())
model.add(Dense(64, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(10, activation='softmax'))
model.compile(
    loss="categorical_crossentropy",
    optimizer=adam,
    metrics=['acc']
)
print(model.summary())
"""
Model: "sequential_3"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_6 (Conv2D)            (None, 26, 26, 16)        160       
_________________________________________________________________
max_pooling2d_6 (MaxPooling2 (None, 13, 13, 16)        0         
_________________________________________________________________
conv2d_7 (Conv2D)            (None, 11, 11, 32)        4640      
_________________________________________________________________
max_pooling2d_7 (MaxPooling2 (None, 5, 5, 32)          0         
_________________________________________________________________
flatten_3 (Flatten)          (None, 800)               0         
_________________________________________________________________
dense_6 (Dense)              (None, 64)                51264     
_________________________________________________________________
dropout_7 (Dropout)          (None, 64)                0         
_________________________________________________________________
dense_7 (Dense)              (None, 10)                650       
=================================================================
Total params: 56,714
Trainable params: 56,714
Non-trainable params: 0
_________________________________________________________________
"""
early_stopping = EarlyStopping(
                        monitor='val_loss',
                        min_delta=0.0,
                        patience=10,
                )

# val_lossの改善が2エポック見られなかったら、学習率を0.5倍する。
reduce_lr = ReduceLROnPlateau(
                        monitor='val_loss',
                        factor=0.5,
                        patience=2,
                        min_lr=0.0001
                )

history = model.fit(X_train, y_train,
                    batch_size=128,
                    epochs=50,
                    verbose=2,
                    validation_data=(X_test, y_test),
                    callbacks=[early_stopping, reduce_lr],
                    )
"""
   (途中は省略。以下は最終的な結果)
Epoch 26/50
60000/60000 - 12s - loss: 0.2263 - acc: 0.9138 - val_loss: 0.3177 - val_acc: 0.8998
"""

さて、学習が完了したことで、history に結果が入りましたので、 監視していた val_loss と学習率 lrをみてみましょう。


# val_loss と lr を可視化
fig = plt.figure(figsize=(10, 10), facecolor="w")
ax = fig.add_subplot(2, 1, 1)
ax.set_title("val_loss")
ax.plot(range(len(history.history["val_loss"])), history.history["val_loss"])
ax = fig.add_subplot(2, 1, 2)
ax.set_title("lr")
ax.plot(range(len(history.history["lr"])), history.history["lr"])
plt.show()

学習率が段階的に半減していっているのが確認できますね・

kerasの学習率調整の結果をhistoryオブジェクトから確認する

昨日の記事に続いて、学習率調整の話です。
参考: kerasのLearningRateSchedulerで学習途中に学習率を調整する

昨日の記事では、動作確認のため、LearningRateSchedulerの引数、verboseに1を設定して、
エポックごとの学習率を出力して動きを見ていました。
しかし、見ての通り、出力がかなり煩雑になって、なかなか煩わしいです。
なので、普段は verbose=0 で使うのですが、そうすると、思うように学習が進まなかった時に、
調査の一環で学習率の変化の具合を見ようと思うと困っていました。
(verboseを1になおして再実行すると時間がかかるし、結果も変わるので。)

その時、何気なくhistoryオブジェクト(fit関数の戻り値)を可視化してみると、その中に lr も含まれているのを見つけました。
昨日の記事のコードの続きで実行してみるとこんな感じで。


print(history.history["lr"])
# [0.002, 0.002, 0.002, 0.001, 0.001, 0.0005, 0.0005, 0.0005, 0.0005, 0.0005]

history.history には, accもlossも入っているので、学習率の変化やその影響の確認は,
これをみるのが一番良さそうです。

kerasのLearningRateSchedulerで学習途中に学習率を調整する

機械学習や深層学習において、より効率的にモデルを学習させるテクニックとして、
学習の進捗に応じて、学習率を変更するというものがあります。

kerasでは、これを手軽に実行するために LearningRateScheduler というコールバックが用意されています。
これに、 エポックのインデックスを受け取って学習率を返す関数を渡して、
それをcallbackに設定しておくと実現できます。

だいたい次のイメージで使えます。
例によって、モデルはすでに構築されているものとします。
(下のコードで動かしてるモデルはこのブログの CNNで手書き数字文字の分類 の記事からコピーして持ってきました。)


# 学習率を返す関数を用意する
def lr_schedul(epoch):
    x = 0.002
    if epoch >= 3:
        x = 0.001
    if epoch >= 5:
        x = 0.0005
    return x


lr_decay = LearningRateScheduler(
    lr_schedul,
    # verbose=1で、更新メッセージ表示。0の場合は表示しない
    verbose=1,
)

history = model.fit(X_train, y_train,
                    batch_size=128,
                    epochs=10,
                    verbose=2,
                    validation_data=(X_test, y_test),
                    callbacks=[lr_decay],
                    )

# 以下出力
"""
Train on 60000 samples, validate on 10000 samples
Epoch 1/10

Epoch 00001: LearningRateScheduler setting learning rate to 0.002.
 - 13s - loss: 0.4354 - acc: 0.8625 - val_loss: 0.0707 - val_acc: 0.9762
Epoch 2/10

Epoch 00002: LearningRateScheduler setting learning rate to 0.002.
 - 12s - loss: 0.1748 - acc: 0.9476 - val_loss: 0.0534 - val_acc: 0.9823
Epoch 3/10

Epoch 00003: LearningRateScheduler setting learning rate to 0.002.
 - 12s - loss: 0.1376 - acc: 0.9590 - val_loss: 0.0387 - val_acc: 0.9872
Epoch 4/10

Epoch 00004: LearningRateScheduler setting learning rate to 0.001.
 - 14s - loss: 0.1105 - acc: 0.9675 - val_loss: 0.0332 - val_acc: 0.9882
Epoch 5/10

Epoch 00005: LearningRateScheduler setting learning rate to 0.001.
 - 15s - loss: 0.1041 - acc: 0.9694 - val_loss: 0.0311 - val_acc: 0.9902
Epoch 6/10

Epoch 00006: LearningRateScheduler setting learning rate to 0.0005.
 - 15s - loss: 0.0960 - acc: 0.9725 - val_loss: 0.0293 - val_acc: 0.9899
Epoch 7/10

Epoch 00007: LearningRateScheduler setting learning rate to 0.0005.
 - 14s - loss: 0.0889 - acc: 0.9735 - val_loss: 0.0275 - val_acc: 0.9899
Epoch 8/10

Epoch 00008: LearningRateScheduler setting learning rate to 0.0005.
 - 18s - loss: 0.0880 - acc: 0.9747 - val_loss: 0.0273 - val_acc: 0.9899
Epoch 9/10

Epoch 00009: LearningRateScheduler setting learning rate to 0.0005.
 - 14s - loss: 0.0856 - acc: 0.9746 - val_loss: 0.0274 - val_acc: 0.9905
Epoch 10/10

Epoch 00010: LearningRateScheduler setting learning rate to 0.0005.
 - 13s - loss: 0.0811 - acc: 0.9764 - val_loss: 0.0264 - val_acc: 0.9906
"""

今回はお試しで学習率が変わっていることを見たかったので、 verbose=1 を指定して、LearningRateSchedulerにも
ログを出力させました。
最初の 0.002 から、 0.0005へと、学習率が変わっていっていることがわかります。

ただ、少し煩わしいので、普段の利用では verbose=0 (デフォルトなので未指定でも可)がおすすめです。

ImageDataGeneratorで拡張したデータでモデルを学習する

ImageDataGenerator で画像データを拡張(水増し)したら、それを使って機械学習のモデルを学習したいと思います。

一番単純なのは、欲しいデータ量を決めて、flowメソッドで必要なだけデータを生成し、
それを配列に格納しておいてがくしゅうする方法だと思います。

ただ、これを実行すると、生成したデータ量にもよりますがすぐメモリの消費量が増えてメモリエラー等が発生します。
このような時、拡張された具体的なデータではなく、ジェネレーターをそのまま渡して学習するメソッドがkerasには用意されています。

それが、モデルクラスの fit_generator です。

それぞれのメソッドの引数をみてみましょう。


fit(
    x=None,
    y=None,
    batch_size=None,
    epochs=1,
    verbose=1,
    callbacks=None,
    validation_split=0.0,
    validation_data=None,
    shuffle=True,
    class_weight=None,
    sample_weight=None,
    initial_epoch=0,
    steps_per_epoch=None,
    validation_steps=None
)

fit_generator(
    generator,
    steps_per_epoch=None,
    epochs=1,
    verbose=1,
    callbacks=None,
    validation_data=None,
    validation_steps=None,
    class_weight=None,
    max_queue_size=10,
    workers=1,
    use_multiprocessing=False,
    shuffle=True,
    initial_epoch=0
)

fitでは、最初のふたつの引数x,yでデータとラベルを渡しますが、fit_generatorではそれがgeneratorになります。
そして、このgeneratorが、バッチサイズ分のデータとラベルを一式生成し続けてそれを使って学習します。

次のようなコードで、利用することができます。
(例ではvalidation_dataは固定にしましたが、ここでもジェネレーターを使えます。)


from tensorflow.keras.preprocessing.image import ImageDataGenerator
datagen = ImageDataGenerator(
    # 設定は適当
    rotation_range=30,
    width_shift_range=0.2,
    height_shift_range=0.2,
    zoom_range = 0.2,
    channel_shift_range = 0.2,
    horizontal_flip=True,
)
datagen.fit(x_tr)

# モデル(model)と、データ(x_train,y_train)は別途準備されているものとします。
history = model.fit_generator(
            datagen.flow(x_train, y_train, batch_size=256),
            epochs=100,
            verbose=2,
            validation_data=(x_val, y_val),
        )

ImageDataGeneratorの引数

前々回の記事: kerasによる画像データ拡張 で紹介した、
ImageDataGenerator を使って、色々画像を変換してみます。
引数の一覧はドキュメントにある通りで、
平行移動や回転、色の変更や拡大など色々行えます。
同時に複数を設定することももちろんできるのですが、今回は個々の動作をみたいので、1個ずつ適用しました。
また、どれもランダムに変換するものなので1枚の画像を5回ずつ変換して出力しています。

次が書いてみたコードです。結果がわかりやすいように変数はどれも大きめの値を渡しました。


from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.datasets import cifar10
import matplotlib.pyplot as plt

# データ取得
(x_train, y_train), (x_test, y_test) = cifar10.load_data()

# 1データだけ抽出。
data = x_train[91: 92] / 255.
label = y_train[91: 92]

parameters = [
    # 整数.画像をランダムに回転する回転範囲
    {"rotation_range": 90},
    # 浮動小数点数(横幅に対する割合).ランダムに水平シフトする範囲
    {"width_shift_range": 0.5},
    # 浮動小数点数(縦幅に対する割合).ランダムに垂直シフトする範囲
    {"height_shift_range": 0.5},
    # 浮動小数点数.シアー強度(反時計回りのシアー角度)
    {"shear_range": 90},
    # 浮動小数点数または[lower,upper].ランダムにズームする範囲.
    # 浮動小数点数が与えられた場合,[lower, upper] = [1-zoom_range, 1+zoom_range]
    {"zoom_range": 0.5},
    # 浮動小数点数.ランダムにチャンネルをシフトする範囲
    {"channel_shift_range": 0.5},
    # 真理値.水平方向に入力をランダムに反転します
    {"horizontal_flip": True},
    # 真理値.垂直方向に入力をランダムに反転します
    {"vertical_flip": True},
]

fig = plt.figure(figsize=(11, 20))
for j, kwargs in enumerate(parameters):
    datagen = ImageDataGenerator(
                            **kwargs
                        )
    # 画像の基本的な統計量を学習する
    datagen.fit(data)

    for i in range(5):
        generated_data, generated_label = datagen.flow(data, label).next()

        ax = fig.add_subplot(len(parameters), 5, i+j*5+1)
        ax.set_xticklabels([])
        ax.set_yticklabels([])
        ax.imshow(generated_data[0])
        if i == 0:
            ax.set_title(kwargs)

plt.show()

結果がこちら。
猫の画像が変換されています。

平行移動や回転した時に、元々のと比べて隙間が発生してしまいますが、
それは、 fill_mode: {“constant”, “nearest”, “reflect”, “wrap”} で指定した方法で保管されます。
(デフォルトは ‘nearest’)

ドキュメントには下記のように記載があります。
“constant”: kkkkkkkk|abcd|kkkkkkkk (cval=k)
“nearest”: aaaaaaaa|abcd|dddddddd
“reflect”: abcddcba|abcd|dcbaabcd
“wrap”: abcdabcd|abcd|abcdabcd

constant は cval で指定した定数。
nearest は境界部分の色を引き延ばした形。
reflect は境界を境に線対称。
wrap は反対側からループするようなイメージです。

kerasによる画像データ拡張

画像データを対象とする機械学習をするとき、画像に変更を加えて教師データを増やすことはとても有効な手段です。
(それで最近、numpyをつかって行列をシフトさせたり反転させたりしています。)

そして、kerasには画像データの拡張専用のツールが用意されています。
それが、ImageDataGeneratorクラスです。
今回の記事ではこれの基本的な使い方を紹介します。
非常に多くのオプションがあり、どんな変換をするか細かく指定できるのですが、
一旦今回はランダムに、最大90度画像を回転させてみます。
使うメソッドは.flowです。


from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.datasets import cifar10
import matplotlib.pyplot as plt

# データ取得
(x_train, y_train), (x_test, y_test) = cifar10.load_data()
# 例なので、データを10件に絞る
data = x_train[:10]
label = y_train[:10]
# 0~1に正規化
data = data/255.

target_name = {
        0: "airplane",
        1: "automobile",
        2: "bird",
        3: "cat",
        4: "deer",
        5: "dog",
        6: "frog",
        7: "horse",
        8: "ship",
        9: "truck",
    }

# 例として、ランダムに90度以内画像を回転させるジェネレーターを生成する。
datagen = ImageDataGenerator(
                        rotation_range=90
                    )
# 画像の基本的な統計量を学習する
datagen.fit(data)

# 変換した画像を生成する(generatorなので、for文で順次生成する。)
for generated_data, generated_label in datagen.flow(data, label):
    cnt += 1
    fig = plt.figure(figsize=(11, 5))
    # 可視化
    for i in range(10):
        ax = fig.add_subplot(2, 5, i+1)
        ax.set_xticklabels([])
        ax.set_yticklabels([])
        ax.imshow(generated_data[i])
        ax.set_title(target_name[generated_label[i, 0]])
    plt.show()

    # 無限に生成し続けるのでforを抜ける。
    break

こうして生成された画像が次です。

回転する角度はランダムなので、中途半端な傾きになっていることがわかります。
また、結果は実行するたびに変わります。