机器学习的入门笔记(第九周)

本周观看了李沐老师的《动手学深度学习》,李沐老师很注重代码的编写,但是对概念的讲解并不是特别多,所以还要去结合一些其他的资料去进行了解,下面是本周的所看的课程总结。

丢弃法(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)

个人总结

这周进行学习了正则化的一些方法来避免过拟合,如何提高数值稳定性的方法来防止梯度爆炸或者梯度消失,以及神经网络的基础和卷积操作,下周将继续学习池化层以及其他的算法,并且阅读相应的文献,理论与实践相结合。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值