"卷积神经网络" 资料整理归纳
卷积神经网络(Convolutional Neural Networks, CNN)是一种包含 卷积 计算且具有深度结构的 前馈神经网络(Feedforward Neural Networks)
基础理解:
- 卷积:滤波,特征提取
- 池化:即采样,对数据进行降维压缩,常见的有最大池化(每组数中用最大值作为代表)、平均池化(每组数中用平均值作为代表)
- 全连接层:将数据展平为一维向量,再进行全连接运算(即加权求和),然后映射到具体的类别或输出。
(全连接层可以多层叠加,通常最后一个全连接层会接一个 softmax 函数,输出每个类别的概率。) - 激活函数:防止梯度消失或爆炸,增强神经网络表达能力。常见的有Sigmoid函数、ReLU函数等。
- 神经网络:万能函数拟合器,也可以理解为一个输入x到输出y的映射函数,即f(x)=y。
(简单理解,y=wx+b) - 前向传播:根据初始w和b(一般为随机数),从输入层x开始,逐层计算每个神经元的输出,直到得到最终的预测结果f(x)
- 损失函数:度量模型的预测值f(x)与真实值Y的差异程度
- 反向传播:从最后一层逐步调整权重参数w
- 梯度下降:一种优化算法,可以用在神经网络的训练中,以更新网络的权重参数,使得损失函数的值最小化。梯度下降法需要计算损失函数对每个参数的梯度,即偏导数,表示参数变化时损失函数的变化率。
在机器学习中,我们知道输入的feature(或称为x)需要通过模型(model)预测出y,此过程称为前向传播(forward pass),而要将预测与真实值的差值减小需要更新模型中的参数,这个过程称为反向传播(backward pass)。其中损失函数(lossfunction)就基于这两种传播之间,在机器学习中,想让预测值无限接近于真实值,所以需要将差值降到最低(在这个过程中就需要引入损失函数)。而在此过程中损失函数的选择是十分关键的,在具体的项目中,有些损失函数计算的差值梯度下降的快,而有些下降的慢。
反向传播是用来快速求解目标函数关于各个参数的梯度;而梯度下降则是根据计算得到的梯度来更新各个权重参数。
一个非常不错的过程动画:
模型调参过程(b站视频)
AlexNet
一种经典的卷积神经网络模型,在ILSVRC比赛中,AlexNet所用的数据集是ImageNet,总共识别1000个类别。
有一点需要解释一下,鉴于当时的硬件资源限制,由于AlexNet结构复杂、参数很庞大,难以在单个GPU上进行训练。因此AlexNet采用两路GTX 580 3GB GPU并行训练。也就是说把原先的卷积层平分成两部分FeatureMap分别在两块GPU上进行训练(例如卷积层55x55x96分成两个FeatureMap:55x55x48)。
AlexNet的网络结构如下:
-
输入层:接收一个RGB三通道的224×224×3大小的图像(原论文中,AlexNet的输入图像尺寸是224x224x3。但是实际图像尺寸为227x227x3);
-
卷积层C1:【卷积 --> ReLU --> 局部响应归一化(LRN) --> 最大池化】
使用96个11×11×3的卷积核,步长为4,不扩充边缘,输出(55x55x48)x2的特征图,然后使用ReLU激活函数和局部响应归一化(LRN),最后进行3×3的最大池化,步长为2,输出(27×27×48)x2的特征图;注:这里(55x55x48)x2的特征图是指55x55x96特征图被分成两部分进行运算
-
卷积层C2:【卷积 --> ReLU --> 局部响应归一化(LRN) --> 最大池化】
使用256个5×5×48的卷积核,步长为1,扩充边缘为2,输出(27×27×128)×2的特征图,然后使用ReLU激活函数和LRN,最后进行3×3的最大池化,步长为2,输出(13×13×128)×2的特征图; -
卷积层C3:【卷积 --> ReLU】
使用384个3×3×256的卷积核,步长为1,扩充边缘为1,输出(13×13×192)x2的特征图,然后使用ReLU激活函数; -
卷积层C4:【卷积 --> ReLU】
使用384个3×3×192的卷积核,步长为1,扩充边缘为1,输出(13×13×192)x2的特征图,然后使用ReLU激活函数; -
卷积层C5:【卷积 --> ReLU --> 最大池化】
使用256个3×3×192的卷积核,步长为1,扩充边缘为1,输出(13×13×128)x2的特征图,然后使用ReLU激活函数,最后进行3×3的最大池化,步长为2,输出6×6×128x2的特征图; -
全连接层F6: 【全连接 --> ReLU --> Dropout】
将6×6×256的特征图展平为一维向量,然后使用4096个神经元进行全连接,输出4096维的向量,然后使用ReLU激活函数和Dropout;注:“将6×6×256的特征图展平为一维向量输入为6×6×256”是指:使用4096个6×6×256的卷积核进行卷积,由于卷积核尺寸与输入的尺寸完全相同,即卷积核中的每个系数只与输入尺寸的一个像素值相乘一一对应,根据公式:(input_size + 2 * padding - kernel_size) / stride + 1=(6+2*0-6)/1+1=1,得到输出是1x1x4096。
-
全连接层F7:【全连接 --> ReLU --> Dropout】
使用4096个神经元进行全连接,输出4096维的向量,然后使用ReLU激活函数和Dropout; -
全连接层F8:【全连接 --> Softmax】
使用1000个神经元进行全连接,输出1000维的向量,然后使用Softmax函数,得到每个类别的概率值。
代码实现
这里的代码是针对CIFAR-10数据集进行训练,故而部分参数有一些修改。
其次,由于局部响应归一化(LRN)饱受争议,通常认为其对模型泛化能力提升很小,故而此处没有使用。
-
定义网络模型结构
import torch
import torch.nn as nn
class AlexNet(nn.Module):
def init(self, config):
super(AlexNet, self).init()
self._config = config
# 定义卷积层和池化层
self.features = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(64, 192, kernel_size=5, stride=1, padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(192, 384, kernel_size=3, stride=1, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(384, 256, kernel_size=3, stride=1, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
)
# 自适应层,将上一层的数据转换成6x6大小
self.avgpool = nn.AdaptiveAvgPool2d((6, 6))
# 全连接层
self.classifier = nn.Sequential(
nn.Dropout(),
nn.Linear(256 * 6 * 6, 4096),
nn.ReLU(inplace=True),
nn.Dropout(),
nn.Linear(4096, 1024),
nn.ReLU(inplace=True),
nn.Linear(1024, self._config['num_classes']),
)def forward(self, x):
x = self.features(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.classifier(x)
return x -
模型保存函数(保存训练好的模型)
def saveModel(self):
torch.save(self.state_dict(), self._config['model_name']) -
模型加载函数(加载保存后的模型)
def loadModel(self, map_location):
state_dict = torch.load(self._config['model_name'], map_location=map_location)
self.load_state_dict(state_dict, strict=False) -
数据预处理
import torchvision
from torch.utils.data import DataLoader
import torchvision.transforms as transforms
# 定义构造数据加载器的函数
def Construct_DataLoader(dataset, batchsize):
return DataLoader(dataset=dataset, batch_size=batchsize, shuffle=True)
# 图像预处理
transform = transforms.Compose([
transforms.Resize(96),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
# 加载CIFAR-10数据集函数
def LoadCIFAR10(download=False):
# Load CIFAR-10 dataset
train_dataset = torchvision.datasets.CIFAR10(root='../CIFAR10', train=True, transform=transform, download=download)
test_dataset = torchvision.datasets.CIFAR10(root='../CIFAR10', train=False, transform=transform)
return train_dataset, test_datase -
模型训练函数(这里将训练函数进行了封装)
from torch.autograd import Variable
from torch.utils.data import DataLoaderimport torch.nn as nn
class Trainer(object):
# 初始化模型、配置参数、优化器和损失函数
def init(self, model, config):
self._model = model
self._config = config
self._optimizer = torch.optim.Adam(self._model.parameters(),
lr=config['lr'], weight_decay=config['l2_regularization'])
self.loss_func = nn.CrossEntropyLoss()
# 对单个小批量数据进行训练,包括前向传播、计算损失、反向传播和更新模型参数
def _train_single_batch(self, images, labels):
y_predict = self._model(images)loss = self.loss_func(y_predict, labels)
# 先将梯度清零,如果不清零,那么这个梯度就和上一个mini-batch有关
self._optimizer.zero_grad()
# 反向传播计算梯度
loss.backward()
# 梯度下降等优化器 更新参数
self._optimizer.step()
# 将loss的值提取成python的float类型
loss = loss.item()计算训练精确度
这里的y_predict是一个多个分类输出,将dim指定为1,即返回每一个分类输出最大的值以及下标
_, predicted = torch.max(y_predict.data, dim=1)
return loss, predicteddef _train_an_epoch(self, train_loader, epoch_id):
"""
训练一个Epoch,即将训练集中的所有样本全部都过一遍
"""
# 设置模型为训练模式,启用dropout以及batch normalization
self._model.train()
total = 0
correct = 0从DataLoader中获取小批量的id以及数据
for batch_id, (images, labels) in enumerate(train_loader):
images = Variable(images)
labels = Variable(labels)
if self._config['use_cuda'] is True:
images, labels = images.cuda(), labels.cuda()loss, predicted = self._train_single_batch(images, labels)
计算训练精确度
total += labels.size(0)
correct += (predicted == labels.data).sum()print('[Training Epoch: {}] Batch: {}, Loss: {}'.format(epoch_id, batch_id, loss))
print('Training Epoch: {}, accuracy rate: {}%%'.format(epoch_id, correct / total * 100.0))
def train(self, train_dataset):
# 是否使用GPU加速
self.use_cuda()
for epoch in range(self._config['num_epoch']):
print('-' * 20 + ' Epoch {} starts '.format(epoch) + '-' * 20)
# 构造DataLoader
data_loader = DataLoader(dataset=train_dataset, batch_size=self._config['batch_size'], shuffle=True)
# 训练一个轮次
self._train_an_epoch(data_loader, epoch_id=epoch)用于将模型和数据迁移到GPU上进行计算,如果CUDA不可用则会抛出异常
def use_cuda(self):
if self._config['use_cuda'] is True:
assert torch.cuda.is_available(), 'CUDA is not available'
torch.cuda.set_device(self._config['device_id'])
self._model.cuda()保存训练好的模型
def save(self):
self._model.saveModel() -
加载之前定义的函数,实现模型的训练与测试
from torch.autograd import Variable
# 定义参数配置信息
alexnet_config =
{
'num_epoch': 20, # 训练轮次数
'batch_size': 500, # 每个小批量训练的样本数量
'lr': 1e-3, # 学习率
'l2_regularization':1e-4, # L2正则化系数
'num_classes': 10, # 分类的类别数目
'device_id': 0, # 使用的GPU设备的ID号
'use_cuda': True, # 是否使用CUDA加速
'model_name': '../AlexNet.model' # 保存模型的文件名
}if name == "main":
# AlexNet 模型
train_dataset, test_dataset = LoadCIFAR10(True)
# 定义 AlexNet model
alexNet = AlexNet(alexnet_config)模型训练阶段
# 实例化模型训练器
trainer = Trainer(model=alexNet, config=alexnet_config)
# # 训练
trainer.train(train_dataset)
# # 保存模型
trainer.save()模型测试阶段
alexNet.eval()
alexNet.loadModel(map_location=torch.device('cpu'))
if alexnet_config['use_cuda']:
alexNet = alexNet.cuda()correct = 0
total = 0
# 对测试集中的每个样本进行预测,并计算出预测的精度
for images, labels in Construct_DataLoader(test_dataset, alexnet_config['batch_size']):
images = Variable(images)
labels = Variable(labels)
if alexnet_config['use_cuda']:
images = images.cuda()
labels = labels.cuda()y_pred = alexNet(images)
_, predicted = torch.max(y_pred.data, 1)
total += labels.size(0)
temp = (predicted == labels.data).sum()
correct += temp
print('Accuracy of the model on the test images: %.2f%%' % (100.0 * correct / total))
参考:
- CSDN博主「小林学编程」文章:损失函数(lossfunction)的全面介绍
- Github 源码:ClassicNetworks
- 知乎用户「红色石头」文章:手撕 CNN 经典网络之 AlexNet(理论篇)
浙公网安备 33010602011771号