
标前七期我们攒齐了半套装备第2期注意力是矩阵乘法第3期softmax第4期交叉熵第5期LayerNorm第6期反向传播第7期讲学习率。今天把第7期留的尾巴收了学习率调对之后优化器本身还能不能再快先摆个靶子。Rosenbrock函数长得像一根香蕉谷底又细又长是优化器的经典考场。起点(-1, 1)函数值4.0目标是走到谷底的(1, 1)。SGD学习率0.001跑2000步函数值还剩7.42e-02。能走但慢。慢在哪香蕉谷底两个方向的坡度差了上百倍。学习率按最陡的方向设到了平缓方向就只能一步步挪。这时候动量(momentum)登场。想法很直观梯度方向基本一致的时候把历史梯度累积起来越走越快方向来回震荡的时候正负抵消自动减速。像推着一个小球滚下山有惯性。代码不长核心就三行importnumpyasnpdefrosenbrock(x):return(1-x[0])**2100*(x[1]-x[0]**2)**2defgrad(x):g0-2*(1-x[0])-400*x[0]*(x[1]-x[0]**2)g1200*(x[1]-x[0]**2)returnnp.array([g0,g1])defmomentum(x0,lr0.001,beta0.9,steps2000):xx0.copy()vnp.zeros(2)# 速度累积历史梯度for_inrange(steps):vbeta*vgrad(x)x-lr*vreturnrosenbrock(x)beta0.9是惯例相当于记住最近10步左右的梯度方向。Adam是两步再往前动量管往哪走再加一个每步走多大。它给每个参数单独算学习率梯度一直很大的方向步子自动调小梯度稀稀拉拉的方向步子自动放大。用梯度平方的滑动平均来估计每个方向的忙碌程度。另外有个容易忽略的细节叫偏差修正刚开始滑动平均都是0前几步会被低估除以(1-beta^t)把它修正回来。defadam(x0,lr0.01,steps2000):xx0.copy()mnp.zeros(2)# 一阶矩梯度的滑动平均(动量)vnp.zeros(2)# 二阶矩梯度平方的滑动平均(自适应步长)b1,b20.9,0.999fortinrange(1,steps1):ggrad(x)mb1*m(1-b1)*g vb2*v(1-b2)*g*g mhm/(1-b1**t)# 偏差修正vhv/(1-b2**t)x-lr*mh/(np.sqrt(vh)1e-8)returnrosenbrock(x)环境只要numpypip install numpy就行。四组实验我机器上真实跑出来的数字起点都是(-1, 1)都跑2000步SGD学习率0.0017.42e-02Momentum学习率0.0018.97e-09Adam学习率0.0011.23e-01Adam学习率0.011.74e-19看出门道了吗。动量是质变同样学习率Momentum比SGD快了7个数量级。Adam在学习率0.01时最猛1.74e-19基本贴着谷底。但注意第三行Adam在学习率0.001时只走到1.23e-01还不如SGD。这就是今天最大的坑Adam不是无脑强它照样吃学习率学习率没设对再花哨的自适应也救不了。第7期讲学习率不是白讲的优化器换得再勤学习率这一课都躲不掉。为什么Adam在小学习率下反而拉胯它的自适应步长是按历史梯度平方来缩放的Rosenbrock这种狭长山谷里二阶矩估计会把本就不大的步子压得更小2000步根本走不到谷底。调大到0.01一阶矩的动量效果才放出来。所以Adam的自适应不是不用调是调起来和SGD不是一个手感。第二个坑Adam快但不总是最好。小模型、数据干净、调参时间充足的时候SGD动量调好了泛化经常更好。Adam是开箱即用的选择SGD是愿意花时间调的选择。面试被问为什么默认用Adam别只背收敛快把这句也带上。收藏这篇下次有人问你Adam和SGD的区别直接把这四组数字甩出来比背公式管用。下期预告第9期讲正则化——L2、Dropout、早停三个防止过拟合的手段NumPy手写看效果。这个系列在专栏《ML数学基础》里连着更新从矩阵乘法一路写到优化器关注后专栏里能连着看。讨论你训模型时默认用Adam还是SGD有没有遇到过Adam还不如SGD的时候最后怎么解决的—— Cyrus