05.04 微分

「ゼロから作るDeep Learning」でいうとP97「数値微分」という章に入ってきました。
微分」と聞いただけで「やべぇー」って声が出そうな感じですが、一応できるところまで進んでみましょう。今回やることを数式で見ると、こんな感じで ぞっとします。

f x x = lim h 0 f x + h - f x h

そもそも微分って何でしょう。それが分かれば上の数式も分かるかも!?
大体高校で習う微分や積分というのは公式ばかりで、どういう意味を持って何に使うかとかいうことは二の次なので、こういう時に非常に困りものです。でもこの「ゼロから作るDeep Learning」を読んでいってわかったことは「微分」というのは「『ある瞬間』の変化量」らしいのです。

例えばいつも図書館で勉強した結果をHPに書いているのですが、図書館まで車で15分かかります。時速30Kmで走ってくると仮定すると距離は

  30Km/h × 0.25h = 7.5Km

いうことになります。逆に

   7.5Km ÷ 0.25h = 30Km/h

で速度が求まります。でも実際はどうかというと最初は止まっているし途中信号があれば止まりますし、前に車があれば減速、車が一台もいなければ加速する場合もあります。こうした途中の段階は全く無視して一定(均質という)の速さで移動してきたと仮定して1時間当たりの距離を計算しています。でも実際は全然違って車の速度は一定ではありません(不均質という)。では、運行状況を詳しく知りたいときにはどうすればよいか?
例えば時間を3つに区切り0~5,6~10、11~15分各々5分間ずつに分割するというのはどうでしょうか。最初と最後の5分が1.66Km、途中5分が4.16Kmを進んだとすると

    1.67 ÷ 0.0833 ≒ 20
    4.17 ÷ 0.0833 ≒ 50

最初と最後が20Km/h、途中が50Km/hの速度で走ってきたことになります。今5分間隔にしましたが、1分だったらとか1秒だったらとか、どんどん小さくしていったらどうでしょう。1分でも1秒でも時間に換算してしまえばすべて同じ単位の速度となるのです。

では、出発してからt分後の自動車の距離をy=f(t)で表した場合、x分経過した時に目的地に着くとf(x)が距離、平均速度はv = f(x) / xとなります。ではそこからh分だけ進んだ時の速度はどうかというと


v= f x + h - f x h
この式どこかで見覚えないですか?
前に出てきた式です。そしてこれを、「微分」の英語differentialの先頭を取って「 f x x 」と表記し「ディーエフエックス ディエックス」と呼びます。そしてこの表記はxの変化に対する関数f(x)がどれだけ変化するかを表しています。これを極限まで小さくしていくという意味で「 lim h 0 」という記号がついているわけです。 そう考えてみると車の事例で考えると「瞬間的速度」になりますが、ニューラルネットワークの場合何になるかというと例えば非線形曲線の「傾き=接線」を求めるために必要なのではないかということが薄々わかってきたましたね。

★ なぜ微分なのか?
そもそもなぜ微分を使わないといけないかを考えてみましょう。
GGEも「ゼロから作るDeep Learning」を見ながら書いているため専門に勉強したい方はHPではなく「ゼロから作るDeep Learning」で勉強された方が良いかも。間違ったこと書いているかもしれませんからね。

原点に立ち返って考えると、今何が問題で何をしようとしているかというと、

最適な重みとバイアスを探そうとしている
         ↓
損失関数の値ができるだけ小さなものを探そうとしている
         ↓
損失関数の結果に対しパラメータを変化させた時の反応を見ようとしている

GGEは昔船関係の大学で航海専門ではありませんでしたが、実習で一度だけ大型実習船に乗った時に小型船舶免許を持っていたために船長の好意で瀬戸内から大阪をへて和歌山に抜ける水道を操船できることになりましたが、車のように直ぐに舵が効かないので「後方の船影を見てみ」とか言われ見るとジグザグコースでした。上のケースも同じで少しずつ舵を切り様子を見て右転舵、悪ければ左転舵といったように目標まで進めていく試みなのです。

この行為ができるのは船と同じで「動いていなければ成り立たない」ということも言えます。それはニューラルネットワークも同じで連続的な動きをしないと学習は成り立たないのです。活性化関数のシグモイド関数が描く軌跡の場合、微分=接線は常に変化しています。ところがステップ関数はどうでしょう。ステップ関数は0→1の一瞬を除いて変化は常に0なために変更しても結果は出てこないことになります。

ニューラルネットワークは
連続的に変化し、傾きが0にならないもの
でなけれんば学習はできない。

これを大前提として
一瞬の変化する傾き(微分)により損失関数の値が小さな値を探る

そのために、これから微分を行うことになります。


【ステップ関数とシグモイド関数計算結果グラフ】


★ Pythonに実装
では下記数式に見る数値微分(numerical differentiation)を実装してみることにしましょう。

f x x = lim h 0 f x + h - f x h

前にもお話ししたように微分というのは、プログラム化した時にはそんなに難しいわけではありません。それは実際のプログラムを見てもらえばわかります。要するに式そのものなのです!

def numerical_diff_ng(f, x):
  h = 1e-50
  return (f(x+h) - f(x)) / h

def numerical_diff_ok(f, x):
  h = 1e-4
  return (f(x+h) - f(x-h)) / (2*h)


但しここで2つの関数が書いてあるのはコンピュータ上の落とし穴というものがあってnumerical_diff_ng()のままでは実際にはNGで問題点が2つあるからなのです(とはいえ自分ではその違いが判らなかったので「ゼロから作るDeep Learning」の通りですが)。

① 丸め誤差
下記のように「32bit浮動小数点float32型で見ると1e-50は0になってしまうのです」と書いてあり実際やってみるとそうなのですが、少々愚痴です。C言語は最初に倣うデータ型で必ずと言っていいほど構成しているビット数がかいてあるものです。というのも、このようなデータに使用制限があるからです。ところがPythonのリファレンスマニュアルでは構成ビットが書いてあるものは、ほとんど見たことがないわりに暗黙のうちに下記のような制限が出てきています。 どういうこっちゃ!?
でもかなり詳しく書いてあるリファレンスを見ると、どうやらPythonは単精度浮動小数点を標準で使用しているようです。要するにC言語でいえばfloatが標準で使用されれているということです。このため「ゼロから作るDeep Learning」の筆者は「np.float32()」で値を見ているということらしいですね。テストの結果「1e-10」位から結果は出てきますが「ある程度小さな値」ということで、「1e-4」を余裕見て使用しているようです。

>>> import numpy as np
>>> np.float32(1e-50)
0.0
>>> np.float32(1e-40)
1e-40



② 中心差分
この改善点は「ゼロから作るDeep Learning」を見なければわからなかった点ですが、車の事例のように5分間隔で計測した速度というのは実際の速度とは一致しません.下記のような曲線上の1点における接線はxと(x+h)とで近似した接線との間に差があります。hを小さくするというのも手ですが、前述の丸目があり限界があります。そこで「ゼロから作るDeep Learning」の筆者は(x + h)と(x - h)との差分の平均を取る方法を採用しています。これを中心差分といいます(平均なので分母に2かけている点要注意です)。一方前述のようなxと(x+h)を前方差分といいます。


【数値微分による中央差分方式近似接線】


★ 数値微分
では次に示す2次関数を実装し、X=5の微分を計算します。
y = 0.01 x 2 + 0.1 x

ここで学校で習う公式が活躍です。上記の式を微分するには、確かこうだったから
x n = n x n-1

結果は、 こんな感じになります。
f x x = 0.02 x + 0.1

GGEは既に頭がオーバーヒート寸前冷やすにはウィスキーの水割りが必要です。 もう少しガンバロ! では次にx=5の場合は、

 0.02 × 0.5 + 0.1 = 0.2

ではプログラムを作成して値を見てみましょう。サンプルコードはch04/gradient_1d.pyとなりますので用意してください。これはすんなり動きますので修正は必要ありません。では動かしてみましょう。

# coding: utf-8
import numpy as np
import matplotlib.pylab as plt

def numerical_diff(f, x):
  h = 1e-4 # 0.0001
  return (f(x+h) - f(x-h)) / (2*h)

def function_1(x):
  return 0.01*x**2 + 0.1*x

def tangent_line(f, x):
  d = numerical_diff(f, x)
  print(d)
  y = f(x) - d*x
  return lambda t: d*t + y

x = np.arange(0.0, 20.0, 0.1)
y = function_1(x)
plt.xlabel("x")
plt.ylabel("f(x)")

tf = tangent_line(function_1, 5)
y2 = tf(x)

plt.plot(x, y)
plt.plot(x, y2)
plt.show()


グラフとともにコンソールにnumerical_diffの結果が出ていますので見てみると「0.1999999」です。前述の計算値「0.2」と誤差内で一致していることが分かり、またグラフも接線となっていることが分かります。


【数値微分による接線近似事例】



【プログラムワンポイントアドバイス】
① lambda(ラムダ)って何やねん?
「ゼロから作るDeep Learning」においても何の解説もないので少々調べて解説を載せておきます。
シェルで試していただけると直ぐに分かるのですが、単純に言うとメソッドはdef  xxx:のように名前を定義しないといけませんが、lambdaは名前なしで処理を作成するための関数です。この事例の場合関数tangent_line(f, x)が呼ばれた戻り値は値ではなく引数が必要な関数だということです。このため代入されたtfは値が入っているのではなく関数である為使用する時にtf(x)=(d * t + y)のように引数が必要になります。


② 0.01*x**2
多分ご存知だとは思いますが、これは階乗(xの2乗)を示します。

>>> def add_def(a,b=1):
>>>  return a+b
>>> add_lambda = lambda a , b=1 : a+b
>>> print(add_def(3,4))
7
>>> print(add_def(3))
4
>>> print(add_lambda(3,4))
7
>>> print(add_lambda(3))
4


05.05 三次元プロット

「ゼロから作るDeep Learning」でいうとP102「偏微分」という章に入ってきました。
偏微分の章へ行き説明する前に今までの2次元ではなく下記のような3次元のグラフが出てきます。

f x 0 x 1 = x 0 2 + x 1 2

ところが「ゼロから作るDeep Learning」はというと残念なことにグラフは表示れていますが、どうやって角かのサンプルがないので、電脳倶楽部ではまずグラフを書いてから進んでいきます。
ここで使用するのはmpl_toolkitsというモジュールのAxes3Dというものを使用します。大体は次元が1つ増えるのでarangeを1つ増やして下記のような手順を踏むと表示できますが、Axes3Dで3Dにした時にビューポイントやアングルが設定していないなという疑問があったのですが表示してびっくり! これって図形の上でマウスでつまんで動かすとビューポイントが自由に選べる仕掛けになっています。そのまま書出しもできる優れものでした。
 
# coding: utf-8
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
import matplotlib.pylab as plt

#引数の2乗和を計算する関数を例として考える
def func1(x, y):
return x**2 + y**2

#描写データの作成
# 3次元で描写するには2次元メッシュが必要
# 2次元配列をarangeを用いて作る
# x, y をそれぞれ1次元領域で分割する
x0 = np.arange(-3, 3.0, 0.25)
x1 = np.arange(-3, 3.0, 0.25)

#2次元メッシュはmeshgridでつくる
# Xの行にxの行列を,Yは列にyの配列を入れたものになっている
X, Y = np.meshgrid(x0, x1)
Z = func1(X, Y)

#グラフの作成
# figureで2次元の図を生成する
# その後,Axes3D関数で3次元にする
fig = plt.figure()
ax = Axes3D(fig)

#軸ラベルの設定
ax.set_xlabel("x0")
ax.set_ylabel("x1")
ax.set_zlabel("f(x)")

#グラフ描写
ax.plot_wireframe(X, Y, Z)
plt.show()



f x 0 x 1 = x 0 2 + x 1 2 グラフ】



05.06 偏微分

前述でグラフ化した下記の式における微分はどうでしょうか。
変数が2つ(X0,X1)ありますでの、どうしましょう?

f x 0 x 1 = x 0 2 + x 1 2

もし1つなら前回と全く同じことになります。であれば!もう一方を固定して、どれを微分するか指定してやれば前と同じ状態になりませんか。これをx0で微分する、x1で微分するというのを数式で書き表すと、「 f x 0 」「 f x 1 」と表記します。

プログラム的にはやって調べるまでもなく x0**2 + x1**2が元の式なので、
f x 0 」の場合は、x0**2 + 2**2

f x 1 」の場合は、2**2 + x1**2

のようになるだけなので、あえてここではサンプルは載せません。

【参考文献】
辻真吾著「Python スタートブック ~いちばんやさしいパイソンの本~」技術評論社

斎藤康毅著「ゼロから作るDeep Learning」O'REILLY オライリー・ジャパン

瀬山士郎著「頭にしみこむ微分積分」技術評論社


≪メインフレーム・目次が表示されない場合はここをクリックしてください≫