• 动手学深度学习

            1 引言

            接触机器学习半年多了,也看了三四十篇论文,复现了不少模型,自认为Python基础还可以。但看着写好的代码,仿佛空中楼阁,想一想自己真正代码上的入门到头来其实只有小土堆的那一门速成的课程,还是不够深入、代码、相关经验其实都远远落后。趁着开学事情少巩固一下技术,积累一些知识。

            神经网络是一门语言

            强推李沐,我素未谋面的恩师 - 跟李沐学AI的个人空间-跟李沐学AI个人主页-哔哩哔哩视频 (bilibili.com)

            本文是博主在学习《动手学深度学习v2》这门课程时的笔记,水平有限,如有错误与不足欢迎指正。

            GNN相关内容年底补充

            2024.9.12 - 9.25 ©️郭同学的笔记本

             

            2 预备知识

            2.3 线性代数

            1.一些容易忘记的特殊矩阵。

            x2=xx0 generalizes to xAx0

            2.特征向量与特征值。

            矩阵就是一次空间的扭曲,而特征向量就是不会被矩阵改变方向的向量。

            Ax=λx

            对称矩阵总是可以找到特征矩阵。

            矩阵特征值和特征向量详细计算过程_特征向量怎么求-CSDN博客

            3.矩阵按照特定轴作sum。

            注意在求x.sum()时,下面的这几个的区别:

            4.torch在矩阵与向量相乘时,是不区分行向量与列向量的。

            2.4 微积分

            1.亚导数?

            将导数扩展到不可微的函数:

            image-20240721154930595

            |x|x={1ifx>01ifx<0aifx=0,a[1,1]

            另一个例子:

            xmax(x,0)={1ifx>00ifx<0aifx=0,a[0,1]

            2.梯度?

            将导数扩展到向量,下面的图表使用分子布局来表示。

            image-20240721155435510

            1. y/x

              x=[x1x2xn]yx=[yx1,yx2,...,yxn]

              关于列向量的导数是一个行向量,举个例子:

              image-20240721160144410

            2. y/x

              y/x是行向量,y/x是列向量。这个被称之为分子布局符号,反过来的版本叫分母布局符号。

            3. y/x

              x=[x1x2xn]y=[y1y2ym]
              yx=[y1xy2xymx]=[y1x1,y1x2,,y1xny2x1,y2x2,,y2xnymx1,ymx2,,ymxn]

            2.5 自动微分

            1.两个手动求导的例子,快速回忆。

            image-20240721171920837

            image-20240721171931924

            但是神经网络动不动就几百层链式,很难手动求导,所以我们需要自动求导。

            2.计算图

            自动求导是计算一个函数在指定值上的导数。

            它有别于:

            • 符号求导
            in[1]:=D[4x3+x2+3,x]Out[1]=2x+12x2
            • 数值求导
            f(x)x=limh0f(x+h)f(x)h

            数值求导不需要知道函数到底长什么样子,他就是带入一个特别小的h现场算一个近似值即可。

            在了解自动求导之前,我们先引入计算图的概念:

            image-20240721173354444

            3.自动求导

            链式法则:yx=yununun1...u2u1u1x

            自动求导的两种方式:

            image-20240721185910088

            因此,前向是执行图,存储中间结果;反向从是相反方向执行图,去除不需要的枝。

            image-20240721190055728

            复杂度:

            4.pyTorch隐式求导示例:

             


             

            3 线性神经网络

            3.2 Softmax回归

            1.交叉熵:交叉熵常用来衡量两个概率的区别

            H(p,q)=ipilog(qi)

            将他们作为损失函数

            l(y,y^)=iyilogy^i=logy^y

            可以证明只有当真实值与预测值相等时,才有最小值。

            2.梯度:梯度就是真实概率与预测概率的区别

            oil(y,y^)=softmax(o)iyi

            证明如下:

            img

             


             

            4 多层感知机

            4.1 单层感知机

            1.局限性:例如不能拟合XOR函数,它只能产生线性分割面。(下面这张图是无法通过一条线来分割的)

            image-20240829151613209

            4.2 多层感知机

            image-20240829152043093

            1.为什么要激活函数?以单隐藏层-单分类问题为例。

            h=σ(W1x+b1)o=w2Th+b2

            σ是按元素的激活函数

            image-20240829152953780

            4.4 权重衰退

            1.使用均方范数作为硬性限制

            通过限制参数值的选择范围来控制模型容量

            min(w,b)    subjecttow2θ

            2.使用均方范数作为柔性限制

            L2正则化:

            对每个θ,都可以找到使得之前的目标函数等价于下面

            min(w,b)+λ2w2

            可以通过拉格朗日乘子来证明!超参数λ控制了正则项的重要程度。

            image-20240829204927054

            这张图还是挺直观的

            那么为什么叫权重衰退呢?

            权重衰退时最广泛使用的正则化技术之一。

            4.5 丢弃法

            image-20240910170357628

            一般都是通过一个mask来实现,因为矩阵乘法往往要比选择快。

            h={0概率为 ph1p其他情况

            根据此模型的设计,其期望值保持不变,即E[h]=hE[h]=h

            4.6 数值稳定性

            考虑如下有d层的神经网络:

            ht=ft(ht1)andy=fd...f1(x)

            计算损失关于参数Wt的梯度:

            Wt=hdhdhd1...ht+1hthtWt

            中间包含(d-t)次矩阵乘法。

            这就会带来两个问题,梯度爆炸梯度消失

            例如:1.51004×10170.81002×1010

            梯度爆炸:以一个MLP为例

            加入如下MLP,为了简单省略了偏移

            ft(ht1)=σ(Wtht1)σ 是激活函数

            htht1=diag(σ(Wtht1))(Wt)Tσ 是σ的导数函数

            这里求导用了链式法则,可以自己再琢磨一下,其实很简单

            i=td1hi+1hi=i=td1diag(σ(Wihi1))(Wi)T

            如果我们使用ReLU作为激活函数

            σ(x)=max(0,x)andσ(x)={1ifx>00otherwise

            那么对角矩阵不是1就是0,最后的值的一些元素就会

            i=td1hi+1hi=i=td1diag(σ(Wihi1))(Wi)T的一些元素会来自于i=td1(Wi)T

            梯度消失

            使用sigmoid作为激活函数

            σ(x)=11+exσ(x)=σ(x)(1σ(x))

            image-20240910173913992

            i=td1hi+1hi=i=td1diag(σ(Wihi1))(Wi)T的元素值是(d-t)个小数值的乘积。

            4.7 让训练更加稳定

            目标:让梯度值在合理的范围内,例如[1e-6, 1e3]

            合理的权重初始化和激活函数

            image-20240910182057609

            1.权重初始化

            2.Xavier初始(比较常用

            3.假设线性的激活函数

            image-20240910190527579

            4.检查常用激活函数

            image-20240910190400354

             


             

            5 深度学习计算

            5.1 层和块

            nn.Sequential定义了一种特殊的Module,Module在Pytorch中是一个很重要的概念。

            Module可以认为是,任何一个层或者任何一个神经网络都可以认为是Module的一个子类。

            自定义块

            在实现我们自定义块之前,我们简要总结一下每个块必须提供的基本功能。

            1. 将输入数据作为其前向传播函数的参数。
            2. 通过前向传播函数来生成输出。请注意,输出的形状可能与输入的形状不同。例如,我们上面模型中的第一个全连接的层接收一个20维的输入,但是返回一个维度为256的输出。
            3. 计算其输出关于输入的梯度,可通过其反向传播函数进行访问。通常这是自动发生的。
            4. 存储和访问前向传播计算所需的参数。
            5. 根据需要初始化模型参数。

            在下面的代码片段中,我们从零开始编写一个块。 它包含一个多层感知机,其具有256个隐藏单元的隐藏层和一个10维输出层。 注意,下面的MLP类继承了表示块的类。 我们的实现只需要提供我们自己的构造函数(Python中的__init__函数)和前向传播函数。

            顺序块

            现在我们可以更仔细地看看Sequential类是如何工作的, 回想一下Sequential的设计是为了把其他模块串起来。 为了构建我们自己的简化的MySequential, 我们只需要定义两个关键函数:

            1. 一种将块逐个追加到列表中的函数;
            2. 一种前向传播函数,用于将输入按追加块的顺序传递给块组成的“链条”。

            下面的MySequential类提供了与默认Sequential类相同的功能。

            在正向传播函数中执行代码

            反向计算是不需要定义的,都是自动求导

            Sequential类使模型构造变得简单, 允许我们组合新的架构,而不必定义自己的类。 然而,并不是所有的架构都是简单的顺序架构。 当需要更强的灵活性时,我们需要定义自己的块。 例如,我们可能希望在前向传播函数中执行Python的控制流。 此外,我们可能希望执行任意的数学运算, 而不是简单地依赖预定义的神经网络层。

            混合搭配各种组合块的方法

            5.2 参数管理

            我们首先关注具有单隐藏层的多层感知机

            参数访问

            image-20240911141113125

            1.目标参数

            2.一次性访问所有参数

            3.从嵌套块收集参数

            设计了网络后,我们看看它是如何工作的。

            因为层是分层嵌套的,所以我们也可以像通过嵌套列表索引一样访问它们。 下面,我们访问第一个主要的块中、第二个子块的第一层的偏置项。

            参数初始化

            1.内置初始化

            为什么实际中我们不能把权重全部初始化为常数?

            1. 对称性问题(Symmetry Problem)

              如果你将所有的权重初始化为相同的值(例如全零或者某个常数),神经网络中的每个神经元在每一层中都会执行完全相同的计算,并且在反向传播时更新的梯度也是相同的。这会导致所有神经元保持相同的权重更新,因此它们的行为无法多样化。具体来说:

              • 每个神经元在每一层执行的操作都是相同的,无法学习到不同的特征。
              • 网络的学习能力会被限制住,训练的效果非常差甚至没有效果。
            2. 梯度传播问题

              如果将所有权重初始化为常数(特别是全零),可能会导致梯度在反向传播时变得非常小,尤其是使用基于梯度的优化算法(如 SGD、Adam 等)时,无法有效更新权重。特别是:

              • 如果权重初始化为零,所有的神经元在反向传播时梯度都会变得一样,因此它们的权重更新也是相同的,这种更新无法让模型学到有意义的特征。
              • 如果权重初始化为一个非常大的常数,则可能导致梯度爆炸,导致网络训练不稳定。

            我们还可以对某些块应用不同的初始化方法。 例如,下面我们使用Xavier初始化方法初始化第一个神经网络层, 然后将第三个神经网络层初始化为常量值42。

            2.自定义初始化

            有时,深度学习框架没有提供我们需要的初始化方法。 在下面的例子中,我们使用以下的分布为任意权重参数𝑤定义初始化方法:

            w{U(5,10)可能性 140可能性 12U(10,5)可能性 14

            更暴力的方法有...

            参数绑定

            有时我们希望在多个层间共享参数: 我们可以定义一个稠密层,然后使用它的参数来设置另一个层的参数。

            5.3 自定义层

            1.构造一个没有任何参数的自定义层

            将层作为组件合并到更复杂的模型中

            2.带参数的层

            • torch.randn():生成 标准正态分布 的随机数,数值范围可能是正数或负数,均值为 0,标准差为 1。
            • torch.rand():生成 均匀分布 的随机数,数值范围为 [0, 1)

            pytorch一维张量不区分行跟列,会自动转换:

            image-20240911195634984

            我们可以使用自定义层直接执行前向传播计算

            使用自定义层构建模型

            5.4 读写文件

            1.加载和保存张量

            存储一个张量列表,然后把它们读回内存

            写入或读取从字符串映射到张量的字典

            2.加载和保存模型参数

            将模型的参数存储在一个叫做“mlp.params”的文件中

            为了恢复模型,我们实例化了原始多层感知机模型的一个备份。 这里我们不需要随机初始化模型参数,而是直接读取文件中存储的参数。

             


             

            6 卷积神经网络

            ad91c2685578c3cbae68a5714191f422

            6.1 卷积层

            1.对全连接层使用平移不变性和局部性得到卷积层

            image-20240913204513695

            2.二维交叉相关

            R

            image-20240913204748119

            *一般表示的都是卷积操作。

            3.二维卷积层

            image-20240913205009735

            超参数是卷积核的大小,代表着而他的局部性。

            卷积层其实就是一个特殊的全连接层

            4.其他一些维度

            image-20240913210206663

            三维一般情况都是多一个时间维度。

            5.我们以一个图像的卷积为例,关注他的代码实现

            1. 先实现他的互相关运算:

            2. 实现二维卷积:

            3. 学习一个由X生成Y的卷积核:

            6.2 填充和步幅

            都是超参数

            填充

            img

            在应用多层卷积时,我们常常丢失边缘像素。 由于我们通常使用小卷积核,因此对于任何单个卷积,我们可能只会丢失几个像素。 但随着我们应用许多连续卷积层,累积丢失的像素数就多了。 解决这个问题的简单方法即为填充(padding):在输入图像的边界填充元素(通常填充元素是0)。

            ../_images/conv-pad.svg

            (nhkh+ph+1)×(nwkw+pw+1)

            当卷积核的高度和宽度不同时,我们可以填充不同的高度和宽度,使输出和输入具有相同的高度和宽度。在如下示例中,我们使用高度为5,宽度为3的卷积核,高度和宽度两边的填充分别为2和1。

            步幅

            R

            填充减小的输出大小与层数线性相关

            ../_images/conv-stride.svg

            一个稍微复杂的例子

            6.3 多输入多输出通道

            彩色图像可能有RGB三个通道,转换为灰度会丢失信息。

            image-20240913234737817

            image-20240913234751264

            多个输入通道

            每个通道都有一个卷积核,结果是所有通道卷积结果的和

            ../_images/conv-multi-in.svg

            (1×1+2×2+4×3+5×4)+(0×0+1×1+3×2+4×3)=56

            在 Python 中,zip() 是一个内置函数,用于将多个可迭代对象(如列表、元组等)的对应元素打包成一个个元组,并返回一个迭代器。

            多个输出通道

            ../_images/conv-1x1.svg

            互相关计算使用了具有3个输入通道和2个输出通道的 1×1 卷积核。其中,输入和输出具有相同的高度和宽度。

            stack()是 PyTorch 中的一个函数,它用于沿着一个新的维度将多个张量(具有相同形状的张量)拼接起来,返回一个新的张量。换句话说,它会把一组形状相同的张量堆叠在一起。

            K = torch.stack((K, K + 1, K + 2), 0)将三个张量 (K, K + 1, K + 2) 沿着第0维度拼接在一起。

            1x1卷积

            ../_images/conv-1x1.svg

            kh=kw=1是一个受欢迎的选择。 它不识别空间模式,只是融合通道。

            因为使用了最小窗口,1×1卷积失去了卷积层的特有能力——在高度和宽度维度上,识别相邻元素间相互作用的能力。 其实1×1卷积的唯一计算发生在通道上。

            1x1的其实等价于一个全连接

            为了验证这一观点,我们使用全连接的方法来构建模型,然后与先前的卷积方法作比较

            6.4 池化层

            双重目的:降低卷积层对位置的敏感性,同时降低对空间降采样表示的敏感性。

            缓解卷积层对于未知的敏感性。

            例如:如果我们拍摄黑白之间轮廓清晰的图像X,并将整个图像向右移动一个像素,即Z[i, j] = X[i, j + 1],则新图像Z的输出可能大不相同。而在现实中,随着拍摄角度的移动,任何物体几乎不可能发生在同一像素上。即使用三脚架拍摄一个静止的物体,由于快门的移动而引起的相机振动,可能会使所有物体左右移动一个像素(除了高端相机配备了特殊功能来解决这个问题)。

            ../_images/pooling.svg

            max(0,1,3,4)=4

            2x2的池化可以容纳1像素的移位

            最大池化层:每个窗口中最强的模式信号

            image-20240914180412040

            平均池化层:将最大池化层中的“最大”操作替换为“平均”

            image-20240914180551128

            实现池化层的正向传播:

            填充和步幅:与卷积层一样,汇聚层也可以改变输出形状。

            默认情况下,深度学习框架中的步幅与汇聚窗口的大小相同。 因此,如果我们使用形状为(3, 3)的汇聚窗口,那么默认情况下,我们得到的步幅形状为(3, 3)

            填充和步幅可以手动设定。

            当然,我们可以设定一个任意大小的矩形汇聚窗口,并分别设定填充和步幅的高度和宽度。

            多个通道的情况:

            6.5 卷积神经网络(LeNet)

            ../_images/lenet.svg

            LeNet(LeNet-5)由两个部分组成:卷积编码器和全连接层密集块

            reshapeview 都是用于对张量进行重塑的操作,但它们在一些细节上有所不同:

            1. view 是 PyTorch 中一种常用的操作,它不改变数据的内存布局,而是通过改变张量的视图来重新组织张量的形状。

              view 需要保证张量在内存中是连续的,即必须是连续存储的张量。

              性能view 通常效率较高,因为它不进行数据的复制,只是改变形状。

            2. reshape 类似于 view,也能改变张量的形状。不同的是,reshape 不强制要求张量是连续的。如果张量不是连续的,reshape 会自动生成一个新的张量来实现所需的形状。

              reshape 更加灵活,因为它可以处理非连续的张量,不需要手动调用 .contiguous()

            ../_images/lenet-vert.svg

            现在我们已经实现了LeNet,让我们看看LeNet在Fashion-MNIST数据集上的表现。

            定义计算精度的函数:

            训练函数:

            训练和评估LeNet-5模型:

            ../_images/output_lenet_4a2e9e_67_1.svg

             


             

            7 现代卷积神经网络

            7.1 深度卷积神经网络(AlexNet)

            image-20240915102106719

            2012年,AlexNet横空出世。它首次证明了学习到的特征可以超越手工设计的特征。它一举打破了计算机视觉研究的现状。 AlexNet使用了8层卷积神经网络,并以很大的优势赢得了2012年ImageNet图像识别挑战赛。

            ../_images/alexnet.svg

            AlexNet和LeNet的设计理念非常相似,但也存在显著差异。

            1. AlexNet比相对较小的LeNet5要深得多。AlexNet由八层组成:五个卷积层、两个全连接隐藏层和一个全连接输出层。
            2. AlexNet使用ReLU而不是sigmoid作为其激活函数。
            3. AlexNet还使用了dropout、MaxPooling

            7.2 使用块的网络(VGG)

            AlexNet最大的问题其实是长得不规则,结构长得不那么清晰。

            我如果想要变得更深更大,我就需要把我的框架设计的更清晰一点。

            选项:

            在CNN中:

            指的是更多的卷积层,可以提取更复杂的特征。

            指的是卷积核的大小,决定了单个卷积操作能“看到”多少图像区域。

            ../_images/vgg.svg

            VGG块的核心是:

            不同次数的重复块得到不同的架构VGG-16, VGG-19...

            image-20240915115039884

            原始VGG网络有5个卷积块,其中前两个块各有一个卷积层,后三个块各包含两个卷积层。 第一个模块有64个输出通道,每个后续模块将输出通道数量翻倍,直到该数字达到512。由于该网络使用8个卷积层和3个全连接层,因此它通常被称为VGG-11。

            Implementing VGG11 from Scratch using PyTorch

            训练模型:

            ../_images/output_vgg_4a7574_71_1.svg

            7.3 网络中的网络(NiN)

            虽然该网络现在很少被用到,但是它提出的思想还是比较关键的。

            1.全连接层的问题

            最重要的是,它极易带来过拟合。

            NiN的思想就是,我完全不要全连接层

            2.NiN块

            ../_images/nin.svg

            一个卷积层后跟两个全连接层:

            image-20240915140922955

            3.NiN架构

            4.代码实现

            ../_images/output_nin_8ad4f3_51_1.svg

            7.4 含并行连接的网络(GoogLeNet)

            最好的卷积层超参数?LeNet、AlexNet、VGG、NiN用哪个?

            Inception块:小学生才做选择题,我全要了!

            4个路径从不同层面抽取信息,然后在输出通道维合并

            ../_images/inception.svg

            GoogLeNet一共使用9个Inception块和全局平均汇聚层的堆叠来生成其估计值。Inception块之间的最大汇聚层可降低维度。 第一个模块类似于AlexNet和LeNet,Inception块的组合从VGG继承,全局平均汇聚层避免了在最后使用全连接层。

            ../_images/inception-full.svg

            1.段1&2

            image-20240915155511370

            2.段2

            image-20240915155625472

            3.段4&5

            image-20240915155822425

            4.Inception有各种后续变种

            5.随便看看得了,反正就是按照结构体敲代码,都是套娃...

            7.4. 含并行连结的网络(GoogLeNet) — 动手学深度学习 2.0.0 documentation (d2l.ai)

            7.5 批量归一化

            理论

            image-20240915164830752

            现有的问题:

            我们可以在学习底部层的时候避免变化顶部层吗?

            固定小批量里面的均值和方差:

            μB=1|B|iBxi and σB2=1|B|iB(xiμB)2+ϵ

            然后再做额外的调整(可学习的参数):

            image-20240915182904343

            作用位置建议看代码,更好理解一些

            但实际上,沐神的理解说他其实就是一个正则化或者dropout,可以加快收敛速度,但是一般不会改变模型精度https://www.bilibili.com/video/BV1X44y1r77r?t=1063.1

            代码实现

            首先我们实现这一层

            单个维度求均值时

            多个维度求均值时

            这么看来,其实求哪个维度,数值就向哪个维度“聚拢”

            创建一个正确的BatchNorm图层

            应用BatchNorm于LeNet

            在Fashion-MNIST数据集上训练网络

            ../_images/output_batch-norm_cf033c_51_1.svg

            看一下学出来的拉伸参数gammabeta

            简洁实现

            7.6 残差网络(ResNet)

            加ge能更多的层总是改变精度吗?

            image-20240916093857833

            右边这张图展现了残差网络的核心思想。

            1.残差块

            image-20240916094305651

            2.ResNet细节

            image-20240916094608814

            3.不同的残差块

            image-20240916094744500

            4.ResNet块

            image-20240916095018567

            5.ResNet架构

            image-20240916095130257

            残差网络对随后的深层神经网络设计产生了深远影响,无论是卷积类网络还是全连接类网络。

            6.代码实现

            使用样例:

            ResNet模型:

            上述代码实现了如下模型:

            ../_images/resnet18.svg

            ../_images/output_resnet_46beba_126_1.svg

             


             

            8 计算性能

            这一部分主要是在讲多GPU并行、分布式计算等等,所以就随便看了看,没有笔记。

            image-20240916160513426

             


             

            9 计算机视觉

            因为自己现在做的工作跟计算机视觉毫无关系吧哈哈,以后也不打算进入这一个领域,所以这一章也是随便听了听,就当小视频刷了。内容挺多,还是比较有收获的。

            还记得入门机器学习的时候就是看的计算机视觉的内容,所以这部分其实还是挺扎实的昂😗

            image-20240916161524486

             


             

            10 循环神经网络

            10.1 序列模型

            1.序列数据

            2.统计工具

            在时间t观察到x,那么得到T个不独立的随机变量 (x1,...xT)p(x)

            使用条件概率展开 p(a,b)=p(a)p(b|a)=p(b)p(a|b)

            image-20240916192945277

            对条件概率建模

            p(xt|x1,...xt1)=p(xt|f(x1,...xt1))

            对见过的数据建模,也称自回归模型

            3.建模方案1:马尔可夫假设

            image-20240916200321353

            假设当前数据只跟 τ 个过去数据点相关

            image-20240916200810878

            4.建模方案2:潜变量模型

            引入潜变量 ht 来表示过去信息 ht=f(x1,...xt1)

            image-20240916201408166

            这样我们就可以拆成两个模型:

            1. 通过htxt计算ht+1
            2. 通过htxt1计算xt

            5.马尔可夫假设代码实现

            使用正弦函数和一些可加性噪声来生成序列数据,时间步为1,2,.,1000

            ../_images/output_sequence_ce248f_6_0.svg

            将数据映射为数据对 yt=xt 和 xt=[xtτ,,xt1]

            定义模型:

            训练:

            预测:

            ../_images/output_sequence_ce248f_66_0.svg

            如果直接从600开始,往后预测400个点,新预测的点再被用于下一个点的预测:

            ../_images/output_sequence_ce248f_81_0.svg

            从图上绿色的线来看,效果其实还算是很差的。原因是每次的预测都有误差,不断的累计长期就会偏离。

            我们按照这个思想继续进行测试:

            ../_images/output_sequence_ce248f_96_0.svg

            那么我们接下来努力的方向就算如何尽可能远的预测,捕捉更多的序列信息。

            10.2 文本预处理

            1.读取数据集

            将数据集读取到由多条文本行组成的列表中

            2.词元化

            每个文本序列又被拆分成一个标记列表

            按一个词一个词的算,模型其实相对简单

            如果把一个串作为一个词元(token)的话,数量会相对较少,但是坏处就算还需要学怎么用字符构成一个词

            构建一个字典,通常也叫做词汇表(vocabulary),用来将字符串类型的标记映射到从0开始的数字索引中

            3.整合所有功能

            将所有功能打包到load_corpus_time_machine函数中

            按照词频排序的好处一是看起来直观,二是性能会好一些。

            10.3 语言模型和数据集

            给定文本序列x1,...,xT,语言模型的目标是估计联合概率p(x1,...,xT)

            他的应用包括:

            1.使用计数来建模

            假设序列长度为2,我们预测

            p(x,x)=p(x)p(x|x)=n(x)nn(x,x)n(x)

            这里n是总词数,n(x),n(x,x,)是单个单词和连续单词对的出现次数。

            很容易拓展到长为3的情况

            p(x,x,x)=p(x)p(x|x)p(x|x,x)=n(x)nn(x,x)n(x)n(x,x,x)n(x,x)

            2.N元语法

            当序列很长时,因为文本量不够大,很可能n(x1,...,xT)1

            使用马尔科夫假设可以缓解这个问题:

            最大的好处是可以处理比较长的序列。

            3.代码实现-词组统计

            [token for line in tokens for token in line] 是一个列表推导式中的双循环,它的作用相当于:

            逆天python语法

            ../_images/output_language-models-and-dataset_789d14_21_0.svg

            我们现在看一下二元语法的表现:

            最后,我们直观地对比三种模型中的词元频率:一元语法、二元语法和三元语法。

            ../_images/output_language-models-and-dataset_789d14_66_0.svg

            4.代码实现-读取长序列数据

            ../_images/timemachine-5gram.svg

            方法一:随机采样

            随机地生成一个小批量数据的特征和标签以供读取。在随机采样中,每个样本都是在原始的长序列上任意捕获的子序列。

            有一个选取的小窍门:https://www.bilibili.com/video/BV1ZX4y1F7K3?t=961.9&p=2

            方法二:顺序分区

            上述两种方法整合为类:

            10.4 循环神经网络RNN

            循环神经网络跟递归神经网络有区别,注意区分

            潜变量自回归模型

            使用潜变量ht总结过去信息

            image-20240917151028280

            循环神经网络

            image-20240917151523756

            image-20240917151821141

            注意隐变量跟浅变量的区别,可以自行查一下

            使用循环神经网络的语言模型

            image-20240917152309273

            计算损失的时候是通过比较xtot

            困惑度(perplexity)

            衡量一个语言模型的好坏可以用平均交叉熵

            π=1ni=1nlogp(xt|xt1,...)

            p是语言模型的预测概率,xt是真实词

            历史原因NLP使用困惑度exp(π)来衡量,是平均每次可能选项

            梯度裁剪

            迭代中计算这T个时间步上的梯度,在反向传播过程中产生长度为 O(T)的矩阵乘法链,导致数值不稳定

            梯度裁剪能有效预防梯度爆炸

            关于RNN的反向传播梯度分析:RNN/LSTM BPTT详细推导以及梯度消失问题分析 - 知乎 (zhihu.com)

            更多的应用RNNs

            image-20240917154911968

            10.5 RNN代码实现

            从零开始实现

            内容比较多,打个预防针,真给哥们儿看老实了

            在基础RNN模型中,所有时间步的隐藏层都共用同一组参数矩阵 W_xh, W_hh, b_h, W_hq, 和 b_q

            这是RNN的特点之一,称为参数共享。在循环神经网络中,隐藏层的权重矩阵和偏置在每个时间步都是相同的。这种参数共享允许RNN在不同时间步使用相同的规则来处理输入和隐藏状态,从而使得网络能够处理不同长度的序列。

            下面这张图可以很好的解答关于我对于参数的困惑:

            image-20240918003501985

            1.读取数据集

            2.独热编码

            小批量数据形状是(批量大小32,时间步数35

            3.初始化RNN模型参数

            一个init_rnn_state 函数在初始化时返回隐藏状态

            4.做计算的函数

            下面的rnn函数定义了如何在一个时间步内计算隐藏状态和输出

            5.创建一个类来包装这些函数

            检查输出是否具有正确的形状:

            我们可以看到输出形状是(时间步数×批量大小,词表大小), 而隐状态形状保持不变,即(批量大小,隐藏单元数)。

            6.预测函数

            7.梯度裁剪

            norm=psum(gradp2)

            8.训练

            开始训练:

            ../_images/output_rnn-scratch_546c4d_202_1.svg

            看一下随机抽样方法的结果:

            image-20240917194202175

            简洁实现

            谢谢你,pytorch🫶

            pytorch内部只实现了隐藏层的更新与计算,输出那一步需要自己加linear

            1.导入数据

            2.定义RNN层,一层中所使用的隐藏层数定义见3

            3.使用张量来初始化隐藏状态

            它的形状是(隐藏层数,批量大小,隐藏单元数)

            4.通过一个隐藏状态和一个输入,我们就可以用更新后的隐藏状态计算输出

            Y记录了每一个时间步中每一个批次的隐藏层状态,跟前面自己实现的略有不同。

            5.定义RNN模型

            6.基于一个具有随机权重的模型进行预测

            7.训练

            ../_images/output_rnn-concise_eff2f4_87_1.svg

            由于深度学习框架的高级API对代码进行了更多的优化, 该模型在较短的时间内达到了较低的困惑度。

            我们之前自己实现的时候有好多小矩阵乘法,框架会做优化整合成大矩阵乘法,所以总的看来比自己实现快了三倍左右。

             


             

            11 现代循环神经网络

            11.1 控制循环单元(GRU)

            效果上跟LSTM差不多,但是稍微简单一点,实际中这两个用哪个都差不多

            在RNN中,我们处理不了太长的序列,因为我们把整个序列信息全部放在隐藏状态中,他其实放不了太多东西。

            不是每个观察值都同等重要

            image-20240918191041880

            想要记住相关的观察需要:

            Rt=σ(XtWxr+Ht1Whr+br),Zt=σ(XtWxz+Ht1Whz+bz)H~t=tanh(XtWxh+(RtHt1)Whh+bh)Ht=ZtHt1+(1Zt)H~t

            1.门

            可以把门看成一个跟隐藏状态一样长度的向量,他们的计算方式也是相似的。

            Rt=σ(XtWxr+Ht1Whr+br),Zt=σ(XtWxz+Ht1Whz+bz)

            image-20240918191749227

            2.候选隐状态

            是按元素做乘法的意思,称之为“软”控制。

            H~t=tanh(XtWxh+(RtHt1)Whh+bh)

            image-20240918192256905

            3.隐状态

            Ht=ZtHt1+(1Zt)H~t

            image-20240918193133119

            4.代码实现-从零开始

            读取数据集

            image-20240918205236879

            初始化模型参数

            定义隐藏状态的初始化函数

            定义门控循环单元模型

            训练与预测

            ../_images/output_gru_b77a34_66_1.svg

            5.代码实现-简洁实现

            ../_images/output_gru_b77a34_81_1.svg

            11.2 长短期记忆网络(LSTM)

            开个小差,刷完网课就立刻刷到了这个视频,他是真懂啊 热播剧《好事成双》,张小斐说LSTM比transformer效果好?

            It=σ(XtWxi+Ht1Whi+bi)Ft=σ(XtWxf+Ht1Whf+bf)Ot=σ(XtWxo+Ht1Who+bo)C~t=tanh(XtWxc+Ht1Whc+bc)Ct=FtCt1+ItC~tHt=Ottanh(Ct)

            1.门

            It=σ(XtWxi+Ht1Whi+bi)Ft=σ(XtWxf+Ht1Whf+bf)Ot=σ(XtWxo+Ht1Who+bo)

            image-20240918225610650

            2.候选记忆单元

            C~t=tanh(XtWxc+Ht1Whc+bc)

            image-20240918225815568

            3.记忆单元

            Ct=FtCt1+ItC~t

            image-20240918232004983

            4.隐状态

            Ht=Ottanh(Ct)

            image-20240918232259286

            5.代码实现-从零开始

            其实本质没区别,这里就快速写一下吧

            ../_images/output_lstm_86eb9f_66_1.svg

            6.代码实现-简洁实现

            ../_images/output_lstm_86eb9f_81_1.svg

            11.3 深度循环神经网络

            理论

            序列变长不是深度,RNN解决了梯度问题后,开始往深发展

            image-20240919004319628

            现在我要更深

            image-20240919004437438

            公式也比较简单:

            Ht1=f1(Ht11,Xt)Htj=fj(Ht1j,Htj1)Ot=g(HtL)

            代码实现

            从零开始也太无聊了,直接写简洁实现吧

            ../_images/output_deep-rnn_d70a11_30_1.svg

            可以看到收敛更快,更加过拟合了,一般来说就算小网络也要两层,计算速度也会下降一点

            11.4 双向循环神经网络

            理论

            未来很重要

            image-20240919091128947

            双向神经网络:两个隐藏层,一个前向,以后后向,合并两个隐状态得到输出。

            image-20240919091347823

            实现起来很简单,只需要把原本的RNN正反执行两遍,然后把所有输出(隐状态H)拼接起来就可以。

            Ht=ϕ(XtWxh(f)+Ht1Whh(f)+bh(f)),Ht=ϕ(XtWxh(b)+Ht+1Whh(b)+bh(b)),Ht=[Ht,Ht]Ot=HtWhq+bq

            训练的时候简单,但是推理的时候怎么推?

            image-20240919091957541

            双向RNN,非常不适合做推理。几乎是不可以预测未来的词。

            他的主要作用是对一个句子做特征提取,给我的句子我可以双向的去看它。语音识别类似的也可以使用,我可以等你把句子说完再做处理。

            代码实现

            也是比较简洁的实现一下

            下面的是一个错误的案例,使用双向LSTM来预测语言模型

            结果:

            ../_images/output_bi-rnn_c6b17e_6_1.svg

            可以看到收敛的很快,但是结果非常不靠谱。

            双向RNN,在正向跟反向之间没有任何的权重联系,仅仅是分两次跑,然后结果concat在一起

            11.5 机器翻译与数据集

            1.下载和预处理数据集

            我们要把标点符号也翻出来

            2.几个预处理步骤

            3.变成token(词元化)

            这个数据集相对比较简单,所以我们按词来分就可以了

            4.绘制每个文本序列所包含的标记数量的直方图

            ../_images/output_machine-translation-and-dataset_887557_66_0.svg

            5.建立词汇表

            pad表示填充,bos(begin of sentence)表示句子开始,eos表示句子结束

            image-20240919105600913

            6.序列样本都有一个固定的长度截断或填充文本序列

            句子的长度是不一样的,与我们之前可以切成固定长度不同。

            我们这里固定一个长度num_steps,如果超过就切掉,不够就填充。

            7.转换成小批量数据集用于训练

            8.整合

            这里英语与法语都各自做了一个vocab,对于这个简单的数据集已经够了

            现在流行的做法是同意构建一个巨大的词汇表vocab

            11.6 编码器-解码器架构

            对近几年对于模型的抽象影响比较深刻

            1.重新考察CNN

            image-20240919112337086

            编码器:将输入编码成中间表达形式(特征)

            解码器:将中间表示解码成输出

            2.重新考察RNN

            image-20240919113133514

            编码器:将文本表示成向量

            解码器:向量表示成输出

            3.编码器-解码器架构

            一个模型被分为两块:

            image-20240919113357433

            4.代码(不完整)示例

            后面我们做nlp的时候会具体展现,这里只是给一个固定的框架

            编码器

            raise是一个用于手动引发异常的关键字

            解码器

            合并编码器和解码器

            11.7 序列到序列学习(seq2seq)

            概念

            1.机器翻译

            image-20240919143325843

            2.Seq2seq

            image-20240919143848955

            编码器是一个RNN,读取输入句子

            解码器使用另一个RNN来输出

            3.编码器-解码器细节

            编码器是没有输出的RNN

            编码器最后时间步的隐状态用作解码器的初始隐状态

            image-20240919155944797

            具体有很多实现方式

            4.训练

            训练时解码器使用目标句子作为输入

            理解不了图看视频:https://www.bilibili.com/video/BV16g411L7FG?t=434.1

            image-20240919160150702

            image-20240919160210032

            5.衡量生成序列的好坏的BLEU

            pn是预测中所有 n-gram 的精度

            BLEU定义:

            image-20240919160748016

            代码实现

            1.实现循环神经网络编码器

            output是所有时间步的最后一层RNN的隐状态输出,state是最后一个时刻的所有层的隐状态

            2.实例化上述编码器

            3.解码器

            permute用来改变张量的维度顺序

            4.实例化解码器

            5.⭐损失函数

            重点看一下,新知识

            在每个时间步,解码器预测了输出词元的概率分布。 类似于语言模型,可以使用softmax来获得分布, 并通过计算交叉熵损失函数来进行优化。 回想一下之前【点击跳转】, 特定的填充词元被添加到序列的末尾, 因此不同长度的序列可以以相同形状的小批量加载。 但是,我们应该将填充词元的预测排除在损失函数的计算之外。

            为此,我们可以使用下面的sequence_mask函数 通过零值化屏蔽不相关的项, 以便后面任何不相关预测的计算都是与零的乘积,结果都等于零。 例如,如果两个序列的有效长度(不包括填充词元)分别为1和2, 则第一个序列的第一项和第二个序列的前两项之后的剩余项将被清除为零。

            mask在处理变长东西中是一个很常见的操作

            我们还可以使用此函数屏蔽最后几个轴上的所有项。如果愿意,也可以使用指定的非零值来替换这些项。

            通过扩展softmax交叉熵损失函数来遮蔽不相关的预测

            torch.ones记录的是真是标签的label

            但是每一个值的嵌入为10维,为什么直接用一位就可以表示标签?

            在交叉熵损失中,标签只需要提供类别索引,因为交叉熵计算的是模型预测概率和真实类别之间的差异。模型输出的是每个类别的概率分布(如 10 维向量),而标签只需要指明当前样本属于哪个类别(如索引 12 等)。交叉熵根据该索引提取预测的概率并计算损失,不需要提供嵌入向量。

            6.训练

            ../_images/output_seq2seq_13725e_171_1.svg

            7.预测

            越来越复杂了,说实话到这里基本上代码也只是理解大致意思了仅仅,佩服首次实现这些代码的人,希望几年以后我也可以轻松写出这些机器学习的代码。

            num_steps 在这个函数中决定了生成的句子的最大长度

            8.BLUE代码的实现

            11.8 束搜索

            贪心搜索

            在seq2seq中我们使用了贪心搜索来预测序列

            但贪心很可能不是最优的

            image-20240919233744453

            穷举搜索

            最优算法:对所有可能的序列,计算它的概率,然后选取最好的那个

            如果输出字典大小为n,序列最长为T,那么我们需要考察nT个序列:

            束搜索

            保存最好的k个候选。

            在每个时刻,对每个候选新加一项(n种可能),在kn个选项中选出最好的k个。

            image-20240919234918069

            我愿称之为贪心-脚踏两只船版

            image-20240919235311182

             


             

            12 注意力机制

            12.1 注意力机制

            这一节的内容与我们之后要讲的其实关系不大,只是起一个引导作用,来说明注意力这个思想其实也不是新提出来的

            心理学

            ../_images/eye-coffee.svg

            不随意线索:由于突出性的非自主性提示(红杯子),注意力不自主地指向了咖啡杯,这是“无意识”线索

            ../_images/eye-book.svg

            随意线索:当人想读书时,依赖于任务的意志提示(想读一本书),注意力被自主引导到书上,这是“有意识”线索

            注意力机制

            卷积、全连接、池化层都只考虑不随意线索

            注意力机制则显示的考虑随意线索:

            ../_images/qkv.svg

            非参注意力池化层

            Nadaraya-Watson核回归

            到这里,我们发现没有产生什么可以学的参数,那么⬇️

            参数化的注意力机制

            在之前基础上引入可以学习的 w

            f(x)=i=1nsoftmax(12((xxi)w)2)yi

            注意,w 在这里是一个一维的标量

            image-20240921235516919

            可以一般的写作

            f(x)=iα(x,xi)yi

            这里 α(x,xi) 是注意力权重。

            代码实现

            这里就是简单过一下

            1.最简单的平均聚合

            生成一些随机数据

            简单画一下这个函数图

            ../_images/output_nadaraya-waston_736177_39_0.svg

            2.非参数注意力汇聚

            这个代码其实很好理解,可以仔细看一看

            只要给足够多的数据,函数时可以拟合出来的,但是现实中不会有那么多的数据

            ../_images/output_nadaraya-waston_736177_54_0.svg

            现在来观察注意力的权重。 这里测试数据的输入相当于查询,而训练数据的输入相当于键。 因为两个输入都是经过排序的,因此由观察可知“查询-键”对越接近, 注意力汇聚的注意力权重就越高。

            ../_images/output_nadaraya-waston_736177_69_0.svg

            3.带参数的注意力汇聚

            带batch的矩阵乘法:

            定义模型:

            训练:

            ../_images/output_nadaraya-waston_736177_144_0.svg

            ../_images/output_nadaraya-waston_736177_159_0.svg

            为什么新的模型更不平滑了呢? 下面看一下输出结果的绘制图: 与非参数的注意力汇聚模型相比, 带参数的模型加入可学习的参数后, 曲线在注意力权重较大的区域变得更不平滑。权重更集中了

            ../_images/output_nadaraya-waston_736177_174_0.svg

            12.2 注意力分数

            注意力分数

            image-20240922144136395

            下面这张图画的非常好,与之前不同的是,输入可能不是一个值而是变成了一个向量:

            ../_images/attention-output.svg

            拓展到高维度

            假设query qRqm 对 key-value (k1,v1),...,这里 kiRk,viRν

            注意力池化层:

            f(q,(k1,v1),,(km,vm))=i=1mα(q,ki)viRv,

            image-20240922150051193

            所以现在关键就是a这个注意力评分函数怎么设计

            Additive Attention

            “可加性的注意力”,之类的加包含了加减的意思

            可学参数:

            WkRh×k,WqRh×q,vRh
            a(k,q)=vTtanh(Wkk+Wqq)

            等价于将 key 和 quary 合并起来后放入到一个隐藏大小为 h 输出大小为 1的单隐藏层 MLP

            Scaled Dot-Product Attention

            缩放点积注意力机制

            如果 query 和 key 都是同样的长度,q,kiRd,那么可以

            a(q,ki)=q,ki/d

            除以 d 是为了归一化,对长度变化没那么敏感。

            向量化版本:

            以上这是注意力中两种常见的分数及算方法。

            代码实现-掩蔽softmax操作

            遮蔽softmax操作:

            这里不能像之前一样设成0了,做指数就会有问题

            演示此函数是如何工作的

            代码实现-加性注意力

            forward函数有些难懂,用了广播机制,建议多看看

            到底是什么样的神人能写出这样的代码,我这辈子也达不到这样的高度

            用一个小例子来演示上面的AdditiveAttention类, 其中查询、键和值的形状为(批量大小,步数或词元序列长度,特征大小), 实际输出为(2,1,20)(2,10,2)(2,10,4)。 注意力汇聚输出的形状为(批量大小,查询的步数,值的维度)。

            尽管加性注意力包含了可学习的参数,但由于本例子中每个键都是相同的, 所以注意力权重是均匀的,由指定的有效长度决定。

            ../_images/output_attention-scoring-functions_2a8fdc_96_0.svg

            代码实现-缩放点积注意力

            可以看到它的好处是不需要学习任何参数,实现简单。

            ../_images/output_attention-scoring-functions_2a8fdc_141_0.svg

            加下来我们就需要学习怎样将attention的概念应用到我们的网络中,讲key、value、query对应到原网络的概念中。

            12.3 使用注意力机制的seq2seq

            动机

            机器翻译中,每个生成的词可能相关于源句子中不同的词,即翻译任务有一定的对应关系

            img

            加入注意力

            ../_images/seq2seq-attention-details.svg

            代码实现-Bahdanau 注意力

            叫这个名字是因为这个人是一作

            1.带有注意力机制的解码器基本接口

            2.核心实现,带有Bahdanau注意力的循环神经网络解码器

            attention只作用在Decoder上,Encoder是不变的

            enc_valid_lens 在这个 Seq2SeqAttentionDecoder 中的作用是记录编码器输出的有效长度,用来标记原句子的长度。

            3.测试Bahdanau注意力解码器

            4.训练

            ../_images/output_bahdanau-attention_7f08d9_53_1.svg

            5.将几个英语句子翻译成法语

            6.可视化注意力权重

            ../_images/output_bahdanau-attention_7f08d9_87_0.svg

            常见QA

            12.4 自注意力和位置编码

            在深度学习中,经常使用卷积神经网络(CNN)或循环神经网络(RNN)对序列进行编码。 想象一下,有了注意力机制之后,我们将词元序列输入注意力池化中, 以便同一组词元同时充当查询、键和值。 具体来说,每个查询都会关注所有的键-值对并生成一个注意力输出。 由于查询、键和值来自同一组输入,因此被称为 自注意力(self-attention)。

            本节将使用自注意力进行序列编码,以及如何使用序列的顺序作为补充信息。

            自注意力

            有点像RNN昂

            image-20240923122104995

            在处理序列方面,与CNN、RNN对比:

            image-20240923122246056

             CNN(k为窗口大小)RNN自注意力
            计算复杂度O(knd^2)O(nd^2)O(n^2d)
            并行度O(n)O(1)O(n),并行度verygood
            最长路径(视野)O(n/k)O(n)O(1)

            自注意力特别适合处理长文本,首先并行度高,其次可以看到近乎无限远。所以GPT等等都用了自注意力。

            位置编码

            跟CNN/RNN不同,自注意力并没有记录位置信息

            位置编码将位置信息注入到输入里:

            P 的元素如下计算:

            pi,2j=sin(i100002j/d),pi,2j+1=cos(i100002j/d)

            P 的讲解:https://www.bilibili.com/video/BV19o4y1m7mo?t=1158.8,这里其实表示的是相对位置信息

            image-20240923162801870

            ⭐学到这里我其实一直有两个疑问:

            1. 为什么向量为什么可以相加呢?相加后向量的大小和方向就变了,语义不就变了吗?

              我找到了一个不错的解答:https://www.zhihu.com/question/374835153

            2. 为什么要使用这个位置编码,有什么好处?

              一文读懂Transformer模型的位置编码 - 知乎 (zhihu.com)

            绝对位置信息

            计算机使用的二进制编码:

            image-20240923172609069

            位置编码矩阵:

            image-20240923173040742

            看到上面这个图,我觉的对这个奇怪的位置矩阵已经是非常形象了!

            相对位置信息

            位置于 i+δ 的位置编码可以线性投影位置 i 处的位置编码来表示

            ωj=1/100002j/d,那么:

            image-20240923175438602

            i 表示的是序列中的位置,j 表示的是在维度中的位置,δ 是偏移量。

            这也是使用位置编码矩阵的原因之一:https://www.bilibili.com/video/BV19o4y1m7mo?t=1522.4

            绝对位置总是有问题的,相对位置才有用。可以做到不管两个向量在序列中的哪个位置,都可以通过线性变换来快速转换。

            代码实现

            这里就非常简单的过一下,省略了一些。

            这里的位置编码还是不需要学习的,Bert中我们将介绍可学习的位置编码。

            位置编码

            简单用GPT写了一下自注意力的代码,挺简单的一看就懂

            img

            得到的自注意力输出是直接替换掉原本的模型输入还是与模型输入加和/拼接在一起?

            12.5 Transformer

            Transformer架构

            可以说它是一个纯基于注意力的,或者说是自注意力的架构。

            image-20240923221132217

            上面的是使用注意力机制的seq2seq,下面的是Transformer

            image-20240923221301212

            多头注意力

            Transformer中的是多头注意力机制

            img

            对同—key,value,query,希望抽取不同的信息

            多头注意力使用 h 个独立的注意力池化

            image-20240923222515134

            image-20240923222559308

             query qRdq, key kRdk, value vRdν

            i 的可学习参数 Wi(q)Rpq×dq,Wi(k)Rpk×dk,Wi(ν)Rpν×dν

            i 的输出 hi=f(Wi(q)q,Wi(k)k,Wi(ν)v)Rpν

            输出的 可学习参数 WoRpo×hpν

            多头注意力的输出:

            Wo[h1hh]Rpo

            img

            那多头以后怎么处理最后的向量?

            其实很简单,只需要三步:

            1. 将 8 个向量 concat 起来得到长长的参数矩阵
            2. 将该矩阵与一个参数矩阵 𝑊0 进行相乘,该参数矩阵的长是一个 𝑍 向量的长度,宽是 8 个 𝑍 向量 cat 后的长度
            3. 相乘的结果的形状就是一个 𝑍 向量的形状

            这样我们通过一个参数矩阵完成了对 8 个向量的特征提取

            img

            下图就是 multi-headed attention 的全部流程:

            img

            有掩码的多头注意力

            其实也是多头注意力机制

            image-20240923224451931

            解码器对序列中一个元素输出时,不应该考虑该元素之后的元素

            可以通过掩码来实现

            基于位置的前馈网络

            image-20240923225738687

            其实我一直不是很理解为什么 1*1 卷积等价于一个全连接,下面我画了张图,有助于理解:

            image-20240924230354659

            image-20240925102721374

            层归一化

            image-20240923231429927

            批量归一化(BatchNormalization 见7.5)对每个特征/通道里元素进行归一化

            层归一化对每个样本里的元素进行归一化,d 表示隐层维度(一个字/词的向量表示),b 表示 batch_size

            image-20240923231551262

            信息传递

            这里是一个正常的注意力机制,不是自注意力了

            image-20240923234226049

            这个我第一次学的时候有一些误解,认为n次编码块会把每一次的都给对应的解码块,例如:

            • EncoderBlock[1]->DecoderBlock[1]
            • EncoderBlock[2]->DecoderBlock[2]
            • ......
            • EncoderBlock[n]->DecoderBlock[n]

            上述理解是错的!

            实际内部的图是这样的:

            image-20240925232014223

            预测

            预测第 t+1 个输出时

            解码器中输入前 t 个预测值

            image-20240923234933637

            这部分预测写的稍微有点问题,建议直接看代码理解

            代码实现-多头注意力

            1.主要代码,选择缩放点积注意力作为每一个注意力头

            这里挺巧妙的,多头按理来说需要很多个q、k、v,但是这里通过transpose_qkv取了个巧,体现了我们之前多次说到的将小矩阵运算转换为大矩阵运算的提速思想。

            2.使多个头并行计算

            3.测试

            4.下面这张图概括了数据在这一模块的流动变化:

            image-20240925103533528

            代码实现-Transformer

            1.基于位置的前馈网络

            2.对比不同维度的层归一化和批量归一化的效果

            使用残差连接和归一化

            残差连接要求两个输入的形状相同,以便加法操作后输出张量的形状相同。

            3.实现编码器的一个层(Transformer EncoderBlock)

            正如从代码中所看到的,Transformer编码器中的任何层都不会改变其输入的形状。

            看上去很复杂,实际上还行,这个参数一般就是下面这样设置了。

            4.Transformer编码器

            blk.attention

            • blkEncoderBlock,它包含一个多头注意力模块(MultiHeadAttention)。
            • blk.attention 指的是 EncoderBlock 中的 MultiHeadAttention 实例。

            blk.attention.attention

            • MultiHeadAttention 中,self.attentionDotProductAttention 实例,它用于计算点积注意力的实际操作。

            blk.attention.attention.attention_weights

            • DotProductAttention 中,attention_weights 记录了在点积注意力机制中,查询(queries)与键(keys)之间的相似度分数(权重),用于最终对值(values)进行加权求和。

            image-20240925182305900

            下面我们指定了超参数来创建一个两层的Transformer编码器。 Transformer编码器输出的形状是(批量大小,时间步数目,num_hiddens)。

            5.实现解码器的一个层(Transformer DecoderBlock)

            编码器和解码器的特征维度都是 num_hiddens

            6.Transformer解码器

            7.训练

            ../_images/output_transformer_5722f1_201_1.svg

            比RNN不会慢到哪里去

            8.预测

            9.一些可视化

            当进行最后一个英语到法语的句子翻译工作时,让我们可视化Transformer的注意力权重。编码器自注意力权重的形状为(编码器层数,注意力头数,num_steps或查询的数目,num_steps或“键-值”对的数目)。

            逐行呈现两层多头注意力的权重:

            ../_images/output_transformer_5722f1_246_0.svg

            ⬆️编码器的自注意力权重

            为了可视化解码器的自注意力权重和“编码器-解码器”的注意力权重,我们需要完成更多的数据操作工作。例如用零填充被掩蔽住的注意力权重。值得注意的是,解码器的自注意力权重和“编码器-解码器”的注意力权重都有相同的查询:即以序列开始词元(beginning-of-sequence,BOS)打头,再与后续输出的词元共同组成序列。

            ../_images/output_transformer_5722f1_276_0.svg

            ⬆️编码器到解码器的注意力权重

            与编码器的自注意力的情况类似,通过指定输入序列的有效长度,输出序列的查询不会与输入序列中填充位置的词元进行注意力计算。

            ../_images/output_transformer_5722f1_291_0.svg

            ⬆️解码器带掩码的的自注意力权重

            尽管Transformer架构是为了序列到序列的学习而提出的,但正如本书后面将提及的那样,Transformer编码器或Transformer解码器通常被单独用于不同的深度学习任务中。

            12.6 BERT预训练

            芝麻街的大门由此被打开🥵

            NLP里的迁移学习

            在NLP中,普通的nn.Embedding层与用word2vec嵌入有什么区别?

            普通的 nn.Embedding 层是一个可训练的查找表,用于将词索引映射到随机初始化的嵌入向量,而 word2vec 嵌入是通过无监督学习从大规模文本中预训练得到的固定向量。这意味着 nn.Embedding 在训练过程中可以更新嵌入,而 word2vec 的嵌入通常是静态的,不能在模型训练期间进一步调整。

            BERT的动机

            image-20240926084401201

            视频表述:https://www.bilibili.com/video/BV1yU4y1E7Ns?t=449.5

            BERT架构

            idea很简单,但是效果非常好

            对输入的修改

            image-20240926090012473

            当然可以做更多的句子,譬如一次性输入三条上下文,但是一般都采取两条就够了。

            注意最终的结果是把他们三层的结果按位加和在了一起。

            Token Embedding

            这个<cls>,有说法的,具体看 点击跳转

            Segment Embedding

            如果仅仅通过引入句子分隔符,对于transformer来说可能还不是很够,因此我们再引入SegmentEmbedding层来增加句子之间的区分。

            第一个句子的Segment是0,第二个句子为1。或者第一个句子给一个固定的向量,第二个也给一个固定的向量。

            Position Embedding

            原先的sin、cos的位置编码是不可学习的,这里不再用了,变成一个可以学的位置编码方式。

            预训练任务1:带掩码的语言模型

            80%mask用来训练模型有效抓去信息,10%用来提升鲁棒性,10%用来保持和微调时同样的分布

            预训练任务2:下一句子预测

            拓展:ELMo、GPT和BERT之间的差异

            ../_images/elmo-gpt-bert.svg

            代码实现-BERT模型本身

            这里我把BERT分为了本身的模型实现训练数据处理、以及到底怎么进行预训练这三部分代码

            1.输入表示

            2.BERTEncoder class

            注意上述三个

            假设词表大小为10000,为了演示BERTEncoder的前向推断,让我们创建一个实例并初始化它的参数:

            下面这两个与训练任务都是在BERT已经跑出结果的基础上进行⬇️

            3.掩蔽语言模型(Masked Language Modeling)

            使用样例:

            4.下一句预测(Next Sentence Prediction)

            使用样例:

            5.整合代码

            encoded_X 的形状是 (batch_size, seq_len, num_hiddens),其中:

            encoded_X[:, 0, :] 取的是所有样本在第一个时间步(即位置 0)对应的特征向量。这通常对应于序列中的 <cls> 标记,代表整个输入序列的聚合信息。

            代码实现-训练数据预处理

            1.WikiText-2数据集

            2.生成下一句预测任务的数据

            3.生成遮蔽语言模型任务的数据

            ... 这部分的代码实际上都是一些预处理,比较无聊,我这里就不继续写了,完整版跳转

            n.最终使用示例

            其实我是没太搞懂为什么还需要mlm_weights_X的,明明用一个pred_positions_X就可以了啊:

            image-20240926115525217

            代码实现-BERT预训练

            首先,我们加载WikiText-2数据集作为小批量的预训练样本,用于遮蔽语言模型和下一句预测。批量大小是512,BERT输入序列的最大长度是64。注意,在原始BERT模型中,最大长度是512。

            1.定义一个小的BERT,使用了2层、128个隐藏单元和2个自注意头。

            2.辅助函数,计算遮蔽语言模型和下一句子预测任务的损失

            3.训练

            函数的输入num_steps指定了训练的迭代步数,而不是像train_ch13函数那样指定训练的轮数

            ../_images/output_bert-pretraining_41429c_69_1.svg

            4.用BERT表示文本

            一个单句子的BERT特征提取

            考虑“a crane is flying”这句话。插入特殊标记“”(用于分类)和“”(用于分隔)后,BERT输入序列的长度为6。因为零是“”词元,encoded_text[:, 0, :]是整个输入语句的BERT表示。为了评估一词多义词元“crane”,我们还打印出了该词元的BERT表示的前三个元素。

            一个句子对的BERT特征提取

            12.7 BERT微调

            就是载入一个训练好的模型,给下游任务,继续训练

            🚩 BERT微调的时候,一般是不会固定预训练模型的参数的,固定会快,不固定效果会更好。

            BERT在实际部署的时候,一般搬到C++到后端。

            如果设备性能不够,可以通过 模型蒸馏 等技术将模型变成原本的十分之一(举例)大小

            微调Bert

            BERT对每一个词元返回抽取了上下文信息的特征向量

            不同的任务使用不同的特性

            image-20240926153102492

            对下面这几种应用场景的详细介绍:15.6. 针对序列级和词元级应用微调BERT — 动手学深度学习 2.0.0 documentation (d2l.ai)

            句子分类

            对应的向量输入到全连接层分类

            image-20240926153456965

            命名实体识别

            识别一个词元是不是命名实体,例如人名、机构、位置

            将非特殊词元放进全连接层分类

            ../_images/bert-tagging.svg

            问题回答

            给定一个问题,和描述文字,找出一个片段作为回答

            对片段中的每个词元预测它是不是回答的开头或结束

            ../_images/bert-qa.svg

            表述不清楚,可以看这个 自然语言处理:bert 用于问答系统_bert 问答-CSDN博客

            当使用BERT做问答时,找到答案的开始结束位置之后,中间的文本就是答案(目前可以就这么粗略的理解)

            代码实现-自然语言推理数据集

            1.斯坦福自然语言推理(SNLI)语料库

            2.读取数据集

            打印前3对前提和假设:

            0、1和2分别对应于“蕴涵”、“矛盾”和“中性”

            训练集约有550000对,测试集约有10000对。下面显示了训练集和测试集中的三个标签“蕴涵”“矛盾”和“中性”是平衡的。

            3.定义用于加载数据集的类

            代码通过 self.vocab[line] 查找词汇表中的词,如果词不在词汇表中,会自动处理为 <unk>(未知词)。

            4.整合代码

            现在我们打印第一个小批量的形状。与情感分析相反,我们有分别代表前提和假设的两个输入X[0]X[1]

            代码实现-Bert微调

            1.加载预训练的BERT

            2.加载预先训练好的BERT参数

            3.微调BERT的数据集

            大致看看得了

            生成训练和测试样本:

            4.微调BERT

            用于自然语言推断的微调BERT只需要一个额外的多层感知机,该多层感知机由两个全连接层组成(请参见下面BERTClassifier类中的self.hiddenself.output)。这个多层感知机将特殊的“”词元的BERT表示进行了转换,该词元同时编码前提和假设的信息为自然语言推断的三个输出:蕴涵、矛盾和中性。

            5.训练

            ../_images/output_natural-language-inference-bert_1857e6_102_1.svg

             


             

            13 优化算法

            之前都学过,这里只写一些需要注意的知识点

            1.凸函数

            image-20240926182456510

            这个概念容易混淆,记住上面这个才是凸函数,而不是凹函数。

            目前为止只有两个是凸的:

            2.冲量法

            梯度模拟物理中的动量

            冲量法使用平滑过的梯度对权重更新

            image-20240926183934438

            使用随机梯度下降:

            image-20240926184244740

            使用冲量法的随机梯度下降:

            image-20240926184316539

            基本上所有的SGD都有这个选项。

            3.Adam详解

            他对学习率远不如SGD敏感,做了相当多的平滑处理。

            https://www.bilibili.com/video/BV1bP4y1p7Gq?t=1674.8