本期内容是针对神经网络层结构的一个补充
相关内容:
由浅入深,走进深度学习(补充篇:神经网络基础)-CSDN博客
在这里我也是对自己做一个学习记录,如果不符合大家的口味,大家划走就可以啦
可能没有什么文字或者原理上的讲解,基本上都是代码,但是我还是想说,如果基础不是很好,认认真真敲一遍,会有不一样的感受!!
目录
卷积层
假如就是要看一个3 * 3的局部信息的话 卷积核就是一个 3 * 3 的矩阵 不会因为输入变得特别大导致权重变得特别大
卷积层将输入和核矩阵进行交叉相关 加上偏移后得到输出
核矩阵核偏移是可学习的参数
核矩阵的大小是超参数
python
# 卷积层操作 自定义
# 互相关运算
import torch
from torch import nn
from d2l import torch as d2l
def corr2d(X, K): # X 为输入 K为核矩阵
# X 为输入 K为核矩阵
h, w = K.shape # 核矩阵的行数和列数
Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1)) # X.shape[0]为输入高
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
Y[i, j] = (X[i:i + h, j:j + w] * K).sum() # 图片的小方块区域与卷积核做点积
return Y
# 验证上述二维互相关运算的输出
X = torch.tensor([[0.0, 1.0, 2.0],
[3.0, 4.0, 5.0],
[6.0, 7.0, 8.0]])
K = torch.tensor([[0.0, 1.0],
[2.0, 3.0]])
corr2d(X, K)
# 实现二维卷积层
class Conv2D(nn.Module):
def __init__(self, kernel_size):
self.weight = nn.Parameter(torch.rand(kernel_size))
self.bias = nn.Parameter(torch.zeros(1))
def forward(self, x):
return corr2d(x, self.weight) + self.bias
# 卷积层的一个简单应用 检测图片中不同颜色的边缘
X = torch.ones((6, 8))
X[:, 2:6] = 0 # 把中间四列设置为0
print('X:', X) # 0 与 1 之间进行过渡 表示边缘
K = torch.tensor([[1.0, -1.0]]) # 如果左右原值相等 那么这两原值乘1和-1相加为0 则不是边缘
Y = corr2d(X, K)
print('Y:', Y)
print(corr2d(X.t(), K)) # X.t() 为X的转置 而K卷积核只能检测垂直边缘
# 学习由X生成Y的卷积核
conv2d = nn.Conv2d(1, 1, kernel_size = (1, 2), bias = False) # 单个矩阵 输入通道为1 黑白图片通道为1 彩色图片通道为3 这里输入通道为1 输出通道为1
X = X.reshape((1, 1, 6, 8)) # 通道维:通道数 RGB图3通道 灰度图1通道 批量维就是样本维 就是样本数
Y = Y.reshape((1, 1, 6, 7))
for i in range(10):
Y_hat = conv2d(X)
l = (Y_hat - Y) ** 2
conv2d.zero_grad()
l.sum().backward()
conv2d.weight.data[:] -= 3e-2 * conv2d.weight.grad # 3e-2是学习率
if(i + 1) % 2 == 0:
print(f'batch {i + 1}, loss {l.sum():.3f}')
# 所学的卷积核的权重张量
print(conv2d.weight.data.reshape((1,2)))
卷积里的 填充和步幅
奇数卷积核更容易做padding 我们假设卷积核大小为k * k
为了让卷积后的图像大小与原图一样大 根据公式可得到padding=(k-1)/2
这里的k只有在取奇数的时候 padding才能是整数 否则padding不好进行图片填充
k为偶数时 p为浮点数 所做的操作为一个为向上取整 填充 一个为向下取整 填充
填充和步幅是卷积层的超参数
填充在输入周围添加额外的行/列 来控制输出形状的减少量
步幅是每次滑动核窗口时的行/列的步长 可以成倍的减少输出形状
python
# 在所有侧边填充1个像素
import torch
from torch import nn
def comp_conv2d(conv2d, X): # conv2d 作为传参传进去 在内部使用
X = X.reshape((1, 1) + X.shape) # 在维度前面加入一个通道数和批量大小数
Y = conv2d(X) # 卷积处理是一个四维的矩阵
return Y.reshape(Y.shape[2:]) # 将前面两个维度拿掉
conv2d = nn.Conv2d(1, 1, kernel_size = 3, padding = 1) # padding=1 为左右都填充一行
X = torch.rand(size = (8, 8))
print(comp_conv2d(conv2d,X).shape)
conv2d = nn.Conv2d(1, 1, kernel_size = (5, 3), padding = (2, 1))
print(comp_conv2d(conv2d,X).shape)
# 将高度和宽度的步幅设置为2
conv2d = nn.Conv2d(1, 1, kernel_size = 3, padding = 2)
print(comp_conv2d(conv2d,X).shape)
# 一个稍微复杂的例子
conv2d = nn.Conv2d(1, 1, kernel_size = (3, 5), padding = (0, 1), stride = (3, 4))
print(comp_conv2d(conv2d,X).shape)
python
# 多个输入通道
# 核的通道数与输入的通道数一样
# 每个输出通道可以匹配图片里面特定的模式
# 把每个通道里面识别出来的模式组合起来 就得到组合模式识别
# 输出通道数是卷积层的超参数
# 每个输入通道有独立的二维卷积核 所有通道结果相加得到一个输出通道结果
# 每个输出通道有独立的三维卷积核
# 输入与输出 自定义
# 多输入通道互相关运算
import torch
from d2l import torch as d2l
from torch import nn
# 多通道输入运算
def corr2d_multi_in(X, K):
return sum(d2l.corr2d(x, k) for x, k in zip(X, K)) # X,K为3通道矩阵 for使得对最外面通道进行遍历
X = torch.tensor([[[0.0, 1.0, 2.0],
[3.0, 4.0, 5.0],
[6.0, 7.0, 8.0]],
[[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
[7.0, 8.0, 9.0]]])
K = torch.tensor([[[0.0, 1.0],
[2.0, 3.0]],
[[1.0, 2.0],
[3.0, 4.0]]])
print(corr2d_multi_in(X, K))
# 多输出通道运算
def corr2d_multi_in_out(X,K): # X为3通道矩阵 K为4通道矩阵 最外面维为输出通道
return torch.stack([corr2d_multi_in(X,k) for k in K],0) # 大k中每个小k是一个3D的Tensor 0表示stack堆叠函数里面在0这个维度堆叠
print(K.shape)
print((K + 1).shape)
print((K + 2).shape)
print(K)
print(K + 1)
K = torch.stack((K, K + 1, K + 2),0) # K与K+1之间的区别为K的每个元素加1
print(K.shape)
print(corr2d_multi_in_out(X, K))
python
# 1×1 卷积
# 1×1卷积的多输入 多输出通道运算
def corr2d_multi_in_out_1x1(X, K):
c_i, h, w = X.shape # 输入的通道数、宽、高
c_o = K.shape[0] # 输出的通道数
X = X.reshape((c_i, h * w)) # 拉平操作 每一行表示一个通道的特征
K = K.reshape((c_o, c_i))
Y = torch.matmul(K, X)
return Y.reshape((c_o, h, w))
X = torch.normal(0, 1, (3, 3, 3)) # norm函数生成0到1之间的(3,3,3)矩阵
K = torch.normal(0, 1, (2, 3, 1, 1)) # 输出通道是2 输入通道是3 核是1X1
Y1 = corr2d_multi_in_out_1x1(X, K)
Y2 = corr2d_multi_in_out(X, K)
assert float(torch.abs(Y1 - Y2).sum()) < 1e-6
print(float(torch.abs(Y1 - Y2).sum()))
# 1×1 卷积 使用框架
def comp_conv2d(conv2d, X): # conv2d 作为传参传进去,在内部使用
X = X.reshape((1, 1) + X.shape) # 在维度前面加入一个通道数和批量大小数
Y = conv2d(X) # 卷积处理是一个四维的矩阵
return Y.reshape(Y.shape[2:]) # 将前面两个维度拿掉
X = torch.rand(size = (8, 8))
conv2d = nn.Conv2d(1, 1, kernel_size = 3, padding = 1, stride = 2) # Pytorch里面卷积函数的第一个参数为输出通道 第二个参数为输入通道
print(comp_conv2d(conv2d, X).shape)
conv2d = nn.Conv2d(1,1,kernel_size = (3, 5), padding = (0, 1), stride = (3, 4)) # 一个稍微复杂的例子
print(comp_conv2d(conv2d, X).shape)
python
# VGG使用可重复使用的卷积块来构建深度卷积神经网络
# 不同的卷积块个数和超参数可以得到不同复杂度的变种
# VGG网络 自定义
import torch
from torch import nn
from d2l import torch as d2l
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, stride = 2))
return nn.Sequential(*layers)
conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))
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(0.5),
nn.Linear(4096, 4096),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(4096, 10))
net = vgg(conv_arch)
# 观察每个层输出的形状
X = torch.randn(size = (1, 1, 224, 224))
for blk in net:
X = blk(X)
print(blk.__class__.__name__,'output shape:\t', X.shape) # VGG使得高宽减半 通道数加倍
python
# 由于VGG-11比AlexNet计算量更大 因此构建了一个通道数较少的网络
ratio = 4
small_conv_arch = [(pair[0], pair[1] // ratio) for pair in conv_arch] # 所有输出通道除以4
net = vgg(small_conv_arch)
lr, num_epochs, batch_size = 0.05, 10, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize = 224)
d2l.train_ch6(net,train_iter,test_iter,num_epochs,lr,d2l.try_gpu())
NiN网络
一个卷积层后跟两个全连接层
步幅1 无填充 输出形状跟卷积层输出一样
起到全连接作用
NiN架构
无全连接层
交替使用NiN块和步幅为2的最大池化层
逐步减小高宽和增大通道数
最后使用全局平均池化层得到输出
其输入通道数是类别数
在全局平均池化层(GAP)被提出之前 常用的方式是将feature map直接拉平成一维向量
但是GAP不同 是将每个通道的二维图像做平均 最后也就是每个通道对应一个均值
假设卷积层的最后输出是h × w × d 的三维特征图 具体大小为6 × 6 × 3
经过GAP转换后 变成了大小为 1 × 1 × 3 的输出值 也就是每一层 h × w 会被平均化成一个值
NiN块使用卷积层加两个1×1卷积层 后者对每个像素增加了非线性性
NiN使用全局平均池化层来替代VGG和AlexNet中的全连接层 不容易过拟合 更少的参数个数
python
import torch
from torch import nn
from d2l import torch as d2l
def nin_block(in_channels, out_channels, kernel_size, strides, padding):
return nn.Sequential(nn.Conv2d(in_channels, out_channels, kernel_size, strides, padding),
nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size = 1),
nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size = 1),
nn.ReLU())
net = nn.Sequential(nin_block(1, 96, kernel_size = 11,strides = 4, padding = 0),
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())
# 查看每个块的输出形状
X = torch.rand(size = (1, 1, 224, 224))
for layer in net:
X = layer(X)
print(layer.__class__.__name__, 'output shape:\t', X.shape)
Inception块
Inception块使用了大量1X1卷积层 使得参数相对单3X3、5X5卷积层更少
跟单3×3或5×5卷积层比 Inception块更少的参数个数和计算复杂度
批量归一化
固定小批量中的均值和方差 然后学习出适合的偏移和缩放
可以加速收敛速度 但一般不改变模型精度
残差网络
残差块使得很深的网络更加容易训练 甚至可以训练一千层的网络
残差网络对随后的深层神经网络设计产生了深远影响 无论是卷积类网络还是全连接网络
python
# ReaNet网络 自定义
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
class Residual (nn.Module):
def __init__(self, input_channels, num_channels, use_1x1conv = False, strides = 1): # num_channels为输出channel数
super().__init__()
self.conv1 = nn.Conv2d(input_channels, num_channels, kernel_size = 3, padding = 1, stride = strides) # 可以使用传入进来的strides
self.conv2 = nn.Conv2d(num_channels, num_channels, kernel_size = 3, padding = 1) # 使用nn.Conv2d默认的strides=1
if use_1x1conv:
self.conv3 = nn.Conv2d(input_channels, num_channels, kernel_size = 1, stride = strides)
else:
self.conv3 = None
self.bn1 = nn.BatchNorm2d(num_channels)
self.bn2 = nn.BatchNorm2d(num_channels)
self.relu = nn.ReLU(inplace = True) # inplace原地操作 不创建新变量 对原变量操作 节约内存
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)
# 输入和输出形状一致
blk = Residual(3, 3) # 输入三通道 输出三通道
X = torch.rand(4, 3, 6, 6)
Y = blk(X) # stride用的默认的1 所以宽高没有变化 如果strides用2 则宽高减半
print(Y.shape)
# 增加输出通道数的同时 减半输出的高和宽
blk1 = Residual(3, 6, use_1x1conv = True, strides = 2) # 由3变为6 通道数加倍
print(blk1(X).shape)
# ResNet的第一个stage
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size = 7, stride = 2, padding = 3),
nn.BatchNorm2d(64),nn.ReLU(),
nn.MaxPool2d(kernel_size = 3, stride = 2, padding = 1))
# class Residual为小block resnet_block 为大block 为Resnet网络的一个stage
def resnet_block(input_channels, num_channels, num_residuals, first_block = False):
blk = []
for i in range(num_residuals):
if i == 0 and not first_block: # stage中不是第一个block则高宽减半
blk.append(Residual(input_channels, num_channels, use_1x1conv = True, strides = 2))
else:
blk.append(Residual(num_channels, num_channels))
return blk
b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block = True)) # 因为b1做了两次宽高减半 nn.Conv2d、nn.MaxPool2d 所以b2中的首次就不减半了
b3 = nn.Sequential(*resnet_block(64, 128, 2)) # b3、b4、b5的首次卷积层都减半
b4 = nn.Sequential(*resnet_block(128, 256, 2))
b5 = nn.Sequential(*resnet_block(256, 512, 2))
net = nn.Sequential(b1, b2, b3, b4, b5, nn.AdaptiveAvgPool2d((1,1)), nn.Flatten(), nn.Linear(512,10))
# 观察一下ReNet中不同模块的输入形状是如何变化的
X = torch.rand(size = (1, 1, 224, 224))
for layer in net:
X = layer(X)
print(layer.__class__.__name__,'output shape:\t',X.shape) # 通道数翻倍、模型减半
**注:**上述内容参考b站up主"我是土堆"的视频,参考吴恩达深度学习,机器学习内容,参考李沐动手学深度学习!!!