BETA

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

投稿日:2018-10-17
最終更新:2018-10-24
※この記事は外部サイト(http://www.procrasist.com/entry/16-neural-net)からのクロス投稿です

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

ディープラーニングの勉強。

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

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

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

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

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

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

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

の構成で作ってみる

ニューラルネットの設計

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

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

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

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

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

【ReLU, PReLU, シグモイド etc...】ニューラルネットでよく使う活性化関数の効果をKerasで調べてみた

順伝搬計算

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

逆伝搬計算

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

重みの更新

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

実装

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

ネットワークの設計

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

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) ではでは!

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

@hokekiyooの技術ブログ

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる
技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
or 外部アカウントではじめる
10秒で技術ブログが作れます!