从全连接到卷积
在图片中找Pattern的两个原则(Inductive bias):
- 平移不变性
- 局部性
而卷积层就是通过感受野(Receptive Field) + 权重共享(Parameter Sharing) 实现
网络越深,就可以侦测到越大的Pattern,因为同样的Pattern可以出现在不同的地方,所以神经元之间可以共享参数
不同的角度来看卷积层:
卷积:二维交叉相关(Cov2d)
对全连接层使用平移不变性和局部性得到卷积层
一维交叉相关(Cov1d):文本、语言、时间序列
三维交叉相关(Cov3d):视频、医学图像、气象地图
卷积层
kernel
将输入和kernel矩阵进行交叉相关运算,然后加上bias得到输出(输出的就是feature map)
kernel和bias是可学习的参数
kernel的size时超参数(控制局部性,保证权重不会因为输入的变大而变得特别大)
【根据李宏毅老师说的,CNN的capacity是小于DNN的】
不同的卷积核带来不同的效果
而神经网络就是学习这个核来得到想要的输出
Padding
由于卷积核会导致输出变小,想要做深的网络就要填充(在周围加入额外的raw/col)
通常填充的行高(ph)和列高(pw)取:ph=kh-1;pw=kw-1
Stride
由于通常kernel比较小3.3 or 5.5,需要大量计算才能得到较小输出,于是有了步幅(移动窗口的时候移动的格数:如高度3,宽度2)
- kernel_size, padding, stride是卷积层的超参数
- 填充在输入周围添加额外的行列控制输出形状的减少量
- 步幅是每次滑动核窗口的行列步长,可成倍减少输出形状
多输入/多输出通道
这俩是一般卷积层中要详细设计的超参数
多输入通道
- 每个通道都有一个卷积核,结果是所有通道卷积结果的和
输入和核都是多通道,输出是单通道
多输出通道
卷积核多了一个输出通道数的维度
多输入和多输出通道到底干啥
- 每个输出通道可以识别特定模式
- 输入通道核识别并组合输入中的模式
(此外,1 * 1 卷积层 [kernel的h和w都是1] 它不识别空间模式,只做通道融合)
一个完整的二维卷积层的样子:
池化层
卷积对位置敏感,并且需要一定程度的平移不变性。故通过pooling缓解卷积的位置敏感性
二维最大池化
返回滑动窗口中的最大值(允许图像存在一定程度偏移,类似模糊化的效果)
超参数和卷积层类似,都有窗口大小,填充和步幅,没有可学习的参数;输入通道数=输出通道数
平均池化层
将最大池化层中“max”操作替换为平均(有点柔和化的效果)
一些经典的CNN架构
LeNet
LeNet是早期成功的神经网路,先用卷积学习图片空间信息,然后用全连接层转换到类别空间
class Reshape(nn.Module):
def forward(self,x):
return x.view(-1,1,28,28) #批量数不变,通道数变1,高宽变28
net = nn.Sequential(
Reshape(),nn.Conv2d(1,6,kernel_size=5,padding=2),nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2,stride=2),
nn.Conv2d(6,16,kernel_size=5), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2,stride=2),
nn.Flatten(),
nn.Linear(16*5*5,120),
nn.Linear(120,84), nn.Sigmoid(),
nn.Linear(84,10)
)
# 迭代看每一层的情况
X = torch.rand(size=(1,1,28,28),dtype=torch.float32)
for layer in net:
X = layer(X)
print(layer.__class__.__name__, 'out shape: \t',X.shape)
# 使用GPU训练
def train(net,train_iter,test_iter,epochs,lr,device):
def init_weights(m):
if type(m) == nn.Linear or type(m) == nn.Conv2d:
nn.init.xavier_uniforn_(m.weights)
net.apply(init_weights) #参数初始化
net.to(device) #使用GPU
optimizer = torch.optim.SGD(net.parameters(),lr=lr) #定义优化器
loss = nn.CrossEntrophyLoss() #定义loss func
# 迭代训练
for epoch in range(epochs):
net.train(),
for i, (X,y) in enumerate(train_iter):
optimizer.zero_grad() #优化器梯度清零
X,y = X.to(device),y.to(device) #数据移至GPU
y_hat = net(X) #预测值
l = loss(y_hat,y) #计算损失
l.backward() #对损失进行bp
optimizer.step() #优化一步
AlexNet
模型设计
激活函数
使用ReLU()
而不是Sigmoid()
,原因在模型选择那里讲了,运算容易而且梯度好
容量控制和预处理
通过Dropout控制全连接层的模型复杂度,而LeNet只使用Weight Decay,并且AlexNet使用了Data Augmentation
实现
net = nn.Sequential(
nn.Conv2d(1,96,kernel_size=11,stride=4,padding=1),nn.ReLU(),
nn.MaxPool2d(kernel_size=3,stride=2),
nn.Conv2d(96,256,kernel_size=5,padding=2),nn.ReLU(),
nn.MaxPool2d(kernel_size=3,stride=2),
nn.Conv2d(256,384,kernel_size=3,padding=1),nn.ReLU(),
nn.Conv2d(384,384,kernel_size=3,padding=1),nn.ReLU(),
nn.Conv2d(384,256,kernel_size=3,padding=1),nn.ReLU(),
nn.MaxPool2d(kernel_size=3,stride=2), nn.Flatten(),
nn.Linear(6400,4096),nn.ReLU(),nn.Dropout(p=0.5),
nn.Linear(4096,4096),nn.ReLU(),nn.Dropout(p=0.5),
nn.Linear(4096,10)
)
VGG
VGG块
用的3 * 3的卷积,深但窄的效果更好(同样的计算开销下)
def vgg_block(num_convs,in_channels,out_channels):
layers = []
for _ in range(num_convs):
layers.append(nn.Conv2d(in_channels,out_channels,kernel_size=3,padding=1))
layers.append(nn.ReLU())
in_channels = out_channels
layers.append(nn.MaxPool2d(kernel_size=2,tride=2))
return nn.Sequential(*layers)
超参数:层数和通道数
VGG架构
多个VGG块后接全连接层,不同次数的重复块得到不同的架构,如VGG-16,VGG-19
def vgg(conv_arch):
conv_blks = []
in_channels = 1
# 卷积层部分
for (num_convs,out_channels) in conv_arch: conv_blks.append(vgg_block(num_convs,in_channels,out_channels))
in_channels = out_channels
return nn.Sequential(
*conv_blks, nn.Flatten(),
# 全连接层部分
nn.Linear(out_channels*7*7,4096), nn.ReLU(),nn.Dropout(p=0.5),
nn.Linear(4096,4096), nn.ReLU(), nn.Dropout(p=0.5),
nn.Linear(4096,10)
)
conv_arch = ((1,64),(1,128),(2,256),(2,512),(2,512)) #VGG架构,每一块里多少层卷积核多少输出通道; [五大块,为什么5?因为224除以5次2后是7]
net = vgg(conv_arch)
NiN(网络中的网络)
AlexNet和VGG的改进主要在于如何扩大和加深卷积层+Pooling层,而如果使用了全连接层,可能会完全放弃表征的空间结构, 并且第一个全连接层的参数特别大,且容易过拟合,NiN提出了一个解决方案:在每个pixel的通道上分别使用MLP(对每个像素增加了非线性性)
NiN块
def nin_block():
return nn.Sequential(
nn.Conv2d(in_channels,out_channels,kernel_size,strides,padding), nn.ReLU(),
# 然后两个1 * 1卷积层
nn.Conv2d(in_channels,out_channels,kernel_size=1), nn.ReLU(),
nn.Conv2d(in_channels,out_channels,kernel_size=1), nn.ReLU()
)
NiN 架构
无全连接层,交替使用 NiN块 和 stride=2 的MaxPooling,逐步减小高宽增大通道数,最后使用全局平均池化层(池化层的高宽等于输入的高宽)得到输出,其通道数设置成了类别数(用这个替代全连接不同意过拟合,以及更少的参数个数)
net = nn.Sequential(
nin_block(1,96,kernel_size=11,strides=4,padding=0), #灰度图所以in_channel设成了1
nn.MaxPool2d(3,stride=2),
nin_block(96,256,kernel_size=5,strides=1,padding=2),
nn.MaxPool2d(3,stride=2),
nin_block(256,384,kernel_size=3,strides=1,padding=1),
nn.MaxPool2d(3,stride=2),
nn.Dropout(0.5),
nin_block(384,10,kernel_size=3,strides=1,padding=1),
nn.AdaptiveAvgPool2d((1,1)),
nn.Flatten()
)
GoogLeNet
并行连接
Inception块
4个路径从不同层面抽取信息,然后在输出通道合并
主要优点:模型参数小,计算复杂度低
1 * 1 卷积层的作用:https://blog.csdn.net/ding_programmer/article/details/104109895
变种
- Inception-BN(v2)
- Inception-V3:(修改了Inception块)替换5 * 5为多个3 * 3卷积层;替换5 * 5为1 * 7和7 * 1的卷积层;替换3 * 3 为1 * 3和3 * 1的卷积层;更深
- Inception-V4:使用ResBlock
class Inception(nn.Module):
def __init__(self,in_channels,c1,c2,c3,c4,**kwargs): #c1,c2,c3,c4是每一条path里的channel数
super(Inception,self).__init__(**kwargs)
# 线路1,单1x1卷积层
self.p1_1 = nn.Conv2d(in_channels,c1,kernel_size=1)
self.p2_1 = nn.Conv2d(in_channels,c2[0],kernel_size=1)
self.p2_2 = nn.Conv2d(c2[0],c2[1],kernel_size=3,padding=1)
# 线路3,1x1卷积层后接5x5卷积层
self.p3_1 = nn.Conv2d(in_channels, c3[0], kernel_size=1)
self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
# 线路4,3x3最⼤汇聚层后接1x1卷积层
self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
self.p4_2 = nn.Conv2d(in_channels, c4, kernel_size=1)
def forward(self, x):
p1 = F.relu(self.p1_1(x))
p2 = F.relu(self.p2_2(F.relu(self.p2_1(x))))
p3 = F.relu(self.p3_2(F.relu(self.p3_1(x))))
p4 = F.relu(self.p4_2(self.p4_1(x)))
# 在通道维度上拼接输出
return torch.cat((p1, p2, p3, p4), dim=1)
GoogLeNet Model
stage1 = nn.Sequential(
nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)
stage2 = nn.Sequential(
nn.Conv2d(64, 64, kernel_size=1),
nn.ReLU(),
nn.Conv2d(64, 192, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)
stage3 = nn.Sequential(
Inception(192, 64, (96, 128), (16, 32), 32),
Inception(256, 128, (128, 192), (32, 96), 64),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)
stage4 = nn.Sequential(
Inception(480, 192, (96, 208), (16, 48), 64),
Inception(512, 160, (112, 224), (24, 64), 64),
Inception(512, 128, (128, 256), (24, 64), 64),
Inception(512, 112, (144, 288), (32, 64), 64),
Inception(528, 256, (160, 320), (32, 128), 128),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)
stage5 = nn.Sequential(
Inception(832, 256, (160, 320), (32, 128), 128),
Inception(832, 384, (192, 384), (48, 128), 128),
nn.AdaptiveAvgPool2d((1,1)),
nn.Flatten()
)
net = nn.Sequential(stage1, stage2, stage3, stage4, stage5, nn.Linear(1024, 10))
Batch Normalization
提出背景
一个深层的神经网络中,常常后面的层训练的比较快,底部的层训练的比较慢(数据在最底部,损失在最后面),因为梯度从顶传到底一直乘会变得很小。
而每一次底部的权重一变,上面的又得重新训练(白学了),那么就存在一个问题:训练在持续进行的时候,能否在改变底部的时候,避免变化顶部层?
BN的思路
固定小批量里面的均值和方差
然后再做额外的调整(可学习的参数γ、β)
常常作用在全连接层和卷积层之后,激活函数前;也可以作用在全连接和卷积的输入上,使得输入的数据方差和均值比较好【BN是一个线性变换】
对全连接层:作用在特征维上
对卷积层:作用在通道维上
一般来说,BN层可以加速收敛速度,但不改变模型精度(学习率可以稍微比较大)
BN在做什么?
最初论文试想用它来减少内部协变量转移;
后续有论文指出他可能就是通过在每个小批量里加入噪音来控制模型复杂度,因此没必要和Dropout混合使用
ResNet
对于非嵌套类函数,较复杂的函数类不能保证更接近“真”函数(model bias),这种在嵌套函数类中不会发生
核心思想:每个附加层都应该更容易地包含原始函数作为其元素之一
残差块
class Residual(nn.Module):
def __init__(self,in_channels,num_channels,use_1x1conv=False,strides=1):
super().__init__()
self.conv1 = nn.Conv2d(in_channels,num_channels,kernel_size=3,padding=1,stride=strides)
self.conv2 = nn.Conv2d(num_channels,num_channels,kernel_size=3,padding=1)
if use_1x1conv:
self.conv3 = nn.Conv2d(in_channels,num_channels,kernel_size=1,stride=strides)
else:
self.conv3 = None
self.bn1 = nn.BatchNorm2d(num_channels)
self.bn2 = nn.BatchNorm2d(num__channels)
def forward(self,X):
Y = F.relu(self.bn1(self.conv1(x)))
Y = self.bn2(self.conv2(Y))
if self.conv3:
X = self.conv3(X)
Y += X #经典短路操作
return F.relu(Y)
ResNet 架构
就是ResBlock堆叠,和VGG差不多
DenseNet
从ResNet到DenseNet
ResNet用泰勒展开就是
f
(
x
)
=
x
+
g
(
x
)
f(x)=x+g(x)
f(x)=x+g(x),一个线性项+一个非线性项,如果再将f拓展成超过两部分的信息就是DenseNet
稠密块
def conv_block(in_channels,num_channels):
return nn.Sequential(
nn.BatchNorm2d(in_channels),nn.ReLU(),
nn.Conv2d(in_channels,num_channels,kernel_size=3,padding=1)
)
class DenseBlock(nn.Module):
def __init__(self,num_convs,in_channels,num_channels):
super(DenseBlock,self).__init__()
layer=[]
for i in range(num_convs):
layer.append(conv_block())
self.net = nn.Sequential(*layer)
def forward(self,X):
for blk in self.net:
Y = blk(X)
X = torch.cat((X,Y),dim=1)
return X
CNN的一些问题
CNN并不能处理图像放大缩小或旋转的问题(所以在图像识别的时候往往要做Data Augmentation)【Spatial Transformer Layer可以处理】