からっぽのしょこ

読んだら書く!書いたら読む!読書読読書読書♪同じ事は二度調べ(たく)ない

4.5.1:2層ニューラルネットワークのクラス【ゼロつく1のノート(実装)】

はじめに

 「プログラミング」学習初手『ゼロから作るDeep Learning』民のための実装攻略ノートです。『ゼロつく1』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。

 関数やクラスとして実装される処理の塊を細かく分解して、1つずつ処理を確認しながらゆっくりと組んでいきます。

 この記事は、4.5.1項「2層ニューラルネットワークのクラス」の内容になります。2層ニューラルネットワークとその損失関数や精度の計算を含めたクラスを実装します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

4.5 学習アルゴリズムの実装

 これまでにニューラルネットワークの学習に関する基本的な知識を学びました。この節ではそれらを用いて、実際に手書き文字の学習を行います。

 よってこの節では、これまでに実装した関数を用います。そのため、3.2.4項のシグモイド関数4.2.3項の交差エントロピー誤差の関数定義を再度読み込む必要があります。また3.5.1項のソフトマックス関数4.4節の勾配も用いますが、これらは前項で2次元配列に対応するための変更を加えたのでそちらを利用します。

 または、次の方法でマスターデータから読み込むことも可能です。

# 関数の読み込みに利用するライブラリを読み込む
import sys
import os

# ファイルパスを指定
sys.path.append("C:\\Users\\「ユーザー名」\\Documents\\・・・\\deep-learning-from-scratch-master")

# ファイルから関数を読み込む
from common.functions import sigmoid # シグモイド関数:3.2.4項
from common.functions import softmax # ソフトマックス関数:3.5.1項
from common.functions import cross_entropy_error # 交差エントロピー誤差:4.2.3項
from common.gradient import numerical_gradient # 勾配の計算:4.4節


 まずは4.5.1項にて、各種機能を備えたクラスを実装します。続く4.5.2項では、訓練データを用いた学習を行います。最後に4.5.3項で、テストデータによる評価を行います。

4.5.1 2層ニューラルネットワークのクラス

 この項では、「パラメータ(重みとバイアス)の初期値をランダムに設定(初期化)」「2層のニューラルネットワークによる推論(分類)」「損失関数による誤差の計算」「認識精度の計算」「勾配の計算」の5つの機能を1つのクラスとして実装します。今はまだ複雑に見えるかとは思いますが、どの処理も基本的な流れはこれまでやってきたものです!

 実装時の処理の確認用に仮の入力データと教師データを作成します。本の通りであれば、次のようにしてダミーの値を生成して用います。

# 仮の訓練画像を生成
x = np.random.rand(100, 784)
print(x.shape)

# 仮の訓練ラベルを生成
t = np.random.rand(100, 10)
print(t.shape)
(100, 784)
(100, 10)


 これでは(ラベルデータが0か1ではないなどで)イメージしにくければ、1枚分の画像データを用意しました。(次項でload_mnist()で読み込みますが、)ここではコピペで済ませましょう。

・画像データ(クリックで展開)

# 仮の訓練画像
x = np.array([
    [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.01,0.07,
     0.07,0.07,0.49,0.53,0.69,0.1, 0.65,1.0, 0.97,0.5, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.12,0.14,0.37,0.6, 0.67,0.99,
     0.99,0.99,0.99,0.99,0.88,0.67,0.99,0.95,0.76,0.25,0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.19,0.93,0.99,0.99,0.99,0.99,0.99,
     0.99,0.99,0.99,0.98,0.36,0.32,0.32,0.22,0.15,0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.07,0.86,0.99,0.99,0.99,0.99,0.99,
     0.78,0.71,0.97,0.95,0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.31,0.61,0.42,0.99,0.99,0.8,
     0.04,0.0, 0.17,0.6, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.05,0.0, 0.6 ,0.99,0.35,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.55,0.99,0.75,
     0.01,0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.04,0.75,0.99,
     0.27,0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.14,0.95,
     0.88,0.63,0.42,0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.32,
     0.94,0.99,0.99,0.47,0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.18,0.73,0.99,0.99,0.59,0.11,0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.06,0.36,0.99,0.99,0.73,0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.98,0.99,0.98,0.25,0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.18,0.51,0.72,0.99,0.99,0.81,0.01,0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.15,0.58,
     0.9 ,0.99,0.99,0.99,0.98,0.71,0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.09,0.45,0.87,0.99,
     0.99,0.99,0.99,0.79,0.31,0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.09,0.26,0.84,0.99,0.99,0.99,
     0.99,0.78,0.32,0.01,0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.07,0.67,0.86,0.99,0.99,0.99,0.99,0.76,
     0.31,0.04,0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.22,0.67,0.89,0.99,0.99,0.99,0.99,0.96,0.52,0.04,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.53,0.99,0.99,0.99,0.83,0.53,0.52,0.06,0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ]
])
print(x.shape)

# 仮の訓練ラベル
t = np.array([
    [0, 0, 0, 0, 0, 1, 0, 0, 0, 0]
])
print(t.shape)
(1, 784)
(1, 10)


 クラスとしてまとめて実装する前に、1つずつ関数として実装してみましょう。急がば回れ!念には念を!亀になれ!

 まずは各データのサイズ(ニューロンの数)を設定します。input_sizeには、画像データのピクセル数(列数)784を指定します。hidden_sizeは、隠れ層のニューロン数なのでいくつでもいいのですが、ここでは100とします。output_sizeには、0から9の数字に対応するため10ですよね。(3つの値(サイズ)の対応関係は図3.26-27で確認できます。理由については3.3.2項「行列の積」の内容になります。)

 また、各重みの初期値の標準偏差をweight_init_stdとして指定します。

## 各データのサイズ(ニューロンの数)を指定

# 入力層
input_size = 784

# 隠れ層
hidden_size = 100

# 出力層
output_size = 10

# 重みの初期値の標準偏差
weight_init_std = 0.01

 np.random.randn()は、平均0、標準偏差1の標準正規分布に従う乱数を生成します。生成した要素の標準偏差は約1となります。全ての要素に任意の値を掛けることで、標準偏差をその値に変更できます。

 指定した形状となるパラメータを作成します。

 重みは、np.random.randn(行数, 列数)でランダムに値とる2次元配列を作成します。

 バイアスは、np.zeros(要素数)で全ての要素が0の1次元配列を作成します。

## パラメータの初期値を設定

# 第1層の重み
W1 = weight_init_std * np.random.randn(input_size, hidden_size)
print(W1.shape)
print(W1[:, 0:5])

# 第1層のバイアス
b1 = np.zeros(hidden_size)
print(b1.shape)
print(b1)

# 第2層の重み
W2 = weight_init_std * np.random.randn(hidden_size, output_size)
print(W2.shape)
print(W2[0:5, :])

# 第2層のバイアス
b2 = np.zeros(output_size)
print(b2.shape)
print(b2)
(784, 100)
[[-0.01816772  0.00379656  0.0127303   0.0168115  -0.00240824]
 [-0.01545036 -0.01382784 -0.00898753 -0.01249721  0.00816511]
 [ 0.01338953 -0.00605344  0.00416199  0.01400031  0.00387049]
 ...
 [ 0.0024635  -0.01439405  0.01804761 -0.00265219  0.00158621]
 [ 0.02884843  0.02542001  0.00038251 -0.00233701  0.01171595]
 [ 0.00754187 -0.00605913 -0.00293599 -0.0068374   0.00050707]]
(100,)
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0.]
(100, 10)
[[-0.00566943 -0.0081242   0.01291806 -0.01450363  0.00504592  0.00789576
   0.02149283 -0.00529043 -0.00777389 -0.01072185]
 [-0.00967977  0.00045618 -0.00662146 -0.01435483 -0.00043302  0.01268846
  -0.0092964  -0.00309668  0.02082456 -0.00050601]
 [-0.00074482  0.0075946   0.01226315 -0.00263505  0.01109011  0.00608475
  -0.01236731  0.00344455 -0.01793453 -0.00436127]
 [ 0.00100929  0.01166849 -0.00591745 -0.00750589  0.00740857  0.01543674
   0.00062166 -0.00139146 -0.00244013 -0.00794902]
 [-0.00546783  0.01551858 -0.02166503 -0.00033079  0.01291599  0.00649528
   0.00913919  0.00568149 -0.00531881 -0.00786011]]
(10,)
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]

 行列の積の計算では、行数や列数が対応していないと計算できないのでした。このように先にサイズを決めておいて、各パラメータを自動で作成することはエラー回避の意味もあります。

 これをディクショナリ型の変数としてまとめます。3.4.3項でもパラメータをディクショナリ型として保存しました。その時は値を適当に決めましたが、ここでは乱数によって設定します。

# インスタンス変数の設定(初期化)
def init_params(input_size, hidden_size, output_size, weight_init_std=0.01):
    
    # パラメータのディクショナリ変数を作成
    params = {} # 初期化
    params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
    params['b1'] = np.zeros(hidden_size)
    params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
    params['b2'] = np.zeros(output_size)
    
    return params

 パラメータを作成(初期化)する関数を定義しました。これで必要な形状のパラメータを簡単に作成することができます。

 また作成した変数(オブジェクト)は、キーを指定することでいつでも必要なパラメータにアクセスできるのでした。

# パラメータの初期化
params = init_params(input_size, hidden_size, output_size, weight_init_std=0.01)
print(params.keys())

# 第1層の重みを取得
W1 = params['W1']
print(W1[:, 0:5])
print(np.std(W1)) # 標準偏差を計算
dict_keys(['W1', 'b1', 'W2', 'b2'])
[[-0.00725172  0.01366423  0.00831813  0.00650994  0.01223709]
 [-0.00255899 -0.01066872  0.00065642  0.01114878 -0.00168115]
 [ 0.01532259  0.01064444 -0.00729933  0.01139096  0.00945245]
 ...
 [ 0.00534464  0.00648034  0.00573037  0.00187181 -0.00735561]
 [-0.01934573  0.00967734  0.01672581 -0.01612427  0.00144806]
 [ 0.00298948  0.00617366  0.00310523 -0.02545154 -0.01431605]]
0.009960860341882415

 重みパラメータの標準偏差が約0.01になっています。

 続いて、推論関数として2層のニューラルネットワークを実装します。これは3.6.2項で実装したpredict()とほぼ同じですね。

# 推論メソッド
def predict(x, params):
    
    # パラメータを取得
    W1, W2 = params['W1'], params['W2']
    b1, b2 = params['b1'], params['b2']
    
    # 第1層
    a1 = np.dot(x, W1) + b1
    z1 = sigmoid(a1)
    
    # 第2層
    a2 = np.dot(z1, W2) + b2
    y = softmax(a2)
    
    return y


 最終層にソフトマックス関数を用いると、出力の各要素は0から1の値をとり、その総和が1となるのでした。

# 2層のニューラルネットワーク
y = predict(x, params)
print(np.sum(y))
print(y)
1.0
[[0.09814476 0.10311726 0.10083286 0.09765875 0.0930179  0.10720149
  0.09573035 0.1043807  0.09721556 0.10270036]]

 この関数の出力を、以降の全ての処理に用います。

 交差エントロピー誤差を計算します。これは前項でやりましたね。ここではニューラルネットワークpredicto()の最終層にソフトマックス関数を用いているので、既に正規化されている状態です。よって、cross_entropy_error()で処理するだけです。

# 損失関数を定義
def loss(x, params, t):
    
    # 推論処理
    y = predict(x, params)
    
    # 交差エントロピー誤差を計算
    return cross_entropy_error(y, t)


 この関数の実装方法だと、ラベルデータはone-hot表現である必要があります。

# 損失関数による計算
loss_score = loss(x, params, t)
print(loss_score)
2.2330441599411444

 推論の性能を各ラベルの確率の誤差から評価しました。

 次は、推論の性能を認識(分類)の正解精度で測ります。

 np.argmax()で引数に指定された変数の最大値のインデックス(要素番号)を抽出します。axis=1とすることで、各行ごとの最大値を調べて返します(ただしこの実装だと2次元配列を入力する必要があります)。

 認識結果と正解ラベルを比較演算子==で調べて、Trueとなれば正解を予測したことが分かります。

# 推論処理
y = predict(x, params)

# 認識結果を抽出
y_label = np.argmax(y, axis=1)
print(y_label)

# 正解ラベルを抽出
t_label = np.argmax(t, axis=1)
print(t_label)

# 検証
print(y_label == t_label)
[5]
[5]
[ True]


 Trueは1、Falseは0として認識されるので、全ての要素の照合した結果を合計します。それをデータ数で割ることで、認識精度となります。

# 正解率を計算
accuracy_score = np.sum(y_label == t_label) / x.shape[0] # (正解数) / (画像数)
print(accuracy_score)
1.0

 まだ学習を行っていないので、当たるも当たらないも運です。

 これを精度関数として実装します。

# 精度関数
def accuracy(x, params, t):
    
    # 推論
    y = predict(x, params)
    
    # 抽出
    y = np.argmax(y, axis=1) # 予測結果
    t = np.argmax(t, axis=1) # 正解ラベル
    
    # 精度を計算
    accuracy = np.sum(y == t) / float(x.shape[0]) # (正解数) / (画像数)
    return accuracy


 ところで、上書きしてしまわないように変数名と関数名が同じにならないようにしています。

# 精度を計算
accuracy_score = accuracy(x, params, t)
print(accuracy_score)
1.0


 損失関数を導入したのは、勾配法による更新を行うためでした。損失関数の勾配(損失関数についての各パラメータの偏微分)を求めます。

 勾配の計算自体は4.4節で実装しました(ものを4.4.2項で2次元配列にも対応するように実装しなおしました)。numerical_gradient()の第1引数には、勾配の計算に用いる関数と変数を1つ指定する必要があります。そのため、損失関数の結果をそのまま返す関数を作成する必要があります。

# loss()の出力をそのまま返す関数を作成
fn = lambda W: loss(x, params, t)
# 勾配を計算
dW1 = numerical_gradient(fn, params['W1'])
print(dW1)
[[0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 ...
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]]


 勾配もディクショナリ型の変数を作成して、パラメータごとに格納します。

# 勾配の計算を定義
def numerical_gradient_tmp(x, W, t):
    
    # 損失関数を定義
    loss_W = lambda W: loss(x, params, t)
    
    # ディクショナリ型の各パラメータの勾配
    grads = {} # 初期化
    grads['W1'] = numerical_gradient(loss_W, params['W1'])
    grads['b1'] = numerical_gradient(loss_W, params['b1'])
    grads['W2'] = numerical_gradient(loss_W, params['W2'])
    grads['b2'] = numerical_gradient(loss_W, params['b2'])
    
    return grads

 全ての勾配を計算する関数を作成しました。ここで確認するための関数なので、また関数名が重複しないようにtmpと付けています。

 これでパラメータと同様にディクショナリ型の変数として扱えます。

# 勾配を計算
grads = numerical_gradient_tmp(x, params, t)
print(grads.keys())
dict_keys(['W1', 'b1', 'W2', 'b2'])


 以上で必要な機能の確認ができました。ではクラスとして実装します。

 前項と同様に、パラメータはインスタンス変数として定義します。これによりインスタンス自体がパラメータを持つことになるので、各メソッドの引数にパラメータを指定する必要がなくなります。

# 2層ニューラルネットワークの定義
class TwoLayerNet:
    
    # インスタンス変数の設定(初期化)
    def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
        
        # パラメータのディクショナリ変数を作成
        self.params = {} # 初期化
        self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
        self.params['b1'] = np.zeros(hidden_size)
        self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
        self.params['b2'] = np.zeros(output_size)
    
    # 推論メソッド
    def predict(self, x):
        
        # パラメータを取得
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
        
        # 第1層
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        
        # 第2層
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)
        
        return y
    
    # 損失関数メソッド
    def loss(self, x, t):
        
        # 推論
        y = self.predict(x)
        
        # 交差エントロピー誤差を計算
        return cross_entropy_error(y, t)
    
    # 認識精度メソッド
    def accuracy(self, x, t):
        
        # 推論
        y = self.predict(x)
        
        # 抽出
        y = np.argmax(y, axis=1) # 予測結果
        t = np.argmax(t, axis=1) # 正解ラベル
        
        # 精度を計算
        accuracy = np.sum(y == t) / float(x.shape[0]) # (正解数) / (画像数)
        return accuracy
    
    # 勾配メソッド
    def numerical_gradient(self, x, t):
        
        # 損失関数を定義
        loss_W = lambda W: self.loss(x, t)
        
        # ディクショナリ型の各パラメータの勾配
        grads = {} # 初期化
        grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
        grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
        grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
        grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
        
        return grads


 各メソッドの動作確認を行いましょう。まずはインスタンス化からです。

# インスタンスを作成
net = TwoLayerNet(input_size=784, hidden_size=100, output_size=10)

# パラメータを確認
net.params.keys()
dict_keys(['W1', 'b1', 'W2', 'b2'])


 各パラメータの形状が各引数に指定した値と一致しているか見ます。

# パラメータの形状を確認
print(net.params['W1'].shape)
print(net.params['b1'].shape)
print(net.params['W2'].shape)
print(net.params['b2'].shape)
(784, 100)
(100,)
(100, 10)
(10,)


 ニューラルネットワークによる処理を行います。

# 推論
y = net.predict(x)
print(y)
print(np.sum(y))
print(np.argmax(y))
[[0.09970516 0.10768702 0.09881568 0.10609831 0.09255124 0.10485145
  0.09911962 0.09014741 0.10005442 0.1009697 ]]
1.0
1


 交差エントロピー誤差を計算します。

# 損失関数の計算
loss = net.loss(x, t)
print(loss)
2.255209748128194


 認識精度を測定します。

# 認識精度を計算
accuracy = net.accuracy(x, t)
print(accuracy)
0.0


 各パラメータに対する損失関数の勾配を求めます。

# 各パラメータの勾配を計算
grad = net.numerical_gradient(x, t)
print(grad.keys())
print(grad['W1'].shape)
print(grad['b1'].shape)
print(grad['W2'].shape)
print(grad['b2'].shape)
dict_keys(['W1', 'b1', 'W2', 'b2'])
(784, 100)
(100,)
(100, 10)
(10,)

 (各パラメータの全ての要素分の計算を行うので、処理に時間がかかります。)

 この項では、これまでの知識を総動員した2層のニューラルネットワークをクラスとして実装しました。次項では、作成したクラスを利用して手書き文字に対するミニバッチ学習を行います。

参考文献

  • 斎藤康毅『ゼロから作るDeep Learning』オライリー・ジャパン,2016年.

おわりに

 4章クライマックスです。次項は、この記事で実装したクラスを使い学習を行います。このまま進めちゃいましょう!

【次節の内容】

www.anarchive-beta.com


 あと挿入歌です!ハロー!プロジェクトのグループ「こぶしファクトリー」による「念には念を(念入りVer.)」のアカペラバージョンです。

 「急がば回れ」と「亀になれ!」もあるのですが、アルバム曲なので公式には上がってないです(残念)。