Transformer原理与代码
最近接触nlp领域,从最基础的模型学起,本文记录学习过程的个人理解,如有不对还请各位大神指正。
参考资料:
唐宇迪transformer
李宏毅transformer
1. Self-attention原理
-
我的理解:所谓的self-attention其实就是一种加权求特征的过程,在计算it的特征时考虑到其他所有词汇,方法是加权。
-
过程:
用三个向量表示输入数据,其中q与k代表输入数据去计算“加权值”,v用来进行加权的特征生成。
多头:类似于cnn中的channel(多个核),因为一组qkv只能获得“一种”特征,用多头捕获多层次的特征。
多层encoder-decoder堆叠:类似于cnn中的多个block(cnn的多个卷积+池化),显然在经典的cnn算法中不是只有一层block(特征语义不充分,高层语义的特征描述更有价值),因此这里采用多层堆叠的方式。
2. Transformer模型
2.1 模型架构
(1)输入模块:
-
文本嵌入:将自然界语言转换为机器语言,并将每一个单词用embedding表示。例如,构建词表,按照词表索引将句子向量化,向量化后作为模型的输入,经过文本嵌入转为高阶语义信息。
-
位置编码:
使用原因:
单词在句子中不同位置表示的含义不同
transformer中取消了如rnn的上下文嵌入过程,缺少了前后关联信息
使用方法:
原论文中采用余弦+正弦的方式进行位置编码
可以设计其他方法进行位置编码,例如学习出位置编码
(2)Encoder模块
多头自注意力层+规范化与残差连接
前馈全连接层+规范化与残差连接
- 规范化与残差连接:原文的设计如图所示,在每个模块后面加入正则化,正则化后再进行残差连接。有论文将正则化移到其他层后效果好于原论文,此处也可根据实际情况设计。
- 多头:在词嵌入维度上画切片(代码里是这样做的,一组qkv+不同的词嵌入维度(输入数据的不同切片),似乎与描述不太一致)
(3)Decoder模块
多头自注意力层+规范化与残差连接
多头注意力层+规范化与残差连接
前馈全连接层+规范化与残差连接
相比于encoder模块,decoder多了“多头注意力机制”,也是该模块将encoder和decoder模块连接。
3. 代码实现与解析
3.1 输入模块
(1)导入使用的包
import torch
import torch.nn as nn
import math
import copy
import torch.nn.functional as F
from torch.autograd import Variable
(2)文本嵌入模块
Class Embeddings(nn.Moudle):
def __init__(self, d_size, vocab):
'''
id_size: 词嵌入的维度(每个单词用多长的向量表示)
vocab: 词表的大小(每个句子的单词数量)
nn.embedding作用: 将给定的元素按照输入维度转换为向量
eg: [1,2] 当d_size为3时可以变换为: [[0.6473, 0.4637, 0.6574], [0.6472, 0.2784, 0.7482]]
'''
super(Embeddings, self).__init__()
self.d_size = d_size
self.embed = nn.Embedding(vocab, self.d_size)
def forward(self, x):
"""
x: 自然语言转换为机器语言后的向量 维度是:句子数量*句子长度(单词数)
eg: 英文单词按顺序转为数字产生的向量,即为x
按照词嵌入规则获得高阶语义
"""
# math 部分有缩放的作用
return slef.embed(x) * math.sqrt(self.d_size)
(3)位置编码模块
Class PositionalEncoding(nn.Module):
def __init__(self, d_size, dropout, max_len=5000):
'''
d_size: 词嵌入的维度
droupout: 置0比率
max_len: 每个句子的最大长度
'''
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
# 初始化位置编码,长度:最大句子长度(单词数量)/宽度:词嵌入的维度
pembed = torch.zero(max_len, d_size)
# 初始化绝对位置编码,即:按照单词的索引编码, 维度max_len*1
position = torch.arange(0, max_len).unsqueeze(1)
# 设计转换矩阵:1*d_size, 将绝对位置编码的维度扩展为文本嵌入的维度
# 转换矩阵还可以将绝对位置编码缩放为足够小的数字,从而便于梯度下降(sin cos)
div_term = torch.exp(torch.arange(0, d_size, 2)) * -(math.log(10000.0) / d_size))
pembed[:, 0::2] = torch.sin(position * div_term)
pembed[:, 1::2] = torch.cos(position