- AlexNet文献阅读与代码实现
- 文献内容介绍
- 代码实现
- 内容总结
AlexNet文献阅读与代码实现
前言:笔者目前研一,刚开始入门深度学习,所以想记录一下自己学习的过程,接下来的时间里,我会定期阅读深度学习领域的经典文献,并尝试用代码实现它们,也欢迎大家积极评论。
注:博客本身更侧重于代码实现,关于一些深度学习的概念,如卷积核之类的内容,大家有疑惑的话可以自行搜索。
文献地址:
ImageNet Classification with Deep Convolutional Neural Networks
文献内容讲解:
9年后重读深度学习奠基作之一:AlexNet【论文精读·2】_哔哩哔哩_bilibili
一些术语:
术语 | 含义 |
---|---|
trick | 一些技巧,比如alexnet就提到它采用了dropout、relu这样的技巧来加速训练并避免过拟合 |
一些超参
超参名 | 含义 |
---|---|
weight decay | weight decay是一种正则化技术,防止模型在训练数据上过拟合 |
momentum | momentum是一种加速梯度下降的优化方法,它通过考虑前一个步骤的更新方向,来帮助当前的梯度更新更快地收敛 |
batchsize | 将几个数据放到一起作为一个批次 |
可以在优化器里很轻松的设置它们:
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=1e-4)
文献内容介绍
这篇文章是2012年发表的,作者为Alex Krizhevsky,Ilya Sutskever,Geoffrey Hinton(前段时间拿到诺贝尔物理学家的那位,因为这事我才了解到这位大佬)。AlexNet在2012年的ImageNet比赛中取得了显著成功。(AlexNet是一个大型的用于图片分类的数据集),这篇文章最终发表在2012年的NIPS(NeurIPS)会议上。自此之后,深度学习在计算机视觉领域得到广泛应用。
AlexNet由五层卷积,和三层全连接组成,网络结构如下所示:
网络是上下两层的原因是GPU容量不够,所以作者把图片切开,分成两层来训练,最后拼接起来。他们用的GPU是GTX 580,只有3G的内存,而我们现在实现的时候不需要再切开了。所以我们可以把上下拼起来来看这个网络结构。(当然,作者在第二层的卷积后,做了一次上下两层连接的工作,我们现在忽略这一步)
文章第三节采用了一些trick来加速训练。
- Relu
之前的文章主要采用sigmod来作为激活函数,而本文采用了relu,relu训练起来会快很多。(不过我也不知道为什么)
- 用多个GPU训练
文章中介绍他如何采用多个GPU来训练,也就是把数据切开来分开训练
- 正则化(local response normalization)
就是把数据归一化(现在来看这个东西不太重要)
- overlapping pooling
对传统的pooling技术做了改进(这个我也没懂)
文章第四节介绍了一些方法来避免过拟合
- 数据增强
原图像是256 * 256的,它随机裁剪出224 * 224的部分出来,这样数据量就变为原来2048倍(现在来看不能这么算,因为裁剪出来的很多是一样的)
- dropout
作者把前两层全连接层中随机的50%的权重设为0,以此来避免过拟合
代码实现
为了减小内存的开销,我们这里采用的数据集是minist数据集(也就是手写数字识别的数据集),也就是把输入图像变为1 * 28 * 28大小的图像,输出类别变为了10(也就是0-9)。代码目录如下所示:
在data目录下,存放我们的minist数据集,model.py存放我们设计好的网络模型,train.py实现对模型的训练,并在训练完成后保存模型权重,test.py加载保存好的权重并进行测试。
一般而言深度学习的项目会有一个专门的文件夹用来处理数据,我们这里为了方便起见,直接调用了别人做好的数据,因此不用再额外写处理数据的代码。(数据集是自动下载的,权重是训练过程中保存的,也就是只要有3个.py文件,项目就可以运行起来)
model.py
AlextNet类是我们设计的网络模型,后面的main函数是我们用来对当前文件做测试的(建议为每个python文件都添加main函数,这样方便我们单独测试每一个文件)
import torch
import torch.nn as nnclass AlexNet(nn.Module):def __init__(self):super(AlexNet, self).__init__()self.features = nn.Sequential(nn.Conv2d(1, 96, kernel_size=5, stride=1), # 输入通道为1(灰度图)nn.ReLU(inplace=True),nn.MaxPool2d(kernel_size=2, stride=2), # 下采样nn.Conv2d(96, 256, kernel_size=5, stride=1),nn.ReLU(inplace=True),nn.MaxPool2d(kernel_size=2, stride=2),nn.Conv2d(256, 384, kernel_size=3, stride=1, padding=1),nn.ReLU(inplace=True),nn.Conv2d(384, 384, kernel_size=3, stride=1, padding=1),nn.ReLU(inplace=True),nn.Conv2d(384, 256, kernel_size=3, stride=1, padding=1),nn.ReLU(inplace=True),nn.MaxPool2d(kernel_size=2, stride=2), # 这里的池化)self.classifier = nn.Sequential(nn.Dropout(),nn.Linear(256 * 2 * 2, 4096), # 修改为256 * 2 * 2nn.ReLU(inplace=True),nn.Dropout(),nn.Linear(4096, 4096),nn.ReLU(inplace=True),nn.Linear(4096, 10), # MNIST有10个类)def forward(self, x):print(f"Input shape: {x.shape}") # 打印输入形状# 第一层x = self.features[0](x) # 经过5 * 5的卷积 大小变为[1, 96, 24, 24]x = self.features[1](x) # 经过relu 大小不变x = self.features[2](x) # 经过池化 大小变为[1, 96, 12, 12]# 第二层x = self.features[3](x) # 经过5 * 5的卷积x = self.features[4](x)x = self.features[5](x)# 第三层x = self.features[6](x) # 经过3 * 3的卷积x = self.features[7](x)# 第四层x = self.features[8](x) # 经过3 * 3的卷积x = self.features[9](x)# 第五层x = self.features[10](x) # 经过3 * 3的卷积x = self.features[11](x)x = self.features[12](x) # Max Poolx = x.view(x.size(0), 256 * 2 * 2) # 展平# 后面的全连接层x = self.classifier(x) # 分类return xif __name__ == "__main__":model = AlexNet()# 使用 torch.randn 生成随机输入,形状为 (1, 1, 28, 28)test_input = torch.randn(1, 1, 28, 28) # 生成一个随机的28x28图像output = model(test_input)print(output.shape) # 应该输出 (1, 10) 对应于一个样本的10个类的输出
train.py
import os
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
from model import AlexNet # 确保你的model.py文件在合适的位置# 设置随机种子以确保可重复性
torch.manual_seed(42)# 配置超参数
batch_size = 64
learning_rate = 0.001
num_epochs = 10
data_dir = './data' # 数据存放路径# 数据变换(适合MNIST的数据)
transform = transforms.Compose([transforms.ToTensor(),transforms.Normalize((0.5,), (0.5,)) # 假设输入通道为1(灰度图)
])# 检查数据存放路径是否存在,如果不存在,则下载数据集
if not os.path.exists(data_dir):os.makedirs(data_dir)# 载入MNIST数据集,设置download=True以下载数据(如果尚未存在)
train_dataset = datasets.MNIST(root=data_dir, train=True, download=True, transform=transform)
train_loader = DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)# 创建模型实例
model = AlexNet()
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model.to(device)# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=1e-4)# 训练函数
def train(model, train_loader, criterion, optimizer, num_epochs):model.train() # 设定模型为训练模式best_accuracy = 0.0 # 记录最佳准确率for epoch in range(num_epochs):running_loss = 0.0correct = 0total = 0for i, (images, labels) in enumerate(train_loader):images, labels = images.to(device), labels.to(device)# 前向传播outputs = model(images)loss = criterion(outputs, labels)# 反向传播和优化optimizer.zero_grad() # 清空梯度loss.backward() # 计算梯度optimizer.step() # 更新参数# 统计running_loss += loss.item()_, predicted = torch.max(outputs.data, 1)total += labels.size(0)correct += (predicted == labels).sum().item()# 每十个batch打印loss和准确率if (i + 1) % 10 == 0:print(f'Epoch [{epoch + 1}/{num_epochs}], Step [{i + 1}/{len(train_loader)}], Loss: {loss.item():.4f}, Accuracy: {100 * correct / total:.2f}%')# 计算并打印每个epoch的平均loss和准确率avg_loss = running_loss / len(train_loader)avg_accuracy = correct / total * 100print(f'Epoch [{epoch + 1}/{num_epochs}], Average Loss: {avg_loss:.4f}, Average Accuracy: {avg_accuracy:.2f}%')# 保存最佳模型if avg_accuracy > best_accuracy:best_accuracy = avg_accuracytorch.save(model.state_dict(), 'best_model.pth') # 保存模型权重print(f'Saved best model with accuracy: {best_accuracy:.2f}%')if __name__ == "__main__":train(model, train_loader, criterion, optimizer, num_epochs)
test.py
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
from model import AlexNet # 确保你的model.py文件在合适的位置 # 设置设备
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 数据变换(适合MNIST的数据)
transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) # 假设输入通道为1(灰度图)
]) # 载入MNIST测试集
test_dataset = datasets.MNIST(root='./data', train=False, download=False, transform=transform)
test_loader = DataLoader(dataset=test_dataset, batch_size=64, shuffle=False) # 创建模型实例并加载最佳模型权重
model = AlexNet()
model.load_state_dict(torch.load('best_model.pth'))
model.to(device)
model.eval() # 设置为评估模式 # 测试函数
def test(model, test_loader): correct = 0 total = 0 with torch.no_grad(): # 禁用梯度计算 for images, labels in test_loader: images, labels = images.to(device), labels.to(device) # 前向传播 outputs = model(images) _, predicted = torch.max(outputs.data, 1) total += labels.size(0) # 总样本数量 correct += (predicted == labels).sum().item() # 统计正确分类的样本数量 accuracy = 100 * correct / total print(f'Test Accuracy: {accuracy:.2f}%') if __name__ == "__main__": test(model, test_loader)
最终输出的准确率为99.2%,可以通过调参,将结果调到99.5%,手写数字识别数据集还是比较简单的,后续我会尝试用其他更大的数据集进行训练。
内容总结
我对卷积的认知就是提取特征并降低维度。因为图像往往都比较大,比如说224 * 224,如果直接把它展平,作为一个向量输入到全连接网络中,参数量就太大了,这样很难去训练。所以我们在使用全连接网络之前会通过卷积来提取图像特征,然后再它图像展平作为全连接网络的输入,最终得到我们想要的结果。大家也可以尝试多加几层卷积,来尝试对准确率的影响。