数据科学与机器学习(第 06 部分):梯度下降·进阶篇
📉

数据科学与机器学习(第 06 部分):梯度下降·进阶篇

(2/3)·从成本函数微分到学习曲率调步,多数交易者卡在第二步的迭代逻辑

含代码示例偏理论 第 2/3 篇
把梯度下降当成黑箱调参按钮的人,往往在学习曲率上拍脑袋设值,结果模型要么收敛过慢要么直接跳过最小值。唐纳德·高德纳那句“过早优化是万恶之源”放在这里很合适——没搞清链式法则和步长含义就跑优化,只是制造噪声。

◍ 为什么不直接用默认线性模型

用函数库直接生成默认线性模型,看似省事,但默认参数拟合出来的结果往往不是误差最小的那个。要让计算机自己找到最优参数,就得让它从误差里学习,而不是套一个通用初值。 梯度下降是后面要讲的反向传播和自学习机制的地基。系列文章逐步逼近人工神经网络,如果这一步不啃透,后面权重更新、损失曲面这些只会更绕。 外汇和贵金属市场噪声大、过拟合风险高,用梯度下降搜参数时,务必用 MT5 历史数据做跨品种验证,别把某段行情里跑顺的权重当成普适解。

用梯度下降逼出回归系数

拿工资数据集做例子,经验年限对薪资显然是回归问题,但能画出的拟合线有上百万条,误差最小的才值得用。成本函数先定义为真实值减预测值,平方后求和再除以样本数 m,这就成了均方误差 MSE 的雏形。 梯度下降不再用 while 死循环,而是把迭代次数钉死在 10000 次以内,控制算力消耗。斜率 B1 和截距 B0 每轮同步更新,学习率取 0.01 时,算法从试过的万级模型里挑出了局部最优的那条线。 日志里有一行实测:第 0 次迭代 b0=1520.06、cost_B0=-152006,B1=9547.974、cost_B1=-954797.4,说明初始梯度还远没收敛。DBL_MAX_MIN 只是兜底——一旦系数撞上双精度极限就报警,避免算出 NaN 还以为模型正常。 漏掉特征标准化这步,梯度可能在不同量纲上畸形更新,让收敛变慢甚至跑偏。外汇和贵金属行情用类似回归建模时波动更剧烈,属于高风险场景,系数未经标准化直接丢进下降循环,结果可能完全不可信。

MQL5 / C++
class="kw">import pandas <span class="keyword">as</span> pd
class="kw">import numpy <span class="keyword">as</span> np
class="kw">import matplotlib.pyplot <span class="keyword">as</span> plt
data&nbsp;&nbsp;= pd.read_csv(r<span class="class="type">class="kw">string">"C:\Users\Omega Joctan\AppData\Roaming\MetaQuotes\Terminal\892B47EBC091D6EF95E3961284A76097\MQL5\Files\Salary_Data.csv"</span>)
print(data.head(<span class="number">class="num">10</span>))
x = data[<span class="class="type">class="kw">string">"YearsExperience"</span>]
y = data[<span class="class="type">class="kw">string">"Salary"</span>]
plt.figure(figsize=(<span class="number">class="num">16</span>,<span class="number">class="num">9</span>))
plt.title(<span class="class="type">class="kw">string">"Experience vs Salary"</span>)
plt.scatter(x,y,c=<span class="class="type">class="kw">string">"green"</span>)
plt.xlabel(xlabel=<span class="class="type">class="kw">string">"Years of Experience"</span>)
plt.ylabel(ylabel=<span class="class="type">class="kw">string">"Salary"</span>)
plt.show()
&nbsp;&nbsp;
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span> cost_B0=<span class="number">class="num">0</span>, cost_B1=<span class="number">class="num">0</span>;
&nbsp;&nbsp;
&nbsp;&nbsp; <span class="keyword">if</span> (costFunction == MSE)
&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">int</span> iterations=<span class="number">class="num">0</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span> (<span class="keyword">class="type">int</span> i=<span class="number">class="num">0</span>; i&lt;m_iterations; i++, iterations++)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; cost_B0 = Mse(b0,b1,Intercept);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; cost_B1 = Mse(b0,b1,Slope);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; b0 = b0 - m_learning_rate * cost_B0;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; b1 = b1 - m_learning_rate * cost_B1;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">printf</span>(<span class="class="type">class="kw">string">"%d b0 = %.8f cost_B0 = %.8f B1 = %.8f cost_B1 = %.8f"</span>,iterations,b0,cost_B0,b1,cost_B1);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; DBL_MAX_MIN(b0); DBL_MAX_MIN(cost_B0); DBL_MAX_MIN(cost_B1);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span> (<span class="functions">NormalizeDouble</span>(cost_B0,<span class="number">class="num">8</span>) == <span class="number">class="num">0</span> &amp;&amp; <span class="functions">NormalizeDouble</span>(cost_B1,<span class="number">class="num">8</span>) == <span class="number">class="num">0</span>)&nbsp;&nbsp;<span class="keyword">class="kw">break</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">printf</span>(<span class="class="type">class="kw">string">"%d Iterations Local Minima are\nB0(Intercept) = %.5f&nbsp;&nbsp;||&nbsp;&nbsp;B1(Coefficient) = %.5f"</span>,iterations,b0,b1);&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;}

「梯度下降在 EURUSD M1 上的收敛轨迹」

在 MT5 策略测试器里跑一段线性回归的梯度下降,EURUSD M1 品种上能看到系数 b0、B1 随迭代次数变化的原始打印。前几步 b0 从 1995.087 爬到 2821.239,B1 从 12056.692 微调到 12858.674,cost_B0 和 cost_B1 的绝对值同步缩小,说明学习率没炸。 跑到第 6672 步时,b0 停在 25792.20019866、B1 停在 9449.96232146,cost_B0 与 cost_B1 都已逼近 0(打印值 -0.00000001 与 0.00000000)。从首次打印到收敛耗时约 30.248 秒(17:29:17.999 到 17:29:48.247),外汇与贵金属高杠杆品种做这类数值实验需自行承担滑点与重算风险。 别把前几行的单调下降当稳态。前 8 步 cost_B1 还在 -3165 到 +1425 之间晃,第 5 步才刚翻正,早期迭代对初始点的依赖很强,直接拿前几轮系数去挂单大概率偏得离谱。

◍ 梯度下降收敛时的参数锁定现象

在 EURUSD M1 周期上跑梯度下降回归测试,迭代到 6674~6684 步时,截距项 b0 稳定停在 25792.20019866,斜率项 B1 锁定在 9449.96232146,连续 11 个 tick 未再浮动。 成本函数的梯度 cost_B0 与 cost_B1 已衰减到 1e-8 量级(末尾一步 cost_B0 甚至到 -0.00000000),说明这组线性拟合在该样本窗口内已逼近局部极小。 外汇与贵金属属高杠杆高风险品种,这种收敛只代表历史样本内的数值稳定,换周期或行情结构断裂后参数可能瞬间失准,开 MT5 把同样脚本挂 M5 对比便能验证。

梯度下降在 EURUSD M1 上的局部极小结果

在 EURUSD 的 M1 周期上跑了一轮梯度下降测试,日志显示共迭代 6684 次后落入局部极小。这种迭代量在 M1 这种噪声极多的周期里并不算小,说明代价函数地形复杂,容易卡在不是全局最优的点。 最终拟合出的截距项 B0 为 25792.20020,系数项 B1 为 9449.96232。这两个数值本身只是该次局部极小下的回归参数,直接拿去预测下一根 M1 K 线并不可靠,外汇与贵金属 M1 交易本身属于高风险行为,参数过拟合概率偏高。 想验证这套结果,可在 MT5 策略测试器里用相同品种周期重跑梯度下降例程,对比迭代次数与 B0/B1 是否落在相近区间;若每次随机初始化后结果漂移很大,就说明局部极小问题确实绕不开。

MQL5 / C++
LE      class="num">0    class="num">17:class="num">29:class="num">48.247   gradient-descent test(EURUSD,M1)   class="num">6684 Iterations Local Minima are
OJ      class="num">0    class="num">17:class="num">29:class="num">48.247   gradient-descent test(EURUSD,M1)   B0(Intercept) = class="num">25792.20020  ||  B1(Coefficient) = class="num">9449.96232

「把输入变量压到同一尺度再喂给回归」

梯度下降里学习曲率(learning rate)选错,代价函数不一定收敛。用 0.1 跑 1000 次迭代,B1 在第 7 步已经冲到 -3.48e10、cost_B1 到 4.03e11,系统直接触及双精度上限,模型基本废了。换 0.01 虽慢,但至少不会爆数值。 每个数据集都有自己合适的学习曲率,多维变量时挨个调参很低效。把全部自变量做归一化(通常压到 0~1),可读性变好,训练时间倾向缩短,而且只要固定一个学习曲率(比如 0.01)就能通吃不同数据集,不用再赌尺度。 工资原始区间 39343~121782,工龄才 1.1~10.5。不归一的话,工资数量级会压过工龄,模型误以为它更重要。让自变量彼此影响力相近,这一步在外汇/贵金属特征工程里同样关键,杠杆品种价格跳变大,未归一直接回归大概率失真,属高风险操作。 下面这段 MT5 日志就是 0.1 学习曲率跑崩的现场,注意 b0/B1 每步指数级放大:

MQL5 / C++
GM        class="num">0     class="num">17:class="num">28:class="num">14.819    gradient-descent test(EURUSD,M1)     Gradient Descent CostFunction MSE
OP        class="num">0     class="num">17:class="num">28:class="num">14.819    gradient-descent test(EURUSD,M1)     class="num">0 b0 = class="num">15200.60000000 cost_B0 = -class="num">152006.00000000 B1 = class="num">95479.74000000 cost_B1 = -class="num">954797.40000000
GR        class="num">0     class="num">17:class="num">28:class="num">14.819    gradient-descent test(EURUSD,M1)     class="num">1 b0 = -class="num">74102.05704000 cost_B0 = class="num">893026.57040000 B1 = -class="num">512966.08473333 cost_B1 = class="num">6084458.24733333
NM        class="num">0     class="num">17:class="num">28:class="num">14.819    gradient-descent test(EURUSD,M1)     class="num">2 b0 = class="num">501030.91374462 cost_B0 = -class="num">5751329.70784622 B1 = class="num">3356325.13824362 cost_B1 = -class="num">38692912.22976952
LH        class="num">0     class="num">17:class="num">28:class="num">14.819    gradient-descent test(EURUSD,M1)     class="num">3 b0 = -class="num">3150629.51591119 cost_B0 = class="num">36516604.29655810 B1 = -class="num">21257352.71857720 cost_B1 = class="num">246136778.56820822
KD        class="num">0     class="num">17:class="num">28:class="num">14.819    gradient-descent test(EURUSD,M1)     class="num">4 b0 = class="num">20084177.14287909 cost_B0 = -class="num">232348066.58790281 B1 = class="num">135309993.40314889 cost_B1 = -class="num">1565673461.21726084
OQ        class="num">0     class="num">17:class="num">28:class="num">14.819    gradient-descent test(EURUSD,M1)     class="num">5 b0 = -class="num">127706877.34210962 cost_B0 = class="num">1477910544.84988713 B1 = -class="num">860620298.24803317 cost_B1 = class="num">9959302916.51181984
FM        class="num">0     class="num">17:class="num">28:class="num">14.819    gradient-descent test(EURUSD,M1)     class="num">6 b0 = class="num">812402202.33122230 cost_B0 = -class="num">9401090796.73331833 B1 = class="num">5474519904.86084747 cost_B1 = -class="num">63351402031.08880615
JJ        class="num">0     class="num">17:class="num">28:class="num">14.819    gradient-descent test(EURUSD,M1)     class="num">7 b0 = -class="num">5167652856.43381691 cost_B0 = class="num">59800550587.65039062 B1 = -class="num">34823489070.42410278 cost_B1 = class="num">402980089752.84948730
把微分和迭代交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到成本函数梯度的可视化轨迹,你只需判断步长是否偏离常态。

常见问题

梯度下降用于最小化成本函数找局部最优参数,梯度上升则沿梯度方向找局部最大值,常见于某些极值反转策略的阈值求解,外汇贵金属波动剧烈时局部极值易漂移,结论带概率性。
成本函数度量全数据集预测与真实的均损,值越大说明平均偏离越高,模型捕捉x与y关系的能力越弱,该结论基于可微假设,实盘需防过拟合。
可以,小布内置了回归输入的梯度下降轨迹视图,省去手写Python绘制,你专注解读学习曲率是否适配当前品种波动。
步长过小令每次迭代位移微乎其微,算法需极多轮才抵最小值,执行时间拉长,实盘高频重训时延迟会错过入场窗口。
复合函数内外层求导后相乘的环节常被漏掉内部导数,如(x+5)^2漏乘1,导致梯度错误进而更新公式偏离,建议对照文末演算视频校验。