本記事は、学習機能を伴ったニューラルネットワークを作成し、学習を行い、失敗するところまでを取り扱います。
失敗したものを記事にするのはどうかとも思ったのですが、個人的な記録としては価値あるものなのであえて書くことにします。
今回の内容は前回の続きとなりますので、まだそちらをご覧で無ければ前回記事からお読みいただければ幸いです。
記事構成
- データの準備(前回記事)
- モデルの定義
- 学習の実行
- 失敗まとめ
- 画像の判定実験
前回のまとめ(1. データの準備)
前回の記事では、画像認識に使用する画像データを用意しました。
単にデータを用意すると言っても、画像データそのままではニューラルネットワークに読み込ませることはできません。機械学習を行うためにはいくつかの前処理が必要となります。
簡単におさらいすると、次のようなことを行なったのでした。
- MNISTデータの呼び出し
- 画像データの標準化
- 正解データの行列化
- 訓練データの抽出
詳しくは、前回記事を参照してください。
実装
2. モデルの定義
ここではニューラルネットワークのモデルを定義します。
また、モデルで使用するいくつかの関数についてもここで実装を行います。
必要な関数は次の通りです。
- シグモイド関数
- ソフトマックス関数
- 交差エントロピー誤差関数
- 勾配取得関数
モデル本体には、次のような機能をもたせます。
- モデルの初期化関数(___init___)
モデルを初めて呼び出した時に、パラメータをランダムな値で初期化する。 - パラメータの形状確認関数(printParamsShape)
設定されたパラメータのshapeを確認する。必須ではない。 - 予測値出力関数(predict)
現在設定されているパラメータに基づき、予測値を出力する。 - 損失取得関数(get_loss)
現在設定されているパラメータに基づき取得された予測値と正解データを比較し、損失を出力する。 - 勾配取得関数(call_get_gradients)
現在設定されているパラメータについて、損失関数に対する勾配を取得する。
では、上記のような仕様に基づいて関数及びモデルの実装を行います。
関数群及びモデルの定義
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
import numpy as np # 活性化関数:シグモイドの実装 def sigmoid(x): return 1 / (1 + np.exp(-x)) # 活性化関数:ソフトマックスの実装 def softmax(x): exp_x = np.exp(x) sum_exp_x = np.sum(exp_x) y = exp_x / sum_exp_x return y # 損失関数:交差エントロピー誤差 def cross_entropy_error(y, t): delta = 1e-7 ##計算不能に陥ることを避けるための微小な値 batch_size = 1 ##渡されたパラメータが多次元であった場合、誤差の総和を次元数で除算する if y.ndim != 1: batch_size = y.shape[0] return - np.sum(t * np.log(y)) / batch_size # 勾配の取得関数1 (1次元のパラメータ用) def get_gradients_1d(f, x): ## 勾配の入れ物リスト gradients = np.zeros_like(x) ## 微小な差分を定義する極限の値。 0.0001 h = 1e-4 for i in range(x.size): ## 前方向の極限値の算出 tmp = x[i] x[i] = tmp + h ahead_val = f(x) ## 後ろ方向の極限値の算出 x[i] = tmp - h back_val = f(x) gradients[i] = (ahead_val - back_val) / (2 * h) x[i] = tmp return gradients # 勾配の取得関数2 (多次元のパラメータ用) def get_gradients(f, x): ## パラメータが1次元であれば、そのまま勾配を計算する if x.ndim == 1 : return get_gradients_1d(f, x) ## パラメータが多次元であれば、各次元について勾配を計算する else : gradients = np.zeros_like(x) for i, x in enumerate(x): gradients[i] = get_gradients_1d(f, x) return gradients |
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# ニューラル・ネットワークの定義 class threeLayerNetwork: ## 初期化関数。パラメータをランダムな値で初期化する。パラメータは後で更新されるので、初期値はなんでも良い。 def __init__(self, input_col_length, ##入力値の列数 node1_col_length, ##一つ目の隠れ層のノード数 node2_col_length, ##二つ目の隠れ層のノード数 output_col_length, ##出力層の列数 weight_init_std = 0.01): self.params = {} ### 重み W ### 値は上述の通りなんでも良いが、shapeは前の層からの入力値と次の層への出力値を意識しなければいけない。 self.params['W1'] = weight_init_std * np.random.randn(input_col_length, node1_col_length) self.params['W2'] = weight_init_std * np.random.randn(node1_col_length, node2_col_length) self.params['W3'] = weight_init_std * np.random.randn(node2_col_length, output_col_length) ### バイアス b self.params['b1'] = np.random.randn(node1_col_length) self.params['b2'] = np.random.randn(node2_col_length) self.params['b3'] = np.random.randn(output_col_length) # 設定したパラメータの形状を確認する def printParamsShape(self): for key in self.params: print(key + ' : {}'.format(self.params[key].shape)) # 現在のモデルにおける出力 y を算出して返却する ## x : 入力値 def predict(self, x): ## 現在設定されているパラメータの取り出し W1, W2, W3 = self.params['W1'], self.params['W2'], self.params['W3'] b1, b2, b3 = self.params['b1'], self.params['b2'], self.params['b3'] ## 入力層→1層目 ### 重みとバイアスの適用 val1_a = np.dot(x, W1) + b1 ### 活性化 val1_b = sigmoid(val1_a) ## 1層目→2層目 val2_a = np.dot(val1_b, W2) + b2 val2_b = sigmoid(val2_a) ## 2層目→3層目 val3_a = np.dot(val2_b, W3) + b3 y = softmax(val3_a) ## 3層目の出力をモデルの出力として返却する return y # 損失関数を算出する def get_loss(self, x, t): y = self.predict(x) ## 出力と正解を比較し、損失を求める return cross_entropy_error(y, t) # 現在のパラメータにおける勾配を取得する ## x : 画像データ ## t : 正解データ def call_get_gradients(self, x, t): ## 勾配を求めるために、損失関数を変数に格納する ## 損失値ではなく関数そのものを渡したいため、ラムダ式を使用する loss_L = lambda L: self.get_loss(x, t) ## 勾配の入れ物 gradients = {} ## 現在のパラメータにおける勾配を、各パラメータについて取得する gradients['W1'] = get_gradients(loss_L, self.params['W1']) gradients['W2'] = get_gradients(loss_L, self.params['W2']) gradients['W3'] = get_gradients(loss_L, self.params['W3']) gradients['b1'] = get_gradients(loss_L, self.params['b1']) gradients['b2'] = get_gradients(loss_L, self.params['b2']) gradients['b3'] = get_gradients(loss_L, self.params['b3']) return gradients |
3. 学習の実行(失敗)
ここまでで、使用する画像データの準備とモデルの定義が終了しました。
いよいよ、学習を実行していきましょう。
今回の学習仕様は次の通りとします。
- 学習回数は 200 回とする。
- 一度の学習で使用するデータは、全データから無作為に抽出した 100 件とする。
- 学習率(一度の学習でパラメータを更新する割合)は 0.1 とする。
つまり今回は、パラメータを200回更新することでモデルを最適化するという過程をとります。
では、学習処理を実装していきましょう。
予め述べておくと、ここでの学習は失敗しました。
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# 学習の準備と実行 ## 各パラメータにおける損失の値を保存するリスト loss_list = [] ## 学習に必要な各設定 iters_num = 200 # 学習の実行回数 batch_size = 100 # 一度の学習で処理対象にするデータ数 learning_late = 0.1 # 学習時に更新するパラメータの更新率 train_row_size = x_train.shape[0] # 読み込むデータの総行数 train_col_size = x_train.shape[1] # 読み込むデータの総列数 ## 3層ニューラルネットワークの生成 network = threeLayerNetwork(input_col_length = train_col_size, node1_col_length = 10, node2_col_length = 10, output_col_length = 10) print("入力データのshape : {}".format(x_train.shape)) network.printParamsShape() # 指定回数、学習を行う for i in range(iters_num): # 一度の学習における処理数に基づき、元データから対象データをランダムにピックアップする batch_mask = np.random.choice(train_row_size, batch_size) x_batch = x_train[batch_mask] t_batch = t_train[batch_mask] # 現在のパラメータにおける勾配の取得 current_gradients = network.call_get_gradients(x_batch, t_batch) # パラメータの更新 for key in ('W1','W2','W3', 'b1', 'b2','b3'): ## 求められた勾配に学習率を乗算し、現在のパラメータを勾配の逆方向へと更新する upd_grad = current_gradients[key] * learning_late network.params[key] -= upd_grad # 損失値の保存 loss = network.get_loss(x_batch, t_batch) loss_list.append(loss) if i % 10 == 0: print("進行率...{}".format(i / iters_num * 100) + "%") print("現在の損失...{}".format(loss)) plt.plot(loss_list) plt.show() |
先ほど定義したモデルを呼び出し、指定回数の学習を行なっています。
順調に行けば学習のたびに損失関数が減少していくはずなのですが、今回はここで問題が発生しました。
下の図が、実行時の損失の推移です。損失が思うように減少していないことがわかります。
問題の発生
ここではおきた問題をまとめると、つぎのようになります。
- 損失の減少が緩やかすぎる。
- 学習に時間がかかりすぎる。
- 損失を減少させるのに十分な学習数になっていない
まず 2 ですが、今回はテスト用として学習数 200 を設定しています。が、この実行だけでも私のPCでは30分程度を要しました。これで結果が出るならば良いのですが、それにしてもあまりに時間がかかっています。
そして1です。損失は少しずつ減少しているのですが、あまりに緩やかで、モデルを最適化できているとは言い難いものです。恐らくこれは、層の数に対して学習数が不足していることに起因するものと思われます。(上記プログラムをベースに2層のネットワークも構築してみましたが、そちらはもう少しまともな結果が出ました)
問題の原因
今回は3層のネットワークですが、それでも学習数 200 というのは不足であり、学習数をもっと増やさなければいけないということが判明しました。
ですが、私のPCのスペック的にはこれ以上学習数を増やすと、結果を出すまでに数日かかってしまいます。従って、方法を変えるしかありません。
では、今回の処理に時間を要している原因は、勾配を計算する処理にあります。
勾配を求めるに当たって、対象の変数の極限の変化率を求めているわけですが、その処理を全パラメータに対して行なっているために非常に時間がかかるということになっています。
問題の解決
上記のように勾配を一つ一つ計算すると時間がかかってしまうことがわかりました。
それを解決する方法が、誤差逆伝播という勾配計算方法です。
誤差逆伝播と言う技術を用いると、一つ一つの勾配計算にかかる時間が大幅に短縮されます。処理に時間がかからなければ学習数を増やすこともできるので、結果的にモデルを最適化することができるわけです。
この方法の詳細についてはいずれ記事にまとめることとして、まずはこの技術を使用してモデルを作り直してみたいと思います。
が今回の記事はすでに長くなってしまいましたので、次回の記事でモデルの再定義を行なっていきたいと思います。
今回のまとめ
- 学習を行うためには、画像データ、使用する関数、ベースになるネットワークの定義が必要
- 層の大きさに応じて、パラメータの計算には時間がかかるようになる
- 勾配を一つ一つ計算すると、モデルを最適化するのに非常に時間がかかる
最後に
今回の失敗の原因は学習数不足にあると断定しましたが、他の原因にお気づきになられた方がいましたら、教えていただけると嬉しいです。
以上