CCS機械学習講座 Part2
目次
- Kerasとは
- 多層パーセプトロンの実装
- 手書き数字画像(MNIST)の識別
- 畳み込みニューラルネットワーク(CNN)
- CNNの実装
Kerasとは
KerasとはPythonで利用できるニューラルネットワーク用のライブラリです。
(正確には裏でTensorFlowという機械学習関係のライブラリを動かしています)
多層パーセプトロンなどの実装や重みの更新処理を簡単に記述できます。
機械学習関係のライブラリは他にもたくさんありますが、この講座ではKerasを用いて実装していきます。
Kerasのドキュメント:https://keras.io/ja/
多層パーセプトロンの実装
早速多層パーセプトロンの実装をしていきます。
以下のような問題を設定する事にします。
- 2次元の点
$(x_1, x_2)$ が関数$x_2 = x_1 (x_1 + 1)(x_1 - 1)$ の上にあるか下にあるかで2クラス分類(ラベルは上が1、下が0) - 隠れ層が1つの3層構成で、誤差関数には交差エントロピーを利用
これをプログラムにすると、データの生成や結果の図示なども含め以下のようになります。
(この後各部分について説明するので、照らし合わせて下さい)
from keras.models import Sequential from keras.layers import Dense import numpy as np from sklearn.model_selection import train_test_split import matplotlib.pyplot as plt # 2つのクラスの境界線 def f(x): return 2 * x * (x + 1) * (x - 1) # データの作成(最終的に入力xとラベルtを得る) data_num = 700 # データ数 x1 = np.random.rand(data_num) * 2 - 1 # [-1, 1)からランダムな値 x2 = np.random.rand(data_num) * 2 - 1 # [-1, 1)からランダムな値 x = np.c_[x1, x2] # x1とx2の各要素について、(x1i, x2i)と連結 t = x2 >= f(x1) # 各要素についてx2 >= f(x1)ならTrue(1)、そうでないならFalse(0) # 訓練データとテストデータに分離(test_sizeはテストデータの割合) x_train, x_test, t_train, t_test = train_test_split(x, t, test_size=0.2) # 多層パーセプトロンの構築 model = Sequential() # 空のモデルを定義 model.add(Dense(5, activation="tanh", input_dim=2)) # 中間層(入力の次の層のみ入力データの次元を入力) model.add(Dense(1, activation="sigmoid")) # 出力層 # モデルの各層やパラメータ数などの情報を出力 model.summary() # 誤差関数などの設定 model.compile(loss="binary_crossentropy", optimizer="sgd", metrics=["accuracy"]) # 学習 history = model.fit(x_train, t_train, batch_size=1, epochs=50, validation_data=(x_test, t_test)) # 損失の履歴をプロット plt.plot(history.history["loss"]) plt.plot(history.history["val_loss"]) plt.title("model loss") plt.xlabel("epoch") plt.ylabel("loss") plt.legend(["loss", "val_loss"], loc="upper right") plt.grid() plt.savefig("loss.png") plt.show() # 精度の履歴をプロット plt.plot(history.history["acc"]) plt.plot(history.history["val_acc"]) plt.title("model accuracy") plt.xlabel("epoch") plt.ylabel("accuracy") plt.legend(["acc", "val_acc"], loc="lower right") plt.grid() plt.savefig("acc.png") plt.show() # テストデータを分類し描画 output = model.predict_classes(x_test) # 各入力に対しクラス番号を推定 # 推定した値で色を決めて描画 for i in range(x_test.shape[0]): if output[i] == 1: plt.plot(x_test[i, 0], x_test[i, 1], marker="o", color="r") else: plt.plot(x_test[i, 0], x_test[i, 1], marker="v", color="g") # 正解の境界線の作成 x_fig = np.arange(-1, 1.1, 0.1) y_fig = np.array(f(x_fig)) plt.plot(x_fig, y_fig) # 正解の境界線の描画 plt.show()
モデル(多層パーセプトロン)の構築
まず、モデルを構築します。Kerasではレイヤー(層)を順番に追加する事で好きなモデルを構築していきます。
重みなどは追加したレイヤーの前後関係から自動的に決定してくれるので、ユニットの数や用いる活性化関数などに注目するだけで良いです。
ここでは中間層1層の多層パーセプトロンを構築します。
入力は2次元、中間層は5次元、出力層は1次元とします。
バイアス項は自動的に組み込まれるので考える必要はありません。(バイアス項をなくす事も可能です)
from keras.models import Sequential from keras.layers import Dense # 多層パーセプトロンの構築 model = Sequential() # 空のモデルを定義 model.add(Dense(5, activation="tanh", input_dim=2)) # 中間層(入力の次の層のみ入力データの次元を入力) model.add(Dense(1, activation="sigmoid")) # 出力層 # モデルの各層やパラメータ数などの情報を出力 model.summary() # 誤差関数などの設定 model.compile(loss="binary_crossentropy", optimizer="sgd", metrics=["accuracy"])
まずmodel = Seqential()
で空のモデルを用意します。
(Seqential()
の他にModel()
という複雑な構築向きのものもあります)
そこにadd(加えたいレイヤー)
で層を追加していきます。
多層パーセプトロンを作成したいので、今回はDense
レイヤーを追加します。
Dense
レイヤーは全結合層を意味し、全てのユニットが前の層の全てのユニットに繋がっている層の事です。
Dense
レイヤーの引数の最初の数字はユニット数(出力の次元)を表します。
また、activation
には活性化関数を指定します。今回はシグモイド関数を指定しています。
また、最初に追加する層のみ入力層の次元(input_dim
)を指定する必要があります。今回は2次元なので2を指定します。
隠れ層と同様に出力層も設定します。出力層は1次元の出力なのでユニット数1、活性化関数はシグモイド関数にします。
これで2次元の入力に対し1次元の出力(確率)が得られる多層パーセプトロンが完成です。
summary
で各層が何の層であるか、パラメータ数がどれくらいかを出力できます。
目的のモデルを作れているかなどの確認に使えます。
レイヤーの追加が終わったら最後にcompile
で誤差関数などの設定を行います。
loss
は用いる誤差関数で、ここでは交差エントロピー(binary_crossentropy
)を選択しています。
(平均二乗誤差はmse
、多クラス交差エントロピーはcategorical_crossentropy
になります)
optimizer
は用いる最適化方法で、ここでは確率的勾配降下法(sgd
)を選択しています。
(他にも様々な最適化方法を選べます)
metrics
は追加する評価指標のリストで、ここでは正答率(accuracy
)を追加しています。
使用できるレイヤーや活性化関数、追加で指定できる引数などの詳細はドキュメントを適宜参照して下さい。
データの作成
入力するデータを作成します。
今回は乱数を用いて2次元の点
点の生成範囲は
import numpy as np # 2つのクラスの境界線 def f(x): return 2 * x * (x + 1) * (x - 1) # データの作成(最終的に入力xとラベルtを得る) data_num = 700 # データ数 x1 = np.random.rand(data_num) * 2 - 1 # [-1, 1)からランダムな値 x2 = np.random.rand(data_num) * 2 - 1 # [-1, 1)からランダムな値 x = np.c_[x1, x2] # x1とx2の各要素について、(x1i, x2i)と連結 t = x2 >= f(x1) # 各要素についてx2 >= f(x1)ならTrue(1)、そうでないならFalse(0) # 訓練データとテストデータに分離(test_sizeはテストデータの割合) x_train, x_test, t_train, t_test = train_test_split(x, t, test_size=0.2)
np.random.rand
は0から1の範囲で引数の数だけ乱数を生成します。(1は含まない)
なので、それにかけたり引いたりして目的の範囲のランダムな値を取得します。
np.c_
では配列の結合が行われます。
これにより
PythonだとTrue, Falseはそれぞれ計算時に1, 0で扱われるので、これでラベルが作れます。
(配列を比較すると各要素ごとに比較しTrueかFalseを返します)
作成したデータtrain_test_split
関数で訓練用のデータとテスト用のデータに分離します。
test_size
に設定した割合に従ってランダムにデータを分離します。
学習
作成したモデルとデータを利用して実際に学習(重みの更新)を行います。
# 学習 history = model.fit(x_train, t_train, batch_size=1, epochs=50, validation_data=(x_test, t_test))
学習はfit
で実行できます。引数の最初の2つは訓練用の入力データとラベルです。
batch_size
には1度にまとめて処理をするデータ数を与えます。(確率的勾配降下法でのミニバッチです)
epochs
には訓練データ全体に対し、何周学習を実行するかを与えます。
validation_data
にはテスト用の入力データとラベルを指定します。
ここにデータを与えると各エポックでのテストデータに対する誤差や精度を出力します。(汎化性能の確認)
誤差の変化などのプロット
fit
を実行すると返り値として誤差の変化などの情報が返ってきます。
これをhistory
に保存しプロットします。学習が進むにつれて誤差が減り精度が上がっている場合、上手く学習できています。
(逆に誤差が減っていない場合、上手くいってない事がわかります)
例えば、history["loss"]
で訓練データの誤差の変化を取得できます。
(オブジェクト名がhistory
なので、history.history["loss"]
となってややこしくなってますが…)
import matplotlib.pyplot as plt # 損失の履歴をプロット plt.plot(history.history["loss"]) plt.plot(history.history["val_loss"]) plt.title("model loss") plt.xlabel("epoch") plt.ylabel("loss") plt.legend(["loss", "val_loss"], loc="upper right") plt.grid() plt.savefig("loss.png") plt.show() # 精度の履歴をプロット plt.plot(history.history["acc"]) plt.plot(history.history["val_acc"]) plt.title("model accuracy") plt.xlabel("epoch") plt.ylabel("accuracy") plt.legend(["acc", "val_acc"], loc="lower right") plt.grid() plt.savefig("acc.png") plt.show()
また、テストデータに対する推定結果を実際にプロットします。
# テストデータを分類し描画 output = model.predict_classes(x_test) # 各入力に対しクラス番号を推定 # 推定した値で色を決めて描画 for i in range(x_test.shape[0]): if output[i] == 1: plt.plot(x_test[i, 0], x_test[i, 1], marker="o", color="r") else: plt.plot(x_test[i, 0], x_test[i, 1], marker="v", color="g") # 正解の境界線の作成 x_fig = np.arange(-1, 1.1, 0.1) y_fig = np.array(f(x_fig)) plt.plot(x_fig, y_fig) # 正解の境界線の描画 plt.show()
クラス番号を推定したい場合はpredict_classes
を実行します。
(回帰など、出力値自体を得たい場合はpredict
を実行します)
これらを実行すると以下のような結果が得られます。(実行するごとに多少結果は変わると思います)
誤差はだんだん減り、精度は上がっています。(精度に関しては結構ブレが生じてる気もしますが…)
また、分類結果を出力してみると、多少の失敗はありますが概ね上手く分類できているように思えます。
層数やユニット数を変えたり、データ数や境界線を変更していろいろ試してみて下さい。
(あまり上手くいかない場合もあると思いますが…)
手書き数字画像(MNIST)の識別
2次元の点を分離しても何も面白くないと思うので、今度は画像の識別を行いたいと思います。
とは言うものの、画像データが手元に無いという問題が最初にやってきます。
例えば特定アニメキャラクターの識別をしたい場合、そのキャラクターとそれ以外のキャラクターの画像がそれなりの枚数必要になります。
そのため、アニメを見てキャプチャ画像をたくさん用意するといった手間がかかります。
(しかも、顔の識別の場合は顔部分を切り取るという手間も…)
ここではそのような時間のかかる作業はしたくないので、MNISTと呼ばれるデータセットを利用して画像の識別を行います。
MNISTは手書き数字画像のデータで、0から9のいずれかが書かれた画像データとそれが何の数字であるかのラベルがまとまったものです。
画像のサイズは28*28で1チャンネル(グレースケール)となっています。
これが学習用に6万枚、テスト用に1万枚の計7万枚用意されています。
手軽に利用できるので、機械学習のチュートリアルなどによく利用されます。
他にもCIFAR10(自動車や鳥など計10クラスの画像データ)などの様々なデータセットが公開されていて、利用する事ができます。
多層パーセプトロンを使ってMNISTデータセットの分類を行ってみましょう。
10種類の数字があるので、10クラス分類を行うことになります。
プログラムの全体像は以下のようになります。
from keras.models import Sequential from keras.layers import Dense, Dropout from keras.datasets import mnist from keras.utils import np_utils import matplotlib.pyplot as plt # 多層パーセプトロンの構築 def build_multilayer_perceptron(): model = Sequential() model.add(Dense(512, activation="relu", input_shape=(784,))) model.add(Dropout(0.5)) model.add(Dense(512, activation="relu")) model.add(Dropout(0.5)) model.add(Dense(10, activation="softmax")) return model # MNISTデータの読み込み (x_train, y_train), (x_test, y_test) = mnist.load_data() # 多層パーセプロトンでの入力の形 x_train = x_train.reshape(60000, 784) x_test = x_test.reshape(10000, 784) # numpy配列へ変換し入力値を[0,1]とする x_train = x_train.astype("float32") x_test = x_test.astype("float32") x_train /= 255 x_test /= 255 # ラベルをone hot表現へ変換(例えば3は[0,0,1,0,0,0,0,0,0,0]になる) y_train = np_utils.to_categorical(y_train, 10) y_test = np_utils.to_categorical(y_test, 10) # 使うモデルや誤差関数などの設定 model = build_multilayer_perceptron() # 試すモデルを設定 model.summary() model.compile(loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"]) # 学習 history = model.fit(x_train, y_train, batch_size=128, epochs=20, validation_data=(x_test, y_test)) # 損失の履歴をプロット plt.plot(history.history["loss"]) plt.plot(history.history["val_loss"]) plt.title("model loss") plt.xlabel("epoch") plt.ylabel("loss") plt.legend(["loss", "val_loss"], loc="upper right") plt.grid() plt.savefig("loss.png") plt.show() # 精度の履歴をプロット plt.plot(history.history["acc"]) plt.plot(history.history["val_acc"]) plt.title("model accuracy") plt.xlabel("epoch") plt.ylabel("accuracy") plt.legend(["acc", "val_acc"], loc="lower right") plt.grid() plt.savefig("acc.png") plt.show()
Kerasではmnist.load_data
関数を実行するだけでデータの画素値とラベルを取得する事ができます。
(他のデータセットについてはドキュメントを参照)
また、画像データは28*28の行列で表されますが、多層パーセプトロンはベクトルの形で入力をするのでそれに合わせて784次元のベクトル(1次元配列)へ変換します。
(単に1行に並べているだけで、すごく乱暴な感じがしますがここではそうします)
input_shapeは整数またはタプルを渡せて、784と(784,)は等価らしいですが、整数を渡したらエラーになってしまった…
なので、タプルの形で指定しています。(謎だ…)
また、画素値は0から255ですが、大きな値の入力は学習に影響を与えるので255で割って入力の範囲を0から1にしておきます。
シグモイド関数の形を見ると、入力値が大きくなった時に微分値が非常に小さくなる事が分かります。
また、誤差逆伝播法では活性化関数の微分値をかける処理が加わります。
この事から、大きな値を入力すると学習が進まない(重みの更新がほとんど行われない)可能性が生じてきます。(勾配消失問題)
そのため、このような処理を行っています。
ReLUの場合、大きな値を入れても微分値は1で固定ですが、更新時に負に振り切れると死にます。
(負になると微分値が0になり学習されない)
今回は10クラス分類なので、出力は10次元になります。また、教師データとしてone hot表現のものを用意する必要があります。
ラベルをone hot表現に変換する関数(np_utils.to_categorical(ラベル, クラス数)
)があるのでそれを利用します。
今回、多層パーセプトロンは中間層を2つにし、それぞれ512次元にします。
また、中間層の活性化関数はReLUを使用し、出力層にはソフトマックス関数を用います。
誤差関数には多クラス交差エントロピー、最適化方法にはAdamと呼ばれるものを使用します。
また、今回はドロップアウトという手法も利用しています。
ドロップアウトとは、ニューラルネットワークの学習時に、特定の層のユニットをランダムに無効化する手法の事を言います。
このような処理を行う事で、過学習を防ぎ精度の向上を期待する事ができます。
疑似的に複数の判定器を使っている(アンサンブル学習)ように見る事ができるようです。
実行すると、以下のような結果が得られました。
約98%と、非常に高い精度を得る事ができました。
これでも十分高いように見えますが、さらに上を目指してみたくなります。(なりませんか?)
畳み込みニューラルネットワーク(CNN)
手書き文字画像の識別を行いましたが、多層パーセプトロンの場合28*28の画像を784次元のベクトルへ変換してしまいました。
(手書き文字画像に関してはこれでも割と上手くいきますが…)
しかし、これだと入力画像の形状が同じでも少しずれただけで全然違うベクトルになってしまいます。
これは良いとは言い難いですし、できればデータの構造(縦方向や横方向の関係性など)を反映させたいです。
ここでは画像認識の分野で優れた性能を示している畳み込みニューラルネットワーク(CNN)の概要について説明します。
画像関係の分野では様々なモデルが提案されていますが、ほとんどこれ関係なイメージがあります。
CNNの他には再帰的ニューラルネットワーク(RNN)などがあります。
CNNは多層パーセプトロンと異なり、入力データは画像の形(行列)になります。
CNNでは基本的に以下の流れで処理が行われます。
- 入力データに畳み込みを行い活性化関数をかける
- 何回か畳み込んだ後にプーリングを行う
- 1と2を1セットとして何回か繰り返した後ベクトルの形へ変換
- 全結合層を用いて分類
以下、これらの処理について説明します。
畳み込みでは、特定サイズのフィルタを用意し以下のアニメーションのようにフィルタをずらしながら計算を行います。
出典:https://github.com/vdumoulin/conv_arithmetic (下の画像もここが出典です)
数式で書くと以下のようになります。
位置
基本的にはフィルタは1マスずつずらしていきますが、多めにずらす場合もあります。
このずらす値の事をストライドと言います、この式の場合はストライド=1です。
注意点としては、フィルタサイズとストライドによりますが畳み込み処理を行うと画像サイズが少し小さくなります。
そこで、画像サイズを小さくしすぎないために入力の周囲に0の要素を追加するパディングと呼ばれる処理が行われる事があります。
プーリングでは以下のように一定範囲から最も値の大きいものを選択するという処理をします。
(正確にはMAXプーリングで、他にも一定範囲の平均値をとったりもします)
少し位置がずれてもプーリング結果はあまり変化しない事から、位置の変化に強くなる効果があると言われています。
出典:https://deepage.net/deep_learning/2016/11/07/convolutional_neural_network.html
これらの処理を何回か行った後、ベクトルの形に変換し多層パーセプトロンと同様の全結合層を用いて分類を行います。
これが基本的なCNNの流れになります。
重みの更新などは多層パーセプトロンと同様に最急降下法やその類で行われます。
式の導出は…多層パーセプトロンの時より地獄なので今回はスルーしましょう(遠い目)
有名なCNNの例としては2014年のILSVRC(大規模画像認識の大会)で提案されたVGG16という1000クラス分類のモデルがあります。
これは13層の畳み込み層(その間にプーリング層が5層)と3層の全結合層から構成されています。
CNNの実装
CNNの概要について説明したので、Kerasを使って実装してみましょう。
データセットは先ほど紹介したMNISTを利用します。
プログラムの全体像は以下のようになります。
大部分は多層パーセプトロンの時と同じなので、そこの説明は省きます。
from keras.models import Sequential from keras.layers import Dense, Dropout, Flatten from keras.layers import Conv2D, MaxPooling2D from keras.datasets import mnist from keras.utils import np_utils import matplotlib.pyplot as plt # CNNの構築 def build_cnn(): model = Sequential() model.add(Conv2D(30, (5, 5), activation="relu", input_shape=(28, 28, 1))) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Flatten()) model.add(Dense(100, activation="relu")) model.add(Dense(10, activation="softmax")) return model # CNN(層の深いCNN)の構築 def build_deep_cnn(): model = Sequential() model.add(Conv2D(16, (3, 3), padding="same", activation="relu", input_shape=(28, 28, 1))) model.add(Conv2D(16, (3, 3), padding="same", activation="relu")) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Conv2D(32, (3, 3), padding="same", activation="relu")) model.add(Conv2D(32, (3, 3), padding="same", activation="relu")) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Conv2D(64, (3, 3), padding="same", activation="relu")) model.add(Conv2D(64, (3, 3), padding="same", activation="relu")) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Flatten()) model.add(Dense(50, activation="relu")) model.add(Dropout(0.5)) model.add(Dense(10, activation="softmax")) return model # MNISTデータの読み込み (x_train, y_train), (x_test, y_test) = mnist.load_data() # CNNでの入力の形 x_train = x_train.reshape(60000, 28, 28, 1) x_test = x_test.reshape(10000, 28, 28, 1) # numpy配列へ変換し入力値を[0,1]とする x_train = x_train.astype("float32") x_test = x_test.astype("float32") x_train /= 255 x_test /= 255 # ラベルをone hot表現へ変換(例えば3は[0,0,1,0,0,0,0,0,0,0]になる) y_train = np_utils.to_categorical(y_train, 10) y_test = np_utils.to_categorical(y_test, 10) # 使うモデルや誤差関数などの設定 model = build_cnn() # 試すモデルを設定 model.summary() model.compile(loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"]) # 学習 history = model.fit(x_train, y_train, batch_size=128, epochs=20, validation_data=(x_test, y_test)) # 損失の履歴をプロット plt.plot(history.history["loss"]) plt.plot(history.history["val_loss"]) plt.title("model loss") plt.xlabel("epoch") plt.ylabel("loss") plt.legend(["loss", "val_loss"], loc="upper right") plt.grid() plt.savefig("loss.png") plt.show() # 精度の履歴をプロット plt.plot(history.history["acc"]) plt.plot(history.history["val_acc"]) plt.title("model accuracy") plt.xlabel("epoch") plt.ylabel("accuracy") plt.legend(["acc", "val_acc"], loc="lower right") plt.grid() plt.savefig("acc.png") plt.show()
今回は2種類のCNNを作成して比較してみます。
片方は畳み込み層が1層のみ、もう片方は畳み込み層が6層と多めになっています。
また、適宜プーリング層を挟んでいます。
畳み込み層はConv2D
で追加できます。引数の最初の値はチャンネル数(フィルタ数)、2つ目はフィルタサイズになります。
padding
は"same"
か"valid"
が選べ、"same"
を選ぶと入力と出力のサイズが同じになるようにパディングされます。
プーリング層はMaxPooling2D
で追加できます。pool_size
にはどのくらいの範囲から最大値を選ぶかを指定します。
ここでは2*2の範囲から最大値を選ぶ事にします。
Flatten
では入力の形をベクトルにする処理が行われます。
誤差関数の設定などは多層パーセプトロンの時と同様です。
それでは実際に実行して比較してみましょう。
上が畳み込み層1層のみのCNN、下が畳み込み層6層のCNNの結果です。
いずれも多層パーセプトロンの時に比べて精度が向上している事が分かります。
また、畳み込み層の多いものは精度が99%を超えていて、優れた結果になっている事が分かります。
とは言うものの、この精度の背景にはMNISTが分類問題のデータセットとしては非常に簡単なものであるという側面もあります。
他のデータセットや一般の問題に対してこのレベルの精度が得られる事はまず無いでしょう…
(MNISTが簡単すぎるという理由(?)からFashion-MNISTという服などの画像版MNISTが生まれたりもしています)
チャンネル数を変えてみたり、層の数を変えてみたり、データセットを変更したり、いろいろ試してみて下さい。