Task05:动手学深度学习——卷积神经网络

(学习笔记,待补充)
本文目录如下:

  • 1.卷积神经网络基础
    • 1.1 二维互相关运算
    • 1.2 二维卷积层
    • 1.3 填充和步幅
    • 1.4 多通道输入和输出
    • 1.5 池化层
    1. 几个经典的卷积神经网络
    • 2.1 LeNet
    • 2.2 AlexNet
    • 2.3 VGG-16
    • 2.4 NiN
    • 2.5 GoogleNet

1.卷积神经网络基础

1.1 二维互相关运算

def corr2d(X, K):
    H, W = X.shape
    h, w = K.shape
    Y = torch.zeros(H - h + 1, W - w + 1)
    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是指输入,K是指核。

X = torch.tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
K = torch.tensor([[0, 1], [2, 3]])
print(X, '\n')
print(K, '\n')
Y = corr2d(X, K)
print(Y)

结果为:


二维互相关运算结果

1.2 二维卷积层

二维卷积层将输入和卷积核做互相关运算,并加上bias

class Conv2D(nn.Module):
   def __init__(self, kernel_size):
       super(Conv2D, self).__init__()
       self.weight = nn.Parameter(torch.randn(kernel_size))
       self.bias = nn.Parameter(torch.randn(1))
   def forward(self, x):
       return corr2d(x, self.weight) + self.bias
# 通常,我们如果想在模型中维护一些可学习的参数,就会将它们定义为nn.Parameter.
# 首先,Parameter本身是tensor的子类,所以会自动地给参数附上梯度;其次,nn.Module的一个子类,会维护参数的一个集合,用nn.Parameter会自动将参数注册进去。

下面用一个边缘检测的例子。
我们构造一张6 \times 8的图像,中间4列为黑(0),其余为白(1),希望检测到颜色边缘。我们的标签是一个6 \times 7的二维数组,第2列是1(从1到0的边缘),第6列是-1(从0到1的边缘)。

X = torch.ones(6, 8)
Y = torch.zeros(6, 7)
X[:, 2: 6] = 0
Y[:, 1] = 1
Y[:, 5] = -1
print(X)
print(Y)

输入和输出

我们希望学习一个卷积层,通过卷积层来检测颜色边缘。
为什么是呢?
因为在我们构造的这个例子里,我们其实关注的是同一行相邻两个元素的变化,所以我们每次关注的是输入当中一个一行二列的区域。

conv2d = Conv2D(kernel_size=(1,2))
step = 30
lr = 0.01
for i in range(step):
    Y_hat = conv2d(X)
    l = ((Y_hat - Y) ** 2).sum()
    l.backward()
    # 梯度下降
    conv2d.weight.data -= lr * conv2d.weight.grad
    conv2d.bias.data -= lr * conv2d.bias.grad
    
    # 梯度清零
    conv2d.weight.grad.zero_()
    conv2d.bias.grad.zero_()
    # 每隔5个训练步输出一下
    if (i + 1) % 5 == 0:
        print('Step %d, loss %.3f' % (i + 1, l.item()))
        
print(conv2d.weight.data)
print(conv2d.bias.data)
    
image.png
image.png

1.3 填充和步幅

填充
上一节我们知道,如果我们用n{\times}n的输入,f{\times}f的核做卷积,那么输出则应该是(n-f+1){\times}(n-f+1),当神经网络的层数越来越多时,这会带来两个问题:
第一,做了几次卷积之后,我们的图像就会变得很小。第二,我们会损失掉一些边缘信息。
为了解决这个问题,我们可以在卷积操作之前填充这幅图像。
填充(padding)是指在输入高和宽的两侧填充元素,通常填充的是0元素。

padding

如果原输入的高和宽是和,卷积核的高和宽是和,在高的两侧一共填充行,在宽的两侧一共填充列,则输出形状为:

(n_h+p_h-k_h+1)\times(n_w+p_w-k_w+1)

我们在卷积神经网络中使用奇数高宽的核,比如3 \times 35 \times 5的卷积核,对于高度(或宽度)为大小为2 k + 1的核,令步幅为1,在高(或宽)两侧选择大小为k的填充,便可保持输入与输出尺寸相同。
步幅
在互相关运算当中,每次滑动的行数1就是步幅(stride)。
一般来说,当高上步幅为s_h,宽上步幅为s_w时,输出形状为:

\lfloor(n_h+p_h-k_h+s_h)/s_h\rfloor \times \lfloor(n_w+p_w-k_w+s_w)/s_w\rfloor

如果p_h=k_h-1p_w=k_w-1,那么输出形状将简化为\lfloor(n_h+s_h-1)/s_h\rfloor \times \lfloor(n_w+s_w-1)/s_w\rfloor。更进一步,如果输入的高和宽能分别被高和宽上的步幅整除,那么输出形状将是(n_h / s_h) \times (n_w/s_w)

p_h = p_w = p时,我们称填充为p;当s_h = s_w = s时,我们称步幅为s

1.4 多通道输入和输出

多通道输入

之前的输入和输出都是二维数组,但真实数据的维度经常更高。例如,彩色图像在高和宽2个维度外还有RGB(红、绿、蓝)3个颜色通道。假设彩色图像的高和宽分别是hw(像素),那么它可以表示为一个3 \times h \times w的多维数组,我们将大小为3的这一维称为通道(channel)维。

多通道输入

多通道输出

卷积层的输出也可以包含多个通道,设卷积核输入通道数和输出通道数分别为c_ic_o,高和宽分别为k_hk_w。如果希望得到含多个通道的输出,我们可以为每个输出通道分别创建形状为c_i\times k_h\times k_w的核数组,将它们在输出通道维上连结,卷积核的形状即c_o\times c_i\times k_h\times k_w

对于输出通道的卷积核,我们提供这样一种理解,一个c_i \times k_h \times k_w的核数组可以提取某种局部特征,但是输入可能具有相当丰富的特征,我们需要有多个这样的c_i \times k_h \times k_w的核数组,不同的核数组提取的是不同的特征。

1.5 池化层(pooling layers)

除了卷积层,卷积神经网络通常也使用池化层来缩减模型大小,提高计算速度。
(1)怎么缩减规模呢?
以最大池化层为例,每个区域只保留最大值,则模型规模减小了。

最大池化层

(2)池化层输出的大小计算与卷积层一样

其中,p为padding,s为stride
(3)值得注意的是,与卷积层不同,池化层的每个通道是分别计算的,输入个通道,就输出个通道,而卷积层则是输出1个通道(除非多个过滤器/核)。
(4)通常使用最大池化层,而平均池化层很少用。
(5)池化过程中没有需要学习的参数。
(6)最大池化层很少用padding。

2. 几个经典的卷积神经网络

2.1 LeNet

LeNet的结构就是:
卷积-池化-卷积-池化-卷积-池化.......卷积-池化-sigmoid函数


LeNet
#net
class Flatten(torch.nn.Module):  #展平操作
    def forward(self, x):
        return x.view(x.shape[0], -1)

class Reshape(torch.nn.Module): #将图像大小重定型
    def forward(self, x):
        return x.view(-1,1,28,28)      #(B x C x H x W)
    
net = torch.nn.Sequential(     #Lelet                                                  
    Reshape(),
    nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5, padding=2), #b*1*28*28  =>b*6*28*28
    nn.Sigmoid(),                                                       
    nn.AvgPool2d(kernel_size=2, stride=2),                              #b*6*28*28  =>b*6*14*14
    nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5),           #b*6*14*14  =>b*16*10*10
    nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),                              #b*16*10*10  => b*16*5*5
    Flatten(),                                                          #b*16*5*5   => b*400
    nn.Linear(in_features=16*5*5, out_features=120),
    nn.Sigmoid(),
    nn.Linear(120, 84),
    nn.Sigmoid(),
    nn.Linear(84, 10)
)

接下来构造一个高和宽均为28的单通道数据样本,并逐层进行向前计算查看每个层的输出情况

X = torch.randn(size=(1, 1, 28, 28), dtype=torch.float32)
for layer in net:
    X = layer(X)
    print(layer.__class__.__name__,'output shape: \t',X.shape)

输出结果为:


image.png

#计算准确率
'''
(1). net.train()
  启用 BatchNormalization 和 Dropout,将BatchNormalization和Dropout置为True
(2). net.eval()
不启用 BatchNormalization 和 Dropout,将BatchNormalization和Dropout置为False
'''

def evaluate_accuracy(data_iter, net,device=torch.device('cpu')):
    """Evaluate accuracy of a model on the given data set."""
    acc_sum,n = torch.tensor([0],dtype=torch.float32,device=device),0
    for X,y in data_iter:
        # If device is the GPU, copy the data to the GPU.
        X,y = X.to(device),y.to(device)
        net.eval()
        with torch.no_grad():
            y = y.long()
            acc_sum += torch.sum((torch.argmax(net(X), dim=1) == y))  #[[0.2 ,0.4 ,0.5 ,0.6 ,0.8] ,[ 0.1,0.2 ,0.4 ,0.3 ,0.1]] => [ 4 , 2 ]
            n += y.shape[0]
    return acc_sum.item()/n
#训练函数
def train_ch5(net, train_iter, test_iter,criterion, num_epochs, batch_size, device,lr=None):
    """Train and evaluate a model with CPU or GPU."""
    print('training on', device)
    net.to(device)
    optimizer = optim.SGD(net.parameters(), lr=lr)
    for epoch in range(num_epochs):
        train_l_sum = torch.tensor([0.0],dtype=torch.float32,device=device)
        train_acc_sum = torch.tensor([0.0],dtype=torch.float32,device=device)
        n, start = 0, time.time()
        for X, y in train_iter:
            net.train()
            
            optimizer.zero_grad()
            X,y = X.to(device),y.to(device) 
            y_hat = net(X)
            loss = criterion(y_hat, y)
            loss.backward()
            optimizer.step()
            
            with torch.no_grad():
                y = y.long()
                train_l_sum += loss.float()
                train_acc_sum += (torch.sum((torch.argmax(y_hat, dim=1) == y))).float()
                n += y.shape[0]
        test_acc = evaluate_accuracy(test_iter, net,device)
        print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f, '
              'time %.1f sec'
              % (epoch + 1, train_l_sum/n, train_acc_sum/n, test_acc,
                 time.time() - start))
# 训练
lr, num_epochs = 0.9, 10

def init_weights(m):
    if type(m) == nn.Linear or type(m) == nn.Conv2d:
        torch.nn.init.xavier_uniform_(m.weight)

net.apply(init_weights)
net = net.to(device)

criterion = nn.CrossEntropyLoss()   #交叉熵描述了两个概率分布之间的距离,交叉熵越小说明两者之间越接近
train_ch5(net, train_iter, test_iter, criterion,num_epochs, batch_size,device, lr)
# test
for testdata,testlabe in test_iter:
    testdata,testlabe = testdata.to(device),testlabe.to(device)
    break
print(testdata.shape,testlabe.shape)
net.eval()
y_pre = net(testdata)
print(torch.argmax(y_pre,dim=1)[:10])
print(testlabe[:10])

2.2 AlexNet

AlexNet的结构其实和LeNet非常类似。二者的区别在于:
(1)层数:LeNet经过了5层变换,而AlexNet则有8层变换,其中有5层卷积层和2层全连接隐藏层,和1层全连接输出层。其对比如下图所示。
(2)LeNet使用的是sigmoid激活函数,而AlexNet使用的是ReLU激活函数。两个激活函数的区别在前面多层感知机的时候提到了,简单来说sigmoid函数训练模型更慢,也容易出现梯度消失。
(3)LeNet使用的是平均池化层,而AlexNet使用的是最大池化层,说明后者抓住的是最重要的特征,在模型的训练过程中会有参数稀疏的作用。
(4)AlexNet的通道数是LeNet通道数的数十倍,代表更多的特征。
(5)AlexNet用dropout来控制模型的复杂度,使得模型的泛化能力更强。
(6)最早的LeNet和AlexNet使用的数据集不同,前者使用的是MNIST,后者使用的是IMAGENET。


LeNet和AlexNet对比
  1. MNIST

    深度学习领域的“Hello World!”,入门必备!MNIST是一个手写数字数据库,它有60000个训练样本集和10000个测试样本集,每个样本图像的宽高为28*28。此数据集是以二进制存储的,不能直接以图像格式查看,不过很容易找到将其转换成图像格式的工具。

    最早的深度卷积网络LeNet便是针对此数据集的,当前主流深度学习框架几乎无一例外将MNIST数据集的处理作为介绍及入门第一教程,其中Tensorflow关于MNIST的教程非常详细。数据集下载~12MB
    2. ImageNet

    ImageNet数据集有1400多万幅图片,涵盖2万多个类别。其中有超过百万的图片有明确的类别标注和图像中物体位置的标注,相关信息如下:

    1)非空的同义词集总数:21841
    2)图像总数:14,197,122
    3)边界框注释的图像数:1,034,908
    4)具有SIFT特征的同义词集数:1000
    5)具有SIFT特征的图像数:120万

    Imagenet数据集是目前深度学习图像领域应用得非常多的一个领域,关于图像分类、定位、检测等研究工作大多基于此数据集展开。Imagenet数据集文档详细,有专门的团队维护,使用非常方便,在计算机视觉领域研究论文中应用非常广,几乎成为了目前深度学习图像领域算法性能检验的“标准”数据集。

AlexNet的代码与LeNet的区别不大,只不过在中间网络的部分,即net()函数的书写要按照AlexNet的结构去写。

#目前GPU算力资源预计17日上线,在此之前本代码只能使用CPU运行。
#考虑到本代码中的模型过大,CPU训练较慢,
#我们还将代码上传了一份到 https://www.kaggle.com/boyuai/boyu-d2l-modernconvolutionalnetwork
#如希望提前使用gpu运行请至kaggle。


import time
import torch
from torch import nn, optim
import torchvision
import numpy as np
import sys
sys.path.append("/home/kesci/input/") 
import d2lzh1981 as d2l
import os
import torch.nn.functional as F

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

class AlexNet(nn.Module):
    def __init__(self):
        super(AlexNet, self).__init__()
        self.conv = nn.Sequential(
            nn.Conv2d(1, 96, 11, 4), # in_channels, out_channels, kernel_size, stride, padding
            nn.ReLU(),
            nn.MaxPool2d(3, 2), # kernel_size, stride
            # 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
            nn.Conv2d(96, 256, 5, 1, 2),
            nn.ReLU(),
            nn.MaxPool2d(3, 2),
            # 连续3个卷积层,且使用更小的卷积窗口。除了最后的卷积层外,进一步增大了输出通道数。
            # 前两个卷积层后不使用池化层来减小输入的高和宽
            nn.Conv2d(256, 384, 3, 1, 1),
            nn.ReLU(),
            nn.Conv2d(384, 384, 3, 1, 1),
            nn.ReLU(),
            nn.Conv2d(384, 256, 3, 1, 1),
            nn.ReLU(),
            nn.MaxPool2d(3, 2)
        )
         # 这里全连接层的输出个数比LeNet中的大数倍。使用丢弃层来缓解过拟合
        self.fc = nn.Sequential(
            nn.Linear(256*5*5, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),
            #由于使用CPU镜像,精简网络,若为GPU镜像可添加该层
            #nn.Linear(4096, 4096),
            #nn.ReLU(),
            #nn.Dropout(0.5),

            # 输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
            nn.Linear(4096, 10),
        )
    # 这一步的目的是将batch_size*channels*height*width转化为batch_size*hiddens然后传入全连接层
    def forward(self, img):

        feature = self.conv(img)
        output = self.fc(feature.view(img.shape[0], -1))
        return output

在执行的过程中,可以验证自己是否写错了:

net = AlexNet()
print(net)

2.3 VGG-16

VGG最大的特点是可以通过重复使用简单的基础块来构建深层模型。因此参数较多。
每个VGG块的结构为多个卷积层加上一个池化层。

VGG-16,图片有误,VGG块最后一层应该为2x2的最大池化层,而不是3x3

2.4 NiN

(1)前面将的三种网络,其结构基本上都是类似的,大致都是由卷积层和全连接层两个大块组成。而NiN(网络中的网络)则是串联了多个由卷积层和“全连接层”构成的小网络来构建深层网络的。
为什么这里打引号呢?
因为我们知道卷积层的输出应该是:样本数{\times}通道数{\times}{\times}宽,而全连接层的输入则应该是:样本数{\times}神经元个数,二者的转换需要有一个展平的操作,不但不方便且影响了这个结构,因此这里使用了一个1x1的卷积层来代替全连接层,不需要进行展平操作了。
(2)前三者通过全连接层来调整输出,使得输出等于类别数,而NiN则是通过调整通道数来控制输出的类别数。
(3)补充知识点:1\times1的卷积核的作用

  • a. 代替全连接层
  • b. 通过控制卷积核数量来达到通道数的放缩。
  • c. 增加非线性
  • d. 卷积层和全连接层相比,计算参数少
NiN

2.5 GoogleNet

GoogleNet吸收了NiN串联网络的思想

    1. 由Inception基础块组成
    1. 每个基础块都有4条线路并行
    1. 可以自定义的超参数是每一层的输出 通道数,用于控制模型的复杂度。


      GoogleNet

class Inception(nn.Module):
    # c1 - c4为每条线路里的层的输出通道数
    def __init__(self, in_c, c1, c2, c3, c4):
        super(Inception, self).__init__()
        # 线路1,单1 x 1卷积层
        self.p1_1 = nn.Conv2d(in_c, c1, kernel_size=1)
        # 线路2,1 x 1卷积层后接3 x 3卷积层
        self.p2_1 = nn.Conv2d(in_c, c2[0], kernel_size=1)
        self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
        # 线路3,1 x 1卷积层后接5 x 5卷积层
        self.p3_1 = nn.Conv2d(in_c, c3[0], kernel_size=1)
        self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
        # 线路4,3 x 3最大池化层后接1 x 1卷积层
        self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
        self.p4_2 = nn.Conv2d(in_c, 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)  # 在通道维上连结输出
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,348评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,122评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,936评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,427评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,467评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,785评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,931评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,696评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,141评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,483评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,625评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,291评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,892评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,492评论 2 348