作者有话说:
这篇文章写的还是比混乱的。因为本人也是第一次做这样的尝试,虽然接触深度学习有一年了,但是对于模型的优化仅仅是局限于理论上。通过这一次的实验,我对于模型的理解也更深了几分。我不期望这篇文章能帮你能解决多大问题,我只希望能通过这篇文章告诉你,学习一定要结合实践!!!
一、前言
上篇文章《卷积神经网络实验二:CNN实现猫狗分类任务-CSDN博客》通过在原有的多层感知机上增加卷积层,在相同的数据上表现明显好于多层感知机模型。但是在损失变化曲线上可以发现,训练损失在逐渐的减少,但是测试损失不降反增,出现了过拟合现象。因此,我在设想,既然训练集上模型的性能能逐渐的提升,是不是只要让模型不再过拟合,在测试集上的效果也能提升?
实验过程中,只提供更改的代码,完整代码请参考《卷积神经网络实验一:多层感知机进行猫狗分类》
二、实验环境
本次实验环境依然使用FunHPC | 算力简单易用 AI乐趣丛生这个平台提供的免费P4显卡进行实验,具体实验环境如图:
感谢这个平台对于学生用户的支持, FunHPC | 算力简单易用 AI乐趣丛生这个算力平台显卡在全网中性价比最高,并且学生用户认证可以使用这个平台免费提供的P4显卡,单次最长使用时间为24小时,没有总时间限制,可以无限次使用。
新用户注册有15元的体验金,可以完全不充值白嫖使用,非常值得推荐!!!
三、过拟合解决策略
首先,我们得明白什么叫做过拟合?为什么过拟合?过拟合解决的方案有哪些?
什么叫做过拟合?
过拟合(Overfitting)是指在机器学习模型中,模型在训练数据上表现良好,但在未见过的新数据(测试数据)上表现较差的现象。简单来说,过拟合发生在模型学习到了训练数据中的噪声和细节,而不仅仅是学习到了数据的基本规律。
过拟合的原因
- 模型复杂度过高:当模型的参数过多时,它可能会过度拟合训练数据。复杂模型能够捕捉到训练数据中的所有细节,但这些细节往往并不具有普遍性。
- 训练数据不足:当训练数据量不足时,模型容易记住每个样本的特征,而不是学习到数据的通用模式。
- 数据噪声:如果训练数据中包含噪声或异常值,模型可能会试图将这些噪声作为有效信号进行学习,导致过拟合。
- 特征选择不当:包含过多无关特征或不相关的变量也会导致模型在训练数据上表现很好,但在新数据上表现不佳。
解决过拟合的方案
-
简化模型:
- 使用更简单的模型(如减少层数、神经元数等)。
- 降低模型的复杂度,以降低过拟合风险。
-
正则化:
- L1 正则化(Lasso):通过增加权重绝对值的和来惩罚模型,使某些权重趋向于零,达到特征选择的效果。
- L2 正则化(Ridge):通过增加权重平方和的惩罚项来减少模型的复杂度。
-
数据增强:
通过对训练数据进行变换(如旋转、缩放、翻转等),增加训练数据的多样性,从而减少过拟合的可能性。
四、实验设计
在不考虑数据的情况下,引起模型过拟合的原因主要在于模型的结构和训练过程。为此,我们对于模型过拟合的解决分三步进行,分别是:模型层数验证、卷积层的调整、正则化和残差连接。
- 模型层数验证:主要是指增加或减少模型的层次结构,例如增加卷积层或减少全连接层等等。
- 卷积层的调整:在确定整体的模型结构后,不同神经网络层的设置依然会影响模型的效果,例如卷积核的大小等等。
- 正则化和残差连接:这一步主要进行的是:增加正则化操作、残差连接等等。
五、第一步
基础知识:
在进行第一步优化前我们先来了解以下全连接层和卷积层的基础知识。
特性 | 全连接层 (FC Layer) | 卷积层 (Convolutional Layer) |
---|---|---|
作用 | 特征组合:全连接层将上层提取的特征重新组合,将局部信息整合为全局信息。 非线性变换:通过学习权重和偏置,增加非线性表达能力。 分类和回归:用于输出层,进行类别或数值预测。 | 局部特征提取:通过卷积核扫描局部区域,提取边缘、角点等空间结构特征。 参数减少:只连接局部区域,降低参数数量。 多层次特征提取:通过堆叠卷积层逐层提取复杂特征。 |
层数多和少的影响 | 多层:增加表达能力,能学习复杂特征组合,但可能过拟合。 少层:简单结构,减少计算,适合基本任务。 | 多层:学习丰富、复杂的特征,适合处理复杂结构数据。 少层:适合简单任务,减少计算,特征学习能力有限。 |
单层中神经元/卷积核数量多和少的影响 | 神经元多:提高拟合能力,适合复杂数据,但易过拟合。 神经元少:降低参数量,有助泛化,但可能无法捕捉复杂模式。 | 卷积核多:捕获更多样的特征,增强模型特征表达能力。 卷积核少:计算资源减少,但特征丰富度降低。 |
卷积核大小 | 不适用 | 小卷积核(如 3x3):捕捉细节,堆叠后可扩大感受野,参数效率高。 大卷积核(如 5x5 或 7x7):覆盖更大区域,计算复杂度高,适合少层情况。 |
输入/输出通道数 | 不适用 | 输入通道数:输入通道越多,卷积核可接收更多特征。 输出通道数:增加输出通道数可增强特征捕获能力,通常层数增加后通道逐步增加。 |
滑窗步长 (Stride) | 不适用 | 小步长(如 1):获取更高分辨率特征,但计算量大。 大步长(如 2 或 3):减小特征图尺寸、降低计算量,但过大会导致信息丢失。 |
Padding(填充) | 不适用 | 无填充 (padding=0):特征图尺寸减小,适合内部特征。 零填充 (padding>0):保留尺寸,适合深层结构保留空间分辨率。 |
原来的模型:
# 定义CNN模型
class CNN(nn.Module):def __init__(self):super(CNN, self).__init__()# 第一个卷积层: 输入3个通道(RGB),输出20个通道,卷积核大小为5x5,步长为1self.conv1 = nn.Conv2d(3, 20, kernel_size=5, stride=1, padding=0)# 第二个卷积层: 输入20个通道,输出50个通道,卷积核大小为4x4,步长为1self.conv2 = nn.Conv2d(20, 50, kernel_size=4, stride=1, padding=0)# 计算经过卷积层和池化后的特征图大小# 输入图像的假设尺寸为 (3, 150, 150)self.fc1_input_size = 50 * 35 * 35 # 根据卷积后的特征图大小来设置# 全连接层self.fc1 = nn.Linear(self.fc1_input_size, 128)self.fc2 = nn.Linear(128, 64)self.fc3 = nn.Linear(64, 2)def forward(self, x):# 第一个卷积层 + ReLU + 最大池化x = F.relu(self.conv1(x))x = F.max_pool2d(x, 2, 2) # 池化层 (2x2),降低特征图尺寸# 第二个卷积层 + ReLU + 最大池化x = F.relu(self.conv2(x))x = F.max_pool2d(x, 2, 2) # 池化层 (2x2)# 展平特征图x = x.view(-1, self.fc1_input_size)# 全连接层 + ReLUx = F.relu(self.fc1(x))x = F.relu(self.fc2(x))# 输出层 (无激活函数,因为最后要用交叉熵损失)x = self.fc3(x)return x
损失曲线:
模型实验:
实验1:减少全连接层
在这一步中,通过将全连接层fc2剔除,来降低模型的复杂程度:
# 定义CNN模型
class CNN(nn.Module):def __init__(self):super(CNN, self).__init__()# 第一个卷积层: 输入3个通道(RGB),输出20个通道,卷积核大小为5x5,步长为1self.conv1 = nn.Conv2d(3, 20, kernel_size=5, stride=1, padding=0)# 第二个卷积层: 输入20个通道,输出50个通道,卷积核大小为4x4,步长为1self.conv2 = nn.Conv2d(20, 50, kernel_size=4, stride=1, padding=0)# 计算经过卷积层和池化后的特征图大小# 输入图像的假设尺寸为 (3, 150, 150)self.fc1_input_size = 50 * 35 * 35 # 根据卷积后的特征图大小来设置# 全连接层self.fc1 = nn.Linear(self.fc1_input_size, 128)#self.fc2 = nn.Linear(128, 64)self.fc3 = nn.Linear(128, 2)def forward(self, x):# 第一个卷积层 + ReLU + 最大池化x = F.relu(self.conv1(x))x = F.max_pool2d(x, 2, 2) # 池化层 (2x2),降低特征图尺寸# 第二个卷积层 + ReLU + 最大池化x = F.relu(self.conv2(x))x = F.max_pool2d(x, 2, 2) # 池化层 (2x2)# 展平特征图x = x.view(-1, self.fc1_input_size)# 全连接层 + ReLUx = F.relu(self.fc1(x))#x = F.relu(self.fc2(x))# 输出层 (无激活函数,因为最后要用交叉熵损失)x = self.fc3(x)return x
去掉一个全连接层后发现,模型损失的减少变得缓慢,并且最低测试损失大于原来的模型。通过实验可以发现,减少一层全连接层就降低了模型的表达能力和综合能力,并且最终也出现了过拟合。
实验2:降低全连接层的神经元数量
在实验1的基础上降低全连接层fc1的神经元数量,降低模型的拟合能力。
# 定义CNN模型
class CNN(nn.Module):def __init__(self):super(CNN, self).__init__()# 第一个卷积层: 输入3个通道(RGB),输出20个通道,卷积核大小为5x5,步长为1self.conv1 = nn.Conv2d(3, 20, kernel_size=5, stride=1, padding=0)# 第二个卷积层: 输入20个通道,输出50个通道,卷积核大小为4x4,步长为1self.conv2 = nn.Conv2d(20, 50, kernel_size=4, stride=1, padding=0)# 计算经过卷积层和池化后的特征图大小# 输入图像的假设尺寸为 (3, 150, 150)self.fc1_input_size = 50 * 35 * 35 # 根据卷积后的特征图大小来设置# 全连接层self.fc1 = nn.Linear(self.fc1_input_size, 64)#self.fc2 = nn.Linear(128, 64)self.fc3 = nn.Linear(64, 2)def forward(self, x):# 第一个卷积层 + ReLU + 最大池化x = F.relu(self.conv1(x))x = F.max_pool2d(x, 2, 2) # 池化层 (2x2),降低特征图尺寸# 第二个卷积层 + ReLU + 最大池化x = F.relu(self.conv2(x))x = F.max_pool2d(x, 2, 2) # 池化层 (2x2)# 展平特征图x = x.view(-1, self.fc1_input_size)# 全连接层 + ReLUx = F.relu(self.fc1(x))#x = F.relu(self.fc2(x))# 输出层 (无激活函数,因为最后要用交叉熵损失)x = self.fc3(x)return x
通过实验发现,模型损失降低的速度增加,并且最低损失小于原有的模型和实验1。
通过降低全连接层的神经元的数量可以明显降低模型的过拟合,但是模型还是出现了过拟合现象。为什么模型的能力还是不行?
实验3:增加卷积层
考虑到模型的性能提升并不高,这是不是因为特征的问题,因此在实验2的基础上增加了一层卷积层。
# 定义CNN模型
class CNN(nn.Module):def __init__(self):super(CNN, self).__init__()# 第一个卷积层: 输入3个通道(RGB),输出20个通道,卷积核大小为5x5,步长为1self.conv1 = nn.Conv2d(3, 20, kernel_size=5, stride=1, padding=0)# 第二个卷积层: 输入20个通道,输出50个通道,卷积核大小为4x4,步长为1self.conv2 = nn.Conv2d(20, 50, kernel_size=4, stride=1, padding=0)# 第三个卷积层: 输入50个通道,输出70个通道,卷积核大小为4x4,步长为1self.conv3 = nn.Conv2d(50, 70, kernel_size=4, stride=1, padding=0)# 计算经过卷积层和池化后的特征图大小# 输入图像的假设尺寸为 (3, 150, 150)self.fc1_input_size = self._get_fc1_input_size((3, 150, 150))# 全连接层self.fc1 = nn.Linear(self.fc1_input_size, 64)self.fc3 = nn.Linear(64, 2)def _get_fc1_input_size(self, input_shape):with torch.no_grad():x = torch.zeros(1, *input_shape) # 创建一个符合输入图像尺寸的假数据x = F.max_pool2d(F.relu(self.conv1(x)), 2, 2)x = F.max_pool2d(F.relu(self.conv2(x)), 2, 2)x = F.max_pool2d(F.relu(self.conv3(x)), 2, 2)return x.numel() # 计算展平后的特征大小def forward(self, x):# 第一个卷积层 + ReLU + 最大池化x = F.relu(self.conv1(x))x = F.max_pool2d(x, 2, 2) # 池化层 (2x2),降低特征图尺寸# 第二个卷积层 + ReLU + 最大池化x = F.relu(self.conv2(x))x = F.max_pool2d(x, 2, 2) # 池化层 (2x2)# 第三个卷积层 + ReLU + 最大池化x = F.relu(self.conv3(x))x = F.max_pool2d(x, 2, 2) # 池化层 (2x2)# 展平特征图x = x.view(-1, self.fc1_input_size)# 全连接层 + ReLUx = F.relu(self.fc1(x))# 输出层 (无激活函数,因为最后要用交叉熵损失)x = self.fc3(x)return x
通过实验发现,果然如猜测一样,模型虽然通过全连接层的结构简化可以降低过拟合, 但是在特征提取上不够也不行。增加了卷积层之后,模型的性能提升较高,最低损失比以往的都低。但也可以发现,增加卷积层后,模型损失降低的速度也变慢了。
实验4: 再增加一层卷积层
通过上面的实验可以发现,增加卷积层非常有用,既然有用,是不是说明继续增加卷积层,提取增加综合的特征是不是模型的能力会在此提升。
# 定义CNN模型
class CNN(nn.Module):def __init__(self):super(CNN, self).__init__()# 第一个卷积层: 输入3个通道(RGB),输出20个通道,卷积核大小为5x5,步长为1self.conv1 = nn.Conv2d(3, 20, kernel_size=5, stride=1, padding=0)# 第二个卷积层: 输入20个通道,输出50个通道,卷积核大小为4x4,步长为1self.conv2 = nn.Conv2d(20, 50, kernel_size=4, stride=1, padding=0)# 第三个卷积层: 输入50个通道,输出70个通道,卷积核大小为4x4,步长为1self.conv3 = nn.Conv2d(50, 70, kernel_size=4, stride=1, padding=0)# 新增的第四个卷积层: 输入70个通道,输出100个通道,卷积核大小为3x3,步长为1self.conv4 = nn.Conv2d(70, 100, kernel_size=3, stride=1, padding=0)# 计算经过卷积层和池化后的特征图大小# 输入图像的假设尺寸为 (3, 150, 150)self.fc1_input_size = self._get_fc1_input_size((3, 150, 150))# 全连接层self.fc1 = nn.Linear(self.fc1_input_size, 64)self.fc3 = nn.Linear(64, 2)def _get_fc1_input_size(self, input_shape):with torch.no_grad():x = torch.zeros(1, *input_shape) # 创建一个符合输入图像尺寸的假数据x = F.max_pool2d(F.relu(self.conv1(x)), 2, 2)x = F.max_pool2d(F.relu(self.conv2(x)), 2, 2)x = F.max_pool2d(F.relu(self.conv3(x)), 2, 2)x = F.max_pool2d(F.relu(self.conv4(x)), 2, 2)return x.numel() # 计算展平后的特征大小def forward(self, x):# 第一个卷积层 + ReLU + 最大池化x = F.relu(self.conv1(x))x = F.max_pool2d(x, 2, 2) # 池化层 (2x2),降低特征图尺寸# 第二个卷积层 + ReLU + 最大池化x = F.relu(self.conv2(x))x = F.max_pool2d(x, 2, 2) # 池化层 (2x2)# 第三个卷积层 + ReLU + 最大池化x = F.relu(self.conv3(x))x = F.max_pool2d(x, 2, 2) # 池化层 (2x2)# 新增的第四个卷积层 + ReLU + 最大池化x = F.relu(self.conv4(x))x = F.max_pool2d(x, 2, 2) # 池化层 (2x2)# 展平特征图x = x.view(-1, self.fc1_input_size)# 全连接层 + ReLUx = F.relu(self.fc1(x))# 输出层 (无激活函数,因为最后要用交叉熵损失)x = self.fc3(x)return x
实验结果如猜想一样,最低损失进一步降低,但模型的损失减低速度也变得更慢了。
实验5:继续增加卷积层
为了进一步验证增加卷积层是否能无线提高模型的性能,我们继续添加卷积层。
# 定义CNN模型
class CNN(nn.Module):def __init__(self):super(CNN, self).__init__()# 第一个卷积层: 输入3个通道(RGB),输出20个通道,卷积核大小为5x5,步长为1self.conv1 = nn.Conv2d(3, 20, kernel_size=5, stride=1, padding=0)# 第二个卷积层: 输入20个通道,输出50个通道,卷积核大小为4x4,步长为1self.conv2 = nn.Conv2d(20, 50, kernel_size=4, stride=1, padding=0)# 第三个卷积层: 输入50个通道,输出70个通道,卷积核大小为4x4,步长为1self.conv3 = nn.Conv2d(50, 70, kernel_size=4, stride=1, padding=0)# 第四个卷积层: 输入70个通道,输出100个通道,卷积核大小为3x3,步长为1self.conv4 = nn.Conv2d(70, 100, kernel_size=3, stride=1, padding=0)# 新增的第五个卷积层: 输入100个通道,输出150个通道,卷积核大小为3x3,步长为1self.conv5 = nn.Conv2d(100, 150, kernel_size=3, stride=1, padding=0)# 计算经过卷积层和池化后的特征图大小# 输入图像的假设尺寸为 (3, 150, 150)self.fc1_input_size = self._get_fc1_input_size((3, 150, 150))# 全连接层self.fc1 = nn.Linear(self.fc1_input_size, 64)self.fc3 = nn.Linear(64, 2)def _get_fc1_input_size(self, input_shape):with torch.no_grad():x = torch.zeros(1, *input_shape) # 创建一个符合输入图像尺寸的假数据x = F.max_pool2d(F.relu(self.conv1(x)), 2, 2)x = F.max_pool2d(F.relu(self.conv2(x)), 2, 2)x = F.max_pool2d(F.relu(self.conv3(x)), 2, 2)x = F.max_pool2d(F.relu(self.conv4(x)), 2, 2)x = F.max_pool2d(F.relu(self.conv5(x)), 2, 2)return x.numel() # 计算展平后的特征大小def forward(self, x):# 第一个卷积层 + ReLU + 最大池化x = F.relu(self.conv1(x))x = F.max_pool2d(x, 2, 2) # 池化层 (2x2),降低特征图尺寸# 第二个卷积层 + ReLU + 最大池化x = F.relu(self.conv2(x))x = F.max_pool2d(x, 2, 2) # 池化层 (2x2)# 第三个卷积层 + ReLU + 最大池化x = F.relu(self.conv3(x))x = F.max_pool2d(x, 2, 2) # 池化层 (2x2)# 第四个卷积层 + ReLU + 最大池化x = F.relu(self.conv4(x))x = F.max_pool2d(x, 2, 2) # 池化层 (2x2)# 新增的第五个卷积层 + ReLU + 最大池化x = F.relu(self.conv5(x))x = F.max_pool2d(x, 2, 2) # 池化层 (2x2)# 展平特征图x = x.view(-1, self.fc1_input_size)# 全连接层 + ReLUx = F.relu(self.fc1(x))# 输出层 (无激活函数,因为最后要用交叉熵损失)x = self.fc3(x)return x
这次实验的结果发现,模型的最低损失比上次降低一点,但不多。这说明模型再增加卷积层上已经无法再进一步的前进了。
总结
通过一系列模型修改实验,我们其实可以得出结论。那就是,卷积层的增加的确会提高模型提取特征的能力,但是有限度。并且增加卷积层也会降低模型的收敛速度。全连接层的简化可以防止模型的过拟合。模型性能的好坏,其实是需要我们不断进行尝试修改的,这真的就是一个炼丹的过程,没人可以直接写出一个性能最优的模型。
预告
接下来我会继续完成模型的第二步和第三步优化,敬请期待!