机器学习小白实战【正则化——破解“过拟合”与“欠拟合”】

📅 2026/7/29 9:53:34 👁️ 阅读次数 📝 编程学习
机器学习小白实战【正则化——破解“过拟合”与“欠拟合”】

机器学习小白实战【正则化——破解“过拟合”与“欠拟合”】

前言

学完线性回归逻辑回归后,我们在前面的实验中经常遇到一个问题:只要给模型多加几个特征(比如把 x 变成 x², x³),模型就会疯狂扭曲,非要强行穿过每一个数据点。
这种“死记硬背”的现象在机器学习里叫过拟合(Overfitting)。
为了解决这个问题,我们需要引入正则化(Regularization)。
今天我用一个对比实验,来看看正则化到底是怎么把疯狂扭曲的曲线拉直的,以及如果正则化力度太大,又会出现什么后果。

一、工具准备与环境配置

还是老规矩,不用复杂的框架,用numpy算梯度,用matplotlib画图。

importnumpyasnpimportmatplotlib.pyplotasplt# 解决中文乱码plt.rcParams['font.sans-serif']=['SimHei']plt.rcParams['axes.unicode_minus']=False

二、生成模拟数据

为了让过拟合现象足够明显,我们生成了 30 个 样本(样本越少,模型越容易死记硬背)。
真实规律是:y = 0.5x² + x + 2,外加一点随机噪音。
但是,为了给模型挖坑,我并没有只用 x 去训练,而是构造了 8 个特征:
X = [x, x², x³, x⁴, x⁵, x⁶, x⁷, x⁸]。
有了这么多高次方特征,模型很容易画出像蛇一样扭曲的曲线。

np.random.seed(42)m=30# 样本少,容易过拟合x=np.random.uniform(-3,3,m)y=0.5*x**2+x+2+np.random.normal(0,0.5,m)# 构造 8 个高次特征(挖坑)X=np.column_stack([x**iforiinrange(1,9)])# 归一化(防止高次数字太大了炸掉)X_mean=np.mean(X,axis=0)X_std=np.std(X,axis=0)X_norm=(X-X_mean)/X_std

三、定义核心算法

这里的重点在于改动成本函数和梯度计算的公式:
成本函数:多了一项(λ / 2m) * Σ(w²)这就是对权重 w 收取的“罚单”。
J(w,b)=12m∑i=1m(h(i)−y(i))2+λ2m∑j=1nwj2 J(w, b) = \frac{1}{2m} \sum_{i=1}^{m} (h^{(i)} - y^{(i)})^2 + \frac{\lambda}{2m} \sum_{j=1}^{n} w_j^2J(w,b)=2m1i=1m(h(i)y(i))2+2mλj=1nwj2
梯度下降:在计算 w 的梯度时,加上(λ / m) * w。这样每次更新时,模型都会强行把 w 往 0 的方向拉一点点。

defcompute_cost_reg(X,y,w,b,lambda_reg):m=len(y)h=np.dot(X,w)+b mse=(1/(2*m))*np.sum((h-y)**2)# 加上对 w 的惩罚项(不惩罚 b)reg_term=(lambda_reg/(2*m))*np.sum(w**2)returnmse+reg_termdefgradient_descent_reg(X,y,w_init,b_init,alpha,iterations,lambda_reg):w=w_init b=b_init m=len(y)foriinrange(iterations):h=np.dot(X,w)+b# 梯度计算多了一项 (lambda/m)*wgrad_w=(1/m)*np.dot(X.T,(h-y))+(lambda_reg/m)*w grad_b=(1/m)*np.sum(h-y)w=w-alpha*grad_w b=b-alpha*grad_breturnw,b

四、训练模型

我设定了 3 个不同的正则化系数(λ) 进行训练:
模型 A(λ = 0):没有紧箍咒,模拟完全自由发挥。
模型 B(λ = 1):轻度惩罚,模拟恰到好处的约束。
模型 C(λ = 10):重度惩罚,模拟过度压制。

iterations=2000alpha=0.05w_A,b_A=gradient_descent_reg(X_norm,y,np.zeros(8),0,alpha,iterations,lambda_reg=0)w_B,b_B=gradient_descent_reg(X_norm,y,np.zeros(8),0,alpha,iterations,lambda_reg=1)w_C,b_C=gradient_descent_reg(X_norm,y,np.zeros(8),0,alpha,iterations,lambda_reg=10)print(f"模型A的权重:{np.round(w_A,2)}")print(f"模型B的权重:{np.round(w_B,2)}")print(f"模型C的权重:{np.round(w_C,2)}")

五、结果可视化

plt.figure(figsize=(15,5))x_test=np.linspace(-3,3,100)X_test=np.column_stack([x_test**iforiinrange(1,9)])X_test_norm=(X_test-X_mean)/X_std# 1. 无正则化(过拟合)plt.subplot(1,3,1)plt.scatter(x,y,color='blue',alpha=0.6)y_A=np.dot(X_test_norm,w_A)+b_A plt.plot(x_test,y_A,color='red',linewidth=2)plt.title('λ=0:无正则化 (过拟合)')plt.grid(True)# 2. 轻度正则化(最佳模型)plt.subplot(1,3,2)plt.scatter(x,y,color='blue',alpha=0.6)y_B=np.dot(X_test_norm,w_B)+b_B plt.plot(x_test,y_B,color='green',linewidth=2)plt.title(' λ=1:轻度正则化 (完美)')plt.grid(True)# 3. 重度正则化(欠拟合)plt.subplot(1,3,3)plt.scatter(x,y,color='blue',alpha=0.6)y_C=np.dot(X_test_norm,w_C)+b_C plt.plot(x_test,y_C,color='orange',linewidth=2)plt.title('λ=10:重度正则化 (欠拟合)')plt.grid(True)plt.tight_layout()plt.show()

运行结果如下图所示:

通过这三张图的对比,我们可以非常直观地看清正则化的威力:

过拟合(左图):无正则化时,红线像蛇一样扭曲。模型把训练集的每一个微小波动(甚至噪音)都死死记住了,导致高次项的权重非常巨大。

完美拟合(中图):加入轻度正则化(λ=1)后,红线变得平滑、自然。模型捕捉到了数据的核心趋势,同时忽略掉了噪音。这是因为正则化强制将那些不必要的权重缩小了。

欠拟合(右图):正则化力度过大(λ=10)时,模型的权重被压缩到了近乎 0,导致它连基本的趋势都抓不住了,变成了一条过度平缓的曲线。

总结

正则化,本质上就是给模型套上一道“紧箍咒”。
在训练过程中,普通的模型会为了讨好训练集里的每一个数据点——尤其是那些极端的“异常值”——而过度扭曲自己的形态,导致原本应该平滑的规律变成疯狂波动的过山车(这就是过拟合)。这种模型虽然能在训练集上拿满分,但在面对未知的新数据时往往表现得一塌糊涂。
正则化(L2)所做的,是在模型计算“损失(错误率)”的同时,额外强行加上一笔“罚单”。这笔罚单会惩罚模型中那些数值巨大的权重。为了少交罚款,模型就必须把权重缩得更小、更克制。
一句话总结:正则化是用一点点“训练集上的误差”作为代价,换取了模型在真实世界中更强的“泛化能力”。
加得恰到好处,模型就能做到“坚守全局规律,无视局部噪音”,画出一条稳定笔直的线;加得太多,模型就会被吓破胆,变成一条毫无预测能力的直线(欠拟合)。所以,找到一个适中的“惩罚力度 λ”,是正则化实战中最核心的艺术。