卷积神经网络
- 1. 背景
- 2. 架构
- 2.1. 卷积
- 2.1.1. 单卷积层(多Channel输出)
- 2.2.2. 多层卷积(卷积堆叠)
- 2.2.3. 卷积关键参数
- 2.2.4. 卷积常用计算公式
- 2.2. 池化层(pooling)
- 3. 经典网络结构
- 3.1. VGG-16
- 3.2. ResNet
- 4. 代码
- 4.1. 数据构建和载入
- 4.2. 网络结构定义
- 4.3. 模型训练和验证
- 4.4. 完整代码
1. 背景
在看大模型的一些内容中,为了更好的理解其实现,现对一些经典的网络模型进行学习。本文参考bilibili视频内容:https://www.bilibili.com/video/BV1zF411V7xu/
,感谢博主
本文学习的卷积神经网络CNN,在视觉领域非常知名,但也不只用于视觉领域。相比于普通NN,CNN引入了卷积层和池化层两个概念
- 卷积层:对图片的一块一块的小方格位置进行特征提取,通过不同权重的卷积核可以提出图形的各类特征
- 池化层:在提取完特征后,考虑到特征数目多,可以将特征矩阵小方格内最大的权重拿出来,通过这种方式进行降采样。
本文使用的pytorch进行的代码实践,在涉及到具体CNN代码训练时,本文作者对 train,optimiser,loss.backward等逻辑并没有很好的认知,代码内部有些过于黑盒了,学起来心里面太虚。 于是决定在余下的一些时间中,详细看下基本NN的代码训练逻辑,和NN的一些理论能够结合起来,让作者对代码能够认识的更清楚一些
2. 架构
- 包括输入层、卷积层、池化层、全连接层
如下图是一个7层卷积神经网络图
2.1. 卷积
- 下图对一个32323的图片,使用一个333的卷积核进行处理
- 卷积操作是将卷积核在图片中游走,在游走到某个位置时,执行的操作是对应位置的元素进行相乘再相加(下图右边以3*3的核进行的卷积,只表达了两维的概念)
- 注意:卷积核的第三个维度一定和输入特征的第三个维度相同
- 对于一个三维核,可将第三维展开,例如如下图一个图片的第三维代表RGB三种颜色值,使用一个333的卷积核进行卷积计算,可以展开成3个3*3的二维卷积
2.1.1. 单卷积层(多Channel输出)
- 对一个输入图片32323,每个卷积5*5的卷积核,会对应生成一个二维特征图,也叫 一个channel
- 可以使用多个卷积核,生成多个二维图,即如下图右边多个平面,平面的数目即是输出channel的数目
2.2.2. 多层卷积(卷积堆叠)
- 实际应用中,不会只有一层卷积层,而是多层卷积堆叠而成
- 如下图,第一层卷积使用 5 ∗ 5 ∗ 3 5*5*3 5∗5∗3,第二层卷积使用 5 ∗ 5 ∗ 6 5*5*6 5∗5∗6
- 第一层卷积的输出是
28
∗
28
∗
6
28*28*6
28∗28∗6,28和6是怎么来的?
- 28来源于 32 − 5 + 1 32 - 5 + 1 32−5+1,其中5是卷积核的大小,且采用的卷积核滑动窗口为1
- 6是特征图的个数,这表明使用6个553的卷积核
- 第二层卷积的输出是
24
∗
24
∗
10
24*24*10
24∗24∗10,24和10是怎么来的?
- 24等于 28 − 5 + 1 28 -5 +1 28−5+1
- 10是特征图的个数,这里使用的是 5 ∗ 5 ∗ 6 5*5*6 5∗5∗6的卷积核
2.2.3. 卷积关键参数
- 滑动窗口步长:最常用是1,或者是2
- 边缘填充:边缘填充的圈数,填充0(目的是为了让原有边缘更靠内,确保边缘计算公平性)
- 卷积核大小:最常用是33,或者55
- 卷积核个数:生成特征图的个数
2.2.4. 卷积常用计算公式
- 一层卷积输出特征是多少?
- 以其中一维的长度举例,它等于 输入长度 + 2 ∗ 边缘填充 − 核大小 滑动窗口步长 + 1 \frac{输入长度 + 2 * 边缘填充 - 核大小}{滑动窗口步长} + 1 滑动窗口步长输入长度+2∗边缘填充−核大小+1
- 以输入 32 ∗ 32 ∗ 3 32*32*3 32∗32∗3图像为例,用 10 10 10个 5 ∗ 5 ∗ 3 5*5*3 5∗5∗3的卷积核filter来指定步长为1,边界填充为2,则最终卷积输出特征为 32 ∗ 32 ∗ 10 32*32*10 32∗32∗10,其长度计算为 32 + 2 ∗ 2 − 5 1 + 1 = 32 \frac{32 + 2*2 - 5}{1}+1=32 132+2∗2−5+1=32,宽度计算和长度计算等同。
- 一层卷积的参数有多少个?
- 用 10 10 10个 5 ∗ 5 ∗ 3 5*5*3 5∗5∗3的卷积核,每个卷积核包括 W = 5 ∗ 5 ∗ 3 W = 5*5*3 W=5∗5∗3以及额外的 b b b,则总参数个数为: 10 ∗ 5 ∗ 5 ∗ 3 + 10 = 760 10 * 5 * 5 * 3 + 10 = 760 10∗5∗5∗3+10=760。
2.2. 池化层(pooling)
- 池化是用来降采样的(选取最有作用的特征)
- 一般是使用最大特征,即Max-Pooling
3. 经典网络结构
3.1. VGG-16
- 13个卷积层(2个 卷积(2)-池化 +3个 卷积(3)-池化)
- 最后3个全连接层:FC-4096,FC-4096,FC-1000
3.2. ResNet
- 网络更深,不见得更好
- 直通越过某些无用的层,可以更多层,至少不会比层数少的网络差
4. 代码
从视频中,一字一句抄了一份代码,目前IDE的补全能力很强,基本上写几个字母就可以猜到后续要用什么,因此代码实际抄起来还挺快。
这份代码包含三个部分
- 数据构建和载入
- 网络结构定义
- 网络训练和验证
4.1. 数据构建和载入
torchvision有datasets库可以自动从网上下载MNIST数据集,完全不需要任何数据准备的工作,傻瓜式写代码。数据集要和dataloader绑定在一起,具体dataloader的作用机制是如何的,可以参考大模型系列3-pytorch dataloader的原理。
4.2. 网络结构定义
利用前述知识,可以很容易构建一个三层CNN,它包括两个卷积层和一个全连接层,每个卷积后面接Relu和MaxPooling,经典网络有2卷1pooling,和三卷1pooling的。
- conv1:第1层卷积
- conv2:第2层卷积
- out:全连接层
特别要注意的是卷积的各个参数的设定
- in_channels:输入数据的第三维的大小
- out_channels:输出的特征图的个数
- kernel_size:卷积核第1和2维的大小
- stripe:卷积的滑动窗口步长
- padding:卷积边缘补充0的宽度,为2表明周围补充两圈
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv1 = nn.Sequential(
nn.Conv2d(in_channels=1,out_channels=16,kernel_size=5,stride=1,padding=2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2),
) #14*14*16
self.conv2 = nn.Sequential(
nn.Conv2d(in_channels=16,out_channels=32,kernel_size=5,stride=1,padding=2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2),
)#7*7*32
self.out = nn.Linear(32*7*7,10)
def forward(self,x):
x=self.conv1(x)
x=self.conv2(x)
x=x.view(x.size(0),-1)
output=self.out(x)
return output
4.3. 模型训练和验证
训练阶段的代码很简单,基本上都是分成多轮epoch,每轮分batch遍历所有的数据,在每个batch中执行如下工作:训练,损失计算,优化器,反向传播
net.train()
output = net(data)
loss = criterion(output, target)
optimizer.zero_grad()
loss.backward()
optimizer.step()
- 对这块代码做不到很强的认知,后续会从基本NN的逻辑出发,看看其实现的机制是什么,本文暂不展开,等分析完其逻辑之后,会将相关的链接贴在这里。
for epoch in range(num_epochs):
train_rights=[]
for batch_idx, (data, target) in enumerate(train_loader):
net.train()
output = net(data)
loss = criterion(output, target)
optimizer.zero_grad()
loss.backward()
optimizer.step()
rights= accuracy(output, target)
train_rights.append(rights)
if batch_idx % 100 == 0:
net.eval()
val_rights=[]
for (data,target) in test_loader:
output = net(data)
right = accuracy(output, target)
val_rights.append(right)
train_r = (sum([top[0] for top in train_rights]), sum([top[1] for top in train_rights]))
val_r = (sum([top[0] for top in val_rights]), sum([top[1] for top in val_rights]))
print('当前epoch: {}[{}/{}({:.0f}%)]\t损失:{:.6f}\t训练集准确率:{:.2f}%\t测试集正确率:{:.2f}%'.format(
epoch,
batch_idx * batch_size,
len(train_loader.dataset),
100.*batch_idx/len(train_loader),
loss.data,
100.*train_r[0]/train_r[1],
100.*val_r[0]/val_r[1]))
4.4. 完整代码
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torchvision import datasets,transforms
import matplotlib.pyplot as plt
import numpy as np
#定义超参数
input_size=28 #图像的总尺寸28*28
num_classes=10 #标签的种类数
num_epochs=3 #训练的总循环周期
batch_size=64 #一个(批次)的大小,64张图片
#训练集
train_dataset = datasets.MNIST(root="./data", train=True, transform=transforms.ToTensor(), download=True)
# 测试集
test_dataset = datasets.MNIST(root="./data", train=False, transform=transforms.ToTensor(), download=True)
#构建batch数据
train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=True)
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv1 = nn.Sequential(
nn.Conv2d(in_channels=1,out_channels=16,kernel_size=5,stride=1,padding=2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2),
) #14*14*16
self.conv2 = nn.Sequential(
nn.Conv2d(in_channels=16,out_channels=32,kernel_size=5,stride=1,padding=2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2),
)#7*7*32
self.out = nn.Linear(32*7*7,10)
def forward(self,x):
x=self.conv1(x)
x=self.conv2(x)
x=x.view(x.size(0),-1)
output=self.out(x)
return output
def accuracy(predictions, labels):
pred = torch.max(predictions.data,1)[1]
rights = pred.eq(labels.data).cpu().sum()
return rights,len(labels)
net = CNN()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.001)
for epoch in range(num_epochs):
train_rights=[]
for batch_idx, (data, target) in enumerate(train_loader):
net.train()
output = net(data)
loss = criterion(output, target)
optimizer.zero_grad()
loss.backward()
optimizer.step()
rights= accuracy(output, target)
train_rights.append(rights)
if batch_idx % 100 == 0:
net.eval()
val_rights=[]
for (data,target) in test_loader:
output = net(data)
right = accuracy(output, target)
val_rights.append(right)
train_r = (sum([top[0] for top in train_rights]), sum([top[1] for top in train_rights]))
val_r = (sum([top[0] for top in val_rights]), sum([top[1] for top in val_rights]))
print('当前epoch: {}[{}/{}({:.0f}%)]\t损失:{:.6f}\t训练集准确率:{:.2f}%\t测试集正确率:{:.2f}%'.format(
epoch,
batch_idx * batch_size,
len(train_loader.dataset),
100.*batch_idx/len(train_loader),
loss.data,
100.*train_r[0]/train_r[1],
100.*val_r[0]/val_r[1]))