机器学习小白实战【正则化——破解“过拟合”与“欠拟合”】
机器学习小白实战【正则化——破解“过拟合”与“欠拟合”】
前言
学完线性回归和逻辑回归后,我们在前面的实验中经常遇到一个问题:只要给模型多加几个特征(比如把 x 变成 x², x³),模型就会疯狂扭曲,非要强行穿过每一个数据点。
这种“死记硬背”的现象在机器学习里叫 过拟合(Overfitting)。
为了解决这个问题,我们需要引入正则化(Regularization)。
今天我用一个对比实验,来看看正则化到底是怎么把疯狂扭曲的曲线拉直的,以及如果正则化力度太大,又会出现什么后果。
一、工具准备与环境配置
还是老规矩,不用复杂的框架,用 numpy 算梯度,用 matplotlib 画图。
import numpy as np
import matplotlib.pyplot as plt
# 解决中文乱码
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**i for i in range(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^2 J(w,b)=2m1i=1∑m(h(i)−y(i))2+2mλj=1∑nwj2
梯度下降:在计算 w 的梯度时,加上 (λ / m) * w。这样每次更新时,模型都会强行把 w 往 0 的方向拉一点点。
def compute_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)
return mse + reg_term
def gradient_descent_reg(X, y, w_init, b_init, alpha, iterations, lambda_reg):
w = w_init
b = b_init
m = len(y)
for i in range(iterations):
h = np.dot(X, w) + b
# 梯度计算多了一项 (lambda/m)*w
grad_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_b
return w, b
四、训练模型
我设定了 3 个不同的正则化系数(λ) 进行训练:
模型 A(λ = 0):没有紧箍咒,模拟完全自由发挥。
模型 B(λ = 1):轻度惩罚,模拟恰到好处的约束。
模型 C(λ = 10):重度惩罚,模拟过度压制。
iterations = 2000
alpha = 0.05
w_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**i for i in range(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)所做的,是在模型计算“损失(错误率)”的同时,额外强行加上一笔“罚单”。这笔罚单会惩罚模型中那些数值巨大的权重。为了少交罚款,模型就必须把权重缩得更小、更克制。
一句话总结:正则化是用一点点“训练集上的误差”作为代价,换取了模型在真实世界中更强的“泛化能力”。
加得恰到好处,模型就能做到“坚守全局规律,无视局部噪音”,画出一条稳定笔直的线;加得太多,模型就会被吓破胆,变成一条毫无预测能力的直线(欠拟合)。所以,找到一个适中的“惩罚力度 λ”,是正则化实战中最核心的艺术。
更多推荐
所有评论(0)