B站UP:霹雳吧啦Wz
课程合集链接:1.1 卷积神经网络基础_哔哩哔哩_bilibili
代码参考B站UP:霹雳吧啦Wz的《深度学习-图像分类篇章》视频,代码根据个人编程习惯及本人自用数据集进行少量改动,欢迎大佬们批评指正。
1 网络结构
inception 有什么好处呢?Szegedy从多个角度进行了解释:
解释1:在直观感觉上在多个尺度上同时进行卷积,能提取到不同尺度的特征。特征更为丰富也意味着最后分类判断时更加准确。
解释2:利用稀疏矩阵分解成密集矩阵计算的原理来加快收敛速度。举个例子下图左侧是个稀疏矩阵(很多元素都为 0,不均匀分布在矩阵中),和一个 2x2 的矩阵进行卷积,需要对稀疏矩阵中的每一个元素进行计算;如果像下图右图那样把稀疏矩阵分解成2个子密集矩阵,再和 2x2 矩阵进行卷积,稀疏矩阵中 0 较多的区域就可以不用计算,计算量就大大降低。这个原理应用到 inception 上就是要在特征维度上进行分解!传统的卷积层的输入数据只和一种尺度(比如 3x3 )的卷积核进行卷积,输出固定维度(比如 256 个特征)的数据,所有 256 个输出特征基本上是均匀分布在 3x3 尺度范围上,这可以理解成输出了一个稀疏分布的特征集;而 inception 模块在多个尺度上提取特征(比如 1x1,3x3,5x5 ),输出的 256 个特征就不再是均匀分布,而是相关性强的特征聚集在一起(比如 1x1 的 96 个特征聚集在一起,3x3 的 96 个特征聚集在一起,5x5 的 64 个特征聚集在一起),这可以理解成多个密集分布的子特征集。这样的特征集中因为相关性较强的特征聚集在了一起,不相关的非关键特征就被弱化,同样是输出 256 个特征,Inception 方法输出的特征“冗余”的信息较少。用这样的“纯”的特征集层层传递最后作为反向计算的输入,自然收敛的速度更快。
来自 深度学习之图像分类(五)--GoogLeNet网络结构_图像分类网络结构_木卯_THU的博客-CSDN博客
辅助分类器(注意:在训练时使用,在测试时并不使用辅助分类器)
由于网络比较深,梯度传到前面几层的时候很可能就消失了,所以定义辅助分类器。Inception Net一共有22层,除了最后一层的输出结果,中间节点的分类效果也有可能是很好的,所以GoogLeNet将中间某一层的输出作为分类,并以一个较小的权重(0.3)加到最终的分类结果中。一共有2个这样的辅助分类节点。
辅助分类器相当于对模型做了融合,同时给网络增加了反向传播的梯度信号,在一定程度上提供了正则化的作用。
来自 <GoogLeNet 神经网络结构_辅助分类器_-牧野-的博客-CSDN博客>
2 代码实现
2.1 模型搭建
import torch
# 把卷积层和激活函数打包成一个基础的卷积模块
class BasicConv2d(torch.nn.Module):
def __init__(self, in_channels, out_channels, **kwargs): # **kwargs表示传入任意数量的参数,可能包括kernel_size, stride, padding等
super(BasicConv2d, self).__init__()
self.conv = torch.nn.Conv2d(in_channels, out_channels, **kwargs)
self.active = torch.nn.ReLU(inplace=True)
def forward(self, x):
x = self.conv(x)
x = self.active(x)
return x
# 定义Inception模块
class Inception(torch.nn.Module):
def __init__(self, in_channels, ch1x1, ch3x3reduce, ch3x3, ch5x5reduce, ch5x5, ch_pool): # 输入Inception的4条分支的参数
super(Inception, self).__init__()
# 第一个分支
self.branch1 = BasicConv2d(in_channels, ch1x1, kernel_size=1)
# 第二个分支
self.branch2 = torch.nn.Sequential(
BasicConv2d(in_channels, ch3x3reduce, kernel_size=1),
BasicConv2d(ch3x3reduce, ch3x3, kernel_size=3, padding=1, stride=1),
)
# 第三个分支
self.branch3 = torch.nn.Sequential(
BasicConv2d(in_channels, ch5x5reduce, kernel_size=1),
BasicConv2d(ch5x5reduce, ch5x5, kernel_size=5, padding=2, stride=1),
)
# 第四个分支
self.branch4 = torch.nn.Sequential(
torch.nn.MaxPool2d(kernel_size=3, padding=1, stride=1),
BasicConv2d(in_channels, ch_pool, kernel_size=1),
)
def forward(self, x):
branch1 = self.branch1(x)
branch2 = self.branch2(x)
branch3 = self.branch3(x)
branch4 = self.branch4(x)
outputs = [branch1, branch2, branch3, branch4]
# [batch, channels, H, W] 从channels维度进行拼接
return torch.cat(outputs, dim=1)
# 定义辅助分类器
class InceptionAux(torch.nn.Module):
def __init__(self, in_channels, num_classes):
super(InceptionAux, self).__init__()
self.averagepool = torch.nn.AvgPool2d(kernel_size=5, stride=3)
self.conv = BasicConv2d(in_channels, 128, kernel_size=1)
self.fc1 = torch.nn.Linear(128*4*4, 1024)
self.fc2 = torch.nn.Linear(1024, num_classes)
def forward(self, x):
# 辅助分类器aux1: N x 512 x 14 x 14, 辅助分类器aux2: N x 528 x 14 x 14
x = self.averagepool(x)
# aux1: N x 512 x 4 x 4, aux2: N x 528 x 4 x 4
x = self.conv(x)
# N x 128 x 4 x 4
x = torch.flatten(x, start_dim=1)
# N x 2048
# 在model.train()模式下self.training=True,在model.eval()模式下self.training=False,为Ture时执行dropout,为False时不执行
x = torch.nn.functional.dropout(x, p=0.5, training=self.training)
x = self.fc1(x)
x = torch.nn.functional.relu(x, inplace=True)
x = torch.nn.functional.dropout(x, p=0.5, training=self.training)
# x = torch.nn.ReLU(inplace=True)
# x = torch.nn.Dropout(0.5)
x = self.fc2(x)
return x
class Model(torch.nn.Module):
# 在定义模型时aux_logits=True, init_weight=False这种,如果实例化模型后传入了aux_logits,aux_logits,以传入的为准,如果没有传入这个参数,就以定义模型时设置的为准
def __init__(self, num_classes, aux_logits=True, init_weights=False): # aux_logits=True表示使用辅助分类器
super(Model, self).__init__()
self.aux_logits = aux_logits
self.conv1 = BasicConv2d(3, 64, kernel_size=7, stride=2, padding=3)
self.maxpool1 = torch.nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True) # ceil_mode=True代表计算结果向上取整(可以理解成在原来的数据上补充了值为-NAN的边界)
self.conv2 = BasicConv2d(64, 64, kernel_size=1)
self.conv3 = BasicConv2d(64, 192, kernel_size=3, padding=1)
self.maxpool2 = torch.nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True)
self.incep3a = Inception(192, 64, 96, 128, 16, 32, 32)
self.incep3b = Inception(256, 128, 128, 192, 32, 96, 64)
self.maxpool3 = torch.nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True)
self.incep4a = Inception(480, 192, 96, 208, 16, 48, 64)
self.incep4b = Inception(512, 160, 112, 224, 24, 64, 64)
self.incep4c = Inception(512, 128, 128, 256, 24, 64, 64)
self.incep4d = Inception(512, 112, 144, 288, 32, 64, 64)
self.incep4e = Inception(528, 256, 160, 320, 32, 128, 128)
self.maxpool4 = torch.nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True)
self.incep5a = Inception(832, 256, 160, 320, 32, 128, 128)
self.incep5b = Inception(832, 384, 192, 384, 48, 128, 128)
if aux_logits: # 如果传入的aux_logits为True,则定义self.aux1和self.aux2
self.aux1 = InceptionAux(512, num_classes)
self.aux2 = InceptionAux(528, num_classes)
self.averagepool = torch.nn.AvgPool2d(kernel_size=7, stride=1)
self.dropout = torch.nn.Dropout(0.4)
self.fc = torch.nn.Linear(1024, num_classes)
if init_weights: # 如果传入的init_weight为True,则采用_initialize_weights()的参数初始化方法
self._initialize_weights()
def forward(self, x):
x = self.conv1(x)
x = self.maxpool1(x)
x = self.conv2(x)
x = self.conv3(x)
x = self.maxpool2(x)
x = self.incep3a(x)
x = self.incep3b(x)
x = self.maxpool3(x)
x = self.incep4a(x)
if self.training and self.aux_logits: # 如果是训练过程,并且使用辅助分类器
aux1 = self.aux1(x)
x = self.incep4b(x)
x = self.incep4c(x)
x = self.incep4d(x)
if self.training and self.aux_logits: # 如果是训练过程,并且使用辅助分类器
aux2 = self.aux2(x)
x = self.incep4e(x)
x = self.maxpool4(x)
x = self.incep5a(x)
x = self.incep5b(x)
x = self.averagepool(x)
x = torch.flatten(x, start_dim=1)
x = self.dropout(x)
x = self.fc(x)
if self.training and self.aux_logits: # 如果是训练过程,并且使用辅助分类器
return x, aux2, aux1
return x # 如果不是训练过程,或者不使用辅助分类器,就直接返回x
def _initialize_weights(self):
for m in self.modules(): # 遍历每一层网络结构
if isinstance(m, torch.nn.Conv2d): # 如果是卷积层
torch.nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu') # (何)恺明初始化方法
# torch.nn.init.xavier_uniform_(m.weight)
if m.bias is not None: # 如果有偏置参数
torch.nn.init.constant_(m.bias, 0) # 把偏置参数初始化为0
elif isinstance(m, torch.nn.Linear): # 如果是全连接层
# torch.nn.init.xavier_uniform_(m.weight)
torch.nn.init.normal_(m.weight, 0, 0.01)
torch.nn.init.constant_(m.bias, 0) # 把偏置参数初始化为0
2.2 模型训练
import os
import torch
from torch.utils.data import DataLoader
from torchvision import transforms, datasets
import matplotlib.pyplot as plt
from GoogLeNet_model import Model
batch_size = 32
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))]),
"test":transforms.Compose([transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
}
# 获得数据集的路径
data_root = os.path.abspath(os.path.join(os.getcwd(), "../"))
data_path = data_root + "/dataset/SIPaKMeD/"
# 加载训练集
train_dataset = datasets.ImageFolder(root=os.path.join(data_path, "train"), transform=transform["train"])
train_loader = DataLoader(dataset=train_dataset, shuffle=True, batch_size=batch_size)
# 加载测试集
test_dataset= datasets.ImageFolder(root=os.path.join(data_path, "test"), transform=transform["test"])
test_loader = DataLoader(dataset=test_dataset, shuffle=True, batch_size=batch_size)
print("using {} images for training, {} images for test".format(len(train_dataset), len(test_dataset)))
model = Model(num_classes=5, aux_logits=True, init_weights=True)
# 如果要使用官方的预训练权重,注意是将权重载入官方的模型,不是我们自己实现的模型
# 官方的模型中使用了bn层以及改了一些参数,不能混用
# import torchvision
# net = torchvision.models.googlenet(num_classes=5)
# model_dict = net.state_dict()
# # 预训练权重下载地址: https://download.pytorch.org/models/googlenet-1378be20.pth
# pretrain_model = torch.load("googlenet.pth")
# del_list = ["aux1.fc2.weight", "aux1.fc2.bias",
# "aux2.fc2.weight", "aux2.fc2.bias",
# "fc.weight", "fc.bias"]
# pretrain_dict = {k: v for k, v in pretrain_model.items() if k not in del_list}
# model_dict.update(pretrain_dict)
# net.load_state_dict(model_dict)
loss_function = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.0003)
loss_list = []
accuracy_list = []
best_acc = 0
for epoch in range(10):
model.train()
loss_sum = 0
for i, (inputs, targets) in enumerate(train_loader):
logits, logits_aux2, logits_aux1 = model(inputs)
loss0 = loss_function(logits, targets)
loss1 = loss_function(logits_aux1, targets)
loss2 = loss_function(logits_aux2, targets)
loss = loss0 + 0.3 * loss1 + 0.3 * loss2
loss_sum += loss.item()
loss_list.append(loss.item())
optimizer.zero_grad()
loss.backward()
optimizer.step()
rate = (i+1) / len(train_loader)
a = "*" * int(rate * 50)
b = "-" * int(50 * (1-rate))
print("\rtrain loss:{:.3f}% [{}->{}] {:.3f}".format(rate*100, a, b, loss), end="")
print()
model.eval()
total = 0
correct = 0
with torch.no_grad():
for (images, labels) in test_loader:
y_pred = model(images)
_,predicted = torch.max(y_pred.data, dim=1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
accuracy_list.append(correct / total)
print("[epoch %d], train_loss:%.3f, test_accuracy:%.3f%% " % (epoch+1, loss_sum / (i+1), 100 * correct / total))
if correct / total > best_acc:
best_acc = correct / total
torch.save(model.state_dict(), "GoogLeNet.pth")
print("Finishing Training")
plt.subplot(121)
plt.plot(range(len(loss_list)), loss_list)
plt.xlabel("step")
plt.ylabel("loss")
plt.subplot(122)
plt.plot(range(epoch + 1), accuracy_list)
plt.xlabel("epoch")
plt.ylabel("accuracy")
plt.show()
2.3 模型预测
import torch
from torchvision import transforms
from PIL import Image
import matplotlib.pyplot as plt
from GoogLeNet_model import Model
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("im_Dyskeratotic.bmp")
plt.imshow(img)
img = transform(img)
img = torch.unsqueeze(img, dim=0)
model = Model(num_classes=5, aux_logits=False)
missing_keys, unexpected_keys = model.load_state_dict(torch.load("GoogLeNet.pth"), strict=False)
classes = ["im_Dyskeratotic", "im_Koilocytotic", "im_Metaplastic", "im_Parabasal", "im_Superficial_Intermediate"]
model.eval()
with torch.no_grad():
output = model(img)
predict = torch.softmax(output, dim=1).numpy() # torch.softmax返回的是一个张量形式的二维矩阵,通过.numpy()转换成数组形式的二维矩阵
print(predict)
predict_cla = torch.argmax(output).item() # 把张量形式的值取出来
# print(predict_cla)
print("img is predicted as %s, accuracy is %.3f" % (classes[predict_cla], predict[0][predict_cla]))
plt.show()