yikegaya’s blog

仕事関連(Webエンジニア)と資産運用について書いてます

「ゼロから作るDeep Learning」を読み返した

一度ざっと読んだものの理解できず本棚に放り込んだままになってたオライリーの「ゼロから作るDeep Learning」を読み返した。

文系卒で微分や行列計算などは習っていないけどその知識は必要そうなので高校生向けの本をざっと読んでおいた。

数学知識は付け焼き刃なんでまだ理解できてないところも正直あるんだけど、勉強がてら全体の流れをざっくり整理して書いてみる。

内容

なるべくライブラリを使わずディープラーニングで画像分類器を作るという内容。

同じく機械学習の本でこの間読んだ「自然言語処理アプリケーション開発入門」だとKerasというGoogleが作ったTensorflowという機械学習のラッパーライブラリを使ってディープラーニングを実装していたので実践的ではあったもののブラックボックスだった部分は多かった。この本はその部分の理解を助けてくれるような内容。

画像の扱いについて

ディープラーニングの実行には入力データを数字の配列(多次元配列)として渡さないといけない。

自然言語処理の場合は単語を辞書に登録したり出現頻度調べたりベクトル化のために手間がかかったけど今回扱う画像データの場合はそもそも縦、横、チャンネルの多次元配列の構造になっているのでその点はあまり気にする必要がない。

パーセプトロン

まず基本となるのがパーセプトロンという入力値に重みをかけて0か1の数字を出力するアルゴリズム

この本ではパーセプトロンを使ってAND、OR、XORなど2進数計算からなる論理回路の実装が説明される。

パーセプトロンでのAND回路実装例

def AND(x1, x2):
  w1, w2, theta = 0.5, 0.5, 0.7
  tmp = x2*w1 + x2*w2
  if tmp <= theta:
    return 0
  elif tmp > theta:
    return 1

これにバイアスを足して重みを変えるとORやNAND回路も作れる。

XOR(入力が[0, 0]か[1, 1]の時に0を出力してそれ以外は1を出力する回路)の場合はANDのように単純に実装はできないけど、パーセプトロンを多層にすることで実装ができる。

def XOR(x1, x2):
  s1 = NAND(x1, x2)
  s2 = OR(x1, x2)
  y = AND(x1, x2)
  return y

このように層を複数重ねたパーセプトロンを多層パーセプトロンという。多層パーセプトロンを使うと複雑な問題も解決でき理論上コンピュータそのものも作ることができる。

ニューラルネットワーク

パーセプトロンは複雑な問題も解決できるが、重みを人の手で設定しなければならない。ニューラルネットワークではこの重みを設定する作業をデータから自動で学習させることができる。(パーセプトロンニューラルネットワークの定義は記事や本によってまちまちな気もする)

活性化関数

活性化関数とは「閾値を境として出力が切り替わる関数」のことで「パーセプトロンの場合はステップ関数を実行している」と説明できる。

ステップ関数

ステップ関数の単純な実装

def step_function(x):
  if x > 0:
    return 1
  else:
    return 0

シグモイド関数

ニューラルネットワークではこの活性化関数にステップ関数以外の関数を使う。パーセプトロンニューラルネットワークの違いはこの活性化関数のみ。ニューラルネットワークでよく使われる活性化関数の一つにシグモイド関数がある。

numpy(np)を使った実装例

def sigmoid(x):
  return 1 / (1 + np.exp(-x))

exp関数は例えばexp(1)とすると2.71828183、exp(2)とすると7.3890561を返す。

ネイピア数と指数関数 │ Python 数値計算入門

ステップ関数とシグモイド関数の入力値をx軸、出力値をy軸としてグラフで描画するとステップ関数は0を境に急速に出力が変わるのに対してシグモイド関数は滑らかな曲線を描く。

また、ステップ関数は0か1を出力するのに対してシグモイド関数は「0.731...」、「0.881...」などの実数を返す。

ReLU関数

シグモイド関数ニューラルネットワークの歴史で古くから使われてきたが、最近ではReLU関数が使われることが多い。 ReLU関数は入力値が0以上ならそれをそのまま出力し0以下なら0を出力する関数

def relu(x):
  return np.maximum(0, x)

シグモイド関数を使ったニューラルネットワークの実装例

# indentity_functionは仮置き
def indentity_function(x):
  return x

def sigmoid(x):
  return 1 / (1 + np.exp(-x))

def init_network:
  network = {}
  network['W1'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
  network['b1'] = np.array([[0.1, 0.2, 0.3])
  network['W2'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
  network['b2'] = np.array([0.1, 0.2])
  network['W3'] = np.array([[0.1, 0.3], [0.2, 0.4]])
  network['W3'] = np.array([0.1, 0.2])
  
  return network

def foword(network, x):
  W1, W2, W3 = network['W1'], network['W2'], network['W3']
  b1, b2, b3 = network['b1'], network['b2'], network['b3']

  a1 = np.dot(x, W1) + b1
  z1 = sigmoid(a1)
  a2 = np.dot(z1, W2) + b2
  z2= sigmoid(a2)
  a3 = np.dot(z2, W3) + b3
  y = identity_function(a3)

  return y

network = init_network()
x = np.array([1.0, 0.5])
y = forward(network, x)
print(y)

init_networkで重みを初期化。その後「np.dot」でnumpyを使って行列計算を実行し入力値に対して重みとバイアスをかけていく。 その結果を活性化関数(この場合はシグモイド関数)に与えて重みを調整する。

それを繰り返して最適な重みを出力する。

indentity_functionは出力層の活性化関数でこの例では入力をそのまま出力するのみ。出力層の活性化関数については後述。

出力層の設計

入力値に重みとバイアスをかけて結果を出力し、その結果を活性化関数が受け取って重みを何層かに渡って調整する。その後、結果をシステムの目的に応じて選択された関数に与えて出力する必要がある。

上記のindentity_functionのような入力をそのまま返す関数は恒等関数と呼ばれる

ニューラルネットワークはも回帰問題にも分類問題にも使うことができ、回帰問題の場合は恒等関数。分類問題の場合はソフトマックス関数が使われる。

回帰問題とは連続値などの値の予測のことで売り上げ、株価予測のシステムで採用されたりする。

今回の画像識別は分類問題になるのでソフトマックス関数を実装する。

ソフトマックス関数

def softmax(a):
  exp_a = np.exp(a)
  sum_exp_a = np.sum(exp_a)
  y = exp_a / sum_exp_a
  
  return y

softmax関数の使用例

>>> a = np.array([0.3, 2.9, 4.0])
>>> y = softmax(a)
>>> print(y)
[0.01821127, 0.24519181, 0.73659691]
>> np.sum(y)
1.0

このようにソフトマックス関数は0から1の間の実数値を出力する。このおかげでソフトマックス関数の出力値は「確率」として解釈できる。

この例では3番目のクラスである確率が最も高く約73%なので3番目のクラスに分類される。など

ニューラルネットワークの学習

上記の処理で重みを調整することができるが、その重みが正しいか判定するために損失関数を使う。

ニューラルネットワークの学習では、ある「ひとつの指標」を手がかりにパラメータを探索する。ニューラルネットワークの学習で用いられる指標は「損失関数」と呼ばれる。

この損失関数は、任意の関数を用いることができるが、一般には二乗和誤差や交差エントロピー誤差などが用いられる。どっちも正解データとニューネットワークの出力結果の誤差を判定できる関数

機械学習の問題は、「訓練データに対する損失関数を求め、その値をできるだけ小さくするようなパラメータを探し出す」ということ。

なぜ認識精度ではなく損失関数を指標にするのかというと、パラメータの微分がほとんどの場所で0になってしまい、パラメータの更新ができなくなってしまうから。

重みのパラメータを少しだけ変化させたときに正解にどれだけ近づいているのかを判定したいので、ある瞬間の変化の量を表す数値微分を使いたいが、そのためには損失関数を指標にする必要がある。

誤差逆伝播

重みパラメータに対する勾配を数値微分ではなく「誤差逆伝播法」という手法で求めると計算の効率が良くなる。で、この誤差逆伝播法について1章分割かれてるけど正直わからん。多分微分についてちゃんと理解してリベンジしないと無理そうなので誤差逆伝播法の詳細については略。

CNN

画像認識や音声認識など至る所で使われる畳み込みニューラルネットワーク(CNN)というニューラルネットワークのモデルがある。 これまで見てきたニューラルネットワークと同様でレゴブロックのようにレイヤを組み合わせて作ることができるが、新たに「Convolutionレイヤ(畳み込み層)」と「Poolingレイヤ(プーリング層)」が登場する。

これまで見てきたニューラルネットワークでは全結合層(Afflineレイヤー)を用いていた。全結合層はデータの形状が無視されてしまう問題がある。入力データが画像の場合は縦、横、チャンネル方向の3次元の形状だが、全結合層に入力するときには3次元のデータを平らな1次元にする必要がある。

空間的に近いピクセルは似たような値であったり、RGBの各チャンネル間にはそれぞれ密接な関わりがあったり、距離の離れたピクセル同士はあまり関わりがなかったりなど3次元の形状には汲み取るべき本質的なパターンがあるが、全結合層ではそれが無視されてしまう。

一方、畳み込み層(Convolutionレイヤ)は形状を維持することができる。

その後のプーリング層は縦横方向の空間を小さくする。識別に不必要な特徴量をそぎ落として識別に必要な特徴量を抽出する。

その他

GPUとかディープラーニングの歴史、応用事例の話題もあったけどこの記事書いたのはディープラーニングを使って画像分類器を実装する方法を整理したかったからなんでその辺りは略

終わりに

活性化関数や損失関数についてはこういったユースケースではこの関数を使うとうまい結果が得られるよ。。という知識は得られたけどどうしてその関数だとうまくいくのか理解がぼんやりしている気はする。数式とコードが詳しく記載されてるんだけどちょくちょく読めない数学記号とかがあって辛い。この辺り理解するにはやっぱり数学ちゃんとやった後にまた読み返さないとダメそう。

数字ベクトルを入力⇨重みとベクトルをかける⇨活性化関数で出力を調整する⇨損失関数で重みを調整して最適な重みを導く。。という仕組みだったり精度を高めるための手法にどんなものがあるのかざっくり知れたんでとりあえずいいや