Keras: Fashion-MNISTを使ってCNNを可視化する
Fahion-MNISTのデータを使って学習したニューラルネットワークの畳み込み層を可視化します。
Fashion-MNIST
Fashion-MNISTは衣料品の画像を10クラス (Coat, Shirtなど) に分類するデータセットです。MNISTと同じく、学習サンプル数60,000・テストサンプル数10,000で、各画像は28x28のグレースケールとなっています。Kerasに付属されていますので、簡単に利用できます。
モデルの実装と可視化
今回使うパッケージたちです。
import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns import keras from keras.datasets import fashion_mnist from keras.models import Model, Sequential from keras.layers import Dense, Dropout, Flatten from keras.layers import Conv2D, AveragePooling2D, MaxPooling2D from sklearn.metrics import confusion_matrix
Fashion-MNISTのデータのロード
fashion_mnist.load_data()でFashion-MNISTの画像とラベルをロードできます。
- 各画像は28ピクセルx28ピクセルで、各ピクセルは0〜255のグレースケール画像です
- 0〜9のラベルとなっており、0が"T-shirt/top"、1が"Trouser"、...というようになってます
# 画像とラベルのデータのロード (X_train_image, Y_train_label), (X_test_image, Y_test_label) = fashion_mnist.load_data() print(X_train_image.shape, Y_train_label.shape, X_test_image.shape, Y_test_label.shape) # ((60000, 28, 28), (60000,), (10000, 28, 28), (10000,)) # 値域を確認 print(np.min(X_train_image), np.max(X_train_image), np.min(X_test_image), np.max(X_test_image)) # (0, 255, 0, 255) # ラベルに偏りが無いことを確認 print(np.unique(Y_train_label, return_counts=True)) # (array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=uint8), array([6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000])) print(np.unique(Y_test_label, return_counts=True)) # (array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=uint8), array([1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000])) # ラベルデータをセット # https://github.com/zalandoresearch/fashion-mnist#labels label_map = pd.Series([ 'T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat', 'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot' ]) # 36枚を表示 plt.figure(figsize=(10,10)) for i in range(36): plt.subplot(6, 6, i+1) plt.xticks([]) plt.yticks([]) plt.grid(False) plt.imshow(X_train_image[i]) plt.xlabel(label_map[Y_train_label[i]])
学習サンプルの一部を表示してみると、こんな感じです。サンダルやバッグなども含まれていること、人が見てもコート・シャツ・セーター (pullover) の区別が難しそうなことなどがわかるかと思います。
LeNet-5で実装・学習
ベースとなるモデルとして、LeNet-5でニューラルネットワークを実装・学習します。以前の投稿も参考にしてみてください。
- X -> Conv -> AveragePooling -> Conv -> AveragePooling -> FC -> FC -> FC -> softmax -> y という簡単な構造です
- 活性化関数には"relu"を使っています
# 0.0〜1.0へ正規化 X_train = X_train_image.astype('float32') X_test = X_test_image.astype('float32') X_train /= 255 X_test /= 255 # チャネルの追加 X_train = X_train.reshape(X_train.shape + (1,)) X_test = X_test.reshape(X_test.shape + (1,)) # one-hotエンコーディング num_labels = label_map.size Y_train = keras.utils.to_categorical(Y_train_label, num_labels) Y_test = keras.utils.to_categorical(Y_test_label, num_labels) # モデルを作る input_shape = (X_train.shape[1], X_train.shape[2], 1) model = Sequential() model.add(Conv2D(6, kernel_size=(5, 5), strides=(1, 1), padding='same', activation='relu', input_shape=input_shape)) model.add(AveragePooling2D((2, 2), strides=(2, 2))) model.add(Conv2D(16, kernel_size=(5, 5), strides=(1, 1), padding='valid', activation='relu')) model.add(AveragePooling2D((2, 2), strides=(2, 2))) model.add(Flatten()) model.add(Dense(120, activation='relu')) model.add(Dense(84, activation='relu')) model.add(Dense(num_labels, activation='softmax')) model.compile( loss=keras.losses.categorical_crossentropy, optimizer=keras.optimizers.Adadelta(), metrics=['accuracy'] ) # 学習 epochs = 50 batch_size = 1000 history = model.fit( x=X_train,y=Y_train, epochs=epochs, batch_size=batch_size, validation_data=(X_test, Y_test), verbose=1 )
損失関数の値の推移を見ると、20エポックあたりから学習データとテストデータで差が広がり始めており、過学習の傾向が見られます。
畳み込み層の可視化
各層の出力と重みについて可視化してみます。
出力の可視化
畳み込み層のみを抽出したモデルを作り、画像データ (ここではテストサンプル) を入力させます。
# 畳み込み層のみを抽出 conv_layers = [l.output for l in model.layers[:4]] conv_model = Model(inputs=model.inputs, outputs=conv_layers) # 畳み込み層の出力を取得 conv_outputs = conv_model.predict(X_test) for i in range(len(conv_outputs)): print(f'layer {i}:{conv_outputs[i].shape}') # layer 0:(10000, 28, 28, 6) # layer 1:(10000, 14, 14, 6) # layer 2:(10000, 10, 10, 16) # layer 3:(10000, 5, 5, 16)
例えば、以下のスニーカーの画像の各層の出力を見ます。
def plot_conv_outputs(outputs): filters = outputs.shape[2] for i in range(filters): plt.subplot(filters/6 + 1, 6, i+1) plt.xticks([]) plt.yticks([]) plt.xlabel(f'filter {i}') plt.imshow(outputs[:,:,i]) # 1層目 (Conv2D) plot_conv_outputs(conv_outputs[0][0]) # 2層目 (AveragePooling2D) plot_conv_outputs(conv_outputs[1][0]) # 3層目 (Conv2D) plot_conv_outputs(conv_outputs[2][0]) # 4層目 (AveragePooling2D) plot_conv_outputs(conv_outputs[3][0])
- 1層目のConv2Dは、輪郭抽出の働きをしていると思われます
- 3層目のConv2Dは、解釈が難しいですが、画像の各パーツ (領域) の抽出に役立っていそうです
- 2層目・3層目でAveragePoolingが挟まり、だんだん粗く・ぼやけた出力となっていってます
重みの可視化
次に重みを可視化します。
- 各レイヤからget_weights()メソッドを使うことで重みを得られます
- フィルタの重みとバイアスがタプルで返されます
def plot_conv_weights(filters): filter_num = filters.shape[3] for i in range(filter_num): plt.subplot(filter_num/6 + 1, 6, i+1) plt.xticks([]) plt.yticks([]) plt.xlabel(f'filter {i}') plt.imshow(filters[:, :, 0, i]) # 1層目 (Conv2D) plot_conv_weights(model.layers[0].get_weights()[0])
解釈が難しいですが、1層目の重みを見ると全体的にぼやけていることから正則化が必要かな、ということろでしょうか。
改善
重みが少しノイジーでしたので、各層にDropoutを加えることで正則化効果を狙ってみます。また、どちらかというとより一般的なMaxPooling2Dに切り替えてみます。
new_model = Sequential() new_model.add(Conv2D(6, kernel_size=(5, 5), strides=(1, 1), padding='same', activation='relu', input_shape=input_shape)) new_model.add(MaxPooling2D((2, 2), strides=(2, 2))) new_model.add(Dropout(0.2)) new_model.add(Conv2D(16, kernel_size=(5, 5), strides=(1, 1), padding='valid', activation='relu')) new_model.add(MaxPooling2D((2, 2), strides=(2, 2))) new_model.add(Dropout(0.2)) new_model.add(Flatten()) new_model.add(Dense(120, activation='relu')) new_model.add(Dropout(0.3)) new_model.add(Dense(84, activation='relu')) new_model.add(Dense(num_labels, activation='softmax')) new_model.compile( loss=keras.losses.categorical_crossentropy, optimizer=keras.optimizers.Adadelta(), metrics=['accuracy'] )
損失関数の値は0.25を下回ってきました (精度は90%程度) が、こんどは適合不足の傾向が見られました。
1層目 (Conv2D) の重みですが、もう少し白黒をはっきりすることを期待したのですが、今ひとつ違いがわからないというところです。
2層目と5層目 (いずれもMaxPooling2D) の出力ですが、輪郭をはっきり捉えられていそうです。
更に改善を進めてみます。
適合不足の傾向があり、また、各層の出力でも意味のある (真っ黒や真っ白が無い) になっていそうでしたので、今度は各層のフィルタの数を増やして、表現力を強めてみます。
- 第1層と第4層の畳み込み層のフィルタをそれぞれ2倍にします
new_model = Sequential() new_model.add(Conv2D(12, kernel_size=(5, 5), strides=(1, 1), padding='same', activation='relu', input_shape=input_shape)) new_model.add(MaxPooling2D((2, 2), strides=(2, 2))) new_model.add(Dropout(0.2)) new_model.add(Conv2D(32, kernel_size=(5, 5), strides=(1, 1), padding='valid', activation='relu')) new_model.add(MaxPooling2D((2, 2), strides=(2, 2))) new_model.add(Dropout(0.2)) new_model.add(Flatten()) new_model.add(Dense(120, activation='relu')) new_model.add(Dropout(0.3)) new_model.add(Dense(84, activation='relu')) new_model.add(Dense(num_labels, activation='softmax')) new_model.compile( loss=keras.losses.categorical_crossentropy, optimizer=keras.optimizers.Adadelta(), metrics=['accuracy'] )
テストサンプルの損失関数の値は0.21 (精度は92%) を切ってきています。
第5層 (2つ目のMaxPooling2D) の出力を見ると、いずれのフィルタの出力も強く反応している (白色) 箇所があるため、意味のあるものになっているようです。
まとめ
Fashion-MNISTを例に、CNNの可視化と、それを使った改善をやってみました。精度で90%から92%への小さな改善となりました。