transformer李宏毅讲解-笔记

2.2 输入部分实现

文本嵌入层

基础方法介绍

代码链接: embedding_test.py

embedding(词嵌入) 3.2.2-4.2.2

Embedding是一种将离散数据映射到连续向量空间中的技术,主要用于深度学习模型中。通过将离散数据映射到连续向量空间,可以使模型更好地处理这些数据,并提高模型的性能和准确性。

在自然语言处理领域中,embedding被广泛应用于文本分类、情感分析、问答系统等任务中。下面是一个简单的文本分类任务的embedding用法示例:

假设我们要对一组文本进行分类,可以使用Word2Vec模型对文本中的单词进行嵌入表示,即将每个单词映射到一个固定大小的向量。然后,将这些向量输入到一个神经网络模型中,进行分类预测。

具体步骤如下:

  1. 数据预处理:对文本数据进行预处理,包括去除停用词、分词等操作,得到每个单词的表示。
  2. 训练Word2Vec模型:使用Word2Vec模型对文本中的单词进行训练,得到每个单词的嵌入向量。
  3. 构建神经网络模型:使用一个神经网络模型(如卷积神经网络或循环神经网络)来处理嵌入向量,并进行分类预测。
  4. 训练模型:使用训练数据对神经网络模型进行训练,优化模型的参数。
  5. 测试和评估:使用测试数据对模型进行测试和评估,计算模型的准确率、召回率等指标。

除了Word2Vec模型外,还有许多其他的embedding方法,如GloVe、FastText等。这些方法都可以将离散数据映射到连续向量空间中,从而使得深度学习模型可以更好地处理这些数据。

一个简单的映射例子, onehot

One-hot编码,又称“独热编码”,是一种特殊的编码方式。在机器学习算法中,我们经常会遇到离散化的特征或标签。对于这些离散化的标签,如果仅仅对原始的离散标签进行编码,那么可能无法很好地利用这些标签的信息。因此,我们通常会将这些离散的标签转换为一种能够更好地表示它们之间关系的编码方式,这种编码方式就是one-hot编码。

具体来说,one-hot编码就是用N位状态寄存器来对N个状态进行编码,每个状态都由其独立的寄存器位,并且在任意时候只有一位有效。也就是说,对于N个不同的状态,我们使用N个二进制位来表示它们,其中只有一个二进制位为1,其余位都为0。通过这种方式,我们可以将离散的标签转换为一种连续的向量表示,从而更好地利用这些标签的信息。

One-hot编码的主要优点是可以将离散的标签转换为连续的向量表示,从而使得机器学习算法能够更好地处理这些标签。此外,one-hot编码还可以避免一些由于标签不均衡而导致的分类问题。但是,one-hot编码也存在一些缺点,例如它可能会占用大量的存储空间和计算资源,并且可能会引入一些噪声和冗余信息。因此,在实际应用中,我们需要根据具体情况选择是否使用one-hot编码。

import torch
from torch import nn

embedding = nn.Embedding(10, 3)
input = torch.LongTensor([[1,2,4,5],[4,3,2,9]])
print(embedding(input))
print(embedding.size())

在PyTorch的nn.Embedding模块中,第一个参数表示输入数据的最大索引值加1。也就是说,如果你有10个不同的输入,那么第一个参数应该是10。第二个参数代表输出的维度。这个维度决定了每个嵌入向量的长度。例如,如果第二个参数为2,那么每个输入的离散值将被映射到一个长度为2的连续向量。这个参数可以根据具体任务和数据来调整,以便更好地捕获数据的内在结构和关系。在这个例子中,nn.Embedding(10, 3)表示输入数据中的每个元素都是一个0到9的整数,这些整数将被映射到一个3维的向量空间中。

这段代码使用了PyTorch库中的nn.Embedding模块来创建一个嵌入层,并使用一个整数张量input作为输入进行前向传播。

首先,我们创建一个嵌入层embedding,它有10个词的词汇表(即10个不同的整数代表不同的单词或类别),每个单词被映射到一个3维的向量。

接下来,我们创建了一个名为input的整数张量,它有两个2维的序列,每个序列包含4个整数。这些整数代表词汇表中的单词。

然后,我们使用embedding层来对input进行前向传播,得到一个4x3的张量作为输出。每个整数都被替换为其对应的3维向量。

最后,我们打印出嵌入层的输出和其大小。

输出应该是:

tensor([[0.1713, 0.5346, 0.4774],
        [0.5346, 0.2438, 0.1713],
        [0.4774, 0.2438, 0.5346],
        [0.3782, 0.6218, 0.5346]])
tensor([4, 3])

第一个输出是一个4x3的张量,其中每个元素都是一个3维向量。第二个输出是嵌入层的大小,它是一个2维张量,表示嵌入层的维度。

对于一个M×N的输入 input, input的所有元素枚举数量为num_embeddings(最多有num_embeddings个不同的数字), 进行nn.Embedding(num_embeddings, embedding_dim)操作后, 得到一个M×N×embedding_dim的向量

embeddings类

代码实例

embedding.py

import torch
from torch import nn
import torch.nn.functional as F
from torch.autograd import Variable
import math
import matplotlib.pyplot as plt
import numpy as np
import copy


# 定义一个名为 "Embeddings" 的类,该类继承自 "torch.nn.Module"。
# 在 PyTorch 中,自定义的神经网络模块需要继承自 "torch.nn.Module"。
class Embeddings(torch.nn.Module):
    # d_model: 词映射的维度, nn.Embedding方法的 embedding_dim 参数
    # vocab: 词汇表的大小
    # 
    def __init__(self, d_model, vocab):
        # 调用父类 "torch.nn.Module" 的初始化方法。这是必需的,以确保实例可以正确地注册子模块等。
        super(Embeddings, self).__init__()
        # 创建一个嵌入层,将词汇表中的每个单词映射到一个维度为 "d_model" 的向量。
        # "vocab" 是词汇表的大小,即词汇表中的单词数量。
        self.lut = nn.Embedding(vocab, d_model)  # 创建一个嵌入层
        # 保存嵌入向量的维度 "d_model",以便在后续的计算中使用。
        self.d_model = d_model

        # 定义前向传播的方法。当对输入数据进行计算时,这个方法会被调用。
    def forward(self, x):
        return self.lut(x) * math.sqrt(self.d_model)  # 对输入的整数序列进行前向传播,得到嵌入向量序列


# 词嵌入维度是512维
d_model = 512
# 词表大小是1000
vocab = 1000
# 输入x是一个使用Variable封装的长整形张量, 形状是2x4
x = Variable(torch.LongTensor([[100,2,421,508], [491,998,1,221]]))

emb = Embeddings(d_model, vocab)
embr = emb(x)
print('embr: ', embr)
print('embr: ', embr.shape)

torch.randn

x = torch.randn(4,4) 这行代码是在使用PyTorch库生成一个4x4的张量(tensor),其中每个元素都是从标准正态分布(均值为0,标准差为1)中随机采样的。

具体来说:

  • torch.randn 是一个函数,用于从标准正态分布(也称为高斯分布)中随机采样。
  • (4,4) 是这个张量的形状(shape)。这意味着这个张量有4行和4列,总共16个元素。
  • x 是这个新生成的张量的变量名。

执行这行代码后,x 将是一个4x4的张量,其中的值都是随机的,且符合标准正态分布。

nn.Dropout

m=nn.Dropout(p=0.2)
input = torch.randn(4,5)
output=m(input)
print(output)

结果

tensor([[ 0.0000,  0.0000, -0.4830,  0.5161,  0.6801],
        [ 1.2099,  0.2645,  0.0000, -0.7044,  0.7474],
        [ 0.0000,  0.1686,  1.1160,  1.6692,  0.0000],
        [-1.3290, -0.1441,  0.4216, -0.0000, -1.9159]])

这段代码是使用PyTorch库来创建一个Dropout层,并对一个随机输入向量进行Dropout操作。

  1. m=nn.Dropout(p=0.2)

这行代码创建了一个Dropout层,其中p参数决定了在每次前向传播时随机关闭的单元的比例。在这个例子中,p=0.2意味着每次前向传播时,会有20%的单元被随机关闭。

  1. input = torch.randn(4,5)

这行代码创建了一个4x5的随机输入矩阵,其中每个元素都是从标准正态分布(均值为0,标准差为1)中随机采样的。

  1. output=m(input)

这行代码将上述随机输入矩阵传递给Dropout层。由于Dropout层会随机关闭输入矩阵中的某些元素,因此输出的矩阵中某些位置的值会是0(表示该位置的单元被随机关闭了)。

  1. print(output)

这行代码将输出矩阵打印到控制台。

总体来说,这段代码演示了如何使用Dropout层来随机关闭神经网络中的某些单元,这是为了防止神经网络过拟合的一种常见技巧。

torch.unsqueeze

unsqueeze 是一个在 PyTorch 中常用的函数,用于在指定维度上增加一个大小为 1 的维度。

函数定义
torch.unsqueeze(input, dim)
参数
  • input:输入的张量。
  • dim:在哪个维度上增加新的维度。
返回值

返回一个新的张量,该张量是在指定的维度上增加了一个大小为 1 的维度。其他维度的大小与输入张量相同。

例子
  1. 基本例子
import torch
a = torch.tensor([1, 2, 3])  # a is a 1D tensor: [1, 2, 3]
b = torch.unsqueeze(a, 0)     # b will be a 2D tensor: [[1], [2], [3]]
  1. 维度不匹配的例子
    如果你尝试在长度为3的一维张量上,在维度1上增加一个维度,你会得到一个形状为 (3, 1) 的二维张量。
a = torch.tensor([1, 2, 3])  # a is a 1D tensor: [1, 2, 3]
b = torch.unsqueeze(a, 1)     # b is a 2D tensor: [[1], [2], [3]]
  1. 在多维张量上使用
    你也可以在一个更高维度的张量上使用 unsqueeze。例如,对于一个形状为 (3, 4) 的二维张量:
a = torch.tensor([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])  # a is a 2D tensor: [3, 4]
b = torch.unsqueeze(a, 1)                                       # b is a 3D tensor: [3, 1, 4]

b的形状为(3, 1, 4),因为我们在维度1上增加了一个大小为1的维度。

总之,unsqueeze 可以用于增加指定维度的大小,并在该维度上赋予其值。

x = torch.tensor([1,2,3,4])
unsqueeze_res = torch.unsqueeze(x, 1)
print(unsqueeze_res)

结果:

tensor([[1],
        [2],
        [3],
        [4]])

这段代码是使用PyTorch库来创建一个张量,并对这个张量进行维度扩展。

  1. x = torch.tensor([1,2,3,4])

这行代码创建了一个一维张量x,其中包含四个元素:1, 2, 3和4。

  1. unsqueeze_res = torch.unsqueeze(x, 1)

这行代码对x进行了维度扩展。torch.unsqueeze函数会在指定的维度上增加一个维度。在这里,x原本是一个一维张量,维度大小为4。通过torch.unsqueeze(x, 1),我们在第二个维度(索引为1的维度)上增加了一个新的维度,使得新的张量unsqueeze_res的形状变为(4, 1)

  1. print(unsqueeze_res)

这行代码将打印扩展后的张量unsqueeze_res

执行这段代码后,输出将是:

tensor([[1],
        [2],
        [3],
        [4]])

可以看到,原来的四个元素现在被组织成一个2x2的矩阵,其中第一维的大小为4(与原来一致),第二维的大小为1(新增加的维度)。

x= torch.tensor([1,2,3,4])
y= torch.unsqueeze(x,0)
print(y.shape, y)
z =torch.unsqueeze(x,1)
print(z.shape, z)

这段代码中,我们首先创建了一个一维张量 x,然后使用 torch.unsqueeze 函数来扩展其维度。

  1. x = torch.tensor([1,2,3,4]):这行代码创建了一个一维张量 x,其中包含四个元素:1, 2, 3和4。
  2. y = torch.unsqueeze(x, 0):这行代码对一维张量 x 进行维度扩展。torch.unsqueeze 函数会在指定的维度上增加一个维度。在这里,x 是一个一维张量,维度大小为4。通过 torch.unsqueeze(x, 0),我们在第一个维度(索引为0的维度)上增加了一个新的维度,使得新的张量 y 的形状变为 (1, 4)。执行结果如下:
  • y.shape 的输出是 (1, 4),表示张量 y 的形状是 1 行 4 列。
  • y 的输出是:
tensor([[1, 2, 3, 4]])

可以看到,原来的四个元素现在被组织成一个1x4的矩阵。

  1. z = torch.unsqueeze(x, 1):这行代码同样对一维张量 x 进行维度扩展。与上一行代码类似,通过 torch.unsqueeze(x, 1),我们在第二个维度(索引为1的维度)上增加了一个新的维度,使得新的张量 z 的形状变为 (4, 1)。执行结果如下:
  • z.shape 的输出是 (4, 1),表示张量 z 的形状是 4 行 1 列。
  • z 的输出是:
tensor([[1],
[2],
[3],
[4]])`
可以看到,原来的四个元素现在被组织成一个4x1的矩阵。

总结:这段代码演示了如何使用 PyTorch 的 torch.unsqueeze 函数来对一维张量进行维度扩展,并在指定的维度上增加一个大小为1的维度。

(原来x的维度在第0个,
y=torch.unsqueeze(x, 0), 在第0个维度上新增了一个维度, 原来的第0个维度移动到第1个维度, 所以变成了(1,4)维度的张量,
z=torch.unsqueeze(x, 1), 在第1个维度上新增了一个维度, 原来的第0个维度不变, 所以变成了(4,1)维度的张量,

view

改变张量形状

y = x.view(16)
这行代码是将张量x重新塑形(reshape)为一个包含16个元素的1D张量。

具体来说:

  • x.view() 是PyTorch中用于改变张量形状的方法。
  • (16) 指定了新的形状。因为原始张量x是一个4x4的2D张量,所以它包含16个元素。
  • y 是这个新生成的1D张量的变量名。

执行这行代码后,y 将是一个1D张量,其中包含原始张量x中的所有16个元素,但是其形状已经被改变。
view 方法的参数主要包括:

  1. 必需参数
  • size:目标张量的形状。它是一个表示新形状的整数或元组。如果这个参数与原始张量的总元素数量不匹配,将会抛出错误。
  1. 可选参数
  • strides:目标张量的步长(strides)。它是一个表示新步长的整数或元组。默认值是 None,表示使用原始张量的步长。
  • dtype:目标张量的数据类型。默认值是原始张量的数据类型。
  • device:目标张量应存储在哪个设备上(例如CPU或GPU)。默认值是 None,表示使用原始张量的设备。
  • requires_grad:一个布尔值,表示是否需要为新张量计算梯度。默认值是 False

指定方式示例:

  • 创建一个形状为 (16,) 的1D张量:
y = x.view(16)
  • 创建一个形状为 (4,4) 的2D张量:
y = x.view(4, 4)
  • 指定步长:
y = x.view(16, stride=4)  # 假设x是一个8x4的张量,那么新的y将有步长为4
  • 指定数据类型:
y = x.view(16, dtype=torch.float32)  # 将所有元素转换为float32类型
  • 将张量移至GPU上:
y = x.view(16, device=torch.device('cuda'))  # 如果x原本在CPU上,现在y将在GPU上

transpose

在指定维度上转置张量

a = torch.randn(1,2,3,4)
print(a.size(), a)
b = a.transpose(1,2)
print(b)

首先,让我们分析代码中每一步的作用:

  1. a = torch.randn(1,2,3,4):这行代码创建了一个4维的张量a,其形状为(1,2,3,4)。张量中的元素是随机生成的,遵循标准正态分布(均值为0,标准差为1)。
  2. print(a.size(), a):这行代码首先打印出张量a的形状,然后打印出张量a的内容。
  3. b = a.transpose(1,2):这行代码对张量a进行转置操作。具体来说,它交换了第2维(索引为1的维度)和第3维(索引为2的维度)。所以,原来的形状(1,2,3,4)会变为(1,3,2,4)
  4. print(b):这行代码打印出转置后的张量b的内容。

现在,我们来具体解释每一部分:

  • a.size():这将返回一个表示张量a形状的元组,即(1,2,3,4)
  • a:这将打印出张量a的内容。由于这是一个四维张量,并且每一维的大小都不同,因此输出的内容会是一系列嵌套的列表,表示各个维度的大小。
  • b:这将打印出转置后的张量b的内容。由于b是通过对第2维和第3维进行转置得到的,所以输出的内容会有所不同。具体来说,原始张量中在第2维的元素(索引为1的维度)现在会出现在第3维的位置(索引为2的维度),而原始张量中在第3维的元素现在会出现在第2维的位置。

这样,你就能清楚地看到transpose操作是如何改变张量的维度顺序的。

view和transpose对比

a = torch.randn(1,2,3,4)
print(a.size(), a)
b = a.transpose(1,2)
print(b.size(), b)
c = a.view(1,3,2,4)
print(c.size(), c)

让我们一步步分析这段代码:

  1. a = torch.randn(1,2,3,4)
    这行代码创建了一个形状为 (1,2,3,4) 的四维张量 a。每个元素都是从标准正态分布中随机采样的。
  2. print(a.size(), a)
    这行代码打印了张量 a 的形状和内容。由于 a 的形状是 (1,2,3,4),所以输出的 a.size() 将是 (1,2,3,4),而 a 的内容是该形状的随机数矩阵。
  3. b = a.transpose(1,2)
    这行代码对张量 a 进行转置。具体来说,它交换了第2维(索引为1)和第3维(索引为2)。因此,b 的形状将从 (1,2,3,4) 变为 (1,3,2,4)
  4. print(b.size(), b)
    这行代码打印了转置后的张量 b 的形状和内容。输出的 b.size() 将是 (1,3,2,4),而 b 的内容是该形状的转置矩阵。
  5. c = a.view(1,3,2,4)
    这行代码改变了张量 a 的形状,将其重新塑形为一个形状为 (1,3,2,4) 的四维张量。注意,这里与之前的转置操作不同,view 方法不会改变张量中的元素,只是改变了它们的布局方式。因此,c 和原始的 a 将包含相同的元素,只是它们的排列顺序不同。
  6. print(c.size(), c)
    这行代码打印了重新塑形后的张量 c 的形状和内容。输出的 c.size() 将是 (1,3,2,4),而 c 的内容是该形状的矩阵。

总结:这段代码展示了如何在 PyTorch 中创建、转置和重新塑形四维张量。通过这些操作,你可以改变张量的维度顺序和大小,而不改变其包含的元素。

deepcopy

def clones(module, N):
    return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])

这个函数clones用于创建一个包含多个克隆模块的列表。让我们逐步解释这个函数:

  1. 输入参数:
  • module: 这是要克隆的原始模块。
  • N: 表示我们想要克隆module多少次。
  1. 函数功能:
  • 使用列表推导式,函数会创建Nmodule的深度复制(deep copy)。这意味着每个克隆模块都是原始模块的一个完全独立的副本,它们之间的任何更改都不会相互影响。
  • nn.ModuleList是一个特殊的PyTorch容器,它用于存储模块列表,并确保当这个容器被传递给其他函数或方法时,其内容(即模块)也被传递,而不是只传递引用。这对于确保模块的独立性非常有用。
  1. 返回值:
  • 返回一个包含N个克隆模块的nn.ModuleList

简单地说,这个函数允许您轻松地创建多个独立副本的特定模块,这对于某些神经网络结构(例如,复制相同的网络层多次)是非常有用的。

代码解释

这段代码定义了一个名为MultiHeadedAttention的PyTorch神经网络模块,该模块实现了多头注意力机制。下面是对代码的逐行解释:

import copy  # 导入copy模块,用于深度复制对象。
import attention  # 导入attention模块,这个模块中应该定义了实现注意力机制的函数或类。
from torch import nn  # 从torch库中导入神经网络模块nn。

def clones(module, N):  # 定义一个名为clones的函数,用于复制一个模块N次。
    return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])

class MultiHeadedAttention(nn.Module):  # 定义一个名为MultiHeadedAttention的类,继承自nn.Module。
    def __init__(self, head, embedding_dim, dropout=0.1):  # 初始化方法,用于设置模型的参数。
        super(MultiHeadedAttention, self).__init__()  # 调用父类的初始化方法。
        assert embedding_dim % head == 0, "Embedding dimension must be divisible by number of heads."  # 断言:嵌入维度必须能被头数整除。
        self.d_k = embedding_dim // head  # 计算每个头的维度(每个头的特征数)。
        self.head = head  # 记录头数。
        self.embedding_dim = embedding_dim  # 记录嵌入向量的维度。
        self.linears = clones(nn.Linear(embedding_dim, embedding_dim), 4)  # 使用clones函数复制线性层4次,创建4个线性层。
        self.attn = None  # 初始化注意力权重为None。
        self.dropout = nn.Dropout(p=dropout)  # 创建dropout层,用于防止过拟合。

    def forward(self, query, key, value, mask=None):  # 定义前向传播方法。
        if mask is not None:  # 如果提供了遮盖(mask):
            mask = mask.unsqueeze(1)  # 扩展mask的维度。
        batch_size = query.size(0)  # 获取查询向量的第一个维度的大小,即批次大小。
        query, key, value = [model(x).view(batch_size, -1, self.head, self.d_k).transpose(1, 2) for model, x in zip(self.linears, query, key, value)]  # 对查询、键、值向量进行变换和整形。
        x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)  # 调用attention函数计算输出向量和注意力权重。
        x = x.transpose(1, 2).contiguous().view(batch_size, -1, self.head * self.d_k)  # 对输出向量进行转置、连接和整形。
        return self.linears[-1](x)  # 使用最后一个线性层对输出向量进行变换,并返回结果。

这个代码实现了一个多头注意力机制的类,其中使用了四个线性层(self.linears),这些线性层可能用于实现查询、键、值和输出变换。在前向传播方法中,对输入的查询、键、值向量进行了变换和整形,然后调用了attention函数进行多头注意力计算,最后对输出向量进行了转置和整形操作。

PositionalEncoding 5.2.2-6.2.2

位置编码器

因为在transformer的编码器结构中,并没有针对词汇位置信息的处理,因此需要在 Embedding层后加入位置编码器,将词汇位置不同可能会产生不同语义的信息加入到词嵌入张量中,以弥补位置信息的缺失

positional_encoding.py

class PositionalEncoding(nn.Module):
    def __init__(self, embedding_dim, dropout, max_len=5000):
        super(PositionalEncoding, self).__init__()
        self.embedding_dim = embedding_dim
        self.max_len = max_len
        self.dropout = nn.Dropout(p=dropout)
        
        pe = torch.zeros(max_len, embedding_dim)
        # 绝对位置矩阵初始化之后,接下来就是考虑如何将这些位置信息加入到位置编码矩阵中,最简单思路就是先将ma×_len×1
        # 的绝对位置矩阵,变换成max_len×d_model形状,然后覆盖原来的初始位置编码矩阵即可
        # 要做这种矩阵变换,就需要一个1xd_model形状的变换矩阵div_term, 我们对这个变换矩阵的要求除了形状外
        # 还希望它能够将自然数的绝对位置编码缩放成足够小的数字,有助于在之后的梯度下降过程中更快的收敛, 这样我们就可以开始初始化
        # 首先使用arange获得一个自然数矩阵,但是细心的同学们会发现,我们这里并没有按照预计的一而是有了一个跳跃,只初始化了一半即1xd_mode1 / 2的矩阵。为什么是一半呢,其实这里并不是
        # 我们可以把它看作是初始化了两次,而每次初始化的变换矩阵会做不同的处理,第一次初始化的变护并把这两个矩阵分别填充在位置编码矩阵的偶数和奇数位置上,组成最终的位置编码矩阵
        position = torch.arange(0, max_len).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, embedding_dim, 2) * -(math.log(10000.0) / embedding_dim))
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        pe = pe.unsqueeze(0)
        self.register_buffer('pe', pe)
        
    def forward(self, x):
        x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)
        return self.dropout(x)


# x = res
dropout = 0.1
max_len = 60
# pe = PositionalEncoding(d_model, dropout, max_len)
# pe_result = pe(x)
# print(pe_result)
# print(pe_result.shape)

这段代码定义了一个名为PositionalEncoding的PyTorch模块,它用于给输入的序列添加位置编码。位置编码是一种在Transformer模型中使用的技术,用于捕获序列中元素的位置信息。

以下是代码的详细解释:

  1. 初始化函数 (__init__):
  • embedding_dim: 嵌入的维度。
  • dropout: dropout的比率。
  • max_len: 序列的最大长度。
  • pe: 用于存储计算出的位置编码的张量。
  • position: 生成一个从0到max_len-1的张量,并为其增加一个维度。
  • div_term: 用于控制正弦和余弦函数的频率。
  • 通过正弦和余弦函数计算位置编码,并将其存储在pe中。最后,将pe添加为一个模块的不可训练的缓冲区。
  1. 前向传播函数 (forward):
  • x: 输入的序列。
  • 通过将输入xpe相加,为其添加位置编码。
  • 应用dropout层。
  • 返回经过位置编码和dropout处理的序列。

在Transformer模型中,这种位置编码技术使得模型能够理解输入序列中元素的位置信息,这对于一些需要理解序列中元素顺序的任务(如机器翻译)是非常重要的。

在人工智能(AI)领域,特别是在自然语言处理(NLP)和机器学习中,位置编码(Positional Encoding)是一种重要的技术,尤其在处理序列数据时。位置编码的主要目的是为了让模型能够理解序列中元素的位置信息,因为许多模型(如Transformer)由于其固有的结构特性,在处理输入序列时会丢失位置信息。

在自然语言处理任务中,如机器翻译或文本生成,单词的顺序和位置对理解句子的含义至关重要。例如,“我爱你”和“你爱我”虽然包含相同的单词,但由于单词顺序的不同,它们的意思完全不同。

在Transformer模型中,位置编码通常是通过向输入嵌入(Input Embeddings)添加额外的位置嵌入(Positional Embeddings)来实现的。这些位置嵌入通常是固定大小的向量,它们被添加到相应的输入嵌入中,以便模型能够区分不同位置的单词。

位置编码可以通过多种方式实现,但最常见的方法之一是使用正弦和余弦函数。在这种方法中,每个位置都被赋予一个独特的向量,该向量由一系列正弦和余弦波的值组成。这些波的频率和相位是根据位置在序列中的索引来计算的。通过这种方式,模型能够捕获到单词之间的相对位置信息,而不仅仅是它们的绝对位置。

总的来说,位置编码在AI领域的作用是帮助模型理解输入序列中元素的位置信息,从而提高其在各种NLP任务中的性能。

绘制词向量特征分布曲线 7.2.2

import matplotlib.pyplot as plt
import numpy as np
import torch
from torch.autograd import Variable

from transformer.positional_encoding import PositionalEncoding
#创建一张15×5大小的画布
plt.figure(figsize=(15,5))
#实例化PositionalEncoding类得到pe对象,输入参数是2g和g
pe = PositionalEncoding(20,0.05)
# 然后向pe传入被Variable封装的tensor,这样pe会直接执行forward函数,
# 且这个tensor里的数值都是0,被处理后相当于位置编码张量
y = pe(Variable(torch.zeros(1,100,20)))
plt.plot(np.arange(100), y[0, :, 4:8].data.numpy())
#然后定义画布的横纵坐标,横坐标到100的长度,纵坐标是某一个词汇中的某维特征在不同长度下对应的值
# 因为总共有20维之多,我们这里只查看4,5,6,7维的值.
plt.legend(["dim %d"% p for p in [4,5,6,7]])
plt.show()

pe_test.py

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

输出效果分析:

每条颜色的曲线代表某一个词汇中的特征在不同位置的含义.

保证同一词汇随着所在位置不同它对应位置嵌入向量会发生变化.

正弦波和余弦波的值域范围都是1到-1这又很好的控制了嵌入数值的大小,有助于梯度的快速计算.

小结

  • 学习了文本嵌入层的作用

    • 无论是源文本嵌入还是目标文本嵌入,都是为了将文本中词汇的数字表示转变为向量
      表示,希望在这样的高维空间捕捉词汇间的关系
      学习并实现了文本嵌入层的类:Embeddings
    • 初始化函数以d_model,词嵌入堆度,和vocab,词汇总数为参数,内部主要使用了nn中的
      预定层Embedding进行词嵌入.
      在forward函数中,将输入x传入到Embedding的实例化对象中,然后乘以一个根号下
      dmode进行缩放控制数值大小,·它的输出是文本嵌入后的结果,
  • 学习了位置编码器的作用:

    • 因为在Transformer的编码器结构中,并没有针对词汇位置信息的处理。因此需要在
      Embedding层后加入位置编码器,将词汇位置不同可能会产生不同语义的信息加入到
      词嵌入张量中,以弥补位置信息的缺失
  • 学习并实现了位置编码器的类:PositionalEncoding

    • 初始化函数以d_model,dropout,max_len为参数,分别代表d_model:词嵌入维度
      dropou比置0比率,nax_len:每个句子的最大长度
      forward函数中的输入参数为x,是Embedding层的输出
      最终输出一个加入了位置编码信息的词嵌入张量,
    • 实现了绘制调汇向量中特征的分布曲线
    • 保证同一词汇随着所在位置不同它对应位置嵌入向量会发生变化
      正弦波和余弦波的值域范围都是1到1,这又很好的控制了骸入数值的大小,有助于梯度
      的快速计算

2.3 编码器部分实现

P9 9.2.3.1

  • 编码器部分:
    • 由N个编码器层堆叠而成
    • 每个编码器层由两个子层连接结构组成
    • 第一个子层连接结构包括一个多头自注意力子层和规范化层以及一个残差连接
    • 第二个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连援

2.3.1 掩码张量

·了解什么是掩码张量以及它的作用
,掌生成码张量的实现过程
·什么是掩码张量:
·撞代表遍掩,码就是我们张量中的数值,它的尺寸不定。里面一般只有1和0的元素,代表
位置被遍掩或者不被遮掩,至于是0位置被遮掩还是1位置被遍掩可以自定义,因此它的作
用就是让另外一个张量中的一些数值被遮掩,也可以说被替换它的裹现形式是一个张量。
掩码张量的作用:
·在transformer中,掩码张量的主要作用在应用attention(将在下一小节讲解时.有一些生
成的attention张量中的值计算有可能已知了未来信息而得到的,未来信息被看到是因为训
练时会把整个输出结果都一次性进行Embedding,但是理论上解码器的的输出却不是一次
就能产生最终结果的,而是一次次通过上一次结果综合得出的,因此,未来的信息可能被
提前利用,所以,我们会进行遮掩,关于解码器的有关知识将在后面的章节中讲解

基础方法介绍

np.triu

mask.py

np.triu 是 NumPy 库中的一个函数,用于生成一个上三角矩阵。这个函数返回一个数组,该数组的上三角部分(包括主对角线)包含1,而下三角部分(不包括主对角线)包含0。

函数的语法如下:

import numpy as np

a = [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]
print(np.triu(a, k=0))
print(np.triu(a, k=1))
print(np.triu(a, k=-1))

[[1 2 3]
 [0 5 6]
 [0 0 9]
 [0 0 0]]
 
[[0 2 3]
 [0 0 6]
 [0 0 0]
 [0 0 0]]
 
[[ 1  2  3]
 [ 4  5  6]
 [ 0  8  9]
 [ 0  0 12]]

其中:

  • a 是输入的数组或矩阵。
  • k 是指定下标偏移量。如果 k 是正数,那么下三角部分(不包括主对角线)将包含1。如果 k 是负数,那么上三角部分(包括主对角线)将包含0。如果 k 是0,那么结果就是一个上三角矩阵。

所以,np.triu函数的主要用途是生成一个上三角矩阵,它可以用在各种数学和科学计算中,特别是在需要处理矩阵运算的场合。

代码介绍

定义下三角矩阵

mask.py

可视化

def subsequent_mask(size):
    attn_shape = (1, size, size)
    subsequent_mask = np.triu(np.ones(attn_shape), k = 1).astype('uint8')
    return torch.from_numpy(1 - subsequent_mask)

size = 5
sm = subsequent_mask(size)
print('sm: ', sm)

plt.figure(figsize=(5,5))
plt.imshow(subsequent_mask(20)[0])
plt.show()

2.3.1掩码张量总结

·学习了什么是掩码张量:
·掩代表遮掩,码就是我们张量中的数值,它的尺寸不定,里面一般只有1和0的元素,
代表使置被遮掩或者不被遮掩,至于是0位置被遮掩还是1位置被遮掩可以自定义,因
此它的作用就是让另外一个张量中的一些数值被遮掩,也可以说被替换,它的表现形式
是一个张量
·学习了掩码张量的作用:
·在transformer中,掩码张量的主要作用在应用attention(将在下一小节讲解)时,有一些
生成的attetion张量中的值计算有可能已知量未来信息而得到的,未来信息被看到是
因为训练时会把整个输出结果都一次性进行Embedding,.但是理论上解码器的的输出
却不是一次就能产生最终结果的,而是一次次通过上一次结果综合得出的,因此,未
来的信息可能被提前利用,所以,我们会进行遮掩.关于解码器的有关知识将在后面的
章节中讲解
·学习并实现了生成向后遮掩的撞码张量函数:subsequent mask
·它的输入是size,代表掩码张量的大小
·它的输出是一个最后两维形成1方阵的下三角阵
·最后对生成的掩码张量进行了可视化分析,更深一步理解了它的用途

2.3.2注意力机制

·学习目标
·了解什么是注意力计算规则和注意力机制
掌握注意力计算规则的实现过程
·什么是注意力:
我们观察事物时,之所以能够快速判断一种事物(名然允许判断是错误的),是因为我们大脑能够很快把注意力放在事物最具有辨识度的部分从而作出判断,而并非是从头到尾的观察一遍事物后,才能有判断结果.正是基于这样的理论,就产生了注意力机制
·什么是注意力计算规则:
·它需要三个指定的输入Q(query),Kkey),V(value),然后通过公式得到注意力的计算结果,这个结果代表quey在key和value作用下的表示.而这个具体的计算规则有很多种

这里只介绍我们用到的这一种我门这里使用的注意力的计算规则

A t t e n t i o n ( Q , K , V ) = s o f t m a x ( Q K T x ) V Attention(Q,K,V) = softmax(\frac{QK^T}{\sqrt{x}})V Attention(Q,K,V)=softmax(x QKT)V

·什么是注意力机制
·注意力机制是注意力计算规则能够应用的深度学习网络的载体,除了注意力计算规则外,还包括一些必要的全连接层以及相关张量处理,使其与应用网络融为一体使用自注意力计算规则的注意力机制称为自注意力机制
·注意力机割在网络中实现的图形表示:

基础方法介绍

masked_fill

mask_fill_test.py

import torch
from torch.autograd import Variable

x = Variable(torch.randn(5,5))
print(x)
mask = Variable(torch.zeros(5,5))
print(mask)
y = x.masked_fill(mask==0, 1e-9)
print(y)

这段代码使用PyTorch库来创建一个随机的5x5张量(矩阵)x,然后创建另一个5x5全零的张量mask。最后,它使用masked_fill()函数将mask中所有为0的位置在x中的值替换为非常接近0的值(1e-9)。

具体来说:

  1. x = Variable(torch.randn(5,5)): 这行代码创建一个5x5的张量x,其中的元素是从标准正态分布(均值为0,标准差为1)中随机采样的。
  2. mask = Variable(torch.zeros(5,5)): 这行代码创建一个5x5的张量mask,其中的元素都是0。
  3. y = x.masked_fill(mask==0, 1e-9): 这行代码使用masked_fill()函数来修改张量x。这个函数会查找与掩码mask中所有为0的位置对应的元素,并将这些元素的值替换为1e-9。因为掩码中所有元素都是0,所以这实际上会将张量x中的所有元素替换为1e-9。
  4. print(x), print(mask), print(y): 这些print语句将打印出xmasky的值。

需要注意的是,虽然这段代码可以运行,但现在已经不再推荐使用Variable了。在较新版本的PyTorch中,你应该使用Tensor而不是Variable。另外,对于掩码操作,通常推荐使用布尔索引而不是masked_fill()。以下是使用Tensor和布尔索引的等效代码:

import torch

x = torch.randn(5, 5)
mask = torch.zeros(5, 5)
y = x.clone()  # 创建一个x的副本以避免修改原始数据

y[mask == 0] = 1e-9  # 使用布尔索引来替换值

print(x)
print(mask)
print(y)

代码介绍

attention

attention_func.py

import torch
import math

import torch.nn.functional as F
from torch.autograd import Variable

from transformer.positional_encoding import pe_result


def attention(query, key, value, mask=None, dropout=None):
    d_k = query.size(-1)
    scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
    if mask is not None:
        scores = scores.masked_fill(mask == 0, 1e-9)

    p_attn = F.softmax(scores, dim=-1)
    if dropout is not None:
        p_attn = dropout(p_attn)

    return torch.matmul(p_attn, value), p_attn

query = key = value = pe_result
attn, p_attn = attention(query, key, value)
print('attn:', attn)
print(attn.shape)
print('p_attn:', p_attn)
print(p_attn.shape)

mask = Variable(torch.zeros(2,4,4))
attn, p_attn = attention(query, key, value, mask=mask)
print('attn:', attn)
print(attn.shape)
print('p_attn:', p_attn)
print(p_attn.shape)

这段代码实现了多头注意力机制的一个基本版本。我会为你逐步解释每一部分。

  1. 导入库:

    • torchmath:这是Python的内置库,用于数学运算。
    • torch.nn.functional:提供了许多神经网络操作。
    • torch.autograd.Variable:用于自动微分。但在较新版本的PyTorch中,直接使用Tensor即可,因此这部分可能是过时的。
    • transformer.positional_encoding 中的 pe_result:这似乎是一个位置编码的结果,但代码中没有给出具体实现。
  2. attention函数:

  • 输入参数:
  • query: 查询向量。
  • key: 键向量。
  • value: 值向量。
  • mask: 一个掩码,用于指示哪些位置是有效的、哪些位置应该被忽略。
  • dropout: 一个dropout层,用于防止过拟合。
  • 功能:
  • 首先,计算查询和键之间的分数,分数是通过矩阵乘法得到的,然后通过缩放(/ math.sqrt(d_k))进行归一化。
  • 如果提供了掩码,则将分数中掩码为0的位置设置为非常小的值(-1e9)。
  • 使用softmax函数对分数进行归一化,得到注意力权重。
  • 如果提供了dropout层,则应用dropout。
  • 输出:
  • 输出的第一个值是加权的值向量。
  • 输出的第二个值是注意力权重。
  1. 使用attention函数:
  • 初始化query, key, 和 valuepe_result
  • 调用attention函数并打印输出。
  1. 掩码的使用:
  • 创建一个2x4x4的零张量作为掩码。
  • 使用这个掩码再次调用attention函数并打印输出。

总结:这段代码展示了如何使用多头注意力机制的基本版本。通过这个机制,模型可以聚焦于输入中的不同部分来生成输出。

2.3.2注意力机制总结

·学习了什么是注意力:·我们观察事物时,之所以能够快速判断一种事物(当然允许判断是错误的),是因为我们大脑能够很快把注意力放在事物最具有拆识度的部分从而作出判断,而并非是从头到尾的观察一遍事物后,才能有判断结果.正是基于这样的理论。就产生了注意力机制

什么是注意力计算规则:

它需要三个指定的输入Q(query),Kkey),V(value),然后通过公式得到注意力的计算结果,这个结果代表quey在key和valuet作用下的表示.而这个具体的计算规则有很多种,我这里只介绍我们用到的这一种

学习了Q,KV的比编解释

Q是一段准备被概括的文本:

K是给出的提示:

V是大脑中的对提示K的延伸

当Q=K=V时,称作自注意力机制

什么是注意力机制

注意力机制是注意力计算规则能够应用的深度学习网络的载体,除了注意力计算规则外,还包括一些必要的全连接层以及相关张量处理,使其与应用网络融为一体

使用自注意力计算规则的注意力机制称为自注意力机制

·学习并实现了注意力计算规则的函数:attention

它的输入就是Q,K,V以及mask和dropout,

mask用于掩码

dropout用于随机置0.

它的输出有两个,quey的注意力表示以及注意力张量

2.3.3 多头注意力机制

·学习目标

·了解多头注意力机制的作用

·掌握多头注意力机制的实现过程

·什么是多头注意力机制:

从多头注意力的结构图中,貌似这个所谓的多个头就是指多组线性变换层,其实并不是,我只有使用了一组线性变化层,即三个变换张量对Q,K,V分别进行线性变换,这些变换不会改变原有张量的尺寸,因此每个变换矩阵都是方阵,得到输出结果后,多头的作用才开始显现,每个头开始从词义层面分割输出的张量,也就是每个头都想获得一组Q,K,V进行注意力机制的计算,但是句子中的每个词的表示只获得一部分,也就是只分割了最后一维的词嵌入向量.这就是所谓的多头,将每个头的获得的输入送到注意力机制中,就形成多头注意力机制

多头注意力机制结构:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

·多头注意力机制的作用:

·这种结构设计能让每个注意力机制去优化每个词汇的不同特征部分,从而均衡同一种注意力机制可能产生的偏差,让词义拥有来自更多元的表达,实验表可以从而提升模型效果

基本方法

contiguous

让 transpose之后的张量可以执行view方法

xx.transpose(1,2).contiguous().view(batch_size,-1,self.head self.d_k)

代码讲解

#我们使用一个类来实现多头注意力机制的处理
class MultiHeadedAttention(nn.Module):
    def __init__(self,Thead,embedding_dim,dropout=0.1):
        '''在类的初始化时,会传入三个参数,head代表头数,embedding._dim代表词嵌入的维度,    dropout代表进行dropout操作时置0比率,默认是0.1.'''
        super(MultiHeadedAttention,self).__init__()
        #在函数中,首先使用了一个测试中常用的assert语句,判断h是否能被d_model整除,
        #这是因为我们之后要给每个头分配等量的词特征.也就是embedding_dim/head个.
        assert embedding_dim head == 0
        #得到每个头获得的分割词向量维度d_k
        self.d_k =embedding_dim /head
        #传入头数h
        self.head= head
        #然后获得线性层对象,通过nn的Linear?实例化,它的内部变换矩阵是embedding_dim x embedc
        #为什么是四个呢,这是因为在多头注意力中,Q,K,V各需要一个,最后拼接的矩阵还需要一个,因
        self.linears = clones(nn.Linear(embedding_dim,embedding_dim),4)
        #self.attn为None,它代表最后得到的注意力张量,现在还没有结果所以为None.
        self.attn = None
        #最后就是一个self.dropout对象,它通过nn中的Dropout实例化而来,置0比率为传进来的参数d
        self.dropout = nn.Dropout(p=dropout)
    def forward(self,query,key,value,mask=None):
        '''前向逻辑函数,它的输入参数有四个,前三个就是注意力机制需要的Q,K,V,
        最后一个是注意力机制中可能需要的mask掩码张量,默认是None,'''
        #如果存在掩码张量mask
        if mask is not None:
        #使用unsqueeze拓展维度,代表多头中的第n头
        mask = mask.unsqueeze(1)
        #接着,我们获得一个batch_size的变量,他是query尺寸的第1个数字,代表有多少条样本.
        batch_size query.size(0)
        #之后就进入多头处理环节
        #首先利用z1p将输入QKV与三个线性层组到一起,然后使用for循环,将输入QKV分别传到线性层中,
        #做完线性变换后,开始为每个头分割输入,这里使用v1w方法对线性变换的结果进行维度重塑,多】
        #这样就意味着每个头可以获得一部分词特征组成的句子,其中的-1代表自适应维度,
        #计算机会根据这种变换自动计算这里的值.然后对第二维和第三维进行转置操作,
        #为了让代表句子长度维度和词向量维度能够相邻,这样注意力机制才能找到词义与句子位置的关系,
        #从attention函数中可以看到,利用的是原始输入的倒数第一和第二维.这样我们就得到了每个头的
        query,key,value =\
        [model(x).view(batch_size,-1,self.head,self.d_k).transpose(1,2)
            for model,x in zip(self.linears,(query,key,value))]

        #得到每个头的输入后,接下来就是将他们传入到attention中,
        #这里直接调用我们之前实现的attention函数.同时也将mask和dropout传入其中.
        x,self.attn = attention(query,key,value,mask=mask,dropout=self.dropout
        #通过多头注意力计算后,我们就得到了每个头计算结果组成的4维张量,我们需要将其转换为输入的
        #因此这里开始进行第一步处理环节的逆操作,先对第二和第三维进行转置,然后使用cont1 guous方
        #这个方法的作用就是能够让转置后的张量应用v1w方法,否则将无法直接使用,
        #乐以下一生部是使甲w香组形,交成和岭入形担月
        xx.transpose(1,2).contiguous().view(batch_size,-1,self.head self.d_k)
        #最后使用线性层列表中的最后一个线性层对输入进行线性变换得到最终的多头注意力结构的输出,
        return self.linears[-1](x)

2.3.3 多头注意力机制总结

·学习了什么是多头注意力机制明
·每个头开始从词义层面分割输出的张量,也就是每个头都想获得一组Q,K,V进行注
意力机制的计算,但是句子中的每个词的表示只获得一部分,也就是只分剂了最后一
维的词嵌入向量.这就是所谓的多头,将每个头的获得的输入送到注意力机制中,就形成
类多头注意力机制,
·学习了多头注意力机制的作用:
·这种结构设计能让每个注意力机制去优化每个词汇的不同特部分,从而均衡同一种
注意力机制可能产生的偏差,让词义拥有来自更多元的表达,实验表明可以从而提升
模型效果
·学习并实现了多头注意力机割的类:MultiHeadedAttention
·因为多头注意力机制中需要使用多个相同的线性层,首先实现了克隆函数clones.
clones函数的输入是nodule,N,分别代表克隆的目标层,和克隆个数
·clones函数的输出是装有N个克隆层的Module列表
·接着实现MultiHeadedAttention类,它的初始化函数输入是h,d_model,dropout分别代
表头数,词嵌入维度和置零比率
·它的实例化对象输入是Q,K,V以及掩码张量mask
·它的实例化对象输出是通过多头注意力机制处理的Q的注意力表示

2.3.4 前馈全连接层

·学习目标
·了解什么是前馈全连接层及其它的作用
掌握前馈全连接层的实现过程
。什么是前馈全连接层:
·在Transformert中前馈全连接层就是具有两层线性层的全连接网络
。前馈全连接层的作用:
·考虑注意力机制可能对复杂过程的拟合程度不够,通过增加两层网络来增强模型的能力

代码分析

#通过类PositionwiseFeedForward来实现前馈全连接层
class PositionwiseFeedForward(nn.Module):
    def __init__(self,d_model,d_ff,dropout=0.1):
        '''初始化函数有三个输入参数分别是d_model,d_ff,和dropout=0.1,第一个是线性层的输入维度也是第二个线性层的输出维度,
        因为我们希望输入通过前馈全连接层后输入和输出的维度不变.第二个参数d_ff就是第二个线性层的输入维度和第一个线性层的输出
        最后一个是dropout:置0比率.'''
        super(PositionwiseFeedForward,self).__init__()
        #首先按照我们预期使用nn实例化了两个线性层对象,se1f.w1和se1f.w2
        #它们的参数分别是d_model,d_ff和d_ff,d_model1
        self.w1 = nn.Linear(d_model,d_ff)
        self.w2 = nn.Linear(d_ff,d_model)
        #然后使用nn的Dropout实例化了对象self.dropout
        self.dropout = nn.Dropout(dropout)
    def forward(self,x):
    '''输入参数为X,代表来自上一层的输出'''
    #首先经过第一个线性层,然后使用Funtional中relu函数进行激活
    #之后再使用dropout:进行随机置0,最后通过第二个线性层w2,返回最终结果.
        return self.w2(self.dropout(F.relu(self.w1(x))))

ReLU函数公式:ReLU(x)=max(O,x)

2.3.4 前馈全连接层总结:

·学习了什么是前馈全连接层:
·在Transformer中前馈全连接层就是具有两层线性层的全连接网络.
学习了前馈全连接层的作用:
·考虑注意力机制可能对复杂过程的拟合程度不够,通过增加两层网络来增强模型的能力
学习并实现了前馈全连接层的类:PositionwiseFeedForward
它的实例化参数为d_model,d_ff,dropout,分别代表词嵌入维度,线性变换维度,和置零比率
·它的输入参数x,表示上层的输出
·它的输出是经过2层线性网络变换的特征表示

2.3.5 规范化层

学习目标
·了解规范化层的作用
·掌握规范化层的实现过程

规范化层的作用:
·它是所有深层网络模型都需要的标准网络层,因为随着网络层数的增加,通过多层的计算后参数可能开始出现过大或过小的情况,这样可能会导致学习过程出现异常,模型可能收敛非常的慢.因此都会在一定层数后接规范化层进行数值的规范化,使其特征数值在合理范围内.

代码讲解

#通过LayerNorm:实现规范化层的类
class LayerNorm(nn.Module):
    def __init__(self,ffeatures,eps=1e-6):
        """初始化函数有两个参数,一个是features,表示词嵌入的维度,
        另一个是eps它是一个足够小的数,在规范化公式的分母中出现,
        防止分母为0.默认是1e-6."""
        super(LayerNorm,self).__init__()
        #根据features的形状初始化两个参数张量a2,和b2,第一个初始化为1张量,
        #也就是里面的元素都是1,第二个初始化为张量,也就是里面的元素都是0,这两个张量就是规范化层的参数,     
        #因为直接对上一层得到的结果做规范化公式计算,将改变结果的正常表征,因此就需要有参数作为调节因子
        #使其即能满足规范化要求,又能不改变针对目标的表征.最后使用nn.parameter封装,代表他们是模型的参数。
        self.a2 = nn.Parameter(torch.ones(features))
        self.b2 = nn.Parameter(torch.zeros(features))
        #把eps传到类中
        self.eps = eps
    def forward(self,x):
        "”“输入参数x代表来自上一层的输出"
        #在函数中,首先对输入变量x求其最后一个维度的均值,并保持输出维度与输入维度一致.
        #接着再求最后一个维度的标准差,然后就是根据规范化公式,用x减去均值除以标准差获得规范化的结果
        #最后对结果乘以我们的缩放参数,即2,*号代表同型点乘,即对应位置进行乘法操作,加上位移参数b2.返回即可
        mean = x.mean(-1,keepdim=True)
        std = x.std(-1,keepdim=True)
        return self.a2 * (x - mean)/(std + self.eps)+self.b2

2.3.5 规范化层总结:

·学习了规范化层的作用:
·它是所有深层网络模型都需要的标准网络层,因为随着网络层数的增加,通过多层的计算后参数可能开始出现过大或过小的情况,这样可能会导致学习过程出现异常,模型可能收敛非常的慢.因此都会在一定层数后接规范化层进行数值的规范化,使其特征数值在合理范围内,
·学习并实现了规范化层的类:LayerNorm
它的实例化参数有两个,features和eps,分别表示词嵌入特征大小,和一个足够小的数.
·它的输入参数x代表来自上一层的输出
·它的输出就是经过规范化的特征表示.

2.3.6 子层连接结构

学习目标:
·了解什么是子层连接结构:
掌握好层连接结构的实现过程,
·什么是子层连接结构:
如图所示,输入到每个子层以及规范化层的过程中,还使用了残差链接(跳跃连接),因此我们把这一部分结构整体叫做子层连接(代表子层及其链接结构),在每个编码器层中,都有两个子层,这两个子层加上周围的链接结构就形成了两个子层连接结构,
·子层连接结构图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

代码讲解

#使用SublayerConnection来实现子层连接结构的类
class SublayerConnection(nn.Module):
    def __init__(self,size,dropout=0.1):
        '''
        它输入参数有两个,size以及dropout,s1ze一般是都是词嵌入维度的大小,
        dropout本身是对模型结构中的节点数进行随机抑制的比率,
        又因为节点被抑制等效就是该节点的输出都是0,因此也可以把dropout看作是对输出矩阵的随
        '''
        super(SublayerConnection,self).__init__()
        #实例化了规范化对象se1f.norm
        self.norm = LayerNorm(size)
        #又使用nn中预定义的droupout实例化一个self.dropout对象.
        self.dropout = nn.Dropout(p=dropout)
    def forward(self,x,sublayer):
    """前向逻辑函数中,接收上一个层或者子层的输入作为第一个参数,
    将该子层连接中的子层函数作为第二个参数"""
    #我们首先对输出进行规范化,然后将结果传给子层处理,之后再对子层进行dropout操作,
    #随机停止一些网络中神经元的作用,来防止过拟合,最后还有一个add操作,
    #因为存在跳跃连接,所以是将输入X与d「opout后的子层输出结果相加作为最终的子层连接输出,
        return x self.dropout(sublayer(self.norm(x)))

实例化参数

size= 512
dropout =0.2
head =8
d_model= 512
#输入参数:

#令×为位置编码器的输出

x = pe_result
mask = Variable(torch.zeros(8,4,4))

#假设子层中装的是多头注意力层,实例化这个类
self_attn = MultiHeadedAttention(head,d_model)

#使用lambda获得一个函数类型的子层
sublayer = lambda x:self_attn(x,x,x,mask)

#调用:
sc SubLayerConnection(size,dropout]
sc_result sc(x,sublayer]
print(sc_result]

2.3.6子层连接结构总结:

什么是好层连接结构:
·如图所示,输入到每个子层以及规范化层的过程中,还使用了残差链接(跳跃连接),因此我们把这一部分结构整体叫做子层连接(代表子层及其链接结构),在每个编码器层中,都有两个子层,这两个子层加上周围的链接结构就形成了两个子层连接结构.
·学习并实现了子层连接结构的类:SublayerConnection

类的初始化函数输入参数是size,dropout,分别代表词嵌入大小和置零比率
·它的实例化对象输入参数是x,sublayer,分别代表上一层输出以及子层的函数表示
·它的输出就是通过子层连接结构处理的输出

2.3.7 编码器层

·学习目标
·了解编码器层的作用
·掌握编码器层的实现过程
编码器层的作用:
·作为编码器的组成单元,每个编码器层完成一次对输入的特征提取过程,即编码过程
·编码器层的构成图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

代码讲解

#使用EncoderLayer类实现编码器层
class EncoderLayer(nn.Module):
    def __isit_(self,size,self_attn,feed_forward,dropout):
        '''它的初始化函数参数有四个,分别是s1ze,其实就是我们词嵌入维度的大小,它也将作为我们编码
        第二个self_attn,之后我们将传入多头自注意力子层实例化对象,并且是自注意力机制,
        第三个是feed_froward,之后我们将传入前馈全连接层实例化对象,最后一个是置O比率dropout
        '''
        super(EncoderLayer,self).__init__()
        #首先将self_attn和feed_forward传入其中.
        self.self_attn self_attn
        self.feed_forward feed_forward
        #如图所示,编码器层中有两个子层连接结构,所以使用c1ones函数进行克隆
        self.sublayer = clones(SublayerConnection(size,dropout),2)
        #把size传入其中
        self.size size
    def forward(self,x,mask):
        """forward函数中有两个输入参数,x和mask,分别代表上一层的输出,和掩码张量mask."""
        #里面就是按照结构图左侧的流程.首先通过第一个子层连接结构,其中包含多头自注意力子层,
        #然后通过第二个子层连接结构,其中包含前馈全连接子层.最后返回结果.
        ×=self.sublayer[g](x,lambda x:self.self_attn(x,×,×,mask))
        return self.sublayer[1](x,self.feed_forward)

调用

#实例化参数:
size = 512
head = 8
d_model = 512
d_ff = 64
x = pe_result
dropout = 0.2
self_attn = MultiHeadedAttention(head,d_model)
ff = PositionwiseFeedForward(d_model,d_ff,dropout)
mask = Variable(torch.zeros(8,4,4))
# 调用:
el = EncoderLayer(size,self_attn,ff,dropout)
el_result = el(x,mask)
print(el_result)
print(el_result.shape)

2.3.7 编码器层总结

·学习了编码器层的作用:
·作为编码器的组成单元,每个编码器层完成一次对输入的特征提取过程,即编码过程
学习并实现了编码器层的类:EncoderLayer
·类的初始化函数共有4个,分别是siz,其实就是我们词嵌入维度的大小.第二个self_attn,之后我们将传入多头自注意力子层实例化对象,并且是自注意力机制.第三个是feed_froward,之后我们将传入前馈全连接层实例化对象.最后一个是置0比率dropout.
·实例化对象的输入参数有2个,x代表来自上一层的输出,mask代表掩码张量
·它的输出代表经过整个编码层的特征表示

2.3.8 编码器

·学习目标
·了解编码器的作用
·掌握编码器的实现过程
·编码器的作用:
·编码器用于对输入进行指定的特征提取过程,也称为编码,由N个编码器层堆壁而成
·编码器的结构图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

代码讲解

#使用Encoder类来实现编码器
class Encoder(nn.Module):
    def __init__(self,layer,N):
        '''初始化函数的两个参数分别代表编码器层和编码器层的个数'''
        super(Encoder,self).__init__()
        #首先使用clones函数克隆N个编码器层放在se1f.1 ayers中
        self.layers clones(layer,N)
        #再初始化一个规范化层,它将用在编码器的最后面·
        self.norm LayerNormilayer.size)
    def forward(self,x,mask):
        '''forwardi函数的输入和编码器层相同,x代表上一层的输出,mask代表掩码张量'''
        #首先就是对我们克隆的编码器层进行循环,每次都会得到一个新的x,
        #这个循环的过程,就相当于输出的x经过了N个编码器层的处理.
        #最后再通过规范化层的对象self.norm进行处理,最后返回结果.
        for layer in self.layers:
            x = layer(x,mask)
        return self.norm(x)

调用

#实例化参数:
#第一个实例化参数1yer,它是一个编码器层的实例化对象,因此需要传入编码器层的参数
#又因为编码器层中的子层是不共享的,因此需要使用深度拷贝各个对象.
s1ze=512
head =8
d_model 512
d_ff = 64
c = copy.deepcopy
attn = MultiHeadedAttention(head,d_model)
ff = PositionwiseFeedForward(d_model,d_ff,dropout)
dropout = 0.2
layer = EncoderLayer(size,c(attn),c(ff),dropout)
#编码器中编码器层的个数N
N=8
mask = Variable(torch.zeros(8,4,4))

#调用:
en = Encoder(layer,N)
en_result = en(x,mask)
print(entresult)
print(en_result.shape)

2.3.8编码器总结:
·学习了编码器的作用:
编码器用于对输入进行指定的特征提取过程,也称为编码,由N个编码器层堆叠而成,
学习并实现了编码器的类:Encoder
·类的初始化函数参数有两个,分别是Iayer和N,代表编码器层和编码器层的个数,
·forward函数的输入参数也有两个,和编码器层的forward相同,代表上一层的输出,mask代表掩码张量,
·编码器类的输出就是Transformer中编码器的特征提取表示,它将成为解码器的输入的一部分.

2.4 解码器部分实现

学习目标:
·了解解码器中各个组成部分的作用.
·掌握解码器中各个组成部分的实现过程
·解码器部分:
·由N个解码器层堆叠而成
·每个解码器层由三个子层连接结构组成
·第一个子层连接结构包括一个多头自注意力子层和规范化层以及一个残差连接
·第二个子层连接结构包括一个多头注意力子层和规范化层以及一个残差连接
·第三个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

说明:
·解码器层中的各个部分,如,多头注意力机制,规范化层,前馈全连接网络,子层连接结构都与编码器中的实现相同.因此这里可以直接拿来构建解码器层.

2.4.1 解码器层

学习目标:
了解解码器层的作
·掌握解码器层的实现过程
解码器层的作用:
·作为解码器的组成单元,每个解码器层根据给定的输入向目标方向进行特征提取操作,即解码过程

代码讲解

#使用DecoderLayer的类实现解码器层

class DecoderLayer(nn.Module):
    def __init__(self,size,self_attn,src_attn,feed_forward,dropout):
        '''初始化函数的参数有币个,分别是s1z,代表词嵌入的维度大小,同时也代表解码器层的尺寸,
        第二个是self_attn,多头自注意力对象,也就是说这个注意力机制需要Q=K=V,
        第三个是src_attn,多头注意力对象,这里Q!=K=V,第四个是前馈全连接层对象,最后就是d
        '''
        super(DecoderLayer,self).__init__()
        #在初始化函数中,
        主要就是将这些输入传到类中
        self.size = size
        self.self_attn = self_attn
        self.src_attn = src_attn
        self.feed_forward = feed_forward
        #按照结构图使用clones函数克隆三个子层连接对象.
        self.sublayer = clones(SublayerConnection(size,dropout),3)
        
    def forward(self,x,memory,source_mask,target_mask):
        '''forwardi函数中的参数有4个,分别是来自上一层的输入x,
        来自编码器层的语义存储变量me rmory,以及源数据掩码张量和目标数据掩码张量.
        '''
        #将memory表示成m方便之后使用
        m = memory
        #将x传入第一个子层结构,第一个子层结构的输入分别是x和self-attn函数,因为是自注意力机制,所以Q,K,V都是X,
        #最后一个参数是目标数据掩码张量,这时要对目标数据进行遮掩,因为此时模型可能还没有生成任何目标数据,
        #比如在解码器准备生成第一个字符或词汇时,我们其实已经传入了第一个字符以便计算损失,
        #但是我们不希望在生成第一个字符时模型能利用这个信息,因此我们会将其遮掩,同样生成第二个字符或词汇时,
        #模型只能使用第一个字符或词汇信息,第二个字符以及之后的信息都不允许被模型使用.
        x = self.sublayer[0](x,lambda x:self.self_attn(x,x,x,target_mask))
        #接着进入第二个子层,这个子层中常规的注意力机制,q是输入X;k,V是编码层输出memo ry,
        #同样也传入source_mask,但是进行源数据遮掩的原因并非是抑制信息泄漏,而是遮蔽掉对结果没有意义的字符通产生的注意力值,
        #以此提升模型效果和训练速度.这样就完成了第二个子层的处理.
        x = self.sublayer[1](x,lambda x:self.src_attn(x,m,m,source_mask))
        #最后一个子层就是前馈全连接子层,经过它的处理后就可以返回结果.这就是我们的解码器层结构.
        return self.sublayer[2](x,self.feed_forward)

调用

#实例化参数:
#类的实例化参数与解码器层类似,相比多出了src_attn,但是和se1f_attn是同一个类.
head =8
size = 512
d_model = 512
d_ff = 64
dropout = 0.2
self_attn = src_attn MultiHeadedAttention(head,d_model,dropout)
#前馈全连接层也和之前相同
ff = PositionalwiseFeedForward(d_model,d_ff,dropout)

#输入参数:
#x是来自目标数据的词嵌入表示,但形式和源数据的词嵌入表示相同,这里使用per充当.
x = pe_result
#memory是来自编码器的输出
memory = en_result
#实际中source_mask和target._mask并不相同,这里为了方便计算使他们都为mask
mask = Variable(torch.zeros(8,4,4))
source_mask = target_maskmask

#调用:
dl = DecoderLayer(size,self_attn,src_attn,ff,dropout)
dl_result = dl(x,memory,source_mask,target_mask)
print(dl_result)
print(d1_result.shape)

2.4.1 解码器层总结

学习了解码器层的作用:
作为解码器的组成单元,每个解码器层根据给定的输入向目标方向进行特征提取操作,即解码过程
·学习并实现了解码器层的类:DecoderLayer
·类的初始化函数的参数有5个,分别是sz,代表词嵌入的维度大小,同时也代表解码器层的尺寸,第二个是sef_attn,多头自注意力对象,也就是说这个注意力机制需要Q=K=V,第三个是src_attn,多头注意力对象,这里Q!=K=V,第四个是前馈全连接层对象,最后就是droupout置0比率
forward函数的参数有4个,分别是来自上一层的输入x,来自编码器层的语义存储变量mermory,以及源数据掩码张量和目标数据掩码张量
·最终输出了由编码器输入和目标数据一同作用的特征提取结果

2.4.2 解码器

学习目标
·了解解码器的作用.
·掌握解码器的实现过程

解码器的作用:
·根据编码器的结果以及上一次预测的结果,对下一次可能出现的’值’进行特征表示.

代码讲解

#使用类Decoder:来实现解码器
class Decoder(nn.Module):
    def __init__(self,layer,N):
        '''初始化函数的参数有两个,第一个就是解码器层layer,第二个是解码器层的个数N.'''
        super(Decoder,self).__init__()
        #首先使用clones方法克隆了N个layer,然后实例化了一个规范化层.
        #因为数据走过了所有抑解码器层后最后要做规范化处理.
        self.layers = clones(layer,N)
        self.norm = LayerNorm(layer.size)
    def forward(self,x,memory,source_mask,target_mask):
       '''forward函数中的参数有4个,x代表目标数据的嵌入表示,memory是编码器层的输出,
        source_mask,target_mask代表源数据和目标数据的掩码张量'''
        #然后就是对每个层进行循环,当然这个循环就是变量X通过每一个层的处理,
        #得出最后的结果,再进行一次规范化返回即可.
        for layer in self.layers:
        	x = layer(x,memory,source_mask,target_mask)
        return self.norm(x)

调用

#实例化参数:
#分别是解码器层1ayer和解码器层的个数N
size=512
d_model =512
head =8
d_ff 64
dropout 0.2
c copy.deepcopy
attn MultiHeadedAttention(head,d_model)
ff PositionwiseFeedForward(d_model,d_ff,dropout)
layer DecoderLayer(d_model,c(attn),c(attn),c(ff),dropout)
N=8
#输入参数:
#输入参数与解码器层的输入参数相同
x = pe_result
memory = en_result
mask = Variable(torch.zeros(8,4,4))
source_mask = target_mask = mask

#调用:
de = Decoder(layer,N)
de_result = de(x,memory,source_mask,target_mask)
print(de_result)
print(de_result.shape)

2.4.2 解码器总结

  • 学习了解码器的作用:

    • 根据编码器的结果以及上一次预测的结果,对下一次可能出现的值’进行特征表示,
  • 学习并实现了解码器的类:Decoder类的初始化函数的参数有两个,第一个就是解码器层layer,第二个是解码器层的个数N.

    • forward函数中的参数有4个,x代表目标数据的嵌入表示,memory是编码器层的输出,src_mask,tgt_mask代表源数据和目标数据的掩码张量,
    • 输出解码过程的最终特征表示

2.5 输出部分实现

·学习目标:
·了解线性层和softmax的作用.
·掌握线性层和softmax的实现过程,
输出部分包含:
·线性层
·softmax层

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

。线性层的作用:
·通过对上一步的线性变化得到指定维度的输出,也就是转换维度的作用,
。softmax层的作用:
·使最后一维的向量更的数字缩放到0-1的概率值域内,并满足他们的和为1.

代码讲解

#nn.functional工具包装载了网络层中那些只进行计算,而没有参数的层
import torch.nn.functional as F
#将线性层和softmax计算层一起实现,因为二者的共同目标是生成最后的结构
#因此把类的名字叫做Generator,生成器类
class Generator(nn.Module):
    def __init__(self,d_model,vocab_size):
        '''初始化函数的输入参数有两个,d_model代表词嵌入维度,vocab_size代表词表大小.'''
        super(Generator,self).__init__()
        #首先就是使用nn中的预定义线性层进行实例化,得到一个对象self.project等待使用,
        #这个线性层的参数有两个,就是初始化函数传进来的两个参数:d_model,vocab_size
        self.project nn.Linear(d_model,vocab_size)
	def forward(self,x):
        '''前向逻辑函数中输入是上一层的输出张量×'''
        #在函数中,首先使用上一步得到的self.project对x进行线性变化,
        #然后使用F中已经实现的log_softmax:进行的softmax:处理
        #在这里之所以使用log_softmax是因为和我们这个bytorch版本的损失函数实现有关,在其他版本
        #log_softmax就是对softmax的结果又取了对数,因为对数函数是单调递增函数,
        #因此对最终我们取最大的概率值没有影响.最后返回结果即可.
        return F.log_softmax(self.project(x),dim=-1)
nn.Linear

转换向量的维度

#nn.Linear演示:
>>m = nn.Linear(20,30)
>>input = torch.randn(128,20)
>>output = m(input)
>>print(output.size())
torch.Size([128,30])

2.6 模型构建

2.7 模型基本测试运行

  • 12
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值