本周观看了李沐老师的《动手学深度学习》,李沐老师很注重代码的编写,但是对概念的讲解并不是特别多,所以还要去结合一些其他的资料去进行了解,下面是本周的所看的课程总结。
丢弃法(Drop out)
在前面我们介绍了L1,L2正则化,在损失函数后加入正则化项,以减轻过拟合。
为什么加入正则化项可以解决过拟合问题呢?
因为加入正则项后,损失函数进行梯度下降,会让权重w减小,降低参数w的大小范围可以降低模型复杂度,从而去解决过拟合问题。
为什么加入正则化项只限制参数w,而不限制b?
因为b只是偏移量,不会改变形状。
在这里介绍Drop out 丢弃法,它同样也是正则化,也是可以减轻过拟合问题的,它一般应用在神经网络中,减少隐藏层的单元,有了更加简单的网络模型,因此可以解决过拟合问题。
使用Drop out 丢弃法,可以使参数数目下降,进而降低模型复杂度,最终解决过拟合问题。
输出神经元不会过度依赖任意一个输入神经元。
好的模型要有很好的鲁棒性(robust)
Drop out 是在层与层之间加入噪音,Dropout 引入的噪音与常规的数据噪音不同,它是随机加入的,这样可以让模型更加鲁棒,更能够关注整体特征而非局部特征。
丢弃法是在层之间加入噪音,并非在输入增加噪音。
无偏差的加入噪音一般应用在全连接的隐藏层的输出上,并且只作用在训练过程中,并不作用在预测时。
在训练时,会在每一个forward时,随机丢弃掉一些神经元不参加计算和更新,但是在预测时,所有的神经元都参加。
x是一个值,x'是随机变量,因为带有噪音,E[x']才是一个值。
E[x’]=x:其中E[x’]指x‘的期望。虽然对x引入了噪音变成了x’,但 x’的期望(即平均下来)还是等于原x
p为概率,xi为输入x(是向量)的第i个元素。
概率p为丢弃的概率,丢弃法对每个元素进行如下扰动,在概率p下,也就是丢失的概率,样本值为0,在其他条件下,为xi/1-p;因为这样计算xi的期望时,仍然等于xi;E[xi']=p*0+(1-p)*xi/1-p=xi。
所以在这里除以1-p是为了xi'与原理的xi期望相同。
假设神经网络是如下图的单隐藏层,h为第一个隐藏层的输出。
在输入层中经过sigma函数输出变为h,在隐藏层中经过dropout正则化,经过丢弃,变为h',减少隐藏层的神经元,之后对其进行输出,最后通过softmax回归进行概率分布。
推理中的dropout,在前面也说过,只在训练中使用,预测即推理过程中,直接返回输入,如下图
总结
一般我们的丢弃概率设为 0.1,0.5,0.9
从零实现Dropout
1、引入相关模块
import torch
from torch import nn
from d2l import torch as d2l
2、我们实现dropout-layer函数,该函数以dropout的概率丢弃张量输入X中的元素
def dropout_layer(X,dropout): # 丢弃概率
assert 0<=dropout<=1 # assert断言
if dropout == 1: # 丢弃概率为1
return torch.zeros_like(X)
if dropout == 0: # 丢弃概率为0
return X
mask = (torch.rand(X.shape)>dropout).float() # 布尔型张量,每一个值表示X对应下标的值是否要dropout
return mask * X/(1.0-dropout) # 保持期望一致,有的变为0,有的就要放大
3、测试dropout_layer函数
X = torch.arange(16,dtype=torch.float32).reshape(2,8) # 输入X
print(X)
print(dropout_layer(X,0.)) # 丢弃概率为0
print(dropout_layer(X,0.5)) # 丢弃概率为0.5,大约有一半的元素被丢弃,剩下的为了保持期望一致,有的需要放大
print(dropout_layer(X,1.)) # 丢弃概率为1
'''
tensor([[ 0., 1., 2., 3., 4., 5., 6., 7.],
[ 8., 9., 10., 11., 12., 13., 14., 15.]])
tensor([[ 0., 1., 2., 3., 4., 5., 6., 7.],
[ 8., 9., 10., 11., 12., 13., 14., 15.]])
tensor([[ 0., 0., 4., 6., 0., 10., 0., 14.],
[ 0., 18., 0., 0., 0., 26., 28., 0.]])
tensor([[0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0.]])
'''
4、定义具有两个隐藏层的多层感知机,每个隐藏层包含256个单元
num_inputs,num_outputs,num_hiddens1,num_hiddens2 = 784,10,256,256 # 输入,输出,隐藏层大小
dropout1,dropout2 = 0.2,0.5 # 丢弃概率为0.2,0.5
class Net(nn.Module): # 定义神经网络类
def __init__(self,num_inputs,num_outputs,num_hiddens1,num_hiddens2,is_training=True):
super(Net,self).__init__()
self.num_inputs = num_inputs
self.training = is_training
self.lin1 = nn.Linear(num_inputs,num_hiddens1)
self.lin2 = nn.Linear(num_hiddens1,num_hiddens2)
self.lin3 = nn.Linear(num_hiddens2,num_outputs)
self.relu = nn.ReLU()
def forward(self,X):
H1 = self.relu(self.lin1(X.reshape((-1,self.num_inputs))))
if self.training == True: # 若在训练,则作用dropout
H1 = dropout_layer(H1,dropout1)
H2 = self.relu(self.lin2(H1))
if self.training == True:
H2 = dropout_layer(H2,dropout2)
out = self.lin3(H2) # 输出层不作用dropout
return out
net = Net(num_inputs,num_outputs,num_hiddens1,num_hiddens2)
5、训练和测试
num_epochs,lr,batch_size = 10,0.5,256
loss = nn.CrossEntropyLoss()
train_iter,test_iter = d2l.load_data_fashion_mnist(batch_size)
trainer = torch.optim.SGD(net.parameters(),lr=lr)
d2l.train_ch3(net,train_iter,test_iter,loss,num_epochs,trainer)
dropout简洁实现
直接调包,基于框架实现
net = nn.Sequential(nn.Flatten(),
nn.Linear(784,256),
nn.ReLU(),
nn.Dropout(dropout1),
nn.Linear(256,256),
nn.ReLU(),
nn.Dropout(dropout2),
nn.Linear(256,10))
def init_weights(m):
if (type(m)==nn.Linear):
nn.init.normal_(m.weight,std=0.01)
net.apply(init_weights)
trainer = torch.optim.SGD(net.parameters(),lr=lr)
d2l.train_ch3(net,train_iter,test_iter,loss,num_epochs,trainer)
当我们把丢弃率设为0时,我们发现训练误差更低了,训练准确率更高了,但是测试准确率有所波动,说明过拟合了
net = nn.Sequential(nn.Flatten(),
nn.Linear(784,256),
nn.ReLU(),
nn.Dropout(0),
nn.Linear(256,256),
nn.ReLU(),
nn.Dropout(0),
nn.Linear(256,10))
def init_weights(m):
if (type(m)==nn.Linear):
nn.init.normal_(m.weight,std=0.01)
net.apply(init_weights)
trainer = torch.optim.SGD(net.parameters(),lr=lr)
d2l.train_ch3(net,train_iter,test_iter,loss,num_epochs,trainer)
数值稳定性
神经网络的梯度公式如下图所示,其中t表示第几层,并且y并不是预测值,它包含了损失函数,通过链式法则计算损失函数关于参数Wt的梯度。
其中ht表示第t层的输出,同理ht-1表示第t-1层的输出。
数值稳定性的常见两个问题,一个是梯度爆炸,一个是梯度消失。
梯度爆炸就是其中算出来的梯度值比1大,通过链式法则相乘,做100次,形成梯度爆炸;
梯度消失就是其中算出来的梯度值比1小,通过链式法则相乘,做100次,形成梯度消失。
加入MLP(多层感知机)之后的计算如下:
其中,Wt是第t层的权重,ht-1是第t-1层的输出,在这里省略了偏移b,权重与t-1层的输出相乘,得到了第t层的输出ht。
之后进行求偏导,进行链式法则相乘。
梯度爆炸,当W元素值大于1时,层数很深时,连乘会导致梯度爆炸。
梯度爆炸的问题
梯度消失,求导后的元素值是d-t个小数值的乘积,发生梯度消失,越来越小,越来越接近0.
梯度消失后的问题
梯度反向传播从顶层开始,越往下走,梯度会越来越小。
底层靠近输入层,由于反向传播,最底层梯度最后计算,因此,最底层梯度消失严重。
总结
ReLU比较容易梯度爆炸,而sigmoid容易梯度消失。
反向传播
其中里面的反向传播是这样理解的,反向传播是从输出层往回传,训练过程中传输误差,优化模型参数
对于一个神经网络,当激活函数为sigmoid时,前向传播的计算过程如下:
反向传播是将损失的梯度从输出层往输入层的方向,一层层回传,从而更新每层权重参数过程,采用链式法则,计算损失函数对所有参数的梯度。
模型初始化和激活函数
为了让训练更加稳定,我们需要有合理的权重初始和激活函数
我们希望神经网络的每一层的输出和梯度都是均值为0、方差为固定数的随机变量
E[hit],其中t为第t个隐藏层,i为该层的第i个元素,对所有的t和i,正向的输出期望为0,方差为a,为一个常数。
a和b都为常数。
我们知道,在训练开始时更容易出现数值不稳定问题,远离最优解时 更容易出现数值不稳定的地方,出现梯度过大,在最优解附近时,梯度相对会较小。
我们进行假设,假设wtij是独立同分布的,期望为0,方差为一个常数,如果没有激活函数的话,那么第t层的权重乘以第t-1层的输出就为第t层的输出,又因为是独立同分布的,EXY = EX * EY ,期望计算为0,
对于正向方差,有方差公式 DX = E(X^2) - (EX)^2,第t层的输出方差计算得
其中nt-1是第t层的输入维度,wtij的方差为伽马t,因为独立求和共 nt-1个,故当nt-1乘以伽马t等于1时,满足第t层的输出方差等于第t层的输入方差(第t-1层的输出方差)
而反向的均值和方差计算如下:
在这里提出了Xavier初始化,具体如下:
对某层权重初始化时,该层权重的初始化会根据该层的输入维度、输出维度来决定,根据输入输出维度来适配权重,使得输出的方差和梯度都在恒定(合理)的范围内。
例如,假设线性的激活函数,如下图所示,计算其期望和方差,若要满足期望等于0,输出的方差等于输入的方差,则贝塔等于0,阿尔法等于1
反向传播计算过程如下:
最后激活函数为sigma(x)=x
但是我们常用的激活函数都是非线性的,上面只是拿线性激活函数来举例。
一般神经网络权重的取值都是在0附近,而tanh和relu激活函数在原点附近基本满足sigma(x)=x,所以我们使用激活函数一般使用relu激活函数,但是sigmoid函数不过原点,我们可以略微挑战sigmoid函数,并使它过原点。
综上所述,合理的权重初始值和激活函数的选取可以提升数值稳定性。
神经网络模型构造
1、我们可以直接使用nn.Sequential进行模型的构造
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))
X = torch.rand(2,20)
net(X)
2、还可以进行自定义块
class MLP(nn.Module):
def __init__(self):
super().__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) # net(X)直接调用了__call__()方法
3、顺序块,Sequential类是如何工作的,我们定义了一个MySequential类
class MySequential(nn.Module):
def __init__(self,*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)
4、在正向传播函数中执行代码
有时我们可能希望合并既不是上一层的结果也不是可更新参数的项,成为常数参数,现了一个FixedHiddenMLP类
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):
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()
net = FixedHiddenMLP()
net(X)
权重(self.rand_weight)在实例化时被随机初始化,之后为常量。 这个权重不是一个模型参数,因此它永远不会被反向传播更新。 然后,神经网络将这个固定层的输出通过一个全连接层。
同时,模型还可以混合搭配各种组合块,这里就不一一展示了。
神经网络参数管理
1、构建一个单隐藏层的MLP
net = nn.Sequential(nn.Linear(4,8),nn.ReLU(),nn.Linear(8,1))
X = torch.rand(size=(2,4))
2、我们可以通过state_dict()来查看参数,这是一个顺序字典,有每一层的权重和偏置,我们可以按照字典的方式将它们取出来
net.state_dict()
'''
OrderedDict([('0.weight',
tensor([[-0.1695, -0.1372, -0.3904, 0.3979],
[ 0.3936, 0.1350, 0.3523, -0.3376],
[-0.1134, 0.0012, -0.0879, 0.0508],
[ 0.0623, 0.2694, 0.4171, -0.2402],
[-0.0254, -0.3083, -0.1296, 0.1220],
[-0.0769, 0.0810, 0.1792, 0.2316],
[ 0.1187, -0.1702, 0.2684, -0.0632],
[-0.1498, 0.3893, 0.4006, -0.3496]])),
('0.bias',
tensor([ 0.2224, -0.1492, -0.2497, -0.2094, -0.2337, -0.3730, -0.2129, -0.1957])),
('2.weight',
tensor([[ 0.3375, 0.0474, 0.1823, -0.1498, -0.3459, 0.1681, 0.1389, -0.2228]])),
('2.bias', tensor([-0.0654]))])
'''
3、还可以访问每一层的梯度
net[2].weight.grad == None # 访问梯度
'''
True
'''
4、可以从嵌套块收集数据,其中add_module方法可以用来向神经网络模型中添加新的层或模块
def block1():
return nn.Sequential(nn.Linear(4,8),nn.ReLU(),nn.Linear(8,4),nn.ReLU())
def block2():
net = nn.Sequential()
for i in range(4):
net.add_module(f'block {i}',block1())
return net
rgnet = nn.Sequential(block2(),nn.Linear(4,1))
'''
Sequential(
(0): Sequential(
(block 0): Sequential(
(0): Linear(in_features=4, out_features=8, bias=True)
(1): ReLU()
(2): Linear(in_features=8, out_features=4, bias=True)
(3): ReLU()
)
(block 1): Sequential(
(0): Linear(in_features=4, out_features=8, bias=True)
(1): ReLU()
(2): Linear(in_features=8, out_features=4, bias=True)
(3): ReLU()
)
(block 2): Sequential(
(0): Linear(in_features=4, out_features=8, bias=True)
(1): ReLU()
(2): Linear(in_features=8, out_features=4, bias=True)
(3): ReLU()
)
(block 3): Sequential(
(0): Linear(in_features=4, out_features=8, bias=True)
(1): ReLU()
(2): Linear(in_features=8, out_features=4, bias=True)
(3): ReLU()
)
)
(1): Linear(in_features=4, out_features=1, bias=True)
)
'''
5、可以进行内置初始化操作,初始化为正态分布,某一个常数或者0都行,在函数后面net.apply这个函数就可以应用
def init_normal(m):
if type(m) == nn.Linear:
# 下划线为原地操作
nn.init.normal_(m.weight,mean=0,std=0.01)
nn.init.zeros_(m.bias)
def init_constant(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight,1)
nn.init.zeros_(m.bias)
net.apply(init_normal)
net.apply(init_constant)
6、并且还可以进行自定义初始化内容
def my_init(m):
if type(m) == nn.Linear:
print("Init", *[(name, param.shape)
for name, param in m.named_parameters()][0])
nn.init.uniform_(m.weight, -10, 10)
m.weight.data *= m.weight.data.abs() >= 5
net.apply(my_init)
7、参数绑定,我们希望在多个层间共享参数,然后使用它的参数来设置另一个层的参数。
# 我们需要给共享层一个名称,以便可以引用它的参数
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])
'''
tensor([True, True, True, True, True, True, True, True])
tensor([True, True, True, True, True, True, True, True])
'''
自定义层
1、自定义不带参数的层
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]))
'''
tensor([-2., -1., 0., 1., 2.])
'''
2、自定义带参数的层
class MyLinear(nn.Module):
def __init__(self, in_units, units):
super().__init__()
self.weight = nn.Parameter(torch.randn(in_units, units))
self.bias = nn.Parameter(torch.randn(units,))
def forward(self, X):
linear = torch.matmul(X, self.weight.data) + self.bias.data
return F.relu(linear)
linear = MyLinear(5, 3)
linear(torch.rand(2, 5)) # 执行前向传播计算
'''
tensor([[0.0000, 0.0000, 0.0000],
[0.6160, 0.0000, 0.0000]])
'''
3、自定义层当然也可以进行嵌套,只要设定的维度正确,就没问题。
读写文件
1、我们可以通过torch.save,torch.load来进行保存和读出文件
x = torch.arange(4)
torch.save(x,'x-file')
x2 = torch.load('x-file')
x2
我们可以进行存储张量,字典等等各种数据都没问题,也可以去保存我们的神经网络模型参数。
2、保存神经网络模型参数
# 这将保存模型的参数而不是保存整个模型
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') # 将模型的参数存储在一个叫做“mlp.params”的文件中
3、当我们读出文件时,使用torch.load后是一个字典的形式,我们可以再次利用load_state_dict()来应用参数
如下代码,实例化了原始多层感知机模型的一个备份,直接读取文件中存储的参数,得出结果与之前相同
clone = MLP()
clone.load_state_dict(torch.load('mlp.params'))
clone.eval()
'''
MLP(
(hidden): Linear(in_features=20, out_features=256, bias=True)
(output): Linear(in_features=256, out_features=10, bias=True)
)
'''
Y_clone = clone(X)
Y_clone == Y
'''
tensor([[True, True, True, True, True, True, True, True, True, True],
[True, True, True, True, True, True, True, True, True, True]])
'''
卷积神经网络的由来
卷积公式如下:
一个系统,输入是不稳定的,输出稳定,那么用卷积来求系统存量。
图像的卷积操作
如果用卷积公式代表的话,f函数是不稳定输入,而h函数是稳定的输出
图像与卷积核的操作是先相乘,再相加,图像是f函数,而卷积核是h函数
若卷积核为3*3,且每个都为1/9,那么图像经过卷积核为平滑卷积操作,如下图所示:
卷积核就是周围的像素点事如何对当前像素点产生影响的。
给g函数旋转180度后,为卷积核操作,做了翻转操作
以上就引出了卷积神经网络
卷积神经网络一般做图像识别,而CNN识别图像的第一步就是将图像的局部特征挑出,也就是对图像进行卷积操作,也可以对图片进行过滤,保存某些特征,这样叫做过滤层。
从全连接到卷积
当我们进行分类猫和狗这种分类任务时,使用MLP(多层感知机)是很困难的,参数很多,占用内存也很多。
在卷积中,我们有两个重要原则,第一个是平移不变性,第二个是局部性
怎样从全连接层出发,应用上面两个原则,得到卷积
其中hij为输出,wijkl为权重,xkl为输入,我们要考虑空间的信息,所以将输入和输出变成矩阵,它有宽度和高度这两个维度 ,从向量变成了矩阵。
原本权重为二维,输入输出为一维向量,从输入,输出分别选一个节点;现在权重变为四维,输入输出为二维矩阵,从输入,输出分别选一个包含宽,高的节点。
权重共四维,分别是输入通道、输出通道、卷积核长度、卷积核宽度。
i,j对应输出矩阵的位置,k,l对应输入矩阵的位置,将w权重进行重新索引,w的元素进行重新排列,组成了V向量,将w的下标变化得到了卷积,可以看出,x属于不移动的输入,而v属于移动的。
平移不变性
输入的信息x,位置在(i+a,j+b)处,输出信息h在(i,j)处,是通过参数v(i,j,a,b)进行连接。
我们希望i,j变的时候,不能让权重w跟着变,否则失去了平移不变性,不管移动到哪里,模式识别器不变,权重是特征提取器,不该随着位置而变化,这也就是二维卷积的交叉相关。
局部性
局部性其实就是卷积核的大小进行计算,每次只关注input图像中卷积核扫描的那一部分,而不是每次关注全部input图像,意思就是不应该看太远的部分,只需要看局部的,卷积核的部分就行,a和b在[-delta,delta]的范围内。
卷积其实也是一种特殊的全连接层,什么时候应该用卷积呢?当检测对象不因为所处位置而改变的时候,且一般具有局部特征时,可以使用卷积来进行计算。
卷积层
二维交叉相关,如上节所讲,通过输入与卷积核相乘在相加,得到输出。
卷积核也叫做kernel,也是权重,而卷积是特征提取器,每次只移动一个单位。
二维卷积层,我们通过输入X的维度与卷积核的维度,可以计算出输出的维度,如下图所示,并且输出利用二维交叉计算得出,卷积核和偏差b都是可以学习的参数。
举一个例子,一张图片,经过不同的卷积核会有不同的效果
交叉相关和卷积是有对称性的,在实际当中使用没有区别。
卷积有负号,卷积索引w时,是反过来的,是有翻转180度的关系。
我们可以进行比较一下一维和三维交叉相关,
一维可以进行文本,语言,时序序列
三维可以进行视频,医学图像,气象地图
我们可以看到,都是一个函数不动,另一个函数平移,然后对于位置相乘再相加,其中x是不动的输入,w是移动的权重,也是卷积核kernel。
总结
图像卷积代码实现
1、引入相关模块
import torch
from torch import nn
from d2l import torch as d2l
2、互相关运算,通过输入X,卷积核K,根据公式相乘再相加的公式计算输出Y
def corr2d(X,K): # X输入,K为核矩阵Kernel
# 二维互相关运算
h,w = K.shape # 行,列
Y = torch.zeros((X.shape[0]-h+1,X.shape[1]-w+1)) # 输出Y的形状
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
3、验证上述二维互相关运算的输出,最后算出结果正确
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)
'''
tensor([[19., 25.],
[37., 43.]])
'''
4、实现二维卷积层
class Conv2D(nn.Module):
def __init__(self,kernel_size):
super().__init__()
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
5、我们可以实现一个卷积层的简单应用,检测图片中不同颜色的边缘
# 定义输入X
X = torch.ones((6,8))
X[:,2:6]=0 # 中间4列为0
X
'''
tensor([[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.]])
'''
# 定义卷积核K,如果左右原值相等,那么输出会为0,则不是边缘,边缘则不为0
K = torch.tensor([[1.0,-1.0]])
Y = corr2d(X,K)
Y
'''
tensor([[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.]])
'''
# 我们发现它可以检测垂直边缘
# 如果把X转置作为输入,它是否能检测水平边缘呢?
corr2d(X.T,K)
'''
tensor([[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.]])
'''
# 我们发现X的转置检测不出来,所以这个K卷积核只能检测垂直边缘
6、上述我们讨论了当X的边缘为垂直时,使用卷积核K=torch.tensor([[1.0,-1.0]])时,通过互相关运算得到输出Y,可以检测垂直边缘;但是当X的边缘为水平时,检测不出垂直边缘
# 当卷积核 K = torch.tensor([[1.0],[-1.0]]) 时,可以检测水平边缘
K = torch.tensor([[1.0],[-1.0]])
Y = corr2d(X.T,K)
Y
'''
tensor([[ 0., 0., 0., 0., 0., 0.],
[ 1., 1., 1., 1., 1., 1.],
[ 0., 0., 0., 0., 0., 0.],
[ 0., 0., 0., 0., 0., 0.],
[ 0., 0., 0., 0., 0., 0.],
[-1., -1., -1., -1., -1., -1.],
[ 0., 0., 0., 0., 0., 0.]])
'''
7、给定输出X和输出Y,学习得到卷积核
conv2d = nn.Conv2d(1,1,kernel_size=(1,2),bias=False) # 输入通道为1,输出通道为1,卷积核1*2,黑白通道1,彩色通道3
X = X.reshape((1,1,6,8)) # 通道,批量
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)))
'''
batch 2,loss 10.773
batch 4,loss 1.828
batch 6,loss 0.315
batch 8,loss 0.056
batch 10,loss 0.011
tensor([[ 0.9779, -0.9881]])
'''
我们发现最后学习得到的卷积核与我们当初设立的K=torch.tensor([[1.0,-1.0]]很接近。
填充和步幅
填充
给定32*32的输入图像,卷积核为5*5,根据公式输出Y的形状为(nh-kn+1)*(nw-kw+1),所以第一层得到的输出为(32-5+1) = 28,并且每经过一层,就会舍弃4个输出,所以第7层的输出是(32-4*7) = 4,故为4*4,更大的卷积核可以更快的减小输出大小。
填充就是在周围添加行和列,如下图,在行和列各添加了两列,经过卷积核,得到的输出比没有添加额外的行/列更大一些,而填充的数字都为0,填充之后再做卷积会发现输出会由2*2变成4*4,也就是说输出比输入还要大 。
- 填充padding可以用p所表示,填充的行数和列出,经过卷积得到的输出形状的公式如下图所示
- 不过我们一般取填充的行数为卷积核的行数-1,填充的列数位卷积核的列数-1,因为这样根据上面的公式会将kh,ph,1全部抵消,输出的形状与输入的形状一致。
步幅
假设输入比较大的话需要进行大量的计算,可以可以去增加步幅,在原本的情况下步幅默认为1。
- 应该是需要55层才能将输出降低到4*4
- 因为卷积核大小为5*5,所以经过一层减少4个输出,(224-4*55=4,故要经过55层。
步幅指的滑动窗口在行/列上移动的步长,也就是往右走多少列,往下走多少行
- 我们了解到padding也就是填充,它会使输出形状增大,而我们这一节的步幅可以使输出形状减小。
- 卷积的目的就是提取信息,减小信息量,通过步幅可以去提取主要的特征。
- 也就是丢失信息和节省资源时间,这两个特性做取舍。
如下图,我们可以通过输入的大小,卷积核的大小,填充的大小,步幅的大小来计算输出形状,具体公式如下:
总结
综上,我们可以去总结通过输入大小,卷积核大小,填充大小,步幅大小来计算输出大小的简洁公式:
输出大小 = [(输入大小-卷积核大小+2*填充)/步幅]向下取整 + 1
填充和步幅的代码实现
我们需要注意到是,上一节公式中的ph和pw是指在行或者列一共填充了多少,而我们代码中的padding是指在一个方向是填充了多少,另一个方向相同。
1、定义函数,先将输入X从二维转为四维,经过卷积运算,得到输出Y,将四维度的Y取后两维,得到二维矩阵
import torch
from torch import nn
def comp_conv2d(conv2d,X):
X = X.reshape((1,1) + X.shape) # 是指将X重塑为4维变量,默认批量大小为1,通道数为1
Y = conv2d(X)
return Y.reshape(Y.shape[2:]) # 取Y的后两个维度,变成二维矩阵
2、侧边分别填充一个像素
conv2d = nn.Conv2d(1,1,kernel_size=3,padding=1) # padding为1,相当于之前的公式中的ph和pw为2,-3+2+1=0,相当于不改变输入形状
X = torch.rand(size=(8,8))
print(comp_conv2d(conv2d,X).shape)
'''
torch.Size([8, 8])
'''
3、卷积核大小为5*3,上下分别填充两行,左右分别填充1列
根据公式 输出大小 = [(输入大小-卷积核大小+2*填充)/步幅]向下取整 + 1 得到 (8-5+4)/1 +1 = 8,(8-3+2)/1+1=8
conv2d = nn.Conv2d(1,1,kernel_size=(5,3),padding=(2,1)) # (8-5+4)/1 +1 = 8,(8-3+2)/1+1=8
comp_conv2d(conv2d,X).shape
4、上下左右分别填充1列,步幅为2
conv2d = nn.Conv2d(1,1,kernel_size=3,padding=1,stride=2) # (8-3+2)/2+1=4
comp_conv2d(conv2d,X).shape
5、卷积核大小为3*5,上下填充0行,左右分别填充1行,在行上步幅为3像素,在列上步幅为4像素
conv2d = nn.Conv2d(1,1,kernel_size=(3,5),padding=(0,1),stride=(3,4)) # (8-3+0)/3+1=2,(8-5+2)/4+1=2
comp_conv2d(conv2d,X).shape
卷积层里的多输入输出通道
我们了解到的Mnist数据集是灰度图片,所以只有一个通道,一张彩色的图片是由红,绿,蓝三种颜色组成,因此通道数为3,如下图:
如下图,是有多个输入通道,每一个通道都有一个卷积核,因此输入的通道数与卷积核的通道数肯定是一样的,下图中输入和卷积核的通道数为2,每个通道进行相乘再相加的操作,最后再次相加,得到了一个输出,这也是相当于特征融合,最终只有一个输出。
如下图,ci代表输入通道的层数,当然也是卷积核的通道数目,最后相乘再相加,再累加得到一个输出的公式如下:
注:卷积核的大小是根据习惯设定的,一般会设定为3*3这样的奇数维数,但是卷积核的参数是先随机设定,之后慢慢学出来的。
- 多个输出通道,我觉得可以这样去理解,之前是通道数为3,卷积核的通道也为3,最后得到一个输出,但是这样的话,如果我们想同时进行垂直边缘检测和水平边缘检测的话,就要进行两次操作了。
- 所以,我们可以设定多个卷积核(与通道数不同),为了方便理解,可以相当于多组卷积核,如下图,属于输入通道,卷积核通道为3,卷积核个数为2,所以最后的输出通道数也是2,即卷积核的个数与输出通道个数相同。
- 下图展示了多个输出通道的数学公式的体现,其中ci为输入通道的个数,也是卷积核的通道数目,co为输出通道的个数,也是卷积核的个数。卷积核也是权重可以想象成一个四维的存在。
- 并且每一核生成一个输出通道。
- 有ci个通道,每个通道有co种卷积核,所以共有ci*co种卷积核。
- ci与co并没有任何相关性。
- 多个输入和输出通道,如下图,我们要进行识别一只猫,每个输出通道可以认为是在识别某一个特定的模式(特征),通过学习不同卷积核的参数来匹配某一个特定的模式,下图中的输出通道为6,每一个输出通道代表着识别某个特征。
- 下面的一些层的不同通道识别不同的局部特征信息,越往上,上层会将局部的特征组合起来,变成了更为整体的特征,最后组合起来形成了识别的类别。
- 相当于特征提取再进行组合,一般池化层负责信息的融合。
- 1*1的卷积层,是卷积核的高和宽都等于1 ,意味着它不会识别空间信息,只能看一个像素,它只是融合通道。
- 输出的值是将对应的输入位置上的不同通道上的值做加权和,如下图输入通道为3,宽,高为3*3,卷积核通道为3,它输出通道数为2,所以卷积核个数为2,进行加权和操作,对多个通道的相同位置的像素进行融合。
- 相当于输入形状为nh*nw*ci,将输入拉成一个向量,权重为co*ci的全连接层。
我们最通用的情况下的是二维卷积层,输入X为通道为ci,高和宽分别为nh,nw;卷积核W为个数co,通道为ci,高和宽分别为kh和kw;偏差B为co个卷积核,每个核有ci个偏差;最后输出Y为输出通道为co,高和宽分别为mh和mw。
总结
- 输入通道数不是卷积层的超参数,它是前一层的超参数。
- 每个输出通道有独立的三维卷积核,所以最后的卷积核是一个4维的张量
多输入多输出通道的代码实现
1、实现多输入通道互相关运算
import torch
from torch import nn
from d2l import torch as d2l
def corr2d_multi_in(X,K): # X,K都为3维
return sum(d2l.corr2d(x,k) for x,k in zip(X,K)) #x,k每轮都为1个二维矩阵进行互相关运算,最后相加得到一个输出
2、验证互相关运算的输出
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]]])
corr2d_multi_in(X,K)
'''
tensor([[ 56., 72.],
[104., 120.]])
'''
3、多输出通道运算
- 其中每个K都是一个组成的4维向量,每个小k都是循环得到的3维向量,与X进行多输入的互相关运算,最后通过torch.stack()在一个新维度上进行连接。
- K为四维向量,k为三维向量,与X进行多输入的互相关运算,得到输出为二维向量,进行stack操作,最后为三维向量。
def corr2d_multi_in_out(X,K): # X为3通道,K为4通道
# 从4D的K拿出一个3D的k进行上一步操作
# 大K中的每个小k是一个3D的Tensor,0表示stack堆叠函数在0这个维度堆叠
return torch.stack([corr2d_multi_in(X,k) for k in K],0) # torch.stack()沿着一个新维度对输入张量序列进行连接
4、组成四维卷积核
- 在这一轮可以看出K原本为一个(2,2,2)的向量,经过堆叠操作,为(3,2,2,2)的向量,即3个卷积核,每一个卷积核都有2个通道,每个通道为2*2的矩阵。
- 所以每个小k都是(2,2,2)的,X是(2,3,3)的,与X进行多输入互相关操作,得到的输出为(2,2)的矩阵,再次经过堆叠操作,最后输出为(3,2,2)的向量。
K = torch.stack((K,K+1,K+2),0)
K.shape
'''
torch.Size([3, 2, 2, 2])
'''
K
'''
tensor([[[[0., 1.],
[2., 3.]],
[[1., 2.],
[3., 4.]]],
[[[1., 2.],
[3., 4.]],
[[2., 3.],
[4., 5.]]],
[[[2., 3.],
[4., 5.]],
[[3., 4.],
[5., 6.]]]])
'''
result = corr2d_multi_in_out(X,K)
result.shape
'''
torch.Size([3, 2, 2])
'''
result
'''
tensor([[[ 56., 72.],
[104., 120.]],
[[ 76., 100.],
[148., 172.]],
[[ 96., 128.],
[192., 224.]]])
'''
5、实现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)) # (3,3,3)
K = torch.normal(0,1,(2,3,1,1))
corr2d_multi_in_out_1x1(X,K)
个人总结
这周进行学习了正则化的一些方法来避免过拟合,如何提高数值稳定性的方法来防止梯度爆炸或者梯度消失,以及神经网络的基础和卷积操作,下周将继续学习池化层以及其他的算法,并且阅读相应的文献,理论与实践相结合。