一、PyTorch神经网络基础
1. 模型构造
本应在jupytor里实现,现在还在跑模型,因此先在此记录一下
1.1 层和块
# 多层感知机
import torch
from torch import nn
from torch.nn import functional as F
net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10)) # Sequential定义了一种特殊的`Module`
X = torch.rand(2, 20) # 2是批量大小,20是输入维度
net(X)
1.2 自定义块
# 任何一个层或者说神经网络都应该是module的一个子类
class MLP(nn.Module): # 自定义一个MLP实现多层感知机,是nn.Module的一个子类,有很多好用的函数
# 每个module有两个最重要的函数,init和forward
def __init__(self): # 定义了需要的类和参数,定义了网络所可能需要的全部的层
super().__init__() # 先调用父类的init函数,设置好内部需要的参数
# 下面定义两个全连接层
self.hidden = nn.Linear(20, 256)
self.out = nn.Linear(256, 10)
def forward(self, X): # 给定输入进行输出
return self.out(F.relu(self.hidden(X)))
# 使用方法:
net = MLP()
net(X)
1.3 顺序块
当Sequential不能满足需求时,可以在init自定义大量计算
# 实现上面的Sequential的功能
class MySequential(nn.Module):
def __init__(self, *args): # *args:收集参数,相当于把若干个参数打包成一个来传入
super().__init__()
for block in args:
self._modules[block] = block # 相当于顺序表(排好序的),存放需要的层
def forward(self, X):
for block in self._modules.values(): # 按插入顺序,输入输入数据
X = block(X)
return X
# 使用
net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
net(X)
1.4 在正向传播函数中执行代码
class FixedHiddenMLP(nn.Module):
def __init__(self):
super().__init__()
self.rand_weight = torch.rand((20, 20), requires_grad=False)
self.linear = nn.Linear(20, 20) # 可以直接生成参数不参与梯度计算
def forward(self, X): # forward中定义很多自己的东西
X = self.linear(X)
X = F.relu(torch.mm(X, self.rand_weight)+1)
X = self.linear(X)
while X.abs().sum() > 1:
X /= 2
return X.sum() # 返回标量
# 反向计算就是对forward的自动求导
net = FixedHiddenMLP()
net(X)
1.5 混合搭配各种组合块的方法
torch中的已有的东西、我们自己定义的东西,都是Module的子类可以嵌套使用:
# 嵌套使用例子:
class NestMLP(nn.Module):
def __init__(self): # 定义了需要的类和参数,定义了网络所可能需要的全部的层
super().__init__()
self.net = nn.Sequential(nn.Linear(20, 64), nn.ReLU(),
nn.Linear(64, 32), nn.ReLU())
self.linear = nn.Linear(32, 16)
def forward(self, X):
return self.linear(self.net(X))
# 对于Sequential来说,输入可以是任何module的子类,也可以是我们自己定义的子类
chimera = nn.Sequential(NestMLP(),nn.Linear(16,20),FixedHiddenMLP())
chimera(X)
总结:
继承nn.Module类就可以进行比较灵活的构造
-
其中需要在init中定义好使用的层(torch在定义层时会自动初始化)
-
需要确定前向计算如何计算
2. 参数管理
假设我们已经定义好我们的类了,我们的参数如何访问。
我们首先关注具有单隐藏层的多层感知机
import torch
form torch import nn
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8,1)) # 单隐藏层的MLP
X = torch.rand(size=(2,4))
net(X)
2.1 参数访问
需要将每一层中的权重拿出来,net就是一个Sequential,相当于一个list
print(net[2].state_dict()) # 2访问的就是第三层(relu算一层);state是状态,权重可以认为是一个状态;_dict可以认为是_modules,就是ordereddict是排好序的全部参数
2.2 访问目标参数
可以直接访问某一个具体的参数
print(type(net[2].bias)) # 类型,这里是parameter类型的,定义的是一个可以优化的参数
print(net[2].bias) # 全部打印,包括值和梯度
print(net[2].bias.data) # 只打印值
net[2].weight.grad == None # 只访问梯度,这里还没有反向计算,因此为None
2.3 一次性访问所有参数
print(*[(name, param.shape) for name, param in net[0].named_parameters()])
print(*[(name, param.shape) for name, param in net.named_parameters()])
net.state_dict()['2.bias'].data # 有名字之后还可以通过名字获取我们需要的参数
2.4 从嵌套块收集参数
若网络中存在嵌套
def block1():
return nn.Sequential(nn.Linear(4,8), nn.ReLU(), nn.Linear(8,4), nn.ReLU())
def block2(): # 每次插入一个block1
net = nn.Sequential()
for i in range(4):
net.add_module(f'block {i}', block()) # 相当于sequantial,但是add_module的好处就是可以给块命名
return net
rgnet = nn.Sequential(block2(), nn.Linear(4, 1)) # 相当于sequential嵌套block2的sequential嵌套block1的sequential
rgnet(X)
# 想要查看网络结构,可以打印输出一下
print(rgnet) # 可以看到整体的嵌套结构,但是由于网络很复杂时难以理解,因此最好能在构建网络时就对网络进行分块,更好访问和打印
2.5 内置初始化
上面讲了如何访问参数,下面将讲如何修改默认的初始函数
# 首先定义一个函数,初始化为正态分布
def init_normal(m): # 这里m就是一个module
if type(m) == nn.Linear: # 如果是线性类,初始化,不是则不用管
nn.init.normal_(m.weight, mean=0, std=0.01) # 下划线在后面,说明是替换函数,原地操作,而非返回一个值
nn.init.zeros_(m.bias) # 这些用于初始化的函数都在init这个module里
net.apply(init_normal) # 对网络里所有的module(有嵌套的会进行嵌套)进行初始化,这个apply可以进行很多操作,本质上就是对网络进行一个遍历,遍历每一层
net[0].weight.data[0], net[0].bias.data[0]
# 定义一个函数
def init_constant(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 1) # 初始化为constant为1的参数,当然不应该这么做(初始化为常数)!这样没法继续训练了
nn.init.zeros_(m.bias)
net.apply(init_normal)
net[0].weight.data[0], net[0].bias.data[0]
2.6 对某些块应用不同的初始化方法(apply)
def xavier(m): # 初始化方法之一
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight)
def init_42(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 42)
net[0].apply(xavier) # 可以对不同的层使用apply函数进行遍历
net[2].apply(init_42)
print(net[0].weight.data[0])
print(net[2].weight.data)
2.7 自定义初始化
因为初始化函数是自定义的,因此可以自定义一些东西,例如帮助debug,或者进行一些判断等等。
2.8 参数绑定
有两个数据流,希望一些层可以共享权重,也就是这些层的权重是一样的
# 首先构造需要share的层
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), shared, nn.ReLU(), shared, nn.ReLU(), nn.Linear(8, 1))
net(X)
print(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0, 0] =100
print(net[2].weight.data[0] == net[4].weight.data[0]) # 这两层是一样的,要变一起变
3. 自定义层
和自定义网络没有区别,因为层也是nn.module的子类
3.1 构造一个没有任何参数的自定义层
import torch
import torch.nn.functional as F
from torch import nn
class CenteredLayer(nn.Module):
def __init__(self):
super().__init__()
def forward(self, X):
return X - X.mean()
layer = CenteredLayer()
layer(torch.FloatTensor([1, 2, 3, 4, 5]))
这个层可以作为组件合并到构建更复杂的模型中
net = nn.Sequential(nn.Linear(8, 128), CenteredLayer())
Y = net(torch.rand(4, 8))
Y.mean()
3.2 构造一个带参数的图层
class MyLayer(nn.Module):
def __init__(self, in_units, units): # 线性层有输入维度和输出维度
super().__init__()
self.weight = nn.Parameter(torch.randn(in_units, units)) # 层的参数都是parameter类的实例,因此调用parameter设置参数,这里也进行初始化了,放入nn.parameter里后会自动帮忙加入梯度、给一个合适的命名
self.bias = nn.Parameter(torch.randn(units,)) # 有逗号创建的是向量
def forward(self, X):
linear = torch.matmul(X,self.weight.data)+self.bias.data # 需要注意是.data才能访问参数值
return F.relu(linear)
# 访问参数
dense = MyLinear(5,3)
dense.weight
# 构建网络
net = nn.Sequential(MyLinear(64, 8), MyLayer(8, 1))
net(torch.rand(2, 64))
总结:
自定义层和自定义网络没区别,都要继承nn.module,但是要是有参数需要放入nn.parameter里
4.读写文件
训练好的东西如何存下来
4.1 加载和保存张量
import torch
from torch import nn
from torch.nn import function as F
# 保存矩阵
x = torch.arange(4)
torch.save(x, 'x-file') # 保存下的内容及保存到的文件名
x2 = torch.load("x-file") # 读取
x2
# 保存张量列表,list
y = torch.zeros(4)
torch.save([x, y], 'x-files')
x2, y2 = torch.load('x-files') # 两个
# 保存张量字典
mydict = {'x':x,'y':y}
torch.save(mydic, 'mydict')
mydict2 = torch.load('mydiact')
mdict2 # 字典
4.2 加载和保存模型参数
保存模型需要保存的东西:
- pytorch不方便存下整个网络的模型(定义)
- 可以存下来的是模型的权重
# 定义
class MLP(nn.Module):
def __init__(self):
super().__init__()
self.hidden = nn.Linear(20, 256)
self.output = nn.Linear(256, 10)
def forward(self, x):
return self.output(F.relu(self.hidden(x)))
# 测试
net = MLP()
X = torch.randn(size(2, 20))
Y = net(X)
# 保存
torch.save(net.state_dict(), 'mlp.params') # 访问全部参数
# 读取
clone = MLP() # 需要读取保存参数的文件&读取并生成整个网络模型!这里的模型参数已经被随机初始化了,但是没关系
clone.load_state_dict(torch.load('mlp.params')) # 1.从文件中读取出参数信息;2.放入刚刚定义好的模型
clone.eval() # 进入测试模式
Y_clone = clone(X)
Y_clone == Y # 可以发现,二者相等
补充:
-
train和eval两种模式,除非存在droupout或batchnorm,两者没有区别
-
将类别变量转换(独热编码)成伪变量时炸内存:
- 使用稀疏矩阵来存
- 独热编码的名词选择更改,不选用句子,而选用里面的词
-
mlp每层单元数可以靠交叉验证寻参
-
实例调用的
net(x)
是调用了父类实现的net.__call__()
函数,因为用的是nn.module,将—call函数等价于forward函数了,两者调用效果相同(官方不建议调用forward函数) -
forward函数是对着paper里的公式得到的,你这块儿的网络怎么实现就怎么进行forward
-
还有很多初始化方法:例如kaiming初始化,初始化只是在模型训练最开始时不要直接炸掉
-
torch默认会进行一个初始化
-
自定义的激活函数可能非处处可导,但是一般都能导,数值计算遇到不可导点概率较低,遇到随便给个值就像,因此不用特别关心
二、GPU购买与学习
1. 使用GPU
- 在电子课本对应章节,选择pytorch版本代码,再点击右上角,可以直接使用免费gpu
1.1 在单GPU上如何计算:
- 首先确定有GPU:
nvidia-smi
,可以看到CUDA版本,有几块,每块多大,用了多少,占用百分比(百分比低于50%说明模型定义的不好) - 如何表达硬件:深度学习的模型默认在CPU上运行,如果需要在GPU上运行,那么需要指定
import torch
from torch import nn
# 查看有几块GPU
torch.cuda.device_count()
# 下面分别表示CPU、第0个GPU、第1个GPU
torch.device('cpu'), torch.cuda.device('cuda'), torch.cuda.device('cuda:1')
1.2 请求的GPU不存在
需要定义函数,当请求的GPU不存在时,运行如下代码:
def try_gpu(i=0):
# 如果存在,则返回gpu(i),否则返回cpu()
if torch.cuda.device_count() >= i+1:
return torch.device(f'cuda:{i}')
return torch.device('cpu')
def try_all_gpus():
# 返回所有可用的GPU,如果没有GPU则返回[cpu(),]
devices = [torch.device(f'cuda:{i}') for i in range(torch.cuda.device_count())]
return device if devices else [torch.device('cpu')]
try_gpu(), try_gpu(10), try_all_gpus()
1.3 在GPU上创建tensor
x = torch.tensor([1, 2, 3]) # 默认创建的tensor
x.device # 由此可以看到张量在哪里,默认是在CPU上
# 储存在GPU上
X = torch.ones(2, 3, device=try_gpu()) # 确定张量放在哪里
X
# 在第二个GPU上创建一个随机张量
Y = torch.rand(2, 3, device=try_gpu(1))
Y
1.4 不同device上的张量计算
当两个张量都在CPU或者都在同一块GPU上时,直接计算,不在时需要移动:GPU上移动数据到CPU、GPU间移数据是很慢的,因此不如直接报错,不在同一块就不运算
# 上面定义的X、Y,需要做运算时,要确定在哪里执行这个操作
Z = X.cuda(1) # 将X放到第1个GPU上
print(X)
print(Z)
# 在同一块GPU上,可以开始计算,结果也会写在这个GPU上
Y+Z
# 判断Z是否已经在需要的GPU上,先判断可以避免重复运算
Z.cuda(1) is Z
1.5 在GPU上做神经网络
net = nn.Sequential(nn.Linear(3, 1)) # 正常创建
net = net.to(device=try_gpu()) # 将网络移动到需要的GPU上去,将所有的参数在0号GPU上拷贝一份
net(X) # X也在0号GPU上,因此该网络会在GPU上做运算
# 确认模型参数存储在同一个GPU上
net[0].weight.data.device # 访问存储的GPU位置
总结:
- 首先有GPU
- 安装cuda
- 将数据手动移动到同一GPU上(对于神经网络就是将参数和输入数据进行拷贝)
- 进行操作(数据在哪里,操作也会在哪里进行)
2. 购买GPU
重点:
- 显存
- 计算能力:每秒完成的浮点数计算
- 价格
建议:
- 买新不买旧
- 买贵的(但是不需要太大的内存,显存很贵,但是越大越好~)
- 可以不买卡
- 注意电源能供上电
总结:
- GPU性能会有上限
- 跑项目显存不够可以把
batch_size
调小,但是这样性能会变小==》可以把模型变小 - GPU使用率越高越好,不怕长时间满负荷,但是怕散热(自己的卡,不要长时间保持温度高于80度!会掉性能&烧了)
- 最好在最后也就是在network之前放到GPU,因为很多数据变化在GPU上不太支持&这样会占用GPU资源(如果数据读的比GPU快,需要将数据处理放到CPU上,这样更多的GPU可以用来u训练网络),需要注意,支持的话这一步可以往前提
- nn.module只能通过
to(device)
调整到需要的GPU上 - GPU上推理就是forward
- 在nvidia上训练模型不是很好用
- cuda就是GPU的编译器
- GPU上推理会比train好一点,因为推理的话内存不是关键,可以将batch_size设置大一些
- 创建一个实例,就是设置一个新的参数(默认不share),只有将创建的实例放在不同地方时,才是共享参数
三、略
第18节,还没看
四、卷积层
例如图片分类模型,如果还是使用MLP,图片的输入(像素个数)太多了,需要的参数太多了,这就需要卷积层。
对于图像判定,需要在任何位置都能找到这个”像素“,需要有两个原则:
- 平移不变性
- 局部性
1. 重新考察全连接层
从全连接层出发,应用上述原则,得到卷积,因此卷积就是一个特殊的全连接层
- (图像信息有相关性,不能当成一个向量来输入)将输入和输出变形为矩阵(宽度,高度)
- 将权重变形为4-D(二维x的权重+偏移<两个维度>、输出“分类”数量<一个维度>、层数<一个维度>),因为是张量(h,w)到(h’,w’),下式中第i层、j个神经元,对应的第k行、l列的输入的参数,因此是四维
h i , j = ∑ k , l w i , j , k , l x k , l = ∑ a , b v i , j , a , b x i + a , j + b h_{i,j}=\sum_{k,l}w_{i,j,k,l}x_{k,l}=\sum_{a,b}v_{i,j,a,b}x_{i+a,j+b} hi,j=k,l∑wi,j,k,lxk,l=a,b∑vi,j,a,bxi+a,j+b
- V是W的重新索引 v i , j , a , b = w i , j , i + a , j + b v_{i,j,a,b}=w_{i,j,i+a,j+b} vi,j,a,b=wi,j,i+a,j+b,这样就能引出卷积是怎么做的
1.1 应用原则1-平移不变性
- x的平移导致h的平移 h i , j = ∑ a , b v i , j , a , b x i + a , j + b h_{i,j}=\sum_{a,b}v_{i,j,a,b}x_{i+a,j+b} hi,j=∑a,bvi,j,a,bxi+a,j+b这里x位置变化,(i,j)变化,所乘的v也会变化,这不符合原则,v不应该依赖于(i,j),**借鉴弹幕:输入信息位置(i+a,j+b)输出位置(i,j)两者依靠(i,j,a,b)连接,这个连接考虑到了绝对位置(i,j)和相对位置(a,b),而根据平移不变性不应该考虑(i,j),只需要考虑(a,b)。**也就是说无论输入图像中目标像素出现在那个位置,输出都是不变的。
- 解决方案: v i , j , a , b = v a , b v_{i,j,a,b}=v_{a,b} vi,j,a,b=va,b
h i , j = ∑ a , b v a , b x i + a , j + b h_{i,j}=\sum_{a,b}v_{a,b}x_{i+a,j+b} hi,j=a,b∑va,bxi+a,j+b
也就是说前两个维度是不变的,是常数(也就是说,对一张图像使用同一个卷积核进行扫描,这样不管目标出现在哪里,都是使用的同一个“探测器”,对应位置的输出不变),这就是卷积核
相当于输出,就是对应位置的输入,在周围进行(a,b)大小的移动(访问周围的像素),和对应的参数相乘做累积。
这里(a,b)就是一个二维卷积核
二维卷积可以理解为全连接,但是由于平移不变性,使得一些参数是重复的,不是每一个元素都可以自由变换,这样取值范围受限,模型复杂度降低,需要存储的参数数量降低。
- 这就是2维交叉相关(卷积)
1.2 应用原则2-局部性
-
也就是说想要(i,j)输出,需要看(i,j)输入的(a,b)范围,实际上不应该看的太远,输出的(i,j)只应该考虑输入(i,j)附近的像素,不应该考虑较远的参数
-
解决方案:当|a|,|b|>△时,使得 v a , b = 0 v_{a,b}=0 va,b=0
h i , j = ∑ a = − △ △ ∑ b = − △ △ v a , b x i + a , j + b h_{i,j}=\sum_{a=-△}^△\sum_{b=-△}^△v_{a,b}x_{i+a,j+b} hi,j=a=−△∑△b=−△∑△va,bxi+a,j+b
卷积核大小变小只考虑△大小的范围内的像素
总结:
对全连接层使用平移不变性和局部性就可以得到卷积层
2. 卷积层
2.1 二维交叉相关
含义:
2.2 二维卷积层
输入被核扫描计算出输出,输出维度会发生变化(输入-核+1)
下图中的星星就是上面定义的二位交叉相关计算
例子
不同卷积核值,可以带来不同的效果:
神经网络可以通过核来检测到想要的东西。
2.3 交叉相关vs卷积
- 二维交叉相关:
y i , j = ∑ a = 1 h ∑ b = 1 w w a , b x i + a , j + b y_{i,j}=\sum_{a=1}^h\sum_{b=1}^ww_{a,b}x_{i+a,j+b} yi,j=a=1∑hb=1∑wwa,bxi+a,j+b
- 二维卷积:(有个负号)
y i , j = ∑ a = 1 h ∑ b = 1 w w − a , − b x i + a , j + b y_{i,j}=\sum_{a=1}^h\sum_{b=1}^w w_{-a,-b}x_{i+a,j+b} yi,j=a=1∑hb=1∑ww−a,−bxi+a,j+b
- 由于对称性(w是学到的),在实际使用中没有区别
2.4 一维和三维交叉相关
2.4.1 一维
可以是文本、语言、时序序列
y
i
=
∑
a
=
1
h
w
a
x
i
+
a
y_i=\sum_{a=1}^hw_ax_{i+a}
yi=a=1∑hwaxi+a
w就是向量了
2.4.2 三维
可以是视频、医学图像、气象地图
y i , j , k = ∑ a = 1 h ∑ b = 1 w ∑ c = 1 d w a , b , c x i + a , j + b , k + c y_{i,j,k}=\sum_{a=1}^h\sum_{b=1}^w\sum_{c=1}^dw _{a,b,c}x_{i+a,j+b,k+c} yi,j,k=a=1∑hb=1∑wc=1∑dwa,b,cxi+a,j+b,k+c
总结
- 卷积层将输入和核矩阵进行交叉相关,加上偏移后得到输出
- 核矩阵和偏移是可学习得参数
- 核矩阵的大小是超参数(卷积核的大小,局部性的大小,一旦顶下和输入数据量大小关系不大)
3. 代码实现
补充:
- 单mlp就是全连接层。
- 卷积核(感受野)不必过大,就像全连接层中隐藏层不必浅且大,可以深但是窄一点。
- 损失迭代抖动很厉害,没关系,只要能下降就行,有可能是因为学习率低;数据多样性较大(没关系的,可以将批量大小变大)。
- 全连接层放不下的问题在于w大小取决于输入数据大小,对图片问题不好用。
五、卷积层里的填充和步幅
这两个操作可以控制卷积层的输出大小
1. 填充
给定(32*32)输入图像,应用5*5卷积核,每次卷积减小4*4,而更大的卷积核可以更快地减小输出大小,这有可能和我们期望的大小不一样,而且很小的结果不能得到更深的网络。
因此一个解决方法就是填充:
在输入周围添加额外的行/列
1.1 填充大小计算
-
填充 p h 行 , p w 列 p_h行,p_w列 ph行,pw列,输出形状: ( n h − k h + p h + 1 ) ∗ ( n w − k W + p W + 1 ) (n_h-k_h+p_h+1)*(n_w-k_W+p_W+1) (nh−kh+ph+1)∗(nw−kW+pW+1)
-
通常取 p h = k h − 1 , p w = k w − 1 p_h=k_h-1,p_w=k_w-1 ph=kh−1,pw=kw−1(这样卷积后输出大小=输入大小)
- 当 k h k_h kh为奇数:在上下两侧填充 p h 2 \frac{p_h}{2} 2ph
- 当 k h k_h kh为偶数(卷积核很少为偶数):在上侧填充 ⌈ p h 2 ⌉ \lceil\frac{p_h}{2}\rceil ⌈2ph⌉在下侧填充 ⌊ p h 2 ⌋ \lfloor\frac{p_h}{2}\rfloor ⌊2ph⌋
2. 步幅
填充减小的输出大小与层数线性相关,步幅变为指数相关
对于一个较大的输出,若使用较小的卷积核,想要得到的较小的输出需要计算很多层,计算量太大。当然可以使用较大的卷积核,但是我们使用的卷积核一般为5*5或3*3很少使用很大的卷积核。
2.1 定义
步幅是指行/列的滑动步长
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ItZDVtLO-1652441310715)(https://gitee.com/tianyiduan/figurebed/raw/master/img/image-20220506153331691.png)]
2.2 步幅的计算
当步幅为
s
h
行
和
s
w
列
s_h行和s_w列
sh行和sw列,输出的形状:
⌊
(
n
h
−
k
h
+
p
h
+
s
h
)
s
h
⌋
∗
⌊
(
n
w
−
k
w
+
p
w
+
s
w
)
s
w
⌋
\lfloor\frac{(n_h-k_h+p_h+s_h)}{s_h}\rfloor*\lfloor\frac{(n_w-k_w+p_w+s_w)}{s_w}\rfloor
⌊sh(nh−kh+ph+sh)⌋∗⌊sw(nw−kw+pw+sw)⌋
如果
p
h
=
k
h
−
1
,
p
w
=
k
w
−
1
p_h=k_h-1,p_w=k_w-1
ph=kh−1,pw=kw−1,输出结果为:
⌊
(
n
h
+
s
h
−
1
)
s
h
⌋
∗
⌊
(
n
w
+
s
w
−
1
)
s
w
⌋
\lfloor\frac{(n_h+s_h-1)}{s_h}\rfloor*\lfloor\frac{(n_w+s_w-1)}{s_w}\rfloor
⌊sh(nh+sh−1)⌋∗⌊sw(nw+sw−1)⌋
如果输入高度和宽度可以被步幅整除(很容易遇到,一般步幅取2;又因为向下取整,会将
s
h
s_h
sh计算出的那个1约掉):
(
n
h
s
h
)
∗
(
n
w
s
w
)
(\frac{n_h}{s_h})*(\frac{n_w}{s_w})
(shnh)∗(swnw)
总结:
- 填充和步幅都是卷积层的超参数(还有一个是卷积核的大小)
- 填充在输入周围添加额外的行/列(通常值为0),来控制输出形状的减少量(图片比较小,实现网络较深)
- 步幅是每次滑动核窗口时的行/列的步长,可以成倍的减少输出形状(图片比较大,实现计算量减少)
3. 代码实现
在调用pytorch的函数时,加两个参数即可。
补充:
- 填充一般是核-1;步幅一般是1,这是最好的,计算量太大时不为1,通常为2减半;因此核大小的确定是最重要的,一般为奇数,好填充。
- 步幅填充是网络架构的一部分,一般不需要调节(参考别人的论文时)
- 虽然一层中的卷积核很小,但是因为层数很多,因此感受野还是很大
- 可以将超参数也一起训练:nas(多建几个试一试,多几个路选最好的路并最终只要这一条路)
- 信息永远在丢失,就算输出和输入是一样的。
- 验证集够好,可以避免过拟合
- 多层小卷积等效于低层大卷积,但是小卷积计算更快~
- 底层可以用大kernel,上层一定用小kernel;但是构造复杂,计算复杂,因此不如全部都用3*3的kernel
六、卷积层中的多输入多输出通道
一个重要的超参数:通道数
1. 多个输入和输出通道
1.1 多个输入通道
-
彩色图像可能有RGB三个通道,转换为灰度会丢失信息
-
当存在多个输入通道时:对每个通道都有一个卷积核,结果是所有通道卷积结果的和
公式表示为:
1.2 多个输出通道
- 无论有多少输入通道,到目前为止我们只用到单输出通道
- 我们可以有多个三维卷积核,每个核生成一个输出通道
- 输入 X : c i ∗ n h ∗ n w X:c_i*n_h*n_w X:ci∗nh∗nw
- 核 W : c o ∗ c i ∗ k h ∗ k w W:c_o*c_i*k_h*k_w W:co∗ci∗kh∗kw
- 输出 Y : c o ∗ m h ∗ m w Y:c_o*m_h*m_w Y:co∗mh∗mw
Y i , : , : = X ★ W i , : , : , : f o r i = 1 , . . . , c o Y_{i,:,:}=X★W_{i,:,:,:}\quad for\quad i=1,...,c_o Yi,:,:=X★Wi,:,:,:fori=1,...,co
1.3 多个输入和输出通道
为什么要这样的原因:
- 每个输出通道可以识别特定模式
- 输入通道和识别并组合输入中的模式(下一层处理上一层的多个通道的输入,对各个模式信息进行识别,并将识别结果进行组合)
1.4 特殊的1*1卷积层
k
h
=
k
w
=
1
k_h=k_w=1
kh=kw=1是一个受欢迎的选择。它不识别空间模式,只是融合通道(特征维度)。相当于输入形状为
n
h
n
w
∗
c
i
n_hn_w*c_i
nhnw∗ci,权重为
c
o
∗
c
i
c_o*c_i
co∗ci的连接层。
1.5 二维卷积层
这就是二维卷积层的全部参数:
模型存储比较小,但是计算量较大
总结:
- 输出通道数是卷积层的超参数(输入不是,输入是前一层的超参数)
- 每个输入通道都有独立的二维卷积核,所有通道结果相加得到一个输出通道结果
- 每个输出通道有独立的三维卷积核
2. 代码实现
当成单通道计算得到的值与当成矩阵计算出的值相等,单通道就相当于全连接,而矩阵相当于卷积,因此二者相等
调包实现:
补充:
- 输出通道数可以任意定,但是一般宽高不变则通道数不怎么变;宽高减半时一般将通道数加倍
- 填充0,会增加一点点计算性能;对模型性能没有影响;而且对于计算数值相当于多了一个偏差值,对于神经网络来说无所谓。
- 每个通道的卷积核不相同(不共享参数),同一层不同通道的卷积核大小相同(一般是一样的存粹是因为计算上效率更高,也可以不一样)。
- bias的影响较低,但是是有用的(起到均值化操作),而且对计算性能没什么影响。
- 核的参数是学出来的。
- 本节讲的2d,和3d处理方法不相同。
- 可以先对输入进行一个卷积,不在加起来再输出,提取空间信息;然后使用1*1卷积调整输出通道。
- 卷积就是输出位置对应的输入位置的元素的局部信息的提取,因此卷积对位置信息很敏感。
- 推荐jupyter和vscode。
- feature map就是卷积的输出。
- 输入通道不能动态变化。
- 3d可以处理视频,可以处理rgb+深度信息;当然也可以用2d卷积,一个深度一个深度地处理然后用rnn或者别的连起来就像。
七、池化层
1. 池化层
- 卷积对位置敏感
- 需要一定程度的平移不变性(因为对位置过于敏感并不是我们想要的),这就是池化层要做的
1.1 二维最大池化
- 返回滑动窗口中的最大值
允许输入有一点点小的偏移,有一点模糊化的效果
1.1.1 填充,步幅和多个通道
- 池化层与卷积层类似,都具有填充和步幅
- 没有可学习的参数(没有要学习的kernel,就是取最大即可)
- 在每个输入通道应用池化层以获得相应的输出通道(不会融合,这是卷积干的事)
- 输出通道数=输入通道数
1.2 平均池化层
- 最大池化层:每个窗口中最强的模式信号
- 平均池化层:将最大池化层中的“最大”操作替换为“平均”
总结:
- 池化层返回窗口中最大或平均值
- 缓解卷积层对位置的敏感性(通常作用在卷积层之后)
- 同样有窗口大小、填充、步幅作为超参数(不会融合,不用学习)
2. 代码实现
补充:
- 池化层现在出现的较为少,其窗口有重叠和没有重叠是没有影响的。
- python的forloop开销巨大
- 矩阵拼接很慢,但是可以创建一个list,list of list再转成tensor,更快
- 池化层后计算量可能减小,步幅不为1,不padding;也可能输入输出大小不变,而维度又不会变,因此可能不会减小计算量。
- 池化:位置不敏感,输出减少计算减少;但是可以用卷积+strite来减少计算量,可以对数据进行数据增强,这样也能对位置信息不敏感,因此池化层用的越来越少了。
- 正则:验证误差降低,训练误差不下降。
八、LeNet-经典卷积神经网络
1. LeNet
最早用于手写数字识别
附带了著名的数据集:MNIST数据集
1*32*32–卷积–>6*28*28–pooling–>6*14*14–卷积–>16*10*10–pooling–>16*5*5–flatten–>拉成一个向量–全连接层–>120向量–全连接层–>84向量–高斯层–>10向量(多分类,类似于多类分类)
总结:
- LeNet是早期成功的神经网络,证明了在图像处理上有效
- 先使用卷积层来学习图片空间信息(压缩到每个模式,模式数量很多,每个模式高宽很小),然后池化层降低图片的敏感度
- 通过全连接层转换到类别空间(逐渐降下来,更加平稳)
2. 代码实现
可以看到模型没有过拟合,这可能表示模型不够大,因为卷积层,参数受限,模型不够大~
补充:
- 时序性数据可以用卷积,也可以用池化
- 高宽变少,通道数变多,信息还是在减少
- view就是将一大块数据另一种方法看,reshape可能会改变存储(功能更多)
- 数据不大可以mlp(快),数据很大只能cnn
- 输出16个通道(匹配了更多模式)是对输入的6个通道的每一个进行加权得到的
- 输出通道数的选择可以自己试一试(把net中通道数量进行改变即可),调参一般先改大,然后慢慢调小(可以多跑几次,小数据集可以跑五次取平均)
- 神经网络准确率什么时候是好,取决于数据集与用户需求
- conv2d(配合maxpool2d)是一个二维图片;conv3d(配合maxpool3d)是三维图片(通常多一个时间轴,或者是立体的)
- 图象识别出来的特征可以打印出来,可以查看神经网络到底学习了什么。使用cnn visuailzation
- 中间层不必尽量大,否则容易过拟合
- 深度学习现在可以不必再必须非常大的数据集了,可以在大数据集上先训练好,再训练到小数据集上
- 神经网络是一种语言,可以用结构化的核拟合结构化或非结构化的数据
九、AlexNet-深度卷积神经网络
机器学习2000年时最主流的方法:首先特征提取;选择核函数来计算相似性(核函数可以把空间拉成我们想要的样子);就变成凸优化问题(可求极值);漂亮的定理
SVM不需要调参,对调参不敏感~
卷积一般用在图片上面,就是计算机视觉就是几何学:抽取特征;描述几何(例如多相机);(非)凸优化;漂亮定理;如果假设满足了,效果会很好
1. 特征工程
十年前,在深度学习领域如何抽取特征非常重要(对于SVM直接放入图片效果很差,需要先提取特征);特征描述子:SIFT,SURF;视觉词袋;最后
2. 硬件发展
90年代用神经网络,较为简单,2000年核方法,现在又回到神经网络,计算量上去了,可以构造更深的神经网络。
ImageNet:一个兴起的数据集(2010)。也是因为神经网络更深了可以抽取更复杂的信息。
3. AlexNet
赢得了2012年ImageNet竞赛,计算机视觉方法论的改变(从人工特征提取到SVM;变成了通过(端到端)CNN学习特征到Softmax回归(一起训练,cnn提取到的特征可能就是softmax需要的))
CNN:Convolitional Neual Network----卷积神经网络
主要改进:
- 丢弃法(正则)
- ReLU(梯度更大,再0点处一阶导更好)
- MaxPooling(输出更大,训练更容易)
3.1 AlexNet架构
3.1.1 第一层
因为图片边打,可以看到的窗口也应该变大,
池化层窗口变大,图片允许"移一点点"的范围变大,使用最大池化
3.1.2 第二层
层数变多,提取的特征变多
3.1.2 第三层
用了两个隐藏层,因为有1000类,因此隐藏层大小更大
3.1.3 其他
- 激活函数sigmoid变为relu
- 隐藏全连接层后加入了丢弃层
- 数据增强(对位置信息不再那么敏感)
3.2 复杂度
相对于图片,卷积层参数增加不多(卷积省参数)。但是计算量增加很多。
总结:
- Alex net是更大更深的lenet,10x参数个数,260x计算复杂度
- 新进入了丢弃法,relu,最大池化层,和数据增强
- Alex net的胜利标志着新一轮神经网络热潮的开始
3.3 代码实现
补充:
- 机器找到的特征不一定符合人类的理解
- 网络的设计除了看论文,不太好解释
- alaxnet中的LRN(Local Response Normalization)消融实验证明没什么用,可以不用理解
- 最后全连接层4096有两个,不能砍掉,因为砍掉效果会差
- 数据增强,太过分效果变差很正常
- lenet不是深度卷积神经网络,只是因为深度神经网络是一个宣传,在attention之前都是老东西。
- cv领域新模型越来越好,现在demo多,相当于在落地,说明技术成熟,但现在做设计不容易,热度高。
- 通常图片大小不符合要求,可以小程度地resize(要保证高宽比),再随机扣几张图出来进行预测。
- 增加了几个卷积层,可能只是这样调参出来效果好。
十、VGG-使用块的网络
alexnet长得不规则,感觉像是随变调的。想要更深更大,需要更好的思想,将框架搞好。
1. VGG思想
- alexnet比lenet更深更大来得到更好的精度,那么能不能更深更大?
- 更多的全连接层(太贵)
- 更多的卷积层(alexnet改的太乱,vgg希望成块)
- 将卷积层组合成块
1.1 VGG块
其实就是alexnet的一个拓展,将其中的部分提出来
- n层,m通道的3*3卷积(深但是窄的效果更好因此是3*3卷积,5*5块计算量大,宽但是浅);填充1
- 2*2最大池化层;步幅2
1.2 VGG架构
- 多个VGG块后接全连接层
- 不同次数的重复块得到不同的架构VGG-16,VGG-19
变成下面这个样子:
1.3 进程
- lenet
- 2卷积+池化层
- 2全连接层
- alexnet
- 更大更深
- relu,dropout,数据增强
- vgg
- 更大更深的alexnet(重复的vgg块)
越往下越慢,需要的内存高,准确率高
总结:
- vgg使用可重复使用的卷积快来构建深度卷积神经网卡(这一思想很重要)
- 不同的卷积快个数和超参数可以得到不同复杂度的变种(不同配置)
2. 代码实现
补充:
-
视觉领域现在都不再进行人工特征研究啦
-
有时间可以学习一下特征值、特征向量、奇异值分解
十一、NiN-网络中的网络
用的不多,但是思想很重要
之后的神经网络一般都有一个自己的块
1. 全连接层的问题
-
卷积层需要较少的参数,数量: c i ∗ c o ∗ k 2 c_i*c_o*k^2 ci∗co∗k2相当于输入的通道数*输出的通道数*窗口的高*窗口的宽
-
但是卷积层后的第一个全连接层的参数很多(会占用很多内存,占用很多计算带宽,容易过拟合),数量:输入通道*输入的高*输入的宽*输出的通道数*输出的高*输出的宽
-
为了解决这一问题cnn完全不要全连接层
2. NiN块
- 一个卷积层后跟两个全连接层(1*1的卷积层相当于全连接层,非线性性)
- 全连接层:步幅1,无填充,输出形状和卷积层输出一样,起到全连接层的作用
这样相比于全连接层,参数数量变小很多。对于全部的像素,使用的参数都是一样的。但是对通道进行了融合。
3. NiN架构
-
NIN块中没有全连接层了
-
需要交替使用NiN块和步幅为2的最大池化层
- 逐步减小高宽和增大通道数
-
最后使用全局平均池化层得到输出(池化层窗口的高宽等于输入的高宽,相当于对每个通道,将最大的值拿出来;将输入变小,没有可学习的参数,计算变简单,模型复杂度贬变低,泛化性;缺点是收敛变慢),最后也没有用到全连接层
- 其输入通道数是类别数,加个softmax就能得到概率
总结:
- NiN块使用卷积层加两个1*1卷积层(当全连接层用),后者对每个像素增加了非线性性(每个像素的通道数做全连接,后面用RELU)
- NiN使用全局平均池化层来代替VGG和alexnet中的全连接层,不容易过拟合,使用更少的参数个数
4. 代码实现
softmax放到loss里面(和交叉熵捆绑在一起了,不需要单独写),可以看到卷积神经网络中都没有softmax
补充:
- 超极宽的但隐藏层极其容易过拟合
- GPU内存:参数(模型有多大)&参数梯度(模型有多深,深的话中间变量存储要求越高)+剩下的内存与batch_size呈线性关系
- 转到C++在第26节答疑讲到(libtorch);TensorRT
- 预测代码在softmax回归全实现中有展示,需要将Xcopy去GPU计算,再copy回CPU使用进行预测
- torch会自动初始化
补充:交叉熵与softmax
一般框架中使用交叉熵就默认加上了softmax,相当于隐式调用了。
十二、GoogLeNet-含并行连结的网络
1. Inception块
之前的lenet、alexnet、nin、vgg块用了不同的结构,但是那个好呢?不知道选哪个,那么googlenet全部都要,四个路径从不同层面抽取信息,四条路均没有改变高宽,只需要在输出通道维合并。高宽不变,通道变多
从左到右:(输入192通道,输出64+128+32+32=256;这些数据不知道怎么来的,但是可以将我们觉得重要的路径的通道数变多)
- 第一条路:64通道
- 第二条路:96通道(1*1卷积是为了改变通道数的,因为卷积核参数和输入输出通道数有关,因此需要降低通道数)–128通道(非1*1卷积是为了提取空间特征)
- 第三条路:16通道–32通道
- 第四条路:32通道
优点:
和单3*3或5*5卷积层相比,inception块有更少的参数个数和计算复杂度,而且具有多样性。
2. GoogLeNet
- 5段(stage:高宽减半叫一个stage),9个inception块
2.1 段(stage)1&2
- 相比alexnet,googlenet有更小的宽口和更多的通道
2.2 段3
使用了两个inception block,使用后高宽不变,但是通道数变多(每条道路上通道数分配并不均匀,重要的分的多)
2.3 段4&5
有点复杂~
3. inception后续很多变种
- inception-BN:使用了batch normalization
- inception-V3:修改了inception块
- 替换5*5为多个3*3卷积块
- 替换5*5为1*7和7*1卷积层
- 替换3*3为1*3和3*1卷积层
- 更深
- inception-V4:使用残差连接
3.1 V3的第3段
3.2 V3的第4段
3.3 V3的第5段
3.4 效果
V3虽然仍然计算量上较慢,内存需求也挺多的,但是准确率比vgg高很多
总结:
- inception块使用4条有不同超参数的卷积层和池化层的路来抽取不同的信息;它的一个主要优点是模型参数小,计算复杂度低
- googlenet使用了9和inception块,是第一个达到上百层的网络;后续有一系列改进
4. 代码实现
照着论文实现就行
比alexnet计算量大很多,性能还不错
补充:
- 1*1卷积层可以降低通道数,减小计算量(相比于其他大小的核,参数更少,更加纯粹)
- 池化层后的1*1卷积层相当于全连接层,之前的卷积层作用于上一层的输出
- 2 n 2^n 2n的通道数,在GPU上计算更快
- 除非数据过于特殊,或者对这一块非常有研究,否则不要轻易去改经典网络架构;但是通道数可以改一下,计算量小;输入输出也可以稍微改一下
- 3*3改为3*1和1*3可以降低计算量,效果不一定好
- linear dense都是全连接 flatten就是将批量大小保持住,其余全变成向量
- 一次性会计算一个批量大小的数据
- 如何调参:hpo工具,可以在imagenet的一个小子集上调参
- inception中不同路径,pytorch不会自动并行
- 模型设计、数据优化、调参数。。。trick都很重要!
- 一个模型效果好,不一定是论文中提到的那个东西的作用,很可能是很多小trick共同作用的结果
十三、BN-批量归一化
对于很深的网络,批量归一化很重要
- 损失出现在最后,后面的层训练较快
- 数据在最底部:
- 底部的层训练较慢(除非梯度爆炸,否则每一层的梯度应该都是一个很小的值,越乘越小)
- 底部层一变化,所有都得跟着变(底层提取的特征都变了,上层也需要变)
- 最后的那些层需要重新学习多次
- 导致收敛变慢
- 我们希望在学习底部层的时候避免变化顶部!
1. 批量归一化
为什么梯度不一样?是因为不同层的分布不一样,如果分布都一样,那么每一层学习就一样了
批量归一化思想就是:尝试固定小批量里面不同层的不同的地方的输出的均值和方差,然后再做额外的调整(可学习的参数)
(公式太复杂不好敲,其中 x i x_i xi是给批量归一化层的输入, x i + 1 x_{i+1} xi+1是批量归一化层的输出,其中均值和方差是算出来的,其他两个$\beta和\gamma $是可以学习的参数)
x
i
+
1
=
γ
x
i
−
μ
B
σ
B
+
β
x_{i+1}=\gamma\frac{x_i-\mu_B}{\sigma_B}+\beta
xi+1=γσBxi−μB+β
作用:假设变成均值为0方差为1的分布不太合适,可以去学习一个新的分布来获得对深度神经网络更好的数据,但是对$\beta和\gamma $的变化要有一个限制,不能变化的太猛烈
1.2 批量归一化层带来的东西
-
可学习的两个参数 $\beta和\gamma $
-
可以作用在:
- 全连接层和卷积层输出上,激活函数前(relu会将输出放到正数范围,归一化又放回去批量归一化是一个线性变换,会奇怪)
- 全连接层和卷积层输入上
-
对全连接层,作用在特征维(相当于列,一行是一个样本,一列是一个特征;对每一个全连接进行处理,而不像数据预处理只作用于数据)
-
对卷积层,作用在通道维(将通道层当作特征,一个通道是一个特征,一个通道的全部像素为一个样本)
2. 批量归一化在做什么
- 最初只是为了减少内部协变量转移
- 后续有论文指出它可能就是通过在每个小批量里加入噪音来控制模型复杂度(因为是在随机的小样本上计算出的均值和方差,随机性很大,又学习一个均值和方差,让噪声不至于过大)
- 因此没必要和丢弃法混合使用
总结:
- 批量归一化固定小批量中的均值和方差(将每一层的每一小批量数据分布固定成一个差不多的分布,就可以使用一样的学习率,不至于上面学太快下面学不到了),然后学习出适合的偏移和缩放
- 可以加速收速度(学习率可以调的更大),但一般不改变模型精度(不要精度不变)
3.代码实现
3.1 全实现
3.2 简洁实现
补充:
- xavier讲的normalization(初始化时较为稳定,不能保证之后)和这里的BN(在整个训练过程中每一层强制使用,以保证稳定)在本质上没有什么区别,是一个思路,希望模型稳定,容易收敛。批量归一化不像权重衰退会对权重有影响。
- BN可以用在mlp中,但是对深度神经网络更有用,对浅层网络没什么用。
- assert就是断言,不是会报个错
- 对于3、5、…维度的实现,需要告诉BN模型的特征维度是几(一般是2,对于卷积是通道;mlp是特征列)
- 虽然只是一个线性变换,但是可以保证数据稳定
- BN后每一层梯度值近似,学习率可以设置的更大,对权重的更新变快因此收敛时间变短
- 严格对比(就是多次实验看变化):batchsize(不能太大不能太小,需要和内存一起调,调到gpu使用效率较高的情况)、学习率(batchsize调好后调)、epoch数(可以大一点,可能会浪费资源,觉得收敛了就停掉即可)、框架(用惯一个就行都一样)都很重要
- 还有很多normalization方法,例如:layernorm(用于数据较大的网络),思想都是一样的,计算的维度不太一样
- BN一般不会用在激活函数之后,因为是线性变换
- 调节方法:调节batchsize,不断增大看每秒能处理的样本数,当不怎么变化时就很好了(当然对于lenet这种,batchsize可以调很大,但是这时候收敛很慢,因为重复的图片很多,影响收敛精度),当然若gpu利用率达到90%左右就很好了
十四、ResNet-残差网络
1. 思想
加更多的层总是能改进精度吗?不一定,可能学偏了,最优解效果还不如小模型(模型偏差)。需要是更深的层严格包含小模型,这样起码不会变差。
2. 残差块
- 串联一个层改变函数类,我们希望能扩大函数类
- 残差块加入加速通道(右边)来得到 f ( x ) = x + g ( x ) f(x)=x+g(x) f(x)=x+g(x)的结构。保证了假设新加的一层什么也没学到,上一层的结果(相当于嵌入小网络)也能传上去,不至于变坏(因为如果 g ( x ) g(x) g(x)没什么效果,那么它就拿不到梯度,只是最开始的很小的随机梯度,因此没什么贡献)。
3. ResNet块细节
下面是两种实现:(右边加上1*1卷积可以变换通道)
3.1 可以使用不同的残差块
4. ResNet块
核心:加了一个加法~
- 高宽减半resnet块(步幅为2),第一个块,(两条道路上的卷积层)实现了高宽减半&通道数加一倍
- 后接多个高宽不变resnet块
5. ResNet架构
- 类似于VGG和GoogleNet的总体架构
- 但是替换成了ResNet块
总结:
- 残差快使得很深的网络更加容易训练(因为有链接,不论多深总可以先将下面的小的神经网络训练好),甚至可以训练一千层的网络
- 残差网络对随后的深层神经网络设计产生了深远影响(现在网络结构中非常常见),无论是卷积类网络还是全连接类网络
6. 代码实现
补充:
- cos学习率一般比step或者固定学习率要好,而且简单
- 残差意思:先训练x,也就是上一层的输入,再训练g(x),也可以理解为先去训练一个平滑效果的模型,然后慢慢微调
- __init__里定义两个BN,是不一样的,两者都有参数要学,两者都有自己的参数
- relu的inplace参数说明不需要再新建一个参数,直接改写输入即可,可以省下一点内存
- 如何加入大量噪音,测试精度是有可能大于训练精度的
- 因为数据集有标错的,识别率达到100%不太可能而且没有意义。一般不能完全假设数据集是没有错误的
7. resnet为什么能训练出深层网络
- 如何处理梯度消失:
- 乘法变加法
相 当 于 对 于 底 层 : y = f ( x ) , 此 时 求 导 w = w − γ ∂ y ∂ w 对 于 普 通 的 深 层 网 络 : y ′ = g ( f ( x ) ) , 此 时 求 导 ∂ y ′ ∂ w = ∂ g ( y ) ∂ y ∂ y ∂ w , 前 者 若 是 很 小 , 连 乘 会 更 小 对 于 r e s n e t 网 络 : y ′ ′ = f ( x ) + g ( f ( x ) ) , 此 时 求 导 ∂ y ′ ′ ∂ w = ∂ y ∂ w + ∂ y ′ ∂ w , 将 乘 法 变 成 了 加 法 , 一 般 来 说 对 于 底 层 参 数 , 因 为 有 了 高 速 通 道 , 不 会 比 只 有 底 层 的 效 果 小 , 加 快 收 敛 , 可 以 更 好 地 训 练 相当于对于底层:y=f(x),此时求导w=w-\gamma \frac{\partial y}{\partial w}\\ 对于普通的深层网络:y'=g(f(x)),此时求导\frac{\partial y'}{\partial w}=\frac{\partial g(y)}{\partial y}\frac{\partial y}{\partial w},前者若是很小,连乘会更小\\ 对于resnet网络:y''=f(x)+g(f(x)),此时求导\frac{\partial y''}{\partial w}=\frac{\partial y}{\partial w}+\frac{\partial y'}{\partial w},将乘法变成了加法,一般来说对于底层参数,因为有了高速通道,不会比只有底层的效果小,加快收敛,可以更好地训练 相当于对于底层:y=f(x),此时求导w=w−γ∂w∂y对于普通的深层网络:y′=g(f(x)),此时求导∂w∂y′=∂y∂g(y)∂w∂y,前者若是很小,连乘会更小对于resnet网络:y′′=f(x)+g(f(x)),此时求导∂w∂y′′=∂w∂y+∂w∂y′,将乘法变成了加法,一般来说对于底层参数,因为有了高速通道,不会比只有底层的效果小,加快收敛,可以更好地训练
补充:
- 也可以让靠近输入的学习率大一些,靠近输出的学习率小一些,但是这样不好设置&调学习率是有瓶颈的
- 从误差的角度来理解底层梯度较小:拿到的梯度和误差是有一定关系的。因为误差在回传的过程中,前面的层逐层消化掉了误差,因此到后面能拿到的误差很小
十五、竞赛
第30节暂略