目 录
1 AlexNet网络
1.1 网络结构
1.2 AlexNet网络各层详解
2 利用Pytorch实现AlexNet网络
2.1 模型定义
2.2 训练过程
2.3 预测部分
该网络是2012年ISLVRC 2012竞赛的冠军网络,该网络的亮点在于:
①首次利用GPU进行网络加速训练;
②使用了ReLU激活函数;
③使用了LRN局部响应归一化;
④在全连接层的前两层中使用了Dropout随即失活神经元操作,以减少过拟合。
对于过拟合:
左图是网络的初始状态;经过训练会得到中间的图,得到一个效果比较好的分类边界;最右面的图能够完美的预测训练集,但是对于其他数据的预测效果差,过度拟合了训练数据但是没有考虑到泛化能力。过拟合的原因有很多,比如特征维度过多,模型假设过于复杂,参数过多,训练数据过少,噪声过多等等。
为了解决该问题,AlexNet网络的Dropout操作在正向传播过程中随机失活了一部分神经元,减少了网络的训练参数,从而可以达到减少过拟合的效果。
kernels | kernel_size | padding | stride | output_size | |
Conv1 | 96 | 11 | [1,2] | 4 | [55,55,96] |
Maxpool1 | \ | 3 | 0 | 2 | [27,27,96] |
Conv2 | 256 | 5 | [2,2] | 1 | [27,27,256] |
Maxpool2 | \ | 3 | 0 | 2 | [13,13,256] |
Conv3 | 384 | 3 | [1,1] | 1 | [13,13,384] |
Conv4 | 384 | 3 | [1,1] | 1 | [13,13,384] |
Conv5 | 256 | 3 | [1,1] | 1 | [13,13,256] |
Maxpool3 | \ | 3 | 0 | 2 | [6,6,256] |
FC1 | 2048 | \ | \ | \ | 2048 |
FC2 | 2048 | \ | \ | \ | 2048 |
FC3 | 1000 | \ | \ | \ | 1000 |
(注:输入为[224,224,3],输出层节点数要根据我们自己分类类别个数调整)
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]
nn.ReLU(inplace=True),
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)
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)
在初始化函数中定义网络正向传播过程中使用的层结构,使用了nn.Sequential模块,可以将一系列的层结构打包,组合成一个新的结构命名为features,专门用来提取图像的特征。若不使用该模块,每创建一个层结构都需要使用self.模块名称=该模块,会大大增加工作量。将全连接层利用nn.Sequential模块打包命名为classifier。
对于第一个卷积层,padding的值若为一个int(1),则表示在四周各补一列0;若为一个tuple(1, 2),则1代表在高度方向上(上方和下方)各补一行0,2代表在左右两侧各补两列0;若想精确的实现在左侧补一列,右侧补两列,上方补一列,下方补两列,需要用到nn.ZeroPad2d((1, 2, 1, 2))方法来实现更精细的操作。在此处为了方便输入一个整型2,但计算出来的H会是一个小数,此时Pytorch会自动将最右侧的一列0和最下侧的一行0舍弃掉,计算的输出是符合要求的。
然后经过一个激活函数nn.ReLU,inplace参数可以理解为Pytorch通过一种方法增加计算量,但是能够降低内存使用,可以通过该方法载入一个更大的模型。
再之后的层的设置和参数的设置同理,然后进入全连接层。AlexNet网络中使用了Dropout方法,一般放在全连接层与全连接层之间,在经过最后一个池化层之后,要将特征矩阵展平成一个一维向量与全连接层进行全连接,在这之间可以加入一个nn.Dropout函数,其中参数p代表随机失活神经元的比例,默认为0.5,在全连接层之后也要加上激活函数,最后一层的输出为数据集类别个数,在初始化函数中有定义默认值为1000,根据数据集不同可以传入不同的参数。
在模型定义时还定义了初始化权重的方法,在Pytorch中卷积层和全连接层是默认使用Kaiming初始化方法的,如果需要使用其他初始化方法也可以在此处定义。
在定义完网络组件之后,进行正向传播,首先将训练样本送入到features组件中,将得到的输出展平,注意是从第一维度(channel)开始展平,展成一个一维向量,再将其送入到classifier中,得到输出。
通过一个函数来实现GPU训练:
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print("using {} device.".format(device))
若当前有可使用的GPU设备,就是用第一块GPU来进行训练,若没有则使用CPU训练。
然后对数据进行预处理:
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)), # cannot 224, must (224, 224)
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])}
当Key为train时,RandomResizedCrop(224)将图片随机裁剪成224×224像素大小,RandomHorizontalFlip将图片在水平方向上随机翻转,然后转化成tensor类型并进行标准化处理;当Key为val时,将图片缩放成224×224的,也将图片转化成tensor类型并进行标准化处理。
读取训练集和测试集并且采用对应的图片处理方式:
train_dataset = datasets.ImageFolder(root="flower_data/train",
transform=data_transform["train"])
validate_dataset = datasets.ImageFolder(root="flower_data/val",
transform=data_transform["val"])
利用class_to_idx获取分类名称所对应的索引,并将key和val反过来,这样做预测完之后我们通过返回的索引,根据该字典能够直接直接获取到对应的类别,将该字典转换为json格式并写到json文件中(indent参数表示缩进几格),方便之后预测时读取类别:
# {'daisy':0, 'dandelion':1, 'roses':2, 'sunflower':3, 'tulips':4}
flower_list = train_dataset.class_to_idx
cla_dict = dict((val, key) for key, val in flower_list.items())
# write dict into json file
json_str = json.dumps(cla_dict, indent=4)
with open('class_indices.json', 'w') as json_file:
json_file.write(json_str)
定义batch_size并载入训练集和测试集:
batch_size = 32
train_loader = torch.utils.data.DataLoader(train_dataset,
batch_size=batch_size, shuffle=True,
num_workers=0)
validate_loader = torch.utils.data.DataLoader(validate_dataset,
batch_size=batch_size, shuffle=False,
num_workers=0)
实例化AlexNet,注意将传入参数改为5;将网络放在指定的设备上训练,定义损失函数和优化器(使用Adam优化器)并指定学习率:
net = AlexNet(num_classes=5, init_weights=True)
net.to(device)
loss_function = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.0002)
然后开始训练(代码已注释):
# 设定训练次数
epochs = 10
# 设置模型存储路径
save_path = './AlexNet.pth'
# 用来记录最优的正确率
best_acc = 0.0
# 一共有多少个batch(训练集数目 / batch_size),用于后面求平均损失
train_steps = len(train_loader)
# 开始迭代
for epoch in range(epochs):
# train
# 启用Dropout方法
net.train()
# 记录每一次的损失,所以每次迭代都清0
running_loss = 0.0
# 生成一个迭代器
train_bar = tqdm(train_loader, file=sys.stdout)
# 通过enumerate获得迭代器的索引和值
for step, data in enumerate(train_bar):
# 将值赋给图像和标签
images, labels = data
# 每次迭代都清空梯度
optimizer.zero_grad()
# 将图像送入到网络训练(注意添加到设备)
outputs = net(images.to(device))
# 将输出值和实际值做对比,计算损失(注意labels也要添加到设备)
loss = loss_function(outputs, labels.to(device))
# 将损失反向传播
loss.backward()
# 通过优化器更新节点参数
optimizer.step()
# print statistics
# 将每一次的损失累加,用于求平均损失
running_loss += loss.item()
# 输出进度
train_bar.desc = "train epoch[{}/{}] loss:{:.3f}".format(epoch + 1,
epochs,
loss)
# validate
# 禁用Dropout方法
net.eval()
# 用来记录正确预测的个数
acc = 0.0
# 不需要计算梯度也不进行反向传播
with torch.no_grad():
# 生成一个迭代器
val_bar = tqdm(validate_loader, file=sys.stdout)
# 开始验证过程
for val_data in val_bar:
# 将值赋给图像和标签
val_images, val_labels = val_data
# 将图像送入到网络训练(注意添加到设备)
outputs = net(val_images.to(device))
# 在outputs的第1维找最大值;返回的第0维是最大值,第1维是最大值对应的标签;[1]将标签赋给predict_y
predict_y = torch.max(outputs, dim=1)[1]
# 将预测值与实际值对比,相同返回1,否则返回0,求和可得正确的个数,通过item()获得其值
acc += torch.eq(predict_y, val_labels.to(device)).sum().item()
# 求预测正确率
val_accurate = acc / val_num
# 打印迭代次数,平均损失,预测正确率
print('[epoch %d] train_loss: %.3f val_accuracy: %.3f' %
(epoch + 1, running_loss / train_steps, val_accurate))
# 找到正确率最高的模型参数,存到目录
if val_accurate > best_acc:
best_acc = val_accurate
torch.save(net.state_dict(), save_path)
几个注意点:
①net.train()和net.eval()是为了保证Dropout方法只在训练过程中生效,在验证过程中不使用该方法;net.train()代表启用Dropout方法,net.eval()代表关闭Dropout方法。
②tqdm()用于在python长循环中添加一个进度提示信息,只需要传入任意的迭代器即可tqdm(iterator),对于参数file=sys.stdout是打印输出到控制台(在pycharm中加了这个参数,进度条是白色的,不加该参数进度条是红色的,还没整明白,之后再查查)。
③通过item()获取值显示精度更高,item()返回的是一个浮点型数据,正常返回的是一个tensor类型,所以我们在求loss或者accuracy时,一般使用item()。
训练完成后可以对图片进行预测:
# 用GPU训练
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# 图片预处理:缩放,转换成tensor,归一化
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_path = "sunflower1.png"
img = Image.open(img_path)
# 显示图片
plt.imshow(img)
# 对图片进行预处理,载入的图片是[H, W, C],经过预处理后会自动把深度提前[C, H, W]
img = data_transform(img)
# 要求的输入有四个维度,所以给图片加一个维度变为[N, C, H, W]
img = torch.unsqueeze(img, dim=0)
# 获取记录类别名称的json文件
json_path = 'class_indices.json'
# 解码成我们需要的字典
with open(json_path, "r") as f:
class_indict = json.load(f)
# 初始化网络
model = AlexNet(num_classes=5).to(device)
# 载入权重
weights_path = "AlexNet.pth"
# 利用torch.load加载权重并利用model.load_state_dict()函数把加载的权重复制到模型的权重中去
model.load_state_dict(torch.load(weights_path))
# 禁用Dropout方法
model.eval()
# 不需要计算梯度也不进行反向传播
with torch.no_grad():
# predict class
# 将图片送入网络,利用squeeze注意要将tensor转换到CPU,因为后面的numpy是CPU-only
output = torch.squeeze(model(img.to(device))).cpu()
# 利用softmax函数使输出满足概率分布
predict = torch.softmax(output, dim=0)
# 获取概率最大处所对应的索引值
predict_cla = torch.argmax(predict).numpy()
# 在记录类别名称的json文件中利用索引获取类别,并且利用索引获得类别对应的概率
print_res = "class: {} prob: {:.3}".format(class_indict[str(predict_cla)],
predict[predict_cla].numpy())
plt.title(print_res)
# 打印出该图片归于每一类的概率
for i in range(len(predict)):
print("class: {:10} prob: {:.3}".format(class_indict[str(i)],
predict[i].numpy()))
# 画图(包括图片展示和归于的最大可能类及其概率)
plt.show()
这里的torch.unsqueeze()函数和torch.squeeze()函数需要注意:torch.unsqueeze是在dim对应的位置加入一个大小为1的维度(本来图片的shape是[3, 224, 224],在dim=0处加一维,shape变为[1, 3, 224, 224]),将图片送入到网络,加入的batch维不变,因此output的shape由[1, 3, 224, 224]变为[1, 5],此时的torch.squeeze()函数会移除张量维度里面有大小为1的部分,因此output的shape为[5]。
上图为预测结果,我找了一张向日葵的图片,该模型预测图片有0.999的概率归属于向日葵。