☰
线性回归与梯度下降——从零手写一个模型
2026/10/7 15:20:41 网站建设 项目流程
个人主页: > for_ever_love__ <
其他栏目: > 我想学python了 <

其他栏目: > iOS项目总结大全 <

其他栏目: > iOS UI <

文章目录

  • 线性回归与梯度下降——从零手写一个模型
    • 一、问题定义:找一条最贴合的直线
    • 二、损失函数:MSE
    • 三、手推梯度
    • 四、梯度下降:一步步走到谷底
    • 五、学习率:最重要的超参数
      • 加上学习率衰减
    • 六、批量梯度下降的三种形态
    • 七、多元线性回归:特征不止一个
    • 八、和 sklearn 对比验证
    • 九、常见坑与注意事项
    • 十、本篇小结

线性回归与梯度下降——从零手写一个模型

承上:上一篇《监督学习全景》我们知道回归用 MSE、数据集要三段划分。

本篇:本篇不用任何机器学习库,从零用 NumPy 实现线性回归与梯度下降。

启下:下一篇《逻辑回归与 Softmax》把回归输出压成概率,变成分类器。

学完这一节,你能动手做:

  1. 手推 MSE 的梯度并用数值法验证正确性
  2. 实现 BGD、SGD、Mini-batch 三种梯度下降并比较
  3. 说清为什么大模型求不出解析解、只能迭代训练

上一篇我们把监督学习的地图铺开了。这一篇,我们不用任何机器学习库,只靠 NumPy 从零训练一个模型。

为什么要这么做?因为线性回归是最简单的神经网络——一个没有隐藏层、没有激活函数的网络。你今天搞懂它的训练过程,后面理解几十亿参数的大模型只是"规模"上的差别,原理完全一样。

本篇你会亲手实现:损失函数、梯度计算、参数更新、批量训练、学习率调度,最后和 sklearn 对比验证。

一、问题定义:找一条最贴合的直线

假设我们有一批数据:房屋面积 (x) 和价格 (y)。想找一条直线ŷ = w·x + b,让预测值尽量接近真实值。

ŷ = w·x + b
  • w:权重(斜率),每平米值多少钱
  • b:偏置(截距),底价

目标:找到让损失最小的w和b。

先造点带噪声的数据:

importnumpyasnpimportmatplotlib.pyplotasplt np.random.seed(42)n=100X=2*np.random.rand(n,1)# 面积(单位:100㎡),0~2y=4+3*X+np.random.randn(n,1)*0.8# 真实关系 y = 3x + 4 + 噪声print("X 前5个:",X[:5].ravel().round(2))print("y 前5个:",y[:5].ravel().round(2))

真实参数是w=3, b=4。我们的模型能不能把它们"学"出来?

二、损失函数:MSE

defmse_loss(y_true,y_pred):returnnp.mean((y_pred-y_true)**2)defpredict(X,w,b):returnX @ w+b# 矩阵写法,支持任意特征数# 随便猜一组参数看看损失w=np.array([[0.0]])b=np.array([0.0])print("初始 loss:",round(float(mse_loss(y,predict(X,w,b))),4))

w=0, b=0时 loss 很大(约 40 多)。我们的任务就是让它降下去。

三、手推梯度

损失:

L = (1/n) · Σ (ŷᵢ - yᵢ)² ŷ = X·w + b

用链式法则:

∂L/∂ŷ = (2/n)(ŷ - y) ∂ŷ/∂w = X → ∂L/∂w = Xᵀ · ∂L/∂ŷ ∂ŷ/∂b = 1 → ∂L/∂b = Σ ∂L/∂ŷ

写成矩阵形式非常简洁:

defgradients(X,y,w,b):n=X.shape[0]y_pred=predict(X,w,b)err=y_pred-y# (n,1)dw=(X.T @ err)*2/n# (features,1)db=np.sum(err)*2/n# scalarloss=np.mean(err**2)returnloss,dw,db loss,dw,db=gradients(X,y,w,b)print("loss:",round(float(loss),4)," dw:",dw.ravel()," db:",round(float(db),4))

用数值梯度验证一下(这个习惯要养成):

defnumerical_check(X,y,w,b,eps=1e-6):_,dw_a,db_a=gradients(X,y,w,b)# 检查 dww1=w.copy();w1[0,0]+=eps w2=w.copy();w2[0,0]-=eps dw_n=(mse_loss(y,predict(X,w1,b))-mse_loss(y,predict(X,w2,b)))/(2*eps)# 检查 dbdb_n=(mse_loss(y,predict(X,w,b+eps))-mse_loss(y,predict(X,w,b-eps)))/(2*eps)print(f"dw 解析={dw_a[0,0]:.6f}数值={dw_n:.6f}")print(f"db 解析={db_a:.6f}数值={db_n:.6f}")numerical_check(X,y,w,b)

两者一致,说明梯度推导正确。

四、梯度下降:一步步走到谷底

核心就一行:w -= lr * dw。

deftrain(X,y,lr=0.1,epochs=200,verbose=True):n,d=X.shape w=np.zeros((d,1))b=np.zeros(1)history=[]forepochinrange(epochs):loss,dw,db=gradients(X,y,w,b)w-=lr*dw# 沿梯度反方向更新b-=lr*db history.append(float(loss))ifverboseand(epoch%40==0orepoch==epochs-1):print(f"epoch{epoch:3d}loss={loss:.4f}w={w.ravel().round(3)}b={b.round(3)}")returnw,b,history w_trained,b_trained,hist=train(X,y,lr=0.1,epochs=200)print("\n最终参数: w =",w_trained.ravel()," b =",b_trained)print("真实参数: w = [3.] b = [4.]")

跑完你会看到w ≈ 3.0、b ≈ 4.0——模型把数据的真实规律学出来了。

画出损失曲线:

plt.figure(figsize=(7,4))plt.plot(hist,lw=2)plt.xlabel("epoch");plt.ylabel("MSE loss")plt.title("Loss Curve")plt.grid(alpha=0.3);plt.tight_layout();plt.show()

一条平滑下降最后趋于平缓的曲线——这就是训练成功的样子。

五、学习率:最重要的超参数

学习率决定每步走多大。走小了慢,走大了发散。

fig,axes=plt.subplots(1,3,figsize=(15,4))forax,lrinzip(axes,[0.01,0.1,1.2]):try:_,_,h=train(X,y,lr=lr,epochs=100,verbose=False)ax.plot(h,color="tab:blue")ax.set_title(f"lr={lr}最终loss={h[-1]:.3f}")exceptExceptionase:ax.set_title(f"lr={lr}发散")ax.set_xlabel("epoch");ax.set_ylim(0,5)plt.tight_layout();plt.show()
学习率现象处理
太小(0.001)loss 缓慢下降,几百轮还没收敛调大,或增加 epoch
合适(0.1)快速下降后收敛✅
偏大(1.0+)震荡,loss 忽高忽低调小
太大(>2)发散,loss 变 inf/nan立刻调小

找学习率的实用技巧:从 1e-3 开始,每次 ×3 试(1e-3 → 3e-3 → 1e-2 → 3e-2 …),观察哪个量级 loss 降得最快又不炸。

加上学习率衰减

训练后期要"小心地靠近谷底",所以逐步减小步长:

deftrain_with_decay(X,y,lr=0.5,epochs=200,decay=0.99):n,d=X.shape w=np.zeros((d,1));b=np.zeros(1)history=[]cur_lr=lrforepochinrange(epochs):loss,dw,db=gradients(X,y,w,b)w-=cur_lr*dw b-=cur_lr*db cur_lr*=decay# 指数衰减history.append(float(loss))returnw,b,history _,_,h_decay=train_with_decay(X,y)print("带衰减最终 loss:",round(h_decay[-1],5))

六、批量梯度下降的三种形态

真实训练不会一次把所有数据喂进去,有三种粒度:

deftrain_minibatch(X,y,lr=0.1,epochs=50,batch_size=16,seed=0):"""Mini-batch 梯度下降:深度学习的事实标准"""rng=np.random.default_rng(seed)n,d=X.shape w=np.zeros((d,1));b=np.zeros(1)history=[]forepochinrange(epochs):idx=rng.permutation(n)# 每轮打乱顺序forstartinrange(0,n,batch_size):batch=idx[start:start+batch_size]Xb,yb=X[batch],y[batch]loss,dw,db=gradients(Xb,yb,w,b)w-=lr*dw b-=lr*db history.append(float(mse_loss(y,predict(X,w,b))))returnw,b,history w_mb,b_mb,h_mb=train_minibatch(X,y)print("Mini-batch 结果: w =",w_mb.ravel().round(3)," b =",b_mb.round(3))
方式每次用多少样本优点缺点
BGD 批量全部梯度准确、稳定收敛大 dataset 内存爆炸、慢
SGD 随机1 个更新快、能跳出局部最优梯度噪声大、震荡
Mini-batch16/32/64/128兼顾,可并行(GPU 友好)需调 batch size

深度学习几乎都用 mini-batch,因为 GPU 最擅长一次算一批矩阵乘法。大模型训练里 batch size 常常是 512 甚至更大。

七、多元线性回归:特征不止一个

真实场景有很多特征。代码完全不变,因为我们已经用了矩阵写法:

fromsklearn.datasetsimportfetch_california_housingfromsklearn.model_selectionimporttrain_test_splitfromsklearn.preprocessingimportStandardScaler data=fetch_california_housing()Xh,yh=data.data,data.target.reshape(-1,1)X_train,X_test,y_train,y_test=train_test_split(Xh,yh,test_size=0.2,random_state=42)# 必须标准化!否则不同量纲的特征会让训练崩溃scaler=StandardScaler()X_train_s=scaler.fit_transform(X_train)X_test_s=scaler.transform(X_test)w_h,b_h,_=train(X_train_s,y_train,lr=0.05,epochs=300,verbose=False)pred_test=predict(X_test_s,w_h,b_h)rmse=np.sqrt(mse_loss(y_test,pred_test))print(f"手写线性回归 测试集 RMSE ={rmse:.4f}")

八、和 sklearn 对比验证

写对了没有?拿官方实现对比:

fromsklearn.linear_modelimportLinearRegression,SGDRegressorfromsklearn.metricsimportmean_squared_error,r2_score sk=LinearRegression().fit(X_train_s,y_train)sk_pred=sk.predict(X_test_s)print("sklearn LinearRegression RMSE:",round(np.sqrt(mean_squared_error(y_test,sk_pred)),4))print("手写梯度下降 RMSE:",round(rmse,4))print("R² (sklearn):",round(r2_score(y_test,sk_pred),4))print("R² (手写) :",round(r2_score(y_test,pred_test),4))print("\n权重最大差异:",np.abs(sk.coef_.ravel()-w_h.ravel()).max())

两者结果几乎一致(差异在 1e-3 量级)。说明我们的实现是正确的。

有意思的是,LinearRegression用的是正规方程(解析解):

# 正规方程:w = (XᵀX)⁻¹ XᵀyX_aug=np.c_[np.ones(len(X_train_s)),X_train_s]# 加一列 1 表示偏置w_closed=np.linalg.pinv(X_aug.T @ X_aug)@ X_aug.T @ y_trainprint("正规方程 b =",w_closed[0].round(4)," sklearn b =",round(sk.intercept_[0],4))
解法原理适用
正规方程一步解析求解(XᵀX)⁻¹Xᵀy特征 < 1万,小数据集
梯度下降迭代逼近任意规模,深度学习的唯一选择

大模型有几十亿参数,XᵀX这个矩阵根本存不下(几十亿 × 几十亿),所以只能用梯度下降。这就是为什么你只听过"训练大模型"而没听过"解方程求大模型"。

九、常见坑与注意事项

坑现象解决
忘了标准化loss 不降或 NaN先StandardScaler
学习率太大loss 变 inf/nan降到 1/10 重试
特征里有字符串UFuncTypeError先编码(One-Hot)
没加偏置 b直线必须过原点,拟合差记得加 bias 项
只在训练集看指标上线效果差一定要看测试集
形状搞错(100,1) vs (100,)统一用二维(n, d)

形状提示:np.mean(err**2)返回标量,而err是(n,1)。矩阵乘法时注意X.T @ err得到(d,1),正好和w同形。

十、本篇小结

  1. 线性回归ŷ = Xw + b就是最简单的神经网络——没有隐藏层、没有激活函数。
  2. 手动推导了 MSE 的梯度∂L/∂w = 2Xᵀ(ŷ-y)/n,并用数值梯度验证正确。
  3. 梯度下降w -= lr·dw是训练的核心动作;学习率是最重要的超参数,可用衰减策略精调。
  4. Mini-batch是深度学习的事实标准(BGD/SGD/Mini-batch 三选一)。
  5. 正规方程 vs 梯度下降:大模型参数太多,求不出解析解,只能迭代——这就是梯度下降不可替代的原因。
  6. 手写实现与 sklearn 结果一致(RMSE 差异 < 1e-3),验证了正确性。

下一篇逻辑回归与 Softmax:把线性回归的输出"压"成概率,就变成了分类器。我们会从零实现 Softmax 回归,并揭示一个关键事实——大模型最后一层输出下一个词的概率,用的正是 Softmax。

本篇是《大模型开发从 0 到 1》专栏第 18 篇,阶段 3「机器学习基础」第 2 篇。专栏文章按「分类专栏」归类,顺序学习体验最佳。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询