文章目录
- 前言
- 一、先上代码
- 二、疑问解答
- 1. 为什么使用Inception
- 2. 为什么使用Residual
- 3. 为什么使用1*1卷积核
- 三、代码讲解
- 1. 这篇文章两份代码所做操作主要是添加了不同的类Inception和Residual
- 四、Reference
- 总结
前言
- 本文依旧使用Mnist数据集+CNN卷积神经网络学习多分类问题。但是本文会加入CNN的进阶内容。Mnist数据集相当于C语言的Hello World,是初学者必学的一个集合。
上图为MNIST数据集样图
一、先上代码
使用Residual的代码如下,重点在Residual类:
import torch
from torchvision import transforms
from torchvision import datasets
import torch.nn.functional as F
from torch.utils.data import DataLoader
import torch.optim as optim
import matplotlib.pyplot as plt
batch_size = 64
mnist_transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize([0.1307, ], [0.3081, ])])
train_datasets = datasets.MNIST("../data/mnist", train=True, download=True, transform=mnist_transform)
test_datasets = datasets.MNIST("../data/mnist", train=False, download=True, transform=mnist_transform)
train_dataloader = DataLoader(dataset=train_datasets, batch_size=batch_size, shuffle=True)
test_dataloader = DataLoader(dataset=test_datasets, batch_size=batch_size, shuffle=False)
class Residual(torch.nn.Module):
def __init__(self, channels):
super(Residual, self).__init__()
self.channels = channels
self.conv1 = torch.nn.Conv2d(channels, channels, kernel_size=3, padding=1)
self.conv2 = torch.nn.Conv2d(channels, channels, kernel_size=3, padding=1)
def forward(self, x):
y = F.relu(self.conv1(x))
y = self.conv2(x)
return F.relu(x + y)
class Net(torch.nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = torch.nn.Conv2d(1, 16, kernel_size=5)
self.conv2 = torch.nn.Conv2d(16, 32, kernel_size=5)
self.rBlock1 = Residual(16)
self.rBlock2 = Residual(32)
self.pooling = torch.nn.MaxPool2d(2)
self.linear = torch.nn.Linear(512, 10)
def forward(self, x):
in_size = x.size(0)
x = F.relu(self.pooling(self.conv1(x)))
x = self.rBlock1(x)
x = F.relu(self.pooling(self.conv2(x)))
x = self.rBlock2(x)
x = x.view(in_size, -1) # 88 * 4 * 4
x = self.linear(x)
return x
model = Net()
criterion = torch.nn.CrossEntropyLoss()
optim = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model.to(device)
def train(epoch):
running_loss = 0.0
for index, data in enumerate(train_dataloader, 0):
inputs, target = data
inputs, target = inputs.to(device), target.to(device) # 设置使用device机器
outputs = model(inputs)
loss = criterion(outputs, target)
optim.zero_grad()
loss.backward()
optim.step()
running_loss += loss.item()
if index % 300 == 299:
print("[%d %5d] loss :%3f " % (epoch + 1, index + 1, running_loss / 300))
def test():
total = 0
correct = 0
with torch.no_grad():
for data in test_dataloader:
inputs, label = data
inputs, label = inputs.to(device), label.to(device) # 设置使用device机器
outputs = model(inputs)
_, predicted = torch.max(outputs, dim=1)
total += label.size(0)
correct += (predicted == label).sum().item()
print("Accuracy: %d %%" % (100 * correct / total))
return 100 * correct / total
if __name__ == '__main__':
epoch_list = []
acc_list = []
for epoch in range(10):
train(epoch)
acc = test()
epoch_list.append(epoch)
acc_list.append(acc)
plt.plot(epoch_list, acc_list)
plt.xlabel("epoch")
plt.ylabel("accuracy")
plt.show()
使用Inception的代码如下,重点在Inception类:
import torch
from torchvision import transforms
from torchvision import datasets
import torch.nn.functional as F
from torch.utils.data import DataLoader
import torch.optim as optim
import matplotlib.pyplot as plt
batch_size = 64
mnist_transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize([0.1307, ], [0.3081, ])])
train_datasets = datasets.MNIST("../data/mnist", train=True, download=True, transform=mnist_transform)
test_datasets = datasets.MNIST("../data/mnist", train=False, download=True, transform=mnist_transform)
train_dataloader = DataLoader(dataset=train_datasets, batch_size=batch_size, shuffle=True)
test_dataloader = DataLoader(dataset=test_datasets, batch_size=batch_size,shuffle=False)
# 构建Inception类,把重复的步骤定义成类,减少代码重复工作
class Inception(torch.nn.Module):
def __init__(self, in_channels):
super(Inception, self).__init__()
self.branch_pool = torch.nn.Conv2d(in_channels, 24, kernel_size=1)
self.branch1x1 = torch.nn.Conv2d(in_channels, 16, kernel_size=1)
self.branch5x5_1 = torch.nn.Conv2d(in_channels, 16, kernel_size=1)
self.branch5x5_2 = torch.nn.Conv2d(16, 24, kernel_size=5, padding=2)
self.branch3x3_1 = torch.nn.Conv2d(in_channels, 16, kernel_size=1)
self.branch3x3_2 = torch.nn.Conv2d(16, 24, kernel_size=3, padding=1)
self.branch3x3_3 = torch.nn.Conv2d(24, 24, kernel_size=3, padding=1)
def forward(self, x):
branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1)
branch_pool = self.branch_pool(branch_pool)
branch1x1 = self.branch1x1(x)
branch5x5 = self.branch5x5_1(x)
branch5x5 = self.branch5x5_2(branch5x5)
branch3x3 = self.branch3x3_1(x)
branch3x3 = self.branch3x3_2(branch3x3)
branch3x3 = self.branch3x3_3(branch3x3)
outputs = [branch1x1, branch5x5, branch3x3, branch_pool]
return torch.cat(outputs, dim=1)
# 模型类
class Net(torch.nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = torch.nn.Conv2d(1, 10, kernel_size=5)
self.conv2 = torch.nn.Conv2d(88, 20, kernel_size=5)
self.inceptionA = Inception(in_channels=10) # 经过池化后还是10个通道
self.inceptionB = Inception(in_channels=20)
self.pooling = torch.nn.MaxPool2d(2)
self.linear = torch.nn.Linear(1408, 10)
def forward(self, x):
in_size = x.size(0)
x = F.relu(self.pooling(self.conv1(x)))
x = self.inceptionA(x)
x = F.relu(self.pooling(self.conv2(x)))
x = self.inceptionB(x)
x = x.view(in_size, -1) # 88 * 4 * 4
x = self.linear(x)
return x
model = Net()
criterion = torch.nn.CrossEntropyLoss()
optim = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model.to(device)
# 训练过程函数
def train(epoch):
running_loss = 0.0
for index, data in enumerate(train_dataloader, 0):
inputs, target = data
inputs, target = inputs.to(device), target.to(device) # 设置使用device机器
outputs = model(inputs)
loss = criterion(outputs, target)
optim.zero_grad()
loss.backward()
optim.step()
running_loss += loss.item()
if index % 300 == 299:
print("[%d %5d] loss :%3f " % (epoch + 1, index + 1, running_loss / 300))
# 测试函数
def test():
total = 0
correct = 0
with torch.no_grad():
for data in test_dataloader:
inputs, label = data
inputs, label = inputs.to(device), label.to(device) # 设置使用device机器
outputs = model(inputs)
_, predicted = torch.max(outputs, dim=1)
total += label.size(0)
correct += (predicted == label).sum().item()
print("Accuracy: %d %%" % (100 * correct / total))
return 100 * correct / total
if __name__ == '__main__':
epoch_list = []
acc_list = []
for epoch in range(10):
train(epoch)
acc = test()
epoch_list.append(epoch)
acc_list.append(acc)
plt.plot(epoch_list, acc_list)
plt.xlabel("epoch")
plt.ylabel("accuracy")
plt.show()
两份代码都和CNN训练MNIST数据集差不多,不同的是加了Inception和Residual类。对于程序看不懂,可进去这篇文章。Pytorch使用CNN实现基本的MNIST数据集学习 通俗理解CNN
二、疑问解答
这里声明,我使用了B站刘二大人的PPT作为讲解。
1. 为什么使用Inception
Inception也叫GoogLeNet,是一种基本的深度学习的结构,我们会在这种通过的结构上面做修改,以满足我们的模型需求。Inception还没提出来的时候,我们要增加网络的精确度,都是通过网络层数的加深的操作,去达到目的,但是随之而来的问题是,深度越深,可能会过拟合,可能会梯度消失。所以提出了Inception,我们先看看Inception的结构。
我们在Inception中进行了多层次的卷积,但是我们是横向扩展的,并非纵向扩展,我们做了多层卷积后,会将多层的卷积合并成一个整体,注意进行合并的时候,需要确保图像的大小即高度和宽度是一样的才能合并,如上图所示。
为什么要进行多层卷积后合并呢?
用通俗的话来讲就是,模型通过不同的视角去观察数据的特征,能够更好地拟合我们的模型,加快了收敛的速度。而不同的视角就是不同的卷积核去卷积,在上图中有(池化+11卷积),(11卷积),(11卷积+55卷积),(11卷积+33卷积+33卷积)。而在其中的11卷积也有融合更多信息去卷积的作用,例如在RGB通道中,1*1卷积把3个通道的值卷积到一个通道中,融合了3个通道的信息去卷积。详细点本文章下文
这里也有一篇文章,比较深入解析了Inception Module,但是里面存在一些数学概念,初学者可以先通俗理解了,再进一步学习。
Inception Module-深度解析
2. 为什么使用Residual
Residual Network的意思是残差网络,目的是为了解决高层数模型学习过程中梯度消失,退化各种的问题。
上图是经典论文 Deep Residual Learning for Image Recognition 中的插图,表示的是训练层数和错误率在训练集和测试集上的对比,我们可以看到我们想要我们的训练准确度高,并非单纯地增加我们的网络层数即可,这样子可能会适得其反。论文中也表示这个结果并非是过拟合导致的,或者并非仅仅是过拟合导致的。就像我们所说的,可能是梯度消失了。模型参数无法更新。
我们知道模型学习的时候,回归的时候,梯度的数值是由更浅层次的层的梯度数值相乘得到,这有点类似于高等数学的链式求导法则,但是当模型学习到一定程度的时候,梯度很小,比较深的层次得到的梯度值就会比较小,从而深层次的参数更新慢,从而训练效果一般。
下图是从数学本质去理解Residual
我们未使用Residual的时候是图片上方求梯度方式,g(x)和f(x)代表两个层,g(x)是前一层计算的结果,作为输入进入f(x),当前一层的梯度很小的时候,也就是序号2的数值很小,那么总的梯度值小,深层是得不到更新。而图片下方使用的是Residual的计算,通过增加上一层的输入值g(x),使得梯度不趋于0。可配合下方图片一起理解
下图是一个Residual架构设计案例。
它在内部使用了两个神经网络层,但是在结果输出的时候,我们会加一个输入值。上图的理解是这样,我们假设H(x)为我们想要拟合的函数曲线,x是已经在其他层数训练的数据,而我们F(x)要训练的是想要拟合的函数H(x)减去已经训练的x的部分,所以H(x)=F(x)+ x,将x移动到左边,就变成H(x)- x = F(x)。
但是Residual的理解方式很多
上图我们也可以理解为,将上一层训练的数据拿过来和新的数据一起去训练,也是就是那上一层残留的数据,所以Residual才有残留,残差等意思。
这里注意,上面的Residual只是一个很普通的设计,它并不是固定的,Residual可以有多种方式。
例如论文里给出了34层的Residual架构。对应下图的34-layer,除此之外,有更多其他层数的Residual架构,如下图,最后一行的FLOPs代表计算的大概复杂度。
这里注意!!!!!
- Residual只是作加法而已,并不会增加我们的模型复杂度。加法并没有权重去赋予那些额外的参数,所以模型的复杂度是不会增加的。
- 它的优点就是让模型收敛快,模型精确度高,如下图,对比了使用和未使用的情况。
在图中的第一段,可以看到使用的Residual,它的收敛速度比较快,而迭代到最后,使用了Residual的模型精确度也会更高。
3. 为什么使用1*1卷积核
减少卷积计算量!!!
这里我们用一个55的卷积层作为例子,我们去对比用了11卷积核和未使用时候的计算量差距。
我们在上图下面的例子中使用了一个11的卷积核,它所进行的计算也类似33的卷积,通过卷积计算,我们把原来192维度的数据缩小至16个维度,进行了信息的融合,然后再进行55的卷积。下图是计算量的对比。
通过对比,我们可以看到使用了11的卷积的计算量明显减少了!这就是它的作用!所以使用了1*1的卷积核的网络也叫做Network in Network(NIN)。
三、代码讲解
1. 这篇文章两份代码所做操作主要是添加了不同的类Inception和Residual
class Inception(torch.nn.Module):
def __init__(self, in_channels):
super(Inception, self).__init__()
self.branch_pool = torch.nn.Conv2d(in_channels, 24, kernel_size=1)
self.branch1x1 = torch.nn.Conv2d(in_channels, 16, kernel_size=1)
self.branch5x5_1 = torch.nn.Conv2d(in_channels, 16, kernel_size=1)
self.branch5x5_2 = torch.nn.Conv2d(16, 24, kernel_size=5, padding=2)
self.branch3x3_1 = torch.nn.Conv2d(in_channels, 16, kernel_size=1)
self.branch3x3_2 = torch.nn.Conv2d(16, 24, kernel_size=3, padding=1)
self.branch3x3_3 = torch.nn.Conv2d(24, 24, kernel_size=3, padding=1)
def forward(self, x):
branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1)
branch_pool = self.branch_pool(branch_pool)
branch1x1 = self.branch1x1(x)
branch5x5 = self.branch5x5_1(x)
branch5x5 = self.branch5x5_2(branch5x5)
branch3x3 = self.branch3x3_1(x)
branch3x3 = self.branch3x3_2(branch3x3)
branch3x3 = self.branch3x3_3(branch3x3)
outputs = [branch1x1, branch5x5, branch3x3, branch_pool]
return torch.cat(outputs, dim=1)
上面的Inception类,按照下图的架构进行构建的。
class Residual(torch.nn.Module):
def __init__(self, channels):
super(Residual, self).__init__()
self.channels = channels
self.conv1 = torch.nn.Conv2d(channels, channels, kernel_size=3, padding=1) # 使用padding让图像的大小不变
self.conv2 = torch.nn.Conv2d(channels, channels, kernel_size=3, padding=1)
def forward(self, x):
y = F.relu(self.conv1(x))
y = self.conv2(x)
return F.relu(x + y)
上面代码就是Inception的主要类,按照下图的架构构建
四、Reference
Deep Residual Learning for Image Recognition
总结
以上就是我个人对Inception和Residual的理解,希望配合上其他文章,能让初学者更容易理解。如果觉得有用,请大家点赞支持!!!!。