【NLP】transformer学习

本文详细介绍了Transformer模型,包括其优势、作用和整体架构。文章深入探讨了模型的输入部分(文本嵌入层和位置编码器)、编码器部分(多头注意力机制、前馈全连接层等)和解码器部分的实现。此外,还提供了模型构建、训练和评估的步骤,以及在实际项目中的应用。
摘要由CSDN通过智能技术生成

Transfomer

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

Transformer的介绍

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

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

输入部分实现

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

输入部分包括:

  • 源文本嵌入层及其位置编码器
  • 目标文本嵌入层及其位置编码器
    在这里插入图片描述

文本嵌入层

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

安装

使用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

# 预定义的网络层torch.nn 如卷积层,lstm层,embedding层
import torch.nn as nn

# 数学计算工具包
import math

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

Embedding:

# 定义Embedding层实现文本嵌入层
class Embeddings(nn.Module):
    def __init__(self, d_model, vocab):
        """
        :param d_model: 词嵌入维度
        :param vocab: 词表大小
        """
        super(Embeddings, self).__init__()
        # 调用nn中预定义层Embedding 获得词嵌入对象self.lut
        self.lut = nn.Embedding(vocab, d_model)
        self.d_model = d_model

    def forward(self, x):
        """
        前向传播逻辑,当传给该类的实例化对象参数的时候,自动调用该函数
        :param x: Embedding层为首层,代表输入给模型的文本通过词汇映射后的张量
        :return:
        """
        # 将x传给Embedding层self.lut并与根号下self.d_model相乘作为返回结果
        # 缩放作用,维度越大
        return self.lut(x) * math.sqrt(self.d_model)


# 演示
# embedding = nn.Embedding(10, 3) #vocab d_model
# input = torch.LongTensor([[1, 2, 4, 5], [4, 3, 2, 9]]) # 两行四列
# res = embedding(input)
# print(res)
'''
# 每个数字被映射为三个数字
# 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=<EmbeddingBackward>)

Process finished with exit code 0
'''
# embedding = nn.Embedding(10, 3, padding_idx=0) # 作用在于让 0 所在值为0
# input = torch.LongTensor([[0, 2, 0, 5]])
# embedding(input)
'''
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封装的长整型张量, 形状是2 x 4
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)  # [2, 4, 512]
'''
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=<MulBackward0>)
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)  # 2 x 3
b = a.unsqueeze(1)  # 2 x 1 x 3 因为在第一维上增加了一维(从0开始)
b = a.unsqueeze(0)  # 1 x 2 x 3 因为在第零维上增加了一维(从0开始)
b = a.unsqueeze(-1) # 2 x 3 x 1 因为在倒数第一维上增加了一维(从-1开始)
b = a.unsqueeze(-2) # 2 x 1 x 3 因为在倒数第二维上增加了一维(从-1开始)

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)  # 2 x 3
b = a.unsqueeze(0)  # 1 x 2 x 3
c = b.squeeze(0)  # 正数第零维(从0开始)2 x 3
c = b.squeeze(-3)  # 倒数第三维(从-1开始)

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.)])) # math.log(x)为求以e为底的x的对数
# ans = torch.exp(torch.tensor([2,8],dtype=torch.float)) # 张量转换为浮点型
>>>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__()

        # 调用nn中预定义的Dropout层 获得Dropout对象self.dropout
        self.dropout = nn.Dropout(p=dropout)

        # 初始化一个位置编码矩阵, 它是一个0阵,[max_len x d_model]
        # 一共 max_len 个词, 每个词映射为 [1 x d_model]
        pe = torch.zeros(max_len, d_model)

        # 初始化一个绝对位置矩阵, 词汇的绝对位置就是用其索引表示.
        # arange获得连续自然数向量shape:[max_len] 范围为[0, max_len-1]
        # unsqueeze升维使向量成为矩阵shape:[max_len x 1]
        # 最后的矩阵中 max_len每一个代表一个词汇,值为对应的位置?????
        position = torch.arange(0, max_len).unsqueeze(1)  # max_len x 1
		position = position.float()  # 原position是Long div_term是float 需要统一格式

        # 绝对位置矩阵position[max_len x 1]初始化之后加入到位置编码矩阵pe[max_len x d_model]中
        # 最简单思路:先将绝对位置矩阵变换成max_len x d_model形状然后覆盖原来的初始位置编码矩阵
        # 因此需要一个变化矩阵div_term, 跳跃式初始化, 有如下要求:
        # 1. 形状为[1 x d_model] : div_term [max_len x 1] x [1 x d_model] = [max_len x d_model]
        # 2. 能够将自然数的绝对位置编码position缩放成足够小的数字,有助于之后的梯度下降过程中更快的收敛
        # div_term初始化过程:
        # 1. arange 获得自然数向量,范围为[0,d_model] 步长为2. shape:[d_model/2] 注意转化为float,因为exp不支持Long
        # 2. -(math.log(10000.0) / d_model作用在于缩放, 为了梯度下降更快收敛
        # 3. 在第一步中只初始化了一半的向量,因此需要初始化两次
        #   第一次 偶数列 正弦波; 第二次 奇数列 余弦波; div_term shape:[d_model/2]可以自动处理为[1 x d_model/2]
        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)  # max_len x d_model/2
        pe[:, 1::2] = torch.cos(position * div_term)

        # embedding 输出为 ? x ? x d_model 是一个三维向量
        # pe目前shape: max_len x d_model 是一个二维向量, 因此需要升维
        pe = pe.unsqueeze(0)  # pe shape:[1 x max_len x d_model]

        # 最后把pe位置编码矩阵注册成模型的buffer
        # buffer:对模型效果有帮助的,但不是模型结构中超参数或者参数,不需要随着优化步骤进行更新的增益对象.
        # 注册之后我们就可以在模型保存后重加载时和模型结构与参数一同被加载.
        self.register_buffer('pe', pe)

    def forward(self, x):
        """
        :param x: 表示文本序列的词嵌入表示, 是一个三维向量,第一维(0)?第二维(1)是句子长度,第三维(2)是d_model
        :return:
        """
        # pe位置编码矩阵 shape: 1 x max_len x d_model
        # 在相加之前我们对pe做一些适配工作, 将这个三维张量的第二维也就是句子最大长度的那一维将切片到与输入的x的第二维相同即x.size(1),
        # 因为我们默认max_len为5000一般来讲实在太大了,很难有一条句子包含5000个词汇,所以要进行与输入张量的适配.
        # 最后Variable封装,使其与x的样式相同,但位置并不需要进行梯度求解,因此requires_grad为false.
        # self.pe[:, :x.size(1)] == self.pe[:, :x.size(1), :]
        x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)
        # 最后self.dropout进行'丢弃'操作, 并返回结果.
        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放GPU上的两种方法
emb = emb.cuda()  # 1
emb.to(device)  # 2
# 判断模型emb在不在GPU上
print(next(emb.parameters()).is_cuda)
# 把数据x放到GPU上的方法
x = Variable(myTensor).cuda() 
# 判断数据x在不在GPU上
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  # 是Embedding的模型
# emb = emb.cuda()
emb.to(device)  # 把模型转移到GPU上
# print('model emb: ', next(emb.parameters()).is_cuda)  # 判断模型是否在GPU上
#  myTensor是输入的张量 2 x 4
myTensor = torch.tensor([[100, 2, 421, 508], [491, 998, 1, 221]], device=device)
myTensor = myTensor.long()  # 转化为LongTensor
x = Variable(myTensor).cuda()  # 输入的x张量
x = emb(x)  # 经过词嵌入模型处理后的x张量 x-shape:2 x 4 x d_model = 2 x 4 x 512
# print(x.device)  # 判断x张量的位置

# 处理位置编码器
pe = PositionalEncoding(d_model, dropout, max_len)  # 创建位置编码器实例
pe.to(device)  # PositionalEncoding 的模型
pe_result = pe(x)
print(pe_result)
print(pe_result.shape)  # x张量经过位置编码器处理后的结果 2 x 4 x 512
绘制词向量特征的分布曲线
from transformer.positionalEncoding import PositionalEncoding as PositionalEncoding

import matplotlib.pyplot as plt
import numpy as np

# 创建一张15 x 5大小的画布
plt.figure(figsize=(15, 5))

# 实例化PositionalEncoding类得到pe对象, 输入参数是20和0  - d_model, dropout
pe = PositionalEncoding(20, 0)  # d_model = 20, 在cpu上

# 向pe传入Variable封装的tensor, pe会直接执行forward函数,
# tensor张量为 1 x 100 x 20 一共100个词 每个词的词嵌入维度为 20, 假设给tensor以及经过embedding
# 且这个tensor里的数值都是0, 处理后相当于位置编码张量
y = pe(Variable(torch.zeros(1, 100, 20)))

# 定义画布的横纵坐标, 横坐标到100的长度, 纵坐标是某一个词汇中的某维特征在不同长度下对应的值
# 总共有20维(d_model = 20), 这里只查看4, 5, 6, 7维的值.
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()

测试图片
作用:

  • 每条颜色的曲线代表某一个词汇中的特征在不同位置的含义.
  • 保证同一词汇随着所在位置不同它对应位置嵌入向量会发生变化【因为pe矩阵初始化为索引值】
  • 正弦波和余弦波的值域范围都是1到-1,很好的控制了嵌入数值的大小, 有助于梯度的快速计算

编码器部分实现

组成部分:

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

掩码张量

内容
值为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],
     [
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值