NLP基本模型总结(一)Transformer原理与代码解析

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 * 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)

3.2 通用架构实现

(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)))

3.3 Encoder模块

(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)

3.4 Decoder模块

(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)

3.5 输出模块

词表上每个词的概率,类似于“多分类”问题

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)

3.6 整体模型

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)
  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值