|
总体流程 将原始段落 分句 分词,形成问答对 同时整理出词典 对词典的单词编号,并将分词后的单词问答对,转化为索引列表,问句与答句依然保持一一对应关系 索引列表补齐形成索引矩阵,同时标记单词位置得到布尔矩阵-mask 模型输入:索引矩阵+mask布尔矩阵 模型输出:词典向量矩阵 损失函数:词典向量 与 单词对应的one hot向量 求距离,多分类问题,使用交叉熵
|
神经元 模拟人的神经,概念很好 但只能处理非常简单的问题 全连接 多个神经元,计算量大 ,可以处理稍微复杂点的问题,但总体来说,还是只能处理简单的问题 卷积 - 流式计算,解决瞬间计算量大的问题, - 可以提取区域特征,提取特征的能力比全连接强 - 之后全连接沦为分类,提取特征的工作就交给卷积了 - 图像领域火了
RNN
- 卷积处理序列特征效果不太好,于是RNN诞生了
- 当下的结果是历史序列造成的
- NLP取得重大突破
- 但RNN太简单了,前后依赖的单词10左右
- 于是LSTM诞生了,能处理20个
- 但LSTM的门有点多,结构有些复杂,于是GRU诞生了
- GRU结构更加简单,效果不比LSTM差
seq2seq
- 除了分类问题,业务场景得到了极大的扩展,
- 比如翻译,语音转文本,信号处理等
- 但处理序列的长度依然只有20个左右
- 于是注意力诞生了,这是革命性的
- 先上seq2seq+注意力依赖能力得到了极大的提升
- 这种结构叫 seq2seq with attention
- 但网络结构也不会太复杂,层数也不会太多,因为会梯度消失
|
|
提取特征就用attention
之间的发展从全连接开始,到卷积,到RNN,然后开始在这基础上改良
改复杂了,再简单点,又复杂了,又想办法让它简单点
不管是卷积,还是RNN都是提取特征的,但它们提取特征的能力都不如attention
所以到transformer到,来了一次大革新,提取特征用attention了
刚开始attention是提取序列特征的,现在它也能提取区域特征
所以提取特征就用attention了
短接:N多层的神经网络不会导致梯度消失
之前seq2seq有梯度消失的问题
Transformer使用了短接,解决了梯度消失的问题
可以设计很多层网络
缺点 因为短接,也为网络层次数,每次参数改变小 所以需要的训练量非常地大 用于大型项目 |
Transformer与seq2seq本质一样,基本流程一致 - 都有编码器,解码器 提取特征的方式不一样,它使用attention提取特征 |
|
|
数据准备
x为0-9数字,小字字母 y为x的逆序并且大写,x最后一个字母重复两次,逆序后y开头的两个字母相同 - 字母转大写 - 数字取10以内的互补数
数据代码
# import os
# 解决mac系统OMP: Error #15: Initializing libiomp5.dylib, but found libomp.dylib already initialized.
# os.environ['KMP_DUPLICATE_LIB_OK']='True'
import random
import numpy as np
import torch
# 定义字典,数据是0-9数字+小写字母,以及 三个标记
str_x = '<PAD>,<SOS>,<EOS>,0,1,2,3,4,5,6,7,8,9,q,w,e,r,t,y,u,i,o,p,a,s,d,f,g,h,j,k,l,z,x,c,v,b,n,m'
dict_x = {word: i for i, word in enumerate(str_x.split(','))}
print(dict_x["<PAD>"]) #0
#所有的key
dict_xr = [k for k, v in dict_x.items()]
# 标签是0-9数字+大写字母
dict_y = {k.upper(): v for k, v in dict_x.items()}
dict_yr = [k for k, v in dict_y.items()]
def get_data():
"""获取一对x,y
"""
# 单词集合,没有标记
words = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'q', 'w', 'e', 'r',
't', 'y', 'u', 'i', 'o', 'p', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k',
'l', 'z', 'x', 'c', 'v', 'b', 'n', 'm'
]
# 每个词被选中的概率
p = np.array([
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26
])
# 转概率,所有单词的概率之和为1
p = p / p.sum()
# 随机选n个词
# Return random integer in range [a, b], including both end points.
n = random.randint(30, 48)
x = np.random.choice(words, size=n, replace=True, p=p)
# 采样的结果就是x
x = x.tolist()
# y是对x的变换得到的
# 字母大写,数字取10以内的互补数
def f(i):
i = i.upper()
if not i.isdigit():
return i
i = 9 - int(i)
return str(i)
y = [f(i) for i in x]
# 每个标签结尾的字母重复2次,增加任务难度
y = y + [y[-1]]
# 逆序
y = y[::-1]
# 加上首尾符号
x = ['<SOS>'] + x + ['<EOS>']
y = ['<SOS>'] + y + ['<EOS>']
# 补pad到固定长度
# 48+2,序列最大长度为50,不足50的补到50
# y由于重复了一个字母,最大长度为51,不足51的补到51
x = x + ['<PAD>'] * 50
y = y + ['<PAD>'] * 51
x = x[:50]
y = y[:51]
# 单词序列转 索引列表
x = [dict_x[i] for i in x]
y = [dict_y[i] for i in y]
# 转tensor
x = torch.LongTensor(x)
y = torch.LongTensor(y)
return x, y
# 定义数据集
class Dataset(torch.utils.data.Dataset):
def __init__(self):
super(Dataset, self).__init__()
def __len__(self):
return 100000
def __getitem__(self, i):
return get_data()
# 数据加载器
loader = torch.utils.data.DataLoader(dataset=Dataset(),
batch_size=8,
drop_last=True,
shuffle=True,
collate_fn=None)
if __name__ == '__main__':
for (X,y) in loader:
print(X.shape,y.shape) #torch.Size([8, 50]) torch.Size([8, 51])
print(X[0])
"""
tensor([ 1, 38, 38, 30, 32, 30, 32, 20, 27, 35, 38, 36, 23, 25, 15, 28, 8, 37,
25, 19, 36, 33, 11, 36, 14, 33, 36, 34, 37, 10, 30, 11, 29, 38, 6, 23,
29, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
"""
print(y[0])
"""
tensor([ 1, 29, 29, 23, 9, 38, 29, 4, 30, 5, 37, 34, 36, 33, 14, 36, 4, 33,
36, 19, 25, 37, 7, 28, 15, 25, 23, 36, 38, 35, 27, 20, 32, 30, 32, 30,
38, 38, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
"""
break
数据处理阶段通过补0形成矩阵数据
也可以说成数据对齐,本质还是将批量的数据矩阵化, 要矩阵化,就先得向量化,即先分析单个样本对应的向量是什么 单个样本是有对应关系的两个序列:seq1,seq2 或者叫编码序列,解码序列 seq1:长度为[30,48]范围内的一个随机, 然后首端添加一个开始标记,尾端添加一个结束标记,最大长度50 random.randint(30, 48) seq2比seq1多一个字符 矩阵处理要求其内向量维度相等 为了将不定的序列的长度统一,以最长序列为基准,不足的补0
数据入模前再从矩阵中取真实长度数据
数据是按批次处理的,需要矩阵化,就需要每个向量维数相等 入模时,最好还是原数据多长就多长,把补的部分去掉; 若没去掉模型也能通过学习,判断所补内容无效,只是迭代次数需要多一些
自动化的 批次加载
# 数据加载器
loader = torch.utils.data.DataLoader(dataset=Dataset(),
batch_size=8,
drop_last=True,
shuffle=True,
collate_fn=None)
for X,y in loader:
print(X.shape,y.shape)
print(X[0])
print(y[0])
break
sys.exit(0)
数字编码,补齐,成对,成批次
$ python main_all.py
torch.Size([8, 50]) torch.Size([8, 51])
tensor([ 0, 36, 30, 7, 26, 27, 38, 31, 26, 32, 27, 31, 31, 13, 9, 28, 31, 37,
32, 29, 27, 34, 38, 33, 23, 36, 30, 26, 31, 31, 35, 33, 18, 7, 19, 19,
33, 28, 30, 25, 36, 1, 2, 2, 2, 2, 2, 2, 2, 2])
tensor([ 0, 36, 36, 25, 30, 28, 33, 19, 19, 8, 18, 33, 35, 31, 31, 26, 30, 36,
23, 33, 38, 34, 27, 29, 32, 37, 31, 28, 6, 13, 31, 31, 27, 32, 26, 31,
38, 27, 26, 8, 30, 36, 1, 2, 2, 2, 2, 2, 2, 2, 2])
按概率取词
# 每个词被选中的概率
p = np.array([
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26
])
# 转概率,所有单词的概率之和为1
p = p / p.sum()
# 随机选n个词
# Return random integer in range [a, b], including both end points.
n = random.randint(30, 48)
x = np.random.choice(words, size=n, replace=True, p=p)
# 采样的结果就是x
x = x.tolist()
开始与结束标记
# 加上首尾符号 x = ['SOS'] + x + ['EOS'] y = ['SOS'] + y + ['EOS']
补齐
# 补pad到固定长度 # 48+2,序列最大长度为50,不足50的补到50 # y由于重复了一个字母,最大长度为51,不足51的补到51 x = x + ['PAD'] * 50 y = y + ['PAD'] * 51 x = x[:50] y = y[:51]
50与51不等长处理
硬性要求x与y向量的维数必须相等,所有数据都是在统一的量纲上计算, 所以,这里要么把50提升到51,要么把51降为50 x序列的最大长度是48,但最后一字母重复了两次,y的最大序列达到49 y再加上开始和结束标记,最大长度达到51, 如果取前50个,那么部分序列将会丢失最后一个结束标记 评估一下丢失的比例,random.randint(30, 48),注意这是python的包 取值范围为[30,48],取值为48的概率为1/19, 通常脏数据比例若低于1/100其影响则忽略,这里将近1/20,比1/100大的多, 但丢失的仅是最后一个结束标记,影响好像又没那么大 最后一个结束标记影响了什么?正常情况是: 上下文+最后一个单词 -- 结束标记 上下文+结束标记 -- 结束标记 ...最多49次循环 但若没有了结束标记,并且刚好遇到最后一个不是结束标记而是单词的情况,则是如下情况: 上下文+倒数第二个单词 -- 最后一个单词 这就是第49次循环,然后序列生成结束 在最多49次循环的代码逻辑下,序列最后位置,是单词还是结束标记,一点都不影响序列生成的正确性 到此,将解码序列的51降为50,不影响模型的准确性
这个环节非常关键,重要,因此单独设一个专栏讲述,请参考多头注意力
原来提取特征的是卷积,RNN,现在用了注意力 整个神经网络的设计中,除了提取特征,还有其他的一些组件:比如, 归一化:加速收敛,注意力计算涉及多层全连接网络,数据分布偏移是必然的, 激活函数:增加非线性能力,注意力的计算全是线性变换,增加一些非线性因素是必要的 dropout:健壮性 当然了,主体还是那个线性变换 -- 全连接分类 加上组件就形成下面的全连接层:
# 全连接输出层
class FullyConnectedOutput(torch.nn.Module):
"""
使用短接进行微调
- 特征数先变大再变小
- 并且是变回原来的大小,这样才能使用短接相加
"""
def __init__(self,features=32):
super().__init__()
self.fc = torch.nn.Sequential(
torch.nn.Linear(in_features=features, out_features=features*4),
torch.nn.ReLU(),
torch.nn.Linear(in_features=features*4, out_features=features),
torch.nn.Dropout(p=0.1)
)
self.norm = torch.nn.LayerNorm(normalized_shape=features,
elementwise_affine=True)
def forward(self, x):
# 保留下原始的x,后面要做短接用
clone_x = x.clone()
# 单词维度归一化
x = self.norm(x)
# 线性全连接运算
# [b, seq_len, feature_nums] -> [b, seq_len, feature_nums]
out = self.fc(x)
# 做短接
out = clone_x + out
return out
|
编码器层:一次多头注意力提取特征+一层全连接变换
# 编码器层
class EncoderLayer(nn.Module):
def __init__(self):
super().__init__()
# 多头注意力
self.mh = MultiHead()
# 全连接输出
self.fc = FullyConnectedOutput()
def forward(self, x, mask):
# 计算自注意力,维度不变
# [b, 50, 32] -> [b, 50, 32]
score = self.mh(x, x, x, mask)
# 全连接输出,维度不变
# [b, 50, 32] -> [b, 50, 32]
out = self.fc(score)
return out
|
|
编码器:由三层编码器层组成,相同的逻辑重复三次
class Encoder(torch.nn.Module):
def __init__(self):
super().__init__()
self.layer_1 = EncoderLayer()
self.layer_2 = EncoderLayer()
self.layer_3 = EncoderLayer()
def forward(self, x, mask):
x = self.layer_1(x, mask)
x = self.layer_2(x, mask)
x = self.layer_3(x, mask)
return x
|
|
|
|
|
|
|
|
分词 -- 索引矩阵 -- 向量化 -- 输入模型
import jieba
import torch
from torch import nn
from tpf.vec3 import mask_pad
from tpf.vec3 import mask_tril
from tpf.layer import DecoderLayer
索引矩阵生成
seq1= "你现阶段的目标是?"
seq2= "不上班还有钱花"
sentence1 = jieba.lcut(seq1)
word2id = {}
word2id["PAD"] = 0 # 补长度 是为了批次处理
len_add = len(word2id)
word2id.update({word:(index+len_add) for index,word in enumerate(set(sentence1))})
sentence2 = jieba.lcut(seq2)
len_add = len(word2id)
word2id.update({word:(index+len_add) for index,word in enumerate(set(sentence2))})
sentence = []
sentence.append([ word2id[word] for word in sentence1])
sentence.append([ word2id[word] for word in sentence2])
#矩阵对齐
max_seq_len = 6
index_mat = []
for word_index in sentence:#取每个句子的索引列表进行对齐
word_index = word_index+[word2id["PAD"]]*max_seq_len
word_index = word_index[:6]
index_mat.append(word_index)
从索引矩阵中求得补码矩阵:01布尔矩阵,0代表对应位置为单词索引,1为补码
index_mat
[[6, 3, 2, 5, 4, 1], [9, 8, 7, 10, 0, 0]]
#索引编码并转换shape为[batch_size,seq_len]
x = torch.tensor(index_mat[0]).unsqueeze(dim=0) #B.shape = [1, 6]
y = torch.tensor(index_mat[1]).unsqueeze(dim=0) #B.shape = [1, 6]
#对索引矩阵进行补码
mask_pad_x = mask_pad(x,padding_index=0) #mask.shape = [1, 1, 6, 6]
mask_tril_y = mask_tril(y,padding_index=0)
索引向量化
#索引向量化
embed = nn.Embedding(num_embeddings=11,embedding_dim=32, padding_idx=0)
x = embed(x) #x.shape = torch.Size([1, 6, 32])
y = embed(y)
|
解码层计算逻辑:
解码层先对自己求一次注意力y
再求y相对解码数据x的注意力
全连接+激活函数 -- 最终输出y
这个y的元素维度并非标签的特征数,因为为解码层将作用主模型众多可循环层的中的一层
它的特征维度,与整个数据流中元素的特征维数是一致的,
这是一种设计方式
- 每层接口设计特征的输入与输出特征维数不变,其内部扩大再缩小
- 当然也可以一直扩大,最后一步再收缩,比如resnet,这只是“一种”设计方式
元素特征维度到标签元素特征维度的映射由主模型最后一步实现
from tpf.att import MultiHead
from tpf.layer import FullyConnectedOutput
# 解码器层
class DecoderLayer(torch.nn.Module):
"""求y相对x的注意力,带补码
- x.shape默认为[B,C,L]
- x.shape默认为[B,C,L]
"""
def __init__(self):
super().__init__()
# 自注意力提取输入的特征
self.mh1 = MultiHead()
# 融合自己的输入和encoder的输出
self.mh2 = MultiHead()
# 全连接输出
self.fc = FullyConnectedOutput()
def forward(self, x, y, mask_pad_x, mask_tril_y):
# 先计算y的自注意力,维度不变
# [b, 50, 32] -> [b, 50, 32]
y = self.mh1(y, y, y, mask_tril_y)
# 结合x和y的注意力计算,维度不变
# [b, 50, 32],[b, 50, 32] -> [b, 50, 32]
y = self.mh2(y, x, x, mask_pad_x)
# 全连接输出,维度不变
# [b, 50, 32] -> [b, 50, 32]
y = self.fc(y)
return y
decoder_layer = DecoderLayer()
y = decoder_layer(x,y,mask_pad_x,mask_tril_y)
y.shape
torch.Size([1, 6, 32])
|
|
多个解码层封装为解码器
class Decoder(torch.nn.Module):
"""解码器
params
- x:[B,L,C]
- y:[B,L,C]
- mask_pad_x:[B,1,L,L],01布尔矩阵
- mask_tril_y:[B,1,L,L],01布尔矩阵
"""
def __init__(self):
super().__init__()
self.layer_1 = DecoderLayer()
self.layer_2 = DecoderLayer()
self.layer_3 = DecoderLayer()
def forward(self, x, y, mask_pad_x, mask_tril_y):
y = self.layer_1(x, y, mask_pad_x, mask_tril_y)
y = self.layer_2(x, y, mask_pad_x, mask_tril_y)
y = self.layer_3(x, y, mask_pad_x, mask_tril_y)
return y
decoder = Decoder() y = decoder(x,y,mask_pad_x,mask_tril_y) y.shape torch.Size([1, 6, 32]) |
x输入的是[b,seq_len] -- [b,1,1,50] -- [b,1,50,50] - 将一句话转换为一个近似2维表的矩阵 -- [b,1,50,50] - row是单词,col也是单词,数据则是单词之间的相似度(数字) - 最终这个矩阵化为一个向量,上下文向量,每个句子对应一个向量 - 这个向量参与 解码序列的计算 y输出的也是[b,seq_len] -- [b,1,1,50] -- [b,1,50,50] - 这个求自注意力的过程与x一样 - 不同的是y补码使用的是下三角矩阵 - 同样是每句话下有[50,50]这个矩阵,但因为补码的不同 - 第i句话只有前i个单词是真实的,后n-i个是补码 比如,你好吗,使用下三角矩阵求自注意力 - 你 -- 你 - 好 -- 你好 - 吗 -- 你好吗 - 然后这个[3,3]矩阵化为一个向量,代表y-你好吗 到这里,就变换一个x相关的向量对应一个y相关的向量 - 于是输入x就得到了y,一个序列得到一个序列 - 但这个下三角的计算是复合的 - 正常的应该是 - seq1+y1 -- y2 - seq1+y1y2 -- y3 - seq1+y1y2y3 -- y4 - 但结果表明下三角的计算是有用的,其结果与这种分步计算相同 |
|
|
编码器:x对自己求几遍/层注意力,然后将数据x传给解码器 解码器:y先自求一遍注意力,再对x求注意力,这个过程来上几遍后,映射到标签维度 主模型:x -- 编码器 -- 解码器 -- 全连接映射到标签
整个过程虽然有很多层,但主体计算是注意力 注意力的计算,有两分支 1. 数据进入模型先clone一份,保存一份原数据,相当于保存了一个镜像 2. 数据开始真正注意力的计算,拆分,交换维度,矩阵乘法求得分,做softmax,再矩阵乘法得上下文向量.... 最后一步输出的是:步骤1+步骤2, 两个分支相加,即短接,解决了多层网络梯度消失的问题, 每次注意力计算只是改变一点点 每次模型计算也只是改变一点点 这是tranformer核心(注意力+序列等整个计算流程)中的核心(短接,真正解决了序列长度依赖的问题)
默认数据流转的维度为embedding_dim,最后一层全连接转到字典个数
import torch
from torch import nn
from tpf.nlp.mask import mask_pad
from tpf.nlp.mask import mask_tril
from tpf.mapping import Decoder,Encoder
from tpf.nlp.pos import PositionEmbedding
# 主模型
class Transformer(torch.nn.Module):
"""Transformer主流程
模型定义参数
- in_features:输入元素向量特征数,比如单词的embedding_dim
- out_features:输出向量特征数,比如标签的维度,词典单词个数
模型对象参数
- x:[B,seq_len],批次索引矩阵,进入模型后先计算补码后编码
- y:[B,seq_len],批次索引矩阵,进入模型后先计算补码后编码
返回
- 标签向量,特征数为单词个数
"""
def __init__(self,seq_len=50,in_features=32,out_features=39):
super().__init__()
self.seq_len = seq_len
# 位置编码和词嵌入层
# out_features对应标签,单词个数/元素个数
self.embed_x = PositionEmbedding(seq_len=seq_len,num_embeddings=out_features,embedding_dim=in_features)
self.embed_y = PositionEmbedding(seq_len=seq_len,num_embeddings=out_features,embedding_dim=in_features)
#单词维度/特征数,在神经网络的流转中保持了不变
self.encoder = Encoder(features=in_features)
self.decoder = Decoder(features=in_features)
#单词embedding_dim 映射到 标签维度
self.fc_out = torch.nn.Linear(in_features, out_features)
def forward(self, x, y):
"""x,y为索引矩阵
# x = [8, 50]
# y = [8, 51]
"""
# [b, 1, 50, 50]
mask_pad_x = mask_pad(x)
mask_tril_y = mask_tril(y)
# 编码,添加位置信息
# x = [b, 50] -> [b, 50, 32]
# y = [b, 50] -> [b, 50, 32]
x, y = self.embed_x(x), self.embed_y(y)
# 编码层计算
# x: [b, 50, 32] -> [b, 50, 32]
# mask_pad_x:[b,1,50,50]
x = self.encoder(x, mask_pad_x)
# 解码层计算
# [b, 50, 32],[b, 50, 32] -> [b, 50, 32]
y = self.decoder(x, y, mask_pad_x, mask_tril_y)
# 全连接输出,维度改变
# [b, 50, 32] -> [b, 50, 39]
y = self.fc_out(y)
return y
将整个编码矩阵,解码矩阵输入主模型,以训练模型参数
最后一步全连接将元素特征维数映射到词典个数
编码序列x 与解码序列的y 的shape皆为[B,seq_len,hidden_size/embedding_dim] 输入是[B,seq_len],在模型内编码, 即将[B,seq_len,1]扩展到[B,seq_len,embedding_dim] 这里就规定所有单词变换的维度皆为embedding_dim,简化一下 模型输出[B,seq_len,embedding_dim],做一个全连接映射到[B,seq_len,dict_size] 求最大值的索引,转化为[B,seq_len]索引矩阵,这就与最初的输入,也与业务对上了 整个过程皆以批次,以矩阵的形式进行计算 但解码序列是一个生成模型,是前N个元素生成第N+1个元素
要兼顾两个点
1. 计算过程中矩阵shape必须一致 2. 第i次计算只涉及前i个单词/元素,因为后面的单词还没生成,这是预测,尚未知
解决办法:以PAD补全向量使得矩阵shape一致,设置PAD的值为0表示该元素无效
第1次计算,输入y0[SOS,PAD,PAD,...,PAD], 输出y1[SOS,单词1,PAD,PAD,...,PAD] 第2次计算,输入y1[SOS,单词1,PAD,...,PAD],输出y2[SOS,单词1,单词2,PAD,...,PAD] ... 第n次计算,输入yn-1[SOS,单词1,单词2,...,PAD],输出yn[SOS,单词1,单词2,...,单词n] 这里使用序列长度控制最大循环次数
预测伪算法
输入:数据X(索引序列向量)及数据字典dict 输出:数据y,一个索引序列向量 计算: - 从x中提取特征 - 建立一个seq_len-1次 循环,每次生成一个y元素 - 第一个y元素为sos的索引y0,初始化向量y=[1,0,0,...,0]后面全是PAD-0 - y1 = model(x,y0,mask_x,mask_y),y=[1,index_y1,0,...,0] - y2 = model(x,y1,mask_x,mask_y),y=[1,index_y1,index_y2,...,0] - ... 每次循环填充向量y一个元素,y向量后面的PAD-0逐步被替换为单词索引 - 直到结束
预测函数实现
# 预测函数
def predict(self, x, dict_y):
"""根据序列x预测序列y
- y的第1个单词为SOS标记
"""
# x = [1, 50]
self.eval()
# [1, 1, 50, 50]
mask_pad_x = mask_pad(x)
# x编码,添加位置信息
# [1, 50] -> [1, 50, 32]
x = self.embed_x(x)
# 编码层计算,维度不变
# [1, 50, 32] -> [1, 50, 32]
x = self.encoder(x, mask_pad_x)
#序列长度为seq_len,第一个单词为SOS标记,余下seq_len-1个需要预测
ydeal_count = self.seq_len-1
# 初始化输出,这个是固定值
# [1, 50]
# [[1,0,0,0...]]
# 每次输入的shape是[1, 50]但第i次处理只有前i个词有效,其余词为PAD
target = [dict_y['<SOS>']] + [dict_y['<PAD>']] * ydeal_count
target = torch.LongTensor(target).unsqueeze(0)
# 遍历生成第1个词到第49个词
for i in range(ydeal_count):
# [1, 50]
y = target
# [1, 1, 50, 50]
mask_tril_y = mask_tril(y)
# y编码,添加位置信息
# [1, 50] -> [1, 50, 32]
y = self.embed_y(y)
# 解码层计算,维度不变
# [1, 50, 32],[1, 50, 32] -> [1, 50, 32]
# 虽然输入的是y向量,多个特征,但会结合mask,只计算mask位置上的单词,
# 因此每次参与计算的,只有i个
y = self.decoder(x, y, mask_pad_x, mask_tril_y)
# 全连接输出,39分类
# [1, 50, 32] -> [1, 50, 39]
# 每次输出的只有一个单词
# y向量第1个维度是批次,每2个维度是seq_len,第3维是单词维度
# 只要序列维度中第i个位置的输出,所以这个out还不是最终结果
out = self.fc_out(y)
# 取出当前词的输出
# [1, 50, 39] -> [1, 39]
out = out[:, i, :]
# 取出分类结果
# [1, 39] -> [1]
out = out.argmax(dim=1).detach()
# 以当前词预测下一个词,填到结果中
target[:, i + 1] = out
return target
|
模型,损失函数,优化器 # 模型 model = Transformer() # 损失函数 loss_func = torch.nn.CrossEntropyLoss() # 优化器,optim并不像其他方法那样在torch.nn下,而是直接在torch下 optim = torch.optim.Adam(model.parameters(), lr=2e-3) 模型输入:[batch_size,seq_len]的索引矩阵, 模型输出:[batch_size,seq_len,dict_count],特征数为字典数的单词向量 组成的矩阵 标签矩阵:[batch_size,seq_len]的索引矩阵 损失函数使用交叉熵,计算模型输出与标签矩阵之间偏差, 优化器迫使 特征数为字典数的单词向量 向标签索引对应的 特征数为字典数的One Hot向量 逼近 训练
for epoch in range(1):
for i, (x, y) in enumerate(loader):
# x = [8, 50]
# y = [8, 51]
# 在训练时,是拿y的每一个字符输入,预测下一个字符,所以不需要最后一个字
# 主要还是因为y是51,x是50,二者要保持一致
# [8, 50, 39]
pred = model(x, y[:, :-1])
# [8, 50, 39] -> [400, 39]
# 多少个单词,展平到单词维度,准备按批次计算偏差
pred = pred.reshape(-1, 39)
# [8, 51] -> [400]
# y[:,0]为SOS标记的索引
y = y[:, 1:].reshape(-1)
# 忽略pad,忽略一个批次中所有的PAD
select = y != dict_y['<PAD>']
pred = pred[select] #选出pred中所有单词位置的向量
y = y[select] #选出y中所有单词的索引,形成一个新的全是单词索引的向量
#pred[-1,dict_count],y单词索引向量
#比如,y[0]=2,one hot转标签向量为[0,0,1],期望模型输出的向量为[0,0.2,0.8],最好是[0,0.1,0.9],
#损失减少的方向就是index=2位置上的数据尽量接近1,其他位置尽量逼近0
loss = loss_func(pred, y)
optim.zero_grad()
loss.backward()
optim.step()
if i % 200 == 0:
# [select, 39] -> [select]
pred = pred.argmax(1)
correct = (pred == y).sum().item()
accuracy = correct / len(pred)
lr = optim.param_groups[0]['lr']
print(epoch, i, lr, loss.item(), accuracy)
|
``` pred = model(x, y[:, :-1]) # [8, 50, 39] -> [400, 39] # 多少个单词,展平到单词维度,准备按批次计算偏差 pred = pred.reshape(-1, 39) # [8, 51] -> [400] # y[:,0]为SOS标记的索引 y = y[:, 1:].reshape(-1) loss = loss_func(pred, y) ``` - 模型输出的序列与真实序列之间错了一位 - 针对完成的序列来说,模型输入的是前n-1个单词,损失函数只输出了单词n 序列1 :你好 序列2 :你好 序列2的完整序列为:SOS,你,好,EOS 模型输入:SOS,你,好 真实标签:你,好,EOS 损失函数逼迫SOS的输出 与 你 接近,进而做到预测下一步未知单词的目的 |
|
|
|
|
|
|
|
|
|
|
|
|
# 测试
for i, (x, y) in enumerate(loader):
break
for i in range(8):
print(i)
print(''.join([dict_xr[i] for i in x[i].tolist()]))
print(''.join([dict_yr[i] for i in y[i].tolist()]))
print(''.join([dict_yr[i] for i in predict(x[i].unsqueeze(0))[0].tolist()]))