barus's diary

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

C++でニューラルネットワークによる手書き文字認識(PythonのNetwork.pyをC++で書き起こし)その② for VS2017 VC++

前回の記事の続き

 

Pythonで書かれたニューラルネットワークのコード

network.pyをVC++で書き起こししています。

 

VC++ の全コードは前回の記事を見てください。

Pythonで書かれたコードはここを見てください。

 

 

5.ニューラルネットワークの処理

void network::SGD(
  vector<MNIST_compact> training_data, //訓練データ
  int epochs, //世代(学習回数)
  int mini_batch_size, //ミニバッチサイズ
  double eta, //学習率
  vector<MNIST_compact> test_data) //テストデータ

 

ここでの処理は 

//----------------------------------------------
//学習回数
//----------------------------------------------
for (int i = 0; epochs > i; i++)

以下の処理をepochs回数繰り返す。

 

 

Pythonで書かれたコード

random.shuffle(training_data)

VC++のコード

std::shuffle(training_data.begin(), training_data.end(), std::mt19937());

訓練データをシャッフルしている。

 

Pythonで書かれたコード

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

VC++のコード

vector<vector<MNIST_compact>> mini_batches;
for (int j = 0; n > j; j += mini_batch_size)
{
  vector<MNIST_compact> mini_batch;
  for (int k = 0; mini_batch_size > k; k++)
  {
   if (k+j > n - 1)break;
   mini_batch.push_back(training_data.at(k + j));
  }
}

0~nまでをmini_batch_size毎に要素を切り出して

vector<vector<MNIST_compact>> 

の型に格納してます。

 

 

Pythonで書かれたコード
for mini_batch in mini_batches :
  self.update_mini_batch(mini_batch, eta)

VC++のコード

Delta delta;
for (int j = 0; mini_batches.size() > j; j++)
{
  vector<MNIST_compact> mini_batch = mini_batches.at(j);
  for (int k = 0; mini_batch.size() > k; k++)
  {
    delta = update_mini_batch(mini_batch, eta);
  }

}

0~nまでをmini_batch_size毎に要素を切り出して格納

したものずつupdate_mini_batch()処理している。

このバッチ処理

vector<vector<MNIST_compact>> 

の配列である。

 

6.ミニバッチ処理update_mini_batch()

Delta network::update_mini_batch(vector<MNIST_compact> mini_batch, double eta)

訓練データが入った

vector<vector<MNIST_compact>> 

の配列について処理を行う。

ミニバッチサイズは、ハードコーディングで10なので

配列10、つまり訓練画像データ10個づつ

バイアスと重みを更新することをここでは

バッチ処理と言っています。

 

Pythonで書かれたコード

#nabla_b nabla_wを0で初期化
nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]

VC++のコード

auto itr_neurons = _neurons.begin();
neurons neuron = *itr_neurons;
delta._biases = neuron._biases;
delta._weights = neuron._weights;
//deltaの要素に0をセットする。
matrix_zeros(&delta);

delta(バイアスと重み)の要素に0をセットする。

Input層(=x)は28x28の画像訓練データを

使用しているので784で固定。

文字列も0~9の範囲なので10個で固定

隠れ層が任意の数字となる。

イメージは以下のような感じとなります。

f:id:hatakeka:20170707111724p:plain

仮にネットワークの構成が「784、2、3、10」の場合

以下のような重みwとバイアスbとなる。

f:id:hatakeka:20170707113520p:plain

(L=0以外の)L層のj行目列目の重み(weight)をf:id:hatakeka:20170623113805p:plain

(L=0以外の)バイアス(biases)はf:id:hatakeka:20170623114404p:plain

活性(Activation)はf:id:hatakeka:20170623114413p:plain

f:id:hatakeka:20170707142010p:plain

として表す。

 

Pythonで書かれたコード
//xはinput,yはoutput
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)]

VC++のコード

for (int i = 0; mini_batch.size() > i; i++)
{
 Delta delta_nabla = backprop(mini_batch.at(i));
 matrix_plus(&delta, delta_nabla);
}

このfor文でΔf:id:hatakeka:20170623113805p:plainや f:id:hatakeka:20170623114413p:plainをbackprop()関数で計算したものを配列にいれて

最後にmatrix_plus()にて、ネットワーク全体を更新している。

この重みとバイアスの(Δf:id:hatakeka:20170623113805p:plainf:id:hatakeka:20170623114413p:plain)微小変化を

backprop()関数にて求めている。

 

7.バイアスや重みの微小変化を求める。backprop()

Delta network::backprop(MNIST_compact mini)

backprop()関数では

最後の出力から、f:id:hatakeka:20170623164658p:plainf:id:hatakeka:20170623164706p:plainが求まるので

逆伝播(最後のL層から、Lー1、・・・1層まで、L=0は含まない)

させて各層でのf:id:hatakeka:20170623165215p:plainを計算して、各層のΔf:id:hatakeka:20170623113805p:plainや f:id:hatakeka:20170623114413p:plainを求めている。

 

 

L層の活性(Activation=f:id:hatakeka:20170623114413p:plain)は L-1層

(aは入力L=0を含む、但しwとbはL>0)の活性を用いて

f:id:hatakeka:20170623114638p:plain

として表される。過去の記事の復習となりますが

この式は以下のような美しいベクトルで表される。

f:id:hatakeka:20170623120029p:plain

f:id:hatakeka:20170623115308p:plainを求める為に、途中でf:id:hatakeka:20170623120555p:plainを計算しています。

f:id:hatakeka:20170623120808p:plainとも書きます。

f:id:hatakeka:20170623120640p:plainの要素はf:id:hatakeka:20170623120853p:plainと書ける。

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

f:id:hatakeka:20170623165114p:plain

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

  

Pythonで書かれたコード
//#nabla_b nabla_wを0で初期化
nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]

VC++のコード
Delta delta;

auto itr_neurons = _neurons.begin();
neurons neuron = *itr_neurons;
delta._biases = neuron._biases;
delta._weights = neuron._weights;
//deltaの要素に0をセットする。
matrix_zeros(&delta);

バイアスと重みの初期化。

 

 VC++のコード
vector<double> activation;
vector<double> y; //回答
//トレーニングデータ(1つ)
for (int i = 0; mini.images.size() > i; i++)
{
 for (int j = 0; mini.images[i].size() > j; j++)
 {
  double data = static_cast<double>(mini.images[i][j]);
  if (data > 0)data = 1;//入力マスを1に
  else data = 0;
  activation.push_back(data);
 }
  y = convert_to_arry(mini.labels.at(i));
}

Pythonで書かれたコードの場合とVC++のコードの場合では

利用している訓練データは異なっている。

Pythonで利用している訓練データは mnist.pkl.gz を用いており

出力yの配列は訓練データに含まれているのでそれを用いているが

VC++のコードで用いた訓練データは

ここtrain-images.idx3-ubyteと、train-labels.idx1-ubyte
を用いていており、解答の配列はないので、

ここでyの配列をわざわざ作成している。

訓練データに関しては、ここを見てください。

 

 

Pythonで書かれたコード

for b, w in zip(self.biases, self.weights) :
 z = np.dot(w, activation) + b
 zs.append(z)
 activation = sigmoid(z)
 activations.append(activation)

 VC++のコード

vector<vector<double>> zs; //1からL層のz値を格納
vector<vector<double>> activations; //1からL層のactivation値を格納
activations.push_back(activation);
for (int i = 0; neuron._biases.size() > i; i++)
{
 vector<double> z;
 //i=L層の w0*a0+b, w1*a1+b, w2*a2+b, wN*aN+b の計算
 matrix_dot(i, &z, &activation, neuron);
 zs.push_back(z);
 activation.clear(); // 先頭から末尾まで削除
 matrix_sigmoid(&activation, z); //z値のsigmoidしたものがL層のactivationになる。
 activations.push_back(activation);
}

ここでは、各層の

f:id:hatakeka:20170623120808p:plain

f:id:hatakeka:20170623120853p:plain

を計算している。

図にすると以下のような感じとなる。 

f:id:hatakeka:20170707143750p:plain

 

Pythonで書かれたコード
delta = self.cost_derivative(activations[-1], y) * \
sigmoid_prime(zs[-1])
nabla_b[-1] = delta
nabla_w[-1] = np.dot(delta, activations[-2].transpose())

 

 VC++のコード
vector<double> out_z;
vector<double> out_activation;
vector<double> delta_c, sigmoidprime;

out_z = matrix_Layer_z(-1, zs); //zs.back(); //最後のL層のアウトプット
out_activation = activations.back(); //最後のL層に入るActivation
sigmoidprime = matrix_sigmoid_prime(out_z);

 

delta_c = cost_derivative(out_activation, y, sigmoidprime);

//L層のバイアス(delta._biases)更新

matrix_Layer_update_biases(-1, &delta, delta_c);

//L層の重み(delta._weight)更新

vector<double> atctivation_front = matrix_Layer_Atctivation(-2, activations);
matrix_Layer_update_weights(-1, &delta, delta_c, atctivation_front);

 

 

ここでは最後のL層の出力yから

f:id:hatakeka:20170623164658p:plainf:id:hatakeka:20170623164706p:plain

を計算している。

なお、Pythonではdeltaとしているが、VC++ではdelta_cとしている。

 

以下はdeltaの出力例である。

f:id:hatakeka:20170707134940p:plain

deltaは配列であることに注意して下さい。

deltaの配列の大きさは各層LのJ(Activation)によって異なりますが

最後の出力層は、数字の0~9の解答なので10で固定です。

 

 

 

Pythonで書かれたコード

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())

 

VC++のコード
int len = neuron._biases.size();
for (int layer = 2; len + 1 > layer; layer++)
{
 vector<double> out_z2;
 vector<double> out_activation2;
 vector<double> sigmoidprime2;
 out_z2 = matrix_Layer_z(-layer, zs);
 sigmoidprime2 = matrix_sigmoid_prime(out_z2);

 //転置
 Delta delta_t = matrix_transpose_weight(-layer + 1, delta);

 //delta更新

 matrix_Layer_update_delta_c(-layer + 1, delta_t, &delta_c, sigmoidprime2);

 //L層のバイアス(delta._biases)更新

 matrix_Layer_update_biases(-layer, &delta, delta_c);

 vector<double> atctivation_front2 = matrix_Layer_Atctivation(-layer-1, activations);
 //L層の重み(delta._weight)更新

 matrix_Layer_update_weights(-layer, &delta, delta_c, atctivation_front2);

}

 

残りのバイアスと重みをL-1層から1まで(L=0は含まない)

逆(出力層から入力層へ)に更新する。(逆伝播)

Pythonで書かれたコードの

self.weights[-l+1].transpose()

VC++では

Delta delta_t = matrix_transpose_weight(-layer + 1, delta);

で処理している。

これは行列の列と行を入れ替えており転置という。

 f:id:hatakeka:20170706132450p:plain

例えばネットワーク層が[784、10、10]の場合は

転置しなくても計算可能(エラーとならない)だが

ネットワーク層が、例えば[784、3、8、10]の場合

逆(出力層から入力層)に遡る場合

8x10の行列を、10x8の行列として

出力層10x(行列10x8)で計算しないと

計算できない(エラーとなる)ために行列の転置をしている。

 

行列の転置についての詳細は過去の記事を参照して下さい。

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

f:id:hatakeka:20170623165114p:plain

この誤差f:id:hatakeka:20170623164757p:plainと、Δf:id:hatakeka:20170623113805p:plainや f:id:hatakeka:20170623114413p:plainを求めている。

なお、Pythonではdeltaとしているが、VC++ではdelta_cとしている。

  

f:id:hatakeka:20170707142246p:plain

 

 

 

 

8.feedforward()関数よりテストデータの出力を求めて評価する。

void network::SGD(
  vector<MNIST_compact> training_data, //訓練データ
  int epochs, //世代(学習回数)
  int mini_batch_size, //ミニバッチサイズ
  double eta, //学習率
  vector<MNIST_compact> test_data) //テストデータ

//----------------------------------------------
//学習回数
//----------------------------------------------
for (int i = 0; epochs > i; i++)
{
 ~省略~

 //バッチの更新
 Delta delta;
 for (int j = 0; mini_batches.size() > j; j++)
 {
  vector<MNIST_compact> mini_batch = mini_batches.at(j);
  for (int k = 0; mini_batch.size() > k; k++)
  {
   delta = update_mini_batch(mini_batch, eta);
  }

 }
 sprintf(_strEpochresult, "Epoch {%d} : {%d} / {%d}\n", i, evaluate(test_data), n_test);
 rlt.push_back(_strEpochresult);
}

 

ここでvoid network::SGD()関数に戻ってくる。

7までのバッチ処理update_mini_batch(mini_batch, eta);

訓練データによって、ネットワーク全体のバイアスと重みが更新された。

これを用いてevaluate(test_data)でテストデータで出力yを求めている。

 

Pythonで書かれたコード
def evaluate(self, test_data):
 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)

 

VC++のコード
int network::evaluate(vector<MNIST_compact> test_data)
{
 vector<TEST_RESLT> test_results;
 for (int i = 0; test_data.size() > i; i++)
 test_results.push_back(feedforward(test_data[i]));

 //正解の合計
 int sum = 0;
 for (int i = 0; test_results.size() > i; i++)
 if (test_results[i].x == test_results[i].y)sum++;
 return sum;
}

feedforward()関数によって、テストデータを入力し

 

出力を求めて

Pythonで書かれたコード

return sum(int(x == y) for (x, y) in test_results

VC++のコード

if (test_results[i].x == test_results[i].y)sum++;

解答を照らし合わせて、その合計値を返している。

 

 

Pythonで書かれたコード

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

VC++のコード
TEST_RESLT network::feedforward(MNIST_compact mini)
{
TEST_RESLT rlt;
 vector<double> activation; //=x
 //vector<double> y; //回答

 //トレーニングデータ(1つ)
 for (int i = 0; mini.images.size() > i; i++)
 {
  for (int j = 0; mini.images[i].size() > j; j++)
  {
   double data = static_cast<double>(mini.images[i][j]);
   if (data > 0)data = 1;//入力マス
   else data = 0;
   activation.push_back(data);
  }
 }

 

 vector<vector<double>> activations;
 auto itr_neurons = _neurons.begin();
 neurons neuron = *itr_neurons;
 //最後のLayer層まで発火させる。
 for (int layer = 0; neuron._biases.size() > layer; layer++)
 {
  vector<double> z;
  matrix_dot(layer, &z, &activation, neuron);
  activation.clear(); // 先頭から末尾まで削除
  //z値のsigmoidしたものがL層のactivationになる。

  matrix_sigmoid(&activation, z);

 }

 

 for (int i = 0; activation.size() > i; i++)
 {
  if (max < activation[i]) { max = activation[i]; index_y = i; }
  printf("%f,", activation[i]);
 }
 rlt.x = index_y; //発火させた結果
 rlt.y = mini.labels[0]; //解答 テストの要素はひとつだけ

 printf("発火させた結果=%d, 解答=%d\n", rlt.x, rlt.y);
 return rlt;
}

 

VC++では

訓練データからactivation=xを求めて

for (int layer = 0; neuron._biases.size() > layer; layer++)

にて、0から最後の層までActivation=zを次々に計算している。

if (max < activation[i]) { max = activation[i]; index_y = i; }

にて、最後の層の出力の一番大きい値をインデックス(index_y)にいれて

これを、テストデータの解答としている。

この解答をevaluate()にて答え合わせをしている。上記参照。

 

 

 

補足

void network::Network(vector<int> nets)

のバイアスと重みをの初期値を

メルセンヌ・ツイスターの使用して-1~1でとっていたが

以下のように、ガウス分布にしてみた。

// biase_j = makerandom(neurons._sizes.at(i));

biase_j = makerandom_gaus(neurons._sizes.at(i));

vector<KATA> network::makerandom_gaus(int howmany)
{
 vector<KATA> data;
 data.reserve(howmany);

 using namespace std;
 default_random_engine generator;
 normal_distribution<double> distribution;
 for (int i = 0; i < howmany; ++i) {
  data.push_back(distribution(generator));
 }
 return data;
}

 

 

参考URL

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

 

以上