【NLP】transformer学习

Transfomer学习

Transfomer

学习链接:Python人工智能20个小时玩转NLP自然语言处理【黑马程序员】
官方笔记:Transformer
我抄的一遍的代码:嘿嘿

Transformer的介绍

优势
1, Transformer能够利用分布式GPU进行并行训练,提升模型训练效率。
2, 在分析预测更长的文本时, 捕捉 间隔较长的语义关联效果更好。
作用
基于 seq2seq架构的transformer模型可以完成NLP领域研究的典型任务, 如机器翻译, 文本生成等。同时又可以构建预训练语言模型,用于不同任务的迁移学习。
视频学习中目标任务是语言翻译。
总体架构

【NLP】transformer学习
四个部分
  • 输入部分
  • 输出部分
  • 编码器部分:N个编码器层堆叠而成;每个编码器层由两个子层连接结构组成;第一个子层包括一个多头自注意力子层和规范化层,以及一个残差连接;第二个子层包括一个前馈全连接子层和规范化层,以及一个残差连接
  • 解码器部分:N个解码器层堆叠而成;每个解码器层由三个子层连接结构组成;第一个子层包括一个都投自注意力子层和规范化层,以及一个残差连接;第二个子层包括一个多头自注意力子层和规范化层,以及一个残差连接;第三个子层包括一个前馈全连接子层和规范化层,以及一个残差连接。

; 输入部分实现

内容:文本嵌入层和位置编码

输入部分包括:

  • 源文本嵌入层及其位置编码器
  • 目标文本嵌入层及其位置编码器
    【NLP】transformer学习

文本嵌入层

文本嵌入层作用:文本中词汇的数字表示转变为向量表示。在高维空间中捕捉词汇关系。

安装

使用pip安装的工具包包括pytorch-0.3.0, numpy, matplotlib, seaborn
pip install http://download.pytorch.org/whl/cu80/torch-0.3.0.post4-cp36-cp36m-linux_x86_64.whl numpy matplotlib seaborn

实际学习中我使用anaconda安装了numpy,matplotlib,seaborn,pytorch
pytorch:1.1.0 torch.__version__
numpy:1.19.2 安装anaconda的时候自带
其他:anaconda安装包指令 conda install package_name
gpu测试是否可用: print(torch.cuda.is_available())

代码分析

工具包:


import torch

import torch.nn as nn

import math

'''
Variable可以把输出的Tensor变成一个输入变量,这样梯度就不会回传了。
detach()也是可以的如果都不加那么就得retain_graph=True了,否则报错
'''
from torch.autograd import Variable

Embedding:


class Embeddings(nn.Module):
    def __init__(self, d_model, vocab):
"""
        :param d_model: 词嵌入维度
        :param vocab: 词表大小
"""
        super(Embeddings, self).__init__()

        self.lut = nn.Embedding(vocab, d_model)
        self.d_model = d_model

    def forward(self, x):
"""
        前向传播逻辑,当传给该类的实例化对象参数的时候,自动调用该函数
        :param x: Embedding层为首层,代表输入给模型的文本通过词汇映射后的张量
        :return:
"""

        return self.lut(x) * math.sqrt(self.d_model)

'''
每个数字被映射为三个数字
2 * 4 * 3 - 2 * 4 * d_model
tensor([[[ 2.5577,  0.4959, -2.0518], # 代表1
         [ 0.0447,  0.5555,  0.9907], # 代表2
         [ 0.1959, -1.9102,  0.4996], # 代表4
         [ 0.8895,  1.0859,  1.2877]], # 代表5

        [[ 0.1959, -1.9102,  0.4996],
         [-0.2384,  0.1826,  0.1482],
         [ 0.0447,  0.5555,  0.9907],
         [ 1.0254, -1.9821, -1.5096]]], grad_fn=)

Process finished with exit code 0
'''

'''
tensor([[[ 0.0000,  0.0000,  0.0000],
         [ 0.1535, -2.0309,  0.9315],
         [ 0.0000,  0.0000,  0.0000],
         [-0.1655,  0.9897,  0.0635]]])
'''

注意点:
nn调用Embedding的时候第一个参数是vocab,第二个参数是d_model
而视频中自定义的Embedding第一个参数是d_model,第二个参数是vocab
假设输入的张量x的shape为a x b,则经过文本嵌入层之后输出张量shape为 a x b x d_model
其实质上是把张量x中的每个数字映射为一个长度为1 x d_model 的向量。
测试:


d_model = 512
vocab = 1000

x = Variable(torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]]))
emb = Embeddings(d_model, vocab)
embr = emb(x)
print(embr)
print(embr.shape)
'''
tensor([[[-15.4299, -17.7916,  -8.9287,  ...,  14.3828,   4.4558, -18.0698],
         [ 40.8252,   1.6852, -26.8057,  ...,  16.4183,  -6.3022,   2.3319],
         [ -8.7622, -41.9216,  -6.2061,  ..., -37.4488, -39.5422, -14.5541],
         [-19.8698, -14.9421,  24.3235,  ..., -44.8080,   9.1618,   3.5722]],

        [[  8.3046,  26.9700,   1.9386,  ..., -15.4103, -19.7201,  19.4218],
         [ 20.7322,  11.4747, -33.0307,  ...,  28.0594, -21.4225, -68.9587],
         [-28.9082,   7.7140,   8.7951,  ...,  -2.4696,  27.7329,   7.1058],
         [  9.8008,  -8.0743,  30.7722,  ...,  15.2633, -24.3229, -14.5709]]],
       grad_fn=)
torch.Size([2, 4, 512])
'''

位置编码器

位置编码器作用:transformer的编码器结构中没有针对词汇位置信息处理。但是实际上词汇之间的顺序对于语义会有影响,因此需要在Embedding层之后加入位置编码器。

位置编码器编写

代码分析

前置内容

torch.arange() 获得连续自然数向量
torch.arange(start,end)的结果为[start,end-1]的连续自然数,数据类型为int64
torch.arange(start,end,step)的结果为[start,end-1]的步长为step的连续自然数

y = torch.arange(1, 6)
print(y)
>>>tensor([1, 2, 3, 4, 5])
print(y.dtype)
>>>torch.int64
y = torch.arange(1, 6, 2)
>>>tensor([1, 3, 5])

torch.squeeze()和torch.unsqueeze()
torch.unsqueeze(dim) 在正数第dim维上升维(从0开始), 如原来维度为[a0 x a1 x a2 x … x an]
升维之后维度为[a0 x a1 x … x a_{dim-1} x 1 x a_{dim} x … x an]
torch.unsqueeze(-dim)在倒数第dim维上升维(从-1开始), 如原来维度为[a0 x a1 x a2 x … x an]
升维之后维度为[a0 x a1 x … x a_{n-dim+1} x 1 x a_{n-dim+2} x … x an]

a = torch.arange(0, 6)
a = a.view(2, 3)
print(a.shape)
b = a.unsqueeze(1)
b = a.unsqueeze(0)
b = a.unsqueeze(-1)
b = a.unsqueeze(-2)

torch.squeeze(dim)在正数第dim维降维,如原来维度为[a0 x a1 x a2 x … x an]
降维之后维度为[a0 x a1 x … x a_{dim-1} x a_{dim+1} x … x an] 并且要求原来在dim维度上为1
torch.squeeze(-dim)在倒数第dim维降维,如原来维度为[a0 x a1 x a2 x … x an]
降维之后维度为[a0 x a1 x … x a_{n-dim} x a_{n-dim+2} x … x an] 并且要求原来在dim维度上为1

a = torch.arange(0, 6)
a = a.view(2, 3)
print(a.shape)
b = a.unsqueeze(0)
c = b.squeeze(0)
c = b.squeeze(-3)

nn.Dropout p代表让神经网络中多少比例的神经元失效

m = nn.Dropout(p=0.2)
input1 = torch.randn(4, 5)
print(input1)
output = m(input1)
print(output)

torch.exp 求以e为底的次幂值
注意:操作不支持Long类型的张量作为输入,因此需要把张量转换为浮点型

ans = torch.exp(torch.tensor([0.0, math.log(2.)]))

>>>tensor([1., 2.])
d_model = 512
position = torch.exp(torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model))

PositionalEncoding


class PositionalEncoding(nn.Module):
    def __init__(self, d_model, dropout, max_len=5000):
"""
        :param d_model: 词嵌入维度
        :param dropout: 置0比率
        :param max_len: 每个句子的最大长度
"""
        super(PositionalEncoding, self).__init__()

        self.dropout = nn.Dropout(p=dropout)

        pe = torch.zeros(max_len, d_model)

        position = torch.arange(0, max_len).unsqueeze(1)
        position = position.float()

        div_term = torch.exp(torch.arange(0, d_model, 2).float() * -(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):
"""
        :param x: 表示文本序列的词嵌入表示, 是一个三维向量,第一维(0)?第二维(1)是句子长度,第三维(2)是d_model
        :return:
"""

        x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)

        return self.dropout(x)

测试和部分问题

数据转移到GPU上
tensor为例,默认在cpu上建立,通过.cuda()是把cpu上的数据复制到GPU上,比较耗时
可以直接在GPU上创建数据,但是只能通过.tensor()建立,而不能用.LongTensor()
参考链接:https://blog.csdn.net/dong_liuqi/article/details/120099335


myTensor = torch.tensor([[100, 2, 421, 508], [491, 998, 1, 221]], device=device)
myTensor = myTensor.long()

myTensor = torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]], device=device)

判断模型和数据在GPU还是CPU

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

emb = emb.cuda()
emb.to(device)

print(next(emb.parameters()).is_cuda)

x = Variable(myTensor).cuda()

print(x.device)

测试

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
d_model = 512
dropout = 0.1
max_len = 60

emb = embedding.emb

emb.to(device)

myTensor = torch.tensor([[100, 2, 421, 508], [491, 998, 1, 221]], device=device)
myTensor = myTensor.long()
x = Variable(myTensor).cuda()
x = emb(x)

pe = PositionalEncoding(d_model, dropout, max_len)
pe.to(device)
pe_result = pe(x)
print(pe_result)
print(pe_result.shape)

绘制词向量特征的分布曲线

from transformer.positionalEncoding import PositionalEncoding as PositionalEncoding

import matplotlib.pyplot as plt
import numpy as np

plt.figure(figsize=(15, 5))

pe = PositionalEncoding(20, 0)

y = pe(Variable(torch.zeros(1, 100, 20)))

plt.plot(np.arange(100), y[0, :, 4:8].data.numpy())

plt.legend(["dim %d" % p for p in [4, 5, 6, 7]])
plt.show()

【NLP】transformer学习
作用:
  • 每条颜色的曲线代表某一个词汇中的特征在不同位置的含义.

  • 保证同一词汇随着所在位置不同它对应位置嵌入向量会发生变化【因为pe矩阵初始化为索引值】

  • 正弦波和余弦波的值域范围都是1到-1,很好的控制了嵌入数值的大小, 有助于梯度的快速计算

编码器部分实现

组成部分:

  • N个编码器层堆叠而成
  • 每个编码器层由两个子层连接结构组成
  • 第一个子层包括一个多头自注意力子层和规范化层,以及一个残差连接
  • 第二个子层包括一个前馈全连接子层和规范化层,以及一个残差连接
    【NLP】transformer学习

; 掩码张量

内容
值为0或1。1表示遮掩还是0表示遮掩是可以人为定义的。
输入掩码张量后两个维度的大小。最后输出一个三维张量。 1 x dim x dim
作用
主要用在attention里面。有些attention张量中的值可能包含了未来信息。但是实际上解码器不是一次就得到的,而是迭代得到的。因此需要遮掩一些”未来”的数据。
代码分析
前置内容

numpy.triu 形成三角矩阵
np.triu(x,k=?) x代表原矩阵,k代表偏移量。如果k为0生成主对角线以下的数据为0的三角矩阵。
如果k大于0,零数据范围上移k步
如果k小于0,零数据范围下移|k|步

import numpy as np

x = [[1, 2, 3],
     [4, 5, 6],
     [7, 8, 9]]
ans = np.triu(x, k=0)
print(ans)
>>>[[1 2 3]
    [0 5 6]
    [0 0 9]]
ans = np.triu(x, k=1)
print(ans)
>>>[[0 2 3]
    [0 0 6]
    [0 0 0]]
ans = np.triu(x, k=-1)
print(ans)
>>>[[1 2 3]
    [4 5 6]
    [0 8 9]]

subsequent_mask代码实现:

import numpy as np

def subsequent_mask(size):
"""
    生成向后遮掩的掩码张量
    :param size:掩码张量最后两个维度的大小, 它的最后两维形成一个方阵
    :return:
"""

    attn_shape = (1, size, size)

    subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')

    return torch.from_numpy(1 - subsequent_mask)

测试和可视化


size = 5
sm = subsequent_mask(size)
print(sm)
plt.figure(figsize=(5,5))
plt.imshow(subsequent_mask(20)[0])
plt.savefig('./sm.png')
plt.show()

该代码中,1表示被遮掩(黄色部分)

【NLP】transformer学习

注意力机制

内容
三个指定输入Q-query,K-key,V-value。通过公式得到注意力的计算结构,代表query在key和value作用下的表示。计算规则有很多种。视频中介绍了一种
A t t e n t i o n ( Q , K , V ) = s o f t m a x ( Q K T d k ) V Attention(Q,K,V)=softmax(\dfrac{QK^T}{\sqrt{d_k}})V A t t e n t i o n (Q ,K ,V )=s o f t m a x (d k ​​Q K T ​)V
比喻解释:

假如我们有一个问题: 给出一段文本,使用一些关键词对它进行描述,
为了方便统一正确答案预先写出的一些关键词作为提示:key,
整个的文本信息:query
我们看到这段文本信息后,脑子里浮现的答案信息:value
假设大家最开始不是很聪明,第一次看到这段文本后脑子里基本上浮现的信息value就只有提示内容key,
因此key与value基本是相同的,但是随着对这个问题的深入理解,脑子里想起来的东西原来越多,
并且能够对query提取关键信息进行表示,从而value发生了变化:注意力作用的过程
根据提示key生成了query的关键词表示方法是另外一种特征表示方法.

一般注意力机制:key和value默认相同,和query不同-使用不同于给定文本的关键词表示它
自注意力机制:query、key、value相同-需要用给定文本自身来表达自己,也就是说你需要从给定文本中抽取关键词来表述它, 相当于对文本自身的一次特征提取.

结构:

【NLP】transformer学习
矩阵乘法(Q K T QK^T Q K T)-规范化(d k \sqrt{d_k}d k ​​)-掩码-softmax(s o f t m a x softmax s o f t m a x)-矩阵乘法(s o f t m a x ( … ) V softmax(\dots)V s o f t m a x (…)V)

; softmax

参考链接:link
多分类的场景(目标函数常选择交叉熵cross-entropy)中使用广泛。把一些输入映射为0-1之间的实数,并且归一化保证和为1,因此多分类的概率之和也为1。
定义:对于一个数组V V V,则元素V i V_i V i ​的softmax值为:
S i = e i ∑ j e j S_i=\dfrac{e_i}{\sum_je^j}S i ​=∑j ​e j e i ​​
即该元素的指数与所有元素指数和的比值。好处在于求导反向更新梯度简单。
测试:

import torch.nn.functional as F
t = torch.arange(0, 24)v
t = t.view(2, 3, 4).float()
print(t)
t = F.softmax(t, 1)
print(t)

判断的时候可以数[符号,第一个[代表第一维。
softmax函数中参数1是softmax对象, 参数2是softmax对象的目标维度。目标维度上的值之和为1。
代码分析
前置内容

numpy.transpose 转置
numpy.transpose(0,1,2) 不变 其代表的是把原来的第0维放到第0维上,原来的第1维放到第1维上…

numpy.transpose(1,0)代表二维转置,原来的第0维(行)放到第1维(列)上,原来的第1维同理
例: arr.tranpose(2,1,0)假设原来arr为2x3x4,则执行后为4x3x2

import numpy as np
arr = np.arange(24).reshape((2, 3, 4))
print('init arr:------------------')
print(arr)
print(arr.shape)
arr = arr.transpose(1, 0, 2)
print('change arr:----------------')
print(arr)
print(arr.shape)

attention函数

import torch.nn.functional as F

def attention(query, key, value, mask=None, dropout=None):
"""
    :param query: 文本 最后一维的维度为d_model 三维的情况下 a x b x d_model
    :param key: 关键词 同上
    :param value: 思考的答案 同上
    :param mask: 掩码张量 三维情况下 a x b x b
    :param dropout: nn.Dropout层的实例化对象
    :return: 三维情况下 a x b x d_model
"""

    d_k = query.size(-1)

    scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)

    if mask is not None:

        scores = scores.masked_fill(mask == 0, -1e9)

    p_attn = F.softmax(scores, dim=-1)

    if dropout is not None:
        p_attn = dropout(p_attn)

    return torch.matmul(p_attn, value), p_attn

自注意力测试

from transformer.input.positionalEncoding import pe_result as pe_result

query = key = value = pe_result

mask = Variable(torch.zeros(2, 4, 4)).cuda()

attn, p_attn = attention(query, key, value, mask=mask)
print('attn', attn)
print('p_attn', p_attn)

多头注意力机制

内容
只有一组线性变换层(对QKV变化),不会改变原有张量的尺寸,因此变化矩阵是方阵。
每个头从词义层面分割输出的张量,即每个头都想获得一组QKV进行注意力机制计算。但是句子中每个词的表示只获得一部分,即只分割了最后一维的词嵌入向量。(即把d_model=512划分为4个头128或8个头64)
作用
让每个注意力机制去优化每个词汇的不同特征部分(d_model中的不同部分),从而均衡同一种注意力机制可能产生的偏差,让词义拥有多元的表达,实验表明可以提升模型效果。

结构:

【NLP】transformer学习
QKV经过线性层 – h代表多头,规范化点乘注意力 – concat连接 – 线性层

代码分析
前置自定义克隆函数clones


def clones(module, N):
"""
    用于生成相同网络层的克隆函数
    :param module: 示要克隆的目标网络层
    :param N: 克隆的数量
    :return:
"""
    return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])

多头注意力机制实现:
注意!torch.view改变形状的时候并不改变内存中数据的存储。但是torch.transpose会改变!


class MultiHeadedAttention(nn.Module):
    def __init__(self, head, embedding_dim, dropout=0.1):
"""
        :param head: 头数
        :param embedding_dim: 词嵌入的维度 d_model
        :param dropout: 置零比率
"""
        super(MultiHeadedAttention, self).__init__()

        assert embedding_dim % head == 0

        self.d_k = embedding_dim // head

        self.head = head

        self.linears = clones(nn.Linear(embedding_dim, embedding_dim), 4)

        self.attn = None

        self.dropout = nn.Dropout(p=dropout)

    def forward(self, query, key, value, mask=None):
"""
        :param query: 文本 三维向量 第3(2)维的维度为d_model  a x b x d_model
        :param key: 关键词 三维向量 第3(2)维的维度维d_model
        :param value: 思考的答案 三维向量 第3(2)维的维度维d_model
        :param mask: 掩码张量 默认为None a x b x b
        :return:
"""

        if mask is not None:

            mask = mask.unsqueeze(0)

        batch_size = query.size(0)

        query, key, value = \
            [model(x).view(batch_size, -1, self.head, self.d_k).transpose(1, 2)
             for model, x in zip(self.linears, (query, key, value))]

        x, self.attn = \
            attention(query, key, value, mask=mask, dropout=self.dropout)

        x = x.transpose(1, 2).contiguous().view(batch_size, -1, self.head * self.d_k)

        return self.linears[-1](x)

为什么分割词嵌入维度的时候要在第二维上自适应字词数量然后转置,而不直接在第三维自适应
以普通的矩阵举个例子:

t = torch.arange(0, 24)
a = t.view(2, 12)
a = a.unsqueeze(0)
print(a.shape)
print(a)
'''
tensor([[[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11],
         [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]]])
'''

b = t.view(1, -1, 3, 4)
c = b.transpose(1, 2)
print(c)
'''
tensor([[[[ 0,  1,  2,  3],  # 头1
          [12, 13, 14, 15]],

         [[ 4,  5,  6,  7],  # 头2
          [16, 17, 18, 19]],

         [[ 8,  9, 10, 11],  # 头3
          [20, 21, 22, 23]]]])
'''

b = t.view(1, -1, 2, 4)
print(b)
'''
tensor([[[[ 0,  1,  2,  3],  # 头1
          [ 4,  5,  6,  7]],

         [[ 8,  9, 10, 11],  # 头2
          [12, 13, 14, 15]],

         [[16, 17, 18, 19],  # 头3
          [20, 21, 22, 23]]]])
'''

容易发现,当不转置直接自适应的时候,每个字词的词嵌入维度值并没有均分给每个头,会出现一个字词均分为三份后其中两份在同一个头中的情况。因此需要采取转置版进行操作。

测试


head = 8
embedding_dim = 512
dropout = 0.2

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
from transformer.input.positionalEncoding import pe_result as pe_result
query = key = value = pe_result
mask = Variable(torch.zeros(8, 4, 4)).cuda()
mha = MultiHeadedAttention(head, embedding_dim, dropout)
mha.to(device)
print('input mask-shape:', mask.shape)
mha_result = mha(query, key, value, mask)
print(mha_result)

为什么mask的大小为[8x4x4]?

x张量为 [2x4x512],2条样本,每条样本长度为4,词嵌入维度为512
head头数为8,则每个头分到的词嵌入维度为512/8=64。
初始QKV的维度均为[2x4x512],多头转置处理后均为[2x8x4x64]
进入attention之后mask对相乘规范后的QK^T进行处理。此时该矩阵的大小为
[2x8x4x64] x [2x8x64x4] = [2x8x4x4],因此需要一个四维的mask。
在普通的注意力机制中mask为三维。
因此输入到多头注意力中的mask只需要三维。大小为[8x4x4]。然后在多头注意力的forward代码中对mask维度拓展了一维。维度变为[1x8x4x4]

前馈全连接层

内容:具有两层线性层的全连接网络,不改变张量的维度大小。
作用:注意力机制可能对复杂过程的拟合程度不够, 通过增加两层全连接来增强模型的能力【实验结果表明】

代码分析
前置内容

F.relu 激活函数:用来加入 非线性因素的,因为线性模型的表达能力不够
ReLU 函数公式:R e L U ( x ) = m a x ( 0 , x ) ReLU(x) = max(0,x)R e L U (x )=m a x (0 ,x )
常见的激活函数 (参考链接 为什么要使用激活函数

  • sigmoid s i g m o i d ( x ) = 1 1 + e − x sigmoid(x)=\dfrac{1}{1+e^{-x}}s i g m o i d (x )=1 +e −x 1 ​ 能把连续实值变化为0和1之间的输出
    缺点1:梯度反向传递容易导致梯度爆炸和梯度消失。梯度消失概率大。
    缺点2:output不是0均值。导致后一层的神经元得到上一层输出的非0均值的信号作为输入。收敛缓慢
    缺点3:解析式中含有幂运算,计算机求解耗时
  • tanh函数t a n h ( x ) = e x − e − x e x + e − x tanh(x) = \dfrac{e^x – e^{-x}}{e^x + e^{-x}}t a n h (x )=e x +e −x e x −e −x ​ 能把实值变化为-1和1之间的输出
    优点:解决了sigmoid的output非0均值;
    缺点:没有解决梯度消失和幂运算耗时问题
  • Relu R e l u ( x ) = m a x ( 0 , x ) Relu(x)=max(0,x)R e l u (x )=m a x (0 ,x ) 并不是全区间可导
    优点1:在正区间解决了梯度消失
    优点2:计算快 只需要判断输入是否大于0
    优点3:收敛速度比sigmoid和tanh快
    缺点1:输出不是output0均值
    缺点2:dead Relu 问题,某些神经元可能永远不会激活。(原因:1参数初始化不幸;2lr太高)(解决方式:Xavier初始化,避免lr过大,可以使用adagrad自动调节lr)

前馈全连接层代码实现


class PositionwiseFeedForward(nn.Module):
    def __init__(self, d_model, d_ff, dropout=0.1):
"""
        :param d_model: 词嵌入维度 第一个线性层的输入维度==第二个线性层的输出维度
        :param d_ff: 第一个线性层的输出维度==第二个线性层的输入维度
        :param dropout: 置零比率
"""
        super(PositionwiseFeedForward, self).__init__()

        self.w1 = nn.Linear(d_model, d_ff)
        self.w2 = nn.Linear(d_ff, d_model)

        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
"""
        :param x: 来自上一层的输出
        :return:
"""

        return self.w2(self.dropout(F.relu(self.w1(x))))

测试


from transformer.encoder.multihead import mha_result as mha_result

d_model = 512

d_ff = 64
dropout = 0.2

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
x = mha_result
ff = PositionwiseFeedForward(d_model, d_ff, dropout)
ff.to(device)
ff_result = ff(x)
print(ff_result)

规范化层

内容和作用:所有深层网络模型都需要的标准网络层,因为随着网络层数的增加,通过多层的计算后参数可能过大或过小,可能导致模型收敛非常的慢。因此需要在一定层数后接规范化层进行数值的规范化,使特征数值在合理范围内。
代码实现
LayerNorm类


class LayerNorm(nn.Module):
    def __init__(self, features, eps=1e-6):
"""
        :param features: 词嵌入维度
        :param eps: 一个足够小的数 出现在规范化公式的分母中防止除0
"""
        super(LayerNorm, self).__init__()

        self.a2 = nn.Parameter(torch.ones(features))
        self.b2 = nn.Parameter(torch.zeros(features))

        self.eps = eps

    def forward(self, x):
"""
        :param x:
        :return:
"""
        """输入参数x代表来自上一层的输出"""

        mean = x.mean(-1, keepdim=True)
        std = x.std(-1, keepdim=True)
        return self.a2 * (x - mean) / (std + self.eps) + self.b2

测试


feature = d_model = 512
eps = 1e-6

from transformer.encoder.positionwise import ff_result as ff_result
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
x = ff_result
ln = LayerNorm(feature, eps)
ln.to(device)
ln_result = ln(x)
print(ln_result)

子层连接结构

内容
输入到每个子层以及规范化层的过程中,还使用了残差链接(跳跃连接):子层连接(代表子层及其链接结构)。
在每个编码器层中,都有两个子层,这两个子层加上周围的连接结构就形成了两个子层连接结构.

子层连接结构图:

【NLP】transformer学习
代码实现
SublayerConnection

class SublayerConnection(nn.Module):
    def __init__(self, size, dropout=0.1):
"""
        :param size: 词嵌入维度大小
        :param dropout:  置零比率 节点数随机抑制
"""
        super(SublayerConnection, self).__init__()

        self.norm = LayerNorm(size)

        self.dropout = nn.Dropout(p=dropout)

    def forward(self, x, sublayer):
"""
        :param x: 接收上一个层或者子层的输入
        :param sublayer: 该子层连接中的子层函数
        :return:
"""

        return x + self.dropout(sublayer(self.norm(x)))

测试


size = d_model = 512
dropout = 0.2
head = 8

from transformer.input.positionalEncoding import pe_result as pe_result
from transformer.encoder.multihead import MultiHeadedAttention as MultiHeadedAttention
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
x = pe_result
mask = Variable(torch.zeros(8, 4, 4)).cuda()

self_attn = MultiHeadedAttention(head, d_model)
self_attn.to(device)

sublayer = lambda x: self_attn(x, x, x, mask)

sc = SublayerConnection(size, dropout)
sc.to(device)
sc_result = sc(x, sublayer)
print(sc_result)

编码器层

内容:是编码器的组成单元。每个编码器层完成一次对输入的特征提取过程, 即编码过程.

结构图:

【NLP】transformer学习
代码实现
EncoderLayer

class EncoderLayer(nn.Module):
    def __init__(self, size, self_attn, feed_forward, dropout):
"""
        :param size: 词嵌入维度 - 将作为编码器层的大小
        :param self_attn: 多头自注意力子层实例化对象 - 自注意力机制
        :param feed_forward: 前馈全连接层实例化对象
        :param dropout: 置0比率
"""
        super(EncoderLayer, self).__init__()

        self.self_attn = self_attn
        self.feed_forward = feed_forward

        self.sublayer = clones(SublayerConnection(size, dropout), 2)

        self.size = size

    def forward(self, x, mask):
"""
        :param x: 上一层的输出
        :param mask: 掩码张量mask
        :return:
"""

        x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))

        return self.sublayer[1](x, self.feed_forward)

测试


from transformer.input.positionalEncoding import pe_result as pe_result
from transformer.encoder.multihead import MultiHeadedAttention as MultiHeadedAttention
from transformer.encoder.positionwise import PositionwiseFeedForward as PositionwiseFeedForward
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
size = d_model = 512
head = 8
d_ff = 64
x = pe_result
dropout = 0.2
self_attn = MultiHeadedAttention(head, d_model)
self_attn.to(device)
ff = PositionwiseFeedForward(d_model, d_ff, dropout)
ff.to(device)
mask = Variable(torch.zeros(8, 4, 4)).cuda()

el = EncoderLayer(size, self_attn, ff, dropout)
el.to(device)
el_result = el(x, mask)
print(el_result)
print(el_result.shape)

编码器

作用:对输入进行指定的特征提取过程, 也称为编码, 由N个编码器层堆叠而成
结构图:

【NLP】transformer学习
代码实现
Encoder

class Encoder(nn.Module):
    def __init__(self, layer, N):
"""
        :param layer: 编码器层
        :param N: 编码器层的个数
"""
        super(Encoder, self).__init__()

        self.layers = clones(layer, N)

        self.norm = LayerNorm(layer.size)

    def forward(self, x, mask):
"""
        :param x: 上一层的输出
        :param mask: 掩码张量
        :return:
"""

        for layer in self.layers:
            x = layer(x, mask)
        return self.norm(x)

测试


from transformer.encoder.multihead import MultiHeadedAttention as MultiHeadedAttention
from transformer.encoder.positionwise import PositionwiseFeedForward as PositionwiseFeedForward
from transformer.encoder.encoderLayer import EncoderLayer as EncoderLayer
from transformer.input.positionalEncoding import pe_result as pe_result
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
size = d_model = 512
head = 8
d_ff = 64
x = pe_result
dropout = 0.2
c = copy.deepcopy

attn = MultiHeadedAttention(head, d_model)
attn.to(device)
ff = PositionwiseFeedForward(d_model, d_ff, dropout)
ff.to(device)
mask = Variable(torch.zeros(8, 4, 4)).cuda()

layer = EncoderLayer(size, c(attn), c(ff), dropout)
layer.to(device)

N = 8

en = Encoder(layer, N)
en.to(device)
en_result = en(x, mask)

解码器部分实现

解码器层

组成部分

  • N个解码器层堆叠而成
  • 每个解码器层由三个子层连接结构组成
  • 第一个子层连接结构:一个多头 注意力子层和规范化层以及一个残差连接
  • 第二个子层连接结构:一个多头注意力子层和规范化层以及一个残差连接
  • 第三个子层连接结构:一个前馈全连接子层和规范化层以及一个残差连接
    结构图:
    【NLP】transformer学习
    作用:每个解码器层根据给定的输入向目标方向进行特征提取操作,即解码过程.

代码实现
DecoderLayer

from transformer.encoder.multihead import clones as clones
from transformer.encoder.sublayerConnection import SublayerConnection as SublayerConnection

class DecoderLayer(nn.Module):
    def __init__(self, size, self_attn, src_attn, feed_forward, dropout):
"""
        :param size: 词嵌入维度 - 解码器层大小
        :param self_attn: 多头自注意力实例化对象 Q=K=V
        :param src_attn: 多头注意力实例化对象 Q!=K=V
        :param feed_forward: 前馈全连接层实例化对象
        :param dropout: 置零比率
"""
        super(DecoderLayer, self).__init__()
        self.size = size
        self.self_attn = self_attn
        self.src_attn = src_attn
        self.feed_forward = feed_forward

        self.sublayer = clones(SublayerConnection(size, dropout), 3)

    def forward(self, x, memory, source_mask, target_mask):
"""
        :param x: 上一层的输入x
        :param memory: 编码器层的语义存储变量memory 编码器的输出
        :param source_mask: 源数据掩码张量
        :param target_mask: 目标数据掩码张量
        :return:
"""

        m = memory

        x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, target_mask))

        x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, source_mask))

        return self.sublayer[2](x, self.feed_forward)

测试


from transformer.encoder.multihead import MultiHeadedAttention as MultiHeadedAttention
from transformer.encoder.positionwise import PositionwiseFeedForward as PositionwiseFeedForward
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
head = 8
size = d_model = 512
d_ff = 64
dropout = 0.2

self_attn = src_attn = MultiHeadedAttention(head, d_model, dropout)
self_attn.to(device)
src_attn.to(device)

ff = PositionwiseFeedForward(d_model, d_ff, dropout)
ff.to(device)

from transformer.input.positionalEncoding import pe_result as pe_result
from transformer.encoder.encoder import en_result as en_result

x = pe_result

memory = en_result

mask = Variable(torch.zeros(8, 4, 4)).cuda()
source_mask = target_mask = mask

dl = DecoderLayer(size, self_attn, src_attn, ff, dropout)
dl.to(device)
dl_result = dl(x, memory, source_mask, target_mask)
print(dl_result)
print(dl_result.shape)

解码器

作用:根据编码器的结果以及上一次预测的结果, 对下一次可能出现的’值’进行特征表示
代码实现
Decoder

from transformer.encoder.multihead import clones as clones
from transformer.encoder.layerNorm import LayerNorm as LayerNorm
import copy

class Decoder(nn.Module):
    def __init__(self, layer, N):
"""
        :param layer: 解码器层layer
        :param N: 解码器层的个数N
"""
        super(Decoder, self).__init__()

        self.layers = clones(layer, N)
        self.norm = LayerNorm(layer.size)

    def forward(self, x, memory, source_mask, target_mask):
"""
        :param x: 目标数据的嵌入表示
        :param memory: 编码器层的输出
        :param source_mask: 源数据的掩码张量
        :param target_mask: 目标数据的掩码张量
        :return:
"""

        for layer in self.layers:
            x = layer(x, memory, source_mask, target_mask)
        return self.norm(x)

测试


from transformer.encoder.multihead import MultiHeadedAttention as MultiHeadedAttention
from transformer.encoder.positionwise import PositionwiseFeedForward as PositionwiseFeedForward
from transformer.decoder.decoderLayer import DecoderLayer as DecoderLayer
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
size = d_model = 512
head = 8
d_ff = 64
dropout = 0.2
c = copy.deepcopy
attn = MultiHeadedAttention(head, d_model)
attn.to(device)
ff = PositionwiseFeedForward(d_model, d_ff, dropout)
ff.to(device)
layer = DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout)
layer.to(device)
N = 8

from transformer.input.positionalEncoding import pe_result as pe_result
from transformer.encoder.encoder import en_result as en_result
x = pe_result
memory = en_result
mask = Variable(torch.zeros(8, 4, 4)).cuda()
source_mask = target_mask = mask

de = Decoder(layer, N)
de.to(device)
de_result = de(x, memory, source_mask, target_mask)
print(de_result)
print(de_result.shape)

输出部分实现

内容:线性层+softmax层
线性层作用:对上一步的线性变化得到指定维度的输出, 也就是 转换维度的作用.

softmax作用:使最后一维的向量中的数字缩放到0-1的概率值域内, 并满足他们的和为1
结构图:

【NLP】transformer学习
代码实现
生成器类

import torch.nn.functional as F

class Generator(nn.Module):
    def __init__(self, d_model, vocab_size):
"""
        :param d_model: 词嵌入维度
        :param vocab_size: 词表大小
"""
        super(Generator, self).__init__()

        self.project = nn.Linear(d_model, vocab_size)

    def forward(self, x):
"""
        :param x: 上一层的输出张量x
        :return:
"""

        return F.log_softmax(self.project(x), dim=-1)

测试


d_model = 512
vocab_size = 1000

from transformer.decoder.decoder import de_result as de_result
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
x = de_result

gen = Generator(d_model, vocab_size)
gen.to(device)
gen_result = gen(x)
print(gen_result)
print(gen_result.shape)

模型构建

编码器-解码器结构

内容

  • 完整实现编码器解码器结构实现
  • 实现transformer模型
    结构图:

【NLP】transformer学习
代码实现
EncoderDecoder

class EncoderDecoder(nn.Module):
    def __init__(self, encoder, decoder, source_embed, target_embed, generator):
"""
        :param encoder: 编码器对象
        :param decoder: 解码器对象
        :param source_embed: 源数据嵌入函数
        :param target_embed: 目标数据嵌入函数
        :param generator: 生成器对象 - 线性层和softmax
"""
        super(EncoderDecoder, self).__init__()
        self.encoder = encoder
        self.decoder = decoder
        self.src_embed = source_embed
        self.tgt_embed = target_embed
        self.generator = generator

    def forward(self, source, target, source_mask, target_mask):
"""
        :param source: 源数据 - 经过词嵌入位置编码的数据
        :param target: 目标数据
        :param source_mask: 源数据掩码张量
        :param target_mask: 目标数据掩码张量
        :return:
"""

        return self.decode(self.encode(source, source_mask), source_mask,
                           target, target_mask)

    def encode(self, source, source_mask):
"""
        编码函数
        :param source: 源数据
        :param source_mask: 源数据掩码
        :return:
"""

        return self.encoder(self.src_embed(source), source_mask)

    def decode(self, memory, source_mask, target, target_mask):
"""
        :param memory: 编码器的输出
        :param source_mask: 源数据掩码
        :param target:
        :param target_mask: 目标数据掩码
        :return:
"""

        return self.decoder(self.tgt_embed(target), memory, source_mask, target_mask)

测试


from transformer.encoder.encoder import en as en
from transformer.decoder.decoder import de as de
from transformer.output.output import gen as gen
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

vocab_size = 1000
d_model = 512
encoder = en
decoder = de
source_embed = nn.Embedding(vocab_size, d_model)
target_embed = nn.Embedding(vocab_size, d_model)
generator = gen

content = torch.tensor([[100, 2, 421, 508], [491, 998, 1, 221]], device=device)
content = content.long()
source = target = Variable(content).cuda()

source_mask = target_mask = Variable(torch.zeros(8, 4, 4)).cuda()

ed = EncoderDecoder(encoder, decoder, source_embed, target_embed, generator)
ed.to(device)

ed_result = ed(source, target, source_mask, target_mask)
print(ed_result)
print(ed_result.shape)

模型构建

代码实现

import copy
from transformer.input.positionalEncoding import PositionalEncoding as PositionalEncoding
from transformer.input.embedding import Embeddings as Embeddings
from transformer.encoder.multihead import MultiHeadedAttention as MultiHeadedAttention
from transformer.encoder.positionwise import PositionwiseFeedForward as PositionwiseFeedForward
from transformer.encoder.encoderLayer import EncoderLayer as EncoderLayer
from transformer.encoder.encoder import Encoder as Encoder
from transformer.decoder.decoderLayer import DecoderLayer as DecoderLayer
from transformer.decoder.decoder import Decoder as Decoder
from transformer.output.output import Generator
from transformer.model.encoderDecoder import EncoderDecoder

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

def make_model(source_vocab, target_vocab, N=6,
               d_model=512, d_ff=2048, head=8, dropout=0.1):
"""
    构建transformer模型
    :param source_vocab: 源数据词表总数
    :param target_vocab: 目标数据词表总数
    :param N: 编码器层和解码器层的层数
    :param d_model: 词嵌入维度
    :param d_ff: 前馈全连接层中变换矩阵的维度
    :param head: 多头注意力机制中的头数
    :param dropout: 置零比率
    :return:
"""

    c = copy.deepcopy

    attn = MultiHeadedAttention(head, d_model)
    attn.to(device)

    ff = PositionwiseFeedForward(d_model, d_ff, dropout)
    ff.to(device)

    position = PositionalEncoding(d_model, dropout)
    position.to(device)

    model = EncoderDecoder(
        Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N),
        Decoder(DecoderLayer(d_model, c(attn), c(attn),
                             c(ff), dropout), N),
        nn.Sequential(Embeddings(d_model, source_vocab), c(position)),
        nn.Sequential(Embeddings(d_model, target_vocab), c(position)),
        Generator(d_model, target_vocab))
    model.to(device)

    for p in model.parameters():
        if p.dim() > 1:
            nn.init.xavier_uniform_(p)
    return model

测试


source_vocab = 11
target_vocab = 11
N = 6

if __name__ == '__main__':
    res = make_model(source_vocab, target_vocab, N)
    print(res)

基本测试运行

测试任务
copy任务描述: 针对数字序列进行学习, 学习的最终目标是使输出与输入的序列相同. 如输入[1, 5, 8, 9, 3], 输出也是[1, 5, 8, 9, 3].

任务意义:
copy任务在模型基础测试中具有重要意义,因为copy操作对于模型来讲是一条明显规律, 因此模型能否在短时间内,小数据集中学会它,可以帮助我们断定模型所有过程是否正常,是否已具备基本学习能力.

过程
1.构建数据集生成器
2.获得Transformer模型及其优化器和损失函数
3.运行模型进行训练和评估
4.使用模型进行贪婪解码

1. 构建数据生成器

代码实现

import numpy as np

from pyitcast.transformer_utils import Batch

def data_generator(V, batch, num_batch):
"""
    该函数用于随机生成copy任务的数据
    :param V: 随机生成数字的最大值+1
    :param batch: 每次输送给模型更新一次参数的数据量
    :param num_batch: 输送num_batch次完成一轮
    :return:
"""
    for i in range(num_batch):

        data = torch.from_numpy(np.random.randint(1, V, size=(batch, 10))).cuda()

        data[:, 0] = 1

        source = Variable(data, requires_grad=False).cuda()
        target = Variable(data, requires_grad=False).cuda()

        yield Batch(source, target)

V = 11

batch = 20

num_batch = 30

if __name__ == '__main__':
    res = data_generator(V, batch, num_batch)
    print(res)

2.获得Transformer模型及其优化器和损失函数

代码实现


from pyitcast.transformer_utils import get_std_opt
from transformer.model.makeModel import make_model

from pyitcast.transformer_utils import LabelSmoothing

from pyitcast.transformer_utils import SimpleLossCompute

model = make_model(V, V, N=2)

model_optimizer = get_std_opt(model)

criterion = LabelSmoothing(size=V, padding_idx=0, smoothing=0.0)

loss = SimpleLossCompute(model.generator, criterion, model_optimizer)

标签平滑测试

from pyitcast.transformer_utils import LabelSmoothing
import matplotlib.pyplot as plt

crit = LabelSmoothing(size=5, padding_idx=0, smoothing=0.5)

predict = Variable(torch.FloatTensor([[0, 0.2, 0.7, 0.1, 0],
                             [0, 0.2, 0.7, 0.1, 0],
                             [0, 0.2, 0.7, 0.1, 0]]))

target = Variable(torch.LongTensor([2, 1, 0]))

crit(predict, target)

plt.imshow(crit.true_dist)
plt.savefig('./smoothing.png')
plt.show()

【NLP】transformer学习

标签平滑图像分析
黄色小方块上:

  • 它相对于横坐标横跨的值域就是标签平滑后的正向平滑值域, 我们可以看到大致是从0.5到2.5.

  • 它相对于纵坐标横跨的值域就是标签平滑后的负向平滑值域, 我们可以看到大致是从-0.5到1.5, 总的值域空间由原来的[0, 2]变成了[-0.5, 2.5].

3.运行模型进行训练和评估

代码实现

由于pytorch版本问题需要修改pyitcast一行代码
参考链接:https://blog.csdn.net/chen645096127/article/details/94019443
把transformer_utils.py中SimpleLossCompute类中的 __call__的返回值改为 return loss.item() * norm


from pyitcast.transformer_utils import run_epoch

def run(model, loss, epochs=10):
"""
    :param model: 将要进行训练的模型
    :param loss: 损失计算方法
    :param epochs: 模型训练的轮数
    :return:
"""

    for epoch in range(epochs):

        model.train()

        run_epoch(data_generator(V, 8, 20), model, loss)

        model.eval()

        run_epoch(data_generator(V, 8, 5), model, loss)

epochs = 20

print('begin training ----------------------------------------')
run(model, loss, epochs)

4.使用模型进行贪婪解码

代码实现


from pyitcast.transformer_utils import greedy_decode
from transformer.packages import device as device

def run2(model, loss, epochs=10):
"""
    :param model: 训练的模型
    :param loss: 损失函数的计算方法
    :param epochs: 总共的计算轮次
    :return:
"""
    for epoch in range(epochs):
        model.train()

        run_epoch(data_generator(V, 8, 20), model, loss)
        model.eval()
        run_epoch(data_generator(V, 8, 5), model, loss)

    model.eval()

    source = Variable(torch.LongTensor([[1,3,2,5,4,6,7,8,9,10]]))

    source_mask = Variable(torch.ones(1, 1, 10))

    if device == torch.device('cuda:0'):
        source = source.cuda()
        source_mask = source_mask.cuda()

    result = greedy_decode(model, source, source_mask, max_len=10, start_symbol=1)
    print(result)

epochs = 30
if __name__ == '__main__':
    print('begin training and eval----------------------------------------')
    run2(model, loss, epochs)

模型的保存和加载
保存

path = './copyModel'
torch.save(model.state_dict(), path)

加载

path = './copyModel'
V = 11
model = make_model(V, V, N=2)
model.load_state_dict(torch.load(path))

Original: https://blog.csdn.net/Sapphire8/article/details/121452208
Author: Sapphire8
Title: 【NLP】transformer学习

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/531714/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球