经典的CNN网络:
由多个堆叠的卷积层和空间池化层组成,通过线性的卷积核+非线性激活函数(ReLU、sigmoid、tanh等)生成特征图
NIN网络:
NIN的结构和传统的神经网络中多层的结构有些类似,后者的多层是跨越了不同尺寸的感受野(通过层与层中间加pool层),从而在更高尺度上提取出特征;NIN结构是在同一个尺度上的多层(中间没有pool层),从而在相同的感受野范围能提取更强的非线性。
NiN块:
- 是NiN中的基础块。
- 由一个卷积层加两个充当全连接层的卷积层(1x1卷积层)串联而成,
- 第一个卷积层的超参数可以自行设置,而第二和第三个卷积层的超参数一般是固定的
- 1x1卷积的作用(池化层压缩宽高,对通道数不起作用)
- 增加非线性函数
- 更改通道数
- 为什么要用到1x1卷积?
- 保持特征图的尺寸
- 把各通道的输入特征图进行线性加权,起到综合各个通道特征图信息的作用
多几层这样的1X1卷积的话,最终提取的特征会更加抽象,如果不用1X1卷积而是用更大的卷积核,那和连续用GLM有什么区别?就是因为用了1X1卷积,才增强了常规卷积的抽象表达能力
MLPconv其实就是在常规卷积后面加了N层1X1卷积,
1x1卷积作为NIN函数逼近器基本单元,除了增强了网络局部模块的抽象表达能力外,
在现在看来, 还可以实现跨通道特征融合和通道升维降维。
创新点:
MLPconv:
先前CNN中简单的线性卷积层被替换为了多层感知机(MLP多层全连接层和非线性函数的组合)
这样做优点:
- 提供了网络层间映射的一种新可能;
- 增加了网络卷积层的非线性能力。
全局平均池化:
去除了容易造成过拟合的全连接输出层,而是将其替换成输出通道数等于标签类别数的NiN块和全局平均池化层
这样做优点:
- 对数据在整个feature上作正则化,防止了过拟合
- 用再关注输入图像的尺寸,因为不管是怎样的输入都是一样的平均方法,传统的全连接层要根据尺寸来选择参数数目,不具有通用性
- 不再需要全连接层,减少了整个结构参数的数目(一般全连接层是整个结构中参数最多的层),过拟合的可能性降低
全局平均池化层:
对每一个feature上的所有点做平均,有n个feature就输出n个平均值作为最后的softmax的输入
实质:窗口形状等于输入空间维形状的平均池化层。
- NiN的这个设计的好处是可以显著减小模型参数尺寸,从而缓解过拟合。
- 但该设计有时会造成获得有效模型的训练时间的增加
代码实现
import torch
from torch import nn, optim
import torchvision
import sys
from time import time
import torch.nn.functional as F
定义模型
def nin_block(in_channels, out_channels, kernel_size, stride, padding):
blk = nn.Sequential(
#普通卷积层
nn.Conv2d(in_channels, out_channels, kernel_size, stride,padding),
nn.ReLU(),
#2个1x1卷积层,相当于2个全连接层,将特征先抽象出来
nn.Conv2d(out_channels, out_channels, kernel_size=1),
nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1),
nn.ReLU()
)
return blk
'''全局平均池化层可通过将池化窗口形状设置成输入的高和宽实现'''
class GlobalAvgPool2d(nn.Module):
def __init__(self):
super(GlobalAvgPool2d, self).__init__()
def forward(self, x):
return F.avg_pool2d(x, kernel_size=x.size()[2:])
# x.size()[2:]表示从第2维开始到最后一维的尺寸大小,返回输入张量的高度和宽度的尺寸信息
# F.avg_pool2d函数是PyTorch中的平均池化操作
'''对 x 的形状转换 '''
class FlattenLayer(nn.Module):
def __init__(self):
super(FlattenLayer, self).__init__()
def forward(self, x):
return x.view(x.shape[0], -1)
搭建NIN网络
class NinNet(nn.Module):
def __init__(self):
super(NinNet, self).__init__()
self.mlpconv = nn.Sequential(
nin_block(1, 96, kernel_size=11, stride=4, padding=0),
nn.MaxPool2d(kernel_size=3, stride=2),
nin_block(96, 256, kernel_size=5, stride=1, padding=2),
nn.MaxPool2d(kernel_size=3, stride=2),
nin_block(256, 384, kernel_size=3, stride=1, padding=1),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Dropout(0.5),
nin_block(384, 10, kernel_size=3, stride=1, padding=1), ## 标签类别数是10
GlobalAvgPool2d(),
FlattenLayer() ## 将四维的输出转成二维的输出,其形状为(批量大小,
)
def forward(self, img):
output = self.mlpconv(img)
return output
训练模型
读取数据集
'''读取数据集'''
def load_data_fashion_mnist(batch_size, resize=None):
if sys.platform.startswith('win'):
num_workers = 0 # 0表示不用额外的进程来加速读取数据
else:
num_workers = 4
'''定义数据预处理的转换函数列表'''
trans = []
if resize: #判断是否需要进行图像尺寸调整(resize)
trans.append(torchvision.transforms.Resize(size=resize))
#将torchvision.transforms.Resize转换函数添加到转换函数列表trans中,并指定目标尺寸为resize
trans.append(torchvision.transforms.ToTensor())
# 将torchvision.transforms.ToTensor转换函数添加到转换函数列表trans中。这个函数用于将图像数据转换为张量,并且按照通道顺序排列(RGB)
transform = torchvision.transforms.Compose(trans)
#通过torchvision.transforms.Compose函数将转换函数列表trans组合成一个转换操作
mnist_train = torchvision.datasets.FashionMNIST(root='data/FashionMNIST',
train=True,
download=True,
transform=transform)
mnist_test = torchvision.datasets.FashionMNIST(root='data/FashionMNIST',
train=False,
download=True,
transform=transform)
train_iter = torch.utils.data.DataLoader(mnist_train,
batch_size=batch_size,
shuffle=True,
num_workers=num_workers)
test_iter = torch.utils.data.DataLoader(mnist_train,
batch_size=batch_size,
shuffle=False,
num_workers=num_workers)
return train_iter, test_iter
设置网络模型
'''设置GPU运行'''
device = torch.device('cuda:1' if torch.cuda.is_available() else 'cpu')
'''选择网络'''
net = NinNet()
print(net)
定义训练函数
'''用于GPU的准确率评估函数'''
def evaluate_accuracy(data_iter, net, device=None):
if device is None and isinstance(net, torch.nn.Module):
device = list(net.parameters())[0].device #如果没指定device就使用net的device
acc_sum, n = 0.0, 0
for X, y in data_iter:
if isinstance(net, torch.nn.Module):
net.eval() # 评估模式, 这会关闭dropout
acc_sum += (net(X.to(device)).argmax(dim=1) == y.to(device)).float().sum().cpu().item()
net.train() # 改回训练模式
else:
if ('is_training' in net.__code__.co_varnames):
acc_sum += (net(X, is_training=False).argmax(dim=1) == y).float().sum().item()
else:
acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
n += y.shape[0]
return acc_sum / n
'''确保计算使用的数据和模型同在内存或显存上'''
def train_ch5(net, train_iter, test_iter, batch_size, optimizer , device, num_epochs):
# 将模型加载到指定运算器中
net = net.to(device)
print("training on ", device)
loss = torch.nn.CrossEntropyLoss()
for epoch in range(num_epochs):
# 在每一个迭代周期中,会使用训练集中所有样本一次(假设样本数能够被批量大小整除)
train_l_sum, train_acc_sum, n ,batch_count= 0.0, 0.0, 0,0
# 分别表示训练损失总和、训练准确度总和、样本数
start = time()
for X, y in train_iter:
X = X.to(device)
y = y.to(device)
'''前向传播'''
y_hat = net(X)
#由于变量 l 并不是一个标量,所以我们可以调用 .sum() 将其求和得到一个标量
'''计算损失'''
l = loss(y_hat, y).sum()
'''梯度清零'''
if optimizer is not None:
optimizer.zero_grad()
elif params is not None and params[0].grad is not None:
for param in params:
param.grad.data.zero_()
'''反向传播'''
l.backward() # 运行 l.backward() 得到该变量有关模型参数的梯度
if optimizer is None:
d2l.sgd(params, lr, batch_size)
else:
'''更新参数'''
optimizer.step()
'''计算精度'''
train_l_sum += l.cpu().item()
train_acc_sum += (y_hat.argmax(dim=1) == y).sum().cpu().item()
n += y.shape[0]
batch_count += 1
test_acc = evaluate_accuracy(test_iter, net)
print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f,\
time %.1f sec' %(epoch+1, train_l_sum/batch_count,
train_acc_sum/n, test_acc,
time()-start))
训练
'''训练模型'''
batch_size = 256
train_iter, test_iter = load_data_fashion_mnist(batch_size, resize=224)
lr, num_epochs = 0.002, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
train_ch5(net, train_iter, test_iter, batch_size, optimizer,device, num_epochs)