一.前言
前面我们学习了Transformer的基本架构,我们知道了Transformer的几个重要部分,本章节我们就是来介绍一下最开始的输入部分,通过本章节了解⽂本嵌⼊层和位置编码的作⽤,掌握⽂本嵌⼊层和位置编码的实现过程。
二.输⼊部分介绍
输⼊部分包含:
源⽂本嵌⼊层及其位置编码器
⽬标⽂本嵌⼊层及其位置编码器
三.文本嵌入层的作用
⽆论是源⽂本嵌⼊还是⽬标⽂本嵌⼊,都是为了将⽂本中词汇的数字表示转变为向量表示, 希望在这样 的⾼维空间捕捉词汇间的关系.
⽂本嵌⼊层的代码分析:
# 导入必备的工具包
import torch
# 预定义的网络层torch.nn,工具开发者已经帮助我们开发好的一些常用层,
# 比如:卷积层、LSTM层、Embedding层等,不需要我们再重新造轮子
import torch.nn as nn
# 数学计算工具包
import math
# torch中变量封装函数Variable
from torch.autograd import Variable
# 定义Embeddings类来实现文本嵌入层
# 这个类继承nn.Module,这样就有标准层的一些功能
# 这是一种设计模式,我们自己实现的所有层都会这样去写
class Embeddings(nn.Module):
def __init__(self, d_model, vocab):
"""
类的初始化函数,有两个参数:
d_model: 指词嵌入的维度(embedding dimension)
vocab: 指词表的大小(vocabulary size)
"""
# 使用super的方式指明继承nn.Module的初始化函数
# 这是我们实现自定义层的标准写法
super(Embeddings, self).__init__()
# 调用nn中的预定义层Embedding,获得一个词嵌入对象self.lut
# lut是"look-up table"的缩写,表示通过这个表查找对应的embedding
self.lut = nn.Embedding(vocab, d_model)
# 将d_model传入类中保存
self.d_model = d_model
def forward(self, x):
"""
前向传播函数,所有层中都会有此函数
当传给该类的实例化对象参数时,自动调用该函数
参数:
x: 因为Embedding层通常是首层,所以代表输入给模型的文本通过词汇映射后的张量
形状通常是(batch_size, sequence_length)的索引张量
返回:
经过嵌入后的张量,形状为(batch_size, sequence_length, d_model)
"""
# 1. 首先通过self.lut进行词嵌入查找
# 2. 将结果乘以math.sqrt(self.d_model)进行缩放
#
# 这样做的目的是:
# - 让embeddings vector在增加之后的position encoding之前相对大一些
# - 主要是为了让position encoding相对小,这样在相加时不至于丢失原始embedding的信息
# - 这是一种常见的transformer架构中的技巧
return self.lut(x) * math.sqrt(self.d_model)
if __name__ == '__main__':
# 测试代码部分
# 定义词嵌入维度为512维
d_model = 512
# 定义词表大小为1000(即词汇表中有1000个不同的词)
vocab = 1000
# 创建输入数据x,这是一个形状为2x4的长整型张量
# 使用Variable封装,Variable在旧版PyTorch中用于自动求导
# 输入数据表示2个句子,每个句子4个词(用词索引表示)
# 例如:[[100, 2, 421, 508], [491, 998, 1, 221]]
x = Variable(torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]]))
# 实例化Embeddings类,创建嵌入层对象emb
emb = Embeddings(d_model, vocab)
# 进行前向传播计算,得到词嵌入结果embr
embr = emb(x)
# 打印输出结果
print("embr:", embr)
# 输出形状应该是(2, 4, 512):
# - 2表示batch_size(2个句子)
# - 4表示sequence_length(每个句子4个词)
# - 512表示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], [ 0.4362, -0.4004, 0.9400], [-0.6431, 0.0748, 0.6969], [ 0.9124, -2.3616, 1.1151]]])
>>> 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]]])
结果展示:
embr: tensor([[[ -4.2945, 9.1117, -9.1677, ..., -16.0778, 4.4005, 43.9077],
[ 18.2505, 6.8187, 10.8989, ..., -22.0975, -2.4065, -9.7472],
[-36.6917, 11.9682, 4.5460, ..., 33.7522, -18.7132, 6.8403],
[-12.6879, 31.1869, 10.3653, ..., -16.7187, -13.9875, 13.6549]],[[ -9.6272, 3.6442, 29.1990, ..., -13.1817, -9.4845, -0.1718],
[ 14.7740, 4.3781, 13.8937, ..., -23.2594, 4.9865, -18.6018],
[ 2.3346, -3.1856, 17.0953, ..., 9.7180, -13.4957, 15.7557],
[-38.7066, 37.0586, -13.9392, ..., 22.7013, 7.2911, -23.9363]]],
grad_fn=<MulBackward0>)
四.位置编码器的作用
因为在Transformer的编码器结构中, 并没有针对词汇位置信息的处理,因此需要在Embedding层后加⼊位置编码器,将词汇位置不同可能会产⽣不同语义的信息加⼊到词嵌⼊张量中, 以弥补位置信息的缺失.
位置编码器的代码分析:
# 导入必备的工具包
import torch
# 预定义的网络层torch.nn,工具开发者已经帮助我们开发好的一些常用层,
# 比如:卷积层、LSTM层、Embedding层等,不需要我们再重新造轮子
import torch.nn as nn
# 数学计算工具包
import math
# torch中变量封装函数Variable
from torch.autograd import Variable
# 定义位置编码器类, 我们同样把它看做⼀个层, 因此会继承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)
# 初始化⼀个位置编码矩阵, 它是⼀个0阵,矩阵的⼤⼩是max_len x d_model.
pe = torch.zeros(max_len, d_model)
# 初始化⼀个绝对位置矩阵, 在我们这⾥,词汇的绝对位置就是⽤它的索引去表示.
# 所以我们⾸先使⽤arange⽅法获得⼀个连续⾃然数向量,然后再使⽤unsqueeze⽅法拓展向量维度使其成为矩阵,
# ⼜因为参数传的是1,代表矩阵拓展的位置,会使向量变成⼀个max_len x 1 的矩阵,
position = torch.arange(0, max_len).unsqueeze(1)
# 绝对位置矩阵初始化之后,接下来就是考虑如何将这些位置信息加⼊到位置编码矩阵中,
# 最简单思路就是先将max_len x 1的绝对位置矩阵, 变换成max_len x d_model形状,然后覆盖原来的初始位置编码矩阵即可,
# 要做这种矩阵变换,就需要⼀个1xd_model形状的变换矩阵div_term,我们对这个变换矩阵的要求除了形状外,
# 还希望它能够将⾃然数的绝对位置编码缩放成⾜够⼩的数字,有助于在之后的梯度下降过程中更快的收敛. 这样我们就可以开始初始化这个变换矩阵了.
# ⾸先使⽤arange获得⼀个⾃然数矩阵, 但是细⼼的同学们会发现, 我们这⾥并没有按照预计的 ⼀样初始化⼀个1xd_model的矩阵,
# ⽽是有了⼀个跳跃,只初始化了⼀半即1xd_model/2 的矩阵。 为什么是⼀半呢,其实这⾥并不是真正意义上的初始化了⼀半的矩阵,
# 我们可以把它看作是初始化了两次,⽽每次初始化的变换矩阵会做不同的处理,第⼀次初始化的变换矩阵分布在正弦波上, 第⼆次初始化的变换矩阵分布在余弦波上,
# 并把这两个矩阵分别填充在位置编码矩阵的偶数和奇数位置上,组成最终的位置编码矩阵.
div_term = torch.exp(torch.arange(0, d_model, 2) *
-(math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
# 这样我们就得到了位置编码矩阵pe, pe现在还只是⼀个⼆维矩阵,要想和embedding的输出(⼀个三维张量)相加,
# 就必须拓展⼀个维度,所以这⾥使⽤unsqueeze拓展维度.
pe = pe.unsqueeze(0)
# 最后把pe位置编码矩阵注册成模型的buffer,什么是buffer呢,
# 我们把它认为是对模型效果有帮助的,但是却不是模型结构中超参数或者参数,不需要随着优化步骤进⾏更新的增益对象.
# 注册之后我们就可以在模型保存后重加载时和模型结构与参数⼀同被加载.
self.register_buffer('pe', pe)
def forward(self, x):
"""forward函数的参数是x, 表示⽂本序列的词嵌⼊表示"""
# 在相加之前我们对pe做⼀些适配⼯作, 将这个三维张量的第⼆维也就是句⼦最⼤⻓度的那⼀维将切⽚到与输⼊的x的第⼆维相同即x.size(1),
# 因为我们默认max_len为5000⼀般来讲实在太⼤了,很难有⼀条句⼦包含5000个词汇,所以要进⾏与输⼊张量的适配.
# 最后使⽤Variable进⾏封装,使其与x的样式相同,但是它是不需要进⾏梯度求解的,因此把requires_grad设置成false.
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 2.0591 -1.3400 -1.7247 -0.9885 0.1286 0.5099 1.3715 0.0000 2.2079 -0.5497 -0.0000 -0.7839 -1.2434 -0.1222 1.2815
[torch.FloatTensor of size 4x5]
torch.unsqueeze演示:
>>> x = torch.tensor([1, 2, 3, 4])
>>> torch.unsqueeze(x, 0)
tensor([[ 1, 2, 3, 4]])
>>> torch.unsqueeze(x, 1)
tensor([[ 1], [ 2], [ 3], [ 4]])
实例化参数:
# 词嵌⼊维度是512维
d_model = 512
# 置0⽐率为0.1
dropout = 0.1
# 句⼦最⼤⻓度
max_len=60
输⼊参数:
# 输⼊x是Embedding层的输出的张量, 形状是2 x 4 x 512
x = embr
调用:
pe = PositionalEncoding(d_model, dropout, max_len)
pe_result = pe(x)
print("pe_result:", pe_result)
结果展示:
pe_result: tensor([[[ 41.8074, 54.4747, 11.9309, ..., -31.4130, 51.5776, -62.0651],
[-18.3517, -0.3810, -7.8546, ..., 29.4209, -10.6266, 16.3635],
[ 31.2419, 24.2770, -32.5763, ..., 26.1189, 2.7321, -15.9024],
[-15.0417, 9.9892, 15.3855, ..., 5.6154, 26.6063, -15.2053]],[[ 31.4674, -17.8223, -0.1647, ..., 44.2820, -26.3868, 0.0000],
[-11.0673, -0.0000, 4.3832, ..., -25.8723, 52.0138, -5.3114],
[ 0.0000, 22.8683, -45.2826, ..., 0.0000, -6.3752, 48.6797],
[ -3.5761, -17.3643, -13.7431, ..., -3.2667, 47.8063, -12.9179]]],
grad_fn=<MulBackward0>)
五.总结
学习了⽂本嵌⼊层的作⽤:
⽆论是源⽂本嵌⼊还是⽬标⽂本嵌⼊,都是为了将⽂本中词汇的数字表示转变为向量表示, 希望 在这样的⾼维空间捕捉词汇间的关系.
学习并实现了⽂本嵌⼊层的类: Embeddings
初始化函数以d_model, 词嵌⼊维度, 和vocab, 词汇总数为参数, 内部主要使⽤了nn中的预定层 Embedding进⾏词嵌⼊.
在forward函数中, 将输⼊x传⼊到Embedding的实例化对象中, 然后乘以⼀个根号下d_model进⾏缩放, 控制数值⼤⼩.
它的输出是⽂本嵌⼊后的结果.
学习了位置编码器的作⽤:
因为在Transformer的编码器结构中, 并没有针对词汇位置信息的处理,因此需要在Embedding 层后加⼊位置编码器,将词汇位置不同可能会产⽣不同语义的信息加⼊到词嵌⼊张量中, 以弥补 位置信息的缺失.
学习并实现了位置编码器的类: PositionalEncoding
初始化函数以d_model, dropout, max_len为参数, 分别代表d_model: 词嵌⼊维度, dropout: 置0 ⽐率, max_len: 每个句⼦的最⼤⻓度.
forward函数中的输⼊参数为x, 是Embedding层的输出.
最终输出⼀个加⼊了位置编码信息的词嵌⼊张量.