赞
踩
最近接触nlp领域,从最基础的模型学起,本文记录学习过程的个人理解,如有不对还请各位大神指正。
参考资料:
唐宇迪transformer
李宏毅transformer
我的理解:所谓的self-attention其实就是一种加权求特征的过程,在计算it的特征时考虑到其他所有词汇,方法是加权。
过程:
用三个向量表示输入数据,其中q与k代表输入数据去计算“加权值”,v用来进行加权的特征生成。
多头:类似于cnn中的channel(多个核),因为一组qkv只能获得“一种”特征,用多头捕获多层次的特征。
多层encoder-decoder堆叠:类似于cnn中的多个block(cnn的多个卷积+池化),显然在经典的cnn算法中不是只有一层block(特征语义不充分,高层语义的特征描述更有价值),因此这里采用多层堆叠的方式。
(1)输入模块:
文本嵌入:将自然界语言转换为机器语言,并将每一个单词用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)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。