技術をかじる猫

適当に気になった技術や言語、思ったこと考えた事など。

ニューラルネット実装

今回やったこと:

  • 入力: 正弦波(  sign(x) )を学習させて、-1 - 1 までの整数食わしたら  sin を返す
  • 中間層: シグモイド関数(3 個)
  • 出力層: 恒等関数
  • 損失関数: 二乗和誤差
  • 最適化アルゴリズム: 勾配降下法
  • バッチサイズ: 1

で実装する。

まずは学習データその他

import numpy as np
import matplotlib.pyplot as plt

%matplotlib inline

wb_width = 0.01 # 重みバイアスの広がり方
epoch    = 2001 # 学習データ数
eta      = 0.1  # 学習係数

input_data   = np.arange(0, np.pi * 2, 0.1) # 学習データ
correct_data = np.sin(input_data)           # 正解データ
input_data   = (input_data - np.pi) / np.pi # -1 - 1 にデータを整形
n_data       = len(correct_data)            # データ数

# 各層のニューロン数
n_in  = 1
n_mid = 3
n_out = 1


# 学習データのプロット
plt.plot(input_data, correct_data)
plt.show()

f:id:white-azalea:20210308231807p:plain

出力層の勾配を考える。参考は 前回の記事 でやったやつ。
二乗和誤差と恒等関数の組み合わせだと、  \delta の値は


\delta_k = y_k - t_k

で、残りのパラメータは


\partial w_{jk} = y_j \delta_k (重み勾配) \\
\partial b_k = \delta_k (バイアス勾配) \\
\partial y_j = \sum^n_{r=1} \delta_r w_{jr} (一つ上の層の出力勾配)
class Neuron:
    def __init__(self, n_upper, n, activation_function, differential_function):
        self.w = wb_width * np.random.randn(n_upper, n)
        self.b = wb_width * np.random.randn(n)
        self.grad_w = np.zeros((n_upper, n))
        self.grad_b = np.zeros((n))
        self.activation_function = activation_function
        self.differential_function = differential_function

    def update(self, eta):
        self.w -= eta * self.grad_w
        self.b -= eta * self.grad_b
    
    def forward(self, x):
        self.x = x
        u = x.dot(self.w) + self.b
        self.y = self.activation_function(u)
        return self.y

    def backword(self, t):
        delta = self.differential_function(self.y, t)
        self.grad_w = self.x.T.dot(delta)
        self.grad_b = np.sum(delta, axis=0)
        self.grad_x = delta.dot(self.w.T)
        return self.grad_x


def identity_func(u):
    """恒等関数"""
    return u


def differential_output(y, t):
    """恒等関数+二乗和誤差の微分"""
    return y - t


class Output(Neuron):
    pass

重みとバイアスは共通の作りなので、Neuron として独立。
forward で順伝播したときに、それぞれの値をクラス内変数に格納する。
backword で格納した値と、目標値  t を受け取って勾配を作る。

中間層はシグモイド関数

こっちはこんな感じ


\delta_j = \partial y_j (1 - y_j)y_j \\
\partial w_{ij} = y_i \delta_j (重み勾配) \\
\partial b_j = \delta_j (バイアス勾配) \\
\partial y_i = \sum^m_{q=1} \delta_q w_{iq} (一つ上の層の出力勾配)
def sigmoid(u):
    """シグモイド関数"""
    return 1 / (1 + np.exp(-u))


def differential_sigmod(grad_y, y):
    """シグモイド関数の微分関数"""
    return grad_y * (1 - y) * y


class Middle(Neuron):
    def backword(self, grad_y):
        delta = self.differential_function(grad_y, self.y)
        self.grad_w = self.x.T.dot(delta)
        self.grad_b = delta.sum(axis = 0)
        self.grad_x = delta.dot(self.w.T)
        return self.grad_x

ということで実際に学習させてみる

middle_layer: Middle = Middle(n_in, n_mid, sigmoid, differential_sigmod)
output_layer: Output = Output(n_mid, n_out, identity_func, differential_output)


# 途中経過の表示間隔
interval = 200

for i in range(epoch):
    # 学習データのシャッフル
    index_random = np.arange(n_data)
    np.random.shuffle(index_random)

    # 学習状況の把握用
    total_err = 0
    plot_x = []
    plot_y = []

    for idx in index_random:
        # ランダムに取り出した x とそれの対となる sin(x) の値を取得
        x = input_data[idx:idx+1]
        t = correct_data[idx:idx+1]

        # 順伝播させて
        y = output_layer.forward(
            middle_layer.forward(x.reshape(1, 1)))

        # 逆伝播(正式解を与えて勾配計算させ)
        middle_layer.backword(
            output_layer.backword(t.reshape(1, 1)))

        # 重み+バイアス更新
        middle_layer.update(eta)
        output_layer.update(eta)

        if i % interval == 0:
            # 二乗和誤差の計算(y = 出力層の結果, t = 正解値)
            total_err += 1.0 / 2.0 * np.sum(np.square(y - t))

            # 出力の記録
            plot_x.append(x)
            plot_y.append(y)

    if i % interval == 0:
        # 出力のグラフ表示
        plt.plot(input_data, correct_data, linestyle = 'dashed')
        plt.scatter(plot_x, plot_y, marker='o')
        plt.show()
        print('Epoch:' + str(i) + '/' + str(epoch), 'Error:' + str(total_err / n_data))

1 回目(学習前)

f:id:white-azalea:20210308234254p:plain

200 回学習

f:id:white-azalea:20210308234328p:plain

400 回学習

f:id:white-azalea:20210308234402p:plain

飛んで 1000 回学習

f:id:white-azalea:20210308234458p:plain

2000 回学習時点

f:id:white-azalea:20210308234546p:plain

ということで、「この時はこの値を返す」みたいなことを学習させることができたと。