最近接触nlp领域,从最基础的模型学起,本文记录学习过程的个人理解,如有不对还请各位大神指正。
参考资料:
唐宇迪transformer
李宏毅transformer
我的理解:所谓的self-attention其实就是一种加权求特征的过程,在计算it的特征时考虑到其他所有词汇,方法是加权。
过程:
用三个向量表示输入数据,其中q与k代表输入数据去计算“加权值”,v用来进行加权的特征生成。
多头:类似于cnn中的channel(多个核),因为一组qkv只能获得“一种”特征,用多头捕获多层次的特征。
多层encoder-decoder堆叠:类似于cnn中的多个block(cnn的多个卷积+池化),显然在经典的cnn算法中不是只有一层block(特征语义不充分,高层语义的特征描述更有价值),因此这里采用多层堆叠的方式。
文本嵌入:将自然界语言转换为机器语言,并将每一个单词用embedding表示。例如,构建词表,按照词表索引将句子向量化,向量化后作为模型的输入,经过文本嵌入转为高阶语义信息。
位置编码:
使用原因:
单词在句子中不同位置表示的含义不同
transformer中取消了如rnn的上下文嵌入过程,缺少了前后关联信息
使用方法:
原论文中采用余弦+正弦的方式进行位置编码
可以设计其他方法进行位置编码,例如学习出位置编码
(2)Encoder模块
多头自注意力层+规范化与残差连接
前馈全连接层+规范化与残差连接
(3)Decoder模块
多头自注意力层+规范化与残差连接
多头注意力层+规范化与残差连接
前馈全连接层+规范化与残差连接
相比于encoder模块,decoder多了“多头注意力机制”,也是该模块将encoder和decoder模块连接。
(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 * div_term)
# 目前pembed是二维矩阵:句子长度*词嵌入维度
# 为了与文本嵌入模块的输出合并,需要增加维度
pembed = pembed.unsqueeze(0)
# 将position编码注册为buffer
# buffer:可以像参数一样随着模型保存与加载,但是不再训练中更新
# 原因: 当d_size的维度确定时pembed是固定的,在整个任务中都不再改变,因此无需反复计算
# self.pembed即可调用
self.register_buffer('pembed', pembed)
def forward(self, x):
'''
x: 文本词嵌入后的向量
默认的max_len是5000,实际上句子很难这么长,因此只需要从位置嵌入中截取句子长度的位置编码即可
'''
x = x + Variable(self.pembed[:, :x.size(1)]), requires_grad=False)
return self.dropout(x)
(1)通用结构:clone、attention
def subsequent_mask(size):
'''
size: 最后两个维度的大小,方针
'''
attn_shape = (1, size, size)
# 上三角1,unit8:节省空间
subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('unit8')
# 变成下三角,1掩,0不掩
# numpy转为tensor
return torch.from_numpy(1-subsequent_mask)
def attention(query, key, value, mask=None, dropout=None):
'''
注意力机制实现
query: 查询向量,理解:原文本
key: 理解:给的参考
value: 理解:学习出的内容
mask: 掩码
'''
# 获得词嵌入的维度
d_size = query.size(-1)
# 按照公式计算注意力张量
scores = torch.matmul(query, key.tanspose(-2, -1)) / math.sqrt(d_size)
# 是否使用掩码张量
if mask is not None:
# 用-1e9替换score中的值,替换位置是mask为0的地方
scores = scores.masked_fill(mask == 0, -1e9)
p_attn = F.softmax(scores, dim=-1)
if dropout is not None:
p_attn = dropout(p_attn)
# 返回:attention得分,注意力张量
return torch.matmul(p_attn, value), p_attn
# 定义clone函数,因为在transformer中很多重复的模块,clone方便
# 其实就是把for循环拎出来写,用copy复制多层(copy可以保证复制的module训练时参数不同)
def clone(module, n):
return nn.ModuleList([copy.deepcopy(module) for _ in range(n)])
(2)多头注意力模块
class MultiHeadAttention(nn.Module):
def __init__(self, head, embedding_dim, dropout=0.1):
'''
head: 头数
embedding_dim: 词嵌入维度
'''
super(MultiHeadAttention, self).__init__()
# 使用测试中常见的assert语句判断head是否能被embedding_dim整除
assert embedding_dim % head == 0
self.d_k = embedding_dim // head
self.head = head
# 初始化4个liner层,三个用于q,k,v变换,一个用于拼接矩阵后的变换
self.liners = clone(nn.Liner(embedding_dim, embedding_dim), 4)
self.attn = None
self.dropout = nn.Dropout(dropout)
def forward(self, query, key, value, mask=None):
if mask is not None:
# 扩展mask维度,对应multihead
mask = mask.unsqueeze(1)
batch_size = query.size(0)
# q,k,v分别进入三个liner层,得到的结果进行多头转换
# -1: 句子的长度(单词数量)
# zip中的model有4层,输入变量只有三个,会自动匹配前三个
# 输出:batch_size*head*length*头切片后的维度
query, key, value = [model(x).view(batch_size, -1, self.head, self.d_k).transpose(1, 2)
for model, x in zip(self.liners, (query, key, value))]
# 计算attention
x, self.attn = attention(query, key, value, mask, self.dropout)
# x是多头的注意力结果,需要将头拼接
# 维度转换:batch_size*length*head*切片后的维度
x = x.transpose(1,2).contiguous().view(batch_size, -1, self.head*self.d_k)
# 拼接后的结果进入liner层
return self.liners[-1](x)
(3)前馈全连接模块
class PositionwiseFeedForward(nn.Module):
def __init__(self, embedding_dim, latent_dim, dropout=0.1):
'''
embedding_dim: 词嵌入维度(多头注意力模块的输出维度)
latent_dim: 隐藏层维度
'''
super(PositionwiseFeedForward, self).__init__()
self.l1 = nn.Liner(embedding_dim, latent_dim)
self.l2 = nn.Liner(latent_dim, embedding_dim)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
return self.l2(self.dropout(F.relu(self.l1(x))))
(4)规范化与残差连接
class LayerNorm(nn.Module):
def __init__(self, embedding_dim, eps=1e-6):
super(LayerNorm, self).__init__
self.a2 = nn.Parameter(torch.ones(embedding_dim))
self.b2 = nn.Parameter(torch.zeros(embedding_dim))
# 防止÷0
self.eps = eps
def forward(self, x):
# keepdim=True: 保持输入输出维度一致
mean = x.mean(-1, keepdim=True)
std = x.std(-1, keepdim=True)
return self.a2 * (x-mean) / (std+self.eps) + self.b2
class SublayerConnection(nn.Module):
def __init__(self, embedding_size, dropout=0.1):
super(SublayerConnection, self).__init__()
self.norm = LayerNorm(embedding_size)
self.dropout = nn.Dropout
def forward(self, x, sublayer):
'''
x: 上一模块的输出
逻辑: 将上一模块的输出规范化,将规范化后的结果输入子模块,进行残差连接
'''
return x + self.dropout(sublayer(self.norm(x)))
(1)编码器层
class EncoderLayer(nn.Module):
def __init__(self, embedding_size, attn_model, feed_forward, dropout):
'''
embedding_size: 词嵌入维度
attn_model: 多头注意力模块
feed_forward: 前馈全连接模块
'''
super(EncoderLayer, self).__init__()
self.attn_model = attn_model
self.feed_forward = feed_forward
self.sublayer = clone(SublayerConnection(embedding_size, dropout), 2)
self.embeddfing_size = embedding_size
def forward(self, x, mask):
# 第一个子模块:多头自注意力模块
x = self.sublayer[0](x, lambda x: self.attn_model(x, x, x, mask))
# 第二个子模块:前馈全连接
return self.sublayer[1](x, self.feed_forward)
(2)编码器
class Encoder(nn.Module):
def __init__(self, encoderlayer, N):
'''
layer: 编码器层
N: 堆叠编码器层数
'''
super(Encoder, self).__init__()
self.encoderlayers = clone(encoderlayer, N)
# encoderlayer.embedding_size: 获得词嵌入的数量
self.norm = LayerNorm(encoderlayer.embedding_size)
def forward(self, x, mask):
for layer in self.encoderlayers:
x = layer(x, mask)
return self.norm(x)
(1)解码器层
class DecoderLayer(nn.Module):
def __init__(self, embedding_size, attn_model_self, attn_model_com, feed_fprward, dropout=0.1):
super(DecoderLayer, self).__init__()
# com: common
self.embedding_size = embedding_size
self.attn_model_self = attn_model_self
self.attn_model_com = attn_model_com
self.feed_forward = feed_forward
self.sublayer = clone(SublayerConnection(embedding_size, dropout), 3)
def forward(self, x, memory, src_mask, tgt_mask):
'''
tgt_mask: 遮蔽掉未来信息,比如解码第二个词时,第三个及之后的词不使用
src_mask: 屏蔽对结果没用的信息
'''
x = self.sublayer[0](x, lambda x: self.attn_model_self(x, x, x, tgt_mask))
x = self.sublayer[1](x, lambda x: self.attn_model_com(x, memory, memory, src_mask))
return slef.sublayer[2](x, self.feed_forward)
(2)解码器
class Decoder(nn.Module):
def __init__(self, decoderlayer, N):
super(Decoder, self).__init__()
self.decoderlayers = clone(decoderlayer, N)
self.norm = LayerNorm(decoderlayer.embedding_size)
def forward(self, x, memory, src_mask, tgt_mask):
for layer in self.decoderlayers:
x = layer(x, memory, src_mask, tgt_mask)
return self.norm(x)
词表上每个词的概率,类似于“多分类”问题
class Generator(nn.Module):
def __init__(self, embedding_size, vocab_size):
super(Generator, self).__init__()
self.project = nn.Layer(embedding_size, vocab_size)
def forward(self, x):
return F.log_softmax(self.project(x), dim=-1)
N个Encoder+N个Decoder
class EncoderDecoder(nn.Module):
def __init__(self, encoder, decoder, src_em_model, tgt_em_model, generator):
'''
encoder: 编码器
decoder: 解码器
src_em_model: 源数据词嵌入模型
tgt_em_model: 目标数据词嵌入模型
generator: 输出
'''
super(EncoderDecoder, self).__init__()
self.encoder = encoder
self.decoder = decoder
self.src_em_model = src_em_model
self.tgt_em_model = tgt_em_model
self.generator = generator
def forward(self, source, target, src_mask, tgt_mask):
'''
source: 输入数据
target: 目标结果
src_mask: 输入数据掩码
tgt_mask: 目标数据掩码
'''
return self.decode(self.encode(source, src_mask), src_mask, target, tgt_mask)
def encode(self, source, src_mask):
return self.encoder(self_em_model(source), src_mask)
def decode(self, memory, src_mask, target, tgt_mask):
return self.decoder(self.tgt_em_model(target), memory, src_mask, tgt_mask)