MNIST用の畳み込み層+全結合層ニューラルネットワーク(CNN)をフルスクラッチで作成 ~Delphiソースコード集
畳み込み層+全結合層ニューラルネットワーク(Convolutional Neural Network)を使用する為のファイルの準備
プロジェクト用フォルダを作成し、
本ページ最下部にある「UMamCnnNn.pas」ソースコードを、プロジェクトフォルダ内に「UMamCnnNn.pas」ファイルとして保存します。
CNN+NNの構造は以下となっています。
MNISTファイルのダウンロード
http://yann.lecun.com/exdb/mnist/
から
「train-images-idx3-ubyte.gz」(トレーニングデータ)、
「train-labels-idx1-ubyte.gz」(トレーニングラベル)、
「t10k-images-idx3-ubyte.gz」(テストデータ)、
「t10k-labels-idx1-ubyte.gz」(テストラベル)
ファイルをダウンロードして解凍し、プロジェクトフォルダ内に保存します。
Delphiを起動して新規作成を行い、必要なコンポーネントをドラッグ&ドロップする
Delphi起動⇒ファイル⇒新規作成⇒WindowsVCLアプリケーション を選択します。
TButton 3個、TEdit 1個、TMemo 1個、をフォームへドラッグ&ドロップします。
「ファイル」⇒「全て保存」でフォルダを作成して、プロジェクトとユニットを保存します。
プロジェクトフォルダ内にUMamCnnNn.pasファイルを配置します。
ソースコードを記述する
Button1のOnClickイベント、Button2のOnClickイベント、Button3のOnClickイベントに以下ソースコードを記述します。
unit Unit1; interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls ,UMamCnnNn; type TFannData=packed record id:Cardinal; num:Cardinal; row:Cardinal; col:Cardinal; data:array of array of array of Byte; end; TFannLabel=record id:Cardinal; num:Cardinal; data:array of Byte; end; TForm1 = class(TForm) Button1: TButton; Button2: TButton; Edit1: TEdit; Button3: TButton; Memo1: TMemo; procedure Button1Click(Sender: TObject); procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure Button2Click(Sender: TObject); procedure Button3Click(Sender: TObject); private { Private 宣言 } cnn:TMamCnnNn; FannTrainData:TFannData; FannTrainLabel:TFannLabel; FannTestData:TFannData; FannTestLabel:TFannLabel; public { Public 宣言 } end; var Form1: TForm1; implementation {$R *.dfm} procedure TForm1.Button1Click(Sender: TObject); var i,j{,k,l,m,n}:integer; strm:TFileStream; rbyte:array[0..3] of Byte; begin Button1.Enabled:=False; Button2.Enabled:=False; Button3.Enabled:=False; Sleep(100); Application.ProcessMessages; { 訓練用画像ファイル「train-images.idx3-ubyte」の構造 id:4byte 0x00000803(2051) num:4byte 60000 row:4byte 28 col:4byte 28 data:num×row×col byte ビッグエンディアンなので、リトルエンディアンが基本のWindowsから読む場合は、 1バイトずつ読んで反転して取得する必要がある } strm:=TFileStream.Create('..\..\train-images.idx3-ubyte',fmOpenRead); strm.Position:=0; strm.Read(rbyte[0],4); FannTrainData.id :=(rbyte[0] shl 24)+(rbyte[1] shl 16)+(rbyte[2] shl 8)+rbyte[3]; strm.Read(rbyte[0],4); FannTrainData.num:=(rbyte[0] shl 24)+(rbyte[1] shl 16)+(rbyte[2] shl 8)+rbyte[3]; strm.Read(rbyte[0],4); FannTrainData.row:=(rbyte[0] shl 24)+(rbyte[1] shl 16)+(rbyte[2] shl 8)+rbyte[3]; strm.Read(rbyte[0],4); FannTrainData.col:=(rbyte[0] shl 24)+(rbyte[1] shl 16)+(rbyte[2] shl 8)+rbyte[3]; setlength(FannTrainData.data,FannTrainData.num); for i := 0 to FannTrainData.num-1 do begin SetLength(FannTrainData.data[i],FannTrainData.row); for j:=0 to FannTrainData.row-1 do begin SetLength(FannTrainData.data[i][j],FannTrainData.col); strm.Read(FannTrainData.data[i][j][0],FannTrainData.col); end; end; strm.Free; { 訓練用ラベルファイル「train-labels.idx1-ubyte」の構造 id:4byte num:4byte data[id]=num; ビッグエンディアンなので、リトルエンディアンが基本のWindowsから読む場合は、 1バイトずつ読んで反転して取得する必要がある } strm:=TFileStream.Create('..\..\train-labels.idx1-ubyte',fmOpenRead); strm.Position:=0; strm.Read(rbyte[0],4); FannTrainLabel.id :=(rbyte[0] shl 24)+(rbyte[1] shl 16)+(rbyte[2] shl 8)+rbyte[3]; strm.Read(rbyte[0],4); FannTrainLabel.num:=(rbyte[0] shl 24)+(rbyte[1] shl 16)+(rbyte[2] shl 8)+rbyte[3]; setlength(FannTrainLabel.data,FannTrainLabel.num); strm.Read(FannTrainLabel.data[0],FannTrainLabel.num); strm.Free; //テストデータの読み込み strm:=TFileStream.Create('..\..\t10k-images.idx3-ubyte',fmOpenRead); strm.Position:=0; strm.Read(rbyte[0],4); FannTestData.id :=(rbyte[0] shl 24)+(rbyte[1] shl 16)+(rbyte[2] shl 8)+rbyte[3]; strm.Read(rbyte[0],4); FannTestData.num:=(rbyte[0] shl 24)+(rbyte[1] shl 16)+(rbyte[2] shl 8)+rbyte[3]; strm.Read(rbyte[0],4); FannTestData.row:=(rbyte[0] shl 24)+(rbyte[1] shl 16)+(rbyte[2] shl 8)+rbyte[3]; strm.Read(rbyte[0],4); FannTestData.col:=(rbyte[0] shl 24)+(rbyte[1] shl 16)+(rbyte[2] shl 8)+rbyte[3]; SetLength(FannTestData.data,FannTestData.num); for i := 0 to FannTestData.num-1 do begin setlength(FannTestData.data[i],FannTestData.row); for j:=0 to FannTestData.row-1 do begin SetLength(FannTestdata.data[i][j],FannTestData.col); strm.Read(FannTestData.data[i][j][0],FannTestData.col); end; end; strm.Free; //テストラベルの読み込み strm:=TFileStream.Create('..\..\\t10k-labels.idx1-ubyte',fmOpenRead); strm.Position:=0; strm.Read(rbyte[0],4); FannTestLabel.id :=(rbyte[0] shl 24)+(rbyte[1] shl 16)+(rbyte[2] shl 8)+rbyte[3]; strm.Read(rbyte[0],4); FannTestLabel.num:=(rbyte[0] shl 24)+(rbyte[1] shl 16)+(rbyte[2] shl 8)+rbyte[3]; setlength(FannTestLabel.data,FannTestLabel.num); strm.Read(FannTestLabel.data[0],FannTestLabel.num); strm.Free; Memo1.Lines.Add('MNISTデータの読み込みが完了しました'); Button1.Enabled:=True; Button2.Enabled:=True; Button3.Enabled:=True; end; procedure TForm1.Button2Click(Sender: TObject); var i,j,k:integer; e:Integer;//エポック(繰り返し学習) AInput: TArray<TArray<Single>>; AOutput:TArray<Single>; begin Button1.Enabled:=False; Button2.Enabled:=False; Button3.Enabled:=False; Sleep(100); Application.ProcessMessages; for e := 1 to 1 do //繰り返し学習回数1 begin SetLength(AInput,FannTrainData.row); for i:=0 to High(FannTrainData.data) do begin for j := Low(FannTrainData.data[i]) to High(FannTrainData.data[i]) do begin SetLength(AInput[j],FannTrainData.col); for k := Low(FannTrainData.data[i][j]) to High(FannTrainData.data[i][j]) do begin AInput[j][k]:=FannTrainData.data[i][j][k]/255; end; end; SetLength(AOutput,10); for j := Low(AOutput) to High(AOutput) do begin if j=FannTrainLabel.data[i] then AOutput[j]:=1 else AOutput[j]:=0; end; cnn.Train(AInput,Aoutput); if(i mod 1000)=0 then begin Edit1.Text:=floattostr(cnn.GetMSE()); Application.ProcessMessages; end; end; end; Edit1.Text:=floattostr(cnn.GetMSE()); Application.ProcessMessages; Memo1.Lines.Add('MNISTデータの学習が完了しました'); Button1.Enabled:=True; Button2.Enabled:=True; Button3.Enabled:=True; end; procedure TForm1.Button3Click(Sender: TObject); var i,j,k:Integer; AInput: TArray<TArray<Single>>; AOutput:TArray<Single>; ans:Integer; mx:Single; CorrectRate:Single; begin Button1.Enabled:=False; Button2.Enabled:=False; Button3.Enabled:=False; Sleep(100); Application.ProcessMessages; SetLength(AInput,FannTestData.row); for j := Low(AInput) to High(AInput) do SetLength(AInput[j],FannTestData.col); CorrectRate:=0; for i := Low(FannTestData.data) to High(FannTestData.data) do begin for j := Low(FannTestData.data[i]) to High(FannTestData.data[i]) do begin for k := Low(FannTestData.data[i][j]) to High(FannTestData.data[i][j]) do begin AInput[j][k]:=FannTestData.data[i][j][k]/255; end; end; AOutput:=cnn.Run(AInput); mx:=-1e37; ans:=0; for j := Low(AOutput) to High(AOutput) do begin if mx<AOutput[j] then begin ans:=j; mx:=AOutput[j]; end; end; if ans=FannTestLabel.data[i] then begin //正解の場合 CorrectRate:=CorrectRate+1; end; end; Memo1.Lines.Add( Format('正答率 %f',[CorrectRate/FannTestData.num]) ); Button1.Enabled:=True; Button2.Enabled:=True; Button3.Enabled:=True; end; procedure TForm1.FormCreate(Sender: TObject); begin //メモリリークの検知 ReportMemoryLeaksOnShutdown := True; cnn:=TMamCnnNn.Create; end; procedure TForm1.FormDestroy(Sender: TObject); begin cnn.Free; end; end.
実行する
実行ボタンを押して実行します。(デバッグ実行でもOK)
Button1をクリックすると、MNISTの訓練データを読み込みます。
Button2をクリックするとMNISTの訓練データで学習を行います。
Button3をクリックするとMNISTのテストデータでテストを行い正答率を表示します。
以下画像では、正答率が91%です。
UMamCnnNn.pasソースコード
unit UMamCnnNn; interface { ■Convolutional Neural Network 2層とする MNIST 28x28(60000画像学習) モノクロ(1チャンネル) パディングは無しとする 1層目 10カーネル25x25 stride=1 ↓ 4x4 1層目 プーリング 2x2 max stride=2 ↓ 10x 2x2 出力層 ↓ ■全結合層へ連携する(活性化関数はSigmoid) ↓ ■全結合層 活性化関数はReLUで出力層はSigmoid 入力層 40ニューロン ↓ 中間層 30ニューロン ↓ 出力層 10ニューロン } uses System.Math,Winapi.windows{,dialogs}; type TMamCnnNn=class(TObject) private fCH,fCW:Integer;//入力層の縦横 fCLNum:integer;//層数 fCKNum:integer;//各層の重み(カーネル)の数 =10 fCStride:Integer;//プーリング時のストライド(2) fCKHW:TArray<Integer>;//各層毎の各重み(カーネル)の縦と横のピクセル数 //重み(カーネル) // 層×Kernel数×Height×Width の4次配列 fCK:TArray<TArray<TArray<TArray<Single>>>>; //重み(カーネル)で畳み込み後の値 // 層×Kernel数×Height×Width の4次配列 fCKOut:TArray<TArray<TArray<TArray<Single>>>>; //順伝播のプーリング時に最大値の場所を保持(最大値=1,それ以外は0を入れる) // 層×Kernel数×Height×Width の4次配列 fCKMax:TArray<TArray<TArray<TArray<Single>>>>; //順伝播のプーリング後の値を入れる // 層×Kernel数×Height×Width の4次配列 fCSum:TArray<TArray<TArray<TArray<Single>>>>; fCB:TArray<TArray<Single>>;//バイアス fCLearningRate:Single;//学習率 fTrainCount:Integer; fLoss:Single; //全結合層 fSum:TArray<TArray<single>>;//各層の値(重み付線形和) fOut:TArray<TArray<single>>;//各層の値(重み付線形和の活性化関数適用後) fLayerCount:Integer;//層の数 fNeuronCountInLayers:TArray<Integer>;//各層のニューロン数 fBias:TArray<TArray<single>>;//バイアス fWeight:TArray<TArray<TArray<single>>>;//重みづけ(-1~1) procedure ForwardProp(CInput:TArray<TArray<Single>>); procedure BackProp(Outputs: TArray<Single>); public constructor Create(); destructor Destroy();override; function Run(CInput:TArray<TArray<Single>>):TArray<Single>; procedure Train(CInput:TArray<TArray<Single>>; COutput:TArray<Single>); function GetMSE:Single; end; implementation //1次配列の場合のsoftmax関数 procedure Softmax1(var source, dest: TArray<Single>); var SumExp:Single; m:Single; i:Integer; begin SetLength(dest,Length(Source)); SumExp:=0; m:=-3.40e+38; for i := Low(source) to High(source) do m:=max(m,source[i]); for i := Low(source) to High(source) do begin dest[i]:=System.Exp(source[i]-m); SumExp:=SumExp+dest[i]; end; for i := Low(dest) to High(dest) do dest[i]:=dest[i]/SumExp; end; //1次配列の場合のsoftmax導関数 procedure SoftmaxDerivative1(var source, dest: TArray<Single>); var i:Integer; begin SetLength(dest,Length(Source)); Softmax1(source,dest); for i := Low(dest) to High(dest) do begin dest[i]:=dest[i]*(1-dest[i]); end; end; //3次配列の場合のsoftmax関数 procedure Softmax3(var source, dest: TArray<TArray<TArray<Single>>>); var SumExp:Single; m:Single; i,j,k:Integer; begin SetLength(dest,Length(Source)); SumExp:=0; m:=-3.40e+38; for i := Low(source) to High(source) do begin SetLength(dest[i],Length(Source[i])); for j := Low(source[i]) to High(source[i]) do begin SetLength(dest[i][j],Length(Source[i][j])); for k := Low(source[i][j]) to High(source[i][j]) do m:=max(m,source[i][j][k]); end; end; //m:=0; for i := Low(source) to High(source) do begin for j := Low(source[i]) to High(source[i]) do for k := Low(source[i][j]) to High(source[i][j]) do begin dest[i][j][k]:=System.Exp(source[i][j][k]-m); SumExp:=SumExp+dest[i][j][k]; end; end; for i := Low(dest) to High(dest) do for j := Low(dest[i]) to High(dest[i]) do for k := Low(dest[i][j]) to High(dest[i][j]) do dest[i][j][k]:=dest[i][j][k]/SumExp; end; //3次配列の場合のsoftmax導関数 procedure SoftmaxDerivative3(var source, dest: TArray<TArray<TArray<Single>>>); var i,j,k:Integer; begin SetLength(dest,Length(Source)); for i := Low(Source) to High(Source) do begin SetLength(dest[i],Length(Source[i])); for j := Low(Source[i]) to High(Source[i]) do SetLength(dest[i][j],Length(Source[i][j])); end; Softmax3(source,dest); for i := Low(dest) to High(dest) do begin for j := Low(dest[i]) to High(dest[i]) do begin for k := Low(dest[i][j]) to High(dest[i][j]) do begin dest[i][j][k]:=dest[i][j][k]*(1-dest[i][j][k]); end; end; end; end; //ReLU関数 function ReLU(x: single): single; begin //if x>0 then result:=x else result:=0; result:=System.Math.Max(x,0); end; //ReLUの導関数 function ReLUDerivative(x: single): single; begin if x>0 then result:=1 else result:=0; end; //シグモイド関数 function Sigmoid(x: Single): Single; begin result:=1.0/(1.0+system.exp(-x)) end; //シグモイド関数の導関数 function SigmoidDerivative(x:single): single; begin result:=Sigmoid(x); result:=result*(1.0-result); end; { TMamCnn } procedure TMamCnnNn.BackProp(Outputs: TArray<Single>); var i,j,k,l:Integer; m,n:Integer; row,col:Integer; //プーリング層で逆伝播する誤差情報(∂E/∂y) PoolingDer:TArray<TArray<TArray<Single>>>; //重み(カーネル)の勾配 AKGrad:TArray<TArray<TArray<Single>>>; //「重み(カーネル)」(fCK)にゼロパディングした値を入れる ACKPad:TArray<TArray<TArray<Single>>>; der:Single; //ASumDer:TArray<TArray<TArray<Single>>>; padh,padw:Integer; xDer:TArray<TArray<TArray<Single>>>;//入力層の逆伝播 COutputDer: TArray<TArray<TArray<Single>>>;//全結合層NNからの誤差逆伝播 layer_grads_x:TArray<Single>; back_error:TArray<Single>; loss_der:single; active_der:single; delta:single; sum_der_w,sum_der_x:TArray<Single>; sum_der_b:Single; begin //NN層 //逆伝播とパラメータ更新 SetLength(layer_grads_x,0); for i := (fLayerCount-1) downto 1 do begin if i=(fLayerCount-1) then begin //出力層の場合 SetLength(back_error,Length(Outputs)); for j := Low(Outputs) to High(Outputs) do begin loss_der:=fOut[i][j]-Outputs[j]; back_error[j]:=loss_der; end; end else begin //隠れ層(次の層への入力の偏微分係数) //最後に追加された入力の勾配(layer_grads_x)をback_errorに入れる SetLength(back_error,Length(layer_grads_x)); Move( layer_grads_x[0], back_error[0], SizeOf(layer_grads_x[0])*Length(layer_grads_x) ); end; for j := Low(fSum[i]) to High(fSum[i]) do begin if i=(fLayerCount-1) then begin //出力層の場合 //シグモイド関数の導関数 active_der:=SigmoidDerivative(fSum[i][j]); end else begin //出力層以外の場合 //ReLU関数の導関数 active_der:=ReLUDerivative(fSum[i][j]); end; //fOut[i-1]配列をsum_der_wにコピー SetLength(sum_der_w,Length(fOut[i-1])); Move( fOut[i-1][0], sum_der_w[0], Sizeof(fOut[i-1][0])*Length(fOut[i-1]) ); sum_der_b:=1.0; //fWeight[i-1][j]配列をsum_der_xにコピー SetLength(sum_der_x,Length(fWeight[i-1][j])); Move( fWeight[i-1][j][0], sum_der_x[0], Sizeof(fWeight[i-1][j][0])*Length(fWeight[i-1][j]) ); //勾配を計算 delta:=back_error[j]*active_der; //勾配からバイアス計算してバイアスを更新する fBias[i-1][j]:=fBias[i-1][j]-delta*sum_der_b*fCLearningRate; if j=0 then SetLength(layer_grads_x,Length(sum_der_w)); //重みと入力 for k := Low(sum_der_w) to High(sum_der_w) do begin //勾配から重みの計算を行い重みを更新する fWeight[i-1][j][k]:= fWeight[i-1][j][k]-delta*sum_der_w[k]*fCLearningRate; //入力は各ノードから前のノードに接続する全ての入力を合計する if j=0 then begin layer_grads_x[k]:=delta *sum_der_x[k]; end else begin layer_grads_x[k]:=layer_grads_x[k]+delta *sum_der_x[k]; end; end; end; end; //NN(1次配列)→CNN(3次配列)へ //誤差情報(出力層の値-正解値)逆伝播を次元変換 m:=0; SetLength(xDer,Length(fCSum[High(fCSum)])); for j := Low(xDer) to High(xDer) do begin SetLength(xDer[j],Length(fCSum[High(fCSum)][j])); for k := Low(xDer[j]) to High(xDer[j]) do begin SetLength(xDer[j][k],Length(fCSum[High(fCSum)][j][k])); for l := Low(xDer[j][k]) to High(xDer[j][k]) do begin xDer[j][k][l]:=layer_grads_x[m]* SigmoidDerivative(fCSum[High(fCSum)][j][k][l]); inc(m); end; end; end; //CNN層 //出力層から入力層まで逆伝播 for i := High(fCKMax) downto Low(fCKMax) do begin SetLength(AKGrad,Length(fCK[i])); SetLength(ACKPad,Length(fCK[i])); SetLength(PoolingDer,Length(fCKMax[i])); //各層の重み(カーネル)の数だけループ for j := Low(fCKMax[i]) to High(fCKMax[i]) do begin { ■プーリング層の逆伝播 ∂E/∂y 順伝播での入力層fCKOutで最大値だった位置(fCKMAX)に 入力層の逆伝播の値を入れる 順伝播 fCKout[i]→2x2の最大値stride=2→fCSum[i+1] l0 l1 l2 l3 最大値の位置 l0 l1 k0 0 1 2 3 k1l1 k1l3 → k0 3 5 k1 2 3 4 5 k1 2 2 k2 1 2 1 1 k2l1 k3l2 →───┘ k3 0 0 2 1 │ │ 逆伝播 ↓ AGrad ←最大の位置に誤差を配置←x or COutput l0 l1 l2 l3 l0 l1 k0 0 0 0 0 k0 1 3 k1 0 1 0 3 k1 4 5 k2 0 4 0 0 k3 0 0 5 0 ↑実際には誤差情報(出力値-正解値)を入れる } SetLength(PoolingDer[j],Length(fCKMax[i][j])); for k := Low(fCKMax[i][j]) to High(fCKMax[i][j]) do begin SetLength(PoolingDer[j][k],Length(fCKMax[i][j][k])); end; for k := Low(fCKMax[i][j]) to High(fCKMax[i][j]) do begin for l := Low(fCKMax[i][j][k]) to High(fCKMax[i][j][k]) do begin //逆伝播してきたxDerを //順伝播で採用した重み(カーネル)の最大値の位置に配置 PoolingDer[j][k][l]:= fCKMax[i][j][k][l]* xDer[j][(k div fCStride)][(l div fCStride)]; end; end; //■重み(カーネル)の勾配 ∂E/∂w //⇒プーリング層から伝播した誤差(PoolingDer)に入力(fCSum)を掛ける SetLength(AKGrad[j],Length(fCK[i][j])); for k := Low(fCK[i][j]) to High(fCK[i][j]) do SetLength(AKGrad[j][k],Length(fCK[i][j][k])); for k := Low(AKGrad[j]) to High(AKGrad[j]) do begin for l := Low(AKGrad[j][k]) to High(AKGrad[j][k]) do begin der:=0; for m := Low(PoolingDer[j])to High(PoolingDer[j]) do begin for n := Low(PoolingDer[j][m]) to High(PoolingDer[j][m]) do begin der:=der+fCSum[i][j][k+m][l+n]*PoolingDer[j][m][n]; end; end; AKGrad[j][k][l]:=der; end; end; //■「入力の逆伝播」(Xder) //「重み(カーネル)」(fCK)から //「上下左右にゼロパディングを追加した重み(カーネル)」(ACKPad)を作成し、 //PoolingDerの範囲内にあるACKPadの上下左右反転にPoolingDerを掛けたもの padh:=High(PoolingDer[j]); padw:=High(PoolingDer[j][0]); //縦方向上下にパディング追加 SetLength(ACKPad[j],Length(AKGrad[j])+padh*2); for k := Low(ACKPad[j]) to High(ACKPad[j]) do begin //横方向左右にパディング追加 SetLength(ACKPad[j][k],Length(AKGrad[j][0])+padw*2); for l := Low(ACKPad[j][k]) to High(ACKPad[j][k]) do begin //ゼロに初期化 ACKPad[j][k][l]:=0; end; end; for k := Low(fCK[i][j]) to High(fCK[i][j]) do begin for l := Low(fCK[i][j][k]) to High(fCK[i][j][k]) do begin ACKPad[j][k+padh][l+padw]:=fCK[i][j][k][l]; end; end; SetLength(xDer[j],Length(fCSum[i][j])); for k := Low(xDer[j]) to High(xDer[j]) do begin SetLength(xDer[j][k],Length(fCSum[i][j][k])); for l := Low(xDer[j][k]) to High(xDer[j][k]) do begin der:=0; row:=High(PoolingDer[j]); col:=High(PoolingDer[j][0]); for m := Low(PoolingDer[j]) to High(PoolingDer[j]) do for n := Low(PoolingDer[j][m]) to High(PoolingDer[j][m]) do der:=der+ACKPad[j][k+(row-m)][l+(col-n)]*PoolingDer[j][m][n]; xDer[j][k][l]:=der; end; end; //■重み(カーネル)の更新 //W=Wー学習率×∂E/∂w // ↑勾配 for k := Low(fCK[i][j]) to High(fCK[i][j]) do begin for l := Low(fCK[i][j][k]) to High(fCK[i][j][k]) do begin fCK[i][j][k][l]:=fCK[i][j][k][l]-fCLearningRate*(AKGrad[j][k][l]); end; end; //■バイアスの更新 //B=B-η×Σ∂E/∂y der:=0; for k := Low(PoolingDer[j])to High(PoolingDer[j]) do begin for l := Low(PoolingDer[j][k])to High(PoolingDer[j][k]) do begin der:=der+PoolingDer[j][k][l]; end; end; fCB[i][j]:=fCB[i][j]-fCLearningRate*der; end; end; end; constructor TMamCnnNn.Create; var i,j,k,l:Integer; num:Integer; scale:Single; begin Randomize; //■CNN層初期化 fTrainCount:=0; fLoss:=0; fCLearningRate:=0.05; //学習率 5% fCLNum:=2; //層の数 fCH:=28; //入力層の高さ fCW:=28; //入力層の幅 fCKNum:=10; //各層の重み(カーネル)の数 SetLength(fCKHW,fCLnum-1);////層の数-1 fCKHW[0]:=25;//1層目の各重み(カーネル)の高さと幅(高さ=幅) //fCKHW[1]:=5; //2層目の各重み(カーネル)の高さと幅(高さ=幅) //畳み込みはストライド=1、パディングは無しとする //プーリング処理は、2x2領域固定でパディング無しとする fCStride:=2;//プーリング時のストライド SetLength(fCSum,fCLNum); for i := Low(fCSum) to High(fCSum) do begin SetLength(fCSum[i],fCKNum); for j := Low(fCSum[i]) to High(fCSum[i]) do begin if i=0 then //入力層の場合 SetLength(fCSum[i][j],fCH) else begin //入力層以外 //切り上げ k:=System.Math.Ceil((Length(FCSum[i-1][0])-fCKHW[i-1]+1)/fCStride); SetLength(fCSum[i][j],k); end; for k := Low(fCSum[i][j]) to High(fCSum[i][j]) do begin if i=0 then //入力層の場合 SetLength(fCSum[i][j][k],fCW) else begin //入力層以外 //切り上げ l:=System.Math.Ceil((Length(fCSum[i-1][j][k])-fCKHW[i-1]+1)/fCStride); SetLength(fCSum[i][j][k],l); end; end; end; end; //重み(カーネル)の初期化 SetLength(fCK,fCLNum-1);//2 for i := Low(fCK) to High(fCK) do begin SetLength(fCK[i],fCKNum); for j := Low(fCK[i]) to High(fCK[i]) do begin SetLength(fCK[i][j],fCKHW[i]); for k := Low(fCK[i][j]) to High(fCK[i][j]) do begin SetLength(fCK[i][j][k],fCKHW[i]); for l := Low(fCK[i][j][k]) to High(fCK[i][j][k]) do begin fCK[i][j][k][l]:=(Random()*1)/Length(fCK[i][j])/Length(fCK[i][j][k]); end; end; end; end; SetLength(fCKOut,fCLNum-1);//層の数-1 SetLength(fCKMax,fCLNum-1);//層の数-1 for i := Low(fCKOut) to High(fCKOut) do begin SetLength(fCKOut[i],fCKNum); SetLength(fCKMax[i],fCKNum); for j := Low(fCKOut[i]) to High(fCKOut[i]) do begin SetLength(fCKOut[i][j], Length(fCSum[i][j])-Length(fCK[i][j])+1); SetLength(fCKMax[i][j],Length(fCKOut[i][j])); for k := Low(fCKOut[i][j]) to High(fCKOut[i][j]) do begin SetLength(fCKOut[i][j][k], Length(fCSum[i][j][0])-Length(fCK[i][j][0])+1); SetLength(fCKMax[i][j][k],Length(fCKOut[i][j][k])); end; end; end; //バイアスの初期化 SetLength(fCB,fCLNum-1);//2 for i := Low(fCB) to High(fCB) do begin SetLength(fCB[i],fCKNum); ZeroMemory(@fCB[i][0],fCKNum*SizeOf(fCB[i][0])); end; //■全結合層 NN初期化 fLayerCount:=3; SetLength(fNeuronCountInLayers,fLayerCount); fNeuronCountInLayers[0]:= Length(fCSum[High(fCSum)])* Length(fCSum[High(fCSum)][0])* Length(fCSum[High(fCSum)][0][0]);//10x2x2 CNN→NNへ繋ぐ入力層 fNeuronCountInLayers[1]:=30; //中間層 fNeuronCountInLayers[2]:=10; //出力層 //値初期化 SetLength(fOut,fLayerCount); SetLength(fSum,fLayerCount); for i := Low(fOut) to High(fOut) do begin num:=fNeuronCountInLayers[i]; SetLength(fOut[i],num); SetLength(fSum[i],num); end; //バイアス初期化(0にする) SetLength(fBias,fLayerCount-1); for i := Low(fBias) to High(fBias) do begin SetLength(fBias[i],fNeuronCountInLayers[i+1]); for j := Low(fBias[i]) to High(fBias[i]) do fBias[i][j]:=0; end; //重みづけ初期化 SetLength(fWeight,fLayerCount-1); for i := Low(fWeight) to High(fWeight) do begin SetLength(fWeight[i],fNeuronCountInLayers[i+1]); for j := Low(fWeight[i]) to High(fWeight[i]) do begin SetLength(fWeight[i][j],Length(fOut[i])); if i=High(fWeight) then begin scale:=1/sqrt(Length(fWeight[i][j])); end else begin //乱数だけだと永遠にMSEが収束しない場合がある(1/20回くらい) //平均0、√(2/(n1+n2))のガウス分布乱数 scale:=sqrt(2/Length(fWeight[i][j])) end; for k := Low(fWeight[i][j]) to High(fWeight[i][j]) do begin if (i=High(fWeight[i])) then begin if k=Low(fWeight[i][j]) then fWeight[i][j][k]:=scale else if k=High(fWeight[i][j]) then fWeight[i][j][k]:=-scale else fWeight[i][j][k]:=RandG(0,scale); end else if k=Low(fWeight[i][j]) then fWeight[i][j][k]:=scale*3.5 else if k=High(fWeight[i][j]) then fWeight[i][j][k]:=-scale*0.5 else fWeight[i][j][k]:=RandG(0,scale); end; end; end; end; destructor TMamCnnNn.Destroy; begin inherited; end; procedure TMamCnnNn.ForwardProp(CInput:TArray<TArray<Single>>); var i,j,k,l,h,w,hmax,wmax:Integer; v:Single; s,sm,av:single; ww:single; begin i:=0; //入力層の値を入れる for j := Low(fCSum[i]) to High(fCSum[i]) do begin for k := Low(fCSum[i][j]) to High(fCSum[i][j]) do begin Move(CInput[k][0],fCSum[i][j][k][0], Length(fCSum[i][j][k])*SizeOf(fCSum[i][j][k][0])); end; end; //CNN層 for i := Low(fCKOut) to High(fCKOut) do begin sm:=0; for j := Low(fCKOut[i]) to High(fCKOut[i]) do begin //畳み込み処理 for k := Low(fCKOut[i][j]) to High(fCKOut[i][j]) do begin for l := Low(fCKOut[i][j][k]) to High(fCKOut[i][j][k]) do begin v:=0; for h := Low(fCK[i][j]) to High(fCK[i][j]) do begin for w := Low(fCK[i][j][h]) to High(fCK[i][j][h]) do begin v:=v+fCSum[i][j][k+h][l+w]*fCK[i][j][h][w]; end; end; fCKOut[i][j][k][l]:=v+fCB[i][j]; end; end; //最大プーリング処理 2x2 ストライド=2 ⇒縦横半分のサイズになる //最大値の位置を保持しておく for k:=Low(fCKMax[i][j]) to High(fCKMax[i][j]) do ZeroMemory(@fCKMax[i][j][k][0], Length(fCKMax[i][j][k])*SizeOf(fCKMax[i][j][k][0])); for k := Low(fCSum[i+1][j]) to High(fCSum[i+1][j]) do begin for l := Low(fCSum[i+1][j][k]) to High(fCSum[i+1][j][k]) do begin v:=-1e38; hmax:=0; wmax:=0; for h := 0 to fCStride-1 do begin if High(fCKOut[i][j])>=(k*fCStride+h) then begin for w := 0 to fCStride-1 do begin if High(fCKOut[i][j][k*fCStride+h])>=(l*fCStride+w) then begin if v<fCKOut[i][j][k*fCStride+h][l*fCStride+w] then begin hmax:=h; wmax:=w; v:=fCKOut[i][j][k*fCStride+h][l*fCStride+w]; end; end; end; end; end; fCKMax[i][j][k*fCStride+hmax][l*fCStride+wmax]:=1; fCSum[i+1][j][k][l]:=v; end; end; end; end; //CNN→NNへ繋ぐ 活性化関数(fCSum[MAX][3次元])→ fOut[1次元]へ変換 i:=0; for j := Low(fCSum[High(fCSum)]) to High(fCSum[High(fCSum)]) do begin for k := Low(fCSum[High(fCSum)][j]) to High(fCSum[High(fCSum)][j]) do begin for l := Low(fCSum[High(fCSum)][j][k]) to High(fCSum[High(fCSum)][j][k]) do begin fSum[0][i]:=fCSum[High(fCSum)][j][k][l]; fOut[0][i]:=Sigmoid(fCSum[High(fCSum)][j][k][l]); inc(i); end; end; end; //入力層から中間層から出力層まで計算する for i := Low(fWeight) to High(fWeight) do begin for j := Low(fWeight[i]) to High(fWeight[i]) do begin //fSum[i+1][j]:= // fOut[i][ 0]*fWeight[i][j][ 0]+ // fOut[i][ 1]*fWeight[i][j][ 1]+ // fOut[i][...]*fWeight[i][j][...]+fBias[i][j]; //fOut[i*1][j]:=活性化関数(fSum[i+1][j]); ww:=fBias[i][j]; for k := Low(fWeight[i][j]) to High(fWeight[i][j]) do begin ww:=ww+fOut[i][k]*fWeight[i][j][k]; end; fSum[i+1][j]:=ww; if i=(fLayerCount-2) then begin //出力層の場合 シグモイド関数 ww:=Sigmoid(ww); end else begin //出力層以外の場合 ReLU関数 ww:=ReLU(ww); end; fOut[i+1][j]:=ww; end; end; end; function TMamCnnNn.GetMSE: Single; begin if fTrainCount>0 then result:=fLoss/fTrainCount else result:=0; end; function TMamCnnNn.Run(CInput:TArray<TArray<Single>>):TArray<Single>; begin ForwardProp(CInput); SetLength(result,Length(fOut[High(fOut)])); Move(FOut[High(fOut)][0],result[0], Length(FOut[High(fOut)])*SizeOf(FOut[High(fOut)][0])); end; procedure TMamCnnNn.Train(CInput: TArray<TArray<Single>>; COutput: TArray<Single>); var i:Integer; sub:Single; begin ForwardProp(CInput); BackProp(COutput); for i := Low(COutput) to High(COutput) do begin inc(fTrainCount); sub:=fOut[High(fOUt)][i]-COutput[i]; fLoss:=fLoss+sub*sub; end; end; end.