
给考研学生讲线性回归代码这几年带下来我发现一件特别有意思的事能把线性回归从零手写明白的人后面学深度学习基本不用我怎么费心只会调库拿一个coef_的人学到反向传播十有八九要卡住。所以在我这个深度学习系列里线性回归这一课从来不讲快就是带着大家一行行写代码把每个数字的来路都看清。这篇笔记面向零基础读者目标只有一个你亲手训练出w和b并且能说清楚为什么它们是这个值。整个过程只用numpy不碰任何高级框架等你看懂这几十行朴素的代码再回头看sklearn、PyTorch这些东西理解速度会完全不一样。1. 为什么考研党和新手的第一堂课总绕不开线性回归1.1 笔试和复试里线性回归到底卡在哪个环节考研初试的数学里最小二乘求导是高频考点复试面试的时候老师很喜欢让你现场讲讲线性回归的原理如果涉及机器学习方向损失函数、梯度下降、正则化这些概念又全部挂在线性回归上面。可以说它既是数学题也是代码题更是“你懂不懂模型训练过程”的口试题。但很多考生的状态是公式会推导代码只会调包一旦被问到“梯度下降每一步在更新什么”就答不上来。1.2 线性回归代码和深度学习的网络骨架是同一套深度学习里一个全连接层本质上就是y wx b外面再套一个非线性激活函数。你用线性回归把“前向计算—损失—反向传播—参数更新”这四个环节跑通一遍后面看到神经网络训练代码时会觉得异常熟悉。因为神经网络的训练循环也是这套骨架只是中间的张量维度更复杂、梯度由自动求导算出来了而已。换句话说线性回归是唯一一个每个环节都能手算验证的模型你完全可以把答案算出来再对照代码结果这种“心里有底”的感觉对新手极其重要。1.3 从公式到代码其实就三步翻译数学上写一元线性回归就是y wx b。翻译成代码第一步把x放成一组数据第二步用w * x b算出预测值第三步算预测值和真实值之间的差距再按梯度方向修一下w和b。难点往往不在“求导”而在“把求和符号翻译成numpy的数组运算”。比如公式里对一个批次求和代码里常常就是np.mean(...)公式里的下标i代码里会被一个向量整体吃掉。这一步需要一点时间适应但一旦适应了你再看任何机器学习代码都能很快抓住主干。2. 动手写代码前先把数据 shape 和模型参数的关系想清楚2.1 输入 X 到底是什么形状这是小白最容易懵的地方很多新手拿到数据后第一件事就是直接跑模型从没想过X的形状代表什么。在numpy里一条数据样本常用一维数组表示一个批次的数据就是二维数组形状是(样本数 m, 特征数 n)。一元线性回归里特征数n 1所以理想状态下X应该长成(m, 1)但实际上我们会为了计算方便先拿一个长度为m的一维数组x做实验。这两种形状写出来的前向计算代码不一样一维时直接用w * x利用广播机制得到每个样本的预测值二维时用X w做的是标准的矩阵向量乘法。我见过大量报错都来自形状不匹配所以建议你写每行代码前都先想想当前的变量到底长什么样不确定就print(x.shape)看一眼这个习惯能救你很多次。2.2 参数 w 和偏置 b 为什么要分开维护书本上为了公式简洁会把w和b拼成一个增广向量同时在X左侧加一列 1。这个技巧在推导和调库时很漂亮但对新手不友好你很难看清到底在更新什么。所以我讲课时推荐先用最笨的办法——w是一个标量b是一个标量各自求梯度各自更新。等代码跑通了你自然能理解为什么正规化写法可以合并那时再追求公式的紧凑也不迟。分开维护的好处是每一步都可以print(w, b)看变化心理压力小很多。2.3 均方误差里的“1/2”到底哪来的损失函数最常见的写法是L (1/m) * Σ (y_pred - y)²但很多教材喜欢写成L (1/(2m)) * Σ (y_pred - y)²。多出的这个1/2纯粹是为了求导时消掉平方带来的因子 2让梯度公式更好看。要注意的是乘不乘1/2在数学上不影响最优点位置因为损失函数乘以一个正常数最小值点不变。但它会影响梯度的大小也就是说它对学习率相当敏感。同一套学习率下带1/2的版本梯度变小一倍收敛看起来更慢。你如果自己实现选哪种都行但一定要清楚自己用的是哪套否则换学习率时容易被梯度符号搞糊涂。3. 从零手写一元线性回归每一行 numpy 代码都要能说出依据3.1 造一份可以反复验证的数据在真正跑任何真实数据集之前先自己伪造一份数据。用什么真实参数生成就用什么参数去验收这比直接拿真实数据训练要踏实得多。我这里用true_w 3.0、true_b 7.0加一点高斯噪声模拟现实情况import numpy as np np.random.seed(42) m 200 x np.random.uniform(-3.0, 3.0, sizem) true_w, true_b 3.0, 7.0 y true_w * x true_b np.random.normal(0, 0.8, sizem)注意这里故意加了噪声。如果我们不加噪声这是纯粹解方程问题不需要什么机器学习加了噪声之后模型学到的w会接近 3.0 但不会完全等于 3.0因为噪声让数据不再完美落在一条直线上。这个细节理解了你就明白线性回归做的是“从带噪声的观测里恢复潜在规律”。3.2 训练循环的四步前向、损失、梯度、更新核心代码其实就是一个循环循环里反复做四件事w, b 0.0, 0.0 lr 0.05 epochs 500 for epoch in range(epochs): # 1. 前向计算 y_pred w * x b # 2. 计算损失 loss np.mean((y_pred - y) ** 2) # 3. 计算梯度这里用的是一阶导没带 1/2 grad_w np.mean(2 * (y_pred - y) * x) grad_b np.mean(2 * (y_pred - y)) # 4. 更新参数 w - lr * grad_w b - lr * grad_b if epoch % 50 0: print(fepoch {epoch}, loss {loss:.4f}, w {w:.4f}, b {b:.4f})每一行是什么意思y_pred是模型当前对所有样本的预测loss是这个批次上预测偏差的平方均值grad_w是所有样本的误差乘以对应x再取平均grad_b直接对误差取平均。之所以求平均而不是求和是为了让梯度大小不随样本数变化这样换一批数据时学习率不用重新调。更新参数那两行就是梯度下降的全部动作往负梯度方向走一小步步子大小由lr决定。3.3 判断训练成功不是看损失变成 0很多新手觉得训练成功就是损失变成 0这个想法在带噪声的数据上非常危险。你如果把噪声也拟合上损失确实可以压得很低但那叫过拟合。判断线性回归是否学好了应该看两点一是w和b是否接近生成数据时用的真实参数二是拟合出来的直线是否穿过了数据的“中心趋势”。比如上面这份数据训练结束后你大概会得到w ≈ 2.9~3.1b ≈ 6.8~7.2这就说明模型看到的是规律而不是噪声。想直观一点可以把x和y的散点图画出来再用训练好的参数画一条直线叠上去看直线是否合理。4. 代码写完先别急着跑三招验证自己写的是不是对的4.1 第一招用已知答案的构造数据反推这是最推荐的自验方法。用真实参数生成数据之后你的w和b大约是从0.0起步一路逼近真实值。跑完500轮如果结果和真实值差得很远先别怀疑噪声先怀疑自己的梯度写错了。常见的错误包括grad_w里忘了乘xgrad_b里对误差多乘了一个2或者更新参数时不小心用了而不是-。用已知答案反推的好处是一旦数值对不上你能立刻缩小 bug 范围而不需要对着真实数据猜模型哪里不对劲。4.2 第二招对比 sk-learn 的 LinearRegression自己没有标准答案时最方便的参照物就是sklearnfrom sklearn.linear_model import LinearRegression X x.reshape(-1, 1) model LinearRegression().fit(X, y) print(model.coef_, model.intercept_)如果自己手写的w和b跟model.coef_[0]、model.intercept_能对到小数点后两位以内基本可以认定代码逻辑正确。但这里有一个容易被坑的点sklearn的LinearRegression走的是最小二乘闭式解也就是后面要讲的正规方程并没有经过梯度下降。它的结果更接近“数学最优解”而你的梯度下降最后收敛到附近两者之间有一点小误差是正常的。把sklearn当“标准答案”可以不要把差异当成 bug。4.3 第三招检查损失曲线的形状如果你在训练里记录了每一轮的损失把损失画出来它应该是一个“陡峭下降后逐渐平坦”的曲线。正常情况不会出现明显的上升段曲线一旦上升说明学习率太大参数跨过了最优点如果曲线从头到尾都基本不动说明学习率太小或者数据没有归一化。还有一种情况是损失一开始就在nan这多半是梯度数值溢出后续章节会专门讲。损失曲线是你诊断模型的第一张化验单一定要养成记录并观察的习惯。5. 手写代码 vs sklearn从优化器差异看懂梯度下降的固执与偷懒5.1 sklearn 的 LinearRegression 其实没有“训练过程”这一点很多教程不会讲。sklearn里LinearRegression默认用的是最小二乘的闭式解也就是直接解正规方程w (XᵀX)⁻¹Xᵀy内部还会用 SVD 处理数值稳定性问题。它没有迭代、没有学习率、没有 epoch一锤子买卖。在数据量不大、特征维度不高的情况下这是最快的路径。而你自己手写的梯度下降则是一个迭代近似过程每走一步都“看一眼”当前梯度方向慢慢蹭过去。两种方法各有适用场景线性回归因为损失函数是凸函数梯度下降最终也能收敛到全局最优只是需要调参数而已。5.2 深度学习为什么必须用梯度下降那深度学习为啥不讲闭式解因为深层网络的损失函数不是凸函数没有可解的“置零公式”而且参数量动不动上百万。正规方程要算(XᵀX)⁻¹这个矩阵求逆在特征维度稍大一点就是灾难。梯度下降的做法就简单粗暴很多不看全局地形只看脚下坡往哪边倾斜沿着坡走一小步反复走。它的优点是能用于任意可导模型缺点是需要你操心学习率、初始化、归一化这些细节。理解了这一点你就知道线性回归手写代码训练的不只是一个模型而是在训练你对“迭代求解”的直觉。5.3 学习率到底是个什么量学习率在代码里就一个数字但它的影响是全局性的。我经常跟学生说学习率是“步子大小”步子太大你会跨过最优点在两边来回震荡甚至越走越远步子太小你走半天还在原地。对一份没有归一化的数据特征量级在[-3, 3]左右时从lr0.05起步是个合理选择如果特征量级到几千几万学习率得相应缩小到1e-4甚至更小否则梯度一乘上巨大特征值直接爆炸。新手最稳妥的办法是先记录损失用一组学习率做小规模对比看哪个方向让损失稳定下降再定最终值。不要迷信某个“万能学习率”它一定依赖你的数据量级、损失函数写法以及是否做归一化。我整理过一个简易的对照方便你感受差异学习率典型表现1.0容易震荡损失曲线跳跃甚至发散0.1下降快但有时在最优点附近来回摆0.05数据量级小的时候比较稳妥0.001大概率能收敛但可能需要很多轮才能看到效果6. 从一元到多元再到逻辑回归改动最小但思维要换一层6.1 多元线性回归把 x 从标量换成向量一元线性回归的代码写熟练之后扩到多元只是把x从一维数组变成二维矩阵其余逻辑几乎不动。前向变成y_pred X w b梯度变成grad_w (2 / m) * (X.T (y_pred - y)) grad_b (2 / m) * np.sum(y_pred - y)这里的X.T (y_pred - y)本质就是把“每个样本误差乘对应特征再求和”这件事用矩阵乘法一次性做完。很多新手第一眼看到会懵其实就是一元情形那个mean(2 * (y_pred - y) * x)的批量版本。理解这个对应关系后你会发现代码简洁了许多也更接近资料里常见的写法。这一步只需要把w从标量变成向量但思维上要接受“一个特征配一个权重”的模式后面看深度学习特征图之类的概念会顺畅很多。6.2 逻辑回归本质是在线性输出外面套一个 sigmoid如果你把线性回归的代码拿过来做分类最直接的办法是把输出z X w b送进sigmoid函数将结果压到 0 到 1 之间当成概率def sigmoid(z): return 1 / (1 np.exp(-z)) z X w b y_pred sigmoid(z) loss -np.mean(y * np.log(y_pred 1e-8) (1 - y) * np.log(1 - y_pred 1e-8))损失函数从均方误差换成交叉熵梯度则神奇地保持了“误差乘特征”的形式grad_w (X.T (y_pred - y)) / m grad_b np.mean(y_pred - y)也就是说逻辑回归与线性回归的代码结构高度相似区别只是前向多套一层 sigmoid、损失换成对数形式、梯度里少一个2。能看出这一层变化你就已经理解了两个经典模型而不是只会背两段代码。6.3 写到这一步深度学习就只剩“反向传播”一个台阶全连接层、激活函数、损失函数、训练循环——线性回归和逻辑回归的手写代码已经覆盖了这些东西。剩下的就是多层堆叠之后梯度不再能直接手推需要靠链式法则从后往前传也就是所谓的反向传播。理解这一步之前你手上已经有了“前向计算”和“损失函数”这两个最重要的感知后面看任何框架的loss.backward()都不会觉得它是什么黑魔法。很多考研党学到深度学习卡壳不是卡在神经网络本身而是卡在“模型到底是什么、训练到底在干嘛”这些前置认知上而这些前置认知恰好就是线性回归代码训练给你的。7. 实际踩过的几个坑学习率、特征归一化与损失值“卡死”7.1 学习率过大导致梯度爆炸参数直接变成 NaN这是新手最常见、也最容易慌的问题。现象是一开始训练损失先是变小然后突然变成nan或者一轮迭代之后w变成了一个天文数字。本质原因很简单学习率太大参数一步跨过头下一步梯度方向反过来又跨到另一边来回震荡幅度越来越大最终数值溢出。解决手段分两路快速止血是把学习率调小到1e-3甚至1e-5根治前提是检查数据量级把特征缩放到较小范围。记住一个经验当你发现某份数据需要特别小的学习率才能不炸时它通常是在提醒你先做归一化而不是继续硬调学习率。7.2 特征不归一化多元回归会被大尺度特征带偏一元回归只有一个特征归一化问题不明显多元回归里如果某个特征是“收入”量级到几万另一个特征是“年龄”量级只有几十梯度方向基本被大尺度特征主导模型会花大量轮次在调整那个大特征对应的权重小特征学得很慢。这不是模型笨而是梯度天然偏向“容易产生大误差的方向”。常见补救是把所有特征标准化为均值 0、方差 1代码很简单x_std (x - x.mean()) / x.std()标准化之后再训练学习率才具有普适性损失曲线也会漂亮很多。这个坑在深度学习里更常见不同层的特征尺度差异一旦控制不住训练就很难稳定这也是批归一化这类技巧出现的背景。7.3 损失值卡在同一个水平不动先怀疑 bug 再怀疑学习率如果损失下降一段后完全卡住甚至从第一轮开始就不动我的排查顺序是这样的先看梯度符号确认是不是和-写反了再看损失函数确认有没有把y和y_pred传反接着检查 shape确认X.T (y_pred - y)的维度对不对最后才调学习率。为什么要最后调学习率因为很多新手一旦发现不收敛就狂调参数却忽略是代码逻辑错了。逻辑错了调一万遍学习率也没用。我见过学生花了几个小时调学习率最后发现是因为把真实标签也标准化了预测目标完全变了形状。所以记住先确认代码对再优化参数。7.4 自己动手复现一遍胜过读十篇教程带考研学生的这几年我越来越觉得“看懂”和“会写”之间隔着一条巨大的鸿沟。你读十篇线性回归教程不如自己动手造一份数据、写一个循环、记录每条损失曲线再看着w和b从初始值慢慢逼近真实值。这个过程里你会踩一些坑会突然理解“哦原来广播是这个意思”会体会到调学习率像调火候一样的感觉——这些都是读教程给不了你的。如果你现在还在啃数学推导却迟迟不动手写代码我的建议是今天就打开编辑器把上面的代码亲手敲一遍然后换个模型多跑几轮很多模模糊糊的地方会一下子变清晰。