利用 MQL5 矩阵的反向传播神经网络·进阶篇
用验证集掐断过度拟合
神经网络在训练集上把误差压到极小并不难,难的是面对未见过的行情数据仍保持同等隐式依赖的捕捉能力。若训练误差单调下降、前向测试却失灵,就是典型的过度拟合,外汇与贵金属这类高波动品种上该风险尤其突出,可能让模型在实盘中断崖式失效。 提早停止是最直接的正则化手段:另备一份验证集(输入 validation、输出 check),每个世代先算 msev 再算训练集 mse。验证误差通常先降后升——网络先抓主形态,随后开始死记训练集特异噪声。代码里用 EMA 平滑误差,p = sqrt(epochs) 经验定平滑周期,ema = 2/(p+1),避开随机波动误判。 停止条件藏在 scale 变量里:scale = data.Rows()/(validation.Rows()+1)+1,当 msevma > msevmap + scale*(msemap-msema) 且已过 p 个世代,循环 break。这相当于用训练/验证样本量比给容差,防止小验证集上的正常抖动触发早停。 Dropout 在 MatrixNet 里走低成本路线:不真删神经元,而是按 dropOutRate 百分比把权重矩阵随机置 0,每世代开始前用 DropOutState 恢复再重选。enableDropOut 默认 0 即关,开启后能在无验证集时也压制过拟合。 训练循环还留了 progress 虚函数钩子,每秒回传一次衡量值,返回 false 可手动中断。默认实现里 NaN 检测也前置——网络溢出或数据错直接 PrintFormat 报 epoch 并 break,避免脏权重写回。
class="type">class="kw">double train(const matrix &data, const matrix &target, const matrix &validation, const matrix &check, const class="type">int epochs = class="num">1000, const class="type">class="kw">double accuracy = class="num">0.001, const ENUM_LOSS_FUNCTION lf = LOSS_MSE) { if(!ready) class="kw">return NaN(); speed = accuracy; ... class="type">class="kw">double mse = DBL_MAX; class="type">class="kw">double msev = DBL_MAX; class="type">class="kw">double msema = class="num">0; class=class="str">"cmt">// MSE averaging of the training set class="type">class="kw">double msemap = class="num">0; class=class="str">"cmt">// MSE averaging of the training set in the previous epoch class="type">class="kw">double msevma = class="num">0; class=class="str">"cmt">// MSE averaging of the validation dataset class="type">class="kw">double msevmap = class="num">0; class=class="str">"cmt">// MSE averaging of the validation dataset in the previous epoch class="type">class="kw">double ema = class="num">0; class=class="str">"cmt">// exponential smoothing factor class="type">int p = class="num">0; class=class="str">"cmt">// EMA period p = (class="type">int)sqrt(epochs); class=class="str">"cmt">// empirically choose the period of the EMA averaging of errors ema = class="num">2.0 / (p + class="num">1); PrintFormat("EMA for early stopping: %d(%f)", p, ema); class="type">int ep = class="num">0; for(; ep < epochs; ep++) { if(validation.Rows() && check.Rows()) { class=class="str">"cmt">// if there is validation, run it before normal pass/training msev = test(validation, check, lf); class=class="str">"cmt">// smooth errors msevma = (msevma ? msevma : msev) * (class="num">1 - ema) + ema * msev; } mse = test(data, target, lf); class=class="str">"cmt">// enable feedForward(data) run msema = (msema ? msema : mse) * (class="num">1 - ema) + ema * mse; ... if(!MathIsValidNumber(mse)) { PrintFormat("NaN at epoch %d", ep); break; class=class="str">"cmt">// will class="kw">return NaN as error indication } const class="type">int scale = (class="type">int)(data.Rows() / (validation.Rows() + class="num">1)) + class="num">1; if(msevmap != class="num">0 && ep > p && msevma > msevmap + scale * (msemap - msema)) { class=class="str">"cmt">// skip the first p epochs to accumulate values for averaging PrintFormat("Stop by validation at %d, v: %f > %f, t: %f vs %f", ep, msevma, msevmap, msema, msemap); break; } msevmap = msevma; msemap = msema; ... if(mse <= accuracy) { PrintFormat("Done by accuracy limit %f at epoch %d", accuracy, ep); break; } if(!progress(ep, epochs, mse, msev, msema, msevma)) { PrintFormat("Interrupted by user at epoch %d", ep); break; }
「训练循环里的容错与进度节流」
这段 CNet 派生类代码展示了神经网络训练收尾与过程监控的两种典型写法。反向传播若失败,立即把 mse 置为 NaN 并 break,避免脏权重被后续逻辑误用。 当训练跑满 epoch 上限时,用 PrintFormat 输出 "Done by epoch limit %d with accuracy %f",这里的 accuracy 实际是 mse 值,回测时若看到该值接近 0 说明拟合倾向过强,外汇与贵金属样本上过拟合概率偏高,属高风险信号。 progress 函数用 static uint trap 配合 GetTickCount 做每秒一次的日志节流,防止 MT5 终端被高频 Print 刷屏。dropOutRate 默认 10%,enableDropOut 可改比例,实盘前建议在 EURUSD 的 M5 历史上把 dropout 调到 20% 对比泛化表现。
if(!backProp(target)) { mse = NaN(); class=class="str">"cmt">// error flag break; } } if(ep == epochs) { PrintFormat("Done by epoch limit %d with accuracy %f", ep, mse); } class="kw">return mse; } class="kw">virtual class="type">bool progress(const class="type">int epoch, const class="type">int total, const class="type">class="kw">double error, const class="type">class="kw">double valid = DBL_MAX, const class="type">class="kw">double ma = DBL_MAX, const class="type">class="kw">double mav = DBL_MAX) { class="kw">static class="type">uint trap; if(GetTickCount() > trap) { PrintFormat("Epoch %d of %d, loss %.5f%s%s%s", epoch, total, error, ma == DBL_MAX ? "" : StringFormat(" ma(%.5f)", ma), valid == DBL_MAX ? "" : StringFormat(", validation %.5f", valid), valid == DBL_MAX ? "" : StringFormat(" v.ma(%.5f)", mav)); trap = GetTickCount() + class="num">1000; } class="kw">return !IsStopped(); } class="type">void enableDropOut(const class="type">uint percent = class="num">10) { dropOutRate = (class="type">int)percent; }
◍ 用批量 rprop 替网络换上自适应步频
恒定学习率在实际训练里两头不讨好:调小了权重收敛拖沓,调大了又在误差面上来回弹跳。把弹性传播(rprop)搬进全量矩阵计算,就得到批量 rprop——它逐权重比对上一代与当前代增量符号,同向就加速、反向就减速。 启用方式很直接:在包含 MatrixNet.mqh 之前定义 BATCH_PROP 宏。此时类里用 speed[] 和 deltas[] 矩阵数组替换原来的标量 speed,前者存每层学习率,后者存上代权重增量,用于符号比较。 四个附加变量框定调节边界:plus=1.1 为加速倍数,minus=0.1 为减速倍数,max=50 封顶,min=0.0 保底。allocate 里给它们写了默认值,想改就在构造后调 setupSpeedAdjust。 训练启动时,train 方法不再给标量赋精度,而是用 accuracy 把 speed[i] 每个矩阵填满,deltas[i] 填 0。backProp 里算完 delta 立刻调 adjustSpeed,把 delta*deltas[i] 乘积传进去判定方向;随后把新 delta 存回 deltas[i] 供下代使用。 adjustSpeed 的逻辑毫无花活:矩阵元素乘积为正,对应速度乘 plus 且不超 max;为负则乘 minus 且不破 min。这样每层、每个连接都有自己的节奏,外汇与贵金属模型训练的高波动样本下,过冲概率可能降低,但杠杆品种仍属高风险,参数须先在 MT5 历史数据上回测。
class="macro">#define BATCH_PROP class MatrixNet { class="kw">protected: ... class="macro">#ifdef BATCH_PROP matrix speed[]; matrix deltas[]; class="macro">#else class="type">class="kw">double speed; class="macro">#endif class="type">class="kw">double plus; class="type">class="kw">double minus; class="type">class="kw">double max; class="type">class="kw">double min; class="type">void allocate() { ArrayResize(weights, n); ArrayResize(outputs, n + class="num">1); ArrayResize(bestWeights, n); dropOutRate = class="num">0; class="macro">#ifdef BATCH_PROP ArrayResize(speed, n); ArrayResize(deltas, n); plus = class="num">1.1; minus = class="num">0.1; max = class="num">50; min = class="num">0.0; class="macro">#endif } class="type">class="kw">double train(const matrix &data, const matrix &target, const matrix &validation, const matrix &check, const class="type">int epochs = class="num">1000, const class="type">class="kw">double accuracy = class="num">0.001, const ENUM_LOSS_FUNCTION lf = LOSS_MSE) { ... class="macro">#ifdef BATCH_PROP for(class="type">int i = class="num">0; i < n; ++i) { speed[i].Fill(accuracy); class=class="str">"cmt">// adjust speeds on the fly deltas[i].Fill(class="num">0); } class="macro">#else speed = accuracy; class="macro">#endif ... } class="type">bool backProp(const matrix &target) { ... for(class="type">int i = n - class="num">1; i >= class="num">0; --i) class=class="str">"cmt">// all layers except the output in reverse order { ... class="macro">#ifdef BATCH_PROP matrix delta = speed[i] * outputs[i].Transpose().MatMul(loss); adjustSpeed(speed[i], delta * deltas[i]); deltas[i] = delta; class="macro">#else matrix delta = speed * outputs[i].Transpose().MatMul(loss); class="macro">#endif ... } ... } class="type">void adjustSpeed(matrix &subject, const matrix &product) { for(class="type">int i = class="num">0; i < (class="type">int)product.Rows(); ++i) { for(class="type">int j = class="num">0; j < (class="type">int)product.Cols(); ++j) { if(product[i][j] > class="num">0) { subject[i][j] *= plus; if(subject[i][j] > max) subject[i][j] = max; } else if(product[i][j] < class="num">0) { subject[i][j] *= minus; if(subject[i][j] < min) subject[i][j] = min; } } } }
训练里抓最小误差权重并落盘
矩阵网络按“世代”循环训练:每个世代把整批训练向量灌进网络,行是样本记录(比如一根根的 OHLC+成交量报价栏),列是字段维度。沿梯度调权本质仍是随机过程——目标函数形态和可变学习速率不均,会让某些世代反而跑到更差的权重配置,所以世代数堆上去并不保证误差单调下降。 因此要在循环里持续盯整体误差:一旦当前世代算出的候选误差(验证集优先,没有就用训练集 mse)小于已记录最小值,就把这一刻的全部权重矩阵拷进 bestWeights。代码里 stats.bestLoss 初值设 DBL_MAX、bestEpoch 设 -1,就是用来接住第一个真正更小的候选值。 训练结束后,getWeights 取最终权重,getBestWeights 取历史最优权重;两者都先判 ready 和矩阵非空,避免拿到空壳。单独一个接收 matrix 数组的构造函数 MatrixNet(w[], ...) 负责把现成权重直接装载成可推理的网络,跳过训练。 这些矩阵数组可以序列化进文件,下次 MT5 启动直接读回,恢复一个已训练就绪的网络。外汇与贵金属行情高波动、过拟合风险大,恢复的网络在历史样本上误差低,不代表实盘推演误差倾向同样低,上实盘前务必用样本外数据复核。
<span class="keyword">class </span>MatrixNet { ... <span class="keyword">class="kw">public</span>: <span class="keyword">class="kw">struct</span> Stats { <span class="keyword">class="type">class="kw">double</span> bestLoss; <span class="comment">class=class="str">"cmt">// smallest error for all epochs</span> <span class="keyword">class="type">int</span> bestEpoch; <span class="comment">class=class="str">"cmt">// index of the epoch with the minimum error</span> <span class="keyword">class="type">int</span> epochsDone; <span class="comment">class=class="str">"cmt">// total number of completed epochs</span> }; Stats getStats() <span class="keyword">const</span> { <span class="keyword">class="kw">return</span> stats; } <span class="keyword">class="kw">protected</span>: <span style="class="type">class="kw">color:rgb(class="num">0, class="num">87, class="num">174);">matrix</span> bestWeights[]; Stats stats; ... <span class="keyword">class="type">class="kw">double</span> train(<span class="keyword">const</span> <span style="class="type">class="kw">color:rgb(class="num">0, class="num">87, class="num">174);">matrix</span> &data, <span class="keyword">const</span> <span style="class="type">class="kw">color:rgb(class="num">0, class="num">87, class="num">174);">matrix</span> &target, <span class="keyword">const</span> <span style="class="type">class="kw">color:rgb(class="num">0, class="num">87, class="num">174);">matrix</span> &validation, <span class="keyword">const</span> <span style="class="type">class="kw">color:rgb(class="num">0, class="num">87, class="num">174);">matrix</span> &check, <span class="keyword">const</span> <span class="keyword">class="type">int</span> epochs = class="num">1000, <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> accuracy = class="num">0.001, <span class="keyword">const</span> <span style="class="type">class="kw">color:rgb(class="num">226, class="num">8, class="num">0);">ENUM_LOSS_FUNCTION</span> lf = <span style="class="type">class="kw">color:rgb(class="num">226, class="num">8, class="num">0);">LOSS_MSE</span>) { ... stats.bestLoss = <span class="macro">DBL_MAX</span>; stats.bestEpoch = -class="num">1; DropOutState state(dropOutRate); <span class="keyword">class="type">int</span> ep = class="num">0; <span class="keyword">for</span>(; ep < epochs; ep++) { ... <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> candidate = (msev != <span class="macro">DBL_MAX</span>) ? msev : mse; <span class="keyword">if</span>(candidate < stats.bestLoss) { stats.bestLoss = candidate; stats.bestEpoch = ep; <span class="comment">class=class="str">"cmt">// save best weights from &class="macro">#x27;weights&class="macro">#x27;</span> <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = class="num">0; i < n; ++i) { bestWeights[i].Assign(weights[i]); } } } ... <span class="keyword">class="type">bool</span> getWeights(<span style="class="type">class="kw">color:rgb(class="num">0, class="num">87, class="num">174);">matrix</span> &array[]) <span class="keyword">const</span> { <span class="keyword">if</span>(!ready) <span class="keyword">class="kw">return</span> <span class="keyword"><span class="macro">false</span></span>; <span class="functions">ArrayResize</span>(array, n); <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = class="num">0; i < n; ++i) { array[i] = weights[i]; } <span class="keyword">class="kw">return</span> <span class="keyword"><span class="macro">true</span></span>; } <span class="keyword">class="type">bool</span> getBestWeights(<span style="class="type">class="kw">color:rgb(class="num">0, class="num">87, class="num">174);">matrix</span> &array[]) <span class="keyword">const</span> { <span class="keyword">if</span>(!ready) <span class="keyword">class="kw">return</span> <span class="keyword"><span class="macro">false</span></span>; <span class="keyword">if</span>(!n || !bestWeights[class="num">0].Rows()) <span class="keyword">class="kw">return</span> <span class="keyword"><span class="macro">false</span></span>; <span class="functions">ArrayResize</span>(array, n); <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = class="num">0; i < n; ++i) { array[i] = bestWeights[i]; } <span class="keyword">class="kw">return</span> <span class="keyword"><span class="macro">true</span></span>; } MatrixNet(<span class="keyword">const</span> <span style="class="type">class="kw">color:rgb(class="num">0, class="num">87, class="num">174);">matrix</span> &w[], <span class="keyword">const</span> <span style="class="type">class="kw">color:rgb(class="num">226, class="num">8, class="num">0);">ENUM_ACTIVATION_FUNCTION</span> f1 = <span style="class="type">class="kw">color:rgb(class="num">226, class="num">8, class="num">0);">AF_TANH</span>, <span class="keyword">const</span> <span style="class="type">class="kw">color:rgb(class="num">226, class="num">8, class="num">0);">ENUM_ACTIVATION_FUNCTION</span> f2 = <span style="class="type">class="kw">color:rgb(class="num">226, class="num">8, class="num">0);">AF_NONE</span>): ready(<span class="keyword"><span class="macro">false</span></span>), af(f1), of(f2), n(<span class="functions">ArraySize</span>(w)) { <span class="keyword">if</span>(n < class="num">2) <span class="keyword">class="kw">return</span>; allocate(); <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = class="num">0; i < n; ++i) { weights[i] = w[i]; <span class="keyword">class="macro">#ifdef</span> BATCH_PROP speed[i] = weights[i]; <span class="comment">class=class="str">"cmt">// instead .Init(.Rows(), .Cols())</span> deltas[i] = weights[i]; <span class="comment">class=class="str">"cmt">// instead .Init(.Rows(), .Cols())</span> <span class="keyword">class="macro">#endif</span> } ready = <span class="keyword"><span class="macro">true</span></span>; }
「用五条曲线把训练误差画在图上」
MatrixNet 自带的 progress 方法只往日志里吐数字,看不出训练走向。所以在 MatrixNet.mqh 里另写了一个从 MatrixNet 派生的 MatrixNetVisual 类,用 CGraphic 的派生小类 CMyGraphic 把按世代变化的误差直接绘成图,图形对象占满整个图表窗口。 这个类内部挂了 5 条 CCurve 曲线,横轴 p 是世代数,纵轴分别装:x 训练集误差、y 验证集误差、z 平滑验证误差(EMA)、q 平滑训练误差(EMA)、b 最小误差对应的世代点。构造函数一上来就调 graph() 建图并加好这五条线。 重写的 progress 方法里,每跑完一代就把 epoch、error、valid、ma、mav 压进对应双精度数组,然后调 plot() 刷新。plot() 用 Update 把数组推给各曲线,最后 CurvePlotAll + graphic.Update 完成重绘。外汇与贵金属模型训练样本外表现不确定,过拟合概率不低,图形只辅助观察、不构成任何方向暗示。 想自己看效果,在 MT5 里用 MatrixNetVisual 替代 MatrixNet 实例化,跑训练时图表会自动出图;若只想改显示范围,调 graph() 里 ChartGetInteger 取到的 width/height 即可。
class MatrixNetVisual: class="kw">public MatrixNet { CMyGraphic graphic; CCurve *c[class="num">5]; class="type">class="kw">double p[], x[], y[], z[], q[], b[]; ... class="type">void graph() { class="type">ulong width = ChartGetInteger(class="num">0, CHART_WIDTH_IN_PIXELS); class="type">ulong height = ChartGetInteger(class="num">0, CHART_HEIGHT_IN_PIXELS); class="type">bool res = false; const class="type">class="kw">string objname = "BPNNERROR"; if(ObjectFind(class="num">0, objname) >= class="num">0) res = graphic.Attach(class="num">0, objname); else res = graphic.Create(class="num">0, objname, class="num">0, class="num">0, class="num">0, (class="type">int)(width - class="num">0), (class="type">int)(height - class="num">0)); if(!res) class="kw">return; c[class="num">0] = graphic.CurveAdd(p, x, CURVE_LINES, "Training"); c[class="num">1] = graphic.CurveAdd(p, y, CURVE_LINES, "Validation"); c[class="num">2] = graphic.CurveAdd(p, z, CURVE_LINES, "Val.EMA"); c[class="num">3] = graphic.CurveAdd(p, q, CURVE_LINES, "Train.EMA"); c[class="num">4] = graphic.CurveAdd(p, b, CURVE_POINTS, "Best/Minimum"); ... } class="kw">public: MatrixNetVisual(const class="type">int &layers[], const ENUM_ACTIVATION_FUNCTION f1 = AF_TANH, const ENUM_ACTIVATION_FUNCTION f2 = AF_NONE): MatrixNet(layers, f1, f2) { graph(); } class="kw">virtual class="type">bool progress(const class="type">int epoch, const class="type">int total, const class="type">class="kw">double error, const class="type">class="kw">double valid = DBL_MAX, const class="type">class="kw">double ma = DBL_MAX, const class="type">class="kw">double mav = DBL_MAX) class="kw">override { class=class="str">"cmt">// fill all the arrays PUSH(p, epoch); PUSH(x, error); if(valid != DBL_MAX) PUSH(y, valid); else PUSH(y, nan); if(ma != DBL_MAX) PUSH(q, ma); else PUSH(q, nan); if(mav != DBL_MAX) PUSH(z, mav); else PUSH(z, nan); plot(); class="kw">return MatrixNet::progress(epoch, total, error, valid, ma, mav); } class="type">void plot() { c[class="num">0].Update(p, x); c[class="num">1].Update(p, y); c[class="num">2].Update(p, z); c[class="num">3].Update(p, q); class="type">class="kw">double point[class="num">1] = {stats.bestEpoch}; b[class="num">0] = stats.bestLoss; c[class="num">4].Update(point, b); ... graphic.CurvePlotAll(); graphic.Update(); }
◍ 用脚本跑通第一遍反向传播
MatrixNet 家族第一次实操,是靠 MatrixNet.mq5 脚本完成的。原始数据按已知分析记录人工生成,核心映射用了这个公式:f = ((x + y + z)^2 / (x^2 + y^2 + z^2)) / 3,比机器学习帮助主题里的示例多除了个 3,把值域压到 0~1。x、y、z 在 -10 到 +10 之间随机取点,单点输出就是 f 的一个标量。 脚本输入里 Epochs=1000、Accuracy=0.001、RandomNoise=0.0,噪声默认关掉。CreateData 负责填 data(100 行 3 列)和 target(100 行 1 列)两个矩阵;再调一次生成 25 行的验证集。网络结构写死成 {3, 11, 7, 1},在预处理前定义 BATCH_PROP 宏开启变速率加速。 训练时有效集误差转头向上就提前终止。默认参数下,日志里第 155 轮触发停止:验证误差 0.034642 已超过滑动均值 0.034371,而训练集误差停在 0.0166 附近。最佳权重拷给 net2 后,训练集误差 0.01546、验证集 0.03212——验证略差,但没崩。 把 RandomNoise 拉到 3.0,情况就难看了。同样跑完,训练结果误差 0.424,验证集 net2 误差直接 1.0629,接近函数值上限 1.0 的量级,关系几乎认不出来。实盘前自己洗数、去异常、查因子独立性的功夫,在这类自造数据里省了,但噪声实验说明真实报价丢进网络前不处理,识别依赖会非常吃力。 脚本末尾挂 while(!IsStopped()) 循环,把图留给你慢慢看。开 MT5 把这段直接丢进脚本,改 RandomNoise 从 0 调到 3,就能复现误差从 0.03 跳到 1.06 的过程。
input class="type">int Epochs = class="num">1000; input class="type">class="kw">double Accuracy = class="num">0.001; input class="type">class="kw">double RandomNoise = class="num">0.0; class="type">bool CreateData(matrix &data, matrix &target, const class="type">int count) { if(!data.Init(count, class="num">3) || !target.Init(count, class="num">1)) class="kw">return false; data.Random(-class="num">10, class="num">10); vector X1 = MathPow(data.Col(class="num">0) + data.Col(class="num">1) + data.Col(class="num">2), class="num">2); vector X2 = MathPow(data.Col(class="num">0), class="num">2) + MathPow(data.Col(class="num">1), class="num">2) + MathPow(data.Col(class="num">2), class="num">2); if(!target.Col(X1 / X2 / class="num">3.0, class="num">0)) class="kw">return false; if(RandomNoise > class="num">0) { matrix noise; noise.Init(count, class="num">3); noise.Random(class="num">0, RandomNoise); data += noise - RandomNoise / class="num">2; noise.Resize(count, class="num">1); noise.Random(-RandomNoise / class="num">2, RandomNoise / class="num">2); target += noise; } class="kw">return true; } class="macro">#define BATCH_PROP class="macro">#include <MatrixNet.mqh> class="type">void OnStart() { const class="type">int layers[] = {class="num">3, class="num">11, class="num">7, class="num">1}; MatrixNetVisual net(layers); matrix data, target; CreateData(data, target, class="num">100); matrix valid, test; CreateData(valid, test, class="num">25); ... Print("Training result: ", net.train(data, target, valid, test, Epochs, Accuracy)); matrix w[]; if(net.getBestWeights(w)) { MatrixNet net2(w); if(net2.isReady()) { Print("Best copy on training data: ", net2.test(data, target)); Print("Best copy on validation data: ", net2.test(valid, test)); } } while(!IsStopped()) { Sleep(class="num">1000); } } EMA for early stopping: class="num">31 (class="num">0.062500) Epoch class="num">0 of class="num">1000, loss class="num">0.20296 ma(class="num">0.20296), validation class="num">0.18167 v.ma(class="num">0.18167) Epoch class="num">120 of class="num">1000, loss class="num">0.02319 ma(class="num">0.02458), validation class="num">0.04566 v.ma(class="num">0.04478) Stop by validation at class="num">155, v: class="num">0.034642 > class="num">0.034371, t: class="num">0.016614 vs class="num">0.016674 Training result: class="num">0.015707719706513287 Best copy on training data: class="num">0.015461956812387292 Best copy on validation data: class="num">0.03211748853774414 Epoch class="num">0 of class="num">1000, loss class="num">2.40352 ma(class="num">2.40352), validation class="num">2.23536 v.ma(class="num">2.23536) Stop by validation at class="num">163, v: class="num">1.082419 > class="num">1.080340, t: class="num">0.432023 vs class="num">0.432526 Training result: class="num">0.4244786772678285 Best copy on training data: class="num">0.4300476339855798 Best copy on validation data: class="num">1.062895214094978