自然语言处理入门
(一)从序列到序列的翻译任务
'''
https://github.com/bentrevett/pytorch-seq2seq/blob/master/1%20-%20Sequence%20to%20Sequence%20Learning%20with%20Neural%20Networks.ipynb
'''
#coding=gbk
import torch
import torch.nn as nn
import torch.optim as optim
from torchtext.datasets import TranslationDataset,Multi30k
from torchtext.data import Field,BucketIterator
import spacy
import random
import math
import os
import time
'''
实现从序列到序列的文本转换
256+512=768 concatenate
'''
SEED=1
random.seed(SEED)
torch.manual_seed(SEED)
torch.backends.cudnn.deterministic=True
spacy_de=spacy.load('de')#德语分词器
spacy_en=spacy.load('en')#英语分词器
'''
加载分词器,将数据集中的每个训练样本(一个字符串)分词成为一个个单词
'''
def tokenize_de(text):#将输入的德语字符串(一句话)分词成列表,列表中的每个元素是一个单词/字符串/token
'''
:param text: 所需要翻译的字符串,
:return: 将字符串分词成一个列表,列表中的每个元素是一个token/字符串,表示一个单词
'''
return [tok.text for tok in spacy_de.tokenizer(text)][::-1]
#将对原始字符串分词完成的字符串列表进行逆序,论文中说这样使优化问题更简单
def tokenize_en(text):#将输入的英语字符串(一句话)分词成列表
return [tok.text for tok in spacy_de.tokenizer(text)]
'''
实例化函数 Field,实现功能:
将数据集中的每个样本(一句话,字符串)切割成一个个单词(构成token列表)
再在每个列表头部加上SOS,尾部加上EOS,然后进行数值编码,即将字符串列表中
的每个单词编码成数值形式,这样每个字符串列表/token列表就会编码成torch.tensor
从字符串到数值的编码方式与词汇表有关
'''
SRC=Field(tokenize=tokenize_de,init_token='',eos_token='',lower=True)
TRG=Field(tokenize=tokenize_en,init_token='',eos_token='',lower=True)
train_data,valid_data,test_data=Multi30k.splits(exts=('.de','.en'),fields=(SRC,TRG))
print(f'Number of training examples:{len(train_data.examples)}')
print(f'Number of validation examples:{len(valid_data.examples)}')
print(f'Number of test examples:{len(test_data.examples)}')
print(vars(train_data.examples[0]))
'''
分别为训练数据中的训练样本(德语)以及对应的标签(英语)字符串列表构建词汇表
例如:在训练数据集中的训练样本(英语字符串),每个训练样本对应一个字符串/token列表
将所有列表中最少出现次数为2的单词取出,放到为训练样本创建的词汇表中
'''
SRC.build_vocab(train_data,min_freq=2)
TRG.build_vocab(train_data,min_freq=2)
print(f'unique tokens in source(de) vovalbulary:{len(SRC.vocab)}')
print(f'unique tokens in target(en) vovalbulary:{len(TRG.vocab)}')
device=torch.device('cuda' if torch.cuda.is_available() else 'cpu')
BATCH_SIZE=128
train_iterator,valid_iterator,test_iterator=BucketIterator.splits(
(train_data,valid_data,test_data),batch_size=BATCH_SIZE,device=device
)
class Encoder(nn.Module):
def __init__(self,input_dim,emb_dim,hid_dim,n_layers,dropout):
'''
:param input_dim: 表示训练样本词汇表的长度(即德语的词汇表)
:param emb_dim:
将训练样本中每个字符串分词成一个个单词之后,会编码成one-hot向量,
再将input_dim维度的向量embedding到某个dense space(因为one hot太稀疏)
:param hid_dim:LSTM的hidden vector和cell vector向量维度
:param n_layers:LSTM层数
:param dropout:多层LSTM时,应用在两层LSTM之间的dropout
encoder使用的是两层的LSTM网络,LSTM模型相对于RNN的改进之处在于
RNN每一个时间节点的输出 h(t)=RNN(x(t),h(t-1))
'''
super().__init__()
self.input_dim=input_dim
self.emb_dim=emb_dim
self.hid_dim=hid_dim
self.n_layers=n_layers
self.dropout=dropout
self.embedding=nn.Embedding(self.input_dim,self.emb_dim)
'''
nn.Embedding 定义模型所需要的参数
num_embeddings (int) – size of the dictionary of embeddings 表示词汇表中的单词个数
embedding_dim (int) – the size of each embedding vector 表示embedding后的需要将每个单词输出的特征向量维度
nn.Embedding 中也会引入 learnable parameters requires_grad=True
其中的可学习参数 shape [num_embeddings,embedding_dim] 其中每一行的向量数值就等于对应索引值在词汇表中的单词的embedding vector编码
则 nn.Embedding 将会生成 num_embeddings个tensor,每个tensor的维度是3
就是说对于词汇表中的每个单词,将会生成3维的特征向量
(如果不指定torch.manual_seed(SEED)中的SEED为固定值,则每次nn.Embedding对于相同的索引值产生的embedding vector都是
随机的,并不相同,这将带来的问题是:decoder每个时间节点处的对于相同的词汇表编码出的embedding vector都不一样
故而需要设定随机种子数相同,以保证每次nn.Embedding对于相同长度的词汇表相同的索引值产生的embedding vector相同)
则如果下一次我给出一个在词汇表中的索引量([src sent length,batch_size] source tensor矩阵中的一个数值)
则将会对于当前索引值输出1个 embedding_dim 维度的embedding vector
则embedding之后的输出tensor变成 [src sent length,batch_size,embedding_dim]
'''
self.rnn=nn.LSTM(self.emb_dim,self.hid_dim,self.n_layers,dropout=self.dropout)
'''
nn.LSTM
input_size 所输入的时序序列特征向量维度,即当前时间的embedding vector维度,在这里为embedding_dim
hidden_size LSTM隐藏层特征维度
num_layers LSTM层数
dropout 在使用多层LSTM时,第L+1层LSTM输入的时间序列是上一层LSTM输出的所有隐藏层时间序列
(只有hidden state,没有cell state),hidden state每个维度的特征向量进行一定概率地随机
dropout之后,再作为下一层LSTM的输入
注意:第0层LSTM的输入值(也就是最原始的输入时间序列embedding后的特征向量)也需要进行dropout
再输入到LSTM中
而最后一层LSTM的输出隐藏层状态序列以及最后一层最后一个时间节点输出的hidden state和cell state
也就是编码器输出的context vector,则不需要进行dropout操作
(注意encoder输出的context vector包含了多层LSTM每一层输出的hidden state 和cell state)
'''
self.dropout=nn.Dropout(dropout)
'''
以dropout的概率将tensor中的数值变成0,dropout层中不包含任何参数,如果dropout=0.9
则以0.9的概率将tensor中的数值变成0
'''
def forward(self, src):
'''
:param src: torch.tensor shape[src sent length,batch_size]
:return:
输入到LSTM中的是 [src sent length,batch_size] 的2-dimension tensor,其中的每个数值range (0,len(src.vocal))
即表示batch size中当前输入字符串的当前单词,在词汇表中的序号(注意并不是将其编码成one-hot tensor,而是直接传入索引值)
输入到encoder之后的步骤如下:
(1)先进行embedding操作,将之前的序号(当前token在原序列词汇表中的序号)通过nn.Embedding对于每个索引值编码出来的
embedding vector填充到source tensor中,得到 shape [src sent length,batch_size,embedding dim]
(2)对于embedding之后的word vector进行dropout操作
(3)将embedding和dropout之后的输入特征tensor送入到LSTM中
'''
embedded=self.dropout(self.emb(src))
# embedded=[src sent length,batch_size,emb_dim]
outputs,(hidden,cells)=self.rnn(embedded)
'''
nn.LSTM的输入参数有两组
Inputs: input, (h_0, c_0)
input shape [sequence_length,batch_size,input_size]
sequence_length 输入序列的时序长度
input_size 输入序列每个时间节点特征向量的维度 embedding dim
h_0 第0时刻的hidden state dimension=LSTM的hidden_size
c_0 第0时刻的cell state dimension=LSTM的hidden_size
对于encoder的LSTM而言,sequence_length等于训练数据集中每句话的最长单词数
h_0 c_0 并没有传入的初始参数,而是被初始化为全0 vector
对于decoder的LSTM而言,sequence_length等于1
h_0 (num_layers * num_directions, batch, hidden_size)
c_0 (num_layers * num_directions, batch, hidden_size)
num_Layers表示的是decoder LSTM的层数,num_directions表示方向(可能是bidirectional LSTM)
用于decoder LSTM每一层的解码输入
nn.LSTM的输出
output, (h_n, c_n)
output shape [sequence_length,batch_size,hidden_size]
表示编码器/解码器LSTM最后一层每个时间节点输出的hidden state vector
(h_n, c_n) 二者shape都为(num_layers * num_directions, batch, hidden_size)
表示编码器/解码器LSTM每一层最后一个时间节点输出的hidden state vector和cellstate vector
在LSTM的图示中,横向前行表示时间节点向前,竖直方向上前行表示LSTM层数不断加深
'''
'''
#outputs = [sent lenght, batch_size,hid dim]
#hidden = [n layers * n directions, batch size, hid dim]
#cell = [n layers * n directions, batch size, hid dim]
如果LSTM是多层的,则将每一层的隐含层特征和cell特征都保存下来作为context information
故而在没有引入attention时,只需要将encoder的context vector即编码器每一层最后一个时间节点
的hidden state和cell state返回(如果使用了attention机制,则需要将编码器最后一层的每个时间节点的hidden state输出)
'''
return hidden,cells#编码器最终输出的是context vectors
class Decoder(nn.Module):
def __init__(self,output_dim,emb_dim,hid_dim,n_layers,dropout):
'''
decoder的隐藏层维度必须要和encoder的隐藏层维度相等
decoder的embedding维度和encoder的embedding维度可以不相等
对于decoder和encoder分别而言,其hidden dimension和cell dimension都相等
编码器和解码器的LSTM层数必须相等(为了保证hidden和cell context vector正常使用)
:param output_dim:等于解码器词汇表单词个数
:param emb_dim:相当于对解码器输出的预测单词进行embedding编码
:param hid_dim:解码器LSTM隐藏层维度
:param n_layers:解码器LSTM层数
:param dropout:
'''
self.emb_dim=emb_dim
self.hid_dim=hid_dim
self.output_dim=output_dim
self.n_layers=n_layers
self.dropout=dropout
self.embedding=nn.Embedding(output_dim,emb_dim)
self.rnn=nn.LSTM(emb_dim,hid_dim,n_layers,dropout=dropout)
self.out=nn.Linear(hid_dim,output_dim)#线性预测层的输出维度与解码器词汇表中单词数相等
self.dropout=nn.Dropout(dropout)
def forward(self, input,hidden,cell):
'''
:param input: [batch_size],是batch size个 sos
:param hidden: 编码器输出的向量 shape [n_layers*n_directions,batch_size,hid_dim]
:param cell: 编码器输出的向量 shape [n_layers*n_directions,batch_size,hid_dim]
:return:
'''
input=input.unsqueeze(0)#shape[1,batch_size]
embedded=self.dropout(self.embedding(input))#由于设定了随机种子,可以保证解码器每个时间序列相同单词embedding的是相同的vector
# embedded shape [1,batch_size,embed_dim]
output,(hidden,cell)=self.rnn(embedded,(hidden,cell))
# output = [sent len,batch_size,hidden_dim*n_directions] 表示最后一层LSTM输出的隐藏层状态
# hidden = [n_layers*n_directions,batch_size,hidden_dim] 表示解码器每一层LSTM最后一个节点输出的特征向量
# cell = [n_layers*n_directions,batch_size,hidden_dim] 表示解码器每一层LSTM最后一个节点输出的特征向量
#输出序列为 [1,batch_size] sent len=1 n_directions=1
# output = [1,batch_size,hidden_dim]
# hidden = [n_layers,batch_size,hidden_dim]
# cell = [n_layers,batch_size,hidden_dim]
predictions=self.out(output.squeeze(0))#解码器是一层层输出的,因为需要保留每个时间节点处的prediction vector
#predictions = [batch_size,output_dim]
'''
解码器负责预测一个时间节点的输出,这是因为每个时刻所需要的input cell依赖于上一时刻LSTM的预测输出
编码器和解码器的重要区别是,
(1)编码器最后一层每个时间节点的hidden state和cell state只用于对下一个
时间节点的LSTM的输入,但是解码器最后一层每个时间节点的hidden state和cell state除了需要作为下一个时间节点
的输入,还需要输入到nn.Linear层中进行单词索引的prediction。
(2)编码器每次是将所有的时间序列都输入,这是因为它知道每个时间节点处的input state是什么,但是解码器每次只能
输入一个时间节点,因为下一个时间节点的input state依赖于上一个时间节点的prediction indices(在解码器词汇表中的索引值)
(3)初始hidden state和cell state不同,encoder 的hidden state和cell state都被初始化为0,而
decoder的hidden state和cell state是来自于encoder输出的context vector
(4)encoder hidden state用h表示,decoder hidden state用s表示
他们之间的相同点是:
对于不是最后一层的LSTM层,中间的LSTM层每个时间节点的输出值(指的是hidden state和cell state)都有两部分决定
它的左边:上一时刻的 hidden state和cell state
它的下面:当前时刻的输入 embedding vector(原始的单词根据在词汇表中的索引值经过embedding和dropout之后)
'''
return predictions,hidden,cell
class Seq2Seq(nn.Module):
def __init__(self,encoder,decoder,device):
super.__init__()
self.encoder=encoder
self.decoder=decoder
self.device=device
assert encoder.hid_dim==decoder.hid_dim,'hidden dimension of encoder must equal decoder'
assert encoder.n_layers==decoder.n_layers,'hidden LSTM layers of encoder must equal decoder'
def forward(self,src,trg,teacher_forcing_rate=0.5):
'''
:param src:shape [src sent length,batch size]
:param trg:shape [trg sent length,batch size]
:param teacher_forcing_rate:使用正确单词标签的概率
:return:
'''
batch_size=trg.shape[1]
max_len=trg.shape[0]#target ground truth的最大序列长度
trg_vocab_size=self.decoder.output_dim#target词汇表中的单词个数
outputs=torch.zeros(max_len,batch_size,trg_vocab_size)
#记录prediction输出的batch size中每个字符串序列每个元素处的单词在词汇表中的位置
hidden,cell=self.encoder(src)
#编码器输出的context vector
#hidden = [n_layers*n_directions,batch_size,hid_dim]
#cell = [n_layers*n_directions,batch_size,hid_dim]
input=trg[0,:]# shape [batch_size]
'''
在trg中的第0个token是sos,将第0个token作为decoder第0个时刻的输入,以预测出第1个时刻的trg单词
然后将第0个时刻decoder预测的输出值直接给了output的第1行,就是说默认模型对于trg的预测的第0个
单词是sos,故而计算损失的时候是从output[1:,:]计算的,并不计算output[0,:]的损失
并没有让decoder预测输出是sos的过程
'''
for t in range(1,max_len):
output,hidden,cell=self.decoder(input,hidden,cell)
outputs[t,:,:]=output#output shape [batch_size,trg_vocab_size]
teacher_force=random.random()']
criterion=nn.CrossEntropyLoss(ignore_index=pad_idx)
'''
pytorch 中的nn.CrossEntropyLoss
将log,softmax和non-negative likelihood loss整合到了一起
计算分类任务损失函数的标准配置:softmax+cross entropy
'''
def train(model,iterator,optimizer,criterion,clip):
model.train()
'''
将模型调整到train模式下,Batch normalization参数要更新,并且dropout
会随机裁剪
'''
epoch_loss=0
for i,batch in enumerate(iterator):
src=batch.src
trg=batch.trg#shape [max len,batch size]
optimizer.no_grad()
output=model(src,trg)
'''
将trg数值传入到模型中仅仅是因为teaching force的存在,需要为
decoder的RNN提供每个节点处的标签
output shape [max len,batch size,len(trg.voclab)] 经过nn.Linear的输出值,并没有经过任何的激活函数
max len表示target字符串中最长的字符串列表长度
len(trg.voclab)表示target词汇表中的单词个数
'''
output=output[1:,:,:].view(-1,output.shape[-1])#len(trg.voclab)=output.shape[-1]
#这是因为对于解码器的第一个节点 sos 不计算损失
trg=trg[1:,:].view(-1)
'''
trg shape [batch_size*max len]
output shape [batch_size*max len,len(trg.voclab)]
相当于是len(trg.voclab)个类别的分类问题
'''
loss=criterion(output,trg)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(),clip)
optimizer.step()
epoch_loss+=loss.item()
return epoch_loss/len(iterator)
def evaluate(model,iterator,criterion):
model.eval()
epoch_loss=0
with torch.no_grad():
for i,batch in enumerate(iterator):
src=batch.src
trg=batch.trg
output=model(src,trg,0)
#trg [trg sent len,batch size]
#output [trg sent len,batch size,len(trg.vocab)]
output=output[1:,:,:].view(-1,output.shape[-1])
trg=trg[1:,:].view(-1)
loss=criterion(output,trg)
epoch_loss+=loss.item()
return epoch_loss/len(iterator)
def epoch_time(start_time, end_time):
elapsed_time = end_time - start_time
elapsed_mins = int(elapsed_time / 60)
elapsed_secs = int(elapsed_time - (elapsed_mins * 60))
return elapsed_mins, elapsed_secs
N_EPOCHS = 10
CLIP = 1
SAVE_DIR = 'models'
MODEL_SAVE_PATH = os.path.join(SAVE_DIR, 'tut1_model.pt')
best_valid_loss = float('inf')
if not os.path.isdir(f'{SAVE_DIR}'):
os.makedirs(f'{SAVE_DIR}')
for epoch in range(N_EPOCHS):
start_time = time.time()
train_loss = train(model, train_iterator, optimizer, criterion, CLIP)
valid_loss = evaluate(model, valid_iterator, criterion)
end_time = time.time()
epoch_mins, epoch_secs = epoch_time(start_time, end_time)
if valid_loss < best_valid_loss:
best_valid_loss = valid_loss
torch.save(model.state_dict(), MODEL_SAVE_PATH)
print(f'Epoch: {epoch + 1:02} | Time: {epoch_mins}m {epoch_secs}s')
print(f'\tTrain Loss: {train_loss:.3f} | Train PPL: {math.exp(train_loss):7.3f}')
print(f'\t Val. Loss: {valid_loss:.3f} | Val. PPL: {math.exp(valid_loss):7.3f}')