プロクラシスト

今日の寄り道 明日の近道

【Day-16】ニューラルネットを0から作り、仕組みを基礎から理解する


スポンサーリンク

f:id:imslotter:20171216170643p:plain

データ分析ガチ勉強アドベントカレンダー 16日目。

今日からは少しディープラーニングの勉強。

ここ数年間、深層学習用ライブラリも猛烈に整備され、誰でも簡単にディープラーニングを使えるようになりました。

その一方で、整備されすぎて、魔法の箱だという認識も多いですよね。 けれど、深層学習と言えど、しているのはほとんど線形代数微積分を組み合わせた数値計算です。

だったら自分で作れるのでは? というわけで、仕組みを理解するために、0からスクラッチで作ることにしました。

尚、勉強にはプロフェッショナルシリーズの深層学習を利用しています。

爆速で技術が進む深層学習界隈では少々obsoleteかもしれませんが*1、きちんと基礎の基礎を知るにはいい本だと思います。詳しい計算方法を学びたい人は、どうぞ。(線形代数偏微分の知識が必要です。)

深層学習 (機械学習プロフェッショナルシリーズ)

深層学習 (機械学習プロフェッショナルシリーズ)

作るニューラルネットワーク

下図のような基本的な3層ニューラルネットワーク

  • 全結合
  • 活性化関数 : 隠れ層 : Sigmoid関数, 出力層 : ソフトマックス関数
  • ミニバッチ処理を採用

の構成で作ってみる

ニューラルネットの設計

ニューラルネットには下記の要素が必要。

  • ネットワーク構成 : 何段構成にするとだとか、どういう計算を施すか解か、どこの層をつなぐとかを考えなければならない。今回は3段の単純なネットワークだけど、実際は用途によって様々な構成が出来る。Network Zooなどが、ネットワーク構成の参考になる。けど、今でも試行錯誤

  • 活性化関数 : ニューラルネット非線形性を生み出す重要な要素。

  • 重みのアップデート方法 : ニューラルネット学習を進める上で重要な要素。

活性化関数も、試行錯誤で作られている事が多い。 近年よく使われるのはReLU等。活性化関数に関しては、以前記事にしたことがあるのでそちらを参考に。

www.procrasist.com

順伝搬計算

入力(ベクトル)から、線形変換と活性化関数による非線形計算をしながら出力層の値を取り出す。 最終的に各値のスコアが出てくるので、回帰ならそのまま用いればいいし、分類ならスコアが最も高いものを選ぶ。

逆伝搬計算

出力値から、誤差を出すことが出来る。その誤差を元にして、各層の重みを調整していく。 このためには微分の知識が必要になってくる。出力層から入力層計算を進めるため、誤差逆伝搬と呼ばれる。 誤差にはクロスエントロピー関数を用いる事が多い

重みの更新

逆誤差伝搬を行うことで、誤差からの各重みが出せる。最もシンプルなのは、単に誤差を一回の微分値で調整する方法(勾配降下法)。確率的勾配法(SGD)などもこれを元にしている。 この更新方法も様々に考えられていて、もう少し高度なモメンタムや、学習率を自動的に更新してくれるAdam, AdaGradなどもある。

f:id:imslotter:20171216162434p:plain
(重みの更新式{\epsilon}は学習率)

実装

以上を踏まえて、実装をしてみた。いつものように

ネットワークの設計

必要なネットワークの構成と、活性化関数、順伝搬計算、逆伝搬計算を、実装

コードをみる
class NN:
    def __init__(self, num_input, num_hidden, num_output, learning_rate):
        self.num_input = num_input
        self.num_hidden = num_hidden
        self.num_output = num_output
        self.learning_rate = learning_rate

        self.w_input2hidden = np.random.random((self.num_hidden, self.num_input))
        self.w_hidden2output = np.random.random((self.num_output, self.num_hidden))
        self.b_input2hidden = np.ones((self.num_hidden))
        self.b_hidden2output = np.ones((self.num_output))

    ##活性化関数(シグモイド関数)
    def activate_func(self, x):
        return 1/(1+np.exp(-x))
    ##活性化関数の微分
    def dactivate_func(self,x):
        return self.activate_func(x)*(1-self.activate_func(x))
    ##ソフトマックス関数
    def softmax_func(self,x):
        C = x.max()
        f = np.exp(x-C)/np.exp(x-C).sum()
        return f
    ##順伝播計算
    def forward_propagation(self, x):
        u_hidden = np.dot(self.w_input2hidden, x) + self.b_input2hidden
        z_hidden = self.activate_func(u_hidden)
        u_output = np.dot(self.w_hidden2output, z_hidden) + self.b_hidden2output
        z_output = self.softmax_func(u_output)
        return u_hidden, u_output, z_hidden, z_output
    ##逆伝播でdeltaを求める
    def backward_propagation(self,t,u_hidden,z_output):
        t_vec = np.zeros(len(z_output))
        t_vec[t] = 1
        delta_output = z_output - t_vec
        delta_hidden = np.dot(delta_output, self.w_hidden2output * self.dactivate_func(u_hidden))
        return delta_hidden, delta_output
    ##wに関するgradient
    def calc_gradient(self,delta,z):
        dW = np.zeros((len(delta), len(z)))
        for i in range(len(delta)):
            for j in range(len(z)):
                dW[i][j] = delta[i] * z[j]
        return dW
    # update(SGD)
    def update_weight(self,w0,gradE):
        return w0 - self.learning_rate*gradE

学習

ランダムにデータを選ぶことで、確率的勾配法にしてみた。

コードをみる
def train(nn, iteration,savefig=False):
    epoch = 0
    for epoch in range(iteration+1):
        grad_i2h = 0
        grad_h2o = 0
        gradbias_i2h = 0
        gradbias_h2o = 0
        rand = randint(0,len(data),100)
        for r in rand:
            u_hidden, u_output, z_hidden, z_output = nn.forward_propagation(data[r])
            delta_hidden, delta_output = nn.backward_propagation(target[r], u_hidden, z_output)
            grad_i2h += nn.calc_gradient(delta_hidden, data[r])
            grad_h2o += nn.calc_gradient(delta_output, z_hidden)
            gradbias_i2h += delta_hidden
            gradbias_h2o += delta_output
        nn.w_input2hidden = nn.update_weight(nn.w_input2hidden, grad_i2h / len(rand))
        nn.w_hidden2output = nn.update_weight(nn.w_hidden2output, grad_h2o / len(rand))
        nn.b_input2hidden = nn.update_weight(nn.b_input2hidden, gradbias_i2h / len(rand))
        nn.b_hidden2output = nn.update_weight(nn.b_hidden2output, gradbias_h2o / len(rand))

実行

sklearnでデータセットを用意し、実際に分類できるかを分離平面で表してみている。 自信度によって色の濃さを調整しているので、学習が進んでいくと、きちんと分類されて、濃くなっていくはず。。。!

コードをみる
#データの用意
num_cls = 5
data,target = make_blobs(n_samples=1000, n_features=2, centers=num_cls)
# Neural Netの用意
nn = NN(num_input=2,num_hidden=20,num_output=num_cls,learning_rate=0.2)
# 学習
train(nn, iteration=500, savefig=True)

plt.figure(figsize=(5,5))
#色の用意
base_color = ["red","blue","green","yellow","cyan",
              "pink","brown","gray","purple","orange"]
colors = [base_color[label] for label in target]
# 教師データのプロット
plt.scatter(data[:,0],data[:,1],color=colors,alpha=0.5)
print("plotting...")
xx = np.linspace(-15,15,100)
yy = np.linspace(-15,15,100)
for xi in xx:
    for yi in yy:
        _,_,_,z_output = nn.forward_propagation((xi, yi))
        cls = np.argmax(z_output) #softmaxのスコアの最大のインデックス
        score = np.max(z_output)
        plt.plot(xi, yi, base_color[cls],marker="x",alpha=s)
    print(".",end="")
plt.xlim=(-15,15)
plt.ylim=(-15,15)
plt.show()
print("finish plotting")

実行結果

以下が、300イテレーション回したときの分離平面の様子である。

また、学習が進んでいく様子もアニメーションにしてみた。(画像が粗くてすみません)

うまく学習が進んでいる様子が見て取れます。

まとめ

今回は実際に作ることで、ニューラルネットの仕組みを理解してみた。 実際に自分で作るとどこで非線形な要素が生まれるのかとか、誤差をどう学習に反映させているのかというのが分かるようになるので、勉強になります。魔法の箱なんかじゃなく、ちゃんとした学習器として理解できたように思います。0空作ればここから柔軟にいろいろと発展させていくことも可能ですしね。

今回は式からプログラムを書き起こしたが、最近は↓のようなスクラッチで書く人のためのとても良い本も売っているので、そちらも参考にしてみてはいかがでしょうか。

また、この辺の基礎を知っている人は、正直ライブラリを使ったほうが早いです。 明日からは、深層学習用ライブラリをいくつか触っていきたいと思います。(KerasかPytorch) ではでは!

*1:言うても2-3年前までは最先端

PROCRASIST