深度学习 — 过拟合与欠拟合
文章目录
- 深度学习 --- 过拟合与欠拟合
- 一.概念
- 1.1 过拟合
- 1.2 欠拟合
- 1.3 判断方式
- 二,解决欠拟合
- 三,解决过拟合
- 3.1 L2正则化
- 3.1.1 定义以及作用
- 3.1.2 代码
- 3.2 L1正则化
- 3.3 L1与L2对比
- 3.4 Dropout
- 示例
- 3.5 数据增强
- 3.5.1 图片缩放
- 3.5.2 随机裁剪
- 3.5.3 随机水平翻转
- 3.5.4 调整图片颜色
- 3.5.5 随机旋转
- 3.5.6 图片转Tensor
- 3.5.7 Tensor转图片
- 3.5.8 归一化
- 3.5.9 数据增强整合
一.概念
1.1 过拟合
过拟合是指模型对训练数据拟合能力很强并表现很好,但在测试数据上表现较差。
过拟合常见原因有:
- 数据量不足:当训练数据较少时,模型可能会过度学习数据中的噪声和细节。
- 模型太复杂:如果模型很复杂,也会过度学习训练数据中的细节和噪声。
- 正则化强度不足:如果正则化强度不足,可能会导致模型过度学习训练数据中的细节和噪声。
1.2 欠拟合
欠拟合是由于模型学习能力不足,无法充分捕捉数据中的复杂关系。
1.3 判断方式
过拟合
训练误差低,但验证时误差高。模型在训练数据上表现很好,但在验证数据上表现不佳,说明模型可能过度拟合了训练数据中的噪声或特定模式。
欠拟合
训练误差和测试误差都高。模型在训练数据和测试数据上的表现都不好,说明模型可能太简单,无法捕捉到数据中的复杂模式。
二,解决欠拟合
欠拟合的解决思路比较直接:
- 增加模型复杂度:引入更多的参数、增加神经网络的层数或节点数量,使模型能够捕捉到数据中的复杂模式。
- 增加特征:通过特征工程添加更多有意义的特征,使模型能够更好地理解数据。
- 减少正则化强度:适当减小 L1、L2 正则化强度,允许模型有更多自由度来拟合数据。
- 训练更长时间:如果是因为训练不足导致的欠拟合,可以增加训练的轮数或时间.
三,解决过拟合
避免模型参数过大是防止过拟合的关键步骤之一。
模型的复杂度主要由权重w决定,而不是偏置b。偏置只是对模型输出的平移,不会导致模型过度拟合数据。
怎么控制权重w,使w在比较小的范围内?
考虑损失函数,损失函数的目的是使预测值与真实值无限接近,如果在原来的损失函数上添加一个非0的变量
L1(y^,y)=L(y^,y)+f(w)L_1(\hat{y},y) = L(\hat{y},y) + f(w) L1(y^,y)=L(y^,y)+f(w)
其中f(w)f(w)f(w)是关于权重w的函数,f(w)>0f(w)>0f(w)>0
要使L1变小,就要使L变小的同时,也要使f(w)f(w)f(w)变小。从而控制权重w在较小的范围内。
3.1 L2正则化
L2 正则化通过在损失函数中添加权重参数的平方和来实现,目标是惩罚过大的参数值。
3.1.1 定义以及作用
维度 | 内容 | 数学表达式 | 解释与作用 |
---|---|---|---|
原始损失函数 | 模型未加正则化的损失函数(如 MSE、交叉熵) | L(θ)L(\theta)L(θ) | 仅衡量模型在训练数据上的误差。 |
L2 正则化项 | 所有权重参数的平方和 | 12∑iθi2\frac{1}{2} \sum_i \theta_i^221∑iθi2 | 惩罚大权重,防止模型复杂度过高。 |
总损失函数 | 原始损失 + L2 正则化项 | Ltotal(θ)=L(θ)+λ2∑iθi2L_{\text{total}}(\theta) = L(\theta) + \frac{\lambda}{2} \sum_i \theta_i^2Ltotal(θ)=L(θ)+2λ∑iθi2 | 加入惩罚项,平衡拟合能力与复杂度。 |
梯度更新规则 | 参数更新时考虑原始梯度 + L2 项的梯度 | θt+1=θt−η(∇L(θt)+λθt)\theta_{t+1} = \theta_t - \eta \left( \nabla L(\theta_t) + \lambda \theta_t \right)θt+1=θt−η(∇L(θt)+λθt) | 每次更新都“衰减”参数(乘以 1−ηλ1 - \eta \lambda1−ηλ),防止权重过大。 |
12\frac{1}{2}21 的作用 | 简化梯度计算 | ∂∂θi(12θi2)=θi\frac{\partial}{\partial \theta_i} \left( \frac{1}{2} \theta_i^2 \right) = \theta_i∂θi∂(21θi2)=θi | 消去系数 2,使梯度更新公式更简洁(避免 2λθi2\lambda \theta_i2λθi)。 |
防止过拟合 | 抑制权重过大,降低模型对训练噪声的敏感性 | - | 权重越小,模型对输入扰动越不敏感,泛化能力增强。 |
限制模型复杂度 | 强制权重接近 0,避免过拟合 | - | 通过惩罚大权重,减少模型自由度,降低 VC 维。 |
提高泛化能力 | 在训练集和测试集上表现更均衡 | - | 正则化项使模型更关注数据的真实规律,而非噪声。 |
平滑权重分布 | 权重逐渐缩小但不直接为 0 | - | 保留所有特征贡献,避免稀疏性(与 L1 不同),使模型更平滑。 |
3.1.2 代码
import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt# 设置随机种子以保证可重复性
torch.manual_seed(42)# 生成随机数据
n_samples = 100
n_features = 20
X = torch.randn(n_samples, n_features) # 输入数据
y = torch.randn(n_samples, 1) # 目标值# 定义一个简单的全连接神经网络
class SimpleNet(nn.Module):def __init__(self):super(SimpleNet, self).__init__()self.fc1 = nn.Linear(n_features, 50)self.fc2 = nn.Linear(50, 1)def forward(self, x):x = torch.relu(self.fc1(x))return self.fc2(x)# 训练函数
def train_model(use_l2=False, weight_decay=0.01, n_epochs=100):# 初始化模型model = SimpleNet()criterion = nn.MSELoss() # 损失函数(均方误差)# 选择优化器if use_l2:optimizer = optim.SGD(model.parameters(), lr=0.01, weight_decay=weight_decay) # 使用 L2 正则化else:optimizer = optim.SGD(model.parameters(), lr=0.01) # 不使用 L2 正则化# 记录训练损失train_losses = []# 训练过程for epoch in range(n_epochs):optimizer.zero_grad() # 清空梯度outputs = model(X) # 前向传播loss = criterion(outputs, y) # 计算损失loss.backward() # 反向传播optimizer.step() # 更新参数train_losses.append(loss.item()) # 记录损失if (epoch + 1) % 10 == 0:print(f'Epoch [{epoch + 1}/{n_epochs}], Loss: {loss.item():.4f}')return train_losses# 训练并比较两种模型
train_losses_no_l2 = train_model(use_l2=False) # 不使用 L2 正则化
train_losses_with_l2 = train_model(use_l2=True, weight_decay=0.01) # 使用 L2 正则化# 绘制训练损失曲线
plt.plot(train_losses_no_l2, label='Without L2 Regularization')
plt.plot(train_losses_with_l2, label='With L2 Regularization')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss: L2 Regularization vs No Regularization')
plt.legend()
plt.show()
3.2 L1正则化
设模型的原始损失函数为 L(θ)L(\theta)L(θ),其中 θ\thetaθ 表示模型权重参数,则加入 L1 正则化后的损失函数表示为:
Ltotal(θ)=L(θ)+λ∑i∣θi∣L_{\text{total}}(\theta) = L(\theta) + \lambda \sum_{i} |\theta_i| Ltotal(θ)=L(θ)+λi∑∣θi∣
- 梯度更新
在 L1 正则化下,梯度更新时的公式是:
θt+1=θt−η(∇L(θt)+λ⋅sign(θt))\theta_{t+1} = \theta_t - \eta \left( \nabla L(\theta_t) + \lambda \cdot \text{sign}(\theta_t) \right) θt+1=θt−η(∇L(θt)+λ⋅sign(θt)) - 作用:
-
稀疏性:L1 正则化的一个显著特性是它会促使许多权重参数变为 零。这是因为 L1 正则化倾向于将权重绝对值缩小到零,使得模型只保留对结果最重要的特征,而将其他不相关的特征权重设为零,从而实现 特征选择 的功能。
-
防止过拟合:通过限制权重的绝对值,L1 正则化减少了模型的复杂度,使其不容易过拟合训练数据。相比于 L2 正则化,L1 正则化更倾向于将某些权重完全移除,而不是减小它们的值。
-
简化模型:由于 L1 正则化会将一些权重变为零,因此模型最终会变得更加简单,仅依赖于少数重要特征。这对于高维度数据特别有用,尤其是在特征数量远多于样本数量的情况下。
-
特征选择:因为 L1 正则化会将部分权重置零,因此它天然具有特征选择的能力,有助于自动筛选出对模型预测最重要的特征。
3.3 L1与L2对比
特性 | L2 正则化(Ridge) | L1 正则化(Lasso) |
---|---|---|
惩罚项 | ∑iθi2\sum_i \theta_i^2∑iθi2 | ∑_iθ_i\sum\_i \theta\_i∑_iθ_i |
效果 | 权重平滑趋近于 0 | 权重稀疏(部分变为 0) |
梯度 | λθi\lambda \theta_iλθi | λ⋅sign(θi)\lambda \cdot \text{sign}(\theta_i)λ⋅sign(θi) |
适用场景 | 特征共线性强、需要保留所有特征 | 特征选择(自动忽略无关特征) |
3.4 Dropout
Dropout 的工作流程如下:
- 在每次训练迭代中,随机选择一部分神经元(通常以概率 p丢弃,比如 p=0.5)。
- 被选中的神经元在当前迭代中不参与前向传播和反向传播。
- 在测试阶段,所有神经元都参与计算,但需要对权重进行缩放(通常乘以 1−p),以保持输出的期望值一致。
Dropout 是一种在训练过程中随机丢弃部分神经元的技术。它通过减少神经元之间的依赖来防止模型过于复杂,从而避免过拟合。
import torchimport torch.nn as nndef test01():x=torch.randint(1,10,(5,5),dtype=torch.float)dropout=nn.Dropout(p=0.5)print(x)print(dropout(x))if __name__ == '__main__':test01()
示例
对图片进行随机丢弃
import torch
from torch import nn
from PIL import Image
from torchvision import transforms
import osfrom matplotlib import pyplot as plttorch.manual_seed(42)def load_img(path, resize=(224, 224)):pil_img = Image.open(path).convert('RGB')print("Original image size:", pil_img.size) # 打印原始尺寸transform = transforms.Compose([transforms.Resize(resize),transforms.ToTensor() # 转换为Tensor并自动归一化到[0,1]])return transform(pil_img) # 返回[C,H,W]格式的tensorif __name__ == '__main__':dirpath = os.path.dirname(__file__)path = os.path.join(dirpath, 'img', 'torch-fcnn/fcnn-demo/100.jpg') # 使用os.path.join更安全# 加载图像 (已经是[0,1]范围的Tensor)trans_img = load_img(path)# 添加batch维度 [1, C, H, W],因为Dropout默认需要4D输入trans_img = trans_img.unsqueeze(0)# 创建Dropout层dropout = nn.Dropout2d(p=0.2)drop_img = dropout(trans_img)# 移除batch维度并转换为[H,W,C]格式供matplotlib显示trans_img = trans_img.squeeze(0).permute(1, 2, 0).numpy()drop_img = drop_img.squeeze(0).permute(1, 2, 0).numpy()# 确保数据在[0,1]范围内drop_img = drop_img.clip(0, 1)# 显示图像fig = plt.figure(figsize=(10, 5))ax1 = fig.add_subplot(1, 2, 1)ax1.imshow(trans_img)ax2 = fig.add_subplot(1, 2, 2)ax2.imshow(drop_img)plt.show()
3.5 数据增强
样本数量不足(即训练数据过少)是导致过拟合(Overfitting)的常见原因之一,可以从以下角度理解:
- 当训练数据过少时,模型容易“记住”有限的样本(包括噪声和无关细节),而非学习通用的规律。
- 简单模型更可能捕捉真实规律,但数据不足时,复杂模型会倾向于拟合训练集中的偶然性模式(噪声)。
- 样本不足时,训练集的分布可能与真实分布偏差较大,导致模型学到错误的规律。
- 小数据集中,个别样本的噪声(如标注错误、异常值)会被放大,模型可能将噪声误认为规律。
数据增强(Data Augmentation)是一种通过人工生成或修改训练数据来增加数据集多样性的技术,常用于解决过拟合问题。数据增强通过“模拟”更多训练数据,迫使模型学习泛化性更强的规律,而非训练集中的偶然性模式。其本质是一种低成本的正则化手段,尤其在数据稀缺时效果显著。
在了解计算机如何处理图像之前,需要先了解图像的构成元素。
图像是由像素点组成的,每个像素点的值范围为: [0, 255], 像素值越大意味着较亮。比如一张 200x200 的图像, 则是由 40000 个像素点组成, 如果每个像素点都是 0 的话, 意味着这是一张全黑的图像。
我们看到的彩色图一般都是多通道的图像, 所谓多通道可以理解为图像由多个不同的图像层叠加而成, 例如我们看到的彩色图像一般都是由 RGB 三个通道组成的,还有一些图像具有 RGBA 四个通道,最后一个通道为透明通道,该值越小,则图像越透明。
数据增强是提高模型泛化能力(鲁棒性)的一种有效方法,尤其在图像分类、目标检测等任务中。数据增强可以模拟更多的训练样本,从而减少过拟合风险。数据增强通过torchvision.transforms模块来实现。
3.5.1 图片缩放
def test01():path="torch-fcnn/fcnn-demo/datasets/100.jpg"img=Image.open(path)print(img.size)transform=transforms.Compose([transforms.Resize((224,224)),transforms.ToTensor()])t_img=transform(img)print(t_img.shape)t_img = torch.permute(t_img, (1, 2, 0))plt.imshow(t_img)plt.show()
3.5.2 随机裁剪
def test02():path="torch-fcnn/fcnn-demo/datasets/100.jpg"img=Image.open(path)print(img.size)transform=transforms.Compose([transforms.RandomCrop((224,224)),transforms.ToTensor()])t_img=transform(img)# print(t_img.shape)t_img = torch.permute(t_img, (1, 2, 0))plt.imshow(t_img)plt.show()
3.5.3 随机水平翻转
def test03():path="torch-fcnn/fcnn-demo/datasets/100.jpg"img=Image.open(path)print(img.size)transform=transforms.Compose([transforms.RandomHorizontalFlip(p=1),transforms.ToTensor()])t_img=transform(img)# print(t_img.shape)t_img = torch.permute(t_img, (1, 2, 0))plt.imshow(t_img)plt.show()
3.5.4 调整图片颜色
img = Image.open('./img/100.jpg')transform = transforms.Compose([transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.2), transforms.ToTensor()])r_img = transform(img)print(r_img.shape)r_img = r_img.permute(1, 2, 0)plt.imshow(r_img)plt.show()
3.5.5 随机旋转
def test04():path="torch-fcnn/fcnn-demo/datasets/100.jpg"img=Image.open(path)print(img.size)transform=transforms.RandomRotation((30,60), expand=False, center=None, fill=0)
3.5.6 图片转Tensor
def test05():t=torch.randn(3,224,224)transform=transforms.Compose([# 转换为PIL图片transforms.ToPILImage(),transforms.ToTensor(),])t_img=transform(t)# print(t_img.shape)t_img = torch.permute(t_img, (1, 2, 0))plt.imshow(t_img)plt.show()
3.5.7 Tensor转图片
import torch
from PIL import Image
from torchvision import transformsdef test002():# 1. 随机一个数据表示图片img_tensor = torch.randn(3, 224, 224)# 2. 创建一个transformstransform = transforms.ToPILImage()# 3. 转换为图片img = transform(img_tensor)img.show()# 4. 保存图片img.save("./test.jpg")if __name__ == "__main__":test002()
3.5.8 归一化
def test06():path="torch-fcnn/fcnn-demo/datasets/100.jpg"img=Image.open(path)print(img.size)t=torch.randn(3,224,224)transform = transforms.Compose([transforms.ToTensor(),# 归一化transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])])t_img=transform(t)# print(t_img.shape)t_img = torch.permute(t_img, (1, 2, 0))plt.imshow(t_img)plt.show()
3.5.9 数据增强整合
from PIL import Image
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import torch
from torchvision import transforms, datasets, utilsdef test01():# 定义数据增强和归一化transform = transforms.Compose([transforms.RandomHorizontalFlip(), # 随机水平翻转transforms.RandomRotation(10), # 随机旋转 ±10 度transforms.RandomResizedCrop(32, scale=(0.8, 1.0)), # 随机裁剪到 32x32,缩放比例在0.8到1.0之间transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1), # 随机调整亮度、对比度、饱和度、色调transforms.ToTensor(), # 转换为 Tensortransforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), # 归一化,这是一种常见的经验设置,适用于数据范围 [0, 1],使其映射到 [-1, 1]])# 加载 CIFAR-10 数据集,并应用数据增强trainset = datasets.CIFAR10(root="./cifar10_data", train=True, download=True, transform=transform)dataloader = DataLoader(trainset, batch_size=4, shuffle=False)# 获取一个批次的数据images, labels = next(iter(dataloader))# 还原图片并显示plt.figure(figsize=(10, 5))for i in range(4):# 反归一化:将像素值从 [-1, 1] 还原到 [0, 1]img = images[i] / 2 + 0.5# 转换为 PIL 图像img_pil = transforms.ToPILImage()(img)# 显示图片plt.subplot(1, 4, i + 1)plt.imshow(img_pil)plt.axis('off')plt.title(f'Label: {labels[i]}')plt.show()if __name__ == "__main__":test01()