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.
