import torch # 预定义的网络层torch.nn, 工具开发者已经帮助我们开发好的一些常用层, 比如,卷积层, lstm层, embedding层等, 不需要我们再重新造轮子. import torch.nn as nn # 数学计算工具包 import math # torch中变量封装函数Variable.现在应该使用torch.Tensor了好像?? from torch.autograd import Variable
文本嵌入层
由于 EEG 本身是数值时间相关的序列,因此嵌入是不必要的。所以我们将原来的嵌入修改为标准的线性层。我们没有删除这个函数,因为我们想采用原始代码的架构。
# 定义Embeddings类来实现文本嵌入层,这里s说明代表两个一模一样的嵌入层, 他们共享参数.
# 该类继承nn.Module, 这样就有标准层的一些功能, 这里我们也可以理解为一种模式, 我们自己实现的所有层都这样去写.
class Embeddings(nn.Module):
def __init__(self, d_model, input_d): #(self,d_model,vocab)
"""类的初始化函数, 有两个参数, d_model: 指词嵌入的维度, input_d: 指词表的大小???"""
# 接着就是使用super的方式指明继承nn.Module的初始化函数, 我们自己实现的所有层都会这样去写.
super(Embeddings, self).__init__()
# 之后就是调用nn中的预定义层Linear, 获得一个对象self.proj
self.proj = nn.Linear(input_d, d_model)
# 下边是用于文本时的用法
# 调用nn中的预定义层Embedding, 获得一个词嵌入对象self.lut
# self.lut = nn.Embedding(vocab, d_model)
# 最后就是将d_model传入类中
# self.d_model = d_model
def forward(self, x):
"""可以将其理解为该层的前向传播逻辑,所有层中都会有此函数
当传给该类的实例化对象参数时, 自动调用该类函数
参数x: 因为Embedding层是首层, 所以代表输入给模型的文本通过词汇映射后的张量"""
# 将x传给self.proj并作为结果返回
return self.proj(x)
# return self.lut(x) * math.sqrt(self.d_model)
d_model = 512
vocab = 1000
x = Variable(torch.LongTensor([[1,2,3,4],[5,6,7,8]])) # (2,4)
emb = Embeddings(d_model,vocab)
embr = emb(x)
print('embr:', embr)
print(embr.shape) # (2,4,512)
简化:
class Embeddings(nn.Module):
def __init__(self, d_model, input_d): #(self,d_model,vocab)
super(Embeddings, self).__init__()
self.proj = nn.Linear(input_d, d_model)
# self.lut = nn.Embedding(vocab, d_model)
# self.d_model = d_model
def forward(self, x):
return self.proj(x)
# return self.lut(x) * math.sqrt(self.d_model)
位置编码
在这里,我们实现了位置编码功能,并在log空间中计算了一次位置编码。与原始版本不同,我们使用 torch.tenser() 而不是 Variable(),因为后者已被弃用。由于transformer不包含递归和卷积,为了让模型利用序列的顺序,我们必须注入一些关于tokens在序列中的相对或绝对位置的信息。为此,我们在编码器和解码器堆栈底部的输入嵌入中添加“位置编码”。位置编码与嵌入具有相同的维度 dmodel,因此可以将两者相加。
# 定义位置编码器类, 我们同样把它看做一个层, 因此会继承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.
# 每一个单词代表一行,一行有d_model列
pe = torch.zeros(max_len, d_model)
# 初始化一个“绝对位置矩阵”, 在我们这里,词汇的绝对位置就是用它的索引去表示.
# 我们首先使用arange方法获得一个连续自然数向量,然后再使用unsqueeze方法拓展向量维度使其成为矩阵,
# 又因为参数传的是1,代表矩阵拓展的位置,会使向量变成一个max_len x 1 的矩阵
# 所以position绝对位置 矩阵的形状是(max_len,1)
position = torch.arange(0.0, max_len).unsqueeze(1)
# 绝对位置矩阵初始化之后,接下来就是考虑如何将这些位置信息加入到位置编码矩阵中.
# 最简单思路就是先将max_lenx1的绝对位置矩阵,变换成max_len x d_model形状,
# 然后“覆盖”原来的初始位置编码矩阵即可.
# 要做这种矩阵变换,需要一个1xd_model形状的变换矩阵div_term
# ->(max_len,1)x(1,d_model)=(max_len,d_model).
# 助于在之后的梯度下降过程中更快的收敛. 这样我们就可以开始初始化这个变换矩阵了.
# 首先使用arange获得一个自然数矩阵,但是细心的同学们会发现,
# 我们这里并没有按照预计的一样初始化一个1xd_model的矩阵,
# 而是有一个跳跃,只初始化了一半即1xd_model/2 的矩阵。为什么是一半呢,
# 其实这里并不是真正意义上的初始化了一半的矩阵,
# 我们可以把它看作是初始化了“两次”,而每次初始化的变换矩阵会做不同的处理.
# 第一次初始化的变换矩阵分布在正弦波上, 第二次初始化的变换矩阵分布在余弦波上,
# 并把这两个矩阵分别填充在位置编码矩阵的偶数和奇数位置上,组成最终的位置编码矩阵.
# arange(0,n,2)每隔两个生成一个,共n/2个
div_term = torch.exp(torch.arange(0.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个词汇
# 所以要进行与输入张量的适配(截取与x第二维相同的长度).
# 最后使用Variable进行封装,使其与x的样式相同
#但是它是不需要进行梯度求解的,因此把requires_grad设置成false.
x = x + Variable(self.pe[:, :x.size(1)], # :x.size(1) 截取操作
requires_grad=False)
# 最后使用self.dropout对象进行'丢弃'操作, 并返回结果.
return self.dropout(x)
简化内容
class PositionalEncoding(nn.Module):
def __init__(self, d_model, dropout, max_len=5000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
pe = torch.zeros(max_len, d_model)
position = torch.arange(0.0, max_len).unsqueeze(1) # [max_len,1]
div_term = torch.exp(torch.arange(0.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.unsqueeze(0)
self.register_buffer('pe', pe)
def forward(self, x):
x = x + torch.tensor(self.pe[:, :x.size(1)], requires_grad=False)
return self.dropout(x)
实例化:
# 实例化参数
# 词嵌入维度是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) # (2,4,512)
绘制词汇向量中特征的分布曲线
import matplotlib.pyplot as plt # 创建一张15 x 5大小的画布 plt.figure(figsize=(15, 5)) # 实例化PositionalEncoding类得到pe对象, 输入参数是20和0 pe = PositionalEncoding(20, 0) # 然后向pe传入被Variable封装的tensor, 这样pe会直接执行forward函数, # 且这个tensor里的数值都是0, 被处理后相当于位置编码张量 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]])
效果分析:
- 每条颜色的曲线代表某一个词汇中的特征在不同位置的含义.
- 保证同一词汇随着所在位置不同它对应位置嵌入向量会发生变化.
- 正弦波和余弦波的值域范围都是1到-1这又很好的控制了嵌入数值的大小, 有助于梯度的快速计算.



