神经网络变得轻松(第三部分):卷积网络·进阶篇
📘

神经网络变得轻松(第三部分):卷积网络·进阶篇

第 2/3 篇

「神经元基类的骨架与虚函数占位」

在 MT5 里用 C++ 风格搭神经网络,第一步往往是先定义一个神经元基类 CNeuronBase,把后续各层都要用的状态和接口先框死。下面这段声明里能看到它持有的核心字段:alpha 是学习率系数,outputVal 存当前输出,m_myIndex 记录自身在层内的序号,gradient 留给反向传播算梯度,Connections 是指向连接数组的指针。 基类里大量使用 virtual 函数并返回 false 或固定值,本质是「占位」。比如 feedForward 直接 return false,activationFunction 恒返回 1.0,说明基类不实现具体计算,只给派生类留重写入口。你在 EA 里若直接调用基类这些方法,网络是不会真正前向传播的。 对外暴露的接口已经比较完整:setOutputVal / getOutputVal 管输入输出值,setGradient / getGradient 管梯度,Init 接收输出数和自身索引做初始化,Save / Load 负责把连接持久化到文件句柄。注意 Load 这里直接转调 Connections.Load,说明权重恢复依赖连接对象自己实现。 打开 MT5 的 MetaEditor,新建一个 mqh 把这段结构抄进去,先别急着写算法——_confirm 编译能过、虚表能建,再往下填各层的具体激活函数。外汇与贵金属杠杆高,拿这类自研模型跑实盘前务必先用历史数据做离线验证。

MQL5 / C++
  class="type">class="kw">double                alpha;
  class="type">class="kw">double                outputVal;
  class="type">uint                  m_myIndex;
  class="type">class="kw">double                gradient;
  CArrayCon            *Connections;
class=class="str">"cmt">//--- 
  class="kw">virtual class="type">bool         feedForward(CLayer *prevLayer)                  { class="kw">return class="kw">false;    }
  class="kw">virtual class="type">bool         calcHiddenGradients( CLayer *&nextLayer)        { class="kw">return class="kw">false;    }
  class="kw">virtual class="type">bool         updateInputWeights(CLayer *&prevLayer)          { class="kw">return class="kw">false;    }
  class="kw">virtual class="type">class="kw">double       activationFunction(class="type">class="kw">double x)                    { class="kw">return class="num">1.0;      }
  class="kw">virtual class="type">class="kw">double       activationFunctionDerivative(class="type">class="kw">double x)          { class="kw">return class="num">1.0;      }
  class="kw">virtual CLayer       *getOutputLayer(class="type">void)                           { class="kw">return NULL;     }
class="kw">public:
                    CNeuronBase(class="type">void);
                   ~CNeuronBase(class="type">void);
  class="kw">virtual class="type">bool         Init(class="type">uint numOutputs, class="type">uint myIndex);
class=class="str">"cmt">//---
  class="kw">virtual class="type">void         setOutputVal(class="type">class="kw">double val)                       { outputVal=val;   }
  class="kw">virtual class="type">class="kw">double       getOutputVal()                                 { class="kw">return outputVal; }
  class="kw">virtual class="type">void         setGradient(class="type">class="kw">double val)                        { gradient=val;    }
  class="kw">virtual class="type">class="kw">double       getGradient()                                 { class="kw">return gradient;  }
class=class="str">"cmt">//---
  class="kw">virtual class="type">bool         feedForward(CObject *&SourceObject);
  class="kw">virtual class="type">bool         calcHiddenGradients( CObject *&TargetObject);
  class="kw">virtual class="type">bool         updateInputWeights(CObject *&SourceObject);
class=class="str">"cmt">//---
  class="kw">virtual class="type">bool         Save( class="type">int class="kw">const file_handle);
  class="kw">virtual class="type">bool         Load( class="type">int class="kw">const file_handle)                   { class="kw">return(Connections.Load(file_handle)); }
class=class="str">"cmt">//---

基类怎么把前向和梯度甩给不同对象

CNeuronBase 里 feedForward 和 calcHiddenGradients 都接 CObject*,靠 Type() 分流:源或目标若是 defLayer 就直接调层接口,若是 defNeuronConv / defNeuronProof 则转去取输出层再喂。这样同一入口能兼容层与神经元两种上游,写 EA 时不用在外层判断类型。 看 feedForward 的 switch:defLayer 分支把 SourceObject 强转 CLayer* 后调重载;神经元类分支则取 temp_n.getOutputLayer() 再递归。CheckPointer 返回 POINTER_INVALID 时直接 false 退出,避免空指针崩 MT5 回测。 calcHiddenGradients 多一层自身 Type() 判断:当自己是普通 defNeuron,就调目标的 calcInputGradients 并传 m_myIndex;否则把自己当临时节点,取输出层传给目标算输入梯度。这种双 switch 结构在卷积类接证明类神经元时才会走 default 分支。 CNeuronProof 公开继承 CNeuronBase,保护成员挂了 OutputLayer、iWindow、iStep,说明证明神经元要记住输出层和滑窗参数。它重写了 feedForward(CLayer*) 与 calcHiddenGradients(CLayer&),构造析构留空壳,具体窗口怎么卷要靠后续实现。

MQL5 / C++
class="kw">virtual class="type">int        Type(class="type">void)                class="kw">const                {  class="kw">return defNeuronBase;                 }
};
class="type">bool CNeuronBase::feedForward(CObject *&SourceObject)
  {
   class="type">bool result=class="kw">false;
class=class="str">"cmt">//---
   if(CheckPointer(SourceObject)==POINTER_INVALID)
      class="kw">return result;
   CLayer *temp_l;
   CNeuronProof *temp_n;
   class="kw">switch(SourceObject.Type())
     {
      case defLayer:
         temp_l=SourceObject;
         result=feedForward(temp_l);
         class="kw">break;
      case defNeuronConv:
      case defNeuronProof:
         temp_n=SourceObject;
         result=feedForward(temp_n.getOutputLayer());
         class="kw">break;
     }
class=class="str">"cmt">//---
   class="kw">return result;
  }
class="type">bool CNeuronBase::calcHiddenGradients(CObject *&TargetObject)
  {
   class="type">bool result=class="kw">false;
class=class="str">"cmt">//---
   if(CheckPointer(TargetObject)==POINTER_INVALID)
      class="kw">return result;
class=class="str">"cmt">//---
   CLayer *temp_l;
   CNeuronProof *temp_n;
   class="kw">switch(TargetObject.Type())
     {
      case defLayer:
         temp_l=TargetObject;
         result=calcHiddenGradients(temp_l);
         class="kw">break;
      case defNeuronConv:
      case defNeuronProof:
         class="kw">switch(Type())
           {
            case defNeuron:
               temp_n=TargetObject;
               result=temp_n.calcInputGradients(GetPointer(this),m_myIndex);
               class="kw">break;
            class="kw">default:
               temp_n=GetPointer(this);
               temp_l=temp_n.getOutputLayer();
               temp_n=TargetObject;
               result=temp_n.calcInputGradients(temp_l);
               class="kw">break;
           }
         class="kw">break;
     }
class=class="str">"cmt">//---
   class="kw">return result;
  }
class CNeuronProof : class="kw">public CNeuronBase
  {
class="kw">protected:
   CLayer         *OutputLayer;
   class="type">int            iWindow;
   class="type">int            iStep;

   class="kw">virtual class="type">bool      feedForward(CLayer *prevLayer);
   class="kw">virtual class="type">bool      calcHiddenGradients( CLayer *&nextLayer);

class="kw">public:
                     CNeuronProof(class="type">void){};
                    ~CNeuronProof(class="type">void);

◍ 滑动窗口神经元的实现细节

CNeuronProof 这个类在 MQL5 神经网络框架里干一件事:把前一层的时间序列按滑动窗口做均值池化,再交给 OutputLayer 存结果。它的 Init 接收 numOutputs、myIndex、window、step、output_count 几个参数,window 和 step 直接决定特征抽取的疏密程度。 看 feedForward 的具体循环:total 被算成 prevLayer.Total()-iWindow+1,也就是能滑多少次;外层循环 i 从 0 以 iStep 为步长递增,内层把窗口内 iWindow 个神经元的输出值累加后除以 iWindow,写进 OutputLayer 的第 i/iStep 个节点。若 prevLayer 总长是 100、window=10、step=5,那 OutputLayer 大约会落 19 个均值特征。 [CODE] virtual bool Init(uint numOutputs,uint myIndex,int window, int step, int output_count); //--- virtual CLayer *getOutputLayer(void) { return OutputLayer; } virtual bool calcInputGradients( CLayer *prevLayer) ; virtual bool calcInputGradients( CNeuronBase *prevNeuron, uint index) ; //--- methods for working with files virtual bool Save( int const file_handle) { return(CNeuronBase::Save(file_handle) && OutputLayer.Save(file_handle)); } virtual bool Load( int const file_handle) { return(CNeuronBase::Load(file_handle) && OutputLayer.Load(file_handle)); } virtual int Type(void) const { return defNeuronProof; } }; bool CNeuronProof::feedForward(CLayer *prevLayer) { if(CheckPointer(prevLayer)==POINTER_INVALID) return false; int total=prevLayer.Total()-iWindow+1; CNeuron *temp; for(int i=0;(i<=total && result);i+=iStep) { double sum=0; for(int j=0;j<iWindow;j++) { temp=prevLayer.At(i+j); if(CheckPointer(temp)==POINTER_INVALID) continue; sum+=temp.getOutputVal(); } temp=OutputLayer.At(i/iStep); if(CheckPointer(temp)==POINTER_INVALID) return false; temp.setOutputVal(sum/iWindow); } //--- return true; } bool CNeuronProof::calcHiddenGradients( CLayer *&nextLayer) {

if(CheckPointer(nextLayer)==POINTER_INVALIDCheckPointer(OutputLayer)==POINTER_INVALIDOutputLayer.Total()<=0)

return false; gradient=0; int total=OutputLayer.Total(); CNeuron *temp; for(int i=0;i<total;i++) { temp=OutputLayer.At(i); if(CheckPointer(temp)==POINTER_INVALID) return false; temp.setGradient(temp.sumDOW(nextLayer)); } //--- return true; } bool CNeuronProof::calcInputGradients(CLayer *prevLayer) { [/CODE] 上面这段里,Init 是虚函数,说明子类可重写;getOutputLayer 直接返回内部 OutputLayer 指针,方便外部读取池化结果。Save 和 Load 把基类与输出层一起序列化,意味着你训练完的滑窗神经元可以整体落盘,下次 MT5 加载不用重训。 calcHiddenGradients 里先把 gradient 清 0,再遍历 OutputLayer 的每个节点,用 temp.sumDOW(nextLayer) 把下一层的误差反传回来写进梯度。注意它检查了 OutputLayer.Total()<=0 就直接退出,所以空输出层不会误算。外汇与贵金属行情用这类网络做特征压缩时,仍属高风险,窗口参数过拟合可能让样本外表现倾向恶化。 改 iWindow 和 iStep 是两个最直接的旋钮:window 调大平滑强但滞后明显,step 调大特征稀疏但计算轻。开 MT5 把这两参数接上 EURUSD 的 M5 收盘序列跑一遍,就能看出池化后特征对原序列的跟踪误差大概落在什么区间。

MQL5 / C++
class="kw">virtual class="type">bool Init(class="type">uint numOutputs,class="type">uint myIndex,class="type">int window, class="type">int step, class="type">int output_count);
class=class="str">"cmt">//---
class="kw">virtual CLayer *getOutputLayer(class="type">void) { class="kw">return OutputLayer; }
class="kw">virtual class="type">bool calcInputGradients( CLayer *prevLayer) ;
class="kw">virtual class="type">bool calcInputGradients( CNeuronBase *prevNeuron, class="type">uint index) ;
class=class="str">"cmt">//--- methods for working with files
class="kw">virtual class="type">bool Save( class="type">int class="kw">const file_handle) { class="kw">return(CNeuronBase::Save(file_handle) && OutputLayer.Save(file_handle)); }
class="kw">virtual class="type">bool Load( class="type">int class="kw">const file_handle) { class="kw">return(CNeuronBase::Load(file_handle) && OutputLayer.Load(file_handle)); }
class="kw">virtual class="type">int Type(class="type">void) class="kw">const { class="kw">return defNeuronProof; }
};
class="type">bool CNeuronProof::feedForward(CLayer *prevLayer)
  {
   if(CheckPointer(prevLayer)==POINTER_INVALID)
     class="kw">return class="kw">false;
   class="type">int total=prevLayer.Total()-iWindow+class="num">1;
   CNeuron *temp;
   for(class="type">int i=class="num">0;(i<=total && result);i+=iStep)
     {
      class="type">class="kw">double sum=class="num">0;
      for(class="type">int j=class="num">0;j<iWindow;j++)
        {
         temp=prevLayer.At(i+j);
         if(CheckPointer(temp)==POINTER_INVALID)
           class="kw">continue;
         sum+=temp.getOutputVal();
        }
      temp=OutputLayer.At(i/iStep);
      if(CheckPointer(temp)==POINTER_INVALID)
        class="kw">return class="kw">false;
      temp.setOutputVal(sum/iWindow);
     }
class=class="str">"cmt">//---
   class="kw">return true;
  }
class="type">bool CNeuronProof::calcHiddenGradients( CLayer *&nextLayer)
  {
   if(CheckPointer(nextLayer)==POINTER_INVALID || CheckPointer(OutputLayer)==POINTER_INVALID || OutputLayer.Total()<=class="num">0)
     class="kw">return class="kw">false;
   gradient=class="num">0;
   class="type">int total=OutputLayer.Total();
   CNeuron *temp;
   for(class="type">int i=class="num">0;i<total;i++)
     {
      temp=OutputLayer.At(i);
      if(CheckPointer(temp)==POINTER_INVALID)
        class="kw">return class="kw">false;
      temp.setGradient(temp.sumDOW(nextLayer));
     }
class=class="str">"cmt">//---
   class="kw">return true;
  }
class="type">bool CNeuronProof::calcInputGradients(CLayer *prevLayer)
  {

「卷积层反向传播里的指针与窗口切片」

在 MT5 自建神经网络里,卷积神经元反向算梯度前必须先确认前层与输出层指针有效。下面这段判断若 prevLayer 或 OutputLayer 任一为 POINTER_INVALID 就直接 return false,避免后续空指针访问把 EA 跑崩。 如果前层首个神经元类型不是 defNeuron,说明当前挂在 proof 层上,需要借 m_myIndex 取出对应 CNeuronProof 并替换 prevLayer 为其输出层,每一步都重新 CheckPointer。这一层套一层的兜底,是卷积结构复用全连接代码时的典型坑。 真正算梯度时,total 取前层神经元数,对每个 prevNeuron 用 iWindow 与 iStep 切出输出层索引区间 [start, stop)。prev_gradient 累加 outputNeuron.getGradient()/iWindow,再 setGradient 回写。注意 start 做了 (start-start%iStep)/iStep 的下取整对齐,stop 同理加 1,区间边界容易写错。 CNeuronConv 类里 param 默认 0.01,即 PReLU 的负半轴斜率;Type() 返回 defNeuronConv 供上层判别。激活与导数、updateInputWeights 都是虚函数,意味着你改一版卷积核更新逻辑只需重写这一类,不用动基类循环。 把下面代码贴进 MT5 的 .mqh 里单步跟踪,重点看 prev_gradient 在 iWindow=3、iStep=1 时是否如预期被 3 除摊薄,外汇与贵金属模型训练波动大,过拟合概率偏高,验证请先用历史样本小批量跑。

MQL5 / C++
if(CheckPointer(prevLayer)==POINTER_INVALID || CheckPointer(OutputLayer)==POINTER_INVALID)
      class="kw">return class="kw">false;
   if(prevLayer.At(class="num">0).Type()!=defNeuron)
      {
       CNeuronProof *temp=prevLayer.At(m_myIndex);
       if(CheckPointer(temp)==POINTER_INVALID)
          class="kw">return class="kw">false;
       prevLayer=temp.getOutputLayer();
       if(CheckPointer(prevLayer)==POINTER_INVALID)
          class="kw">return class="kw">false;
      }
   CNeuronBase *prevNeuron, *outputNeuron;
   class="type">int total=prevLayer.Total();
   for(class="type">int i=class="num">0;i<total;i++)
      {
       prevNeuron=prevLayer.At(i);
       if(CheckPointer(prevNeuron)==POINTER_INVALID)
          class="kw">continue;
       class="type">class="kw">double prev_gradient=class="num">0;
       class="type">int start=i-iWindow+iStep;
       start=(start-start%iStep)/iStep;
       class="type">class="kw">double stop=(i-i%iStep)/iStep+class="num">1;
       for(class="type">int out=(class="type">int)fmax(class="num">0,start);out<(class="type">int)fmin(OutputLayer.Total(),stop);out++)
         {
          outputNeuron=OutputLayer.At(out);
          if(CheckPointer(outputNeuron)==POINTER_INVALID)
             class="kw">continue;
          prev_gradient+=outputNeuron.getGradient()/iWindow;
         }
       prevNeuron.setGradient(prev_gradient);
      }
class=class="str">"cmt">//---
   class="kw">return true;
   }
class CNeuronConv   :  class="kw">public CNeuronProof
   {
class="kw">protected:
    class="type">class="kw">double            param;   class=class="str">"cmt">//PReLU param
    class="kw">virtual class="type">bool      feedForward(CLayer *prevLayer);
    class="kw">virtual class="type">bool      calcHiddenGradients(CLayer *&nextLayer);
    class="kw">virtual class="type">class="kw">double    activationFunction(class="type">class="kw">double x);
    class="kw">virtual class="type">bool      updateInputWeights(CLayer *&prevLayer);
class="kw">public:
                     CNeuronConv() :  param(class="num">0.01) { };
                    ~CNeuronConv(class="type">void)            { };
class=class="str">"cmt">//---
    class="kw">virtual class="type">bool      calcInputGradients(CLayer *prevLayer) ;
    class="kw">virtual class="type">bool      calcInputGradients(CNeuronBase *prevNeuron, class="type">uint index) ;
    class="kw">virtual class="type">class="kw">double    activationFunctionDerivative(class="type">class="kw">double x);
    class="kw">virtual class="type">int       Type(class="type">void) class="kw">const   { class="kw">return defNeuronConv; }
    };
class="type">bool CNeuronConv::updateInputWeights(CLayer *&prevLayer)
   {

卷积层权重回传与结构描述类

卷积网络训练时,权重更新依赖前层输出与输出层梯度的累积乘积。下面这段回传逻辑先校验指针有效性,再按窗口遍历连接,累计 delta 后套用学习率 eta 与动量 alpha 修正权重。 若 prevLayer 或 OutputLayer 指针无效直接返回 false,避免空指针崩溃。循环上限为 iWindow,且每次判断 IsStopped() 以便 EA 被终止时及时跳出;单条连接 con 取自 Connections.At(n),指针异常则 continue 跳过。 内层对 OutputLayer.Total() 个神经元求和:prev 取 prevLayer 中第 n*iStep+i 个,out 逆序取输出层神经元,二者任一指针无效就跳过,否则 delta 累加 prev.getOutputVal()*out.getGradient()。最终 con.weight 增加量 = eta*delta(delta 非 0 时)加上 alpha*con.deltaWeight(上一刻增量非 0 时),这是标准带动量的梯度下降。 CLayerDescription 作为层配置载体,继承 CObject,构造时给 type 默认 defNeuron、count=0、window=1、step=1。CNetConvolution 析构直接 delete layers 释放层数组,MT5 里跑自行卷积网时注意析构是否漏掉连接数组,否则可能内存泄漏。外汇与贵金属品种上套这类模型预测,属高风险尝试,样本外表现可能显著衰减。

MQL5 / C++
if(CheckPointer(prevLayer)==POINTER_INVALID || CheckPointer(OutputLayer)==POINTER_INVALID)
      class="kw">return class="kw">false;
   CConnection *con;
   for(class="type">int n=class="num">0; n<iWindow && !IsStopped(); n++)
     {
       con=Connections.At(n);
       if(CheckPointer(con)==POINTER_INVALID)
         class="kw">continue;
       class="type">class="kw">double delta=class="num">0;
       class="type">int total_i=OutputLayer.Total();
       CNeuron *prev, *out;
       for(class="type">int i=class="num">0;i<total_i;i++)
         {
          prev=prevLayer.At(n*iStep+i);
          out=OutputLayer.At(total_i-i-class="num">1);
          if(CheckPointer(prev)==POINTER_INVALID || CheckPointer(out)==POINTER_INVALID)
            class="kw">continue;
          delta+=prev.getOutputVal()*out.getGradient();
          }
       con.weight+=con.deltaWeight=(delta!=class="num">0 ? eta*delta : class="num">0)+(con.deltaWeight!=class="num">0 ? alpha*con.deltaWeight : class="num">0);
     }
class=class="str">"cmt">//---
   class="kw">return true;  
   }
class CLayerDescription    :  class="kw">public CObject
   {
class="kw">public:
                    CLayerDescription(class="type">void);
                   ~CLayerDescription(class="type">void){};
class=class="str">"cmt">//---
    class="type">int             type;
    class="type">int             count;
    class="type">int             window;
    class="type">int             step;
    };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
CLayerDescription::CLayerDescription(class="type">void)  :  type(defNeuron),
                                          count(class="num">0),
                                          window(class="num">1),
                                          step(class="num">1)
   {}
class CNetConvolution
   {
class="kw">public:
                     CNetConvolution(CArrayObj *Description);
                    ~CNetConvolution(class="type">void)                    {  class="kw">delete layers; }

◍ 卷积网络的构造与存取接口

把网络结构从描述对象里落地,核心在 CNetConvolution 构造函数:它遍历 CArrayObj 里的每一层描述,为每层 new 出 CLayer,并按下一层神经元数决定当前层输出连接数。若 Description 指针无效或 Total()<=0,直接 return,不抛错但网络为空,调用前务必确认描述数组已填充。 类里暴露的 feedForward / backProp / getResults 是训练与推理三段式;getRecentAverageError 直接回传 recentAverageError,平滑因子由静态量 recentAverageSmoothingFactor 控制,默认训练时误差曲线会更平缓。 Save / Load 用 common=true 写到终端公共目录,可把 error、forecast、time 一并落盘,方便小布这类 AIGC 工具隔日续训。外汇与贵金属行情下用卷积网做特征提取仍属高风险,过拟合可能导致样本外概率显著劣化。 构造循环里那句 desc.count+(i>0 && desc.type==defNeuron ? 1 : 0) 值得盯:非首层神经元层会多分配 1 个偏置节点,漏看这一步容易在手动改网络宽度时算错总参数量。

MQL5 / C++
  class="type">bool                 feedForward( CArrayDouble *inputVals);
  class="type">void                 backProp( CArrayDouble *targetVals);
  class="type">void                 getResults(CArrayDouble *&resultVals) ;
  class="type">class="kw">double               getRecentAverageError()                  { class="kw">return recentAverageError; }
  class="type">bool                 Save( class="type">class="kw">string file_name, class="type">class="kw">double error, class="type">class="kw">double undefine, class="type">class="kw">double forecast, class="type">class="kw">datetime time, class="type">bool common=true);
  class="type">bool                 Load( class="type">class="kw">string file_name, class="type">class="kw">double &error, class="type">class="kw">double &undefine, class="type">class="kw">double &forecast, class="type">class="kw">datetime &time, class="type">bool common=true);
  class=class="str">"cmt">//---
  class="kw">static class="type">class="kw">double        recentAverageSmoothingFactor;
  class="kw">virtual class="type">int          Type(class="type">void) class="kw">const   {  class="kw">return defNetConv;  }
class="kw">private:
  CArrayLayer          *layers;
  class="type">class="kw">double               recentAverageError;
  };
CNetConvolution::CNetConvolution(CArrayObj *Description)
  {
   if(CheckPointer(Description)==POINTER_INVALID)
     class="kw">return;
class=class="str">"cmt">//---
   class="type">int total=Description.Total();
   if(total<=class="num">0)
     class="kw">return;
class=class="str">"cmt">//---
   layers=new CArrayLayer();
   if(CheckPointer(layers)==POINTER_INVALID)
     class="kw">return;
   CLayer *temp;
   CLayerDescription *desc=NULL, *next=NULL, *prev=NULL;
   CNeuronBase *neuron=NULL;
   CNeuronProof *neuron_p=NULL;
   class="type">int output_count=class="num">0;
   class="type">int temp_count=class="num">0;
   for(class="type">int i=class="num">0;i<total;i++)
     {
      prev=desc;
      desc=Description.At(i);
      if((i+class="num">1)<total)
        {
         next=Description.At(i+class="num">1);
         if(CheckPointer(next)==POINTER_INVALID)
           class="kw">return;
        }
      else
        next=NULL;
      class="type">int outputs=(next==NULL || next.type!=defNeuron ? class="num">0 : next.count);
      temp=new CLayer(outputs);
      for(class="type">int n=class="num">0;n<(desc.count+(i>class="num">0 && desc.type==defNeuron ? class="num">1 : class="num">0));n++)
        {
         class="kw">switch(desc.type)
           {
            case defNeuron:
              neuron=new CNeuron();
              if(CheckPointer(neuron)==POINTER_INVALID)

常见问题

基类只定接口不写具体逻辑,把前向计算和梯度回传甩给派生类,这样滑动窗口、卷积层等不同结构能各自实现,互不干扰。
它按设定步长在输入数据上切出局部窗口,对每个窗口调一次计算,实现上靠循环加指针偏移取数,窗口大小和步长就是关键参数。
可以,把代码贴给小布,它能对照卷积层反向传播逻辑帮你标出指针越界、窗口切片错位这类隐患,省去手动逐行核对。
结构描述类在构造时绑定了层的 shape 和权重布局,回传时按绑定信息定位写回地址,代码里靠同一个描述实例串起前后向。
至少留构造时传配置、保存权重到文件、从文件载入权重三个接口,保证训练完能落盘、下次直接接着用。