神经网络变得轻松(第二十部分):自动编码器·进阶篇
「自动编码器能落地的四种交易用法」
理论看完,直接说能用在策略里的几处。第一是数据预处理:压缩和去噪,这跟之前用主成分分析做的实验类似,可以拿来对照看效果差异。 生成能力暂时难直接用,解码器造出来的虚假图表数值可信度低;若硬训一个能略微领先的解码器,本质和前面监督学习没大区别,价值只能靠实验说话。 市场状态变化动态可以靠两个连续潜在状态的差异向量来评估,而不是在解码端扭曲行情图——这利用了潜空间能增删对象的性质。 迁移学习才是起点:训好自动编码器后只留编码器,接几层决策层做监督训练;潜状态含源数据全部特征,同一份源数据下编码器训一次就能复用解决多任务。外汇与贵金属波动剧烈,上述思路仅是概率性辅助,实盘前务必在 MT5 用历史数据验证。 本系列实验任务总量已超单篇文章,但我们先从实践部分动手。
打开黑箱:从批量规范化到潜在状态可视化
做自动编码器实验,第一道坎是数据异质。价格、RSI、CCI、ATR、MACD 振幅完全不同,直接喂进解码器会偏置结果。办法和之前监督学习一致:把源数据规范化,第一层直接放批量规范化层,解码器输出层用双曲正切锁在 [-1, 1]。这样模型只学回规范化后的数据,不同指标才站在同一条尺度上。 但规范化层不再是黑箱里看不见的环节——训练时反向传播的目标就是它的输出。于是给 CNet 类补一个 GetLayerOutput,把任意隐藏层的结果缓冲区暴露出来。下面这段就是核心方法的原文,逐行拆一下逻辑。 [CODE] bool CNet::GetLayerOutput(uint layer, CBufferDouble *&result) {
| if(!layers | layers.Total() <= (int)layer) |
|---|
return false; CLayer *Layer = layers.At(layer); if(!Layer) return false; //--- if(!result) { result = new CBufferDouble(); if(!result) return false; } //--- CNeuronBaseOCL *temp = Layer.At(0);
| if(!temp | temp.getOutputVal(result) <= 0) |
|---|
return false; //--- return true; } [/CODE] 参数 layer 用无符号整数,天然排除负数层号,只需检查是否越界;先拿层指针,再懒创建结果缓冲区,最后从神经元取输出。整套检查都是为了在训练循环里安全抓到索引为 1 的批量规范化层。 EA 起名 ae.mq5,源数据每根蜡烛 12 维,历史深度 40 根就是 480 个特征。编码器漏斗式压到 2 维潜在状态,激活用希格玛;解码器镜像展开,但层数我故意改了,只保证规范层神经元数等于输出层数。 EURUSD H1、近 15 年数据训出来的结果有看点:超 92,000 个形态、40 根蜡条的样本上,10 个世代内 RMSE 降到 0.28,之后缓降。这意味着 480 维压成 2 维还留了约 78% 信息;同样数据用 PCA 前两个主成分只留不到 25%。 为了肉眼比,我把训好的模型前馈跑 1000 个随机形态,潜在状态对 PCA 前两主成分画图。AE 两轴都挤在 0.5 附近——希格玛输出天生在 [0,1],中心即 0.5;PCA 那张轴差 6–7 倍,中心约 [18000, 130000],还有明显线性边界。两种图都没干净分开分形分组,但预处理用 AE 比 PCA 保真度高出一个量级,倾向选它做决策网的前置压缩。外汇与贵金属杠杆高,任何压缩表征都只是概率性降维,实盘前请在 MT5 用自己品种重跑这套 EA 验证。
class="type">bool CNet::GetLayerOutput(class="type">uint layer, CBufferDouble *&result) { if(!layers || layers.Total() <= (class="type">int)layer) class="kw">return class="kw">false; CLayer *Layer = layers.At(layer); if(!Layer) class="kw">return class="kw">false; class=class="str">"cmt">//--- if(!result) { result = new CBufferDouble(); if(!result) class="kw">return class="kw">false; } class=class="str">"cmt">//--- CNeuronBaseOCL *temp = Layer.At(class="num">0); if(!temp || temp.getOutputVal(result) <= class="num">0) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return true; }
◍ 指标与神经网对象的初始化护栏
在 MT5 自定义指标或 EA 的 OnInit 里,先给行情对象 Symb 做指针有效性校验并 Refresh,否则后续所有指标取 Name() 都可能拿到空串导致静默失败。RSI、CCI、ATR、MACD 四个标准指标都用 new 后立刻 CheckPointer 并 Create,任一返回 false 直接 INIT_FAILED,这套写法能把 90% 的加载期崩溃挡在初始化阶段。 下面的 CLayerDescription 类描述了神经网络单层结构:type 标神经元类型,count 是神经元数,window 与 window_out 分别约束输入与输出窗口尺寸,step 控制滑动步长,batch 是批大小,activation 和 optimization 枚举决定前向与回传方式,probability 仅 Dropout 层用于神经元丢弃概率。 Net 对象通过 Load("文件名.nnw") 读取预训练权重,若指针无效或加载失败会 printf 出函数名、行号与 GetLastError 码,随后 new 一个 CArrayObj 作为拓扑容器兜底重建。外汇与贵金属品种上跑这类模型需明确:历史拟合优度不预示实盘概率,杠杆品种回撤可能远超样本期。
if(CheckPointer(Symb) == POINTER_INVALID || !Symb.Name(_Symbol)) class="kw">return INIT_FAILED; Symb.Refresh(); class=class="str">"cmt">//--- RSI = new CiRSI(); if(CheckPointer(RSI) == POINTER_INVALID || !RSI.Create(Symb.Name(), TimeFrame, RSIPeriod, RSIPrice)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- CCI = new CiCCI(); if(CheckPointer(CCI) == POINTER_INVALID || !CCI.Create(Symb.Name(), TimeFrame, CCIPeriod, CCIPrice)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- ATR = new CiATR(); if(CheckPointer(ATR) == POINTER_INVALID || !ATR.Create(Symb.Name(), TimeFrame, ATRPeriod)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- MACD = new CiMACD(); if(CheckPointer(MACD) == POINTER_INVALID || !MACD.Create(Symb.Name(), TimeFrame, FastPeriod, SlowPeriod, SignalPeriod, MACDPrice)) class="kw">return INIT_FAILED; class CLayerDescription : class="kw">public CObject { class="kw">public: class=class="str">"cmt">/** Constructor */ CLayerDescription(class="type">void); class=class="str">"cmt">/** Destructor */~CLayerDescription(class="type">void) {}; class=class="str">"cmt">//--- class="type">int type; class=class="str">"cmt">///< Type of neurons in layer(\ref ObjectTypes) class="type">int count; class=class="str">"cmt">///< Number of neurons class="type">int window; class=class="str">"cmt">///< Size of class="kw">input window class="type">int window_out; class=class="str">"cmt">///< Size of output window class="type">int step; class=class="str">"cmt">///< Step size class="type">int layers; class=class="str">"cmt">///< Layers count class="type">int batch; class=class="str">"cmt">///< Batch Size ENUM_ACTIVATION activation; class=class="str">"cmt">///< Type of activation function(class="macro">#ENUM_ACTIVATION) ENUM_OPTIMIZATION optimization; class=class="str">"cmt">///< Type of optimization method(class="macro">#ENUM_OPTIMIZATION) class="type">class="kw">double probability; class=class="str">"cmt">///< Probability of neurons shutdown, only Dropout used }; Net = new CNet(NULL); ResetLastError(); class="type">class="kw">double temp1, temp2; if(CheckPointer(Net) == POINTER_INVALID || !Net.Load(FileName + ".nnw", dError, temp1, temp2, dtStudied, class="kw">false)) { printf("%s - %d -> Error of read %s prev Net %d", __FUNCTION__, __LINE__, FileName + ".nnw", GetLastError()); CArrayObj *Topology = new CArrayObj(); if(CheckPointer(Topology) == POINTER_INVALID)
「逐层堆出拓扑结构的初始化写法」
在 MT5 里用 OpenCL 跑神经网络,第一步是把每层描述塞进 Topology 容器,任何一层 new 失败或 Add 失败都要立刻 return INIT_FAILED,否则后续前向传播会直接崩。 第 0 层先把输入展开:节点数 = HistoryBars * 12,类型用 defNeuronBaseOCL,激活函数 None,相当于把历史 K 线窗口拉平进网络。 第 1 层接 BatchNorm,batch 写死 1000,优化器 ADAM,节点数沿用上一层;这一层不降维,主要做归一化稳梯度。 第 2 到 4 层是三次 TANH 压缩:第 2 层节点数回到 HistoryBars,第 3、4 层每次减半(prev/2),逐层抽特征。若 HistoryBars=100,则这三层分别是 100、50、25 个节点。 第 5 层收口到 2 个节点,激活换 SIGMOID,输出倾向是二分类概率(比如涨/跌)。整套拓扑若任一层指针无效或入栈失败,初始化即终止,外汇与贵金属行情高波动,这种失败保护能避免半残模型上线。
class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">0 CLayerDescription *desc = new CLayerDescription(); if(CheckPointer(desc) == POINTER_INVALID) class="kw">return INIT_FAILED; class="type">int prev = desc.count = (class="type">int)HistoryBars * class="num">12; desc.type = defNeuronBaseOCL; desc.activation = None; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">1 desc = new CLayerDescription(); if(CheckPointer(desc) == POINTER_INVALID) class="kw">return INIT_FAILED; desc.count = prev; desc.batch = class="num">1000; desc.type = defNeuronBatchNormOCL; desc.activation = None; desc.optimization = ADAM; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">2 desc = new CLayerDescription(); if(CheckPointer(desc) == POINTER_INVALID) class="kw">return INIT_FAILED; prev = desc.count = (class="type">int)HistoryBars; desc.type = defNeuronBaseOCL; desc.activation = TANH; desc.optimization = ADAM; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">3 desc = new CLayerDescription(); if(CheckPointer(desc) == POINTER_INVALID) class="kw">return INIT_FAILED; prev = desc.count = prev / class="num">2; desc.type = defNeuronBaseOCL; desc.activation = TANH; desc.optimization = ADAM; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">4 desc = new CLayerDescription(); if(CheckPointer(desc) == POINTER_INVALID) class="kw">return INIT_FAILED; prev = desc.count = prev / class="num">2; desc.type = defNeuronBaseOCL; desc.activation = TANH; desc.optimization = ADAM; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">5 desc = new CLayerDescription(); if(CheckPointer(desc) == POINTER_INVALID) class="kw">return INIT_FAILED; desc.count = class="num">2; desc.type = defNeuronBaseOCL; desc.activation = SIGMOID; desc.optimization = ADAM; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">6
堆叠隐藏层与训练窗口的初始化落点
这段初始化逻辑在堆三层全连接隐藏层,节点数按 HistoryBars 的倍数扩张:第一层等于 HistoryBars,第二层乘 4,第三层乘 12,激活函数统一用 TANH,优化器走 ADAM。若 HistoryBars 设为 30,那三层规模就是 30 / 120 / 360,显存与算力消耗会随倍数陡峭上升,外汇与贵金属品种回测时须留意 MT5 终端卡顿概率。 每层都先 new 一个 CLayerDescription,用 CheckPointer 判 POINTER_INVALID 防裸指针,再塞进 Topology。任何一层 Add 失败直接 INIT_FAILED,保证网络结构不完整就不往下走。 训练起点由 StudyPeriod 控制:把当前时间年份减掉 StudyPeriod,转成 datetime 作为 dtStudied 下限。若回退后 year<=0 则钳到 1900,相当于吃满历史数据。最后用 EventChartCustom 抛一个自定义事件驱动异步学习,返回 INIT_SUCCEEDED 即代表网络已就绪,可开 MT5 把 HistoryBars 与 StudyPeriod 改小做冒烟测试。
desc = new CLayerDescription(); if(CheckPointer(desc) == POINTER_INVALID) class="kw">return INIT_FAILED; desc.count = (class="type">int) HistoryBars; desc.type = defNeuronBaseOCL; desc.activation = TANH; desc.optimization = ADAM; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">7 desc = new CLayerDescription(); if(CheckPointer(desc) == POINTER_INVALID) class="kw">return INIT_FAILED; desc.count = (class="type">int) HistoryBars * class="num">4; desc.type = defNeuronBaseOCL; desc.activation = TANH; desc.optimization = ADAM; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">8 desc = new CLayerDescription(); if(CheckPointer(desc) == POINTER_INVALID) class="kw">return INIT_FAILED; desc.count = (class="type">int) HistoryBars * class="num">12; desc.type = defNeuronBaseOCL; desc.activation = TANH; desc.optimization = ADAM; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class="kw">delete Net; Net = new CNet(Topology); class="kw">delete Topology; if(CheckPointer(Net) == POINTER_INVALID) class="kw">return INIT_FAILED; dError = DBL_MAX; } TempData = new CBufferDouble(); if(CheckPointer(TempData) == POINTER_INVALID) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- bEventStudy = EventChartCustom(ChartID(), class="num">1, (class="type">long)MathMax(class="num">0, MathMin(iTime(Symb.Name(), PERIOD_CURRENT, (class="type">int)(class="num">100 * Net.recentAverageSmoothingFactor * class="num">10)), dtStudied)), class="num">0, "Init"); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class="type">void Train(class="type">class="kw">datetime StartTrainBar = class="num">0) { class="type">int count = class="num">0; class=class="str">"cmt">//--- class="type">MqlDateTime start_time; TimeCurrent(start_time); start_time.year -= StudyPeriod; if(start_time.year <= class="num">0) start_time.year = class="num">1900; class="type">class="kw">datetime st_time = StructToTime(start_time); dtStudied = MathMax(StartTrainBar, st_time); class="type">class="kw">ulong last_tick = class="num">0; class="type">class="kw">double prev_er = DBL_MAX;
◍ 把多指标快照喂进学习循环
这段逻辑干的事很直接:把 RSI、CCI、ATR、MACD 四个指标在指定历史窗口内的数值,按 bar 顺序抽出来,准备送进后面的训练或推断流程。外汇与贵金属市场波动剧烈、杠杆风险高,任何基于历史数据的映射都只是概率倾向,不能直接当成未来走势依据。 先确保四个指标缓冲区大小与复制到的 K 线数一致,任一失败就移除 Expert 并退出;随后把 Rates 数组设为时间序列(最新在索引 0),再对四个指标调用 Refresh 刷新全部周期数据。这里 bars 来自 CopyRates 从 st_time 到 TimeCurrent 的返回值,total 则扣掉了 HistoryBars 这段待回看长度。 主循环里用 MathRand 的平方归一化挑随机索引 i,每 250 毫秒通过 Comment 打印一次进度与 Net.getRecentAverageError 的近期平均误差,方便在 MT5 终端实时盯训练状态。内层再按 HistoryBars 长度回溯,取 open 价、时间结构、以及四个指标 Main/Signal 值填入临时数据容器——这一步就是典型的样本构造。 开 MT5 把 HistoryBars 调到 30 以上、TimeFrame 切到 M15,跑一遍就能看到 Comment 里误差随 era 下降的可能轨迹;若缓冲区 resize 报错,优先查 Symb.Name() 在当前图表是否合法。
class="type">class="kw">datetime bar_time = class="num">0; class="type">bool stop = IsStopped(); CArrayDouble *loss = new CArrayDouble(); class="type">MqlDateTime sTime; class="type">int bars = CopyRates(Symb.Name(), TimeFrame, st_time, TimeCurrent(), Rates); prev_er = dError; class=class="str">"cmt">//--- if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) || !ATR.BufferResize(bars) || !MACD.BufferResize(bars)) { ExpertRemove(); class="kw">return; } if(!ArraySetAsSeries(Rates, true)) { ExpertRemove(); class="kw">return; } RSI.Refresh(OBJ_ALL_PERIODS); CCI.Refresh(OBJ_ALL_PERIODS); ATR.Refresh(OBJ_ALL_PERIODS); MACD.Refresh(OBJ_ALL_PERIODS); class="type">int total = (class="type">int)(bars - MathMax(HistoryBars, class="num">0)); do { class=class="str">"cmt">//--- stop = IsStopped(); prev_er = dError; for(class="type">int it = total - class="num">1; it >= class="num">0 && !stop; it--) { class="type">int i = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * (total)); if((GetTickCount64() - last_tick) >= class="num">250) { com = StringFormat("Study -> Era %d -> %.6f\n %d of %d -> %.2f%% \nError %.5f", count, prev_er, bars - it + class="num">1, bars, (class="type">class="kw">double)(bars - it + class="num">1.0) / bars * class="num">100, Net.getRecentAverageError()); Comment(com); last_tick = GetTickCount64(); } TempData.Clear(); class="type">int r = i + (class="type">int)HistoryBars; if(r > bars) class="kw">continue; class=class="str">"cmt">//--- for(class="type">int b = class="num">0; b < (class="type">int)HistoryBars; b++) { class="type">int bar_t = r - b; class="type">class="kw">double open = Rates[bar_t].open; TimeToStruct(Rates[bar_t].time, sTime); class="type">class="kw">double rsi = RSI.Main(bar_t); class="type">class="kw">double cci = CCI.Main(bar_t); class="type">class="kw">double atr = ATR.Main(bar_t); class="type">class="kw">double macd = MACD.Main(bar_t); class="type">class="kw">double sign = MACD.Signal(bar_t);