Sponsored Link

Monthly Archives: 10月 2013

(8) CIFAR10画像セットを見てみる

CIFAR10画像セットを入手

まだMNISTもちゃんと自動認識できていないけれど、他の画像セットにもちょっと興味あり。

そこで CIFAR10 なるカラー画像セットを見てみることにした。提供元はこちら(↓)
https://www.cs.utoronto.ca/~kriz/cifar.html
https://nnet.dogrow.net/wp-content/uploads/2013/10/20131011_01.png

ダウンロードできるファイルは以下の3種類(2013年10月11日時点)、最近Octaveがお気に入りの自分はMATLAB用を選択。
https://nnet.dogrow.net/wp-content/uploads/2013/10/20131011_02.png

CIFAR10画像セットを解凍

早速Linux上で解凍してみる。

[user@dog-server]$ ls
cifar-10-matlab.tar.gz
[user@dog-server]$ tar zxf cifar-10-matlab.tar.gz
[user@dog-server]$ ls
cifar-10-batches-mat  cifar-10-matlab.tar.gz
[user@dog-server]$

すると、以下のような8個のファイルができた。

[user@dog-server]$ ls -l
合計 179148
-rw-r--r--. 1 user user      299  4月 23 05:57 2010 batches.meta.mat
-rw-r--r--. 1 user user 30568167  4月 23 05:50 2010 data_batch_1.mat
-rw-r--r--. 1 user user 30576389  4月 23 05:50 2010 data_batch_2.mat
-rw-r--r--. 1 user user 30572622  4月 23 05:50 2010 data_batch_3.mat
-rw-r--r--. 1 user user 30565850  4月 23 05:50 2010 data_batch_4.mat
-rw-r--r--. 1 user user 30572760  4月 23 05:50 2010 data_batch_5.mat
-rw-r--r--. 1 user user       88  4月 23 05:50 2010 readme.html
-rw-r--r--. 1 user user 30570596  4月 23 05:50 2010 test_batch.mat
[user@dog-server]$

とりあえず.matファイルをOctaveでロードして、中身を順番に眺めてみる。

(1) batches.meta.mat
画像のラベル名称が書かれている。ラベル1は飛行機、ラベル2は自動車、ラベル10はトラックとのこと。

octave:1> whos -file batches.meta.mat
Variables in the file batches.meta.mat:
   Attr Name             Size                     Bytes  Class
   ==== ====             ====                     =====  =====
        label_names     10x1                         50  cell
Total is 10 elements using 50 bytes
octave:2> load('batches.meta.mat')
octave:3> label_names
label_names =
{
  [1,1] = airplane
  [2,1] = automobile
  [3,1] = bird
  [4,1] = cat
  [5,1] = deer
  [6,1] = dog
  [7,1] = frog
  [8,1] = horse
  [9,1] = ship
  [10,1] = truck
}

(2) data_batch_1.mat ~ data_batch_5.mat
学習用の画像+ラベルのセットが10,000個×5ファイルの全60,000個が格納されている。
「3072」は1画像のバイトサイズで、内訳は 32pixel x 32pixel x RGB3色 x uint8 = 3,072[bytes] で計算できる。

octave:5> whos -file data_batch_1.mat
Variables in the file data_batch_1.mat:
   Attr Name             Size                     Bytes  Class
   ==== ====             ====                     =====  =====
        batch_label      1x21                        21  char
        data         10000x3072                30720000  uint8
        labels       10000x1                      10000  uint8
Total is 30730021 elements using 30730021 bytes
octave:6> load('data_batch_1.mat')
octave:7> batch_label
batch_label = training batch 1 of 5

(3) test_batch.mat
テスト用の画像+ラベルのセットが10,000個格納されている。

octave:10> whos -file test_batch.mat
Variables in the file test_batch.mat:
   Attr Name             Size                     Bytes  Class
   ==== ====             ====                     =====  =====
        batch_label      1x20                        20  char
        data         10000x3072                30720000  uint8
        labels       10000x1                      10000  uint8
Total is 30730020 elements using 30730020 bytes
octave:11> load('test_batch.mat')
octave:12> batch_label
batch_label = testing batch 1 of 1

CIFAR10画像セットを見てみる

どんな画像が入っているのか?実際に画像表示してみる。

画像化して眺める手順は (2)MNIST学習画像を見てみる と同じだ。
試しに学習画像の最初の1個をOctave上で画像化してみる。

octave:1> load('data_batch_1.mat')
octave:2> img = data(1,:);
octave:3> size(img)
ans =
      1   3072
octave:4> img=reshape(img(:),32,32,3);
octave:5> size(img)
ans =
   32   32    3
octave:6> imshow(uint8(permute(img,[2 1 3])))

https://nnet.dogrow.net/wp-content/uploads/2013/10/20131011_03.png

なんだ???

よくわからないので先頭データのラベルを見てみる。

octave:8> labels(1)
ans = 6

ラベル番号が6番なので「[6,1] = dog」のようだ。
この画像が犬なのか?

CIFAR10のホームページを見ると、ラベル番号は0~9を付与すると書かれている。

先ほど見た label_names変数の値は 1始まりだった。ラベル番号は 0始まりの6なので、1始まりに直せば 7だから「[7,1] = frog」が正しいラベルだ。
ややこしい…

続けていくつか見てみる。

(2)#10 truck
https://nnet.dogrow.net/wp-content/uploads/2013/10/20131011_04.png
(3)#10 truck
https://nnet.dogrow.net/wp-content/uploads/2013/10/20131011_05.png
(4)#5 deer
https://nnet.dogrow.net/wp-content/uploads/2013/10/20131011_06.png
(5)#2 automobile
https://nnet.dogrow.net/wp-content/uploads/2013/10/20131011_07.png
(6)#2 automobile
https://nnet.dogrow.net/wp-content/uploads/2013/10/20131011_08.png
(7)#3 bird
https://nnet.dogrow.net/wp-content/uploads/2013/10/20131011_09.png
(8)#8 horse
https://nnet.dogrow.net/wp-content/uploads/2013/10/20131011_10.png
(9)#9 ship
https://nnet.dogrow.net/wp-content/uploads/2013/10/20131011_11.png
(10)#4 cat
https://nnet.dogrow.net/wp-content/uploads/2013/10/20131011_12.png

(7) 隠れ層のsigmoidをtanhにしてみる

sigmoidの代わりにtanh

活性化関数は sigmoid の他に tanh も使用するらしい。
入力値に対して、出力値が一定範囲内に制限されるならば、どんな関数でもよいらしい。

両関数の入力に対する出力をグラフにしてみた。tanhの方が縦に長く、横に短いようだ。
x=0付近の変化に敏感に反応するということか?

octave:1> x = [-10:0.1:10];
octave:2> y_sigmoid = 1./(1+exp(-x));
octave:3> y_tanh    = tanh(x);
octave:4> figure;
octave:5> plot(x,y_sigmoid,'b');
octave:6> hold on;
octave:7> plot(x,y_tanh,'r');
octave:8> 
octave:8> title('activation function');
octave:9> xlabel('IN');
octave:10> ylabel('OUT');
octave:11> h = legend ({'sigmoid', 'tanh'}, 'location', 'east');
octave:12> grid on;

20140824_02

実験結果

シンプル構成初版では、全層の活性化関数をsigmoidとしていた。
http://nnet.dogrow.net/?p=43
今回は、隠れ層をtanh, 出力層をsigmoid にしてMNIST自動認識を実行してみる。

(1) 3層 [784]-[16]-[10] 88.4%(-0.6%)

octave:1> NNET_control([784 16 10], 1)
EPOCH No.1
[ 0]  929 /  980 ( 94.8%)
[ 1] 1101 / 1135 ( 97.0%)
[ 2]  904 / 1032 ( 87.6%)
[ 3]  754 / 1010 ( 74.7%)
[ 4]  880 /  982 ( 89.6%)
[ 5]  773 /  892 ( 86.7%)
[ 6]  881 /  958 ( 92.0%)
[ 7]  926 / 1028 ( 90.1%)
[ 8]  815 /  974 ( 83.7%)
[ 9]  873 / 1009 ( 86.5%)
Total  8836 / 10000 ( 88.4%)

(2) 3層 [784]-[24]-[10] 88.8%(-1.1%)

octave:2> NNET_control([784 24 10], 1)
EPOCH No.1
[ 0]  951 /  980 ( 97.0%)
[ 1] 1113 / 1135 ( 98.1%)
[ 2]  873 / 1032 ( 84.6%)
[ 3]  870 / 1010 ( 86.1%)
[ 4]  839 /  982 ( 85.4%)
[ 5]  769 /  892 ( 86.2%)
[ 6]  877 /  958 ( 91.5%)
[ 7]  911 / 1028 ( 88.6%)
[ 8]  835 /  974 ( 85.7%)
[ 9]  847 / 1009 ( 83.9%)
Total  8885 / 10000 ( 88.8%)

(3) 3層 [784]-[32]-[10] 81.8%(-8.8%)

octave:4> NNET_control([784 48 10], 1)
EPOCH No.1
[ 0]  949 /  980 ( 96.8%)
[ 1] 1118 / 1135 ( 98.5%)
[ 2]  924 / 1032 ( 89.5%)
[ 3]    5 / 1010 (  0.5%) ←???
[ 4]  908 /  982 ( 92.5%)
[ 5]  781 /  892 ( 87.6%)
[ 6]  882 /  958 ( 92.1%)
[ 7]  906 / 1028 ( 88.1%)
[ 8]  822 /  974 ( 84.4%)
[ 9]  884 / 1009 ( 87.6%)
Total  8179 / 10000 ( 81.8%)

(4) 3層 [784]-[64]-[10] 89.6%(-1.2%)

octave:5> NNET_control([784 64 10], 1)
EPOCH No.1
[ 0]  945 /  980 ( 96.4%)
[ 1] 1114 / 1135 ( 98.1%)
[ 2]  900 / 1032 ( 87.2%)
[ 3]  915 / 1010 ( 90.6%)
[ 4]  883 /  982 ( 89.9%)
[ 5]  735 /  892 ( 82.4%)
[ 6]  902 /  958 ( 94.2%)
[ 7]  903 / 1028 ( 87.8%)
[ 8]  814 /  974 ( 83.6%)
[ 9]  851 / 1009 ( 84.3%)
Total  8962 / 10000 ( 89.6%)

(5) 3層 [784]-[128]-[10] 83.9%(-7.6%)

octave:6> NNET_control([784 128 10], 1)
EPOCH No.1
[ 0]  964 /  980 ( 98.4%)
[ 1] 1115 / 1135 ( 98.2%)
[ 2]  944 / 1032 ( 91.5%)
[ 3]  915 / 1010 ( 90.6%)
[ 4]  903 /  982 ( 92.0%)
[ 5]  809 /  892 ( 90.7%)
[ 6]  886 /  958 ( 92.5%)
[ 7]  938 / 1028 ( 91.2%)
[ 8]   24 /  974 (  2.5%) ←???
[ 9]  891 / 1009 ( 88.3%)
Total  8389 / 10000 ( 83.9%)

(6) 4層 [784]-[256]-[64]-[10] 72.3%(-19.6%)

octave:7> NNET_control([784 256 64 10], 1)
EPOCH No.1
[ 0]    1 /  980 (  0.1%) ←???
[ 1] 1113 / 1135 ( 98.1%)
[ 2]  905 / 1032 ( 87.7%)
[ 3]    0 / 1010 (  0.0%) ←???
[ 4]  842 /  982 ( 85.7%)
[ 5]  789 /  892 ( 88.5%)
[ 6]  885 /  958 ( 92.4%)
[ 7]  932 / 1028 ( 90.7%)
[ 8]  888 /  974 ( 91.2%)
[ 9]  875 / 1009 ( 86.7%)
Total  7230 / 10000 ( 72.3%)

すべてのレイヤー構成で全sigmoid版のスコアを下回った…
tanhの使い方が間違っているのか?

プログラムのソースコード

前版からの変更は以下の7ファイルのみ。

(1) NNET_control.m

function NNET_control( num_unit_of_each_layer, num_EPOCH )

  % 学習画像・ラベル、テスト画像・ラベルをファイルから読み込み
  [train_img, train_lbl] = load_MNIST( '../data/train-images-idx3-ubyte', '../data/train-labels-idx1-ubyte' );
  [test_img,  test_lbl ] = load_MNIST( '../data/t10k-images-idx3-ubyte',  '../data/t10k-labels-idx1-ubyte'  );

  % 各画像データを 0.0~1.0の範囲に正規化
  train_img = train_img / 255;
  test_img  = test_img  / 255;

  % 指定された層数、ユニット数でニューラルネットワークを作成
  nn = NNET_setup( num_unit_of_each_layer );

  % 指定EPOCH回数だけ繰り返す
  for epoch=1 : num_EPOCH

    % 学習実行
    nn = NNET_learn( nn, train_img, train_lbl );

    % テスト実行
    result = NNET_test( nn, test_img, test_lbl );

    % テスト結果を表示
    printf('\nEPOCH No.%d\n', epoch);
    for i=1: 10
      printf('[%2d] %4d / %4d (%5.1f%%) \n', i-1, result(i,2), result(i,1), result(i,2)/result(i,1)*100);
    end
    sum_result = sum(result,1);
    printf('Total %5d / %5d (%5.1f%%) \n', sum_result(2), sum_result(1), sum_result(2)/sum_result(1)*100);
    fflush(1);
  end
end

(2) NNET_setup.m

function nn = NNET_setup( num_unit_of_each_layer )

  % 乱数生成器を初期化
  rand('seed', 0);

  % 指定された層数を取得
  num_layer = numel( num_unit_of_each_layer );

  % 全層を初期化
  for i=2 : num_layer

    % 現層と前層のユニット数を取得
    num_unit_pre = num_unit_of_each_layer( i - 1 );
    num_unit     = num_unit_of_each_layer( i );

    % 各結合線の荷重を -1~1の一様分布乱数で初期化
    nn.layer{i}.weight = -1 + rand( num_unit, num_unit_pre ) * 2;

    % バイアスを初期化
    nn.layer{i}.bias = zeros( num_unit, 1 );

    % 活性化関数を登録
    if i==num_layer
      % 出力層であればsigmoid
      nn.layer{i}.actfunc  = @act_sigmoid;
      nn.layer{i}.dactfunc = @act_sigmoid_d;
    else
      % 隠れ層であればtanh
      nn.layer{i}.actfunc  = @act_tanh;
      nn.layer{i}.dactfunc = @act_tanh_d;
    end
  end
end

(3) NNET_propagation_forward.m

function nn = NNET_propagation_forward( nn, train_img )

  % 入力層の出力値を記憶
  nn.layer{1}.out = train_img(:);   % [n0][1]

  % 全層について順伝播を実行
  for i=2 : numel(nn.layer)

    % a = Σwz + bias    w[n1][n0] z[n0][1] bias[n1][1]
    nn.layer{i}.actprm = nn.layer{i}.weight * nn.layer{i-1}.out + nn.layer{i}.bias;

    % out = sigmoid(a)   out[n1][1]
    nn.layer{i}.out = nn.layer{i}.actfunc( nn.layer{i}.actprm );
  end
end

(4) NNET_propagation_back.m

function nn = NNET_propagation_back( nn, train_lbl )

  % 層数を取得
  num_layer = numel(nn.layer);

  % 出力層で検出された誤差量と逆伝播する勾配の初期値を算出
  [err, nn.layer{num_layer}.grad] = lossfunc(nn.layer{num_layer}.out, train_lbl);

  % 全層について誤差逆伝播を実行
  for i=num_layer : -1 : 2

    % 直前層の各ニューロンに伝播する勾配を算出
    %  δout                    
    %  ----- = w x h'(a)        
    %  δin                     
    %           |       δout|  
    %  grad = Σ|gout x -----|  
    %           |       δin |  

    % 配列要素数の同じ2パラメータを先に計算  grad[n1][1] out[n1][1]
    derr = nn.layer{i}.grad .* nn.layer{i}.dactfunc(nn.layer{i}.out);

    % Σ(w・derr)  w[n1][n0] derr[n1][1] grad[n0][1]
    nn.layer{i-1}.grad = nn.layer{i}.weight' * derr;

    % 結合荷重の修正量を算出    
    %  δE                      
    %  ---- = grad・h'(a)・out  
    %  δw                      
    % IN側ユニット-OUT側ユニットの組み合わせごとに算出 out[n0][1] derr[n1][1] dw[n1][n0]
    nn.layer{i}.dweight = derr * nn.layer{i-1}.out';

    % バイアスの修正量を算出
    nn.layer{i}.dbias = derr;
  end
end

(5) act_sigmoid.m

function y = act_sigmoid( x )
  y = 1 ./ (1 + exp(-x));   % sigmoid
end

(6) act_tanh.m

function y = act_tanh( x )
  y = tanh(x);
end

(7) act_tanh_d.m

function y = act_tanh_d( x )
  y = 1 - x.^2;         % tanh
end

00004_simpleNN_04

(6) 自分の手書き文字を認識させてみる

プログラム変更仕様

MNISTで提供されるテスト画像だけでなく、自作の手書き文字画像を自動認識させてみたい。
シンプル構成初期版に追加する機能は以下の通り。
(1)学習機能の仕様追加: 1EPOCH完了ごとに学習済みパラメーターをファイル出力する。
(2)テスト機能の仕様追加: 学習済みパラメーターファイルと単一画像を入力としたテスト機能を新規作成する。


識別結果

意地悪なテスト画像を作ったつもりはないが、結構はずれている… 考察はまた後日にしよう。

No. 自作画像 識別結果 出力層出力値
1 OK
2 OK
3 OK
4 OK
5 OK
6 OK
7 OK
8 OK
9 NOK
10 OK
11 OK
12 OK
13 NOK
14 NOK
15 NOK
16 OK
17 OK
18 NOK
19 NOK
20 NOK

プログラムのソースコード

exec_1test.m

function result = exec_1test( gprmMatFile, testImgFile )

  % パラメーターファイルをロード
  load(gprmMatFile);

  % 画像データをロード
  img = single(imread(testImgFile)) / 255;

  % 画像データが2バンド以上の場合
  if numel(size(img)) > 3
    % グレースケール画像を作成
    img = mean(img,3);
  end

  % 順伝播
  nn = NNET_propagation_forward( gprm.nn, img );

  % 判定結果を出力
  result.out         = nn.layer{numel(nn.layer)}.out;
  [~, result.maxIdx] = max(result.out);

  % 棒グラフ表示
  bar([0:9], result.out);
  xlim([-0.5 9.5]);
  ylim([0 1]);
end

00003_simpleNN_03

(5) EPOCH数を増やして正解率上昇

実験内容

シンプル構成初版では、1EPOCHで90%前後の正解率だった。
http://nnet.dogrow.net/?p=43

今回は EPOCH数を増やして正解率の向上を目論む。
対象は、シンプル構成初版の実験で 90.8%(1EPOCH)のスコアを出した [784]-[64]-[10]の層構成とする。
シンプル構成初版プログラムからの変更は、学習とテストを指定EPOCH数分繰り返すようにしただけ。

実験結果

初回の正解率が 90.8%、34回目には 96.5%まで上昇した。
http://nnet.dogrow.net/wp-content/uploads/2013/10/20131006_01.png

EPOCH No.1
[ 0]  949 /  980 ( 96.8%) 
[ 1] 1109 / 1135 ( 97.7%) 
[ 2]  907 / 1032 ( 87.9%) 
[ 3]  901 / 1010 ( 89.2%) 
[ 4]  898 /  982 ( 91.4%) 
[ 5]  745 /  892 ( 83.5%) 
[ 6]  898 /  958 ( 93.7%) 
[ 7]  929 / 1028 ( 90.4%) 
[ 8]  859 /  974 ( 88.2%) 
[ 9]  888 / 1009 ( 88.0%) 
Total  9083 / 10000 ( 90.8%)
EPOCH No.34
[ 0]  966 /  980 ( 98.6%) 
[ 1] 1120 / 1135 ( 98.7%) 
[ 2]  991 / 1032 ( 96.0%) 
[ 3]  974 / 1010 ( 96.4%) 
[ 4]  956 /  982 ( 97.4%) 
[ 5]  860 /  892 ( 96.4%) 
[ 6]  929 /  958 ( 97.0%) 
[ 7]  983 / 1028 ( 95.6%) 
[ 8]  923 /  974 ( 94.8%) 
[ 9]  953 / 1009 ( 94.4%) 
Total  9655 / 10000 ( 96.5%)

プログラムのソースコード

シンプル構成初版からの変更点は下記の2点のみ。
(1)コマンドライン引数でEPOCH数を指定可能とした。
(2)学習、テストを指定EPOCH数分繰り返すようにした。

NNET_control.m ※シンプル構成初版からの変更はこの1ファイルのみ。

function NNET_control( num_unit_of_each_layer, num_EPOCH )

  % 学習画像・ラベル、テスト画像・ラベルをファイルから読み込み
  [train_img, train_lbl] = load_MNIST( '../data/train-images-idx3-ubyte', '../data/train-labels-idx1-ubyte' );
  [test_img,  test_lbl ] = load_MNIST( '../data/t10k-images-idx3-ubyte',  '../data/t10k-labels-idx1-ubyte'  );

  % 各画像データを 0.0~1.0の範囲に正規化
  train_img = train_img / 255;
  test_img  = test_img  / 255;

  % 指定された層数、ユニット数でニューラルネットワークを作成
  nn = NNET_setup( num_unit_of_each_layer );

  % 正解率記録用領域を確保
  accuracy = zeros(num_EPOCH,1);

  % 指定EPOCH回数だけ繰り返す
  for epoch=1 : num_EPOCH

    % 学習実行
    nn = NNET_learn( nn, train_img, train_lbl );

    % テスト実行
    result = NNET_test( nn, test_img, test_lbl );

    % テスト結果を表示
    printf('\nEPOCH No.%d\n', epoch);
    for i=1: 10
      printf('[%2d] %4d / %4d (%5.1f%%) \n', i-1, result(i,2), result(i,1), result(i,2)/result(i,1)*100);
    end
    sum_result = sum(result,1);
    printf('Total %5d / %5d (%5.1f%%) \n', sum_result(2), sum_result(1), sum_result(2)/sum_result(1)*100);
    fflush(1);

    % 正解率をCSVファイルに出力
    if exist('accuracy.csv','file')~=0
      delete('accuracy.csv');
    end
    accuracy(epoch) = sum_result(2)/sum_result(1);  % 正解率
    csvwrite('accuracy.csv', accuracy);

  end
end

シンプル構成初版のプログラムソースコードはこちら。
http://nnet.dogrow.net/?p=43

00002_simpleNN_02

(4) シンプル構成の初版は正解率91%

プログラムの仕様

ニューラルネットワークの入門書に載っている普通のシンプルなネットワーク構成で実装してみた。
プログラムの仕様は以下の通り。
(1) MNIST画像データ60,000枚を学習
(2) MNISTテストデータ 10,000枚をテスト
(3) 入力層のユニット数は28×28の784個
(4) 出力層のユニット数は数字0~9に対応する10個
(5) 隠れ層の層数と各層のユニット数は引数で任意に指定可能
(6) 数字別に正解率を表示
(7) 各層の活性化関数はSigmoid
(8) 誤差関数は二乗和誤差
(9) Epoch数は1
(10) 1学習データの学習ごとにパラメーターを更新

プログラムの実行結果

適当に選んだ全9パターンの層構成を試した結果は以下のようになった。
この中では 4層 [784]-[256]-[64]-[10] の正解率 91.9% が最も高かった。

(1) 3層 [784]-[16]-[10] 89.0%

octave:10> tic;NNET_control([784 16 10]);toc;
[ 0]  938 /  980 ( 95.7%)
[ 1] 1109 / 1135 ( 97.7%)
[ 2]  888 / 1032 ( 86.0%)
[ 3]  892 / 1010 ( 88.3%)
[ 4]  878 /  982 ( 89.4%)
[ 5]  719 /  892 ( 80.6%)
[ 6]  889 /  958 ( 92.8%)
[ 7]  896 / 1028 ( 87.2%)
[ 8]  803 /  974 ( 82.4%)
[ 9]  890 / 1009 ( 88.2%)
Total  8902 / 10000 ( 89.0%)
Elapsed time is 63.3441 seconds.

(2) 3層 [784]-[24]-[10] 89.9%

octave:7> tic;NNET_control([784 24 10]);toc;
[ 0]  949 /  980 ( 96.8%)
[ 1] 1109 / 1135 ( 97.7%)
[ 2]  907 / 1032 ( 87.9%)
[ 3]  907 / 1010 ( 89.8%)
[ 4]  887 /  982 ( 90.3%)
[ 5]  708 /  892 ( 79.4%)
[ 6]  882 /  958 ( 92.1%)
[ 7]  925 / 1028 ( 90.0%)
[ 8]  858 /  974 ( 88.1%)
[ 9]  856 / 1009 ( 84.8%)
Total  8988 / 10000 ( 89.9%)
Elapsed time is 65.8959 seconds.

(3) 3層 [784]-[32]-[10] 90.0%

octave:6> tic;NNET_control([784 32 10]);toc;
[ 0]  950 /  980 ( 96.9%)
[ 1] 1108 / 1135 ( 97.6%)
[ 2]  885 / 1032 ( 85.8%)
[ 3]  905 / 1010 ( 89.6%)
[ 4]  894 /  982 ( 91.0%)
[ 5]  718 /  892 ( 80.5%)
[ 6]  895 /  958 ( 93.4%)
[ 7]  945 / 1028 ( 91.9%)
[ 8]  828 /  974 ( 85.0%)
[ 9]  869 / 1009 ( 86.1%)
Total  8997 / 10000 ( 90.0%)
Elapsed time is 71.066 seconds.

(4) 3層 [784]-[48]-[10] 90.6%

octave:9> tic;NNET_control([784 48 10]);toc;
[ 0]  955 /  980 ( 97.4%)
[ 1] 1117 / 1135 ( 98.4%)
[ 2]  909 / 1032 ( 88.1%)
[ 3]  931 / 1010 ( 92.2%)
[ 4]  920 /  982 ( 93.7%)
[ 5]  746 /  892 ( 83.6%)
[ 6]  892 /  958 ( 93.1%)
[ 7]  917 / 1028 ( 89.2%)
[ 8]  824 /  974 ( 84.6%)
[ 9]  847 / 1009 ( 83.9%)
Total  9058 / 10000 ( 90.6%)
Elapsed time is 74.398 seconds.

(5) 3層 [784]-[64]-[10] 90.8%

octave:1> tic;NNET_control([784 64 10]);toc;
[ 0]  949 /  980 ( 96.8%)
[ 1] 1109 / 1135 ( 97.7%)
[ 2]  907 / 1032 ( 87.9%)
[ 3]  901 / 1010 ( 89.2%)
[ 4]  898 /  982 ( 91.4%)
[ 5]  745 /  892 ( 83.5%)
[ 6]  898 /  958 ( 93.7%)
[ 7]  929 / 1028 ( 90.4%)
[ 8]  859 /  974 ( 88.2%)
[ 9]  888 / 1009 ( 88.0%)
Total  9083 / 10000 ( 90.8%)
Elapsed time is 81.1507 seconds.

(6) 3層 [784]-[96]-[10] 90.4%

octave:4> tic;NNET_control([784 96 10]);toc;
[ 0]  953 /  980 ( 97.2%)
[ 1] 1114 / 1135 ( 98.1%)
[ 2]  910 / 1032 ( 88.2%)
[ 3]  915 / 1010 ( 90.6%)
[ 4]  851 /  982 ( 86.7%)
[ 5]  720 /  892 ( 80.7%)
[ 6]  888 /  958 ( 92.7%)
[ 7]  937 / 1028 ( 91.1%)
[ 8]  865 /  974 ( 88.8%)
[ 9]  888 / 1009 ( 88.0%)
Total  9041 / 10000 ( 90.4%)
Elapsed time is 112.633 seconds.

(7) 3層 [784]-[128]-[10] 91.5%

octave:5> tic;NNET_control([784 128 10]);toc;
[ 0]  951 /  980 ( 97.0%)
[ 1] 1112 / 1135 ( 98.0%)
[ 2]  931 / 1032 ( 90.2%)
[ 3]  897 / 1010 ( 88.8%)
[ 4]  879 /  982 ( 89.5%)
[ 5]  784 /  892 ( 87.9%)
[ 6]  892 /  958 ( 93.1%)
[ 7]  927 / 1028 ( 90.2%)
[ 8]  868 /  974 ( 89.1%)
[ 9]  904 / 1009 ( 89.6%)
Total  9145 / 10000 ( 91.5%)
Elapsed time is 152.437 seconds.

(8) 4層 [784]-[256]-[32]-[10] 91.5%

octave:12> tic;NNET_control([784 256 32 10]);toc;
[ 0]  957 /  980 ( 97.7%)
[ 1] 1098 / 1135 ( 96.7%)
[ 2]  929 / 1032 ( 90.0%)
[ 3]  901 / 1010 ( 89.2%)
[ 4]  876 /  982 ( 89.2%)
[ 5]  781 /  892 ( 87.6%)
[ 6]  899 /  958 ( 93.8%)
[ 7]  942 / 1028 ( 91.6%)
[ 8]  862 /  974 ( 88.5%)
[ 9]  902 / 1009 ( 89.4%)
Total  9147 / 10000 ( 91.5%)
Elapsed time is 301.459 seconds.

(9) 4層 [784]-[256]-[64]-[10] 91.9%

octave:11> tic;NNET_control([784 256 64 10]);toc;
[ 0]  960 /  980 ( 98.0%)
[ 1] 1109 / 1135 ( 97.7%)
[ 2]  904 / 1032 ( 87.6%)
[ 3]  882 / 1010 ( 87.3%)
[ 4]  909 /  982 ( 92.6%)
[ 5]  786 /  892 ( 88.1%)
[ 6]  910 /  958 ( 95.0%)
[ 7]  936 / 1028 ( 91.1%)
[ 8]  874 /  974 ( 89.7%)
[ 9]  921 / 1009 ( 91.3%)
Total  9191 / 10000 ( 91.9%)
Elapsed time is 309.785 seconds.

プログラムのソースコード

(1) NNET_control.m

function NNET_control( num_unit_of_each_layer )

  % 学習画像・ラベル、テスト画像・ラベルをファイルから読み込み
  [train_img, train_lbl] = load_MNIST( 'train-images-idx3-ubyte', 'train-labels-idx1-ubyte' );
  [test_img,  test_lbl ] = load_MNIST( 't10k-images-idx3-ubyte',  't10k-labels-idx1-ubyte'  );

  % 各画像データを 0.0~1.0の範囲に正規化
  train_img = train_img / 255;
  test_img  = test_img  / 255;

  % 指定された層数、ユニット数でニューラルネットワークを作成
  nn = NNET_setup( num_unit_of_each_layer );

  % 学習実行
  nn = NNET_learn( nn, train_img, train_lbl );

  % テスト実行
  result = NNET_test( nn, test_img, test_lbl );

  % テスト結果を表示
  for i=1: 10
    printf('[%2d] %4d / %4d (%5.1f%%) \n', i-1, result(i,2), result(i,1), result(i,2)/result(i,1)*100);
  end
  sum_result = sum(result,1);
  printf('Total %5d / %5d (%5.1f%%) \n', sum_result(2), sum_result(1), sum_result(2)/sum_result(1)*100);
end

(2) load_MNIST.m

function [image label] = load_MNIST( image_file, label_file )
  %//////////////////////////////////////////////////////////////
  % 画像をロード
  % ファイルからロードした画像を image[28][28][60000]の配列で出力
  fid = fopen(image_file,'r','b');
  magic_number      = fread(fid, 1, 'int32');
  number_of_items   = fread(fid, 1, 'int32');
  number_of_rows    = fread(fid, 1, 'int32');
  number_of_columns = fread(fid, 1, 'int32');
  img               = fread(fid, [number_of_rows*number_of_columns number_of_items],'uint8');
  image = permute(reshape(img, number_of_rows, number_of_columns,number_of_items),[2 1 3]);
  fclose(fid);

  %//////////////////////////////////////////////////////////////
  % ラベルをロード
  % ファイルからロードしたラベルを label[10][60000]の配列で出力
  fid = fopen(label_file,'r','b');
  magic_number    = fread(fid, 1,               'int32');
  number_of_items = fread(fid, 1,               'int32');
  lbl             = fread(fid, number_of_items, 'uint8');
  idx             = [1:number_of_items]';
  lblidx          = lbl * number_of_items + idx;
  label           = zeros(number_of_items,10);
  label(lblidx)   = 1;
  label           = label';
  fclose(fid);
end

(3) NNET_setup.m

function nn = NNET_setup( num_unit_of_each_layer )

  % 乱数生成器を初期化
  rand('seed', 0);

  % 指定された層数を取得
  num_layer = numel( num_unit_of_each_layer );

  % 全層を初期化
  for i=2 : num_layer

    % 現層と前層のユニット数を取得
    num_unit_pre = num_unit_of_each_layer( i - 1 );
    num_unit     = num_unit_of_each_layer( i );

    % 各結合線の荷重を -1~1の一様分布乱数で初期化
    nn.layer{i}.weight = -1 + rand( num_unit, num_unit_pre ) * 2;

    % バイアスを初期化
    nn.layer{i}.bias = zeros( num_unit, 1 );
  end
end

(4) NNET_learn.m

function nn = NNET_learn( nn, train_img, train_lbl )

  % 学習データ数を取得
  num_data = size( train_img, 3 );

  % 学習データをシャッフル
  randvector = randperm( num_data );
  train_img  = train_img(:,:,randvector);
  train_lbl  = train_lbl(:,randvector);

  % 全学習画像を1個ずつ学習させる
  for i=1 : num_data
    % 順伝播
    nn = NNET_propagation_forward( nn, train_img(:,:,i) );

    % 誤差逆伝播
    nn = NNET_propagation_back( nn, train_lbl(:,i) );

    % パラメーター更新
    nn = NNET_update( nn, 0.05 );
  end
end

(5) NNET_propagation_forward.m

function nn = NNET_propagation_forward( nn, train_img )

  % 入力層の出力値を記憶
  nn.layer{1}.out = train_img(:);   % [n0][1]

  % 全層について順伝播を実行
  for i=2 : numel(nn.layer)

    % a = Σwz + bias    w[n1][n0] z[n0][1] bias[n1][1]
    nn.layer{i}.actprm = nn.layer{i}.weight * nn.layer{i-1}.out + nn.layer{i}.bias;

    % out = sigmoid(a)   out[n1][1]
    nn.layer{i}.out = sigmoid( nn.layer{i}.actprm );
  end
end

(6) NNET_propagation_back.m

function nn = NNET_propagation_back( nn, train_lbl )

  % 層数を取得
  num_layer = numel(nn.layer);

  % 出力層で検出された誤差量と逆伝播する勾配の初期値を算出
  [err, nn.layer{num_layer}.grad] = lossfunc(nn.layer{num_layer}.out, train_lbl);

  % 全層について誤差逆伝播を実行
  for i=num_layer : -1 : 2

    % 直前層の各ニューロンに伝播する勾配を算出
    %  δout                    
    %  ----- = w x h'(a)        
    %  δin                     
    %           |       δout|  
    %  grad = Σ|gout x -----|  
    %           |       δin |  

    % 配列要素数の同じ2パラメータを先に計算  grad[n1][1] out[n1][1]
    derr = nn.layer{i}.grad .* dsigmoid(nn.layer{i}.out);

    % Σ(w・derr)  w[n1][n0] derr[n1][1] grad[n0][1]
    nn.layer{i-1}.grad = nn.layer{i}.weight' * derr;

    % 結合荷重の修正量を算出    
    %  δE                      
    %  ---- = grad・h'(a)・out  
    %  δw                      
    % IN側ユニット-OUT側ユニットの組み合わせごとに算出 out[n0][1] derr[n1][1] dw[n1][n0]
    nn.layer{i}.dweight = derr * nn.layer{i-1}.out';

    % バイアスの修正量を算出
    nn.layer{i}.dbias = derr;
  end
end

(7) NNET_update.m

function nn = NNET_update( nn, ratio )

  % 全層について結合荷重とバイアスを更新
  for i=2: numel(nn.layer)
    nn.layer{i}.weight = nn.layer{i}.weight - ratio * nn.layer{i}.dweight;
    nn.layer{i}.bias   = nn.layer{i}.bias   - ratio * nn.layer{i}.dbias;
  end
end

(8) NNET_test.m

function result = NNET_test( nn, test_img, test_lbl )

  % テストデータ数を取得
  num_data = size(test_img, 3);

  % テスト結果の記録領域を初期化
  result = zeros(size(test_lbl,1),2);

  % 全学習画像をテスト実行
  for i=1 : num_data
    % 順伝播
    nn = NNET_propagation_forward( nn, test_img(:,:,i) );

    % 自動識別結果(出力層の出力値)を取得
    result_lbl = nn.layer{numel(nn.layer)}.out;
    [~, idx_cor] = max(test_lbl(:,i));    % 期待値を取得
    [~, idx_res] = max(result_lbl);       % 判定結果を取得

    % テスト結果を記憶
    result(idx_cor ,1) = result(idx_cor ,1) + 1;    % 数字別のテスト数+1
    if idx_cor == idx_res
      result(idx_cor ,2) = result(idx_cor ,2) + 1;  % 数字別の正解数+1
    end
  end
end

(9) sigmoid.m

function y = sigmoid( x )
  y = 1 ./ (1 + exp(-x));
end

(10) dsigmoid.m

function y = dsigmoid( x )
  y = x .* (1 - x);
end

(11) lossfunc.m

function [err grad] = lossfunc( out, lbl )

  % 伝播する誤差の初期値を算出
  grad  = out - lbl;   % out[n1][1] lbl[n1][1]

  % 誤差関数種別は二乗和誤差とする。
  %        1              2         
  % err = ---Σ(out - lbl)          
  %        2                        
  err = grad' * grad / 2;
end

00001_simpleNN

(3) MNIST画像をPNG画像で出力する

MNIST画像データファイルをダウンロードした。
https://nnet.dogrow.net/?p=23

MNIST画像データのいくつかをOctaveで画像化して眺めてみた。
https://nnet.dogrow.net/?p=31

今回は、MNIST画像データファイルに含まれる全画像をPNG画像にして眺めてみたい。
今回も GNU-Octaveを使用する。Octaveは少ないコード量で高機能なプログラムを書けるので重宝している。

作成する関数は cre_MNIST_PNG とする。
第1引数は、MNIST画像データファイルのパス。学習用 or テスト用のどちらか一方を指定する。
第2引数は、PNG化したMNIST画像の出力先ディレクトリのパス。

function cre_MNIST_PNG( image_file, output_dir )
  % 画像をロード
  fid = fopen(image_file,'r','b');
  magic_number      = fread(fid, 1, 'int32');
  number_of_items   = fread(fid, 1, 'int32');
  number_of_rows    = fread(fid, 1, 'int32');
  number_of_columns = fread(fid, 1, 'int32');
  img               = fread(fid, [number_of_rows*number_of_columns number_of_items],'uint8');
  image = permute(reshape(img, number_of_rows, number_of_columns,number_of_items),[2 1 3]);
  fclose(fid);

  % 画像をPNGファイル出力
  if exist(output_dir)==0
    mkdir(output_dir);
  end
  for i=1 : number_of_items
    pngfile = sprintf('%s/img%05d.png', output_dir, i);
    imwrite(uint8(image(:,:,i)), pngfile);
  end
end

まずはテスト用画像 t10k-images-idx3-ubyte をPNG画像化してみる。

octave:1> cre_MNIST_PNG('t10k-images-idx3-ubyte','./img');
octave:2>

以下のようなPNG画像ファイルが出力された。(全10,000個の内の先頭から105個を表示)
https://nnet.dogrow.net/wp-content/uploads/2013/10/20131005_04.png

(2) MNIST学習画像を見てみる

ダウンロードしたMNIST画像は、一般的な画像ビューアで見ることができない。
ここではフリーの数値計算ソフト GNU-Octave を使ってMNIST画像データを可視化してみる。

学習用画像データファイルをオープンする。

octave:2> fid=fopen('train-images-idx3-ubyte','r','b')
fid =  3

学習用画像データファイルのフォーマットは以下の通り。
https://nnet.dogrow.net/wp-content/uploads/2013/10/20131005_03.png
ファイル先頭のヘッダ部分には、画像データに関する情報が4項目格納されている。

octave:3> magic_number = fread(fid,1,'int32')
magic_number =  2051
octave:4> number_of_items = fread(fid, 1, 'int32')
number_of_items =  60000
octave:5> number_of_rows = fread(fid,1,'int32')
number_of_rows =  28
octave:6> number_of_columns = fread(fid,1,'int32')
number_of_columns =  28

縦横28×28ピクセルの画像が60,000枚入っていることが確認できた。

次に画像を読み出してみる。
GNU-Octaveでは、fread関数で取得したデータの出力を2次元配列にすることができる。

octave:7> img = fread(fid, [28*28 60000],'uint8');
octave:8> size(img)
ans =
     784   60000
octave:9> img = reshape(img,28,28,60000);
octave:10> size(img)
ans =
      28      28   60000

先頭から一つずつ画像を表示してみる。
GNU-Octaveでは、imshowコマンドで2次元配列データを簡単に画像表示できる。

octave:11> v = uint8(img(:,:,1)');
octave:12> imshow(v)

これは「5」だろうか?
https://nnet.dogrow.net/wp-content/uploads/2013/10/20131005_M01.png

2番目に格納されている学習画像も見てみる。

octave:13> v = uint8(img(:,:,2)');
octave:14> imshow(v)

これは「0」に間違いない。
https://nnet.dogrow.net/wp-content/uploads/2013/10/20131005_M02.png

60000個目に格納されている学習画像も見てみる。

octave:15> v = uint8(img(:,:,60000)');
octave:16> imshow(v)

これは「8」に間違いない。
https://nnet.dogrow.net/wp-content/uploads/2013/10/20131005_M03.png

(1) MNIST画像データをダウンロード

MNISTファイルの入手

MNIST手書き文字画像データはこちらのサイトから入手できます。
http://yann.lecun.com/exdb/mnist/

http://nnet.dogrow.net/wp-content/uploads/2013/10/20131005_01.png

提供されるデータは以下の4種類
(1)学習用画像
(2)学習用画像のラベル(=識別番号)
(3)テスト用画像
(4)テスト用画像のラベル(=識別番号)
http://nnet.dogrow.net/wp-content/uploads/2013/10/20131005_02.png

ファイルの形式

ファイルの形式は上記ページの下の方の FILE FORMATS FOR THE MNIST DATABASE に書かれている。
仕様はとってもシンプル。例えば、学習用画像データは以下のような決まりでファイルに格納されている。
http://nnet.dogrow.net/wp-content/uploads/2013/10/20131005_03.png

なお、ダウンロードしたファイルはgzip型式で圧縮されており、gunzipコマンドで解凍できる。

[user@dog-server]$ ls -l
合計 11336
-rw-rw-r--. 1 user user 1648877 10月  5 01:51 2013 t10k-images-idx3-ubyte.gz
-rw-rw-r--. 1 user user    4542 10月  5 01:51 2013 t10k-labels-idx1-ubyte.gz
-rw-rw-r--. 1 user user 9912422 10月  5 01:51 2013 train-images-idx3-ubyte.gz
-rw-rw-r--. 1 user user   28881 10月  5 01:51 2013 train-labels-idx1-ubyte.gz
[user@dog-server]$ gunzip *.gz
[user@dog-server]$ ls -l
合計 53672
-rw-rw-r--. 1 user user  7840016 10月  5 01:51 2013 t10k-images-idx3-ubyte
-rw-rw-r--. 1 user user    10008 10月  5 01:51 2013 t10k-labels-idx1-ubyte
-rw-rw-r--. 1 user user 47040016 10月  5 01:51 2013 train-images-idx3-ubyte
-rw-rw-r--. 1 user user    60008 10月  5 01:51 2013 train-labels-idx1-ubyte
[user@dog-server]$

次回はダウンロードしたデータの中身を実際に見てみる。