赞
踩
本文代码所需要的库主要为:pytorch(2.0.0),nltk,tqdm。
这篇文章的内容主要是对上一篇Transformer以及self-attention的一些理解文章所提到的内容进行代码实现和解释。
数据预处理部分参考了大佬 苏同学的方法~
一开始的数据预处理其实是采用torchtext中自带的Multi30k数据集。但是这就出现了很多的问题。
首先是torchtext的版本,torch2.0.0对应的版本是0.15.0,而torchtext在0.8和0.14经历了两个API的大变动,torch中的迭代器模块功能也是随着版本有了变化,比如去掉了Field…
这样下来,一开始代码运行下来报了很多错。查了很多资料之后,对代码进行修改之后,结果又报了爬虫错误。索性放弃了。
个人认为,如果要开始理解代码是如何运作的,那么数据的说明就必不可少。毕竟有些时候看着包里的函数和类实在是有些抽象,不知道到底是什么东西。这个时候就需要输出一些中间变量看一看,自己到底是在干什么。如果对一开始的数据不了解不清楚的话,想输出的时候大概率也是会报错的吧…
这里我们主要需要创建的数据有:
采用的数据集从可以从这里下载。
import re import unicodedata from io import open import nltk from tqdm import tqdm import torch from torch.utils.data import Dataset,random_split,DataLoader #1-- def unicodeToAscii(s): return ''.join(c for c in unicodedata.normalize('NFD',s) if unicodedata.category(c)!='Mn')#这里为什么是'Mn'呢? def normalization(s): s=unicodeToAscii(s.lower().strip()) s=re.sub(r'[^a-zA-z0-9\s]','',s) return s.strip() #--1 #2-- def tokenization(s): tokens=nltk.word_tokenize(s)#将字符串进行分词,返回一个列表 #如"This is a sentence." -> ['This', 'is', 'a', 'sentence', '.'] return tokens class Dictionary: def __init__(self,name): self.name=name self.word2idx={} self.idx2word={} self.idx=0 self.add_word("<pad>") self.add_word("<sos>") self.add_word("<eos>") def __len__(self): return len(self.word2idx) def add_word(self,word): if not word in self.word2idx: self.word2idx[word]=self.idx self.idx2word[self.idx]=word self.idx+=1 #--2 #3-- def padding(t,maxlen):#按最长数据进行补全数据,补的数据不参与训练。 while len(t)<maxlen: t.append(eng_dict.word2idx["<pad>"]) return t def collate_fn_padding(batch): src=[] trg=[] maxlen=0 for i in batch: print(i) maxlen=max(len(i[0]),len(i[1]),maxlen) for i in range(0,len(batch)): src.append(padding(batch[i][0],maxlen)) trg.append(padding(batch[i][1],maxlen)) return torch.LongTensor(src),torch.LongTensor(trg) class TranslateDatasets(Dataset): def __init__(self,x,y): super(TranslateDatasets,self).__init__() self.inputs=x#source self.targets=y#targets def __getitem__(self,idx): return self.inputs[idx],self.targets[idx] def __len__(self): return len(self.inputs) #--3 #1-- lines = open('fra2.txt', encoding='utf-8').read().strip().split('\n') #读入数据,按换行进行分类 pairs=[[normalization(s) for s in l.split('\t')] for l in lines] #--1 #2-- eng_dict=Dictionary("eng") fra_dict=Dictionary("fra") sentences_eng=[] sentences_fra=[] for piece in tqdm(pairs):#tqdm是对循环进行可视化 sentence_eng=[] sentence_fra=[] sentence_eng.append(eng_dict.word2idx["<sos>"]) sentence_fra.append(eng_dict.word2idx["<sos>"]) token_eng=tokenization(piece[0])#piece[0]中是英语 token_fra=tokenization(piece[1])#piece[1]中是法语 for token in token_eng: eng_dict.add_word(token) sentence_eng.append(eng_dict.word2idx[token]) for token in token_fra: fra_dict.add_word(token) sentence_fra.append(fra_dict.word2idx[token]) sentence_eng.append(eng_dict.word2idx["<eos>"]) sentence_fra.append(eng_dict.word2idx["<eos>"]) sentences_eng.append(sentence_eng) sentences_fra.append(sentence_fra) #--2 #3-- full_dataset=TranslateDatasets(sentences_eng,sentences_fra) train_size=int(0.8*len(full_dataset)) test_size=len(full_dataset)-train_size train_dataset,test_dataset=random_split(full_dataset,[train_size,test_size]) train_loader = DataLoader(dataset=train_dataset, batch_size=64, shuffle=True, collate_fn=collate_fn_padding) test_loader = DataLoader(dataset=test_dataset, batch_size=64, shuffle=False, collate_fn=collate_fn_padding) #--3 src_pad_idx=eng_dict.word2idx["<pad>"] trg_pad_idx=fra_dict.word2idx["<pad>"] trg_sos_idx=fra_dict.word2idx["<sos>"] enc_voc_size=len(eng_dict) dec_voc_size=len(fra_dict)
这里的数据集,我为了能够打印数据方便理解,对下载来的数据集进行了一定阉割(其实就是删除了大部分)。大致的数据集长这样:
可以看到数据非常清晰明了:英语+Tab+法语,后面的就是数据来源了,与我们的训练无关。
这样一来Normalization的方向也变的十分清晰,先把数据中的unicode转化为ASCII码,之后用re模块对数据进行处理。
具体解释和方法可见代码中的1- -1部分。
这部分的实现在代码中的2- -2部分。
稍微解释一下NLP中一些标识符的含义:
这里定义了一个Dictionary类,类里有word2idx和idx2word的字典成员变量。定义了add_word方法:如果一个单词在词典中出现过则什么都不做,如果没出现过则按出现顺序编号。这里 <sos>、<eos>、<pad>的序列号分别为1、2、0。
这里的定义还是十分好理解的!
下一个循环实现的功能是:
1.把按行分割的句子加上标识符封装为一个列表,再把每个句子加入到总的句子列表中。
2.将每个词加入英语和法语词典中。
输出一下变量,方便理解。
以英文字典和英文句子为例,eng_dict.word2idx记录了每个单词和序列的键值对,而sentences_eng则是所有句子中单词序列号的索引,如数据集中的第一条“GO!”,在sentences_eng中表示为[1,3,2],其中1为<sos>表示句子的开始,2为<eos>表示句子的结束。
这部分的实现在代码中的3- -3部分。
构建训练集和测试集直接重新定义了一个继承于torch Dataset的类,类中包含inputs和targets的列表成员变量。并且重写了len和索引数据的方法。
训练集和测试集是从整个测试集中分出的,代码中写train_sizeh和test_size实际是规定了训练集和测试集数据的比例,依据这个比例用torch中的random_split函数进行数据的随机分类。
接下来就是构造迭代器了,torch中可以直接用DataLoader类来定义迭代器。collate_fn的参数是一个函数,这个函数可以自定义。如果该参数写上,那么返回的值实际就是自定义函数的返回值。
在此之后定义的变量则是在train模块中所要用到的常量。
这里引用一下大佬博客中的原话:
这里不必将所有的句子都填充到相同长度,训练时只需要保证每个batch中的所有句子长度相等就可以了。所以我这里使用了dataloder的collate_fn参数,在读取每个batch时将句子进行padding,并将其转化为tensor。
同样,输出一下迭代器的内容。
篇幅所限,就截那么多。实则也就是总句子列表中的内容,只不过对句子进行了补齐,并且由于用了random_split的关系,进行了一定的截取,而且每次运行出来的结果是不一样的。
import torch
from torch import nn
class TokenEmbedding(nn.Embedding):
def __init__(self,vocab_size,d_model):
"""
class for token embedding that included positional information
:param vocab_size: size of vocabulary
:param d_model: dimensions of model
"""
super(TokenEmbedding,self).__init__(vocab_size,d_model)
这个embedding层包含了原始数据的位置信息。
vocab_size:词典中包含数据的数量。
d_model:模型的尺度。
表示词嵌入模型,输入单词数为vocab_size,每个单词用d_model个向量表示。
class PositionalEncoding(nn.Module): def __init__(self,d_model,max_len,device): """ constructor of sin encoding class :param d_model: dimension of model :param max_len: max sequence length :param device: hardware device setting """ super(PositionalEncoding,self).__init__() #same size with input matrix(for adding with input matrix) self.encoding=torch.zeros(max_len,d_model,device=device) self.encoding.requires_gradu=False#这里不需要计算梯度 #1D=>2D unsqeeze to represnet word's position pos=torch.arange(0,max_len,device=device) pos=pos.float().unsqueeze(dim=1) _2i=torch.arange(0,d_model,step=2,device=device).float() #i means index of d_model(比如 embedding size=50,i=[0,50]) #step=2 means i mulitiplied with two(same with 2*i) #compute positional encoding to consider positional information of words self.encoding[:,0::2]=torch.sin(pos/(10000**(_2i/d_model))) self.encoding[:,1::2]=torch.sin(pos/(10000**(_2i/d_model))) def forward(self, x): # self.encoding # [max_len = 512, d_model = 512] batch_size, seq_len = x.size() # [batch_size = 128, seq_len = 30] return self.encoding[:seq_len, :] # [seq_len = 30, d_model = 512] # it will add with tok_emb : [128, 30, 512]
这里则是Positional Encoding的实现,原理公式可以参照上一篇博客。
class TransformerEmbedding(nn.Module): """ token embedding + positional encoding (sinusoid) positional encoding can give positional information to network """ def __init__(self,vocab_size,d_model,max_len,drop_prob,device): """ class for word embedding that included positional information :param vocab_size: size of vocabulary :param d_model: dimensions of model """ super(TransformerEmbedding,self).__init__() self.tok_emb=TokenEmbedding(vocab_size,d_model) self.pos_emb=PositionalEncoding(d_model,max_len,device) self.drop_out=nn.Dropout(p=drop_prob) def forward(self,x): tok_emb=self.tok_emb(x) pos_emb=self.pos_emb(x) return self.drop_out(tok_emb+pos_emb)
其实这一个Embedding层就是上述两个层的相加,得到最后的词嵌入模型。
import torch from torch import nn import math class LayerNorm(nn.Module): def __init__(self,d_model,eps=1e-12): super(LayerNorm,self).__init__() self.gamma=nn.Parameter(torch.ones(d_model)) self.beta=nn.Parameter(torch.zeros(d_model)) self.eps=eps def forward(self,x): mean=x.mean(-1,keepdim=True) var=x.var(-1,unbiased=False,keepdim=True) out=(x-mean)/torch.sqrt(var+self.eps) out=self.gamma*out+self.beta return out
这部分是对编码器的每个子层进行残差连接和标准化。
这里放一张图,是代码原理的公式。
对图中的变量做出解释:
μ
\mu
μB:样本均值
σ
\sigma
σB2:样本方差(未修正)
代码中是用变量mean和var来表示的。
对照着公式,可以很清楚的看到代码的实现过程。
class PositionWiseFeedForward(nn.Module):
def __init__(self,d_model,hidden,drop_prob=0.1):
super(PositionWiseFeedForward,self).__init__()
self.linear1=nn.Linear(d_model,hidden)
self.linear2=nn.Linear(hidden,d_model)
self.relu=nn.ReLU()
self.dropout=nn.Dropout(p=drop_prob)
def forward(self,x):
x=self.linear1(x)
x=self.relu(x)
x=self.dropout(x)
x=self.linear2(x)
return x
这是位置前馈网络所对应的公式:
代码中通过nn.Linear()进行线性变换;nn.ReLu()进行线性整流,保留非负部分。
也是比较清晰的。
class ScaleDotProductAttention(nn.Module): """ compute scale dot product attention Query : given sentence that we focused on (decoder) Key : every sentence to check relationship with Qeury(encoder) Value : every sentence same with Key (encoder) """ def __init__(self): super(ScaleDotProductAttention,self).__init__() self.softmax=nn.Softmax(dim=-1) def forward(self,q,k,v,mask=None,e=1e-12): #输入是4个维度的tensor [batch_size,head,length,d_tensor] batch_size,head,length,d_tensor=k.size() #1. dot product Query with key^T to compute similarity k_t=k.transpose(2,3)#转置 score=(q@k_t)/math.sqrt(d_tensor)#@为矩阵乘法 #2. apply masking(可选) if mask is not None: score=score.masked_fill(mask==0,-10000) #3. pass them softmax to make [0,1] range score=self.softmax(score) #4. multiply with Value v=score @ v return v,score
Scale Dot-Product Attention所对应的公式:
首先计算 Q 和 K 之间的点积,为了防止其结果过大,会除以 d k \sqrt{d_k} dk ,其中 d k d_k dk为K向量的维度。
然后利用 Softmax 操作将其结果归一化为概率分布,再乘以矩阵 V 就得到权重求和的表示。
代码中还提到了mask的操作,其实对结果的影响是不大的。
class MultiHeadAttention(nn.Module): def __init__(self,d_model,n_head): super(MultiHeadAttention,self).__init__() self.n_head=n_head self.attention=ScaleDotProductAttention() self.w_q=nn.Linear(d_model,d_model) self.w_k=nn.Linear(d_model,d_model) self.w_v=nn.Linear(d_model,d_model) self.w_concat=nn.Linear(d_model,d_model) def split(self,tensor): """ split tensor by number of head :param tensor: [batch_size, length, d_model] :return: [batch_size, head, length, d_tensor] """ batch_size,length,d_model=tensor.size() d_tensor=d_model//self.n_head#整除 tensor=tensor.view(batch_size,length,self.n_head,d_tensor).transpose(1,2) return tensor def concat(self,tensor): """ inverse function of self.split(tensor : torch.Tensor) :param tensor: [batch_size, head, length, d_tensor] :return: [batch_size, length, d_model] """ batch_size,head,length,d_tensor=tensor.size() d_model=head*d_tensor tensor=tensor.transpose(1,2).contiguous().view(batch_size,length,d_model) return tensor def forward(self,q,k,v,mask=None): #1. dot product with weight matrices q,k,v=self.w_q(q),self.w_k(k),self.w_v(v) #2. split tensor by number of heads q,k,v=self.split(q),self.split(k),self.split(v) #3. do scale dot product to cmpute similarity out,attention=self.attention(q,k,v,mask=mask) #4.concat and pass to linear layer out=self.concat(out) out=self.w_concat(out) #5.visualize attention map #暂不实现 return out
原理可见上一篇博客,这里给出代码实现的公式:
将QKV三个矩阵按head进行划分后,用上文定义的Scale Dot-Produc Attention计算self-Attention
Concat这里的意思是连接,因为计算Attention后还多了一个维度head,这里需要去掉维度head变回原来的数据维度。
最后再进行线性变换即可。
import torch from torch import nn from layer import LayerNorm,MultiHeadAttention,PositionWiseFeedForward from embedding import TransformerEmbedding class EncoderLayer(nn.Module): def __init__(self,d_model,ffn_hidden,n_head,drop_prob): super(EncoderLayer,self).__init__() self.attention=MultiHeadAttention(d_model=d_model,n_head=n_head) self.norm1=LayerNorm(d_model=d_model) self.dropout1=nn.Dropout(p=drop_prob) self.ffn=PositionWiseFeedForward(d_model=d_model,hidden=ffn_hidden,drop_prob=drop_prob) self.norm2=LayerNorm(d_model=d_model) self.dropout2=nn.Dropout(p=drop_prob) def forward(self,x,src_mask): #1. compute self attention _x=x x=self.attention(q=x,k=x,v=x,mask=src_mask) #2. add and norm x=self.dropout1(x) x=self.norm1(x+_x) #3. positionwise feed forward network _x=x x=self.ffn(x) #4. add and norm x=self.dropout2(x) x=self.norm2(x+_x) return x class Encoder(nn.Module): def __init__(self,enc_voc_size,max_len,d_model,ffn_hidden,n_head,n_layers,drop_prob,device): super().__init__() self.emb=TransformerEmbedding(d_model=d_model,max_len=max_len,vocab_size=enc_voc_size,drop_prob=drop_prob,device=device) self.layers=nn.ModuleList([EncoderLayer(d_model=d_model,ffn_hidden=ffn_hidden,n_head=n_head,drop_prob=drop_prob) for _ in range(n_layers)]) def forward(self,x,src_mask): x=self.emb(x) for layer in self.layers: x=layer(x,src_mask) return x
class DecoderLayer(nn.Module): def __init__(self,d_model,ffn_hidden,n_head,drop_prob): super(DecoderLayer,self).__init__() self.self_attention=MultiHeadAttention(d_model=d_model,n_head=n_head) self.norm1=LayerNorm(d_model=d_model) self.dropout1=nn.Dropout(p=drop_prob) self.enc_dec_attention=MultiHeadAttention(d_model=d_model,n_head=n_head) self.norm2=LayerNorm(d_model=d_model) self.dropout2=nn.Dropout(p=drop_prob) self.ffn=PositionWiseFeedForward(d_model=d_model,hidden=ffn_hidden,drop_prob=drop_prob) self.norm3=LayerNorm(d_model=d_model) self.dropout3=nn.Dropout(p=drop_prob) def forward(self,dec,enc,trg_mask,src_mask): #1.compute self attention _x=dec x=self.self_attention(q=dec,k=dec,v=dec,mask=trg_mask) #2.add and norm x=self.dropout1(x) x=self.norm1(x+_x) if enc is not None: #3. compute encoder-decoder attention _x=x x=self.enc_dec_attention(q=x,k=enc,v=enc,mask=src_mask) #4. add and norm x=self.dropout2(x) x=self.norm2(x+_x) #5. positionwise feed forward network _x=x x=self.ffn(x) #6. add and norm x=self.dropout3(x) x=self.norm3(x+_x) return x class Decoder(nn.Module): def __init__(self,dec_voc_size,max_len,d_model,ffn_hidden,n_head,n_layers,drop_prob,device): super().__init__() self.emb=TransformerEmbedding(d_model=d_model,max_len=max_len,vocab_size=dec_voc_size,drop_prob=drop_prob,device=device) self.layers=nn.ModuleList([DecoderLayer(d_model=d_model,ffn_hidden=ffn_hidden,n_head=n_head,drop_prob=drop_prob) for _ in range(n_layers)]) self.linear=nn.Linear(d_model,dec_voc_size) def forward(self,trg,enc_src,trg_mask,src_mask): trg=self.emb(trg) for layer in self.layers: trg=layer(trg,enc_src,trg_mask,src_mask) #pass to LM head output=self.linear(trg) return output
EncoderLayer和DecoderLayer是每一个层中都要做的事,这里的实现只需要将上文中定义过的类和函数进行组合。
Encoder和Decoder类中是对每一个层进行整合连接。
具体组合方式和Transformer model的图差不多:
class Transformer(nn.Module): def __init__(self,src_pad_idx,trg_pad_idx,trg_sos_idx,enc_voc_size,dec_voc_size,d_model,n_head,max_len,ffn_hidden,n_layers,drop_prob,device): super().__init__() self.src_pad_idx=src_pad_idx self.trg_pad_idx=trg_pad_idx self.trg_sos_idx=trg_sos_idx self.device=device self.encoder=Encoder(d_model=d_model,n_head=n_head,max_len=max_len,ffn_hidden=ffn_hidden, enc_voc_size=enc_voc_size,drop_prob=drop_prob,n_layers=n_layers,device=device) self.decoder=Decoder(d_model=d_model,n_head=n_head,max_len=max_len,ffn_hidden=ffn_hidden, dec_voc_size=dec_voc_size,drop_prob=drop_prob,n_layers=n_layers,device=device) def make_src_mask(self,src): src_mask=(src!=self.src_pad_idx).unsqueeze(1).unsqueeze(2) return src_mask def make_trg_mask(self,trg): trg_pad_mask=(trg!=self.trg_pad_idx).unsqueeze(1).unsqueeze(3).to(self.device) trg_len=trg.shape[1] trg_sub_mask=torch.tril(torch.ones(trg_len,trg_len)).type(torch.ByteTensor).to(self.device) trg_mask=trg_pad_mask & trg_sub_mask return trg_mask def forward(self,src,trg): src_mask=self.make_src_mask(src) trg_mask=self.make_trg_mask(trg) enc_src=self.encoder(src,src_mask) output=self.decoder(trg,enc_src,trg_mask,src_mask) return output
Transformer类中是将Encoder和Decoder整合在一起,其实还是有很多细节需要注意的。
import math from collections import Counter import numpy as np def bleu_stats(hypothesis,reference): #Compute statistics for BLEU #BLEU:bilingual evaluation understudy,即:双语互译质量评估辅助工具(双语替换测评)。它是用来评估机器翻译质量的工具。 #BLEU的设计思想:机器翻译结果越接近专业人工翻译的结果,则越好。 stats=[] stats.append(len(hypothesis)) stats.append(len(reference)) for n in range(1,5): s_ngrams=Counter([tuple(hypothesis[i:i+n]) for i in range(len(hypothesis)+1-n)]) r_ngrams=Counter([tuple(reference[i:i+n]) for i in range(len(reference)+1-n)]) stats.append(max([sum((s_ngrams&r_ngrams).values()),0])) stats.append(max([len(hypothesis)+1-n,0])) return stats def bleu(stats): #Compute BLEU given n-gram statistics if len(list(filter(lambda x:x==0,stats)))>0: return 0 (c,r)=stats[:2] log_bleu_prec=sum([math.log(float(x)/y) for x,y in zip(stats[2::2],stats[3::2])])/4. return math.exp(min([0,1-float(r)/c])+log_bleu_prec) def get_bleu(hypotheses, reference): """Get validation BLEU score for dev set.""" stats = np.array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]) for hyp, ref in zip(hypotheses, reference): stats += np.array(bleu_stats(hyp, ref)) return 100 * bleu(stats) def epoch_time(start,end): elapsed_time=end-start elapsed_mins=int(elapsed_time/60) elapsed_secs=int(elapsed_time-(elapsed_mins*60)) return elapsed_mins,elapsed_secs
这里用到了bleu分数来对最后的结果进行评测,一般不用手动实现,包里会自带。但是这里还是手动实现了一下。如何实现不用管,只需要知道分数越高,结果越好。一般来说是0-1之间的数,但是这里乘了个100,变成了0-100的数。
这里就是训练测试的主程序了。
import math import time import torch from torch import nn,optim from torch.optim import Adam from data_try import * from models.model import Transformer from util import get_bleu from util import epoch_time import numpy as np #这里是一些定义的常量 device=torch.device("cuda:0" if torch.cuda.is_available() else "cpu") batch_size=128 max_len=256 d_model=512 n_layers=6 n_heads=8 ffn_hidden=2048 drop_prob=0.1 init_lr=1e-5 factor=0.9 adam_eps=5e-9 patience=10 warmup=100 epoch=8 clip=1 weight_dacay=5e-4 inf=float("inf") #1-- def count_parameters(model): return sum(p.numel() for p in model.parameters() if p.requires_grad) def initialize_weights(m): if hasattr(m,"wieght") and m.weight.dim()>1: nn.init.kaiming_uniform(m.weight.data) model=Transformer(src_pad_idx=src_pad_idx, trg_pad_idx=trg_pad_idx, trg_sos_idx=trg_sos_idx, d_model=d_model, enc_voc_size=enc_voc_size, dec_voc_size=dec_voc_size, max_len=max_len, ffn_hidden=ffn_hidden, n_head=n_heads, n_layers=n_layers, drop_prob=drop_prob, device=device).to(device) print(f'The model has {count_parameters(model):,} trainable parameters') model.apply(initialize_weights) optimizer=Adam(params=model.parameters(),lr=init_lr,weight_decay=weight_dacay,eps=adam_eps) scheduler=optim.lr_scheduler.ReduceLROnPlateau(optimizer=optimizer,verbose=True,factor=factor,patience=patience) criterion=nn.CrossEntropyLoss(ignore_index=src_pad_idx) #--1 #2-- def train(model,iterator,optimizer,criterion,clip): model.train() epoch_loss=0 i=0 for src,trg in iterator: i+=1 src=src.to(device) trg=trg.to(device) optimizer.zero_grad() output=model(src,trg[:,:-1]) output_reshape=output.contiguous().view(-1,output.shape[-1]) trg=trg[:,1:].contiguous().view(-1) loss=criterion(output_reshape,trg) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(),clip) optimizer.step() epoch_loss+=loss.item() print('step :',round((i/len(iterator))*100,2),'% , loss :',loss.item()) return epoch_loss/len(iterator) def idx_to_word(x,vocab): words=[] for i in x.cpu().numpy(): word=vocab.idx2word[i] if '<' not in word: words.append(word) words=" ".join(words) return words def evaluate(model,iterator,criterion): model.eval() epoch_loss=0 batch_bleu=[] with torch.no_grad(): for src,trg in iterator: trg1=trg src=src.to(device) trg=trg.to(device) output=model(src,trg[:,:-1]) output_reshape=output.contiguous().view(-1,output.shape[-1]) trg=trg[:,1:].contiguous().view(-1) loss=criterion(output_reshape,trg) epoch_loss+=loss.item() total_bleu=[] for j in range(batch_size): try: trg_words=idx_to_word(trg1[j],fra_dict) output_words=output[j].max(dim=1)[1] output_words=idx_to_word(output_words,fra_dict) bleu=get_bleu(hypotheses=output_words.split(),reference=trg_words.split()) total_bleu.append(bleu) except: pass total_bleu=sum(total_bleu)/len(total_bleu) batch_bleu.append(total_bleu) batch_bleu=sum(batch_bleu)/len(batch_bleu) return epoch_loss/len(iterator),batch_bleu def run(total_epoch,best_loss): train_losses,test_losses,bleus=[],[],[] for step in range(total_epoch): start_time=time.time() train_loss=train(model,train_loader,optimizer,criterion,clip) valid_loss,bleu=evaluate(model,test_loader,criterion) end_time=time.time() if step>warmup: scheduler.step(valid_loss) train_losses.append(train_loss) test_losses.append(valid_loss) bleus.append(bleu) epoch_mins,epoch_secs=epoch_time(start_time,end_time) if valid_loss<best_loss: best_loss=valid_loss #torch.save(model.state_dict(),'saved/model-{0}.pt'.format(valid_loss)) #将训练好的模型进行存储,下次就不用重新训练了。 f=open("result/train_loss.txt",'w') f.write(str(train_losses)) f.close() f = open('result/bleu.txt', 'w') f.write(str(bleus)) f.close() f = open('result/test_loss.txt', 'w') f.write(str(test_losses)) f.close() print(f'Epoch: {step + 1} | Time: {epoch_mins}m {epoch_secs}s') print(f'\tTrain Loss: {train_loss:.3f} | Train PPL: {math.exp(train_loss):7.3f}') print(f'\tVal Loss: {valid_loss:.3f} | Val PPL: {math.exp(valid_loss):7.3f}') print(f'\tBLEU Score: {bleu:.3f}') #--2 run(epoch,inf)
1- -1
这部分是训练之前对模型的定义和初始化。
count_parameters函数用于计算模型中所用到的参数数量,这个参数是包中调好的。
这里的优化器用了Adam,损失函数用交叉熵损失。
2- -2
这部分开始训练和测试。
train函数部分
src和trg变量在数据预处理部分已经有过展示,是翻译前后每个句子的向量表示。这里to(device)的方法是把变量放到GPU上,因为torch要求所有变量必须在一个设备上(CPU或GPU),否则会抛出异常。
output和trg是两个tensor,表示经过Transformer模型后得出的翻译结果和标准的译文。
这里以输出变量的形式说明,每一步的操作干了什么。
train函数开始第二行的trg变量:
trg1
对trg变量进行变换。
通过对比可以看到,trg[:,1:].contiguous().view(-1)的操作,把所有维度的数据增加至了同一个维度,并且通过切片删除了1的数据(因为1是<sos>)。
output_reshape的变量就比较抽象了,数据也很大,只显示了一部分。
和trg相比,差的不是一星半点。这是因为数据集过小,并且我只取了一个epoch,并没有反向传播进行参数的修改,相当于没开始训练,只是简单演示。
evaluate函数部分
函数中前面的内容和train差不多,主要是后面的部分。
这一部分的功能是获得Bleu分数。
为什么要用到try-catch语句呢?
因为我们并不知道每一个句子的长度大小,所以在每一批训练测试中,用最大的批尺寸来进行代替。但是有些句子不会有这么大的长度,如果不用try-catch会抛出超过列表下标的异常,下方的代码就不会运行。
进行分数的评测我们需要统一标准,所以这里统一用词典中的序列来作为标准。
这里通过写idx_to_word()函数,来获取序列。
有一些细节需要注意。trg变量已经经过变换,但是计算的时候要求原变量。所以要在开始做变量复制。
在idx_to_word函数中,为什么要写x.cpu().numpy()呢?因为数据trg是tensor张量,而tensor张量不是可迭代对象,直接运行会报错。所以需要转化成numpy数组。但是问题又来了,trg是gpu上的变量,gpu上的变量是无法转化为numpy数组的,所以只能先转到cpu上。
同样,打印一些变量。
这里是一些循环中的变量,可以看出翻译前后的效果。Bleu是根据图中的结果来计算分数的。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。