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 は反対側からループするようなイメージです。

scikit-imageで画像データの読み書き

常にデータセットの画像データばかり使うのも飽きてくるので、そのほかの画像ファイルをpythonで読み込む方法を調べました。
いくつか方法があるようですが、anacondaに付いてきた、scikit-imageというライブラリが以前から気になっていたので今回はこれを使います。

ドキュメント: scikit-image

どうやら、 scikit-image.io.img_data で読み込めそうです。

試しに画像を読み込んでみて、numpy配列で読み込めたことを確認しました。
また、ついでにimsaveで別名をつけて保存しています。


from skimage import io
# 画像の読み込み
img_data = io.imread("./sample.jpg")
# 縦*横*チャンネル数のnumpy配列で読み込まれていることを確認。
print(type(img_data))
# 
print(img_data.shape)
# (1066, 1600, 3)

# 別名で保存
io.imsave("./sample_2.png", img_data)

このほか、 as_gray オプションで白黒画像としても読み込めました。
配列の次元数も違えば値の範囲も違うので扱いは要注意です。


# 白黒で読み込み
gray_image_data = io.imread("./sample.jpg", as_gray=True)
# チャンネルがなくなり、2次元のデータになる。
print(gray_image_data.shape)
# (1066, 1600)

# カラーで読み込むと、0〜255の整数値
print(img_data.max())
# 255
# 白黒で読み込むと、0.0〜1.0の浮動小数
print(gray_image_data.max())
# 0.9910556862745099

(最大値が中途半端なのは読み込んだ画像によるものです。)

これで、画像を扱うモデルを作ったら任意の画像で試せそうですね。
(あとはサイズの加工とか必要かな。)

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

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

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

numpy.flipで配列の反転

以前、numpy配列の要素をシフトさせるroll関数を紹介しましたが、
numpyにはこのほか、配列を反転させる、flipという関数も用意されています。
とはいえ、スライス “::-1″で実現できるので、普段はあまりありがたみもないのですが、
画像データの集合などの4次元の配列など、次元が高くなってくるとスライスで書くのは面倒になるので、便利な場面もありそうです。
(スライスで左右反転を書くと ary[:, :, ::-1, :] のようになり、可読性低いので。)

ドキュメント: numpy.flip
引数のaxisで、反転させる次元を指定するのですが、
axis=0(行列では縦の反転),とaxis=1(行列では横の反転) に対応した、
numpy.flipud と、numpy.fliplrという関数もあります。

ついでに紹介しておくと、90度回転させる numpy.rot90というのもあります。


import numpy as np
# サンプルの配列を作成
ary = np.arange(12).reshape(3, 4)
print(ary)
"""
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
"""

# 縦の反転
print(np.flip(ary, axis=0))
print(np.flipud(ary))
print(ary[::-1, :])
# 3つとも同じ出力
"""
[[ 8  9 10 11]
 [ 4  5  6  7]
 [ 0  1  2  3]]
"""

# 横の反転
print(np.flip(ary, axis=1))
print(np.fliplr(ary))
print(ary[:, ::-1])
# 3つとも同じ出力
"""
[[ 3  2  1  0]
 [ 7  6  5  4]
 [11 10  9  8]]
"""

# 90度回転
print(np.rot90(ary))
"""
[[ 3  7 11]
 [ 2  6 10]
 [ 1  5  9]
 [ 0  4  8]]
"""

CIFAR-10データセットの紹介

先日、Fashion-MNISTを触ってみたばかりなのですが、最近はカラー画像のセットであるCIFAR-10も試しています。
このデータセットには、次の10種類のカラー画像が含まれています。
飛行機/車/鳥/猫/鹿/犬/蛙/馬/船/トラック

大元の配布元はこちらのようです。
The CIFAR-10 dataset

ただ、これも kerasのデータセットに含まれていて、とても手軽に扱うことができます。
ドキュメント: CIFAR10 画像分類
(画像の種類数がさらに多い、CIFAR-100というのもあります)

今回もとりあえず読み込んで表示してみます。


from tensorflow.keras.datasets import cifar10
import matplotlib.pyplot as plt

(x_train, y_train), (x_test, y_test) = cifar10.load_data()

# Fashion-MNIST とは 配列の型が違うので注意
print("x_train.shape: ", x_train.shape)
print("y_train.shape: ", y_train.shape)
print("x_test.shape: ", x_test.shape)
print("y_test.shape: ", y_test.shape)
"""
x_train.shape:  (50000, 32, 32, 3)
y_train.shape:  (50000, 1)
x_test.shape:  (10000, 32, 32, 3)
y_test.shape:  (10000, 1)
"""
# 0〜9が なんの画像かの対応はdatasetに含まれないので別途作る
target_name = {
        0: "airplane",
        1: "automobile",
        2: "bird",
        3: "cat",
        4: "deer",
        5: "dog",
        6: "frog",
        7: "horse",
        8: "ship",
        9: "truck",
    }

fig = plt.figure(figsize=(15, 18), facecolor="w")
for i in range(100):
    ax = fig.add_subplot(10, 10, i+1)
    ax.set_xticklabels([])
    ax.set_yticklabels([])
    ax.imshow(x_train[y_train.ravel() == i // 10][i % 10], cmap="gray_r", vmin=0, vmax=255)
    if i % 10 == 0:
        # アイテムの最初の画像にタイトルつける
        ax.set_title(target_name[i//10])

plt.show()

結果がこちら。

numpy配列の中身をシフトさせる

pandasデータフレームにおけるshift操作のようなものを行いたくて調べたのでそのメモです。
numpyか、aryにshiftメソッドがあると勝手に決めつけていたのですが実際は無く、代わりにnumpy.rollというのを使います。

ドキュメント: numpy.roll

元々行いたかったのは、配列の要素を左右(2多次元配列の場合はそれぞれの軸方向にも)ずらして、欠損値になるところにはNoneか何か入るイメージだったのですが、
numpy.rollを使うと、名前の通り、回転させるような動きをします。
(例を見る方がわかりやすいです)

numpy.rollは次の引数を取ります。
a: 回転させる配列
shift: 回転させる幅 (値かタプル)
axis: 回転させる方向 (値かタプル。デフォルトはNone)

まず、1次元配列でやってみます。 1次元しかないのでaxisは意味がなく、shift幅だけ指定してます。


import numpy as np
# 元のデータ作成
ary1 = np.arange(7)
print(ary1)
# [0 1 2 3 4 5 6]

print(np.roll(ary1, 2))
# [5 6 0 1 2 3 4]

print(np.roll(ary1, -3))
# [3 4 5 6 0 1 2]

左右が繋がって回転してるようにずれていますね。

次に2次元です。
axisが未指定(もしくはNoneを渡す)場合と、それぞれの軸の方向を指定した場合で挙動が違うのでやってみます。


ary2 = np.arange(30).reshape(5, 6)
print(ary2)
"""
[[ 0  1  2  3  4]
 [ 5  6  7  8  9]
 [10 11 12 13 14]
 [15 16 17 18 19]]
"""

# axis未指定で、行列全体で回転。左上と右下が繋がる。
print(np.roll(ary2, 2))
"""
[[28 29  0  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]]
"""

# axis=1 で行ごとに回転。
print(np.roll(ary2, 2, axis=1))
"""
[[ 4  5  0  1  2  3]
 [10 11  6  7  8  9]
 [16 17 12 13 14 15]
 [22 23 18 19 20 21]
 [28 29 24 25 26 27]]
"""

# axis=0 で列ごとに回転。
print(np.roll(ary2, 2, axis=0))
"""
[[18 19 20 21 22 23]
 [24 25 26 27 28 29]
 [ 0  1  2  3  4  5]
 [ 6  7  8  9 10 11]
 [12 13 14 15 16 17]]
"""

重複のある配列の要素を順序を保ったまま一意化する

業務でタイトルの処理が必要になり、スマートなやり方を探したのでそのメモです。

まずおさらいですが、配列をユニーク化するだけなら集合に変換して戻せば完成します。


sample_list = ['b', 'c', 'c', 'd', 'a', 'b', 'd', 'e', 'a', 'b', ]
print(list(set(sample_list)))
# ['c', 'e', 'a', 'd', 'b']

この時、単に一意化するのではなく、元々の配列で最初の方に出てきた要素から順番に取り出したい、という要件がありました。
この時、昨日紹介した、配列のindexという関数が使えます。
要は元々の配列で何番目に登場していたかをこの関数でえて、その順番で並べかえれば良いです。
そして、ありがたいことに、sortedや、list.sort関数が、keyという
引数を取ってくれます。
keyに引数を一つとる関数を渡すと、各要素をその関数に適用させた結果で並べ替えてくれます。

それぞれやってみます。


sample_list = ['b', 'c', 'c', 'd', 'a', 'b', 'd', 'e', 'a', 'b', ]

# list.sortを使う方法
# 一度集合に変換して、ユニーク化
sorted_list = list(set(sample_list))
# 元々のインデックスでソート
sorted_list.sort(key=sample_list.index)
print(sorted_list)
# ['b', 'c', 'd', 'a', 'e']

# sortedを使う方法
sorted_list = sorted(set(sample_list), key=sample_list.index)
print(sorted_list)
# ['b', 'c', 'd', 'a', 'e']

sorted の方は、 戻り値は配列型なので、list()でキャストする必要はありません。
どちらかというとこちらの書き方の方がスマートだと思います。

Pythonの配列に含まれる特定の要素のindexを取得する

よく使っているnumpyのarrayではなく、Pythonデフォルトの配列(list)の話です。
だいたいいつもnumpyのarrayの方が便利なのですが、listにしかないメソッドもいくつかあります。

その一つがlist.index(x[, start[, end]])です。
これを使うと、listの中から、特定の要素(x)を探して、最初に現れたインデックスを得ることができます。
また、startやendを指定して、特定の区間から探すこともできます。


# サンプルの配列を準備する
sample_list = ['b', 'c', 'a', 'd', 'a', 'b', 'd', 'b', 'a', 'b', ]

# 最初に現れるdのindex
print(sample_list.index('d'))
# 3

# 次に現れるdは 3+1 = 4番目以降を探す
print(sample_list.index('d', 4))
# 6

配列内や、検索区間内に目当ての要素が存在しない場合はValueErrorが出ます。


sample_list.index('e')
"""
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
 in ()
----> 1 sample_list.index('e')

ValueError: 'e' is not in list
"""

ちなみに、普段はenumerateを使って次のようにして算出することが多いです。


print([i for i, value in enumerate(sample_list) if value == 'd'])
# [3, 6]

ただ、ご覧の通りコードが少し長くなるので、先頭の要素だけでいい時などはindexが手軽そうです。

Fashion-MNIST データセットの紹介

最近受けている講座の中で、kerasに同梱されているデータセットの中に、Fashion-MNISTという画像データがあることを知りました。
画像データを使って機械学習を試す時(と言っても、自分が画像データを扱うことはほぼないのですが)は、
大抵手書き数字のMNISTデータを使っていて、いささか飽きていたので、早速これを確認してみました。

ドキュメントはこちら。
Fashion-MNIST database of fashion articles
手書き数字のMNISTと完全互換らしく、クラス数は10個、画像サイズは28*28、データ件数も訓練データ60,000とテストデータ10,000のデータになっています。

読み込みもほぼ同じです。

今回はデータセットの紹介なので、読み込んで実際に画像を表示してみるところまでやってみます。


from tensorflow.keras.datasets import fashion_mnist
import matplotlib.pyplot as plt

(x_train, y_train), (x_test, y_test) = fashion_mnist.load_data()
print("x_train.shape: ", x_train.shape)
print("y_train.shape: ", y_train.shape)
print("x_test.shape: ", x_test.shape)
print("y_test.shape: ", y_test.shape)
"""
x_train.shape:  (60000, 28, 28)
y_train.shape:  (60000,)
x_test.shape:  (10000, 28, 28)
y_test.shape:  (10000,)
"""
# 0〜9が どんなアイテムに対応しているかの対応はdatasetに含まれないので別途作る
target_name = {
        0: "T-shirt/top",
        1: "Trouser",
        2: "Pullover",
        3: "Dress",
        4: "Coat",
        5: "Sandal",
        6: "Shirt",
        7: "Sneaker",
        8: "Bag",
        9: "Ankle boot",
    }

fig = plt.figure(figsize=(10, 13), facecolor="w")
for i in range(100):
    ax = fig.add_subplot(10, 10, i+1)
    ax.set_xticklabels([])
    ax.set_yticklabels([])
    ax.imshow(x_train[y_train == i // 10][i % 10], cmap="gray_r")
    if i % 10 == 0:
        # アイテムの最初の画像にタイトルつける
        ax.set_title(target_name[i//10])

plt.show()

結果として表示される画像がこちらです。

手書き数字のMNISTよりちょっと楽しそうなデータですね。
そして結構難易度たかそうです。