1.AlexNet概述
论文原文:ImageNet Classification with Deep Convolutional Neural Networks
在LeNet提出后,卷积神经网络在计算机视觉和机器学习领域中很有名气。但卷积神经网络并没有主导这些领域。这是因为虽然LeNet在小数据集上取得了很好的效果,但是在更大、更真实的数据集上训练卷积神经网络的性能和可行性还有待研究。事实上,在上世纪90年代初到2012年之间的大部分时间里,神经网络往往被其他机器学习方法超越,如支持向量机(support vector machines)。
虽然上世纪90年代就有了一些神经网络加速卡,但仅靠它们还不足以开发出有大量参数的深层多通道多层卷积神经网络。此外,当时的数据集仍然相对较小。除了这些障碍,训练神经网络的一些关键技巧仍然缺失,包括启发式参数初始化、随机梯度下降的变体、非挤压激活函数和有效的正则化技术。
2012年,AlexNet横空出世。它首次证明了深度卷积神经网络学习到的特征可以超越手工设计的特征。它一举打破了计算机视觉研究的现状。 AlexNet使用了8层卷积神经网络,并以很大的优势赢得了2012年ImageNet图像识别挑战赛。
AlexNet和LeNet的设计理念非常相似,但也存在显著差异。
-
AlexNet比相对较小的LeNet5要深得多。AlexNet由八层组成:五个卷积层、两个全连接隐藏层和一个全连接输出层。
-
AlexNet使用ReLU而不是sigmoid作为其激活函数。ReLU 能够提高计算速度的同时,有效地解决了梯度消失问题,从而使得训练更加高效。
3.局部响应归一化(Local response nomalization,LRN)。在后来VGG的论文中发现没什么用,还会增加参数。
4.重叠池化(Overlapping Pooling)。池化步长小于池化窗口的大小。现在也不再被采用。
5.在训练阶段全连接层使用了Dropout降低过拟合,每次迭代随机使一半的神经元弃用
Dropout 是一种在神经网络训练过程中常用的正则化技术,通过在神经网络的训练过程中随机丢弃(或者说关闭)一些神经元节点来减少过拟合的发生。
1. 减少过拟合
Dropout 强制让网络在训练时不能依赖于特定的神经元,因为它们在每次迭代中都有可能被随机丢弃。这样可以降低网络对某些特定特征的依赖性,使得网络更加泛化,从而减少了过拟合的风险。
2. 提升网络的鲁棒性
通过随机丢弃节点,网络变得更加健壮。它鼓励网络中不同神经元之间学习到更加独立、更具有鲁棒性的特征表示,而不是过度依赖某些特定的神经元。
3. 防止神经元共适应
神经元之间可能会相互适应,导致它们高度依赖彼此。Dropout 强制网络在训练过程中更加独立地学习,减少了神经元共适应的情况,从而使网络更加健壮。
4. 类似集成多个模型的效果
每次训练迭代中,Dropout 随机地关闭一些神经元,相当于训练了许多不同的网络结构。最终,这些结构共同贡献了一个集成模型,提高了模型的表现力。
6.分布式训练。在当时GPU性能并不高,内存比较小,AlexNet在使用GPU进行训练时,可将卷积层和全连接层分别放到不同的GPU上进行并行计算,从而大大加快了训练速度。
7.数据增强。
1.从256×256中随机裁剪出224×224再水平翻转。
2.第二种数据增强方式是改变训练图像中RGB通道的强度。在整个ImageNet训练集中的RGB像素值集合上执行PCA,在每张训练图像中,我们都会添加所主成分的倍数
整个网络结构图如下,网络结构被切割为两部分,每个GPU单独计算一半的通道数,其中会互相通信三次。
输入尺寸是227,224是论文中写错了。如果是224,则第一次卷积后特征图尺寸((224-11)/4)+1=54.25会出现小数
当然从现在的计算资源来看,AlexNet是一个非常简单的神经网络,已经不再需要分到2个GPU上并行计算了,于是网络结构可以简化如下
2.网络结构详解
1.输入层。227 × 227 × 3,三通道RGB图像
2.卷积C1
- 卷积。96个11×11×3的卷积核,padding = 0,stride = 4。特征图尺寸为((227-11)/4)+1=55,得到输出55×55×96的特征图
- ReLU激活
- 最大池化。核大小为3×3,padding = 0,stride = 2,特征图尺寸为((55-3)/2)+1=27,得到输出27×27×96的特征图
3.卷积C2
- 卷积。256个5×5×96的卷积核,padding = 2,stride = 1。特征图尺寸为((27-5+2×2)/1)+1=27,得到输出27×27×256的特征图
- ReLU激活
- 最大池化。核大小为3×3,padding = 0,stride = 2,特征图尺寸为((27-3)/2)+1=13,得到输出13×13×256的特征图
4.卷积C3
- 卷积。384个3×3×256的卷积核,padding = 1,stride = 1。特征图尺寸为((13-3+2×1)/1)+1=13,得到输出13×13×384的特征图
- ReLU激活
5.卷积C4
- 卷积。384个3×3×384的卷积核,padding = 1,stride = 1。特征图尺寸为((13-3+2×1)/1)+1=13,得到输出13×13×384的特征图
- ReLU激活
6.卷积C5
- 卷积。256个3×3×384的卷积核,padding = 1,stride = 1。特征图尺寸为((13-3+2×1)/1)+1=13,得到输出13×13×256的特征图
- ReLU激活
- 最大池化。核大小为3×3,padding = 0,stride = 2,特征图尺寸为((13-3)/2)+1=6,得到输出6×6×256的特征图
7.全连接层FC6
- 全连接,6×6×256–>>1×1×4096,并使用Dropout,随机50%神经元弃用
- ReLU激活
8.全连接层FC7
- 全连接,1×1×4096–>>1×1×4096,并使用Dropout,随机50%神经元弃用
- ReLU激活
9.全连接层FC8
全连接softmax,1×1×4096–>>1×1×1000。1000是1000个分类类别
3.AlexNet实现CIFAR-10分类
1.读取数据集
CIFAR-10数据集是32*32尺寸的,但AlexNet网络结构是针对ImageNet大尺寸设计的,但ImageNet数据集作为简单实践的话又太大了。这里直接简单的将图片拉大,但实际上这并不是一个好的操作,这里只是简单实践,毕竟AlexNet现在并不常使用。
# 数据预处理
transform = transforms.Compose([
transforms.Resize((227, 227)),
transforms.ToTensor(),
transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)),
])
# 加载CIFAR-10数据集
train_dataset = datasets.CIFAR10(root='./dataset', train=True, download=True, transform=transform)
test_dataset = datasets.CIFAR10(root='./dataset', train=False, download=True, transform=transform)
# 数据加载器
train_dataloader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_dataloader = DataLoader(test_dataset, batch_size=64, shuffle=False)
这个归一化的数据来源于ImageNet数据集百万张统计得到,通常可以作为一般数据集的归一化标准。当然也可以针对自己数据集重新计算均值和标准差用于归一化。
2.搭建AlexNet
class AlexNet(nn.Module):
def __init__(self, num_classes=1000):
super(AlexNet, self).__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 96, kernel_size=11, stride=4),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(96, 256, kernel_size=5, padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(256, 384, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(384, 384, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(384, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
)
self.avgpool = nn.AdaptiveAvgPool2d((6, 6))
self.classifier = nn.Sequential(
nn.Dropout(),
nn.Linear(256 * 6 * 6, 4096),
nn.ReLU(inplace=True),
nn.Dropout(),
nn.Linear(4096, 4096),
nn.ReLU(inplace=True),
nn.Linear(4096, num_classes),
)
def forward(self, x):
x = self.features(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.classifier(x)
return x
# 打印模型结构
model = AlexNet().to(device)
summary(model, (3, 227, 227))
3.使用GPU
device = 'cuda' if torch.cuda.is_available() else 'cpu'
4.模型训练
def train(model, lr, epochs, train_dataloader, device, save_path):
# 将模型放入GPU
model = model.to(device)
# 使用交叉熵损失函数
loss_fn = nn.CrossEntropyLoss().to(device)
# SGD
optimizer = torch.optim.SGD(model.parameters(), lr=lr, weight_decay=5e-4, momentum=0.9)
# 记录训练与验证数据
train_losses = []
train_accuracies = []
# 开始迭代
for epoch in range(epochs):
# 切换训练模式
model.train()
# 记录变量
train_loss = 0.0
correct_train = 0
total_train = 0
# 读取训练数据并使用 tqdm 显示进度条
for i, (inputs, targets) in tqdm(enumerate(train_dataloader), total=len(train_dataloader), desc=f"Epoch {epoch+1}/{epochs}", unit='batch'):
# 训练数据移入GPU
inputs = inputs.to(device)
targets = targets.to(device)
# 模型预测
outputs = model(inputs)
# 计算损失
loss = loss_fn(outputs, targets)
# 梯度清零
optimizer.zero_grad()
# 反向传播
loss.backward()
# 使用优化器优化参数
optimizer.step()
# 记录损失
train_loss += loss.item()
# 计算训练正确个数
_, predicted = torch.max(outputs, 1)
total_train += targets.size(0)
correct_train += (predicted == targets).sum().item()
# 计算训练正确率并记录
train_loss /= len(train_dataloader)
train_accuracy = correct_train / total_train
train_losses.append(train_loss)
train_accuracies.append(train_accuracy)
# 输出训练信息
print(f"Epoch [{epoch + 1}/{epochs}] - Train Loss: {train_loss:.4f}, Train Acc: {train_accuracy:.4f}")
# 绘制损失和正确率曲线
plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
plt.plot(range(epochs), train_losses, label='Training Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.subplot(1, 2, 2)
plt.plot(range(epochs), train_accuracies, label='Accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.legend()
plt.tight_layout()
plt.show()
torch.save(model.state_dict(), save_path)
model = AlexNet(num_classes=10) # 十分类
lr = 0.01
epochs = 10
save_path = './modelWeight/AlexNet_CIFAR10'
train(model,lr,epochs,train_dataloader,device,save_path)
这里只训练了10个epoch,也没有使用验证集调参,仅仅是简单实践而已。可以看到损失还在不断降低,还没收敛。
5.模型测试
def test(model, test_dataloader, device, model_path):
# 将模型设置为评估模式
model.eval()
# 将模型移动到指定设备上
model.to(device)
# 从给定路径加载模型的状态字典
model.load_state_dict(torch.load(model_path))
correct_test = 0
total_test = 0
# 不计算梯度
with torch.no_grad():
# 遍历测试数据加载器
for inputs, targets in test_dataloader:
# 将输入数据和标签移动到指定设备上
inputs = inputs.to(device)
targets = targets.to(device)
# 模型进行推理
outputs = model(inputs)
# 获取预测结果中的最大值
_, predicted = torch.max(outputs, 1)
total_test += targets.size(0)
# 统计预测正确的数量
correct_test += (predicted == targets).sum().item()
# 计算并打印测试数据的准确率
test_accuracy = correct_test / total_test
print(f"Accuracy on Test: {test_accuracy:.4f}")
return test_accuracy
model_path = './modelWeight/AlexNet_CIFAR10'
test(model, test_dataloader, device, save_path)
6.使用Pytorch自带的AlexNet
Pytorch有官方实现的AlexNet以及它在ImageNet上预训练好的权重,如果数据集的分类类别都在ImageNet中存在,而且想快速训练,可以使用预训练好的权重。地址:alexnet — Torchvision main documentation (pytorch.org)
此外还需要修改最后一层全连接层的输出数目
from torchvision import models
# 初始化预训练的AlexNet模型
modelPre = models.alexnet(weights='DEFAULT')
num_ftrs = modelPre.classifier[6].in_features
modelPre.classifier[6] = nn.Linear(num_ftrs, 10) # CIFAR-10有10个类别
modelPre = modelPre.to(device)
summary(modelPre, (3, 224, 224))
weights='DEFAULT'就是使用默认最新最好的预训练权重,直接指定weights='IMAGENET1K_V1'也是一样的,因为AlexNet在pytroch中只有一个权重,其他模型会可能有多个版本权重,可以在官方文档中看。如果只想使用他的模型而不使用预训练权重,直接不设定这个参数就可以了。
Pytorch实现的AlexNet还是有些不同的,一般输入224×224,还使用了全局平均池化支持不同的尺寸输入,AlexNet出来的时候全局平均池化还没被提出。
lr = 0.01
epochs = 10
save_path = './modelWeight/AlexNetPreTrain_CIFAR10'
train(modelPre,lr,epochs,train_dataloader,device,save_path)
可以看到收敛更快
lr = 0.01
epochs = 10
save_path = './modelWeight/AlexNetPreTrain_CIFAR10'
train(modelPre,lr,epochs,train_dataloader,device,save_path)