人工ニューラルネットワークをフルスクラッチで作成 ~Delphiソースコード集
人工ニューラルネットワーク(Artificial Neural Network)を使用する為のファイルの準備
本ページ最下部にある「UMamAnn.pas」ソースコードを、プロジェクトフォルダ内に「UMamAnn.pas」ファイルとして保存します。
ほかに必要なファイルやライブラリはありません。
TMamAnnクラスのインスタンスを作り、Trainプロシージャで学習し、Runプロシージャで学習結果をもとに結果を返します。
活性化関数はシグモイド関数だけです。
参考URL
https://atmarkit.itmedia.co.jp/ait/articles/2202/09/news027.html
Delphiを起動して新規作成を行い、必要なコンポーネントをドラッグ&ドロップする
Delphi起動⇒ファイル⇒新規作成⇒WindowsVCLアプリケーション を選択します。
TButton 2個、TMemo 1個、をフォームへドラッグ&ドロップします。
「ファイル」⇒「全て保存」でフォルダを作成して、プロジェクトとユニットを保存します。
プロジェクトフォルダ内にUMamAnn.pasファイルを配置します。
ソースコードを記述する
Button1のOnClickイベント、Button2の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;
type
TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
Memo1: TMemo;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
private
{ Private 宣言 }
public
{ Public 宣言 }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
uses UMamAnn;
procedure TForm1.Button1Click(Sender: TObject);
var Neurons:TArray<Integer>;
Ann:TMamAnn;
Inputs,OutPuts:TArray<Single>;
epoch:Integer;//繰り返し学習
begin
//排他論理和(XOR)を「回帰問題」として学習させる
Memo1.Clear;
SetLength(Neurons,3);//3層
Neurons[0]:=2;//入力層のニューロン数
Neurons[1]:=2;//中間層のニューロン数
Neurons[2]:=1;//出力層のニューロン数
SetLength(Inputs,2); //入力層データ
SetLength(Outputs,1);//出力層データ
Ann:=TMamAnn.Create(Neurons);
try
//10000回繰り返し学習させる
for epoch := 1 to 10000 do
begin
//0 XOR 0 = 0を学習させる
Inputs[0]:=0;
Inputs[1]:=0;
OutPuts[0]:= Trunc(Inputs[0]) XOR Trunc(Inputs[1]);
Ann.Train(Inputs,Outputs);//学習
//0 XOR 1 = 1を学習させる
Inputs[0]:=0;
Inputs[1]:=1;
OutPuts[0]:= Trunc(Inputs[0]) XOR Trunc(Inputs[1]);
Ann.Train(Inputs,Outputs);//学習
//1 XOR 0 = 1を学習させる
Inputs[0]:=1;
Inputs[1]:=0;
OutPuts[0]:= Trunc(Inputs[0]) XOR Trunc(Inputs[1]);
Ann.Train(Inputs,Outputs);//学習
//1 XOR 1 = 0を学習させる
Inputs[0]:=1;
Inputs[1]:=1;
OutPuts[0]:= Trunc(Inputs[0]) XOR Trunc(Inputs[1]);
Ann.Train(Inputs,Outputs);//学習
end;
//学習結果をファイルに保存する
//Ann.SaveToFile('xor.net');
Memo1.Lines.Add('排他論理和(XOR)を「回帰問題」として学習');
//0 XOR 0 がいくつかAIに聞く
Inputs[0]:=0;
Inputs[1]:=0;
Ann.Run(Inputs,Outputs);
Memo1.Lines.Add(
Format('%1.0f XOR %1.0f = %1.0f',[Inputs[0],Inputs[1],Outputs[0]])
);
//0 XOR 1 がいくつかAIに聞く
Inputs[0]:=0;
Inputs[1]:=1;
Ann.Run(Inputs,Outputs);
Memo1.Lines.Add(
Format('%1.0f XOR %1.0f = %1.0f',[Inputs[0],Inputs[1],Outputs[0]])
);
//1 XOR 0 がいくつかAIに聞く
Inputs[0]:=1;
Inputs[1]:=0;
Ann.Run(Inputs,Outputs);
Memo1.Lines.Add(
Format('%1.0f XOR %1.0f = %1.0f',[Inputs[0],Inputs[1],Outputs[0]])
);
//1 XOR 1 がいくつかAIに聞く
Inputs[0]:=1;
Inputs[1]:=1;
Ann.Run(Inputs,Outputs);
Memo1.Lines.Add(
Format('%1.0f XOR %1.0f = %1.0f',[Inputs[0],Inputs[1],Outputs[0]])
);
finally
Ann.Free;
end;
end;
procedure TForm1.Button2Click(Sender: TObject);
var Neurons:TArray<Integer>;
Ann:TMamAnn;
Inputs,OutPuts:TArray<Single>;
epoch:Integer;//繰り返し学習
begin
//排他論理和(XOR)を「分類問題」として学習させる
Memo1.Clear;
SetLength(Neurons,3);//3層
Neurons[0]:=2;//入力層のニューロン数
Neurons[1]:=2;//中間層のニューロン数
Neurons[2]:=2;//出力層のニューロン数
SetLength(Inputs,2); //入力層データ
SetLength(Outputs,2);//出力層データ
Ann:=TMamAnn.Create(Neurons);
try
//10000回繰り返し学習させる
for epoch := 1 to 10000 do
begin
//0 XOR 0 = 0を学習させる
Inputs[0]:=0;
Inputs[1]:=0;
OutPuts[0]:=1;//答えは「0」か
OutPuts[1]:=0;//答えは「1」か
Ann.Train(Inputs,Outputs);//学習
//0 XOR 1 = 1を学習させる
Inputs[0]:=0;
Inputs[1]:=1;
OutPuts[0]:=0;//答えは「0」か
OutPuts[1]:=1;//答えは「1」か
Ann.Train(Inputs,Outputs);//学習
//1 XOR 0 = 1を学習させる
Inputs[0]:=1;
Inputs[1]:=0;
OutPuts[0]:=0;//答えは「0」か
OutPuts[1]:=1;//答えは「1」か
Ann.Train(Inputs,Outputs);//学習
//1 XOR 1 = 0を学習させる
Inputs[0]:=1;
Inputs[1]:=1;
OutPuts[0]:=1;//答えは「0」か
OutPuts[1]:=0;//答えは「1」か
Ann.Train(Inputs,Outputs);//学習
end;
//学習結果をファイルに保存する
//Ann.SaveToFile('xor.net');
Memo1.Lines.Add('排他論理和(XOR)を「分類問題」として0か1かを学習');
//0 XOR 0 が「0」か「1」のどちらかをAIに聞く
Inputs[0]:=0;
Inputs[1]:=0;
Ann.Run(Inputs,Outputs);
Memo1.Lines.Add(
Format('%1.0f XOR %1.0fが 0の確率%3.0f%%、1の確率%3.0f%%',
[Inputs[0],Inputs[1],Outputs[0]*100,Outputs[1]*100])
);
//0 XOR 1 がいくつかAIに聞く
Inputs[0]:=0;
Inputs[1]:=1;
Ann.Run(Inputs,Outputs);
Memo1.Lines.Add(
Format('%1.0f XOR %1.0fが 0の確率%3.0f%%、1の確率%3.0f%%',
[Inputs[0],Inputs[1],Outputs[0]*100,Outputs[1]*100])
);
//1 XOR 0 がいくつかAIに聞く
Inputs[0]:=1;
Inputs[1]:=0;
Ann.Run(Inputs,Outputs);
Memo1.Lines.Add(
Format('%1.0f XOR %1.0fが 0の確率%3.0f%%、1の確率%3.0f%%',
[Inputs[0],Inputs[1],Outputs[0]*100,Outputs[1]*100])
);
//1 XOR 1 がいくつかAIに聞く
Inputs[0]:=1;
Inputs[1]:=1;
Ann.Run(Inputs,Outputs);
Memo1.Lines.Add(
Format('%1.0f XOR %1.0fが 0の確率%3.0f%%、1の確率%3.0f%%',
[Inputs[0],Inputs[1],Outputs[0]*100,Outputs[1]*100])
);
finally
Ann.Free;
end;
end;
end.
実行する
実行ボタンを押して実行します。(デバッグ実行でもOK)
Button1をクリックすると、排他論理和(XOR)を「回帰問題」として学習し、AIが排他論理和の答えを返します。
Button2をクリックすると、排他論理和(XOR)を「分類問題」として学習し、AIが排他論理和の答えを返します。
UMamAnn.pasソースコード
unit UMamAnn;
interface
uses System.Math,System.Classes,System.SysUtils;
{
人工ニューラルネットワーク(Artificial Neural Network)クラスのライブラリ
活性化関数
TMamActivationFunc.Sigmoid指定時
中間層はシグモイド関数、出力層は恒等関数を使用
TMamActivationFunc.ReLU指定時
中間層はReLU関数、出力層はシグモイド関数を使用
◎=入力層のニューロン
●=中間層のニューロン
○=出力層のニューロン
■=重み
▼=バイアス
値 重み バイアス
fSum fWeight fBias
fOut fGradsW fGradsB
i0 ◎◎
j0j1 j0 j1 j2 j0j1j2
i0■■ ■■ ■■ i0▼▼▼
k0k1 k0k1 k0k1
i1 ●●●
j0j1j2 j0 j1 j0j1
i1■■■ ■■■ i1▼▼
k0k1k2 k0k1k2
i2 ●●
j0j1 j0 j1 j0j1
i2■■ ■■ i2▼▼
k0k1 k0k1
i3 ○○
j0j1
}
type
TMamActivationFunc=(Sigmoid,ReLU);
TMamAnn=class(TObject)
private
fSum:TArray<TArray<single>>;//各層の値(重み付線形和)
fOut:TArray<TArray<single>>;//各層の値(重み付線形和の活性化関数適用後)
fLayerCount:Integer;//層の数
fNeuronCountInLayers:TArray<Integer>;//各層のニューロン数
fLearningRate:single;//学習率
fLoss:Single;//損失値(累計2乗誤差)
fTrainCount:Integer;//学習回数
fBias:TArray<TArray<single>>;//バイアス
fWeight:TArray<TArray<TArray<single>>>;//重みづけ(-1~1)
fActivationFunc:TMamActivationFunc;
procedure ForwardProp(Inputs:TArray<Single>);//順伝播
procedure BackProp(Outputs:TArray<Single>); //逆伝播とパラメータ更新
function Sigmoid(x:Single):Single; //シグモイド
function SigmoidDerivative(x:single):single; //シグモイドの導関数
function ReLU(x: single): single;
function ReLUDerivative(x: single): single;
function GetLearningRate():Single;
procedure SetLearningRate(LearningRage:Single);
public
constructor Create(
NeuronCountInLayers:TArray<Integer>;LerningRate:Single=0.2;
ActivationFunc:TMamActivationFunc=TMamActivationFunc.Sigmoid
);overload;
constructor Create(Filename:String);overload;
//学習
procedure Train(Inputs,Outputs: TArray<Single>);
//予測の実行
procedure Run(Inputs:TArray<Single>;out Outputs:TArray<Single>);
//MSE(平均二乗誤差)出力
function GetMSE():Single;
destructor Destroy; override;
procedure SaveToFile(FileName:String);
procedure LoadFromFile(FileName:String);
property LearningRate:Single read GetLearningRate write SetLearningRate;
end;
implementation
{ TMamAnn }
function TMamAnn.ReLU(x: single): single;
begin
if x>0 then result:=x else result:=0;
end;
function TMamAnn.ReLUDerivative(x: single): single;
begin
if x>0 then result:=1 else result:=0;
end;
procedure TMamAnn.BackProp(Outputs:TArray<Single>);
var i,j,k:Integer;
loss_der:single;
back_error:TArray<Single>;
active_der:single;
delta:single;
sum_der_w,sum_der_x:TArray<Single>;
sum_der_b:Single;
layer_grads_x:TArray<Single>;
begin
//逆伝播とパラメータ更新
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
//出力層の場合
if fActivationFunc=TMamActivationFunc.Sigmoid then
//恒等関数の導関数
active_der:=1.0
else
//シグモイド関数の導関数
active_der:=SigmoidDerivative(fSum[i][j]);
end
else
begin
//出力層以外の場合
if fActivationFunc=TMamActivationFunc.Sigmoid then
//シグモイド関数の導関数
active_der:=SigmoidDerivative(fSum[i][j])
else
//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*fLearningRate;
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]*fLearningRate;
//入力は各ノードから前のノードに接続する全ての入力を合計する
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;
end;
constructor TMamAnn.Create(
NeuronCountInLayers:TArray<Integer>;LerningRate: Single=0.2;
ActivationFunc:TMamActivationFunc=TMamActivationFunc.Sigmoid);
var i,j,k,num:Integer;
scale:Single;
begin
fLearningRate:=LerningRate;
if fLearningRate>0.999 then fLearningRate:=0.999;
if fLearningRate<0.001 then fLearningRate:=0.001;
//Sigmoid または ReLU
fActivationFunc:=ActivationFunc;
fLoss:=0;
fTrainCount:=0;
fLayerCount:=Length(NeuronCountInLayers);
SetLength(fNeuronCountInLayers,fLayerCount);
Move(
NeuronCountInLayers[0],
fNeuronCountInLayers[0],
SizeOf(NeuronCountInLayers[0])*Length(NeuronCountInLayers)
);
Randomize;
//値初期化
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回くらい)
if fActivationFunc=TMamActivationFunc.Sigmoid then
//平均0、1/√nのガウス分布乱数
scale:=1/sqrt(Length(fWeight[i][j]))
else
//平均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 (fActivationFunc=TMamActivationFunc.Sigmoid) 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
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;
end;
constructor TMamAnn.Create(Filename: String);
begin
Randomize;
Self.LoadFromFile(Filename);
end;
destructor TMamAnn.Destroy;
begin
inherited;
end;
procedure TMamAnn.ForwardProp(Inputs: TArray<Single>);
var i,j,k:Integer;
ww:Single;
begin //順伝播
//入力層に入力値を入れる
Move(Inputs[0],fSum[0][0],SizeOf(Inputs[0])*Length(Inputs));
Move(Inputs[0],fOut[0][0],SizeOf(Inputs[0])*Length(Inputs));
//入力層から中間層から出力層まで計算する
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
//出力層の場合
if fActivationFunc=TMamActivationFunc.Sigmoid then
begin
//恒等関数
ww:=ww;
end
else
begin
//シグモイド関数
ww:=Sigmoid(ww);
end;
end
else
begin
//出力層以外の場合
if fActivationFunc=TMamActivationFunc.Sigmoid then
//シグモイド関数
ww:=Sigmoid(ww)
else
//ReLU関数
ww:=ReLU(ww);
end;
fOut[i+1][j]:=ww;
end;
end;
end;
function TMamAnn.GetLearningRate: Single;
begin
result:=fLearningRate;
end;
function TMamAnn.GetMSE: Single;
begin //MSE(平均二乗誤差)出力
if fTrainCount>0 then
result:=fLoss/fTrainCount
else
result:=0;
end;
procedure TMamAnn.LoadFromFile(FileName: String);
var strm:TMemoryStream;
i,j,k:Integer;
act:Integer;
begin //ファイルからの読み込み
strm:=TMemoryStream.Create;
try
strm.LoadFromFile(FileName);
strm.Position:=0;
//活性化関数読み込み
strm.ReadData(act);
if act=0 then
fActivationFunc:=TMamActivationFunc.Sigmoid
else
fActivationFunc:=TMamActivationFunc.ReLU;
//層数読み込み
strm.ReadData(fLayerCount);
//層ごとのニューロン数読み込み
setLength(fNeuronCountInLayers,fLayerCount);
for i := 0 to fLayerCount-1 do
strm.ReadData(fNeuronCountInLayers[i]);
//学習率の読み込み
strm.ReadData(fLearningRate);
//損失値(累計2乗誤差)読み込み
strm.ReadData(fLoss);
//学習回数読み込み
strm.ReadData(fTrainCount);
//fSum,fOutの準備
SetLength(fSum,fLayerCount);
SetLength(fOut,fLayerCount);
for i := 0 to fLayerCount-1 do
begin
SetLength(fSum[i],fNeuronCountInLayers[i]);
SetLength(fOut[i],fNeuronCountInLayers[i]);
end;
//バイアス読み込み
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
begin
strm.ReadData(fBias[i][j]);
end;
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],fNeuronCountInLayers[i]);
for k := Low(fWeight[i][j]) to High(fWeight[i][j]) do
begin
strm.ReadData(fWeight[i][j][k]);
end;
end;
end;
finally
strm.Free;
end;
end;
procedure TMamAnn.Run(Inputs: TArray<Single>; out Outputs: TArray<Single>);
begin //予測の実行
ForwardProp(Inputs);
//出力層の値のコピー
SetLength(Outputs,Length(fOut[High(fOut)]));
Move(
fOut[High(fOut)][0],
Outputs[0],
SizeOf(fOut[High(fOut)][0])*Length(fOut[High(fOut)])
);
end;
procedure TMamAnn.SaveToFile(FileName: String);
var strm:TMemoryStream;
i,j,k:Integer;
act:Integer;
begin //バイナリで保存する
strm:=TMemoryStream.Create();
try
//活性化関数保存
if fActivationFunc=TMamActivationFunc.Sigmoid then
act:=0
else
act:=1;
strm.WriteData(act);
//層数保存
strm.WriteData(fLayerCount);
//層ごとのニューロン数保存
for i := Low(fNeuronCountInLayers) to High(fNeuronCountInLayers) do
strm.WriteData(fNeuronCountInLayers[i]);
//学習率保存
strm.WriteData(fLearningRate);
//損失値(累計2乗誤差)保存
strm.WriteData(fLoss);
//学習回数保存
strm.WriteData(fTrainCount);
//バイアス保存
for i := Low(fBias) to High(fBias) do
for j := Low(fBias[i]) to High(fBias[i]) do
strm.WriteData(fBias[i][j]);
//重み保存
for i := Low(fWeight) to High(fWeight) do
for j := Low(fWeight[i]) to High(fWeight[i]) do
for k := Low(fWeight[i][j]) to High(fWeight[i][j]) do
strm.WriteData(fWeight[i][j][k]);
strm.SaveToFile(FileName);
finally
strm.Free;
end;
end;
procedure TMamAnn.SetLearningRate(LearningRage:Single);
begin
if LearningRate>0.999 then LearningRate:=0.999;
if LearningRate<0.001 then LearningRate:=0.001;
fLearningRate:=LearningRate;
end;
function TMamAnn.Sigmoid(x: Single): Single;
begin //シグモイド関数
result:=1.0/(1.0+system.exp(-x))
end;
function TMamAnn.SigmoidDerivative(x:single): single;
begin //シグモイド関数の微分関数
result:=Sigmoid(x);
result:=result*(1.0-result);
end;
procedure TMamAnn.Train(Inputs, Outputs: Tarray<Single>);
var j:Integer;
sub:Single;
begin //学習
//順伝播
ForwardProp(Inputs);
//逆伝播
BackProp(Outputs);
//MSE(平均二乗誤差)の為に累計2乗誤差計算
for j := Low(Outputs) to High(Outputs) do
begin
inc(fTrainCount);
sub:=fOut[High(fOut)][j]-Outputs[j];
fLoss:=fLoss+sub*sub;
end;
end;
end.
(64Bitコンパイル用)UMamAnn.pasソースコード
unit UMamAnn;
interface
uses System.Math,System.Classes,System.SysUtils,winapi.windows;
{
人工ニューラルネットワーク(Artificial Neural Network)クラスのライブラリ
活性化関数
TMamActivationFunc.Sigmoid指定時
中間層はシグモイド関数、出力層は恒等関数を使用します
TMamActivationFunc.ReLU指定時
中間層はReLU関数、出力層はシグモイド関数を使用します
◎=入力層のニューロン
●=中間層のニューロン
○=出力層のニューロン
■=重み
▼=バイアス
値 重み バイアス
fSum fWeight fBias
fOut fGradsW fGradsB
i0 ◎◎
j0j1 j0 j1 j2 j0j1j2
i0■■ ■■ ■■ i0▼▼▼
k0k1 k0k1 k0k1
i1 ●●●
j0j1j2 j0 j1 j0j1
i1■■■ ■■■ i1▼▼
k0k1k2 k0k1k2
i2 ●●
j0j1 j0 j1 j0j1
i2■■ ■■ i2▼▼
k0k1 k0k1
i3 ○○
j0j1
}
type
TMamActivationFunc=(Sigmoid,ReLU);
TMamAnn=class(TObject)
private
fSum:TArray<TArray<single>>;//各層の値(重み付線形和)
fOut:TArray<TArray<single>>;//各層の値(重み付線形和の活性化関数適用後)
fLayerCount:Integer;//層の数
fNeuronCountInLayers:TArray<Integer>;//各層のニューロン数
fLearningRate:single;//学習率
fLoss:Single;//損失値(累計2乗誤差)
fTrainCount:Integer;//学習回数
fBias:TArray<TArray<single>>;//バイアス
fWeight:TArray<TArray<TArray<single>>>;//重みづけ(-1~1)
fActivationFunc:TMamActivationFunc;
procedure ForwardProp(Inputs:TArray<Single>);//順伝播
procedure BackProp(Outputs:TArray<Single>); //逆伝播とパラメータ更新
function Sigmoid(x:Single):Single; //シグモイド
function SigmoidDerivative(x:single):single; //シグモイドの導関数
function ReLU(x: single): single;
function ReLUDerivative(x: single): single;
function GetLearningRate():Single;
procedure SetLearningRate(LearningRage:Single);
public
constructor Create(
NeuronCountInLayers:TArray<Integer>;LerningRate:Single=0.2;
ActivationFunc:TMamActivationFunc=TMamActivationFunc.Sigmoid
);overload;
constructor Create(Filename:String);overload;
//学習
procedure Train(Inputs,Outputs: TArray<Single>);
//予測の実行
procedure Run(Inputs:TArray<Single>;out Outputs:TArray<Single>);
//MSE(平均二乗誤差)出力
function GetMSE():Single;
destructor Destroy; override;
procedure SaveToFile(FileName:String);
procedure LoadFromFile(FileName:String);
property LearningRate:Single read GetLearningRate write SetLearningRate;
end;
implementation
//SSEで同時4つの浮動小数点演算を行う pD = pD + pS*coff4
procedure coffSum4(var pD, pS, coff4: single);
asm // RCX RDX R8
MOVUPS XMM0, [R8] //XMM0=coff4[0..3]
MOVUPS XMM1, [RDX] //XMM1=pS[0..3]
MOVUPS XMM2, [RCX] //XMM2=pD[0..3]
MULPS XMM1, XMM0 //XMM1=XMM1*XMM0(pS*coff4)
ADDPS XMM2, XMM1 //XMM2=XMM2+XMM1
MOVUPS [RCX],XMM2 //pD=XMM2
end;
procedure coffSum16(var pD, pS, coff4: single);
asm // RCX RDX R8
MOVUPS XMM0, [R8] //XMM0=coff4[0..3]
MOVUPS XMM1, [RDX] //XMM1=pS[0..3]
MOVUPS XMM2, [RCX] //XMM2=pD[0..3]
MULPS XMM1, XMM0 //XMM1=XMM1*XMM0(pS*coff4)
ADDPS XMM2, XMM1 //XMM2=XMM2+XMM1
MOVUPS [RCX],XMM2 //pD[0..3]=XMM2
MOVUPS XMM1, [RDX+16] //XMM1=pS[4..7]
MOVUPS XMM2, [RCX+16] //XMM2=pD[4..7]
MULPS XMM1, XMM0 //XMM1=XMM1*XMM0(pS*coff4)
ADDPS XMM2, XMM1 //XMM2=XMM2+XMM1
MOVUPS [RCX+16],XMM2 //pD[4..7]=XMM2
MOVUPS XMM1, [RDX+32] //XMM1=pS[8..11]
MOVUPS XMM2, [RCX+32] //XMM2=pD[8..11]
MULPS XMM1, XMM0 //XMM1=XMM1*XMM0(pS*coff4)
ADDPS XMM2, XMM1 //XMM2=XMM2+XMM
MOVUPS [RCX+32],XMM2 //pD[8..11]=XMM2
MOVUPS XMM1, [RDX+48] //XMM1=pS[12..15]
MOVUPS XMM2, [RCX+48] //XMM2=pD[12..15]
MULPS XMM1, XMM0 //XMM1=XMM1*XMM0(pS*coff4)
ADDPS XMM2, XMM1 //XMM2=XMM2+XMM
MOVUPS [RCX+48],XMM2 //pD[12..15]=XMM2
end;
procedure AVXcoffSum(var arD, arS: single; const coff: single; Count: integer);
var Coff4: array[0..3] of single; //SSE命令の為の4word(16byte)の入れ物
pD, pS: ^single;
i: integer;
begin
pD := @arD;
pS := @arS;
for i := 0 to High(Coff4) do coff4[i] := coff;
while Count >= 16 do //SSE命令はWin64なら必須だから4個一度に計算可能
begin
coffSum16(pD^, pS^, coff4[0]);
Inc(pD, 16);
Inc(pS, 16);
Dec(Count, 16);
end;
while Count >= 4 do //SSE命令はWin64なら必須だから4個一度に計算可能
begin
coffSum4(pD^, pS^,coff4[0]);
Inc(pD, 4);
Inc(pS, 4);
Dec(Count, 4);
end;
while Count > 0 do //残りは最大3個で普通に計算
begin
pD^ := pD^ + coff * pS^;
Inc(pD);
Inc(pS);
Dec(Count, 1);
end;
end;
//SSEで同時4つの浮動小数点演算を行う coff4[0]=SUM(pD*pS)
//ww:=ww+fOut[i][k]*fWeight[i][j][k];
procedure MulSum4(var pD, pS, coff4: single);
asm // RCX RDX R8
MOVUPS XMM1, [RDX] //XMM1=pS[0..3]
MOVUPS XMM2, [RCX] //XMM2=pD[0..3]
DPPS XMM2, XMM1,$f1 //XMM2=SUM(XMM2*XMM1)
MOVUPS [R8],XMM2 //conn4=XMM2
end;
function MulAdd(var arD, arS: single; Count: integer):single;
var Coff4: array[0..3] of single; //SSE命令の為の4word(16byte)の入れ物
pD, pS: ^single;
begin
pD := @arD;
pS := @arS;
//ZeroMemory(@Coff4[0],Length(Coff4)*Sizeof(Coff4[0]));
result:=0;
while Count >= 4 do //SSE命令はWin64なら必須だから4個一度に計算可能
begin
MulSum4(pD^, pS^,coff4[0]);
result:=result+coff4[0];
Inc(pD, 4);
Inc(pS, 4);
Dec(Count, 4);
end;
while Count > 0 do //残りは最大3個で普通に計算
begin
result := result+ pD^ * pS^;
Inc(pD);
Inc(pS);
Dec(Count, 1);
end;
end;
//SSEで同時4つの浮動小数点演算を行う layer_grads_x[k]:=sum_der_x[k]*delta
procedure Mul4(var pD, pS, coff4: single);
asm // RCX RDX R8
MOVUPS XMM0, [R8] //XMM0=coff4[0..3]
MOVUPS XMM1, [RDX] //XMM1=pS[0..3]
MULPS XMM1, XMM0 //XMM1=XMM1*XMM0(pS*coff4)
MOVUPS [RCX],XMM1 //pD = XMM1
end;
procedure Mul(var arD, arS: single; const coff:Single; Count: integer);
var Coff4: array[0..3] of single; //SSE命令の為の4word(16byte)の入れ物
pD, pS: ^single;
i:Integer;
begin
pD := @arD;
pS := @arS;
for i := 0 to High(Coff4) do coff4[i] := coff;
while Count >= 4 do //SSE命令はWin64なら必須だから4個一度に計算可能
begin
Mul4(pD^, pS^,coff4[0]);
Inc(pD, 4);
Inc(pS, 4);
Dec(Count, 4);
end;
while Count > 0 do //残りは最大3個で普通に計算
begin
pD^ := coff * pS^;
Inc(pD);
Inc(pS);
Dec(Count, 1);
end;
end;
//SSEで同時4つの浮動小数点演算を行う back_error[j]:=fOut[i][j]-Outputs[j]
procedure SUBPS4(var pD, pS, pC: single);
asm // RCX RDX R8
MOVUPS XMM0, [R8] //XMM0=pC[0..3]
MOVUPS XMM1, [RDX] //XMM1=pS[0..3]
SUBPS XMM1, XMM0 //XMM1=XMM1-XMM0
MOVUPS [RCX],XMM1 //pD=XMM1
end;
procedure SUBPS(var arD, arS, arC: single; Count: integer);
var
pD, pS, pC: ^single;
i:Integer;
begin
pD := @arD;
pS := @arS;
pC := @arC;
while Count >= 4 do //SSE命令はWin64なら必須だから4個一度に計算可能
begin
SUBPS4(pD^, pS^, pC^);
Inc(pD, 4);
Inc(pS, 4);
Inc(pC, 4);
Dec(Count, 4);
end;
while Count > 0 do //残りは最大3個で普通に計算
begin
pD^ := pS^ - pC^;
Inc(pD);
Inc(pS);
Inc(pC);
Dec(Count, 1);
end;
end;
{ TMamAnn }
function TMamAnn.ReLU(x: single): single;
begin
if x>0 then result:=x else result:=0;
end;
function TMamAnn.ReLUDerivative(x: single): single;
begin
if x>0 then result:=1 else result:=0;
end;
procedure TMamAnn.BackProp(Outputs:TArray<Single>);
var i,j{,k}:Integer;
//loss_der:single;
back_error:TArray<Single>;
active_der:single;
delta:single;
sum_der_w,sum_der_x:TArray<Single>;
sum_der_b:Single;
layer_grads_x:TArray<Single>;
begin
//逆伝播とパラメータ更新
SetLength(layer_grads_x,0);
for i := (fLayerCount-1) downto 1 do
begin
if i=(fLayerCount-1) then
begin
//出力層の場合
SetLength(back_error,Length(Outputs));
SUBPS(back_error[0],fOut[i][0],Outputs[0],Length(Outputs));
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
//出力層の場合
if fActivationFunc=TMamActivationFunc.Sigmoid then
//恒等関数の導関数
active_der:=1.0
else
//シグモイド関数の導関数
active_der:=SigmoidDerivative(fSum[i][j]);
end
else
begin
//出力層以外の場合
if fActivationFunc=TMamActivationFunc.Sigmoid then
//シグモイド関数の導関数
active_der:=SigmoidDerivative(fSum[i][j])
else
//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*fLearningRate;
if j=0 then SetLength(layer_grads_x,Length(sum_der_w));
//勾配から重みの計算を行い重みを更新する
AVXcoffSum(
fWeight[i-1][j][0],sum_der_w[0],-delta*fLearningRate,Length(sum_der_w));
//入力は各ノードから前のノードに接続する全ての入力を合計する
if j=0 then
begin
Mul(layer_grads_x[0],sum_der_x[0],delta,Length(layer_grads_x));
end
else
begin
AVXcoffSum(
layer_grads_x[0],sum_der_x[0],delta,Length(sum_der_w));
end;
end;
end;
end;
constructor TMamAnn.Create(
NeuronCountInLayers:TArray<Integer>;LerningRate: Single=0.2;
ActivationFunc:TMamActivationFunc=TMamActivationFunc.Sigmoid);
var i,j,k,num:Integer;
scale:Single;
begin
fLearningRate:=LerningRate;
if fLearningRate>0.999 then fLearningRate:=0.999;
if fLearningRate<0.001 then fLearningRate:=0.001;
//Sigmoid または ReLU
fActivationFunc:=ActivationFunc;
fLoss:=0;
fTrainCount:=0;
fLayerCount:=Length(NeuronCountInLayers);
SetLength(fNeuronCountInLayers,fLayerCount);
Move(
NeuronCountInLayers[0],
fNeuronCountInLayers[0],
SizeOf(NeuronCountInLayers[0])*Length(NeuronCountInLayers)
);
Randomize;
//値初期化
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回くらい)
if fActivationFunc=TMamActivationFunc.Sigmoid then
//平均0、1/√nのガウス分布乱数
scale:=1/sqrt(Length(fWeight[i][j]))
else
//平均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 (fActivationFunc=TMamActivationFunc.Sigmoid) 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
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;
end;
constructor TMamAnn.Create(Filename: String);
begin
Randomize;
Self.LoadFromFile(Filename);
end;
destructor TMamAnn.Destroy;
begin
inherited;
end;
procedure TMamAnn.ForwardProp(Inputs: TArray<Single>);
var i,j{,k}:Integer;
ww:Single;
begin //順伝播
//入力層に入力値を入れる
Move(Inputs[0],fSum[0][0],SizeOf(Inputs[0])*Length(Inputs));
Move(Inputs[0],fOut[0][0],SizeOf(Inputs[0])*Length(Inputs));
//入力層から中間層から出力層まで計算する
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]+MulAdd(fOut[i][0],fWeight[i][j][0],Length(fOut[i]));
fSum[i+1][j]:=ww;
if i=(fLayerCount-2) then
begin
//出力層の場合
if fActivationFunc=TMamActivationFunc.Sigmoid then
begin
//恒等関数
//ww:=ww;
end
else
begin
//シグモイド関数
ww:=Sigmoid(ww);
end;
end
else
begin
//出力層以外の場合
if fActivationFunc=TMamActivationFunc.Sigmoid then
//シグモイド関数
ww:=Sigmoid(ww)
else
//ReLU関数
ww:=ReLU(ww);
end;
fOut[i+1][j]:=ww;
end;
end;
end;
function TMamAnn.GetLearningRate: Single;
begin
result:=fLearningRate;
end;
function TMamAnn.GetMSE: Single;
begin //MSE(平均二乗誤差)出力
if fTrainCount>0 then
result:=fLoss/fTrainCount
else
result:=0;
end;
procedure TMamAnn.LoadFromFile(FileName: String);
var strm:TMemoryStream;
i,j,k:Integer;
act:Integer;
begin //ファイルからの読み込み
strm:=TMemoryStream.Create;
try
strm.LoadFromFile(FileName);
strm.Position:=0;
//活性化関数読み込み
strm.ReadData(act);
if act=0 then
fActivationFunc:=TMamActivationFunc.Sigmoid
else
fActivationFunc:=TMamActivationFunc.ReLU;
//層数読み込み
strm.ReadData(fLayerCount);
//層ごとのニューロン数読み込み
setLength(fNeuronCountInLayers,fLayerCount);
for i := 0 to fLayerCount-1 do
strm.ReadData(fNeuronCountInLayers[i]);
//学習率の読み込み
strm.ReadData(fLearningRate);
//損失値(累計2乗誤差)読み込み
strm.ReadData(fLoss);
//学習回数読み込み
strm.ReadData(fTrainCount);
//fSum,fOutの準備
SetLength(fSum,fLayerCount);
SetLength(fOut,fLayerCount);
for i := 0 to fLayerCount-1 do
begin
SetLength(fSum[i],fNeuronCountInLayers[i]);
SetLength(fOut[i],fNeuronCountInLayers[i]);
end;
//バイアス読み込み
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
begin
strm.ReadData(fBias[i][j]);
end;
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],fNeuronCountInLayers[i]);
for k := Low(fWeight[i][j]) to High(fWeight[i][j]) do
begin
strm.ReadData(fWeight[i][j][k]);
end;
end;
end;
finally
strm.Free;
end;
end;
procedure TMamAnn.Run(Inputs: TArray<Single>; out Outputs: TArray<Single>);
begin //予測の実行
ForwardProp(Inputs);
//出力層の値のコピー
SetLength(Outputs,Length(fOut[High(fOut)]));
Move(
fOut[High(fOut)][0],
Outputs[0],
SizeOf(fOut[High(fOut)][0])*Length(fOut[High(fOut)])
);
end;
procedure TMamAnn.SaveToFile(FileName: String);
var strm:TMemoryStream;
i,j,k:Integer;
act:Integer;
begin //バイナリで保存する
strm:=TMemoryStream.Create();
try
//活性化関数保存
if fActivationFunc=TMamActivationFunc.Sigmoid then
act:=0
else
act:=1;
strm.WriteData(act);
//層数保存
strm.WriteData(fLayerCount);
//層ごとのニューロン数保存
for i := Low(fNeuronCountInLayers) to High(fNeuronCountInLayers) do
strm.WriteData(fNeuronCountInLayers[i]);
//学習率保存
strm.WriteData(fLearningRate);
//損失値(累計2乗誤差)保存
strm.WriteData(fLoss);
//学習回数保存
strm.WriteData(fTrainCount);
//バイアス保存
for i := Low(fBias) to High(fBias) do
for j := Low(fBias[i]) to High(fBias[i]) do
strm.WriteData(fBias[i][j]);
//重み保存
for i := Low(fWeight) to High(fWeight) do
for j := Low(fWeight[i]) to High(fWeight[i]) do
for k := Low(fWeight[i][j]) to High(fWeight[i][j]) do
strm.WriteData(fWeight[i][j][k]);
strm.SaveToFile(FileName);
finally
strm.Free;
end;
end;
procedure TMamAnn.SetLearningRate(LearningRage:Single);
begin
if LearningRate>0.999 then LearningRate:=0.999;
if LearningRate<0.001 then LearningRate:=0.001;
fLearningRate:=LearningRate;
end;
function TMamAnn.Sigmoid(x: Single): Single;
begin //シグモイド関数
result:=1.0/(1.0+system.exp(-x))
end;
function TMamAnn.SigmoidDerivative(x:single): single;
begin //シグモイド関数の微分関数
result:=Sigmoid(x);
result:=result*(1.0-result);
end;
procedure TMamAnn.Train(Inputs, Outputs: Tarray<Single>);
var j:Integer;
sub:Single;
begin //学習
//順伝播
ForwardProp(Inputs);
//逆伝播
BackProp(Outputs);
//MSE(平均二乗誤差)の為に累計2乗誤差計算
for j := Low(Outputs) to High(Outputs) do
begin
inc(fTrainCount);
sub:=fOut[High(fOut)][j]-Outputs[j];
fLoss:=fLoss+sub*sub;
end;
end;
end.
