内容
- 概述
- 1. 自适應優化方法的顯著特征
- 1.1. 自适應梯度方法(AdaGrad)
- 1.2. RMSProp 方法
- 1.3. Adadelta 方法
- 1.4. 自适應動量評估方法(亞當)
- 2. 實作
- 2.1. 建構 OpenCL 核心
- 2.2. 修改主程式裡的神經元類代碼
- 2.3. 修改不使用 OpenCL 的類代碼
- 2.4. 修改主程式中的神經網絡類代碼
- 3. 測試
- 結束語
- 參考
- 本文中用到的程式
概述
在之前的文章中,我們采用了不同類型的神經元,但我們始終利用随機梯度下降法來訓練神經網絡。 該方法可稱為基本方法,在實踐中經常會用到其變體。 不過,還有許多其他的神經網絡訓練方法。 今天,我提議研究自适應學習方法。 這一族方法可在神經網絡訓練期間改變神經元學習速率。
1. 自适應優化方法的顯著特征
您知道并非所有饋入神經網絡的特征值都會對最終結果産生相同的影響。 一些參數可能會包含很多噪聲,且比其他變化更頻繁,振幅也有所不同。 其他參數的樣本可能包含稀有值,當采用固定學習速率訓練神經網絡時,這些稀有值可能不會被注意到。 之前研究過的随機梯度下降方法的缺點之一是在此類樣本上無法使用優化機制。 結果就是,學習過程可能在局部最小值處停止。 可采用自适應方法訓練神經網絡來解決該問題。 這些方法能夠在神經網絡訓練過程中動态改變學習率。 這樣的方法及其變體有很多數量。 我們來研究其中最受歡迎的。
1.1. 自适應梯度方法(AdaGrad)
自适應梯度法于 2011 年提出。 它是随機梯度下降法的一種變體。 經由比較這些方法的數學公式,我們輕易注意到一個不同之處:對于所有之前的訓練疊代,AdaGrad 的學習率除以梯度平方和的平方根。 這種方式可降低頻繁更新參數的學習率。
而該方法的主要缺點來自其公式:梯度的平方和隻能增長,是以學習率趨于 0。 這最終會導緻訓練停止。
利用這種方法需要額外的計算和記憶體配置設定,以便儲存每個神經元的梯度平方和。
1.2. RMSProp 方法
AdaGrad 方法的邏輯延續自 RMSProp 方法。 為了避免學習率跌落到 0,在更新權重的公式分母中,已用梯度平方的指數均值替換過去的梯度平方和。 這種方法消除了分母中數值的恒定無限增長。 甚而,它更加關注表征模型目前狀态的最新梯度值。
1.3. Adadelta 方法
Adadelta 自适應方法幾乎與 RMSProp 同時提出。 此方法類似,它在更新權重的公式分母中采用了平方梯度總和的指數均值。 但與 RMSProp 不同,此方法徹底拒絕更新公式中的學習率,并在所分析參數中用之前修改的平方和的指數均值來替代。
這種方式能夠從更新權重的公式中删除學習率,并建立高度自适應的學習算法。 然而,此方法需要額外的計算疊代,并為存儲每個神經元的額外值而配置設定記憶體。
1.4. 自适應動量評估方法(亞當)
2014 年,Diederik P.Kingma 和 Jimmy Lei Ba 提出了自适應動量評估方法(Adam)。 根據作者的說法,該方法結合了 AdaGrad 和 RMSProp 方法的優點,非常适合線上訓練。 該方法在不同樣本上始終展現出良好的結果。 在各種軟體包中,通常建議按照預設使用它。
該方法基于計算出的梯度 m 的指數平均值,和平方梯度 v 的指數平均值。 每個指數平均值都有其自己的超參數 ß,由它判定平均周期。
作者建議預設采用 ß1 0.9 和 ß2 0.999。 在此情況下,m0 和 v0 取零值。 采用這些參數,上面介紹的公式在訓練開始時傳回值接近 0,是以開始時的學習率會很低。 為了加快學習過程,作者建議修改所獲得的動量。
通過調整校正梯度動量 m 與平方梯度 v 的校正動量的平方根之間的比率來更新參數。 為避免除零,在分母裡加入接近 0 的常數 Ɛ。 依據學習因子 α 調整所得比率,學習因子 α 在這種情況下是學習步幅的上限。 作者建議預設采用 α 0.001。
2. 實作
研究過理論方面之後,我們便可以進行實際實作了。 我建議采用作者提供的預設超參數來實作 Adam 方法。 進而,您可以嘗試其他超參數變體。
早前建立的神經網絡采用随機梯度下降法進行訓練,為此我們已經實作了反向傳播算法。 現有的反向傳播功能可用來實作 Adam 方法。 我們隻需要實作權重更新算法。 這個功能需經由 updateInputWeights 方法,它是在每個神經元類裡實作的。 當然,我們不會删除之前建立的随機梯度下降算法。 我們來建立一個替代算法,令您可以選擇要采用的訓練方法。
2.1. 建構 OpenCL 核心
研究 CNeuronBaseOCL 類的 Adam 方法實作。 首先,建立 UpdateWeightsAdam 核心實作 OpenCL 方法。 指向以下矩陣的指針則會通過參數傳遞給核心:
- 權重矩陣 — matrix_w,
- 誤差梯度矩陣 — matrix_g,
- 輸入資料矩陣 — matrix_i,
- 梯度指數均值矩陣 — matrix_m,
- 平方梯度的指數均值矩陣 — matrix_v.
__kernel void UpdateWeightsAdam(__global double *matrix_w,
__global double *matrix_g,
__global double *matrix_i,
__global double *matrix_m,
__global double *matrix_v,
int inputs, double l, double b1, double b2)
另外,在核心參數中,傳遞輸入資料數組的大小和 Adam 算法的超參數。
在核心伊始,擷取在兩維的流序列号,其分别訓示目前層和先前層的神經元數量。 使用接收到的編号,判斷緩沖區中已處理元素的初始編号。 請注意,第二維中的結果流編号應乘以 “4”。 這是因為為了減少流數量和程式執行的總時間,我們将利用含有 4 個元素的向量計算。
{
int i=get_global_id(0);
int j=get_global_id(1);
int wi=i*(inputs+1)+j*4;
判斷已處理元素在資料緩沖區中的位置後,聲明矢量變量,并用相應的數值填充它們。 利用先前講述的方法,并在向量中将缺失資料填充零值。
double4 m, v, weight, inp;
switch(inputs-j*4)
{
case 0:
inp=(double4)(1,0,0,0);
weight=(double4)(matrix_w[wi],0,0,0);
m=(double4)(matrix_m[wi],0,0,0);
v=(double4)(matrix_v[wi],0,0,0);
break;
case 1:
inp=(double4)(matrix_i[j],1,0,0);
weight=(double4)(matrix_w[wi],matrix_w[wi+1],0,0);
m=(double4)(matrix_m[wi],matrix_m[wi+1],0,0);
v=(double4)(matrix_v[wi],matrix_v[wi+1],0,0);
break;
case 2:
inp=(double4)(matrix_i[j],matrix_i[j+1],1,0);
weight=(double4)(matrix_w[wi],matrix_w[wi+1],matrix_w[wi+2],0);
m=(double4)(matrix_m[wi],matrix_m[wi+1],matrix_m[wi+2],0);
v=(double4)(matrix_v[wi],matrix_v[wi+1],matrix_v[wi+2],0);
break;
case 3:
inp=(double4)(matrix_i[j],matrix_i[j+1],matrix_i[j+2],1);
weight=(double4)(matrix_w[wi],matrix_w[wi+1],matrix_w[wi+2],matrix_w[wi+3]);
m=(double4)(matrix_m[wi],matrix_m[wi+1],matrix_m[wi+2],matrix_m[wi+3]);
v=(double4)(matrix_v[wi],matrix_v[wi+1],matrix_v[wi+2],matrix_v[wi+3]);
break;
default:
inp=(double4)(matrix_i[j],matrix_i[j+1],matrix_i[j+2],matrix_i[j+3]);
weight=(double4)(matrix_w[wi],matrix_w[wi+1],matrix_w[wi+2],matrix_w[wi+3]);
m=(double4)(matrix_m[wi],matrix_m[wi+1],matrix_m[wi+2],matrix_m[wi+3]);
v=(double4)(matrix_v[wi],matrix_v[wi+1],matrix_v[wi+2],matrix_v[wi+3]);
break;
}
梯度向量是通過将目前神經元的梯度乘以輸入資料向量而獲得的。
double4 g=matrix_g[i]*inp;
接下來,計算梯度和平方梯度的指數平均值。
double4 mt=b1*m+(1-b1)*g;
double4 vt=b2*v+(1-b2)*pow(g,2)+0.00000001;
計算參數變化增量。
double4 delta=l*mt/sqrt(vt);
請注意,我們尚未調整核心中的接收動量。 在此有意省略了此步驟。 因為 ß1和 ß2 對于所有神經元和 t 都是相同的,其在這裡是 神經元參數更新的疊代次數,對于所有神經元也相同,然後對于所有神經元校正因子也将相同。 這就是為什麼我們不會重新計算每個神經元的因子,而是在主程式代碼中對其進行一次性計算,并将其傳遞給核心,以便依據該值調整後續的學習系數的原因。
在增量計算完成之後,我們隻需要調整權重系數,并更新緩沖區中已計算的動量即可。 然後退出核心。
switch(inputs-j*4)
{
case 2:
matrix_w[wi+2]+=delta.s2;
matrix_m[wi+2]=mt.s2;
matrix_v[wi+2]=vt.s2;
case 1:
matrix_w[wi+1]+=delta.s1;
matrix_m[wi+1]=mt.s1;
matrix_v[wi+1]=vt.s1;
case 0:
matrix_w[wi]+=delta.s0;
matrix_m[wi]=mt.s0;
matrix_v[wi]=vt.s0;
break;
default:
matrix_w[wi]+=delta.s0;
matrix_m[wi]=mt.s0;
matrix_v[wi]=vt.s0;
matrix_w[wi+1]+=delta.s1;
matrix_m[wi+1]=mt.s1;
matrix_v[wi+1]=vt.s1;
matrix_w[wi+2]+=delta.s2;
matrix_m[wi+2]=mt.s2;
matrix_v[wi+2]=vt.s2;
matrix_w[wi+3]+=delta.s3;
matrix_m[wi+3]=mt.s3;
matrix_v[wi+3]=vt.s3;
break;
}
};
此代碼還有另一個技巧。 請注意 switch 運算符中 case 情況的相反順序。 此外, break 操作符僅在 case 0 和 default 情況之後使用。 這種方式可避免所有變體重複相同的代碼。
2.2. 修改主程式裡的神經元類代碼
建構核心之後,我們需要對主程式代碼進行修改。 首先,在 “define” 子產品裡添加操控核心的常量。
#define def_k_UpdateWeightsAdam 4
#define def_k_uwa_matrix_w 0
#define def_k_uwa_matrix_g 1
#define def_k_uwa_matrix_i 2
#define def_k_uwa_matrix_m 3
#define def_k_uwa_matrix_v 4
#define def_k_uwa_inputs 5
#define def_k_uwa_l 6
#define def_k_uwa_b1 7
#define def_k_uwa_b2 8
建立訓示訓練方法的枚舉,并在枚舉中添加動量緩沖區。
enum ENUM_OPTIMIZATION
{
SGD,
ADAM
};
//---
enum ENUM_BUFFERS
{
WEIGHTS,
DELTA_WEIGHTS,
OUTPUT,
GRADIENT,
FIRST_MOMENTUM,
SECOND_MOMENTUM
};
然後,在 CNeuronBaseOCL 類主體中,添加緩沖區,用來存儲動量、指數平均常數、訓練疊代計數器,以及儲存訓練方法的變量。
class CNeuronBaseOCL : public CObject
{
protected:
.........
.........
..........
CBufferDouble *FirstMomentum;
CBufferDouble *SecondMomentum;
//---
.........
.........
const double b1;
const double b2;
int t;
//---
.........
.........
ENUM_OPTIMIZATION optimization;
在類構造函數中,設定常量的值,并初始化緩沖區。
CNeuronBaseOCL::CNeuronBaseOCL(void) : alpha(momentum),
activation(TANH),
optimization(SGD),
b1(0.9),
b2(0.999),
t(1)
{
OpenCL=NULL;
Output=new CBufferDouble();
PrevOutput=new CBufferDouble();
Weights=new CBufferDouble();
DeltaWeights=new CBufferDouble();
Gradient=new CBufferDouble();
FirstMomentum=new CBufferDouble();
SecondMomentum=new CBufferDouble();
}
不要忘記在類的析構函數中加入删除緩沖區對象的代碼。
CNeuronBaseOCL::~CNeuronBaseOCL(void)
{
if(CheckPointer(Output)!=POINTER_INVALID)
delete Output;
if(CheckPointer(PrevOutput)!=POINTER_INVALID)
delete PrevOutput;
if(CheckPointer(Weights)!=POINTER_INVALID)
delete Weights;
if(CheckPointer(DeltaWeights)!=POINTER_INVALID)
delete DeltaWeights;
if(CheckPointer(Gradient)!=POINTER_INVALID)
delete Gradient;
if(CheckPointer(FirstMomentum)!=POINTER_INVALID)
delete FirstMomentum;
if(CheckPointer(SecondMomentum)!=POINTER_INVALID)
delete SecondMomentum;
OpenCL=NULL;
}
在類初始化函數的參數中,添加訓練方法,并根據指定的訓練方法來初始化緩沖區。 如果采用随機梯度下降法進行訓練,初始化增量緩沖區,并删除動量緩沖區。 如果采用 Adam 方法,初始化動量緩沖區,并删除增量緩沖區。
bool CNeuronBaseOCL::Init(uint numOutputs,uint myIndex,COpenCLMy *open_cl,uint numNeurons, ENUM_OPTIMIZATION optimization_type)
{
if(CheckPointer(open_cl)==POINTER_INVALID || numNeurons<=0)
return false;
OpenCL=open_cl;
optimization=optimization_type;
//---
....................
....................
....................
....................
//---
if(numOutputs>0)
{
if(CheckPointer(Weights)==POINTER_INVALID)
{
Weights=new CBufferDouble();
if(CheckPointer(Weights)==POINTER_INVALID)
return false;
}
int count=(int)((numNeurons+1)*numOutputs);
if(!Weights.Reserve(count))
return false;
for(int i=0;i<count;i++)
{
double weigh=(MathRand()+1)/32768.0-0.5;
if(weigh==0)
weigh=0.001;
if(!Weights.Add(weigh))
return false;
}
if(!Weights.BufferCreate(OpenCL))
return false;
//---
if(optimization==SGD)
{
if(CheckPointer(DeltaWeights)==POINTER_INVALID)
{
DeltaWeights=new CBufferDouble();
if(CheckPointer(DeltaWeights)==POINTER_INVALID)
return false;
}
if(!DeltaWeights.BufferInit(count,0))
return false;
if(!DeltaWeights.BufferCreate(OpenCL))
return false;
if(CheckPointer(FirstMomentum)==POINTER_INVALID)
delete FirstMomentum;
if(CheckPointer(SecondMomentum)==POINTER_INVALID)
delete SecondMomentum;
}
else
{
if(CheckPointer(DeltaWeights)==POINTER_INVALID)
delete DeltaWeights;
//---
if(CheckPointer(FirstMomentum)==POINTER_INVALID)
{
FirstMomentum=new CBufferDouble();
if(CheckPointer(FirstMomentum)==POINTER_INVALID)
return false;
}
if(!FirstMomentum.BufferInit(count,0))
return false;
if(!FirstMomentum.BufferCreate(OpenCL))
return false;
//---
if(CheckPointer(SecondMomentum)==POINTER_INVALID)
{
SecondMomentum=new CBufferDouble();
if(CheckPointer(SecondMomentum)==POINTER_INVALID)
return false;
}
if(!SecondMomentum.BufferInit(count,0))
return false;
if(!SecondMomentum.BufferCreate(OpenCL))
return false;
}
}
else
{
if(CheckPointer(Weights)!=POINTER_INVALID)
delete Weights;
if(CheckPointer(DeltaWeights)!=POINTER_INVALID)
delete DeltaWeights;
}
//---
return true;
}
另外,修改權重更新方法 updateInputWeights。 首先,根據訓練方法建立分支算法。
bool CNeuronBaseOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
{
if(CheckPointer(OpenCL)==POINTER_INVALID || CheckPointer(NeuronOCL)==POINTER_INVALID)
return false;
uint global_work_offset[2]={0,0};
uint global_work_size[2];
global_work_size[0]=Neurons();
global_work_size[1]=NeuronOCL.Neurons();
if(optimization==SGD)
{
對于随機梯度下降法,按原樣調用整個代碼。
OpenCL.SetArgumentBuffer(def_k_UpdateWeightsMomentum,def_k_uwm_matrix_w,NeuronOCL.getWeightsIndex());
OpenCL.SetArgumentBuffer(def_k_UpdateWeightsMomentum,def_k_uwm_matrix_g,getGradientIndex());
OpenCL.SetArgumentBuffer(def_k_UpdateWeightsMomentum,def_k_uwm_matrix_i,NeuronOCL.getOutputIndex());
OpenCL.SetArgumentBuffer(def_k_UpdateWeightsMomentum,def_k_uwm_matrix_dw,NeuronOCL.getDeltaWeightsIndex());
OpenCL.SetArgument(def_k_UpdateWeightsMomentum,def_k_uwm_inputs,NeuronOCL.Neurons());
OpenCL.SetArgument(def_k_UpdateWeightsMomentum,def_k_uwm_learning_rates,eta);
OpenCL.SetArgument(def_k_UpdateWeightsMomentum,def_k_uwm_momentum,alpha);
ResetLastError();
if(!OpenCL.Execute(def_k_UpdateWeightsMomentum,2,global_work_offset,global_work_size))
{
printf("Error of execution kernel UpdateWeightsMomentum: %d",GetLastError());
return false;
}
}
在 Adam 方法分支中,為相應的核心設定資料交換緩沖區。
else
{
if(!OpenCL.SetArgumentBuffer(def_k_UpdateWeightsAdam,def_k_uwa_matrix_w,NeuronOCL.getWeightsIndex()))
return false;
if(!OpenCL.SetArgumentBuffer(def_k_UpdateWeightsAdam,def_k_uwa_matrix_g,getGradientIndex()))
return false;
if(!OpenCL.SetArgumentBuffer(def_k_UpdateWeightsAdam,def_k_uwa_matrix_i,NeuronOCL.getOutputIndex()))
return false;
if(!OpenCL.SetArgumentBuffer(def_k_UpdateWeightsAdam,def_k_uwa_matrix_m,NeuronOCL.getFirstMomentumIndex()))
return false;
if(!OpenCL.SetArgumentBuffer(def_k_UpdateWeightsAdam,def_k_uwa_matrix_v,NeuronOCL.getSecondMomentumIndex()))
return false;
然後調整目前訓練疊代的學習率。
double lt=eta*sqrt(1-pow(b2,t))/(1-pow(b1,t));
設定訓練超參數。
if(!OpenCL.SetArgument(def_k_UpdateWeightsAdam,def_k_uwa_inputs,NeuronOCL.Neurons()))
return false;
if(!OpenCL.SetArgument(def_k_UpdateWeightsAdam,def_k_uwa_l,lt))
return false;
if(!OpenCL.SetArgument(def_k_UpdateWeightsAdam,def_k_uwa_b1,b1))
return false;
if(!OpenCL.SetArgument(def_k_UpdateWeightsAdam,def_k_uwa_b2,b2))
return false;
由于我們在核心中采用矢量值進行計算,是以将第二維的線程數減少了四倍。
uint rest=global_work_size[1]%4;
global_work_size[1]=(global_work_size[1]-rest)/4 + (rest>0 ? 1 : 0);
準備工作完成後,調用核心,并增加訓練疊代計數器。
ResetLastError();
if(!OpenCL.Execute(def_k_UpdateWeightsAdam,2,global_work_offset,global_work_size))
{
printf("Error of execution kernel UpdateWeightsAdam: %d",GetLastError());
return false;
}
t++;
}
分支後,無論采用哪種訓練方法,均應讀取重新計算的權重。 正如我在上一篇文章中解釋的那樣,還必須為隐藏層讀取緩沖區,因為此操作不僅讀取資料,且還會開啟核心執行。
//---
return NeuronOCL.Weights.BufferRead();
}
除了訓練方法計算的算法之外,還需要調整存儲和加載有關先前神經元訓練結果資訊的方法。 在 Save 方法中,實作訓練方法的儲存,并添加訓練疊代計數器。
bool CNeuronBaseOCL::Save(const int file_handle)
{
if(file_handle==INVALID_HANDLE)
return false;
if(FileWriteInteger(file_handle,Type())<INT_VALUE)
return false;
//---
if(FileWriteInteger(file_handle,(int)activation,INT_VALUE)<INT_VALUE)
return false;
if(FileWriteInteger(file_handle,(int)optimization,INT_VALUE)<INT_VALUE)
return false;
if(FileWriteInteger(file_handle,(int)t,INT_VALUE)<INT_VALUE)
return false;
儲存的緩沖區方法兩種訓練方法通用,并未改變。
if(CheckPointer(Output)==POINTER_INVALID || !Output.BufferRead() || !Output.Save(file_handle))
return false;
if(CheckPointer(PrevOutput)==POINTER_INVALID || !PrevOutput.BufferRead() || !PrevOutput.Save(file_handle))
return false;
if(CheckPointer(Gradient)==POINTER_INVALID || !Gradient.BufferRead() || !Gradient.Save(file_handle))
return false;
//---
if(CheckPointer(Weights)==POINTER_INVALID)
{
FileWriteInteger(file_handle,0);
return true;
}
else
FileWriteInteger(file_handle,1);
//---
if(CheckPointer(Weights)==POINTER_INVALID || !Weights.BufferRead() || !Weights.Save(file_handle))
return false;
之後,分别為每種訓練方法建立分支算法,同時儲存特定的緩沖區。
if(optimization==SGD)
{
if(CheckPointer(DeltaWeights)==POINTER_INVALID || !DeltaWeights.BufferRead() || !DeltaWeights.Save(file_handle))
return false;
}
else
{
if(CheckPointer(FirstMomentum)==POINTER_INVALID || !FirstMomentum.BufferRead() || !FirstMomentum.Save(file_handle))
return false;
if(CheckPointer(SecondMomentum)==POINTER_INVALID || !SecondMomentum.BufferRead() || !SecondMomentum.Save(file_handle))
return false;
}
//---
return true;
}
在 Load 方法中以相同的順序進行類似的修改。
附件中提供了所有方法和函數的完整代碼。
2.3. 修改不使用 OpenCL 的類代碼
為了令所有類都保持相同的操作條件,在類中進行類似修改,以純 MQL5 進行操作,不使用 OpenCL。
首先,在 CConnection 類裡添加存儲動量資料的變量,并在類構造函數中設定初始值。
class CConnection : public CObject
{
public:
double weight;
double deltaWeight;
double mt;
double vt;
CConnection(double w) { weight=w; deltaWeight=0; mt=0; vt=0; }
還必須将新變量的處理添加到儲存和加載連接配接資料的方法當中。
bool CConnection::Save(int file_handle)
{
...........
...........
...........
if(FileWriteDouble(file_handle,mt)<=0)
return false;
if(FileWriteDouble(file_handle,vt)<=0)
return false;
//---
return true;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool CConnection::Load(int file_handle)
{
............
............
............
mt=FileReadDouble(file_handle);
vt=FileReadDouble(file_handle);
//---
return true;
}
下一步,在 CNeuronBase 神經元類裡添加存儲優化方法和權重更新疊代計數器的變量。
class CNeuronBase : public CObject
{
protected:
.........
.........
.........
ENUM_OPTIMIZATION optimization;
const double b1;
const double b2;
int t;
然後,神經元初始化方法也需要修改。 在方法的參數中添加一個訓示優化方法的變量,并将其儲存在上面定義的變量之中。
bool CNeuronBase::Init(uint numOutputs,uint myIndex, ENUM_OPTIMIZATION optimization_type)
{
optimization=optimization_type;
之後,我們根據優化方法建立算法分支,并将其添加到 updateInputWeights 方法當中。 在周遊連接配接之前,重新計算調整後的學習率,并在循環中建立兩個計算權重的分支。
bool CNeuron::updateInputWeights(CLayer *&prevLayer)
{
if(CheckPointer(prevLayer)==POINTER_INVALID)
return false;
//---
double lt=eta*sqrt(1-pow(b2,t))/(1-pow(b1,t));
int total=prevLayer.Total();
for(int n=0; n<total && !IsStopped(); n++)
{
CNeuron *neuron= prevLayer.At(n);
CConnection *con=neuron.Connections.At(m_myIndex);
if(CheckPointer(con)==POINTER_INVALID)
continue;
if(optimization==SGD)
con.weight+=con.deltaWeight=(gradient!=0 ? eta*neuron.getOutputVal()*gradient : 0)+(con.deltaWeight!=0 ? alpha*con.deltaWeight : 0);
else
{
con.mt=b1*con.mt+(1-b1)*gradient;
con.vt=b2*con.vt+(1-b2)*pow(gradient,2)+0.00000001;
con.weight+=con.deltaWeight=lt*con.mt/sqrt(con.vt);
t++;
}
}
//---
return true;
}
在儲存和加載方法裡添加新變量的處理邏輯。
下面的附件中提供了所有方法的完整代碼。
2.4. 修改主程式中的神經網絡類代碼
除在神經元類裡進行修改之外,還需要在我們的代碼中修改其他對象。 首先,我們需要将有關訓練方法的資訊從主程式傳遞到神經元。 來自主程式的資料經由 CLayerDescription 類傳遞給神經網絡類。 應向該類裡添加相應的方法,進而傳遞有關訓練方法的資訊。
class CLayerDescription : public CObject
{
public:
CLayerDescription(void);
~CLayerDescription(void) {};
//---
int type;
int count;
int window;
int step;
ENUM_ACTIVATION activation;
ENUM_OPTIMIZATION optimization;
};
現在,針對 CNet 神經網絡類的構造函數做最後的補充。 在初始化網絡神經元之處添加一個優化方法的訓示,OpenCL 核心的使用次數遞增,并聲明一個新的優化核心 - Adam。 以下是修改後的構造函數代碼,其中以高亮标記的是修改部分。
CNet::CNet(CArrayObj *Description)
{
if(CheckPointer(Description)==POINTER_INVALID)
return;
//---
int total=Description.Total();
if(total<=0)
return;
//---
layers=new CArrayLayer();
if(CheckPointer(layers)==POINTER_INVALID)
return;
//---
CLayer *temp;
CLayerDescription *desc=NULL, *next=NULL, *prev=NULL;
CNeuronBase *neuron=NULL;
CNeuronProof *neuron_p=NULL;
int output_count=0;
int temp_count=0;
//---
next=Description.At(1);
if(next.type==defNeuron || next.type==defNeuronBaseOCL)
{
opencl=new COpenCLMy();
if(CheckPointer(opencl)!=POINTER_INVALID && !opencl.Initialize(cl_program,true))
delete opencl;
}
else
{
if(CheckPointer(opencl)!=POINTER_INVALID)
delete opencl;
}
//---
for(int i=0; i<total; i++)
{
prev=desc;
desc=Description.At(i);
if((i+1)<total)
{
next=Description.At(i+1);
if(CheckPointer(next)==POINTER_INVALID)
return;
}
else
next=NULL;
int outputs=(next==NULL || (next.type!=defNeuron && next.type!=defNeuronBaseOCL) ? 0 : next.count);
temp=new CLayer(outputs);
int neurons=(desc.count+(desc.type==defNeuron || desc.type==defNeuronBaseOCL ? 1 : 0));
if(CheckPointer(opencl)!=POINTER_INVALID)
{
CNeuronBaseOCL *neuron_ocl=NULL;
switch(desc.type)
{
case defNeuron:
case defNeuronBaseOCL:
neuron_ocl=new CNeuronBaseOCL();
if(CheckPointer(neuron_ocl)==POINTER_INVALID)
{
delete temp;
return;
}
if(!neuron_ocl.Init(outputs,0,opencl,desc.count,desc.optimization))
{
delete temp;
return;
}
neuron_ocl.SetActivationFunction(desc.activation);
if(!temp.Add(neuron_ocl))
{
delete neuron_ocl;
delete temp;
return;
}
neuron_ocl=NULL;
break;
default:
return;
break;
}
}
else
for(int n=0; n<neurons; n++)
{
switch(desc.type)
{
case defNeuron:
neuron=new CNeuron();
if(CheckPointer(neuron)==POINTER_INVALID)
{
delete temp;
delete layers;
return;
}
neuron.Init(outputs,n,desc.optimization);
neuron.SetActivationFunction(desc.activation);
break;
case defNeuronConv:
neuron_p=new CNeuronConv();
if(CheckPointer(neuron_p)==POINTER_INVALID)
{
delete temp;
delete layers;
return;
}
if(CheckPointer(prev)!=POINTER_INVALID)
{
if(prev.type==defNeuron)
{
temp_count=(int)((prev.count-desc.window)%desc.step);
output_count=(int)((prev.count-desc.window-temp_count)/desc.step+(temp_count==0 ? 1 : 2));
}
else
if(n==0)
{
temp_count=(int)((output_count-desc.window)%desc.step);
output_count=(int)((output_count-desc.window-temp_count)/desc.step+(temp_count==0 ? 1 : 2));
}
}
if(neuron_p.Init(outputs,n,desc.window,desc.step,output_count,desc.optimization))
neuron=neuron_p;
break;
case defNeuronProof:
neuron_p=new CNeuronProof();
if(CheckPointer(neuron_p)==POINTER_INVALID)
{
delete temp;
delete layers;
return;
}
if(CheckPointer(prev)!=POINTER_INVALID)
{
if(prev.type==defNeuron)
{
temp_count=(int)((prev.count-desc.window)%desc.step);
output_count=(int)((prev.count-desc.window-temp_count)/desc.step+(temp_count==0 ? 1 : 2));
}
else
if(n==0)
{
temp_count=(int)((output_count-desc.window)%desc.step);
output_count=(int)((output_count-desc.window-temp_count)/desc.step+(temp_count==0 ? 1 : 2));
}
}
if(neuron_p.Init(outputs,n,desc.window,desc.step,output_count,desc.optimization))
neuron=neuron_p;
break;
case defNeuronLSTM:
neuron_p=new CNeuronLSTM();
if(CheckPointer(neuron_p)==POINTER_INVALID)
{
delete temp;
delete layers;
return;
}
output_count=(next!=NULL ? next.window : desc.step);
if(neuron_p.Init(outputs,n,desc.window,1,output_count,desc.optimization))
neuron=neuron_p;
break;
}
if(!temp.Add(neuron))
{
delete temp;
delete layers;
return;
}
neuron=NULL;
}
if(!layers.Add(temp))
{
delete temp;
delete layers;
return;
}
}
//---
if(CheckPointer(opencl)==POINTER_INVALID)
return;
//--- create kernels
opencl.SetKernelsCount(5);
opencl.KernelCreate(def_k_FeedForward,"FeedForward");
opencl.KernelCreate(def_k_CaclOutputGradient,"CaclOutputGradient");
opencl.KernelCreate(def_k_CaclHiddenGradient,"CaclHiddenGradient");
opencl.KernelCreate(def_k_UpdateWeightsMomentum,"UpdateWeightsMomentum");
opencl.KernelCreate(def_k_UpdateWeightsAdam,"UpdateWeightsAdam");
//---
return;
}
附件中提供了所有類及其方法的完整代碼。
3. 測試
經由 Adam 方法進行的優化測試,其所依據條件與早期測試中使用的相同:品種 EURUSD,時間幀 H1,20 根連續燭條的資料饋入網絡,并根據最近兩年的曆史進行訓練。 已為測試建立了 Fractal_OCL_Adam 智能交易系統。 該智能交易系統的建立,則是基于 Fractal_OCL EA,在主程式的 OnInit 函數中描述神經網絡時,指定 Adam 優化方法。
desc.count=(int)HistoryBars*12;
desc.type=defNeuron;
desc.optimization=ADAM;
層和神經元的數量沒有變化。
智能交易系統初始化時的随機權重為 -1 到 1,不包括零值。 在測試期間,在第二個訓練疊代之後,神經網絡誤差穩定在 30% 左右。 您可能還記得,采用随機梯度下降法學習時,在第 5 個訓練疊代之後,誤差穩定在 42% 左右。
缺失的分形圖形展示出數值遞增貫穿于整個訓練過程。 不過,經過 12 個訓練疊代之後,數值遞增率逐漸降低。 第 14 個疊代後,該值等于 72.5%。 當訓練相似的神經網絡時采用随機梯度下降方法,則在不同學習率的情況下,經過 10 個疊代後分形缺失率為 97-100%。
而且,最重要的名額可能是正确定義的分形的百分比。 第五次學習疊代後,該值達到 48.6%,然後逐漸下降到 41.1%。 采用随機梯度下降法時,在 90 個疊代之後該值不超過 10%。
結束語
本文研究優化神經網絡參數的自适應方法。 我們已将 Adam 優化方法添加到先前建立的神經網絡模型當中。 在測試過程中,采用 Adam 方法訓練神經網絡。 當采用随機梯度下降法訓練相似的神經網絡時,結果超過了之前得到的結果。
完成的工作表明我們正朝着目标前進。
參考
- 神經網絡變得輕松
- 神經網絡變得輕松(第二部分):網絡訓練和測試
- 神經網絡變得輕松(第三部分):卷積網絡
- 神經網絡變得輕松(第四部分):循環網絡
- 神經網絡變得輕松(第五部分):OpenCL 中的多線程計算
- 神經網絡變得輕松(第六部分):神經網絡學習率實驗
- Adam: 随機優化方法
本文中用到的程式
# | 名稱 | 類型 | 說明 |
1 | Fractal_OCL_Adam.mq5 | 智能交易系統 | 一款采用 OpenCL 和 Adam 訓練方法的分類神經網絡 EA(輸出層中有 3 個神經元) |
2 | NeuroNet.mqh | 類庫 | 用于建立神經網絡的類庫 |
3 | NeuroNet.cl | 代碼庫 | OpenCL 程式代碼庫 |