Skip to content

记录在李沐老师《动手学习深度学习》学习到的有趣的知识,在此认真记录一下

Notifications You must be signed in to change notification settings

ZhangEnsure/pytorch-d2l-zh

Repository files navigation

动手学习深度学习

在 Bilibili 学习李沐老师的课程过程中,收获了不少的代码实战经验。吴恩达老师的课程更多偏向于知识的讲解而缺少实践,虽然有代码课,但是更多的是对知识的理解。李沐老师的课程不仅讲解基础知识,而且讲解实战代码。沐老师的一些代码精简干练、复用性高,我希望不仅可以熟练应用和复用沐老师的代码,而且能够深刻理解代码的设计和实现,以便在学习深度学习的过程中也可以提升自己的 python、pytorch 编程能力。

在本仓库中,主要存放沐老师的一些 d2l 库 torch 模块的代码以及对该代码的注释分析、自我实现等等,以便自己以后的复习和调用。对课程内容的学习记录在 CSDN 上,在此不做过多分析和介绍。

学习内容提纲

线性回归

在线性回归部分,我们人工构造了一个带有噪声的线性数据集,并尝试使用手工和 pytorch 框架两种方式实现线性回归,并设计了生成数据集函数 synthetic_data()、按批量获取数据函数 data_iter() load_array()、向前传播 net() 函数、损失函数 squared_loss() 函数、优化函数 SGD() 等等。

在手工实现线性回归代码中,我曾经踩坑 SGD 的手动实现,并借此机会认真学习了一下 python 的参数传递规则。总结下来就是可变类型变量(例如列表)作为形参传入,在函数中如何修改该变量的方式,决定了是否创建一个新的变量(分配内容地址),此时新变量的任何修改均不影响原实参,如果未分配内存创建新的变量,则原实参的值也随之变化,具体请参考 test_object_pass() 函数。此外,还有损失函数需要传递 batch_size 批量大小等等。

softmax线性回归

softmax线性回归这里,我们处理的数据是来自 Fashion-MNIST 数据集,我们需要学习该数据集的读取方式、可视化函数 show_images() 的实现。

手动实现部分,为了可以和框架实现部分实现训练代码重用,我们需要从一开始便对手动实现部分的代码进行规范化。例如:

  1. 向前传播的 net 函数
  2. 损失函数 loss
  3. 优化器函数 trainer

其次,我们需要定义 softmax 函数,实现我们对 y_hat 输出值的规范化。Accumulator 类是应用于计算训练集和测试集在模型 net 上的 acc 精确性。train_epoch_ch3 函数是实现一次 epoch 的计算问题,特别地,我们要分别对手动和框架实现进行不同处理。

train_epoch_ch3 中,最主要的是 loss 计算后的形式我们要清楚。手动实现的 loss 是一个向量,我们需要 sum() 求和后进行自动梯度求导计算,在 updater 中还需要求 loss 的平均值后进行参数更新。在框架实现中,我们使用参数 reduction='none',这样的话,得到的损失就是一个向量,随后我们将损失求和取平均后进行自动求导。不过需要注意的是,pytorch 的变量梯度是累加的计算,我们在计算新的梯度前需要把上次计算的梯度清零。

除此之外,在分类问题,我们常常使用的是交叉熵 CrossEntropyLoss 损失函数,对这个函数的介绍,我在 CSDN 中写了详细的文章供参考,包括手动实现 CrossEntropyLoss 等等。

多层感知机 multilayers perceptrons

在softmax regression 中,我们的模型只有输入层和输出层。以 Fashion-MNIST 数据集为例,我们将一个 28*28 的图像展平为 784 的图片向量,并且一次读取一个 batch_size 批量的样本,但是这次我们加入了一个隐藏层叠加在输入层和输出层之间,并且使用非线性激活函数作为隐藏层的激活函数。总结来说,多层感知机也就是使用隐藏层和激活函数来得到非线性模型,在代码部分其实没有什么新知识。

深度学习计算

层和块

块可以描述单个层、多个层组成的组件或者整个模型本身。从编程的角度,块就是一个类。我们需要在类中定义相关参数和方法,例如一个将输入转换为输出的向前传播函数,一个存储参数的初始化方法。

请注意,我们自定义的块实现中 super().__init__() 是调用父类 Module 的构造函数执行必要的初始化操作。

自定义层可以先我们特定的行为,并且可以在网络设计中使用该层。带参数的层是可以进行参数更新的,不过定义的参数需要使用 nn.Parameter() 声明。

参数管理

访问参数,我们可以通过 net[i].weight 方法访问 Sequential 定义的网络中的某 i 层的权重数据,同样可以通过 for loop 遍历 net.named_parameters() 获取 name, param.shape 等等数据。

参数初始化,我们可以为模型中的某一层设置特定的初始化函数,这样的话需要使用 net[i].apply() 方法。

共享参数,也就是将模型中的某几个层绑定相同的参数,同步更新。

读写文件

在这里我们主要介绍了保存模型参数和读取模型参数的两个方法。需要注意的是,我们这里并没有保存模型的结构,而是后续重写模型的参数。

卷积神经网络 Convolutional Neural Network

之前的 mlp 十分适合处理表格类的数据,列是特征,行是样本。我们可以通过寻找到一个恰当的模式去拟合样本特征。但是对于高维的感知数据,CNN 可能是一个更好的选择。例如,对于一个灰度图 2D [k,l]样本,如果使用全连接层 mlp 处理的话,隐藏层的权重矩阵应该是 4D [i,j,k,l]。因为每一个神经元都要与输入的每一个特征连接,这便确定了 4D 的后两维维数。

但是直观上理解,mlp 是试图学习某个特征点处的权值,如果我们做分类问题的话,我们的目标点是否在特定坐标是无联系的,它可能出现在图片中的任意地点,也就是我们要保证 平移不变性,这就要求我们的卷积核是在不同坐标处应该是一致的。与此同时,我们的分类器应该在一定范围内聚合局部特征,具有 局部性,不应过大联系较远区域,这就限制了卷积核的大小。

在卷积层中,输入张量和核张量通过互相关运算,并在添加标量偏置后输出。这里想到在计算所研究生面试时导师问我的一个问题,1*1 卷积核卷积相当于什么?我当时啥也不会,就实话实说了。现在看来这个问题比较简单,就是相当于一个全连接层,它不识别空间模式,因为输入和输出的矩阵位置都是一一对应的,不涉及输入矩阵某像素点临近的像素。所幸,最后导师还是要我了。

填充和步幅

通常我们使用小卷积核,对于单个卷积,我们可能会丢失几个像素,一旦我们应用连续几个卷积层,我们可能会丢失很多的像素。同时再补充一下为什么“通常我们使用小卷积核”?小卷积核会不会使我们的感受野很小?其实不是的,在多层卷积层中计算的感受野是来自前一层卷积计算的输出,前一层卷积的输入是再前一层卷积的输出,所以层数足够深的话小卷积核的感受野是足够的。解决问题的方法就是填充(padding)、在输入的图像边界填充 0 元素。

步幅(Stride):有的时候为了高效计算或者是减缩采样的次数,卷积窗口可以跳过中间的位置,每次多滑动几个元素的位置。

在介绍完这两个概念之后,再在这里记录一下 pytorch 中这两个参数是怎么用的。例如 padding=(2,3) 就是说对于行高 h 上下各填充两行,列宽 w 左右各填充三列。stride=(2,3) 就是说我们向右互相关计算滑动窗口的步幅是 3,向下互相关计算滑动窗口的步幅是 2。

多输入输出通道

卷积层的输入通常是多通道 $(c_i,n_h,n_w)$。我们的卷积核通常是 4D,例如 nn.Conv2d(in_channels, out_channels, kernel_size,...) 对象就是四个维度 $(c_i,c_o,k_h,k_w)$,计算的结果就是有 $c_o$ 个二维矩阵的组合进行输出。

汇聚层

池化层的目的:

  1. 池化层是为了让我们卷积对位置不是那么敏感。
  2. 池化层也是减小计算量的一个方式。现在通常是卷积层使用 stride 减小图片的输出。

不过目前,我们的数据是会做很多增强和扰动,例如旋转。这样,我们的卷积层不会对图片输入特定的位置过拟合,淡化了池化层的作用。

卷积计算

在介绍不同的分类网络模型之前,我们需要学会计算模型的

  1. 参数量
  2. 计算复杂度(浮点计算数 FLOP)

模型

LeNet

在 LeNet 网络中使用了卷积编码器和全连接层密集块。每一层的卷积都增加了通道数,每一个通道相当于一种识别图像空间信息的模式。mlp 将所有的模式进行计算。具体网络模型如下: 模型

现代卷积神经网络

AlexNet网络

在传统的机器学习算法中,我们对图像的原始像素进行手动特征提取,将提取到的特征输入我们的 SVM 分类器进行分类,这是 1980-2012 年主要流行的方式。在深度学习中我们认为,数据的特征本身应当是被学习出来的,特征应当由多个共同学习的神经网络层组成,每个层都有可学习的参数。这是一个端到端、由原始像素输入到分类结果的系统。

对比

在 AlexNet 网络的底层,模型可以学习到类似传统滤波器的特征提取器。网络更高层可以建立在底层表示的基础之上表示更大的特征,例如眼睛、鼻子、草叶等等,更高层可以检测整个物体。最终的 Dense 可以学习到图像的综合表示,从而使属于不同类别的数据易于区分。

AlexNet 与 LeNet 网络对比:

  1. 激活函数修改为 Relu,缓解梯度消失
  2. 隐藏层后添加了丢弃层
  3. 对原始图像进行了数据增强。例如图像的截取、明亮度变化、色温变化等等

模型对比

vgg

为了更好的提升网络的精度,我们需要使用更多和更深的卷积,但是 AlexNet 模型缺少规律性,我们由此引入 vgg 网络。vgg 使用可复用的卷积块构造网络,不同的 vgg 模型可通过每个块中卷积块中卷积层和输出通道数量的差异来进行定义。

NiN块

NiN 主要思想是每个像素位置(针对每个高度和宽度)应用一个全连接层,这样的全连接层是使用 1*1 卷积计算得到的,每一个 1*1 卷积核再加上 Relu 激活函数构成了逐像素的非线性全连接层。NiN 和 AlexNet 之间的一个显著区别是 NiN 完全取消了全连接层。 相反,NiN 使用一个 NiN 块,其输出通道数等于标签类别的数量。最后放一个全局平均汇聚层(global average pooling layer),生成一个对数几率(logits)。NiN设计的一个优点是,它显著减少了模型所需参数的数量。然而,在实践中,这种设计有时会增加训练模型的时间。

GoogLeNet

在学习 LeNet、AlexNet、vgg、Nin 网络之后,googleNet 综合使用不同大小的卷积核组合构成自己的 Inception 块。 googLeNet

对于蓝色的四个块,我们可以认为是在提取抽取信息。第一个 1*1 卷积我们认为在抽取通道信息,不是在抽取空间信息(变换输入和输出通道数);第二和第三个卷积我们认为在抽取空间信息;最后一个 MaxPool 其实也在抽取空间信息,使我们的模型变得更加鲁棒。对于白色的三个块,我们认为这是在降低通道数,使我们的模型变得精简,降低模型和计算复杂度,防止过拟合。

这四条路径最后的输出除通道数不一样外,其他的图像高和宽都保持不变,当然这是通过在添加 padding 的结果。最后我们把四条路径的通道数 concat 连接拼凑得到最后的输出。

可以根据如上示意图的例子进一步计算我们模型的参数量和计算复杂度,例如我们使用 192*28*28 的图片作为输入,最终得到 256*28*28 的输出过程中,如果使用 Inception 块或者其他卷积的数据对比情况如下表所示,通过对比我们可以看出 Inception 块的模型参数小,并且计算复杂度低,这是一个主要的优点。

parameters FLOP
Inception 0.16 M 128 M
3*3 0.44 M 346 M
5*5 1.22 M 963 M

后续模型有了一系列的改进,例如 Inception-V3 修改了 Inception 块:

  • 替换 5@5 为 多个 3@3 卷积
  • 替换 5@5 为 1@7 和 7@1 卷积
  • 替换 3@3 为 1@3 和 3@1 卷积

<<<<<<< HEAD

计算机视觉

=======

循环神经网络

序列模型

在本部分,我们面临的数据不再是单独的随机变量,而是具有某种“关系"的不独立随机变量,这就是序列数据。对序列模型的估计需要专门的统计工具,两种较流行的选择是自回归模型和隐变量自回归模型。在自回归模型中常使用马尔可夫模型进行近似替代,即假设当前当前数据只跟 τ 个过去数据点相关。潜变量模型使用潜变量来概括历史信息。

87c4d9ff2146d24eb7794f75f7504bc000b2208c

About

记录在李沐老师《动手学习深度学习》学习到的有趣的知识,在此认真记录一下

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published