You are on page 1of 59

第二章:Transformer架构解析

m
2.1 认识Transformer架构 he
i ma
. co

i t
w.
ww
学习目标 程



了解Transformer模型的作用
黑 .
了解Transformer总体架构图中各个组成部分的名称.

模型的作用
Transformer

基于seq2seq架构的transformer模型可以完成NLP领域研究的典型任务, 如机器翻译, 文本生


成等. 同时又可以构建预训练语言模型,用于不同任务的迁移学习 m
.
. co
ma
h ei
声明: . it
ww
在接下来的架构分析中, 我们将假设使用 员 Transformer模型架构处理从一种语言文本到另
w
一种语言文本的翻译工作, 因此很多命名方式遵循

序 NLP中的规则. 比如: Embeddding层将
称作文本嵌入层, Embedding

马层产生的张量称为词嵌入张量, 它的最后一维将称作词向量
等.

总体架构图
Transformer

com
a.
eim
ith
w .
ww





m
. co
i ma
the
i
w.
ww




m
. co
m a
h ei
it
w w.
w




总体架构可分为四个部分:
Transformer

输入部分
输出部分
编码器部分
解码器部分 com
a.
e im
ith
.
ww
输入部分包含: 员
w
源文本嵌入层及其位置编码器 马程序
目标文本嵌入层及其位置编码器

m
. co
i ma
the
i
w.
ww


输出部分包含: 马

线性层 黑
softmax层

m
. co
m a
h ei
it
w.
编码器部分: ww
由N个编码器层堆叠而成 程序员
每个编码器层由两个子层连接结构组成


第一个子层连接结构包括一个多头自注意力子层和规范化层以及一个残差连接
第二个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接

com
a.
e im
ith
w.
ww




解码器部分:
由N个解码器层堆叠而成
每个解码器层由三个子层连接结构组成
第一个子层连接结构包括一个多头自注意力子层和规范化层以及一个残差连接
第二个子层连接结构包括一个多头注意力子层和规范化层以及一个残差连接
m
第三个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接
a. co
m
h ei
it
w.
ww




m
. co
ma
h ei
it
w w.
w

小节总结 马



学习了Transformer模型的作用:
基于seq2seq架构的transformer模型可以完成NLP领域研究的典型任务, 如机器翻译, 文
本生成等. 同时又可以构建预训练语言模型,用于不同任务的迁移学习.

总体架构可分为四个部分:
Transformer

输入部分
om
输出部分 a.
c
im
编码器部分 ith
e
.
解码器部分 ww
w




输入部分包含: 黑
源文本嵌入层及其位置编码器
目标文本嵌入层及其位置编码器
输出部分包含:
线性层
softmax处理器
m
. co
ma
编码器部分: i the
i

由N个编码器层堆叠而成 w w.
w
每个编码器层由两个子层连接结构组成



第一个子层连接结构包括一个多头自注意力子层和规范化层以及一个残差连接


第二个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接

解码器部分:
由N个解码器层堆叠而成
每个解码器层由三个子层连接结构组成
第一个子层连接结构包括一个多头自注意力子层和规范化层以及一个残差连接 m
co
第二个子层连接结构包括一个多头注意力子层和规范化层以及一个残差连接
eim
a .
第三个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接
w. it
h
ww




2.2 输入部分实现
学习目标
了解文本嵌入层和位置编码的作用. om
c
掌握文本嵌入层和位置编码的实现过程. im
a.
e
ith
w .
ww
输入部分包含: 员

源文本嵌入层及其位置编码器 马程
目标文本嵌入层及其位置编码器

m
. co
ima
the
i
w.
ww


文本嵌入层的作用 黑

无论是源文本嵌入还是目标文本嵌入,都是为了将文本中词汇的数字表示转变为向量表示,
希望在这样的高维空间捕捉词汇间的关系.

pytorch 0.3.0 及其必备工具包的安装:


# 使用 安装的工具包包括
pip pytorch-0.3.0, numpy, matplotlib, seaborn
om
pip install http://download.pytorch.org/whl/cu80/torch-0.3.0.post4-cp36-
cp36m-linux_x86_64.whl numpy matplotlib seaborn .c
a
im
系统安装 版本 he
# MAC , python <=3.6
it
w.
pip install torch==0.3.0.post4 numpy matplotlib seaborn
ww



文本嵌入层的代码分析: 黑

# 导入必备的工具包
import torch

# 预定义的网络层 torch.nn, 工具开发者已经帮助我们开发好的一些常用层,


# 比如,卷积层 层
, lstm , embedding 层等, 不需要我们再重新造轮子.
import torch.nn as nn

# 数学计算工具包
import math
com
a.
# torch 中变量封装函数 Variable.
e im
from torch.autograd import Variable
ith
w.
# ww
定义Embeddings类来实现文本嵌入层,这里s说明代表两个一模一样的嵌入层 , 他们共享参数.
#该类继承nn.Module, 这样就有标准层的一些功能

员 , 这里我们也可以理解为一种模式, 我们自己实
现的所有层都会这样去写. 程
class Embeddings(nn.Module): 马

def __init__(self, d_model, vocab):
类的初始化函数 有两个参数
""" , , d_model: 指词嵌入的维度, vocab: 指词表的大
小 ."""
接着就是使用
# 的方式指明继承
super nn.Module 的初始化函数, 我们自己实现的所有层
都会这样去写 .
super(Embeddings, self).__init__()
# 之后就是调用 中的预定义层
nn Embedding, 获得一个词嵌入对象self.lut
self.lut = nn.Embedding(vocab, d_model)
# 最后就是将 传入类中
d_model
self.d_model = d_model

def forward(self, x):


m
"""可以将其理解为该层的前向传播逻辑,所有层中都会有此函数 . co
当传给该类的实例化对象参数时, 自动调用该类函数 im
a
参数x: 因为Embedding层是首层,it所以代表输入给模型的文本通过词汇映射后的张
h e
量""" w .
ww
# 将 传给
x self.lut 并与根号下
员 self.d_model 相乘作为结果返回
return self.lut(x) *序math.sqrt(self.d_model)



nn.Embedding 演示:
>>> embedding = nn.Embedding(10, 3)
>>> input = torch.LongTensor([[1,2,4,5],[4,3,2,9]])
>>> embedding(input)
tensor([[[-0.0251, -1.6902, 0.7172],
[-0.6431, 0.0748, 0.6969],
[ 1.4970, 1.3448, -0.9685],
[-0.3677, -2.7265, -0.1685]],

[[ 1.4970, 1.3448, -0.9685], c om


[ 0.4362, -0.4004, 0.9400], m a.
i
[-0.6431, 0.0748, 0.6969], t he
[ 0.9124, -2.3616, 1.1151]]])w.
i
ww

>>> embedding = nn.Embedding(10,

程 3, padding_idx=0)

>>> input = torch.LongTensor([[0,2,0,5]])
>>> embedding(input)

tensor([[[ 0.0000, 0.0000, 0.0000],
[ 0.1535, -2.0309, 0.9315],
[ 0.0000, 0.0000, 0.0000],
[-0.1655, 0.9897, 0.0635]]])

实例化参数:
c om
a.
# 词嵌入维度是 维 512 eim
d_model = 512 ith
w .
ww
# 词表大小是 1000 员
vocab = 1000 序



输入参数:
# 输入 是一个使用
x Variable封装的长整型张量 形状是 , 2 x 4
x = Variable(torch.LongTensor([[100,2,421,508],[491,998,1,221]]))

调用: . co
m
ima
the
i
emb = Embeddings(d_model, vocab) w.
embr = emb(x) ww
print("embr:", embr) 员



输出效果:

embr: Variable containing:


( 0 ,.,.) =
35.9321 3.2582 -17.7301 ... 3.4109 13.8832 39.0272
8.5410 -3.5790 -12.0460 ... 40.1880 36.6009 34.7141
-17.0650 -1.8705 -20.1807 ... -12.5556 -34.0739 35.6536
m
20.6105 4.4314 14.9912 ... co 28.6771
-0.1342 -9.9270
a.
im
( 1 ,.,.) =
t he
27.7016 16.7183 46.6900 ... .i 17.2525 -3.9709
17.9840
w
3.0645 -5.5105 10.8802 ... -13.0069ww 30.8834 -38.3209
33.1378 -32.1435 -3.9369 ... 员15.6094 -29.7063 40.1361
-31.5056 3.3648 1.4726 ...序 2.8047 -9.6514 -23.4909


[torch.FloatTensor of size 2x4x512]

位置编码器的作用
因为在Transformer的编码器结构中, 并没有针对词汇位置信息的处理,因此需要在
Embedding层后加入位置编码器,将词汇位置不同可能会产生不同语义的信息加入到词嵌入
张量中, 以弥补位置信息的缺失.
com
a.
位置编码器的代码分析: eim
ith
w.
w
# 定义位置编码器类 我们同样把它看做一个层 因此会继承
, ,

w nn.Module
class PositionalEncoding(nn.Module):


def __init__(self, d_model, dropout, max_len=5000):
""" 位置编码器类的初始化函数 共有三个参数 分别是


, , d_model: 词嵌入维度,
置 比率
dropout: 0 每个句子的最大长度
, max_len: """
super(PositionalEncoding, self).__init__()

# 实例化nn中预定义的Dropout层, 并将dropout传入其中, 获得对象self.dropout


self.dropout = nn.Dropout(p=dropout)

# 初始化一个位置编码矩阵 它是一个 阵,矩阵的大小是max_len


, 0 x d_model.
pe = torch.zeros(max_len, d_model)

初始化一个绝对位置矩阵, 在我们这里,词汇的绝对位置就是用它的索引去表示.
#
所以我们首先使用arange方法获得一个连续自然数向量,然后再使用
#
om
unsqueeze方法拓
展向量维度使其成为矩阵, . c
# 又因为参数传的是1,代表矩阵拓展的位置,会使向量变成一个
ma max_len x 1 的矩阵,
ei
h
position = torch.arange(0, max_len).unsqueeze(1)
t
w .i
# 绝对位置矩阵初始化之后,接下来就是考虑如何将这些位置信息加入到位置编码矩阵中,
ww
# 最简单思路就是先将max_len
员 x 1的绝对位置矩阵, 变换成max_len x d_model形
状,然后覆盖原来的初始位置编码矩阵即可,


# 要做这种矩阵变换,就需要一个
马 1xd_model形状的变换矩阵div_term,我们对这个变换
矩阵的要求除了形状外, 黑
# 还希望它能够将自然数的绝对位置编码缩放成足够小的数字,有助于在之后的梯度下降过
程中更快的收敛. 这样我们就可以开始初始化这个变换矩阵了.
# 首先使用arange获得一个自然数矩阵, 但是细心的同学们会发现, 我们这里并没有按
照预计的一样初始化一个1xd_model的矩阵,
# 而是有了一个跳跃,只初始化了一半即1xd_model/2 的矩阵。 为什么是一半呢,其实
这里并不是真正意义上的初始化了一半的矩阵,
# 我们可以把它看作是初始化了两次,而每次初始化的变换矩阵会做不同的处理,第一次初
始化的变换矩阵分布在正弦波上, 第二次初始化的变换矩阵分布在余弦波上,
# 并把这两个矩阵分别填充在位置编码矩阵的偶数和奇数位置上,组成最终的位置编码矩
阵. om c 2) *
div_term = torch.exp(torch.arange(0, d_model,
-(math.log(10000.0)m a./ d_model))
i
he
pe[:, 0::2] = torch.sin(position *tdiv_term)
i
w. * div_term)
pe[:, 1::2] = torch.cos(position
ww
# 这样我们就得到了位置编码矩阵pe,员 pe现在还只是一个二维矩阵,要想和embedding的
输出(一个三维张量)相加, 程

# 就必须拓展一个维度,所以这里使用
马 unsqueeze拓展维度.

pe = pe.unsqueeze(0)

最后把pe位置编码矩阵注册成模型的buffer,什么是buffer呢,
#
我们把它认为是对模型效果有帮助的,但是却不是模型结构中超参数或者参数,不需要随
#
着优化步骤进行更新的增益对象.
# 注册之后我们就可以在模型保存后重加载时和模型结构与参数一同被加载.
self.register_buffer('pe', pe)

def forward(self, x):


函数的参数是 表示文本序列的词嵌入表示
"""forward x, """
在相加之前我们对 做一些适配工作, 将这个三维张量的第二维也就是句子最大长度的
# pe m
那一维将切片到与输入的 的第二维相同即
x , x.size(1) . co
因为我们默认
# 为 一般来讲实在太大了,很难有一条句子包含5000个词汇,
max_len 5000 im
a
所以要进行与输入张量的适配 .
it
h e
最后使用
# 进行封装,使其与 的样式相同,但是它是不需要进行梯度求解的,因
Variable x w .
此把 设置成
requires_grad false. ww

x = x + Variable(self.pe[:, :x.size(1)],

requires_grad=False)

# 最后使用 self.dropout 马 '

对象进行 丢弃 操作 并返回结果
' , .
return self.dropout(x)

nn.Dropout 演示:
>>> m = nn.Dropout(p=0.2)
>>> input = torch.randn(4, 5)
>>> output = m(input)
>>> output
Variable containing:
0.0000 -0.5856 -1.4094 0.0000 -1.0290
m
2.0591 -1.3400 -1.7247 -0.9885 0.1286
. co
0.5099 1.3715 0.0000 2.2079 -0.5497
-0.0000 -0.7839 -1.2434 -0.1222 1.2815 ei
ma
h
[torch.FloatTensor of size 4x5] it .
w
ww
torch.unsqueeze 演示: 序



黑 2, 3, 4])
>>> x = torch.tensor([1,
>>> torch.unsqueeze(x, 0)
tensor([[ 1, 2, 3, 4]])
>>> torch.unsqueeze(x, 1)
tensor([[ 1],
[ 2],
[ 3],
[ 4]])

m
co
实例化参数: ei
m a .
t h
i
w w.
# 词嵌入维度是 维 512

w
d_model = 512


# 置 比率为
0 0.1


dropout = 0.1

# 句子最大长度
max_len=60

输入参数:
c om
输入 是 层的输出的张量, 形状是2 a.
# x Embedding x 4 x 512
e im
x = embr
ith
Variable containing: w.
( 0 ,.,.) = ww
35.9321 3.2582 -17.7301 ... 员
3.4109 13.8832 39.0272
8.5410 -3.5790 -12.0460 序
... 程40.1880 36.6009 34.7141
-17.0650 -1.8705 -20.1807 马 -12.5556 -34.0739
... 35.6536
20.6105 4.4314 14.9912 黑
... -0.1342 -9.9270 28.6771

( 1 ,.,.) =
27.7016 16.7183 46.6900 ... 17.9840 17.2525 -3.9709
3.0645 -5.5105 10.8802 ... -13.0069 30.8834 -38.3209
33.1378 -32.1435 -3.9369 ... 15.6094 -29.7063 40.1361
-31.5056 3.3648 1.4726 ... 2.8047 -9.6514 -23.4909
[torch.FloatTensor of size 2x4x512]

m
调用: . co
ima
the
i
w w.
pe = PositionalEncoding(d_model, dropout, max_len)
pe_result = pe(x)
w


print("pe_result:", pe_result)


输出效果:

pe_result: Variable containing:


( 0 ,.,.) =
-19.7050 0.0000 0.0000 ... -11.7557 -0.0000 23.4553
-1.4668 -62.2510 -2.4012 ... 66.5860 -24.4578 -37.7469
9.8642 -41.6497 -11.4968 ... -21.1293 -42.0945 m 50.7943
o
0.0000 34.1785 -33.0712 ... 48.5520 .c 54.1348
3.2540
a
im
( 1 ,.,.) = t he
7.7598 -21.0359 15.0595 ... -35.6061w.i -0.0000 4.1772
-38.7230 ww
8.6578 34.2935 ... -43.3556 26.6052 4.3084
24.6962 37.3626 -26.9271 ... 员49.8989 0.0000 44.9158

程 -52.5447 -4.1475
-28.8435 -48.5963 -0.9892 ... -3.0450

[torch.FloatTensor of size 2x4x512]

绘制词汇向量中特征的分布曲线:
import matplotlib.pyplot as plt

# 创建一张 15 x 5大小的画布
plt.figure(figsize=(15, 5))
m
# 实例化PositionalEncoding 类得到 对象, 输入参数是20和0ma.co
pe
pe = PositionalEncoding(20, 0)
ei
th
# 然后向 传入被
pe 封装的
Variable这样 会直接执行forward函数,
tensor, pe w .i
# 且这个 里的数值都是 被处理后相当于位置编码张量
tensor 0, ww

y = pe(Variable(torch.zeros(1, 100, 20)))


# 然后定义画布的横纵坐标, 横坐标到黑 马的长度, 纵坐标是某一个词汇中的某维特征在不同长度下对
100
应的值
# 因为总共有20维之多, 我们这里只查看4,5,6,7维的值.
plt.plot(np.arange(100), y[0, :, 4:8].data.numpy())
# 在画布上填写维度提示信息
plt.legend(["dim %d"%p for p in [4,5,6,7]])

输出效果:
m
. co
i ma
the
i
w.
ww




效果分析:
每条颜色的曲线代表某一个词汇中的特征在不同位置的含义.
保证同一词汇随着所在位置不同它对应位置嵌入向量会发生变化 co
m .
.
正弦波和余弦波的值域范围都是1到-1这又很好的控制了嵌入数值的大小
eim
a , 有助于梯度
的快速计算. .i
t h
w
ww




小节总结 黑

学习了文本嵌入层的作用:
无论是源文本嵌入还是目标文本嵌入,都是为了将文本中词汇的数字表示转变为向量表
示, 希望在这样的高维空间捕捉词汇间的关系.

学习并实现了文本嵌入层的类: Embeddings m
co
初始化函数以d_model, 词嵌入维度, 和vocab, 词汇总数为参数 im
a , 内部主要使用了nn中的预
.
定层Embedding进行词嵌入. it
h e
.
在forward函数中, 将输入x传入到Embeddingww的实例化对象中
w , 然后乘以一个根号下
d_model进行缩放, 控制数值大小. 员

它的输出是文本嵌入后的结果. 马程

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

学习并实现了位置编码器的类: PositionalEncoding .com


a
初始化函数以d_model, dropout, max_len为参数 h eim , 分别代表d_model: 词嵌入维度,
dropout: 置0比率, max_len: 每个句子的最大长度
.it .
w w
forward函数中的输入参数为x, 是Embedding
w 层的输出.

最终输出一个加入了位置编码信息的词嵌入张量


序 .

实现了绘制词汇向量中特征的分布曲线:
保证同一词汇随着所在位置不同它对应位置嵌入向量会发生变化.
正弦波和余弦波的值域范围都是1到-1, 这又很好的控制了嵌入数值的大小, 有助于梯度的
快速计算.
m
. co
ma
h ei
it
w.
2.3 编码器部分实现 序

ww




学习目标
了解编码器中各个组成部分的作用.
掌握编码器中各个组成部分的实现过程.

编码器部分:
由N个编码器层堆叠而成 a . co
m
每个编码器层由两个子层连接结构组成 e im
h
it
第一个子层连接结构包括一个多头自注意力子层和规范化层以及一个残差连接
ww
w .
第二个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接





m
. co
i ma
the
i
w.
ww




2.3.1 掩码张量
学习目标:
了解什么是掩码张量以及它的作用.
掌握生成掩码张量的实现过程. m
. co
ma
h ei
i t
w w.
w
什么是掩码张量: 员

掩代表遮掩,码就是我们张量中的数值,它的尺寸不定,里面一般只有

程 1和0的元素,代
表位置被遮掩或者不被遮掩,至于是
黑 0位置被遮掩还是1位置被遮掩可以自定义,因此它
的作用就是让另外一个张量中的一些数值被遮掩,也可以说被替换, 它的表现形式是一
个张量.

掩码张量的作用:
在transformer中, 掩码张量的主要作用在应用attention(将在下一小节讲解)时,有一些生
成的attention张量中的值计算有可能已知了未来信息而得到的,未来信息被看到是因为 m
训练时会把整个输出结果都一次性进行Embedding,但是理论上解码器的的输出却不是a . co
一次就能产生最终结果的,而是一次次通过上一次结果综合得出的,因此,未来的信息
h e im
可能被提前利用. 所以,我们会进行遮掩. 关于解码器的有关知识将在后面的章节中讲解
ww
. it .
w



生成掩码张量的代码分析: 黑

def subsequent_mask(size):
""" 生成向后遮掩的掩码张量 参数size是掩码张量最后两个维度的大小, 它的最后两维形成一
,
个方阵 """
# 在函数中 首先定义掩码张量的形状
,
attn_shape = (1, size, size)

# 然后使用 方法向这个形状中添加 元素 形成上三角阵 最后为了节约空间


np.ones 1 , , ,
# 再使其中的数据类型变为无符号 位整形 8 unit8
subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')
m
# 最后将numpy类型转化为torch中的tensor, 内部做一个 . co 1 - 的操作,
# 在这个其实是做了一个三角阵的反转, subsequent_mask im
a 中的每个元素都会被1减,
# 如果是0, subsequent_mask中的该位置由 th
0变成
e 1
# 如果是1, subsequent_mask中的该位置由
w w .i 1变成0
return w
torch.from_numpy(1 - subsequent_mask)




演示:
np.triu

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


array([[ 1, 2, 3],
[ 4, 5, 6],
[ 0, 8, 9],
[ 0, 0, 12]])

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


m
array([[ 1, 2, 3],
. co
[ 0, 5, 6],
i ma
e
[ 0, 0, 9],
th
[ 0, 0, 0]]) .i w
ww


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

array([[ 0, 2, 3], 马
[ 0, 0, 6], 黑
[ 0, 0, 0],
[ 0, 0, 0]])

输入实例:

# 生成的掩码张量的最后两维的大小 com
size = 5 a.
eim
ith
w .
ww

调用: 程



sm = subsequent_mask(size)
print("sm:", sm)
输出效果:

# 最后两维形成一个下三角阵
sm: (0 ,.,.) =
1 0 0 0 0
m
1 1 0 0 0
. co
1 1 1 0 0
i ma
1 1 1 1 0
the
1 1 1 1 1 i
w.
[torch.ByteTensor of size 1x5x5]
ww




掩码张量的可视化: 黑

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

输出效果:
m
. co
m a
h ei
it
w w.
w




com
a.
e im
ith
w.
ww
效果分析: 序


通过观察可视化方阵, 黄色是 马的部分, 这里代表被遮掩, 紫色代表没有被遮掩的信息,
1
横坐标代表目标词汇的位置黑, 纵坐标代表可查看的位置;
我们看到, 在0的位置我们一看望过去都是黄色的, 都被遮住了,1的位置一眼望过去还
是黄色, 说明第一次词还没有产生, 从第二个位置看过去, 就能看到位置1的词, 其他位
置看不到, 以此类推.

2.3.1 掩码张量总结: m
学习了什么是掩码张量: . co
a
im
掩代表遮掩,码就是我们张量中的数值,它的尺寸不定,里面一般只有
it
h e 1和0的元
素,代表位置被遮掩或者不被遮掩,至于是
ww
w . 0位置被遮掩还是1位置被遮掩可以自定
义,因此它的作用就是让另外一个张量中的一些数值被遮掩
员 , 也可以说被替换, 它的
表现形式是一个张量

.程

学习了掩码张量的作用:
在transformer中, 掩码张量的主要作用在应用attention(将在下一小节讲解)时,有一
些生成的attetion张量中的值计算有可能已知量未来信息而得到的,未来信息被看到
是因为训练时会把整个输出结果都一次性进行Embedding,但是理论上解码器的的
输出却不是一次就能产生最终结果的,而是一次次通过上一次结果综合得出的,因
此,未来的信息可能被提前利用. 所以,我们会进行遮掩 . 关于解码器的有关知识将
在后面的章节中讲解. a.
co m
m
h ei
t
i
w w.
学习并实现了生成向后遮掩的掩码张量函数 员
w : subsequent_mask
它的输入是size, 代表掩码张量的大小

序 .

它的输出是一个最后两维形成
黑 1方阵的下三角阵.

最后对生成的掩码张量进行了可视化分析, 更深一步理解了它的用途.

2.3.2 注意力机制
com
a.
学习目标: h ei
m
t
了解什么是注意力计算规则和注意力机制. www.i
掌握注意力计算规则的实现过程. 序员



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

m
什么是注意力计算规则: a . co
im 然后通过公式得到注意力的计算结果,
它需要三个指定的输入Q(query), K(key), tV(value),
e
h
这个结果代表query在key和value作用下的表示
ww
.i . 而这个具体的计算规则有很多种, 我这里
只介绍我们用到的这一种. 员 w




我们这里使用的注意力的计算规则:

的比喻解释:
Q, K, V
co
m
a .
e im
假如我们有一个问题: 给出一段文本,使用一些关键词对它进行描述 i th !
为了方便统一正确答案,这道题可能预先已经给大家写出了一些关键词作为提示
w. .其中这些给出的提示
就可以看作是key, ww
而整个的文本信息就相当于是query,value 的含义则更抽象,可以比作是你看到这段文本信息后,脑

子里浮现的答案信息, 程

这里我们又假设大家最开始都不是很聪明,第一次看到这段文本后脑子里基本上浮现的信息就只有提示

这些信息, 黑
因此key与value基本是相同的,但是随着我们对这个问题的深入理解,通过我们的思考脑子里想起来
的东西原来越多,
并且能够开始对我们query也就是这段文本,提取关键信息进行表示. 这就是注意力作用的过程, 通
过这个过程,
我们最终脑子里的value发生了变化,
根据提示key生成了query的关键词表示方法,也就是另外一种特征表示方法.
刚刚我们说到key和value一般情况下默认是相同,与query是不同的,这种是我们一般的注意力输入
形式,
但有一种特殊情况,就是我们query与key和value相同,这种情况我们称为自注意力机制,就如同我 m
们的刚刚的例子, . co
使用一般注意力机制,是使用不同于给定文本的关键词表示它. 而自注意力机制 a
e im ,
需要用给定文本自身来表达自己,也就是说你需要从给定文本中抽取关键词来表述它 it
h , 相当于对文本自
身的一次特征提取. ww
.
w



什么是注意力机制: 黑

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

m
. co
ima
the
i
w.
ww




注意力计算规则的代码分析:
def attention(query, key, value, mask=None, dropout=None):
""" 注意力机制的实现 输入分别是
, 掩码张量
query, key, value, mask:
co
m ,
是 层的实例化对象 默认为
dropout nn.Dropout , a
None"""
m
.
# 在函数中 首先取
, 的最后一维的大小 一般情况下就等同于我们的词嵌入维度 命名为
query ,
t h ei ,
d_k i
w.
d_k = query.size(-1)
ww
按照注意力公式 将
# 与 的转置相乘 这里面 是将最后两个维度进行转置 再除
, query key
员 , key ,
以缩放系数根号下 这种计算方法也称为缩放点积注意力计算
d_k, 序 .
得到注意力得分张量
# scores 程
马 key.transpose(-2, -1)) / math.sqrt(d_k)

scores = torch.matmul(query,

接着判断是否使用掩码张量
#
if mask is not None:
使用# 的 tensor masked_fill 方法 将掩码张量和
, 张量每个位置一一比较, 如果
scores
掩码张量处为 0
则对应的
# 张量用 scores -1e9 这个值来替换 如下演示
,
scores = scores.masked_fill(mask == 0, -1e9)

# 对scores的最后一维进行softmax操作, 使用F.softmax方法, 第一个参数是softmax对


象, 第二个是目标维度.
# 这样获得最终的注意力张量 om
p_attn = F.softmax(scores, dim = -1) .c a
e im
h
# 之后判断是否使用 dropout 进行随机置0 w .i
t
if dropout is not None: w
# 将 传入
p_attn dropout 丢弃w'处理
对象中进行'员
p_attn = dropout(p_attn) 序


# 最后, 根据公式将p_attn与value
黑 张量相乘获得最终的query注意力表示, 同时返回注意力
张量
return torch.matmul(p_attn, value), p_attn
演示:
tensor.masked_fill

>>> input = Variable(torch.randn(5, 5))


>>> input
Variable containing:
2.0344 -0.5450 0.3365 -0.1888 -2.1803
m
1.5221 -0.3823 0.8414 0.7836 -0.8481
. co
-0.0345 -0.8643 0.6476 -0.2713 1.5645 ma
0.8788 -2.2142 0.4022 0.1997 0.1474 hei
t
2.9109 0.6006 -0.6745 -1.7262 0.6977
w .i
[torch.FloatTensor of size 5x5] ww


>>> mask = Variable(torch.zeros(5, 5))
>>> mask 程

Variable containing: 黑
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
[torch.FloatTensor of size 5x5]

>>> input.masked_fill(mask == 0, -1e9)


Variable containing:
-1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09
m
co
-1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09
.
-1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09ma-1.0000e+09
i
-1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09
t he -1.0000e+09
i
-1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09
[torch.FloatTensor of size 5x5] w w.
w





输入参数:

# 我们令输入的 query, key, value 都相同, 位置编码的输出


query = key = value = pe_result
Variable containing:
( 0 ,.,.) =
46.5196 16.2057 -41.5581 ... -16.0242 -17.8929 -43.0405
-32.6040 16.1096 -29.5228 ... 4.2721 20.6034 -1.2747
-18.6235 14.5076 -2.0105 ... m
15.6462 -24.6081 -30.3391
0.0000 -66.1486 -11.5123 ...
co
20.1519 -4.6823 a. 0.4916
e im
( 1 ,.,.) = ith
-24.8681 7.5495 -5.0765 ...
.
-7.5992ww-26.6630 40.9517
13.1581 -3.1918 -30.9001 ...
w
25.1187 -26.4621 2.9542
-49.7690 -42.5019 8.0198 ...

-5.4809 25.9403 -27.4931

-52.2775 10.4006 0.0000 ... 程-1.9985 7.0106 -0.5189

[torch.FloatTensor of size 2x4x512]

调用:
attn, p_attn = attention(query, key, value)
print("attn:", attn)
print("p_attn:", p_attn)

m
co
输出效果: i ma
.
the
i
ww.
# 将得到两个结果 w
的注意力表示
# query :


attn: Variable containing:程
( 0 ,.,.) = 马
12.8269 7.7403
黑41.2225 ... 1.4603 27.8559 -12.2600
12.4904 0.0000 24.1575 ... 0.0000 2.5838 18.0647
-32.5959 -4.6252 -29.1050 ... 0.0000 -22.6409 -11.8341
8.9921 -33.0114 -0.7393 ... 4.7871 -5.7735 8.3374

( 1 ,.,.) =
-25.6705 -4.0860 -36.8226 ... 37.2346 -27.3576 2.5497
-16.6674 73.9788 -33.3296 ... 28.5028 -5.5488 -13.7564
0.0000 -29.9039 -3.0405 ... 0.0000 14.4408 14.8579
30.7819 0.0000 21.3908 ... -29.0746 0.0000 -5.8475
[torch.FloatTensor of size 2x4x512] m
. co
注意力张量 m a
# :
h ei
p_attn: Variable containing:
it
(0 ,.,.) =
w w.
1 0 0 0 w
0 1 0 0 员

0 0 1 0 程
0 0 0 1 马

(1 ,.,.) =
1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1
[torch.FloatTensor of size 2x4x4]

带有mask的输入参数:
com
query = key = value = pe_result a.
e im
# 令 mask为一个 的零张量
2x4x4
. ith
mask = Variable(torch.zeros(2, 4, 4)) w
ww





调用:
attn, p_attn = attention(query, key, value, mask=mask)
print("attn:", attn)
print("p_attn:", p_attn)

m
co
带有mask的输出效果: i ma
.
the
i
w.
# query 的注意力表示 : ww
attn: Variable containing: 员
( 0 ,.,.) =


0.4284 -7.4741 马 ...
8.8839 1.5618 0.5063 0.5770
0.4284 -7.4741

8.8839 ... 1.5618 0.5063 0.5770
0.4284 -7.4741 8.8839 ... 1.5618 0.5063 0.5770
0.4284 -7.4741 8.8839 ... 1.5618 0.5063 0.5770

( 1 ,.,.) =
-2.8890 9.9972 -12.9505 ... 9.1657 -4.6164 -0.5491
-2.8890 9.9972 -12.9505 ... 9.1657 -4.6164 -0.5491
-2.8890 9.9972 -12.9505 ... 9.1657 -4.6164 -0.5491
-2.8890 9.9972 -12.9505 ... 9.1657 -4.6164 -0.5491
[torch.FloatTensor of size 2x4x512]
m
# 注意力张量 : . co
m a
p_attn: Variable containing:
h ei
(0 ,.,.) =
it
0.2500 0.2500 0.2500 0.2500
w w.
0.2500 0.2500 0.2500 0.2500 w
0.2500 0.2500 0.2500 0.2500 员

0.2500 0.2500 0.2500 0.2500程

(1 ,.,.) =

0.2500 0.2500 0.2500 0.2500
0.2500 0.2500 0.2500 0.2500
0.2500 0.2500 0.2500 0.2500
0.2500 0.2500 0.2500 0.2500
[torch.FloatTensor of size 2x4x4]

2.3.2注意力机制总结: m
co
学习了什么是注意力: im
a .
e
我们观察事物时,之所以能够快速判断一种事物 . it (当然允许判断是错误的), 是因为我
h
们大脑能够很快把注意力放在事物最具有辨识度的部分从而作出判断,而并非是从
ww
w
头到尾的观察一遍事物后,才能有判断结果

员 . 正是基于这样的理论,就产生了注意
力机制. 马

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

m
学习了Q, K, V的比喻解释: a . co
Q是一段准备被概括的文本; K是给出的提示 eim ; V是大脑中的对提示K的延伸.
h
it
当Q=K=V时, 称作自注意力机制ww. w .




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

学习并实现了注意力计算规则的函数: attention
它的输入就是Q,K,V以及mask和dropout, mask.c用于掩码
om , dropout用于随机置0.
a
它的输出有两个, query的注意力表示以及注意力张量
h eim .
it
w w.
w




2.3.3 多头注意力机制 黑

学习目标:
了解多头注意力机制的作用.
掌握多头注意力机制的实现过程.
com
什么是多头注意力机制: m a.
h ei
从多头注意力的结构图中,貌似这个所谓的多个头就是指多组线性变换层,其实并不
w .i
t
是,我只有使用了一组线性变化层,即三个变换张量对ww Q,K,V分别进行线性变换,这
些变换不会改变原有张量的尺寸,因此每个变换矩阵都是方阵,得到输出结果后,多头


的作用才开始显现,每个头开始从词义层面分割输出的张量,也就是每个头都想获得一


组Q,K,V进行注意力机制的计算,但是句子中的每个词的表示只获得一部分,也就是

只分割了最后一维的词嵌入向量. 这就是所谓的多头,将每个头的获得的输入送到注意
力机制中, 就形成多头注意力机制.
多头注意力机制结构图:

m
. co
i ma
the
i
w.
ww




m
. co
m a
h ei
it
w w.
w




c om
m a.
多头注意力机制的作用: t h ei
.i
这种结构设计能让每个注意力机制去优化每个词汇的不同特征部分,从而均衡同一种注
w
ww
意力机制可能产生的偏差,让词义拥有来自更多元的表达,实验表明可以从而提升模型

效果. 马


多头注意力机制的代码实现:
# 用于深度拷贝的copy工具包
import copy

#首先需要定义克隆函数 因为在多头注意力机制的实现中 用到多个结构相同的线性层


, , .
#我们将使用 函数将他们一同初始化在一个网络层列表对象中 之后的结构中也会用到该函数.
clone .
def clones(module, N):
用于生成相同网络层的克隆函数 它的参数
""" 表示要克隆的目标网络层 代表需要克
, module om
c , N
隆的数量 """ .
ma ,
在函数中 我们通过 循环对
# , 进行 次深度拷贝 使其每个
for module 成为独立的层,
N ei
h . module
然后将其放在
# 类型的列表中存放
nn.ModuleList
.i
t
ww
return nn.ModuleList([copy.deepcopy(module)
w for _ in range(N)])






# 我们使用一个类来实现多头注意力机制的处理
class MultiHeadedAttention(nn.Module):
def __init__(self, head, embedding_dim, dropout=0.1):
""" 在类的初始化时 会传入三个参数, 代表头数,
, head embedding_dim 代表词嵌入的维
度,
dropout代表进行dropout操作时置0比率,默认是0.1."""
super(MultiHeadedAttention, self).__init__()

# 在函数中,首先使用了一个测试中常用的 语句,判断h是否能被d_model整除,
assert
# 这是因为我们之后要给每个头分配等量的词特征 也就是embedding_dim/head个.
.
m
assert embedding_dim % head == 0
. co
ma
# 得到每个头获得的分割词向量维度 d_k
h ei
self.d_k = embedding_dim // head it
w w.
# 传入头数 h
w

self.head = head 序

然后获得线性层对象,通过马
# 黑 nn的Linear实例化,它的内部变换矩阵是embedding_dim
,然后使用clones函数克隆四个,
x embedding_dim
为什么是四个呢,这是因为在多头注意力中,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比率为传进
来的参数dropout. m
self.dropout = nn.Dropout(p=dropout) o
a .c
m
def ei
forward(self, query, key, value, mask=None):
h
"""前向逻辑函数, 它的输入参数有四个,前三个就是注意力机制需要的 it Q, K, ,
V
最后一个是注意力机制中可能需要的maskww掩码张量,默认是 w. None. """

# 如果存在掩码张量mask


if mask is not None: 程
# 使用unsqueeze拓展维度


mask = mask.unsqueeze(0)

# 接着,我们获得一个batch_size的变量,他是query尺寸的第1个数字,代表有多少条
样本.
batch_size = query.size(0)

之后就进入多头处理环节
#
首先利用zip将输入QKV与三个线性层组到一起,然后使用for循环,将输入QKV分别传到
#
线性层中,
# 做完线性变换后,开始为每个头分割输入,这里使用view方法对线性变换的结果进行维度
重塑,多加了一个维度h,代表头数, m
# 这样就意味着每个头可以获得一部分词特征组成的句子,其中的 co -1代表自适应维度,
.
# 计算机会根据这种变换自动计算这里的值.然后对第二维和第三维进行转置操作,
m a
i
# 为了让代表句子长度维度和词向量维度能够相邻,这样注意力机制才能找到词义与句子位
he
t
置的关系, w.
i
# 从attention函数中可以看到,利用的是原始输入的倒数第一和第二维
ww .这样我们就得到
了每个头的输入. 员
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维张量,我们需要将其转
换为输入的形状以方便后续的计算,
# 因此这里开始进行第一步处理环节的逆操作,先对第二和第三维进行转置,然后使用
m
contiguous方法, . co
# 这个方法的作用就是能够让转置后的张量应用view im 方法,否则将无法直接使用,
a
# 所以,下一步就是使用view重塑形状,变成和输入形状相同
h e .
it
w.
x = x.transpose(1, 2).contiguous().view(batch_size, -1, self.head *
self.d_k) ww

# 最后使用线性层列表中的最后一个线性层对输入进行线性变换得到最终的多头注意力结构


的输出. 黑

return self.linears[-1](x)

tensor.view 演示:
>>> x = torch.randn(4, 4)
>>> x.size()
torch.Size([4, 4])
>>> y = x.view(16) m
>>> y.size()
. co
a
torch.Size([16])
e im
>>> z = x.view(-1, 8) # the size -1 is inferred h from other dimensions
>>> z.size() . it
w
torch.Size([2, 8]) ww

>>> a = torch.randn(1, 2, 3, 4) 序

>>> a.size() 马
torch.Size([1, 2, 3, 4]) 黑
>>> b = a.transpose(1, 2) # Swaps 2nd and 3rd dimension
>>> b.size()
torch.Size([1, 3, 2, 4])
>>> c = a.view(1, 3, 2, 4) # Does not change tensor layout in memory
>>> c.size()
torch.Size([1, 3, 2, 4])
>>> torch.equal(b, c)
False

m
演示:
torch.transpose . co
i ma
the
i
>>> x = torch.randn(2, 3)
w.
>>> x ww

tensor([[ 1.0028, -0.9893, 0.5809],
[-0.1669, 0.7299, 序 0.4942]])

马 1)
>>> torch.transpose(x, 0,

tensor([[ 1.0028, -0.1669],
[-0.9893, 0.7299],
[ 0.5809, 0.4942]])

实例化参数:

# 头数 head
. co
m
head = 8
m a
h ei
# 词嵌入维度 embedding_dim
w.
it
embedding_dim = 512
ww

# 置零比率 dropout 序
dropout = 0.2 程

输入参数:

# 假设输入的 , , 仍然相等
Q K V
query = value = key = pe_result

# 输入的掩码张量 mask
com
mask = Variable(torch.zeros(8, 4, 4))
a.
e im
ith
w.
ww
调用: 序




mha = MultiHeadedAttention(head, embedding_dim, dropout)
mha_result = mha(query, key, value, mask)
print(mha_result)
输出效果:

tensor([[[-0.3075, 1.5687, -2.5693, ..., -1.1098, 0.0878, -3.3609],


m -1.1488, -1.3984],
[ 3.8065, -2.4538, -0.3708, co
..., -1.5205,
.
[ 2.4190, 0.5376, -2.8475, ..., ma1.4218, -0.4488, -0.2984],
i
[ 2.9356, 0.3620, -3.8722, he -0.7996, 0.1468, 1.0345]],
...,
t
w .i
ww ...,
[[ 1.1423, 0.6038, 0.0954, 2.2679, -5.7749, 1.4132],
[ 2.4066, -0.2777, 2.8102,
员 ..., 0.1137, -3.9517, -2.9246],
[ 5.8201, 1.1534, 序-1.9191, ..., 0.1410, -7.6110, 1.0046],

[ 3.1209, 1.0008,
马 -0.5317, ..., 2.8619, -6.3204, -1.3435]]],

grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])

2.3.3多头注意力机制总结:
学习了什么是多头注意力机制:
每个头开始从词义层面分割输出的张量,也就是每个头都想获得一组Q,K,V进行
注意力机制的计算,但是句子中的每个词的表示只获得一部分,也就是只分割了最
co
m
后一维的词嵌入向量. 这就是所谓的多头.将每个头的获得的输入送到注意力机制中
im
a . ,
就形成了多头注意力机制. it
h e
w.
ww


学习了多头注意力机制的作用马

:

这种结构设计能让每个注意力机制去优化每个词汇的不同特征部分,从而均衡同一

种注意力机制可能产生的偏差,让词义拥有来自更多元的表达,实验表明可以从而
提升模型效果.

学习并实现了多头注意力机制的类: MultiHeadedAttention
因为多头注意力机制中需要使用多个相同的线性层, 首先实现了克隆函数clones.
clones函数的输入是module,N,分别代表克隆的目标层,和克隆个数 co
m .
a .
clones函数的输出是装有N个克隆层的Module列表
m
h ei .
t
接着实现MultiHeadedAttention类, 它的初始化函数输入是
w w .i h, d_model, dropout分别
代表头数,词嵌入维度和置零比率.员 w
它的实例化对象输入是Q, K,马V以及掩码张量

序 mask.

它的实例化对象输出是通过多头注意力机制处理的
黑 Q的注意力表示.
2.3.4 前馈全连接层
学习目标:
了解什么是前馈全连接层及其它的作用. m
co
掌握前馈全连接层的实现过程. ima
.
the
i
w.
ww
什么是前馈全连接层: 序

在Transformer中前馈全连接层就是具有两层线性层的全连接网络


程 .

前馈全连接层的作用:
考虑注意力机制可能对复杂过程的拟合程度不够, 通过增加两层网络来增强模型的能力.

前馈全连接层的代码分析: m
. co
# 通过类 PositionwiseFeedForward 来实现前馈全连接层 m a
class PositionwiseFeedForward(nn.Module): th
ei
i
w.
def __init__(self, d_model, d_ff, dropout=0.1):
初始化函数有三个输入参数分别是
""" 和 ww ,第一个是线性
d_model, d_ff, dropout=0.1
层的输入维度也是第二个线性层的输出维度, 员
因为我们希望输入通过前馈全连接层后输入和输出的维度不变

序 . 第二个参数d_ff就是第
二个线性层的输入维度和第一个线性层的输出维度
马 .
最后一个是dropout黑置0比率."""
super(PositionwiseFeedForward, self).__init__()

# 首先按照我们预期使用 实例化了两个线性层对象,
nn self.w1 self.w2和
# 它们的参数分别是 和
d_model, d_ff d_ff, d_model
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): m


"""输入参数为 ,代表来自上一层的输出
x """ . co
# 首先经过第一个线性层,然后使用 中 函数进行激活,
Funtional reluim
a
之后再使用 进行随机置 ,最后通过第二个线性层 ,返回最终结果. h e
# dropout 0
. it w2
w
return self.w2(self.dropout(F.relu(self.w1(x))))
ww



ReLU 函数公式: ReLU(x)=max(0, x)黑马

ReLU 函数图像:
m
. co
i ma
the
i
w.
ww




m
. co
m a
h ei
it
w w.
w
实例化参数: 序




d_model = 512

# 线性变化的维度
d_ff = 64

dropout = 0.2

输入参数: com
a.
e im
th
# 输入参数 可以是多头注意力机制的输出
x w . i
x = mha_result ww
tensor([[[-0.3075, 1.5687, -2.5693, 员..., -1.1098, 0.0878, -3.3609],
序 ..., -1.5205, -1.1488, -1.3984],
[ 3.8065, -2.4538, -0.3708,


[ 2.4190, 0.5376, -2.8475, ..., 1.4218, -0.4488, -0.2984],

[ 2.9356, 0.3620, -3.8722, ..., -0.7996, 0.1468, 1.0345]],

[[ 1.1423, 0.6038, 0.0954, ..., 2.2679, -5.7749, 1.4132],


[ 2.4066, -0.2777, 2.8102, ..., 0.1137, -3.9517, -2.9246],
[ 5.8201, 1.1534, -1.9191, ..., 0.1410, -7.6110, 1.0046],
[ 3.1209, 1.0008, -0.5317, ..., 2.8619, -6.3204, -1.3435]]],
grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])

m
调用: . co
i ma
the
i
w w. d_ff,
ff = PositionwiseFeedForward(d_model, dropout)
ff_result = ff(x)
w

print(ff_result) 序


输出效果:

tensor([[[-1.9488e+00, -3.4060e-01, -1.1216e+00, ..., 1.8203e-01,


-2.6336e+00, 2.0917e-03],
[-2.5875e-02, 1.1523e-01, -9.5437e-01, ..., -2.6257e-01,
-5.7620e-01, -1.9225e-01],
[-8.7508e-01, 1.0092e+00, -1.6515e+00, ..., m 3.4446e-02,
-1.5933e+00, -3.1760e-01], . co
a
[-2.7507e-01, im ..., 1.0530e+00,
4.7225e-01, -2.0318e-01,
e
-3.7910e-01, -9.7730e-01]], th
i
w w.
[[-2.2575e+00, -2.0904e+00, w2.9427e+00,..., 9.6574e-01,

-1.9754e+00, 1.2797e+00],


[-1.5114e+00, -4.7963e-01, 1.2881e+00, ..., -2.4882e-02,

-1.5896e+00, -1.0350e+00],

[ 1.7416e-01, -4.0688e-01, 1.9289e+00, ..., -4.9754e-01,
-1.6320e+00, -1.5217e+00],
[-1.0874e-01, -3.3842e-01, 2.9379e-01, ..., -5.1276e-01,
-1.6150e+00, -1.1295e+00]]], grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])

2.3.4前馈全连接层总结:
学习了什么是前馈全连接层: a . co
m
在Transformer中前馈全连接层就是具有两层线性层的全连接网络
th
e im .

w .i
ww

学习了前馈全连接层的作用: 程序

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

2.3.5 规范化层 马


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

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




规范化层的代码实现:
# 通过 LayerNorm 实现规范化层的类
class LayerNorm(nn.Module):
def __init__(self, features, eps=1e-6):
""" 初始化函数有两个参数 一个是 , 表示词嵌入的维度,
features,
另一个是 它是一个足够小的数 在规范化公式的分母中出现,
eps ,
防止分母为 默认是
0. 1e-6."""
super(LayerNorm, self).__init__()
com
根据features的形状初始化两个参数张量a2,和b2e,第一个初始化为 a.
1张量,
# im
也就是里面的元素都是1,第二个初始化为0张量,也就是里面的元素都是
#
it
h 0,这两个张量
就是规范化层的参数, w .
ww
# 因为直接对上一层得到的结果做规范化公式计算,将改变结果的正常表征,因此就需要有
参数作为调节因子, 序

# 使其即能满足规范化要求,又能不改变针对目标的表征
程 .最后使用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减去均值除以标准差获
#
得规范化的结果,
# 最后对结果乘以我们的缩放参数,即a2,*号代表同型点乘,即对应位置进行乘法操作,
m
加上位移参数b2.返回即可. a. co
mean = x.mean(-1, keepdim=True)
im
std = x.std(-1, keepdim=True) he
t
/i(std + self.eps) + self.b2
return self.a2 * (x - mean) .
w
ww




实例化参数: 黑

features = d_model = 512


eps = 1e-6

输入参数: m
. co
ma
ei
# 输入 来自前馈全连接层的输出
x ti
h
x = ff_result
w w.
tensor([[[-1.9488e+00, -3.4060e-01, w
-1.1216e+00, ..., 1.8203e-01,
-2.6336e+00, 员
2.0917e-03],

[-2.5875e-02, 程
1.1523e-01, -9.5437e-01, ..., -2.6257e-01,
-5.7620e-01, 马
-1.9225e-01],

[-8.7508e-01, 1.0092e+00, -1.6515e+00, ..., 3.4446e-02,
-1.5933e+00, -3.1760e-01],
[-2.7507e-01, 4.7225e-01, -2.0318e-01, ..., 1.0530e+00,
-3.7910e-01, -9.7730e-01]],

[[-2.2575e+00, -2.0904e+00, 2.9427e+00, ..., 9.6574e-01,


-1.9754e+00, 1.2797e+00],
[-1.5114e+00, -4.7963e-01, 1.2881e+00, ..., -2.4882e-02,
-1.5896e+00, -1.0350e+00],
[ 1.7416e-01, -4.0688e-01, 1.9289e+00, ..., -4.9754e-01,
-1.6320e+00, -1.5217e+00], m
[-1.0874e-01, -3.3842e-01, 2.9379e-01, ..., . co-5.1276e-01,
a
im
-1.6150e+00, -1.1295e+00]]], grad_fn=<AddBackward0>)
e
h
torch.Size([2, 4, 512]) it
w.
ww



调用: 黑

ln = LayerNorm(feature, eps)
ln_result = ln(x)
print(ln_result)

输出效果:
m
. co
ma
tensor([[[ 2.2697, 1.3911, -0.4417, ...,
h ei 0.9937, 0.6589, -1.1902],
[ 1.5876, t
0.5182, 0.6220, i..., 0.9836, 0.0338, -1.3393],
.
[ 1.8261, ww ...,
2.0161, 0.2272,
w 0.3004, 0.5660, -0.9044],
[ 1.5429, 1.3221, -0.2933, ..., 0.0406, 1.0603, 1.4666]],


[[ 0.2378, 0.9952,程 1.2621, ..., -0.4334, -1.1644, 1.2082],


[-1.0209, 0.6435, 0.4235, ..., -0.3448, -1.0560, 1.2347],
[-0.8158, 0.7118, 0.4110, ..., 0.0990, -1.4833, 1.9434],
[ 0.9857, 2.3924, 0.3819, ..., 0.0157, -1.6300, 1.2251]]],
grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])

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

员.



学习并实现了规范化层的类: LayerNorm
它的实例化参数有两个, features和eps,分别表示词嵌入特征大小,和一个足够小
的数.
它的输入参数x代表来自上一层的输出.
它的输出就是经过规范化的特征表示.
com
a.
eim
th
2.3.6 子层连接结构 ww
w . i





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





m
. co
m a
h ei
it
w w.
w
子层连接结构的代码分析: 员


# 使用 SublayerConnection
黑来实现子层连接结构的类

class SublayerConnection(nn.Module):
def __init__(self, size, dropout=0.1):
它输入参数有两个
""" 以及 , 一般是都是词嵌入维度的大小,
, size dropout size
本身是对模型结构中的节点数进行随机抑制的比率,
dropout
又因为节点被抑制等效就是该节点的输出都是 ,因此也可以把 看作是对输出矩
0 dropout
阵的随机置 的比率
0 .
"""
super(SublayerConnection, self).__init__()
# 实例化了规范化对象 self.norm
self.norm = LayerNorm(size)
# 又使用 中预定义的
nn droupout 实例化一个 self.dropout m .
o 对象
self.dropout = nn.Dropout(p=dropout) .c a
e im
def forward(self, x, sublayer): t h
i
""" 前向逻辑函数中 接收上一个层或者子层的输入作为第一个参数,
,
w w.
将该子层连接中的子层函数作为第二个参数

w """

# 我们首先对输出进行规范化,然后将结果传给子层处理,之后再对子层进行
程 dropout操
作, 黑

# 随机停止一些网络中神经元的作用,来防止过拟合 . 最后还有一个add操作,
# 因为存在跳跃连接,所以是将输入x与dropout后的子层输出结果相加作为最终的子层连
接输出.
return x + self.dropout(sublayer(self.norm(x)))
实例化参数

size = 512
m
dropout = 0.2
. co
head = 8
i ma
d_model = 512
the
i
w.
ww


输入参数: 黑

# 令 为位置编码器的输出
x
x = pe_result
mask = Variable(torch.zeros(8, 4, 4))

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

# 使用 lambda 获得一个函数类型的子层
m
sublayer = lambda x: self_attn(x, x, x, mask)
. co
m a
h ei
it
w w.
w
调用: 员




sc = SublayerConnection(size, dropout)
sc_result = sc(x, sublayer)
print(sc_result)
print(sc_result.shape)

输出效果:
m
. co
tensor([[[ 14.8830, 22.4106, -31.4739, ..., 21.0882, a -10.0338,
-0.2588], e im
h
[-25.1435, 2.9246, -16.1235, ..., . it 10.5069, -7.1007,
w
-3.7396], ww
[ 0.1374, 32.6438, 12.3680,
员 ..., -12.0251, -40.5829,
2.2297], 序
[-13.3123, 55.4689,

马9.5420, ..., -12.6622, 23.4496,
21.1531]], 黑
[[ 13.3533, 17.5674, -13.3354, ..., 29.1366, -6.4898,
35.8614],
[-35.2286, 18.7378, -31.4337, ..., 11.1726, 20.6372,
29.8689],
[-30.7627, 0.0000, -57.0587, ..., 15.0724, -10.7196,
-18.6290],
[ -2.7757, -19.6408, 0.0000, ..., 12.7660, 21.6843,
-35.4784]]],
grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])
m
. co
i ma
the
i
子层连接结构总结: w.
2.3.6 ww
什么是子层连接结构: 序员

如图所示,输入到每个子层以及规范化层的过程中,还使用了残差链接(跳跃连


接),因此我们把这一部分结构整体叫做子层连接(代表子层及其链接结构), 在每
个编码器层中,都有两个子层,这两个子层加上周围的链接结构就形成了两个子层
连接结构.

学习并实现了子层连接结构的类: SublayerConnection
类的初始化函数输入参数是size, dropout, 分别代表词嵌入大小和置零比率.
m
它的实例化对象输入参数是x, sublayer, 分别代表上一层输出以及子层的函数表示
a . co .
m
它的输出就是通过子层连接结构处理的输出 ei
th . i
w w.
w




2.3.7 编码器层 黑

学习目标:
了解编码器层的作用.
掌握编码器层的实现过程.
com
m a.
编码器层的作用: t h ei
w .i
作为编码器的组成单元, 每个编码器层完成一次对输入的特征提取过程 , 即编码过程.
ww




编码器层的构成图: 黑
m
. co
i ma
the
i
w.
ww





编码器层的代码分析:
# 使用 类实现编码器层
EncoderLayer
class EncoderLayer(nn.Module):
def __init__(self, size, self_attn, feed_forward, dropout):
它的初始化函数参数有四个,分别是 ,其实就是我们词嵌入维度的大小,它也将作
""" size
为我们编码器层的大小 ,
第二个 ,之后我们将传入多头自注意力子层实例化对象 并且是自注意力机
self_attn ,
制 , o m
第三个是 之后我们将传入前馈全连接层实例化对象 最后一个是置0
feed_froward, a .c ,
比率 dropout."""
he
i m
it
super(EncoderLayer, self).__init__()
.
ww
# 首先将self_attn和feed_forward传入其中.
w

self.self_attn = self_attn


self.feed_forward = feed_forward


# 如图所示 编码器层中有两个子层连接结构 所以使用
, , clones 函数进行克隆
self.sublayer = clones(SublayerConnection(size, dropout), 2)
# 把 传入其中
size
self.size = size

def forward(self, x, mask):


函数中有两个输入参数,x和mask,分别代表上一层的输出,和掩码张量
"""forward
mask."""
# 里面就是按照结构图左侧的流程. 首先通过第一个子层连接结构,其中包含多头自注意力
子层,
# 然后通过第二个子层连接结构,其中包含前馈全连接子层. c最后返回结果
om .
.
x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
ima
return self.sublayer[1](x, self.feed_forward)
e h
. it
w
ww


实例化参数: 马

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))
m
. co
ima
the
调用: w.
i
ww



el = EncoderLayer(size, self_attn, ff, dropout)


el_result = el(x, mask)
print(el_result)
print(el_result.shape)

输出效果:

tensor([[[ 33.6988, -30.7224, 20.9575, ..., m -48.5658,


5.2968,
20.0734], . co
a
[-18.1999, 34.2358, 40.3094, ..., im10.1102, 58.3381,
h e
58.4962],
. it
[ 32.1243, 16.7921, -6.8024,w ..., 23.0022, -18.1463,
-17.1263], ww

[ -9.3475, -3.3605, -55.3494, ..., 43.6333, -0.1900,

0.1625]], 程


[[ 32.8937, -46.2808, 8.5047, ..., 29.1837, 22.5962,
-14.4349],
[ 21.3379, 20.0657, -31.7256, ..., -13.4079, -44.0706,
-9.9504],
[ 19.7478, -1.0848, 11.8884, ..., -9.5794, 0.0675,
-4.7123],
[ -6.8023, -16.1176, 20.9476, ..., -6.5469, 34.8391,
-14.9798]]],
grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])

com
a.
e im
th
2.3.7编码器层总结: w . i
ww
学习了编码器层的作用: 员

作为编码器的组成单元, 每个编码器层完成一次对输入的特征提取过程

程 , 即编码过程.

学习并实现了编码器层的类: EncoderLayer
类的初始化函数共有4个, 别是size,其实就是我们词嵌入维度的大小. 第二个
self_attn,之后我们将传入多头自注意力子层实例化对象, 并且是自注意力机制. 第
三个是feed_froward, 之后我们将传入前馈全连接层实例化对象. 最后一个是置0比率
dropout.

实例化对象的输入参数有2个,x代表来自上一层的输出 m
, mask代表掩码张量.
o
它的输出代表经过整个编码层的特征表示im. a.c
e
i th
w.
ww


2.3.8 编码器 黑

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

o m
编码器的作用: m a .c
h ei
编码器用于对输入进行指定的特征提取过程i, 也称为编码 , 由N个编码器层堆叠而成.
t
w.
ww


编码器的结构图: 马

c om
a.
eim
ith
w .
ww


编码器的代码分析: 马


# 使用 Encoder 类来实现编码器
class Encoder(nn.Module):
def __init__(self, layer, N):
""" 初始化函数的两个参数分别代表编码器层和编码器层的个数"""
super(Encoder, self).__init__()
# 首先使用 函数克隆 个编码器层放在
clones N self.layers 中
self.layers = clones(layer, N)
# 再初始化一个规范化层 它将用在编码器的最后面
, .
self.norm = LayerNorm(layer.size)

def forward(self, x, mask):


m
函数的输入和编码器层相同 代表上一层的输出, mask代表掩码张量"""
"""forward , x
. co
# 首先就是对我们克隆的编码器层进行循环,每次都会得到一个新的x, im
a
# 这个循环的过程,就相当于输出的 经过了 个编码器层的处理.
x
it
e
hN
# 最后再通过规范化层的对象 进行处理,最后返回结果.
.
self.norm
w
for layer in self.layers:ww
x = layer(x, mask)员
return self.norm(x) 序



实例化参数:

# 第一个实例化参数 它是一个编码器层的实例化对象 因此需要传入编码器层的参数


layer, ,
# 又因为编码器层中的子层是不共享的 因此需要使用深度拷贝各个对象
, .
size = 512
head = 8
d_model = 512
d_ff = 64
m
c = copy.deepcopy
. co
attn = MultiHeadedAttention(head, d_model) ma
ei
ff = PositionwiseFeedForward(d_model, d_ff,hdropout)
t
dropout = 0.2
w.i
ww
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(en_result)
com
print(en_result.shape) a.
eim
ith
w .
ww

输出效果: 程



tensor([[[-0.2081, -0.3586, -0.2353, ..., 2.5646, -0.2851, 0.0238],
[ 0.7957, -0.5481, 1.2443, ..., 0.7927, 0.6404, -0.0484],
[-0.1212, 0.4320, -0.5644, ..., 1.3287, -0.0935, -0.6861],
[-0.3937, -0.6150, 2.2394, ..., -1.5354, 0.7981, 1.7907]],
[[-2.3005, 0.3757, 1.0360, ..., 1.4019, 0.6493, -0.1467],
[ 0.5653, 0.1569, 0.4075, ..., -0.3205, 1.4774, -0.5856],
[-1.0555, 0.0061, -1.8165, ..., -0.4339, -1.8780, 0.2467],
[-2.1617, -1.5532, -1.4330, ..., -0.9433, -0.5304, -1.7022]]],
grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])
m
. co
i ma
the
i
编码器总结: w.
2.3.8 ww
学习了编码器的作用: 序员

编码器用于对输入进行指定的特征提取过程

马 , 也称为编码, 由N个编码器层堆叠而成.

学习并实现了编码器的类: Encoder
类的初始化函数参数有两个,分别是layer和N,代表编码器层和编码器层的个数.
forward函数的输入参数也有两个, 和编码器层的forward相同, x代表上一层的输出,
mask代码掩码张量.

编码器类的输出就是Transformer中编码器的特征提取表示om , 它将成为解码器的输入
的一部分. ma
. c
h ei
it
w.
ww





2.4 解码器部分实现
学习目标
了解解码器中各个组成部分的作用.
掌握解码器中各个组成部分的实现过程. om
c
a.
e im
th
解码器部分: w . i
ww
由N个解码器层堆叠而成 员

每个解码器层由三个子层连接结构组成


第一个子层连接结构包括一个多头自注意力子层和规范化层以及一个残差连接

第二个子层连接结构包括一个多头注意力子层和规范化层以及一个残差连接
第三个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接
m
. co
i ma
the
i
w.
ww




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

2.4.1 解码器层 马


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

解码器层的作用: co
m
.
作为解码器的组成单元, 每个解码器层根据给定的输入向目标方向进行特征提取操作,
e im
a
即解码过程. .i
th
w
ww


解码器层的代码实现: 黑

# 使用 DecoderLayer的类实现解码器层
class DecoderLayer(nn.Module):
def __init__(self, size, self_attn, src_attn, feed_forward, dropout):
初始化函数的参数有5个, 分别是size,代表词嵌入的维度大小, 同时也代表解码器层
"""
的尺寸,
第二个是self_attn,多头自注意力对象,也就是说这个注意力机制需要Q=K=V,
第三个是src_attn,多头注意力对象,这里Q!=K=V, 第四个是前馈全连接层对象,
最后就是droupout置0比率.
"""
super(DecoderLayer, self).__init__()
m
# 在初始化函数中, 主要就是将这些输入传到类中 a. co
self.size = size
eim
self.self_attn = self_attn h
self.src_attn = src_attn .it
w
ww
self.feed_forward = feed_forward
# 按照结构图使用 函数克隆三个子层连接对象
clones 员 .

self.sublayer = clones(SublayerConnection(size, dropout), 3)


def forward(self, 黑
x, memory, source_mask, target_mask):
函数中的参数有 个,分别是来自上一层的输入 ,
"""forward 4 x
来自编码器层的语义存储变量 , 以及源数据掩码张量和目标数据掩码张量.
mermory
"""
# 将 表示成 方便之后使用
memory m
m = memory

# 将x传入第一个子层结构,第一个子层结构的输入分别是x和self-attn函数,因为是自
注意力机制,所以Q,K,V都是x,
# 最后一个参数是目标数据掩码张量,这时要对目标数据进行遮掩,因为此时模型可能还没
有生成任何目标数据, m
# 比如在解码器准备生成第一个字符或词汇时,我们其实已经传入了第一个字符以便计算损
. co
失, im
a
# 但是我们不希望在生成第一个字符时模型能利用这个信息,因此我们会将其遮掩,同样生
h e
成第二个字符或词汇时, . it
w
# 模型只能使用第一个字符或词汇信息,第二个字符以及之后的信息都不允许被模型使用
ww .
员 x: self.self_attn(x, x, x,
x = self.sublayer[0](x, lambda
target_mask)) 序


# 接着进入第二个子层,这个子层中常规的注意力机制,
黑 q是输入x; k,v是编码层输出
memory,
# 同样也传入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)
com
a.
eim
实例化参数: . ith
w
ww

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

m
输入参数: . co
i ma
the
i
w w.
# x是来自目标数据的词嵌入表示, 但形式和源数据的词嵌入表示相同 , 这里使用per充当.
x = pe_result
w


# memory是来自编码器的输出 程

memory = en_result 黑
# 实际中 和
source_mask target_mask 并不相同 这里为了方便计算使他们都为mask
,
mask = Variable(torch.zeros(8, 4, 4))
source_mask = target_mask = mask

调用:
m
. co
m a
dl = DecoderLayer(size, self_attn, src_attn, ff,
h ei dropout)
t
dl_result = dl(x, memory, source_mask, target_mask)
print(dl_result) w.i
print(dl_result.shape) ww





输出效果:

tensor([[[ 1.9604e+00, 3.9288e+01, -5.2422e+01, ..., 2.1041e-01,


-5.5063e+01, 1.5233e-01],
[ 1.0135e-01, -3.7779e-01, 6.5491e+01, ..., 2.8062e+01,
-3.7780e+01, -3.9577e+01],
[ 1.9526e+01, -2.5741e+01, 2.6926e-01, ..., -1.5316e+01,
1.4543e+00, 2.7714e+00], m
[-2.1528e+01, 2.0141e+01, 2.1999e+01, ...,co 2.2099e+00,
a.
-1.7267e+01, -1.6687e+01]],
e im
ith
[[ 6.7259e+00,
w.
-2.6918e+01, 1.1807e+01,..., -3.6453e+01,
-2.9231e+01, 1.1288e+01], ww
[ 7.7484e+01, 员
-5.0572e-01, -1.3096e+01,
..., 3.6302e-01,
1.9907e+01, -1.2160e+00],序

[ 2.6703e+01, 马 -3.1590e+01,
4.4737e+01, ..., 4.1540e-03,
5.2587e+00, 黑
5.2382e+00],
[ 4.7435e+01, -3.7599e-01, 5.0898e+01, ..., 5.6361e+00,
3.5891e+01, 1.5697e+01]]], grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])
2.4.1 解码器层总结:
学习了解码器层的作用:
作为解码器的组成单元, 每个解码器层根据给定的输入向目标方向进行特征提取操
作,即解码过程. .c
om
m a
h ei
it
w.
学习并实现了解码器层的类: DecoderLayer
ww

类的初始化函数的参数有 程
序5个, 分别是size,代表词嵌入的维度大小, 同时也代表解码
器层的尺寸,第二个是黑
马 self_attn,多头自注意力对象,也就是说这个注意力机制需
要Q=K=V,第三个是 src_attn,多头注意力对象,这里Q!=K=V, 第四个是前馈全连
接层对象,最后就是droupout置0比率.
forward函数的参数有4个,分别是来自上一层的输入x,来自编码器层的语义存储
变量mermory, 以及源数据掩码张量和目标数据掩码张量.
最终输出了由编码器输入和目标数据一同作用的特征提取结果.

m
. co
ma
ei
2.4.2 解码器 w.
it h
ww


学习目标: 马

了解解码器的作用. 黑
掌握解码器的实现过程.

解码器的作用:
根据编码器的结果以及上一次预测的结果, 对下一次可能出现的'值'进行特征表示.
c om
a.
解码器的代码分析: th
eim
. i
w
# 使用类 Decoder来实现解码器 ww
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代表源数据和目标数据的掩码张量"""
m
# 然后就是对每个层进行循环,当然这个循环就是变量 . co x通过每一个层的处理,
# 得出最后的结果,再进行一次规范化返回即可 ei
m.a
for layer in self.layers: h
.it
x = layer(x, memory, source_mask, target_mask)
w
return self.norm(x) ww



实例化参数: 黑

# 分别是解码器层 layer和解码器层的个数 N
size = 512
d_model = 512
head = 8
d_ff = 64
dropout = 0.2
c = copy.deepcopy
attn = MultiHeadedAttention(head, d_model)
m
co
ff = PositionwiseFeedForward(d_model, d_ff, dropout)
.
layer = DecoderLayer(d_model, c(attn), c(attn),mac(ff), dropout)
i
N = 8 he t
i
w w.
w



输入参数: 黑

# 输入参数与解码器层的输入参数相同
x = pe_result
memory = en_result
mask = Variable(torch.zeros(8, 4, 4))
source_mask = target_mask = mask

com
调用: im
a.
e
ith
.
de = Decoder(layer, N) w ww
de_result = de(x, memory, source_mask,员target_mask)

print(de_result) 程
print(de_result.shape) 马

输出效果:

tensor([[[ 0.9898, -0.3216, -1.2439, ..., 0.7427, -0.0717, -0.0814],


[-0.7432, 0.6985, 1.5551, ..., 0.5232, -0.5685, 1.3387],
[ 0.2149, 0.5274, -1.6414, ..., 0.7476, 0.5082, -3.0132],
[ 0.4408, 0.9416, 0.4522, om
..., -0.1506,
c 1.5591, -0.6453]],

m a.
[[-0.9027, 0.5874, 0.6981, ..., ei 2.2899, 0.2933, -0.7508],
th -1.2377,
[ 1.2246, -1.0856, -0.2497, i..., 0.0847, -0.0221],
.
www ..., -1.5427,
[ 3.4012, -0.4181, -2.0968, 0.1090, -0.3882],
[-0.1050, -0.5140, -0.6494, ..., -0.4358, -1.2173, 0.4161]]],


grad_fn=<AddBackward0>)
torch.Size([2, 4, 512]) 程

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

m
学习并实现了解码器的类: Decoder m a .c
o
类的初始化函数的参数有两个,第一个就是解码器层 h ei layer,第二个是解码器层的个
it
数N. ww
w.
forward函数中的参数有4个,x员 代表目标数据的嵌入表示,memory是编码器层的输
出,src_mask, tgt_mask

代表源数据和目标数据的掩码张量


.

输出解码过程的最终特征表示 黑 .

2.5 输出部分实现
c om
a.
eim
学习目标 w . ith
ww
了解线性层和softmax的作用. 序

掌握线性层和softmax的实现过程. 马程

输出部分包含:
线性层
softmax层

m
. co
i ma
the
i
w.
ww




线性层的作用 黑

通过对上一步的线性变化得到指定维度的输出, 也就是转换维度的作用.

softmax 层的作用
使最后一维的向量中的数字缩放到0-1的概率值域内, 并满足他们的和为1.
m
. co
m a
ei
线性层和softmax层的代码分析: it h
w w.
w
# nn.functional 工具包装载了网络层中那些只进行计算, 而没有参数的层

F序
import torch.nn.functional as 程

# 将线性层和 计算层一起实现 因为二者的共同目标是生成最后的结构
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) m
. co
def forward(self, x): ima
前向逻辑函数中输入是上一层的输出张量
""" x""" th
e
在函数中 首先使用上一步得到的
# , 对 进行线性变化, w.
self.project
i x
然后使用 中已经实现的
# F 进行的 处理 w
log_softmax w softmax .
在这里之所以使用
# 是因为和我们这个 版本的损失函数实现有关,
log_softmax 员 pytorch
在其他版本中将修复 . 程

就是对 的结果又取了对数 因为对数函数是单调递增函数,
# log_softmax 马
softmax ,
因此对最终我们取最大的概率值没有影响 最后返回结果即可
#
黑 . .
return F.log_softmax(self.project(x), dim=-1)
nn.Linear 演示:
>>> m = nn.Linear(20, 30)
>>> input = torch.randn(128, 20)
>>> output = m(input)
>>> print(output.size())
m
torch.Size([128, 30])
. co
i ma
the
i
w.
ww
实例化参数: 序




# 词嵌入维度是 维 512
d_model = 512

# 词表大小是 1000
vocab_size = 1000

输入参数: m
. co
ma
i
# 输入 是上一层网络的输出, 我们使用来自解码器层的输出
x
i t he
x = de_result w.
ww




调用: 黑

gen = Generator(d_model, vocab_size)


gen_result = gen(x)
print(gen_result)
print(gen_result.shape)

c om
输出效果: e im
a.
ith
.
w ww
tensor([[[-7.8098, -7.5260, -6.9244, ..., -7.6340, -6.9026, -7.5232],
[-6.9093, -7.3295, 员
-7.2972,序 ..., -6.6221, -7.2268, -7.0772],
[-7.0263, -7.2229, 程 ...,
-7.8533, -6.7307, -6.9294, -7.3042],
[-6.5045, -6.0504, 马
-6.6241, ..., -5.9063, -6.5361, -7.1484]],

[[-7.1651, -6.0224, -7.4931, ..., -7.9565, -8.0460, -6.6490],
[-6.3779, -7.6133, -8.3572, ..., -6.6565, -7.1867, -6.5112],
[-6.4914, -6.9289, -6.2634, ..., -6.2471, -7.5348, -6.8541],
[-6.8651, -7.0460, -7.6239, ..., -7.1411, -6.5496, -7.3749]]],
grad_fn=<LogSoftmaxBackward>)
torch.Size([2, 4, 1000])

小节总结 . co
m
i ma
学习了输出部分包含: the
i
w.
线性层 ww

softmax层 程



线性层的作用:
通过对上一步的线性变化得到指定维度的输出, 也就是转换维度的作用.

层的作用:
softmax

使最后一维的向量中的数字缩放到0-1的概率值域内, 并满足他们的和为
m
1.

. co
i ma
e
学习并实现了线性层和softmax层的类: Generator i th
w.
初始化函数的输入参数有两个, d_model 员
ww代表词嵌入维度, vocab_size代表词表大小.
forward函数接受上一层的输出 程. 序

最终获得经过线性层和softmax
黑 层处理的结果.

2.6 模型构建
com
a.
学习目标 ith
e im
w .
掌握编码器-解码器结构的实现过程. ww

掌握Transformer模型的构建过程. 程



通过上面的小节, 我们已经完成了所有组成部分的实现, 接下来就来实现完整的编码器-解码器
结构.
Transformer 总体架构图:

m
. co
i ma
the
i
w.
ww




m
. co
m a
h ei
it
w w.
w




编码器-解码器结构的代码实现
m
# 使用 EncoderDecoder 类来实现编码器 解码器结构
-
a .c
o
class EncoderDecoder(nn.Module): m
h ei
def __init__(self, encoder, decoder, source_embed, target_embed,
generator): . it
w
""" 初始化函数中有 个参数 分别是编码器对象 解码器对象
5 , ww , ,
源数据嵌入函数 目标数据嵌入函数 以及输出部分的类别生成器对象
,


,
""" 程

super(EncoderDecoder, self).__init__()
# 将参数传入到类中 黑
self.encoder = encoder
self.decoder = decoder
self.src_embed = source_embed
self.tgt_embed = target_embed
self.generator = generator

def forward(self, source, target, source_mask, target_mask):



""" forward 函数中,有四个参数 代表源数据
, source 代表目标数据,
, target

source_mask target_mask代表对应的掩码张量 """

# 在函数中, 将source, source_mask传入编码函数 m , 得到结果后,


# 与source_mask,target,和target_maska.一同传给解码函数
co .
im source_mask), source_mask,
return self.decode(self.encode(source,
he
target, target_mask)
t
w .i
def ww
encode(self, source, source_mask):
"""编码函数, 以source和source_mask
员 为参数"""
# 使用src_embed对source
序 做处理, 然后和source_mask一起传给self.encoder


return self.encoder(self.src_embed(source), source_mask)

def decode(self, memory, source_mask, target, target_mask):
""" 解码函数 以
, 即编码器的输出
memory , source_mask, target, target_mask 为
参数 """
# 使用 对 做处理 然后和
tgt_embed target , source_mask, target_mask, memory 一
起传给 self.decoder
return self.decoder(self.tgt_embed(target), memory, source_mask,
target_mask)

m
. co
实例化参数 h ei
ma
it
w w.
w
vocab_size = 1000

d_model = 512 序
encoder = en 程

decoder = de 黑
source_embed = nn.Embedding(vocab_size, d_model)
target_embed = nn.Embedding(vocab_size, d_model)
generator = gen

输入参数:
om
# 假设源数据与目标数据相同 实际中并不相同,
m a.
c
source = target = Variable(torch.LongTensor([[100, i 2, 421, 508], [491, 998,
1, 221]])) t he
i
w.
# 假设 与
src_mask tgt_mask 相同,实际中并不相同 ww

source_mask = target_mask = Variable(torch.zeros(8, 4, 4))



调用:
ed = EncoderDecoder(encoder, decoder, source_embed, target_embed,
generator)
ed_result = ed(source, target, source_mask, target_mask)
print(ed_result)
print(ed_result.shape)

m
. co
i ma
he
输出效果: w.
i t
ww

tensor([[[ 0.2102, -0.0826,

-0.0550, ..., 1.5555, 1.3025, -0.6296],

[ 0.8270, 马
-0.5372, -0.9559, ..., 0.3665, 0.4338, -0.7505],
[ 0.4956,

-0.5133, -0.9323, ..., 1.0773, 1.1913, -0.6240],
[ 0.5770, -0.6258, -0.4833, ..., 0.1171, 1.0069, -1.9030]],

[[-0.4355, -1.7115, -1.5685, ..., -0.6941, -0.1878, -0.1137],


[-0.8867, -1.2207, -1.4151, ..., -0.9618, 0.1722, -0.9562],
[-0.0946, -0.9012, -1.6388, ..., -0.2604, -0.3357, -0.6436],
[-1.1204, -1.4481, -1.5888, ..., -0.8816, -0.6497, 0.0606]]],
grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])

m
. co
a
接着将基于以上结构构建用于训练的模型. t h ei
m
i
w w.
w

Tansformer 模型构建过程的代码分析




def make_model(source_vocab, target_vocab, N=6,
d_model=512, d_ff=2048, head=8, dropout=0.1):
""" 该函数用来构建模型 有 个参数,分别是源数据特征 词汇 总数,目标数据特征(词汇)总
, 7 ( )
数,
编码器和解码器堆叠数,词向量映射维度,前馈全连接网络中变换矩阵的维度,
多头注意力结构中的多头数,以及置零比率dropout."""
# 首先得到一个深度拷贝命令,接下来很多结构都需要进行深度拷贝,
# 来保证他们彼此之间相互独立,不受干扰.
c = copy.deepcopy
c om
实例化了多头注意力类,得到对象 a.
# attn
e im
attn = MultiHeadedAttention(head, d_model)
ith
w.
# 然后实例化前馈全连接类,得到对象 ff ww
员 d_ff, dropout)
ff = PositionwiseFeedForward(d_model,

实例化位置编码类,得到对象 程
# 马
position

position = PositionalEncoding(d_model, dropout)

# 根据结构图, 最外层是EncoderDecoder,在EncoderDecoder中,
# 分别是编码器层,解码器层,源数据Embedding层和位置编码组成的有序结构,
# 目标数据Embedding层和位置编码组成的有序结构,以及类别生成器层.
# 在编码器层中有 attention 子层以及前馈全连接子层,
# 在解码器层中有两个 attention 子层以及前馈全连接层 .
model = EncoderDecoder(
Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N),
Decoder(DecoderLayer(d_model, c(attn), c(attn),
c(ff), dropout), N),
nn.Sequential(Embeddings(d_model, source_vocab), c(position)),
m
. co
nn.Sequential(Embeddings(d_model, target_vocab), c(position)),
Generator(d_model, target_vocab)) ma
i
the
# 模型结构完成后,接下来就是初始化模型中的参数,比如线性层中的变换矩阵
i
w.
# 这里一但判断参数的维度大于1,则会将其初始化成一个服从均匀分布的矩阵,
ww

for p in model.parameters():
if p.dim() > 1: 序


nn.init.xavier_uniform(p)
return model 黑

nn.init.xavier_uniform 演示:
# 结果服从均匀分布 U(-a, a)
>>> w = torch.empty(3, 5)
>>> w = nn.init.xavier_uniform_(w, gain=nn.init.calculate_gain('relu'))
>>> w
m
tensor([[-0.7742, 0.5413, 0.5478, -0.4806, -0.2555],
. co
ma
[-0.8358, 0.4673, 0.3012, 0.3882, -0.6375],
i
[ 0.4622, -0.0794, 0.1851, 0.8462, e
h -0.3591]])
it
w w.
w



输入参数: 黑

source_vocab = 11
target_vocab = 11
N = 6
# 其他参数都使用默认值

调用: a.
com
eim
ith
if __name__ == '__main__': w .
ww
res = make_model(source_vocab, target_vocab, N)
print(res) 员



输出效果:
# 根据 Transformer 结构图构建的最终模型结构
EncoderDecoder(
(encoder): Encoder(
(layers): ModuleList(
(0): EncoderLayer(
(self_attn): MultiHeadedAttention(
m
(linears): ModuleList(
. co
a
(0): Linear(in_features=512, out_features=512)
im
(1): Linear(in_features=512, eout_features=512)
h
(2): Linear(in_features=512,
.it out_features=512)
w
)
ww
(3): Linear(in_features=512, out_features=512)



(dropout): Dropout(p=0.1)
) 程


(feed_forward): PositionwiseFeedForward(
(w_1): Linear(in_features=512, out_features=2048)
(w_2): Linear(in_features=2048, out_features=512)
(dropout): Dropout(p=0.1)
)
(sublayer): ModuleList(
(0): SublayerConnection(
(norm): LayerNorm(
)
(dropout): Dropout(p=0.1)
)
m
(1): SublayerConnection(
. co
(norm): LayerNorm( a
) eim
h
(dropout): Dropout(p=0.1)
. it
w
)
)
ww

) 序
(1): EncoderLayer( 程


(self_attn): MultiHeadedAttention(
(linears): ModuleList(
(0): Linear(in_features=512, out_features=512)
(1): Linear(in_features=512, out_features=512)
(2): Linear(in_features=512, out_features=512)
(3): Linear(in_features=512, out_features=512)
)
(dropout): Dropout(p=0.1)
)
(feed_forward): PositionwiseFeedForward(
(w_1): Linear(in_features=512, out_features=2048)
m
(w_2): Linear(in_features=2048, out_features=512)
. co
(dropout): Dropout(p=0.1) a
) e im
h
(sublayer): ModuleList(
. it
w
(0): SublayerConnection(
(norm): LayerNorm(
ww

) 序

(dropout): Dropout(p=0.1)

) 黑
(1): SublayerConnection(
(norm): LayerNorm(
)
(dropout): Dropout(p=0.1)
)
)
)
)
(norm): LayerNorm(
)
)
m
(decoder): Decoder(
. co
a
(layers): ModuleList(
e im
(0): DecoderLayer( h
.it
(self_attn): MultiHeadedAttention(
w
(linears): ModuleList( ww

(0): Linear(in_features=512, out_features=512)

(1): Linear(in_features=512, out_features=512)


(2): Linear(in_features=512, out_features=512)

(3): Linear(in_features=512, out_features=512)
)
(dropout): Dropout(p=0.1)
)
(src_attn): MultiHeadedAttention(
(linears): ModuleList(
(0): Linear(in_features=512, out_features=512)
(1): Linear(in_features=512, out_features=512)
(2): Linear(in_features=512, out_features=512)
(3): Linear(in_features=512, out_features=512)
) m
(dropout): Dropout(p=0.1) . co
a
)
e im
th
(feed_forward): PositionwiseFeedForward(
iout_features=2048)
(w_1): Linear(in_features=512, w.
ww out_features=512)
(w_2): Linear(in_features=2048,

(dropout): Dropout(p=0.1)
) 序


(sublayer): ModuleList(

(0): SublayerConnection(
(norm): LayerNorm(
)
(dropout): Dropout(p=0.1)
)
(1): SublayerConnection(
(norm): LayerNorm(
)
(dropout): Dropout(p=0.1)
)
(2): SublayerConnection( m
(norm): LayerNorm( . co
a
)
e im
(dropout): Dropout(p=0.1) h
) . it
w
) ww
) 员
(1): DecoderLayer( 序


(self_attn): MultiHeadedAttention(

(linears): ModuleList(
(0): Linear(in_features=512, out_features=512)
(1): Linear(in_features=512, out_features=512)
(2): Linear(in_features=512, out_features=512)
(3): Linear(in_features=512, out_features=512)
)
(dropout): Dropout(p=0.1)
)
(src_attn): MultiHeadedAttention(
(linears): ModuleList(
(0): Linear(in_features=512, out_features=512)
(1): Linear(in_features=512, out_features=512)
m
co
(2): Linear(in_features=512, out_features=512)
.
a
im
(3): Linear(in_features=512, out_features=512)
e
)
th
(dropout): Dropout(p=0.1) .i
w
) ww

(feed_forward): PositionwiseFeedForward(

(w_1): Linear(in_features=512, out_features=2048)


(w_2): Linear(in_features=2048, out_features=512)

(dropout): Dropout(p=0.1)
)
(sublayer): ModuleList(
(0): SublayerConnection(
(norm): LayerNorm(
)
(dropout): Dropout(p=0.1)
)
(1): SublayerConnection(
(norm): LayerNorm(
) m
(dropout): Dropout(p=0.1) . co
a
)
eim
(2): SublayerConnection( h
(norm): LayerNorm( . it
w
) ww

(dropout): Dropout(p=0.1)
) 序

) 马
) 黑
)
(norm): LayerNorm(
)
)
(src_embed): Sequential(
(0): Embeddings(
(lut): Embedding(11, 512)
)
(1): PositionalEncoding(
(dropout): Dropout(p=0.1) m
) . co
a
)
e im
(tgt_embed): Sequential( h
(0): Embeddings( . it
w
(lut): Embedding(11, 512) ww
) 员
(1): PositionalEncoding( 序

(dropout): Dropout(p=0.1)马
) 黑
)
(generator): Generator(
(proj): Linear(in_features=512, out_features=11)
)
)

小节总结 m
. co
学习并实现了编码器-解码器结构的类: EncoderDecoder mai
the
i
类的初始化函数传入5个参数, 分别是编码器对象 w w. , 解码器对象, 源数据嵌入函数, 目标数据
嵌入函数, 以及输出部分的类别生成器对象 员
w
.

类中共实现三个函数, forward,

程 encode, decode
forward是主要逻辑函数 黑 , 有四个参数, source代表源数据, target代表目标数据,
source_mask和target_mask代表对应的掩码张量.

encode是编码函数, 以source和source_mask为参数.

decode是解码函数, 以memory即编码器的输出, source_mask, target, target_mask为参


学习并实现了模型构建函数: make_model co
m
有7个参数,分别是源数据特征(词汇)总数,目标数据特征 im
a . (词汇)总数,编码器和解码器
堆叠数,词向量映射维度,前馈全连接网络中变换矩阵的维度,多头注意力结构中的多
.i
t h e
头数,以及置零比率dropout. ww
w
该函数最后返回一个构建好的模型对象 员
序 .


com
a.
eim
ith
w .
ww




You might also like