barus's diary

とても真面目なblogですにゃ

Python(Anaconda3)でDeepLearningPython35を使用してニューラルネットワークで手書き数字を認識する

前回の記事

Python(Anaconda3)をインストールしscikit-learでニューラルネットワーク

では、ニューラルネットワークと深層学習で紹介されている

サンプルコードを動かそうとして

寄り道しAnaconda(Python3)やKerasscikit-learn

をインストールしてニューラルネットワークを作成したのが前回。

 

今回はニューラルネットワークと深層学習で紹介されている

network.py

を利用してニューラルネットワークを作成する。

自分が理解するために、ニューラルネットワークと深層学習から

抜粋していますので、より理解を深める場合は

上記サイト参照するのをお勧めします。

 

このスクリプトを動かすのに必要なインストール

情報はここに詳しく書きましたので参考にしてください。

 

DeepLearningPython35  簡単なコードでニューラルネットワークを作成できるパッケージ

for Python 3.5.2 and Theano with CUDA support

https://github.com/MichalDanielDobrzanski/DeepLearningPython35

よりClone downloadで DeepLearningPython35-master.zip をダウンロードする

ニューラルネットワークと深層学習でダウンロードできるのは

Python2に対応しておりエラーとなるのでPython3に

対応のファイルをダウンロードする。

 

MNISTの訓練データ

手書き数字を認識する方法を学ぶプログラムを作成する際、

確率的勾配降下法とMNISTの訓練データを使用します。 

https://github.com/mnielsen/neural-networks-and-deep-learning/archive/master.zip

より取得する。

 

Python2とPython3の違いがあるみたいで詳細は

Python 2.7.x と 3.x の決定的な違いを例とともに | プログラミング | POSTD

のURLに詳しく書かれています。

 

ダウンロードしたDeepLearningPython35-master.zipのフォルダにて

network_test1.pyを作成する。

MNISTの訓練データを解凍して、フォルダdataをDeepLearningPython35-master.zipを

解凍した同じディレクトリに配置する。

 

こんな感じです。

f:id:hatakeka:20171104123228p:plain

 

 

DeepLearningPython35-master.zipを取得し解凍する。以下のソースコードを「network_test1.py」名で、「network.py」がある同じフォルダに作成する。

 

ファイル名network_test1.py

--ここから---
import random import mnist_loader # Third-party libraries import numpy as np import network def main(): training_data, validation_data, test_data = \ mnist_loader.load_data_wrapper() net = network.Network([784, 30, 10]) net.SGD(training_data, 30, 10, 3.0, test_data=test_data) #X, y = generate_data() #model = build_model(X, y, 3, print_loss=True) #visualize(X, y, model) #plt.title("hidden layer 3 neural networks") #plt.show() return 0 if __name__ == "__main__": main() ---ここまで---

 

ビルドはSpyderではF5

S:\plog\Python\python3\DeepLearningPython35-master>python network_test1.py

 

それでは、このコードがどれだけ良く手書き数字を認識できるかを確認してみます。

まずはMNISTデータをダウンロードし。 ここではmnist_loader.pyを使用します。

training_data, validation_data, test_data = \
    mnist_loader.load_data_wrapper()

MNISTデータを読み込んでいます。

30個の隠れニューロンをもつNetworkを設定します。 

 net = network.Network([784, 30, 10])

最後に、30世代・ミニバッチサイズ10・訓練率の条件で、

MNISTのtraining_dataから確率的勾配降下法を使用して学習します。

net.SGD(training_data, 30, 10, 3.0, test_data=test_data)

もしこの文章を読みながらコードを実行しているならば、

この計算は少々時間がかかるので注意してください。

 

一度ニューラルネットワークを訓練すれば、

私たちは多くのコンピュータープラットホーム上で非常に

高速に実行することができます。

例えば、ニューラルネットワークの重みとバイアスの良いセットがあれば、

webブラウザのJavascriptや、携帯デバイスのアプリに移植し

実行するのは簡単です。 

 

それでは、ニューラルネットワークのある訓練プロセス結果を示しましょう。

Epoch 0 : 8973 / 10000
Epoch 1 : 9222 / 10000
Epoch 2 : 9256 / 10000
Epoch 3 : 9374 / 10000
Epoch 4 : 9340 / 10000
Epoch 5 : 9414 / 10000
Epoch 6 : 9443 / 10000
Epoch 7 : 9468 / 10000
Epoch 8 : 9432 / 10000
Epoch 9 : 9460 / 10000
Epoch 10 : 9459 / 10000
Epoch 11 : 9434 / 10000
Epoch 12 : 9473 / 10000
Epoch 13 : 9479 / 10000
Epoch 14 : 9507 / 10000
Epoch 15 : 9485 / 10000
Epoch 16 : 9506 / 10000
Epoch 17 : 9472 / 10000
Epoch 18 : 9520 / 10000
Epoch 19 : 9509 / 10000
Epoch 20 : 9487 / 10000
Epoch 21 : 9522 / 10000
Epoch 22 : 9514 / 10000
Epoch 23 : 9521 / 10000
Epoch 24 : 9520 / 10000
Epoch 25 : 9530 / 10000
Epoch 26 : 9529 / 10000
Epoch 27 : 9479 / 10000
Epoch 28 : 9516 / 10000
Epoch 29 : 9504 / 10000

 

この出力は訓練のエポックごとにニューラルネットワーク

使用して適切に訓練データを認識できた数を表しています。

最初の世代が終わったあとに10000個中の8973個が正しく認識できており、

その後は増加し続けていることがわかります。

 

訓練の評価は下の方で紹介する

network.py の中にあるevaluate()関数で行っている。


    def evaluate(self, test_data):
        """Return the number of test inputs for which the neural
        network outputs the correct result. Note that the neural
        network's output is assumed to be the index of whichever
        neuron in the final layer has the highest activation."""
        test_results = [(np.argmax(self.feedforward(x)), y)
                        for (x, y) in test_data]
        return sum(int(x == y) for (x, y) in test_results)

 

訓練されたネットワークは95%の分類率を有しており、

ピーク性能は26世代での95.3%でした。

この結果は、最初の試みとしては大変有望です。 

 

それでは、隠れニューロンの数を100個にして上記の実験を再計算してみます。

net = network.Network([784, 100, 10])

Epoch 0 : 7541 / 10000
Epoch 1 : 8489 / 10000
Epoch 2 : 8540 / 10000
Epoch 3 : 8589 / 10000
Epoch 4 : 8619 / 10000
Epoch 5 : 8657 / 10000
Epoch 6 : 8670 / 10000
Epoch 7 : 8678 / 10000
Epoch 8 : 8661 / 10000
Epoch 9 : 8715 / 10000
Epoch 10 : 8715 / 10000
Epoch 11 : 8720 / 10000
Epoch 12 : 8707 / 10000
Epoch 13 : 8701 / 10000
Epoch 14 : 8735 / 10000
Epoch 15 : 8719 / 10000
Epoch 16 : 8715 / 10000
Epoch 17 : 8736 / 10000
Epoch 18 : 8730 / 10000
Epoch 19 : 8736 / 10000
Epoch 20 : 8739 / 10000
Epoch 21 : 8730 / 10000
Epoch 22 : 8748 / 10000
Epoch 23 : 8746 / 10000
Epoch 24 : 8741 / 10000
Epoch 25 : 8754 / 10000
Epoch 26 : 8752 / 10000
Epoch 27 : 8745 / 10000
Epoch 28 : 8741 / 10000
Epoch 29 : 8747 / 10000

 

予想とは異なり良い結果が出ませんでした。

このように、もし

net.SGD(training_data, 訓練のエポック数, ミニバッチサイズ, 学習率, test_data=test_data)

の訓練のエポック数、ミニバッチサイズ、学習率やレイヤー層、隠れ層

を不適切に選択したならば、悪い結果を得ることになります。

 

これらの精度を獲得するために、

訓練のエポック数、ミニバッチのサイズ、学習率を具体的に

選択しなくてはなりませんでした。

上記のように、学習アルゴリズムによって

学習するパラメータ(重みとバイアス)と区別するために、

これらはニューラルネットワークのハイパーパラメータとして呼ばれています。

もしハイパーパラメータを不適切に選択したならば、

悪い結果を得ることになります。

 

ニューラルネットワークの改善テクニックを参照して下さい。

 

 

network.py このファイルは

DeepLearningPython35-master.zipを解凍した

DeepLearningPython35-masterにあります。

---ここから---

# %load network.py

"""
network.py
~~~~~~~~~~
IT WORKS

A module to implement the stochastic gradient descent learning
algorithm for a feedforward neural network.  Gradients are calculated
using backpropagation.  Note that I have focused on making the code
simple, easily readable, and easily modifiable.  It is not optimized,
and omits many desirable features.
"""

#### Libraries
# Standard library
import random

# Third-party libraries
import numpy as np

class Network(object):

    def __init__(self, sizes):
        """The list ``sizes`` contains the number of neurons in the
        respective layers of the network.  For example, if the list
        was [2, 3, 1] then it would be a three-layer network, with the
        first layer containing 2 neurons, the second layer 3 neurons,
        and the third layer 1 neuron.  The biases and weights for the
        network are initialized randomly, using a Gaussian
        distribution with mean 0, and variance 1.  Note that the first
        layer is assumed to be an input layer, and by convention we
        won't set any biases for those neurons, since biases are only
        ever used in computing the outputs from later layers."""
        self.num_layers = len(sizes)
        self.sizes = sizes
        self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
        self.weights = [np.random.randn(y, x)
                        for x, y in zip(sizes[:-1], sizes[1:])]

    def feedforward(self, a):
        """Return the output of the network if ``a`` is input."""
        for b, w in zip(self.biases, self.weights):
            a = sigmoid(np.dot(w, a)+b)
        return a

    def SGD(self, training_data, epochs, mini_batch_size, eta,
            test_data=None):
        """Train the neural network using mini-batch stochastic
        gradient descent.  The ``training_data`` is a list of tuples
        ``(x, y)`` representing the training inputs and the desired
        outputs.  The other non-optional parameters are
        self-explanatory.  If ``test_data`` is provided then the
        network will be evaluated against the test data after each
        epoch, and partial progress printed out.  This is useful for
        tracking progress, but slows things down substantially."""

        training_data = list(training_data)
        n = len(training_data)

        if test_data:
            test_data = list(test_data)
            n_test = len(test_data)

        for j in range(epochs):
            random.shuffle(training_data)
            mini_batches = [
                training_data[k:k+mini_batch_size]
                for k in range(0, n, mini_batch_size)]
            for mini_batch in mini_batches:
                self.update_mini_batch(mini_batch, eta)
            if test_data:
                print("Epoch {} : {} / {}".format(j,self.evaluate(test_data),n_test));
            else:
                print("Epoch {} complete".format(j))

#ミニバッチ1つ分に逆伝播を用いた勾配降下法を適用し、
#ニューラルネットワークの重みとバイアスを更新する。
#"mini_batch"はタプル"(x, y)"のリストで"、
#eta"は学習率。
def update_mini_batch(self, mini_batch, eta): """Update the network's weights and biases by applying gradient descent using backpropagation to a single mini batch. The ``mini_batch`` is a list of tuples ``(x, y)``, and ``eta`` is the learning rate.""" nabla_b = [np.zeros(b.shape) for b in self.biases] nabla_w = [np.zeros(w.shape) for w in self.weights] for x, y in mini_batch: delta_nabla_b, delta_nabla_w = self.backprop(x, y) nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)] nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)] self.weights = [w-(eta/len(mini_batch))*nw for w, nw in zip(self.weights, nabla_w)] self.biases = [b-(eta/len(mini_batch))*nb for b, nb in zip(self.biases, nabla_b)] def backprop(self, x, y): """Return a tuple ``(nabla_b, nabla_w)`` representing the gradient for the cost function C_x. ``nabla_b`` and ``nabla_w`` are layer-by-layer lists of numpy arrays, similar to ``self.biases`` and ``self.weights``.""" nabla_b = [np.zeros(b.shape) for b in self.biases] nabla_w = [np.zeros(w.shape) for w in self.weights] # feedforward activation = x activations = [x] # list to store all the activations, layer by layer zs = [] # list to store all the z vectors, layer by layer for b, w in zip(self.biases, self.weights): z = np.dot(w, activation)+b zs.append(z) activation = sigmoid(z) activations.append(activation) # backward pass delta = self.cost_derivative(activations[-1], y) * \ sigmoid_prime(zs[-1]) nabla_b[-1] = delta nabla_w[-1] = np.dot(delta, activations[-2].transpose()) # Note that the variable l in the loop below is used a little # differently to the notation in Chapter 2 of the book. Here, # l = 1 means the last layer of neurons, l = 2 is the # second-last layer, and so on. It's a renumbering of the # scheme in the book, used here to take advantage of the fact # that Python can use negative indices in lists. for l in range(2, self.num_layers): z = zs[-l] sp = sigmoid_prime(z) delta = np.dot(self.weights[-l+1].transpose(), delta) * sp nabla_b[-l] = delta nabla_w[-l] = np.dot(delta, activations[-l-1].transpose()) return (nabla_b, nabla_w) def evaluate(self, test_data): """Return the number of test inputs for which the neural network outputs the correct result. Note that the neural network's output is assumed to be the index of whichever neuron in the final layer has the highest activation.""" test_results = [(np.argmax(self.feedforward(x)), y) for (x, y) in test_data] return sum(int(x == y) for (x, y) in test_results) def cost_derivative(self, output_activations, y): """Return the vector of partial derivatives \partial C_x / \partial a for the output activations.""" return (output_activations-y) #### Miscellaneous functions def sigmoid(z): """The sigmoid function.""" return 1.0/(1.0+np.exp(-z)) def sigmoid_prime(z): """Derivative of the sigmoid function.""" return sigmoid(z)*(1-sigmoid(z))

 ---ここまで---

 

ニューラルネットワークのコードのコア機能の説明を以下でしましょう。

コードの中心部はNetworkクラスであり、ニューラルネットワーク

表現するために使います。以下が、Networkを初期化するためのコードです。

 


    def __init__(self, sizes):
        self.num_layers = len(sizes)
        self.sizes = sizes
        self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
        self.weights = [np.random.randn(y, x)
                        for x, y in zip(sizes[:-1], sizes[1:])]

 

self.sizes = sizes
sizesは、それぞれの層におけるニューロンの数を表しています。 

もし1層目に2つのニューロン、2層目に3つのニューロン

最終層に1つのニューロンを持つNetworkを作りたいならば、

以下のようにコードを定義します。

 

net = Network([2, 3, 1])

 

self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
self.weights = [np.random.randn(y, x)
                        for x, y in zip(sizes[:-1], sizes[1:])]
Networkの中のバイアスと重みは、Numpyのnp.random.randnによって

生成された平均値0・標準偏差1のガウス分布の乱数に初期化されます。

この初期化のための乱数は、確率的勾配降下法の開始点として使用します。

 

バイアスは後半の層から出力を計算するときにだけ使われるので、

入力層のニューロンのバイアスは省略する仮定をしている

ことについて注意してください。

 

Numpy内の行列のリストとしてバイアスと重みは

保存されることについても注意してください。

なので、net.weights[1]は、

2層目と3層目をつなぐ重みを保存するNumpyの行列です。

Pythonのインデックスは 0から開始されるので、

1層目と2層目を繋ぐ重みではありません。)

 

net.weights[1]は、ここではという行列として示しましょう。

それは、Wjkという行列で表現されていて、

2層目の番目のニューロンと3層目の番目のニューロンを繋ぐ重みです。

このの順序は奇妙に見えるかもしれません。

確かにを交換するほうが理に適っていそうです。

この順序の大きな利点は、ニューロン3層目の

活性化のベクトルは以下を意味することです。

f:id:hatakeka:20170623105558p:plain(1)

この式では、は2層目の活性化のベクトルです。

aを得るために、私たちはと重み行列を掛け算し、

バイアスのベクトルを足し算します。

 

私たちは、ベクトルwa+bに関数を作用させます。

(これは関数のvectorizingと呼ばれます。) 等式 (1) が

シグモイドニューロンの出力を計算するための 等式 (2)

f:id:hatakeka:20170623105838p:plain(2)

 

 と同じ結果になることを確認するのは簡単です。

シグモイド関数はベクトル形式でNumpyを使って定義します。

 

def sigmoid(z):
    """The sigmoid function."""
    return 1.0/(1.0+np.exp(-z))

ネットワークの入力a が与えられたら、

対応した出力を返すfeedforwardNetworkクラスに追加します。

このメソッドは、層ごとに等式 (1) を適用します。

    def feedforward(self, a):
        """Return the output of the network if ``a`` is input."""
        for b, w in zip(self.biases, self.weights):
            a = sigmoid(np.dot(w, a)+b)
        return a

Networkにしてほしいことは学習することです。

そのために確率的勾配降下法(SGD)を使用します。

コードはここに記します。

 


    def SGD(self, training_data, epochs, mini_batch_size, eta,
            test_data=None):
        training_data = list(training_data)
        n = len(training_data)

        if test_data:
            test_data = list(test_data)
            n_test = len(test_data)

        for j in range(epochs):
            random.shuffle(training_data)
            mini_batches = [
                training_data[k:k+mini_batch_size]
                for k in range(0, n, mini_batch_size)]
            for mini_batch in mini_batches:
                self.update_mini_batch(mini_batch, eta)
            if test_data:
                print("Epoch {} : {} / {}".format(j,self.evaluate(test_data),n_test));
            else:
                print("Epoch {} complete".format(j))

 

training_dataは、訓練入力と対応した目的出力の組(x, y)のリストです。

変数epochsmini_batch_sizeは訓練のための世代数と、

サンプリングするときに使用するミニバッチの大きさです。

変数etaは学習率です。

 

if test_data:
                print("Epoch {} : {} / {}".format(j,self.evaluate(test_data),n_test));

もしオプションの引数test_dataがある場合、

プログラムは各訓練のエポックのあとにネットワークを評価して、

現在の進行状況を出力します。

 

 for j in range(epochs):
            random.shuffle(training_data)

各エポックでは、訓練データをランダムにシャッフルすることによって開始し、

mini_batches = [
                training_data[k:k+mini_batch_size]
                for k in range(0, n, mini_batch_size)]

適切なサイズのミニバッチに分割します。

このコードは、訓練データからランダムにサンプルする簡単な方法になります。

 for mini_batch in mini_batches:
                self.update_mini_batch(mini_batch, eta)

各ミニバッチに、勾配降下法を1ステップ実行します。

これは、コードself.update_mini_batch(mini_batch, eta)によって行われ、

ミニバッチの訓練データだけを使用して勾配降下法を実行し、

ネットワークの重みとバイアスを更新します。

ここに、update_mini_batchのコードを示します。


    def update_mini_batch(self, mini_batch, eta):
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        for x, y in mini_batch:
            delta_nabla_b, delta_nabla_w = self.backprop(x, y)
            nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
            nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
        self.weights = [w-(eta/len(mini_batch))*nw
                        for w, nw in zip(self.weights, nabla_w)]
        self.biases = [b-(eta/len(mini_batch))*nb
                       for b, nb in zip(self.biases, nabla_b)]

作業の多くは下記のコードで行われます
delta_nabla_b, delta_nabla_w = self.backprop(x, y)

このコードは、コスト関数の勾配を計算する高速な方法である
誤差逆伝播法(backpropagation)アルゴリズムを起動する部分です。
update_mini_batchは単純にミニバッチ内の訓練データごとに勾配を計算し、
self.weightsself.biasesを適切に更新します。

 

この行では、backpropメソッドを利用して

偏微分f:id:hatakeka:20170623173954p:plain f:id:hatakeka:20170623174004p:plainを計算しています。


    def backprop(self, x, y):
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        # feedforward
        activation = x
        activations = [x] # list to store all the activations, layer by layer
        zs = [] # list to store all the z vectors, layer by layer
        for b, w in zip(self.biases, self.weights):
            z = np.dot(w, activation)+b
            zs.append(z)
            activation = sigmoid(z)
            activations.append(activation)
        # backward pass
        delta = self.cost_derivative(activations[-1], y) * \
            sigmoid_prime(zs[-1])
        nabla_b[-1] = delta
        nabla_w[-1] = np.dot(delta, activations[-2].transpose())
for l in range(2, self.num_layers): z = zs[-l] sp = sigmoid_prime(z) delta = np.dot(self.weights[-l+1].transpose(), delta) * sp nabla_b[-l] = delta nabla_w[-l] = np.dot(delta, activations[-l-1].transpose()) return (nabla_b, nabla_w)


def cost_derivative(self, output_activations, y): """Return the vector of partial derivatives \partial C_x / \partial a for the output activations.""" return (output_activations-y)

def sigmoid_prime(z):
    """Derivative of the sigmoid function."""
    return sigmoid(z)*(1-sigmoid(z))

self.backpropは勾配を計算することを手助けするためのいくつかの

追加機能を使用しています。

σ関数の導関数を計算するsigmoid_primeと、

ベクトル形式のself.cost_derivativeです。

 

 

self.backprop逆伝播と呼ばれる

コスト関数の勾配を高速に計算するアルゴリズムです。

 

逆伝播アルゴリズムはもともと1970年代に導入されました。

しかし逆伝播が評価されたのは、 David Rumelhart・ Geoffrey Hinton・ Ronald Williams による1986年の著名な論文が登場してからでした。

その論文では、逆伝播を用いると既存の学習方法よりもずっと

早く学習できる事をいくつかのニューラルネットワークに対して示し、

それまでニューラルネットワークでは解けなかった問題が解ける事を示しました。

今日では、逆伝播はニューラルネットワークを学習させる便利なアルゴリズムです。

 

逆伝播の本質はコスト関数のネットワークの重み(もしくはバイアス

に関する偏微分 ()です。

 

 

ニューラルネットワーク中の重みの表記は

f:id:hatakeka:20170623113805p:plain と表す。

(l1)番目の層の番目のニューロンから、

番目の層の番目のニューロンへの接続に対する重みを表します。

例えば下図は、2番目の層の4番目のニューロンから、

3番目の層の2番目のニューロンへの接続の重みを表します。

この表記方法は最初は面倒で、

使いこなすのにある程度の練習が必要かもしれません。

しかし、少し頑張ればこの表記方法は簡単で自然だと感じるようになるはずです。

この表記方法で若干曲者なのが、の順番です。 

jを入力ニューロンを出力ニューロンとする方が

理にかなっていると思うかもしれませんが、 実際には逆にしています。

f:id:hatakeka:20170623162429p:plain

 

ニューラルネットワークのバイアスと活性についても似た表記方法を導入します。 

f:id:hatakeka:20170623114404p:plain番目の層のj番目のニューロンのバイアスを表します。

 

f:id:hatakeka:20170623114413p:plain番目の層のj番目のニューロンの活性を表します。 

 下図はこれらの表記方法の利用例です。

 

f:id:hatakeka:20170623114522p:plain

 

これらの表記を用いると、番目の層の番目のニューロンの活性は、

(l1)番目の層の活性と以下の式で関係付けられます

f:id:hatakeka:20170623114638p:plain(3)

式 (2)

f:id:hatakeka:20170623105838p:plainと比較してみてください。

 

ここで、和は番目の層の全てのニューロンについて足しています。

この式を行列で書き直すため、各層に対し重み行列f:id:hatakeka:20170623114842p:plainを定義します。

重み行列f:id:hatakeka:20170623114842p:plainの各要素は番目の層のニューロンを終点とする接続の重みです。

すなわち、j行目列目の要素をf:id:hatakeka:20170623113805p:plainとします。

同様に、各層に対し、バイアスベクトルf:id:hatakeka:20170623115245p:plainを定義します。

おそらく想像できると思いますが、バイアスベクトルの要素はf:id:hatakeka:20170623114404p:plain達で、 

l番目の層の各ニューロンに対し1つの行列要素が伴います。

最後に、活性ベクトルf:id:hatakeka:20170623115308p:plainを活性f:id:hatakeka:20170623114413p:plain達で定義します。式(3)

f:id:hatakeka:20170623114638p:plain

を行列形式に書き直すのに必要な最後の要素は、σなどの関数のベクトル化です。

要点をまとめると、のような関数をベクトルvの各要素に

適用したいというのがアイデアです。

このような各要素への関数適用にはσ(v)という自然な表記を用います。

つまり、σ(v)の各要素はf:id:hatakeka:20170623115738p:plainです。

例えばf:id:hatakeka:20170623115753p:plainとすると、次のようになります。

 

f:id:hatakeka:20170623115854p:plain

 

すなわち、ベクトル化したfはベクトルの各要素を2乗します。

この表記方法を用いると、式 (3)

f:id:hatakeka:20170623114638p:plain

は次のような美しくコンパクトなベクトル形式で書けます。

f:id:hatakeka:20170623120029p:plain(4)

この表現を用いると、ある層の活性とその前の層の活性との関係を俯瞰できます。

我々が行っているのは活性に対し重み行列を掛け、バイアスベクトルを足し、

最後に関数を適用するだけです。

 

f:id:hatakeka:20170623115308p:plainの計算のために式(4)f:id:hatakeka:20170623120029p:plainを利用する時には、

途中でf:id:hatakeka:20170623120555p:plainを計算しています。

 この値は後の議論で有用なので名前をつけておく価値があります。

f:id:hatakeka:20170623120640p:plain番目の層に対する重みつき入力と呼ぶことにします。

式(4)f:id:hatakeka:20170623120029p:plainをしばしば重み付き入力を用いて

 

f:id:hatakeka:20170623120808p:plainとも書きます。f:id:hatakeka:20170623120640p:plainの要素はf:id:hatakeka:20170623120853p:plain

と書ける事にも注意してください。つまり、f:id:hatakeka:20170623120941p:plain番目の層の番目の

ニューロンが持つ活性関数へ与える重みつき入力です。

 

逆伝播の目標はニューラルネットワーク中の任意の重みwまたは

バイアスに関するコスト関数偏微分

すなわちbの計算です。

 

逆伝播が機能するには、コスト関数の形について2つの仮定を置く必要があります。

それらの仮定を述べる前に、コスト関数の例を念頭に置くのが良いでしょう。

 

2乗コスト関数をここでは考えます。

f:id:hatakeka:20170623121708p:plain

ここで、は訓練例の総数、和は個々の訓練例について足しあわせたもの、

は対応する目標の出力、ニューラルネットワークの層数、

f:id:hatakeka:20170623121918p:plainを入力した時のニューラルネットワークの出力のベクトルです。

 

では、逆伝播を適用するために、コスト関数に置く仮定は

どのようなものでしょうか。

1つ目の仮定はコスト関数は個々の訓練例に対するコスト関数f:id:hatakeka:20170623163007p:plainの平均  

f:id:hatakeka:20170623163037p:plainで書かれているという事です。

2乗コスト関数ではこの仮定が成立しています。

それには1つの訓練例に対するコスト関数をf:id:hatakeka:20170623163108p:plainとすれば良い。

 この仮定が必要となる理由は、逆伝播によって計算できるのは

個々の訓練例に対する偏微分Cx/bだからです。

コスト関数の偏微分は全訓練例についての平均を取ることで得られます。

この仮定を念頭に置き、私達は訓練例を1つ固定していると仮定し、コストf:id:hatakeka:20170623163007p:plain

を添字を除いてと書くことにします。

最終的に除いたは元に戻しますが、当面は記法が煩わしいので

暗にが書かれていると考えます。

 

コスト関数に課す2つ目の仮定は、コスト関数は

ニューラルネットワークの出力の関数で書かれているという仮定です。

例えば、2乗誤差関数はこの要求を満たしています、

それは1つの訓練例に対する誤差は以下のように書かれるためです

f:id:hatakeka:20170623163605p:plain

もちろんこのコスト関数は目標とする出力にも依存しています。

コスト関数をの関数とみなさない事を不思議に思うかもしれません。

しかし、訓練例を固定する事で、出力も固定している事に注意してください。

つまり、出力は重みやバイアスをどのように変化させた所で

変化させられる量ではなく、ニューラルネットが学習するものではありません。

ですので、Cを出力の活性f:id:hatakeka:20170623163715p:plain単独の関数とみなし、yは関数を定義するための

単なるパラメータとみなすのは意味のある問題設定です。

 

逆伝播アルゴリズムは、ベクトルの足し算やベクトルと行列の掛け算など、

一般的な代数操作に基づいています。

しかし、その中で1つあまり一般的ではない操作があります。

sが同じ次元のベクトルとした時、を2つのベクトルの

要素ごとの積とします。

つまり、stの要素はf:id:hatakeka:20170623164050p:plainです。 例えば、

f:id:hatakeka:20170623164037p:plain

です。 この種の要素ごとの積はしばしばアダマール積、

もしくはシューア積と呼ばれます。

私達はアダマール積と呼ぶことにします。

よく出来た行列ライブラリにはアダマール積の高速な実装が用意されており、

逆伝播を実装する際に手軽に利用できます。

 

 

逆伝播の基礎となる4つの式

f:id:hatakeka:20170623174254p:plain

 

逆伝播は重みとバイアスの値を変えた時にコスト関

数がどのように変化するかを把握する方法です。

これは究極的にはf:id:hatakeka:20170623164658p:plainf:id:hatakeka:20170623164706p:plainとを計算する事を意味します。

これらの偏微分を計算する為にまずは中間的な値f:id:hatakeka:20170623164757p:plainを導入します。

この値はl番目の層のj番目のニューロンの誤差と呼びます。

逆伝播の仕組みを見るとf:id:hatakeka:20170623164757p:plainf:id:hatakeka:20170623164658p:plainf:id:hatakeka:20170623164706p:plain

と関連づける方法が得られます。

番目の層の番目のニューロンの誤差f:id:hatakeka:20170623164757p:plainを以下のように定義します

f:id:hatakeka:20170623165114p:plain

慣習に沿って、f:id:hatakeka:20170623165215p:plain番目の層の誤差からなるベクトルを表します。

逆伝播により、各層でのf:id:hatakeka:20170623165215p:plainを計算し、

これらを真に興味のあるf:id:hatakeka:20170623164658p:plainf:id:hatakeka:20170623164706p:plainと関連付けることができます。

なぜ重みつき入力f:id:hatakeka:20170623120941p:plainが変わるのか疑問に思うかもしれません。 

確かに、出力活性f:id:hatakeka:20170623114413p:plainを変化させ、その結果のf:id:hatakeka:20170623165600p:plainを誤差の指標として

用いる方が自然かもしれません。

しかし、やってみるとわかるのですが、誤差逆伝播の表示が

数学的に若干複雑になっていますので、我々は誤差の指標として

f:id:hatakeka:20170623165114p:plain

を用いることにしています。

 

攻略計画 逆伝播は4つの基本的な式を基礎とします。

これらを組み合わせると、誤差f:id:hatakeka:20170623165215p:plainとコスト関数の勾配を計算ができます。

以下でその4つの式を挙げていきますが、1点注意があります:

これらの式の意味をすぐに消化できると期待しない方が良いでしょう。

そのように期待するとがっかりするかもしれません。

逆伝播は内容が豊富であり、

これらの式は相当の時間と忍耐がかけて徐々に理解できていくものです。

幸いなことに、ここで辛抱しておくと後々何度も報われることになります。

この節の議論はスタート地点に過ぎませんが、逆伝播の式を深く

理解する過程の中で役に立つもののはずです。

 

誤差逆伝播の式をより深く理解する方法の概略は以下の通りです。

まず、 これらの式の手短な証明を示します。

この証明を見ればなぜこれらの式が正しいのかを理解しやすくなります。

その後、これらの式を擬似コードで書き直し、 その擬似コード

どのように実装できるかを実際のPythonのコードで示します。

 


#ミニバッチ1つ分に逆伝播を用いた勾配降下法を適用し、
#ニューラルネットワークの重みとバイアスを更新する。
#"mini_batch"はタプル"(x, y)"のリストで"、
#eta"は学習率。
def update_mini_batch(self, mini_batch, eta): """Update the network's weights and biases by applying gradient descent using backpropagation to a single mini batch. The ``mini_batch`` is a list of tuples ``(x, y)``, and ``eta`` is the learning rate.""" nabla_b = [np.zeros(b.shape) for b in self.biases] nabla_w = [np.zeros(w.shape) for w in self.weights] for x, y in mini_batch: delta_nabla_b, delta_nabla_w = self.backprop(x, y) nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)] nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)] self.weights = [w-(eta/len(mini_batch))*nw for w, nw in zip(self.weights, nabla_w)] self.biases = [b-(eta/len(mini_batch))*nb for b, nb in zip(self.biases, nabla_b)] def backprop(self, x, y): """Return a tuple ``(nabla_b, nabla_w)`` representing the gradient for the cost function C_x. ``nabla_b`` and ``nabla_w`` are layer-by-layer lists of numpy arrays, similar to ``self.biases`` and ``self.weights``.""" nabla_b = [np.zeros(b.shape) for b in self.biases] nabla_w = [np.zeros(w.shape) for w in self.weights] # feedforward activation = x activations = [x] # list to store all the activations, layer by layer zs = [] # list to store all the z vectors, layer by layer for b, w in zip(self.biases, self.weights): z = np.dot(w, activation)+b zs.append(z) activation = sigmoid(z) activations.append(activation) # backward pass delta = self.cost_derivative(activations[-1], y) * \ sigmoid_prime(zs[-1]) nabla_b[-1] = delta nabla_w[-1] = np.dot(delta, activations[-2].transpose()) # Note that the variable l in the loop below is used a little # differently to the notation in Chapter 2 of the book. Here, # l = 1 means the last layer of neurons, l = 2 is the # second-last layer, and so on. It's a renumbering of the # scheme in the book, used here to take advantage of the fact # that Python can use negative indices in lists. for l in range(2, self.num_layers): z = zs[-l] sp = sigmoid_prime(z) delta = np.dot(self.weights[-l+1].transpose(), delta) * sp nabla_b[-l] = delta nabla_w[-l] = np.dot(delta, activations[-l-1].transpose()) return (nabla_b, nabla_w)

 

アルゴリズムの挙動に対する直感を養う為に、

ニューラルネットワーク内の適当な重みf:id:hatakeka:20170623113805p:plainに微小な

変化 Δf:id:hatakeka:20170623113805p:plainを施してみましょう:

重みの変化により、対応するニューロンの出力活性が変化します:

この変化は引き続いて、次の層のすべての出力活性に変化を引き起こします:

これらの変化はさらに次の層の変化を引き起こします。

これを繰り返して最終層、そしてコスト関数を変化させます:

コスト関数の変化は重みの変化Δf:id:hatakeka:20170623113805p:plainと次式で関連付けられます

f:id:hatakeka:20170623171158p:plain(6)

この式から、f:id:hatakeka:20170623171304p:plainを計算するのに考えられるアプローチとして

次の方法が示唆されます。すなわち、f:id:hatakeka:20170623113805p:plainの微小な変化が

ニューラルネットワークを伝播し、その結果の微小な変化を

引き起こす様子を丁寧に追跡するという方法です。

もしそれができたら、伝播経路の途中にあるすべてのものを、

簡単に計算できる変数で表現する事で、

f:id:hatakeka:20170623171614p:plain を計算できるはずです。

このアイデアを実行に移してみましょう。

重みがΔf:id:hatakeka:20170623113805p:plainだけ変化する事で番目の層の番目の

ニューロンの活性に微小な変化f:id:hatakeka:20170623114413p:plain が発生します。 この変化は

f:id:hatakeka:20170623171828p:plain(5)

。活性の変化f:id:hatakeka:20170623114413p:plain は次の層、すなわちl+1番目の

層のすべての活性に変化を引き起こします。

私達はこれらの活性の中の1つ、例えばf:id:hatakeka:20170623171918p:plainがどのような影響を

受けるかのみに注目します。

f:id:hatakeka:20170623114413p:plainは次のような変化を引き起こします

f:id:hatakeka:20170623172044p:plain

式(5)内の表式をこれで置き換えると、

f:id:hatakeka:20170623172225p:plain

が得られます。 もちろん今度はΔf:id:hatakeka:20170623171918p:plain

が、次の層の活性に変化を引き起こします。

実際には、f:id:hatakeka:20170623113805p:plainからまでのパスのうちの1つを考えると、

このパスでは活性のそれぞれの変化が次の活性の変化を引き起こし、

最終的に出力でのコストの変化を引き起こしています。 

もしこのパスが

f:id:hatakeka:20170623172355p:plain

を通るとしたら、得られる表式は

 

f:id:hatakeka:20170623172424p:plain

となります。すなわち、ニューロンを通過するごとにの形の項が追加され、

最後にf:id:hatakeka:20170623172447p:plainの項が付け加わります。 

この値はの変化のうち、特定のパス内にある活性の変化に由来するものです。 

f:id:hatakeka:20170623113805p:plainの変化を伝播しコストに影響を与えるパスは他にもたくさんあり、

この式はその中の1つしか考慮していません。 

Cの変化の合計を計算するには、最初の重みと最後のコストの間で

取りうる全てのパスについて和を取れば良いです。すなわち、

f:id:hatakeka:20170623172724p:plain

ここで、和はパスを通る中間ニューロンの選び方として考えられる

全体について足し合わせます。式(6)

f:id:hatakeka:20170623171158p:plain

と比較すると、

f:id:hatakeka:20170623172957p:plain(7)

とわかります。式(7)は一見すると複雑そうに見えます。

 しかし、これには直感的な良い解釈があります。

私達は今、ニューラルネットワーク内の重みに関するの変化率を計算しています。

ニューラルネットワーク内の2つのニューロンを繋ぐ全ての枝に対して、

変化率の因子が付随している事がこの式から分かります。

その因子は一方のニューロンの活性に関する、もう一端の

ニューロンの活性の偏微分です。 ただし、先頭の重みから

第1層目のニューロンに接続している枝には始点に

ニューロンが接続していないですが、この枝に対する変化率の因子は

f:id:hatakeka:20170623173110p:plainです。 パスに対する変化率の因子は、

単純にパス内に含まれる変化率の因子を全て掛けたものとします。

そして、 f:id:hatakeka:20170623173150p:plainに対する変化率の合計は最初の重みから

最後のコストへ向かう全てのパスについての変化率の因子を

足しあわせたものです。

下図では1つのパスについてこの手順を図示しています。 

 

これまでの議論は、ニューラルネットワーク内の重みを

摂動させた時に何が起こっているかを発見的に考察する方法でした。

この方向で議論をさらに進める方法を簡単に紹介します。 まず、式(7)

f:id:hatakeka:20170623172957p:plain

内の偏微分はすべて具体的な表式を与えます。

これは若干の計算をするだけで難しくはありません。

これを行うと、添字について和を取る操作を行列操作に

書き直す事ができるようになります。

 

退屈で忍耐が必要な作業かも知れませんが、賢い洞察は必要ありません。

その後できるだけ式を簡単にしていくと、なんと最終的に得られる式は

逆伝播アルゴリズムそのものです!

つまり、逆伝播アルゴリズムは全パスの変化率の因子を総和を

計算する方法とみなすことができるのです。

少し別の表現をすると、逆伝播アルゴリズムは重み(とバイアス)に

与えた小さな摂動がニューラルネットワークを伝播しながら出力に到達し、

コストに影響を及ぼす様子を追跡する為の賢い方法だと言えます。

 

ここでは上の議論には立ち入りません。

議論の詳細を全て追うのは非常にややこしく、相当の注意が必要です。

もし挑戦する意欲があれば、試してみるとよいでしょう。

もしそうでなくても、以上の議論で誤差逆伝播が達成しようしている

事について何かの洞察が得られる事を期待します。

 

逆伝播が速いアルゴリズムであるとはどういう意味か?

どういう意味で逆伝播は速いアルゴリズムか。

これに答える為に、勾配を計算する別のアプローチを考えてみましょう。

初期の時代のニューラルネットワーク研究を想像してみてください。

おそらく1950年代か60年代だと思いますが、あなたは学習への

勾配降下法の適用を考えている世界で最初の研究者です!

あなたの考えがうまくいくかを確かめるには、コスト関数の勾配を

計算する方法が必要です。 微積分学の知識を思い出して、勾配の計算に

連鎖律が使うかを検討しています。

しかし、少しごにょごにょと計算してみると、式は複雑そうなので

がっかりしてしまいます。 そこで、別のアプローチを探します。

コスト関数を重みのみの関数とみなし、C=C(w)と考えることにしました

(バイアスについてはすぐ後で考えます)。

重みをw1,w2,と番号付けし、特定の重みについてC/wjを計算します。 すぐに思いつくのは近似

f:id:hatakeka:20170623174559p:plain(8)

を利用する方法です。 ここで、は微小な正の数で、

ej方向の単位ベクトルです。 言い換えれば、C/wjを計算する為に

2つの若干異なるでコストの値を計算し、式(8)を適用します。

同じアイデアでバイアスについての偏微分C/bにも計算できます。

 

このアプローチはよさそうに見えます。 発想がシンプルな上、

実装も数行のコードで実現できとても簡単です。

連鎖律を用いて勾配を計算するアイデアよりもよっぽど有望なように思えます!

 

このアプローチは有望そうですが、残念ながらこのコードを実装してみると

とてつもなく遅い事がわかります。 なぜかを理解する為に、

ニューラルネットワーク内に100万個の重みがあると想像してみてください。

すると、各重みに対して計算するには、C(w+ϵej)の計算が必要です。

 

これには、勾配計算時に異なる値でのコスト関数計算が100万回必要で、

各訓練例ごとに100万回の順伝播が必要な事を意味します。

 の計算も必要なので、結局ニューラルネットワーク内伝播回数は100万1回です。

 

逆伝播の賢い所は、たった1回の順伝播とそれに続く1回の逆伝播ですべての偏微分を同時に計算できる点です。

逆伝播の計算コストは大雑把には順伝播と同程度です。

従って、逆伝播の合計のコストはニューラルネットワーク全体への

順伝播約2回分です。式(8)

f:id:hatakeka:20170623174559p:plain

に基づくアプローチで必要だった100万1回の順伝播と比較してみてください! 逆伝播は一見に基づく方法よりも複雑ですが、実際にはずっと、ずっと高速なのです。

 

この高速化は1986年に始めて真価がわかり、ニューラルネットワーク

解ける問題の幅を大きく広げ、その結果多くの人が次々に

ニューラルネットワークに押しかけました。

もちろん、逆伝播は万能薬ではありません。

特にディープニューラルネットワーク、すなわち多くの隠れ層を持つ

ネットワークの学習への逆伝播の適用においては、

1980年代後半には既に壁にぶつかっていました。

現代のコンピュータや新しい賢いアイデアにより、

逆伝播を用いてディープニューラルネットワークを訓練する事が

可能になったのです。

 

 

参考URL

ニューラルネットワークと深層学習

 

以上。