一. AlexNet 神经网络介绍
AlexNet 是深度学习历史上一个非常重要的里程碑,它代表了深度卷积神经网络(Convolutional Neural Network, CNN)的一次重大突破。AlexNet 由多伦多大学的Alex Krizhevsky、Ilya Sutskever和Geoffrey Hinton 设计,并在 2012 年的 ImageNet 大规模视觉识别挑战赛(ILSVRC)中赢得了冠军。这一胜利显著降低了图像分类的错误率,从而证明了深度学习在处理复杂视觉任务上的潜力。
二. AlexNet 神经网络结构
AlexNet 的网络结构在深度学习领域具有开创性的意义,其详细结构如下:
输入层
- 输入图像尺寸:227x227x3( RGB 图像)
卷积层
- 第一层 (C1):包含 96 个 11x11 的卷积核,步长为 4,没有填充(padding),输出尺寸为 55x55x96。
- 第二层 (C2):包含 256个5x5 的卷积核,步长为 1,使用 2 的填充,输出尺寸为 27x27x256。
- 第三层 (C3):包含 384个3x3 的卷积核,步长为 1,使用 1 的填充,输出尺寸为 13x13x384。
- 第四层 (C4):包含 384个3x3 的卷积核,步长为 1,使用 1 的填充,输出尺寸为 13x13x384。
- 第五层 (C5):包含 256个3x3 的卷积核,步长为 1,使用 1 的填充,输出尺寸为 13x13x256。
池化层
- 在第一层和第二层卷积层后,分别有一个最大池化层,池化窗口大小为 3x3,步长为 2,这样减少了特征图的尺寸,同时保留了重要特征。
- 第五层卷积层后也有一个最大池化层,同样使用 3x3 的窗口和步长 2。
全连接层
- 第一个全连接层 (FC6):将前一层的输出展平为一维向量,然后通过一个含有 4096 个神经元的全连接层。
- 第二个全连接层 (FC7):同样含有 4096 个神经元。
- 第三个全连接层 (FC8):输出层,含有 1000 个神经元,对应 ImageNet 数据集中的 1000 个类别。
激活函数
- 在每个卷积层和全连接层之后,使用 ReLU(Rectified Linear Unit)作为激活函数,ReLU 函数可以增加网络的非线性表达能力,同时也加速了训练过程。
特殊层
- 局部响应归一化 (Local Response Normalization, LRN):在网络的前面几层中使用,以增强特征的鲁棒性。LRN对每个位置上的特征进行标准化,使得响应较大的特征更加突出,类似于生物视觉系统中的侧抑制效应。
训练技巧
- 使用重叠的最大池化(Overlapping Pooling)来减少过拟合。
- 在全连接层中使用 Dropout 技术来进一步防止过拟合。
- 数据增强,包括随机裁剪和水平翻转,以及颜色抖动,以增加模型的泛化能力。
并行处理
- AlexNet 最初是在两个 GPU 上训练的,网络被分割成两半,每半在单独的 GPU 上处理,然后在最后合并结果。
以上就是 AlexNet 的完整结构,它的设计和训练方法为后来的深度学习研究者提供了许多有价值的思路和实践指导。
三. AlexNet模型亮点
AlexNet 模型在其时代具有多项创新和亮点,这些特点使其在 2012 年的 ImageNet 大规模视觉识别挑战赛中脱颖而出,并对后续的深度学习研究产生了深远影响。以下是 AlexNet 模型的一些主要亮点:
-
ReLU 激活函数的使用:
AlexNet 首次在大规模网络中使用 ReLU(Rectified Linear Unit)作为激活函数,替代了传统的 sigmoid 或 tanh 函数。ReLU 函数可以加速训练过程,因为它的导数对于正数是常数 1,这有助于解决梯度消失问题,使深度网络更容易优化。 -
Dropout 正则化:
AlexNet 引入了 Dropout 技术,这是一种正则化策略,通过在训练过程中随机“丢弃”(即暂时禁用)网络中的一部分神经元,来减少模型的复杂度和防止过拟合。这种随机失活机制迫使网络学习更加健壮的特征表示。 -
重叠最大池化(Overlapping Max Pooling):
AlexNet 使用了重叠的最大池化,即池化区域大于步长,这有助于保留更多细节,避免了平均池化的模糊效果,从而提高了模型的准确性。 -
局部响应归一化(Local Response Normalization, LRN):
在每个卷积层之后,AlexNet 应用了 LRN 层,这可以看作是一种形式的空间规范化,有助于提高模型的泛化能力,尤其是在早期的卷积层中。 -
端到端训练:
AlexNet 能够直接从原始的 RGB 图像开始训练,不需要任何预处理或手工特征提取,这展示了深度学习模型强大的自动特征学习能力。 -
GPU 加速:
AlexNet 充分利用了 GPU 的并行计算能力,大大加速了训练过程。事实上,AlexNet 是首批在训练过程中充分展示 GPU 效能的深度学习模型之一。 -
大规模数据集和数据增强:
AlexNet 的成功部分归功于使用了 ImageNet 这样的大规模数据集进行训练,同时还使用了数据增强技术,如随机裁剪、翻转和颜色变化,以增加训练样本的多样性,进一步提升模型的泛化能力。 -
深度网络结构:
AlexNet 是一个深度网络,包含多个卷积层和全连接层,这比之前的 LeNet-5 等网络要深得多,表明深度结构能够学习到更复杂和抽象的图像特征。
AlexNet 的这些创新点不仅提高了在 ImageNet 竞赛中的性能,也奠定了现代深度学习和卷积神经网络的基础,启发了后续许多成功的网络架构,如 VGGNet、GoogleNet/Inception 系列、ResNet 等。
四. AlexNet 代码实现
开发环境配置说明:本项目使用 Python 3.6.13 和 PyTorch 1.10.2 构建,适用于CPU环境。
- model.py:定义 AlexNet 网络模型
- train.py:加载数据集并训练,计算 loss 和 accuracy,保存训练好的网络参数
- predict.py:用自己的数据集进行分类测试
- model.py
import torch.nn as nn
import torch
class AlexNet(nn.Module):
def __init__(self, num_classes=1000, init_weights=False):
super(AlexNet, self).__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 48, kernel_size=11, stride=4, padding=2), # input[3, 224, 224] output[48, 55, 55]
# padding 只能传入两种类型 int整型和tuple型
# 若传入整型,即padding=1,则会在特征图上下左右都补一行零
# 若传入tuple型,即padding=(1,2),则1代表上下方各补一行零,2代表左右两侧各补两列零
# 补充:nn.ZeroPad2d((1,2,1,2)) 表示左侧补一列、右侧补两列、上方补一行、下方补两行
nn.ReLU(inplace=True),
# inplace=True:Pytorch增加计算量,降低内存使用量的一种方法
nn.MaxPool2d(kernel_size=3, stride=2), # output[48, 27, 27]
nn.Conv2d(48, 128, kernel_size=5, padding=2), # output[128, 27, 27]
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3,stride=2), # output[128, 13, 13]
nn.Conv2d(128, 192, kernel_size=3, padding=1), # output[192, 13, 13]
nn.ReLU(inplace=True),
nn.Conv2d(192, 192, kernel_size=3, padding=1), # output[192, 13, 13]
nn.ReLU(inplace=True),
nn.Conv2d(192, 128, kernel_size=3, padding=1), # output[128, 13, 13]
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2), # output[128, 6, 6]
)
self.classifier = nn.Sequential(
nn.Dropout(p=0.5),
nn.Linear(128 * 6 * 6, 2048),
nn.ReLU(inplace=True),
nn.Dropout(p=0.5),
nn.Linear(2048, 2048),
nn.ReLU(inplace=True),
nn.Linear(2048, num_classes)
)
if init_weights:
self._initialize_weights()
def forward(self, x):
x = self.features(x)
x = torch.flatten(x, start_dim=1)
# start_dim=1 展平索引从1开始,[B, C, H, W]
x = self.classifier(x)
return x
def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 0.01)
nn.init.constant_(m.bias, 0)
- train.py
import torch
import torch.nn as nn
from torchvision import transforms, datasets, utils
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
import numpy as np
import torch.optim as optim
from model import AlexNet
import os
import json
import time
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# print(device)
data_transform = {
"train" : transforms.Compose([transforms.RandomResizedCrop(224), # 随机裁剪
transforms.RandomHorizontalFlip(), # 随机翻转
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5),(0.5, 0.5, 0.5))]),
"val" : transforms.Compose([transforms.Resize((224, 224)), # 不能224,必须(224, 224)
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5),(0.5, 0.5, 0.5))])}
# 获取数据集所在的根目录
# 通过os.getcwd()获取当前的目录,并将当前目录与".."链接获取上一层目录
data_root = os.path.abspath(os.path.join(os.getcwd(), ".."))
# 获取花类数据集路径
image_path = data_root + "/data_set/flower_data/"
# 加载数据集
train_dataset = datasets.ImageFolder(root=image_path + "/train",
transform=data_transform["train"])
# 获取训练集图像
train_num = len(train_dataset)
# 获取分类的名称
# {'daisy': 0, 'dandelion': 1, 'roses': 2, 'sunflowers': 3, 'tulips': 4}
flower_list = train_dataset.class_to_idx
# 采用遍历方法,将分类名称的key与value反过来
cla_dict = dict((val, key) for key, val in flower_list.items())
# 将字典cla_dict编码为json格式
json_str = json.dumps(cla_dict, indent=4)
with open("class_indices.json", "w") as json_file:
json_file.write(json_str)
batch_size = 32
train_loader = DataLoader(train_dataset,
batch_size=batch_size,
shuffle=True,
num_workers=0)
validate_dataset = datasets.ImageFolder(root=image_path + "/val",
transform=data_transform["val"])
val_num = len(validate_dataset)
validate_loader = DataLoader(validate_dataset,
batch_size=batch_size,
shuffle=True,
num_workers=0)
# 将测试集validate_loader转换为可迭代的迭代器
test_data_iter = iter(validate_loader)
# 通过next方法可以获取一批数据,包括图像及其对应的标签值
test_image, test_label = next(test_data_iter)
# def imshow(img):
# img = img / 2 + 0.5 # 反标准化
# npimg = img.numpy() # 转化为np格式
# # 将Tensor转换为原来载入时的shape
# # 将 [channel, height, width] 转为 [height, width, channel]
# plt.imshow(np.transpose(npimg, (1, 2, 0)))
# plt.show()
# # 打印标签
# print(' '.join('%5s' % cla_dict[test_label[j].item()] for j in range(4)))
# # 展示图片
# imshow(utils.make_grid(test_image))
net = AlexNet(num_classes=5, init_weights=True) # 实例化模型
net.to(device)
loss_function = nn.CrossEntropyLoss() # 定义损失函数
#pata = list(net.parameters()) # 查看模型参数
optimizer = optim.Adam(net.parameters(), lr=0.0002) # 定义优化器
# 设置存储权重路径
save_path = './AlexNet.pth'
best_acc = 0.0
for epoch in range(10):
# train
net.train() # 用来管理Dropout方法:训练时使用Dropout方法,验证时不使用Dropout方法
running_loss = 0.0 # 用来累加训练中的损失
t1 = time.perf_counter() # 时间戳,用来计算训练时间
for step, data in enumerate(train_loader, start=0):
# 获取数据的图像和标签
images, labels = data
# 将历史损失梯度清零
optimizer.zero_grad()
# 参数更新
outputs = net(images.to(device)) # 获得网络输出
loss = loss_function(outputs, labels.to(device)) # 计算loss
loss.backward() # 误差反向传播
optimizer.step() # 更新节点参数
# 打印统计信息
running_loss += loss.item()
# 打印训练进度
rate = (step + 1) / len(train_loader)
a = "*" * int(rate * 50)
b = "." * int((1 - rate) * 50)
print("\rtrain loss: {:^3.0f}%[{}->{}]{:.3f}".format(int(rate * 100), a, b, loss), end="")
print()
print(time.perf_counter() - t1)
# validate
net.eval() # 关闭Dropout方法
acc = 0.0
# 验证过程中不计算损失梯度
with torch.no_grad():
for data_test in validate_loader:
test_images, test_labels = data_test
outputs = net(test_images.to(device))
predict_y = torch.max(outputs, dim=1)[1]
# acc用来累计验证集中预测正确的数量
# 对比预测值与真实标签,sum()求出预测正确的累加值,item()获取累加值
acc += (predict_y == test_labels.to(device)).sum().item()
accurate_test = acc / val_num
# 如果当前准确率大于历史最优准确率
if accurate_test > best_acc:
# 更新历史最优准确率
best_acc = accurate_test
# 保存当前权重
torch.save(net.state_dict(), save_path)
# 打印相应信息
print("[epoch %d] train_loss: %.3f test_accuracy: %.3f"%
(epoch + 1, running_loss / step, acc / val_num))
print("Finished Training")
- predict.py
import torch
from model import AlexNet
from PIL import Image
from torchvision import transforms
import matplotlib.pyplot as plt
import json
data_transform = transforms.Compose(
[transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5),(0.5, 0.5, 0.5))])
# 加载预测图片
img = Image.open("./郁金香.png")
# 展示图片
plt.imshow(img)
# 图像预处理 [C, H, W]
img = data_transform(img)
# 扩充图像维度 [N, C, H, W]
img = torch.unsqueeze(img, dim=0)
# 读取 class_indict
try:
json_file = open("./class_indices.json", "r")
class_indict = json.load(json_file)
except Exception as e:
print(e)
exit(-1)
# 初始化网络
model = AlexNet(num_classes=5)
# 加载权重
model_weight_path = "./AlexNet.pth"
# 载入网络模型
model.load_state_dict(torch.load(model_weight_path))
# 采用eval()模式,关闭Dropout方法
model.eval()
# 不去跟踪变量的损失梯度
with torch.no_grad():
# model(img)将图像输入模型得到输出,采用squeeze压缩维度,即将Batch维度压缩掉
output = torch.squeeze(model(img))
# 采用softmax将最终输出转化为概率分布
predict = torch.softmax(output, dim=0)
# 获取概率最大处的索引值
predict_cla = torch.argmax(predict).numpy()
# 打印类别名称及其对应的预测概率
print(class_indict[str(predict_cla)], predict[predict_cla].item())
plt.show()