zuminote

個人的な勉強記録

PythonとKerasによるディープラーニング 第2章 メモ

第2章 予習:ニューラルネットワークの数学的要素

ここでは例として、定番のMNISTの画像分類モデルの学習を行うとともに、ディープラーニングの重み調整アルゴリズムの詳細(確率的勾配降下法とか)について説明しています。

github.com

MNIST画像分類

MNISTデータの概要

mnist.load_data()でダウンロードしてきたデータを確認。

shapeをみると、train_imagesは(60000, 28, 28), test_imagesは(10000, 28, 28)となっており、28×28ピクセルの画像がそれぞれ6万枚、1万枚含まれている。それぞれに、どの数字が書かれているかの正解ラベルが付与されている。

ラベルの形式は、array([5, 0, 4, ..., 5, 6, 8], dtype=uint8) のように、1次元配列の中に整数で、当該画像がなんの数字を表しているかが格納されている。

ネットワーク構造

model = keras.Sequential([
    layers.Dense(512, activation="relu"),
    layers.Dense(10, activation="softmax")
])

2層の全結合層(Dense)のみ。

全結合層は一言でいうと、「出力y」=「入力X」×「重み」+「バイアス」の関数である。この線形変換の出力を活性化関数に入力し、その出力を次の層にどんどん渡していく。

後者の層は10個のユニットを持つソフトマックス層である。

ソフトマックス層は、合計すると1になる10個の確率スコアが含まれた配列を返す。つまり、どの数字である確率が高いかを出力する。

コンパイル

model.compile(optimizer="rmsprop",
              loss="sparse_categorical_crossentropy",
              metrics=["accuracy"])

ここではモデルについて、以下の3つの構成要素を選択する。 * 損失関数:1章で見たとおり、正解と予測値の間のずれを観測し、重みをどう調整するかの手がかりとするためのもの。 * オプティマイザ:与えられたデータと損失関数に基づいてネットワークが自身を更新するメカニズム。 * 訓練とテストを監視するための指標:ここでは正解率のみを考慮。

コンパイルってなんやねんとよく思うけれど、ざっくり言うと、これら3つになんの手法を使うのか決定して、モデル作成!ってすることだと思っておけばよいのだと思う。

前処理

train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype("float32") / 255
test_images = test_images.reshape((10000, 28 * 28))
test_images = test_images.astype("float32") / 255

ここでは、ネットワークに入力するためにデータ形状をととのえ、スケーリングする。

これによって、train_imagesは(60000, 784), test_imagesは(10000, 784)に整形され、画素値は255で割ってスケーリングすることにより0〜1の間の値になっている。

また、型もuint8の整数値からfloat32に変換されている。(255で割るため)

で、たかだかMNISTチュートリアルでも私はこういう形状変換処理のイメージの理解に戸惑うので、図化するとこんな感じ。

(Qiitaとかだったらちゃんとパワポとかで作るけどここは雑記帳なのでひどい図だけど許してね)

f:id:zuminote:20210807190826j:plain
データのreshapeイメージ

ラベルのone-hotエンコーディングについて

で……ここからが書籍とサンプルコードで記述に違いがあり、若干惑わされたところだった。

よく、ディープラーニング初心者向けの解説だと「モデルに入力するために、ラベルはone-hotエンコーディングしましょう!」という説明がなされているように思うが、kerasは(PyTorchにもあるんですかね?)one-hotエンコーディングしていないラベル向けのsparse_categorical_crossentropyという損失関数があるのである。それを使うと、one-hotエンコーディングしなくても学習できてしまう。サンプルコード版はこのsparse_categorical_crossentropyを使っており、ラベルのone-hotエンコーディングなしで学習をおこなっていた。

書籍では以下のように、ラベルのone-hotエンコーディングが必要と書いてある。この場合は、損失関数にcategorical_crossentropyを使わないといけない。

from tensorflow.keras.utils import to_categorical

train_labels_onehot = to_categorical(train_labels)
test_labels_onehot = to_categorical(test_labels)

試しにsparse_categorical_crossentropyを指定したモデルにone-hot化したラベルを与えたところ、以下のようなエラーが発生した。

InvalidArgumentError:  logits and labels must have the same first dimension, got logits shape [128,10] and labels shape [1280]

実は業務で作っているモデルでもこのような怒られが発生して詰まっていたので、なかなかタイムリーだった…

https://minus9d.hatenablog.com/entry/2020/10/25/193018

初めて知ったのですが、logitsとは「ソフトマックス活性化関数に通す前のニューラルネットワークの出力」のことです。

このエラー曰く、「logitsとラベルの1次元目は同じにしてよね!!受け取ったもん見てみたらlogitsは(128, 10)で、ラベルが(1280)になっちゃってるんですけど!?」ということみたいです。

個人的には、ラベルをone-hotで与えている(形状は(ミニバッチ数, カテゴリ数)になっているはず)にもかかわらず、ラベルが(1280)になっている、と言われるのが納得できなかったんですが……。

ここまでの全体像をまとめるとこんな感じ。(誰に謝ってるのか不明だけどひどい画像でほんとごめん)

f:id:zuminote:20210807194636j:plain

ニューラルネットワークの数学的要素

で、本題の数学的要素についてざざっと見ていく。

しかしぶっちゃけるとこの本の説明はあんまり親切じゃない気がするので、このあたりはゼロつくで補足したほうが良いと思った。

最適化・バックプロパゲーションについて

まず、training ループでは以下のことが行われている。

  1. 訓練データxと対応する目的値yをバッチデータとして抽出する
  2. ネットワークをxで実行し、予測値y_predを取得する。(フォワードパス
  3. 損失関数により、このバッチでのlossを計算する。lossは、予測値y_predと正解(目的値)yの不一致の目安となる指標。
  4. このバッチでのlossが小さくなるように、ネットワークのすべての重みを更新する。

この手順4.重みの更新を行う時、どうするか? 単純な方法としては、1つ1つの重みについて、他の重みを凍結させた状態で値を変化させてlossの変化を確認し、lossが小さくなったものを採用する…というフォワードパスでゴリ押しの力技がある。しかしこんな面倒なことはしていられない…

そこで、「ネットワークで使用される演算がすべて微分可能であること」を利用して、ネットワークの係数に関する損失関数の勾配を計算する。

勾配から、損失関数が小さくなる方向(勾配が負であればxを大きくする、勾配が正であればxを小さくする)に重みを調整すればよい。

確率的勾配降下法SGD

以上踏まえ、trainingループで起きていることについて、上記手順4.を詳しく書いてあげるとこんな感じになる。

  1. 訓練データxと対応する目的値yをバッチデータとして抽出する
  2. ネットワークをxで実行し、予測値y_predを取得する。(フォワードパス
  3. 損失関数により、このバッチでのlossを計算する。lossは、予測値y_predと正解(目的値)yの不一致の目安となる指標。
  4. ネットワークのパラメータを調整するために損失関数の勾配を計算する。(バックワードパス)
  5. ネットワークのパラメータを、勾配とは逆方向に少し移動させる(たとえばW -= step * gradient)ことで、このバッチでの損失値を少し小さくする。

この一連の流れが、「ミニバッチ確率的勾配降下法」である。

「確率的」というのは、各データパッチがランダムに抽出されることを表す。

真の確率的勾配降下法は、イテレーションごとにサンプルと目的値を1つだけ抽出する(オンライン学習)。また、全データを使って実行することもできる(バッチ学習)。これらの折衷案がミニバッチ学習。

モーメンタム

SGDの弱点は、局所解に陥ってしまうことがあるという点。

ボールを転がすようなイメージで、大域的最小値を探す。モーメンタムはボールの加速度みたいな感じ。モーメンタムが十分であればボールは局所解のくぼみでは止まらずに、大域的最小値で止まる。

(ゼロつく読もう!)

バックプロパゲーション誤差逆伝播法)

ニューラルネットワークの関数は、それぞれ単純な微分可能な関数をつなぎ合わせたものであり、数学的には、「連鎖律」と呼ばれる恒等式微分可能。

ニューラルネットワークの勾配値の計算に連鎖律を適用したのがバックプロパゲーション

最終的な損失値を出発点として、出力側の層から入力側の層に向かって逆方向に進む。連鎖律を適用し、各パラメータがその損失値にどのような影響を与えたのか計算する。

(もうこの辺の説明はぶっちゃけよくわからんかったので、ゼロつくを読んだほうが良い…。)

所感

MNISTとかいまさら真剣に振り返ることもないっしょwwwと高をくくっていたが、sparse_categorical_crossentropyとcategorical_crossentropyの部分に関しては確認できてよかった。

誤差逆伝播法とかそのあたりに関してはこれまで何回も「完全に理解した」と思っては「あれ、そもそもどういうことだっけ……」というのを繰り返してきているので……こうやって記事にしておくのはそれなりに意義があるはず!(この記事でもややフンワリさせてしまっているので、繰り返し定着させていこう…)