欠拟合、过拟合和正则化
欠拟合(Underfitting)
在机器学习中,欠拟合是指模型在训练数据上就表现得很差,更别说在测试数据上了。模型没有学到数据的模式和规律,可能是因为模型太简单,或者训练数据不足
解决方法
- 增加模型复杂度:如果模型太简单(比如用线性模型去解决复杂问题),就换一个更复杂的模型(比如用深度神经网络)
- 增加训练时间:让模型多学习一会儿,增加迭代次数,确保它有足够的时间去捕捉数据的模式
- 增加特征:给模型提供更多有用的信息,使用多项式或者增加参数
- 减少正则化:如果正则化太强,可能会限制模型的学习能力,可以适当放松约束
- 获取更多数据:如果数据量太少,模型可能学不到足够的模式,增加数据集大小
过拟合(Overfitting)
在机器学习中,过拟合是指模型在训练数据上表现得非常好,但是在新的、没见过的数据(测试数据)上表现很差。换句话说,模型过于“记住”了训练数据的细节和噪声,而没有学到数据的通用规律,一旦跳出训练集,对实际的数据预测效果很差
- 增加数据量:模型有了更多样化的训练数据,就不容易只记住某一类数据的细节。
- 数据增强:如果数据不够多,可以对现有数据做一些变化(比如旋转、缩放图片)
- 简化模型:如果模型太复杂(比如用了很深的神经网络),就容易记住细节。可以减少模型的参数或层数
- 正则化:给模型加一些“约束”,比如 L1/L2 正则化(有点像罚款),防止模型过于关注某些特定的特征,对应一些参数减小其数据量
- Dropout(随机丢弃):在训练神经网络时,随机“关掉”一些神经元,防止模型过于依赖某些特定的神经元
- 早停(Early Stopping):在训练过程中,如果发现模型在验证数据上的表现不再提升,就停止训练,避免模型继续“死记硬背”训练数据
正则化
在机器学习中,正则化通过对模型的复杂度施加“惩罚”,限制模型的参数(比如权重)变得过大,从而让模型更简单、更通用,避免它过于“记住”训练数据的细节和噪声
如何进行正则化
正则化的核心思想是在模型的损失函数(Loss Function)中加入一个“惩罚项”(Penalty Term)。损失函数是用来衡量模型预测结果和真实值之间差距的,而惩罚项的作用是限制模型的复杂度
模型训练时有两个目标:
- 尽量让预测解决接近真实值(减少误差)
- 尽量让模型简单,不要过于复杂(通过惩罚项实现)
常见的正则化方法有 L1 正则化和 L2 正则化
L1 正则化
- 惩罚方式:对模型参数(权重
)的绝对值之和进行惩罚 - 数学表达式:在损失函数中加上一个项:
,其中 是模型的权重, 是惩罚的强度 - 效果:L1 正则化会将一些不重要的权重变得接近于 0,相当于砍掉一些特征的影响
- 适用场景:模型参数很多,但是不知道要砍掉那些不必要的参数
原始损失函数(均方误差):
其中
加入 L1 正则化后的损失函数:
使用 Python 来展示一下线性回归的 L1 正则化:
python
import numpy as np
import matplotlib.pyplot as plt
# matplotlib.rcParams['font.sans-serif'] = ['Arial Unicode MS'] # macOS 可选
# 设置随机种子以保证结果可重复
np.random.seed(42)
# 1. 生成模拟数据
def generate_data(n_samples=100):
"""
生成一个简单的线性数据集:y = 2 * x + 1 + 噪声
"""
X = np.random.rand(n_samples, 1) * 10 # 随机生成 0 到 10 的 x 值
y = 2 * X + 1 + np.random.normal(0, 1, (n_samples, 1)) # 线性关系 + 噪声
return X, y
X, y = generate_data()
# 2. 定义带 L1 正则化的线性回归类
class LassoRegression:
def __init__(self, learning_rate=0.01, lambda_reg=0.1, n_iterations=1000):
self.learning_rate = learning_rate # 学习率
self.lambda_reg = lambda_reg # L1 正则化惩罚系数
self.n_iterations = n_iterations # 迭代次数
self.weights = None # 权重
self.bias = None # 偏置
self.loss_history = [] # 记录损失变化
def fit(self, X, y):
"""
训练模型,使用梯度下降法优化参数
"""
n_samples, n_features = X.shape
# 初始化权重和偏置为 0
self.weights = np.zeros((n_features, 1))
self.bias = 0
# 梯度下降迭代
for _ in range(self.n_iterations):
# 预测值
y_pred = np.dot(X, self.weights) + self.bias
# 计算损失(均方误差 + L1 正则化项)
mse_loss = (1 / n_samples) * np.sum((y_pred - y) ** 2)
l1_loss = self.lambda_reg * np.sum(np.abs(self.weights))
total_loss = mse_loss + l1_loss
self.loss_history.append(total_loss)
# 计算梯度
dw = (2 / n_samples) * np.dot(X.T, (y_pred - y)) # 均方误差部分的梯度
dw += self.lambda_reg * np.sign(self.weights) # L1 正则化部分的梯度
db = (2 / n_samples) * np.sum(y_pred - y) # 偏置的梯度
# 更新参数
self.weights -= self.learning_rate * dw
self.bias -= self.learning_rate * db
def predict(self, X):
"""
使用训练好的模型进行预测
"""
return np.dot(X, self.weights) + self.bias
# 3. 训练模型
model = LassoRegression(learning_rate=0.01, lambda_reg=0.1, n_iterations=1000)
model.fit(X, y)
# 4. 预测并可视化结果
y_pred = model.predict(X)
plt.scatter(X, y, color='blue', label='数据点', alpha=0.5)
plt.plot(X, y_pred, color='red', label='拟合直线')
plt.xlabel('X')
plt.ylabel('y')
plt.title('L1 正则化线性回归结果')
plt.legend()
plt.show()
# 5. 绘制损失变化曲线
plt.plot(model.loss_history, color='green', label='损失变化')
plt.xlabel('迭代次数')
plt.ylabel('损失值')
plt.title('训练过程中的损失变化')
plt.legend()
plt.show()
# 打印最终的权重和偏置
print("训练后的权重:", model.weights)
print("训练后的偏置:", model.bias)
除了损失函数和梯度计算的变化,正则化对线性回归的其他部分几乎没有影响:
python
# 计算损失(均方误差 + L1 正则化项)
mse_loss = (1 / n_samples) * np.sum((y_pred - y) ** 2)
l1_loss = self.lambda_reg * np.sum(np.abs(self.weights))
total_loss = mse_loss + l1_loss
self.loss_history.append(total_loss)
在这里加上了正则化的损失
下面就要开始计算梯度了
python
# 计算梯度
dw = (2 / n_samples) * np.dot(X.T, (y_pred - y)) # 均方误差部分的梯度
dw += self.lambda_reg * np.sign(self.weights) # L1 正则化部分的梯度
db = (2 / n_samples) * np.sum(y_pred - y) # 偏置的梯度
其中,dw
的梯度由原本的均方误差和 L1 部分的梯度共同组成:
dw = MSE部分 + lambda_reg * np.sign(weights)
最后更新参数:
python
# 更新参数
self.weights -= self.learning_rate * dw
self.bias -= self.learning_rate * db