语言模型_Pytorch_代码超详解

1. 环境配置

Python 3.7.4
torch 1.5.1
torchtext 0.6.0
torchvision 0.6.1
numpy 1.16.5

2. 学习目标_语言模型

学习目标

  • 学习语言模型,以及如何训练一个语言模型
  • 学习torchtext的基本使用方法
    • 构建 vocabulary
    • word to inde 和 index to word
  • 学习torch.nn的一些基本模型
    • Linear
    • RNN
    • LSTM
    • GRU
  • RNN的训练技巧
    • Gradient Clipping
  • 如何保存和读取模型

3. 使用库的语法介绍

torchtext介绍和使用教程

4. 项目流程

  1. 我们使用 torchtext 来创建vocabulary, 然后把数据读成batch的格式。
  2. 定义模型RNN,LSTM模型介绍
  • 继承nn.Module
  • 初始化函数
  • forward函数
  • 其余可以根据模型需要定义相关的函数
  1. 初始化一个模型
  2. 定义评估模型
  3. 定义loss function和optimizer
  4. 定义训练模型:
  • 模型一般需要训练若干个epoch
  • 每个epoch我们都把所有的数据分成若干个batch
  • 把每个batch的输入和输出都包装成cuda tensor
  • forward pass,通过输入的句子预测每个单词的下一个单词
  • 用模型的预测和正确的下一个单词计算cross entropy loss
  • 清空模型当前gradient
  • backward pass
  • gradient clipping,防止梯度爆炸
  • 更新模型参数
  • 每隔一定的iteration输出模型在当前iteration的loss,以及在验证集上做模型的评估
  1. 计算perplexity
  2. 使用训练好的模型生成一些句子

5. 项目代码,部分运行结果与解析

import torch
import torchtext
from torchtext import data
from torchtext.vocab import Vectors
import numpy as np
import random

SEED = 53113

random.seed(SEED)
np.random.seed(SEED)
torch.manual_seed(SEED)
torch.cuda.manual_seed(SEED)
# torch.backends.cudnn.deterministic = True

BATCH_SIZE = 32 #一个batch 有多少个句子
EMBEDDING_SIZE = 500 #每个单词多少维
MAX_VOCAB_SIZE = 50000 # 单词总数


  1. 使用torchtext提供的LanguageModelingDataset来处理语言数据
    使用BPTTIterator得到连续的句子
TEXT = torchtext.data.Field(lower = True)
# .Field这个对象包含了我们打算如何预处理文本数据的信息,这里定义单词全部小写

train,val,test = \
torchtext.datasets.LanguageModelingDataset.splits(
    path = ".",
    train = "text8.train.txt" ,
    validation= "text8.dev.txt" ,
    test = "text8.test.txt", 
    text_field=TEXT)

TEXT.build_vocab(train,max_size = MAX_VOCAB_SIZE)
print("vacabulary size:{}".format(len(TEXT.vocab)))
# #TEXT.vocab 就是定义好的词汇表

# 查看生成的test
test
# 观察定义好的词汇表
print(TEXT.vocab.itos[0:50])
print("------"*10)
print(list(TEXT.vocab.stoi.items())[0:50])
#生成连续的句子
VOCAB_SIZE = len(TEXT.vocab)
train_iter,val_iter,test_iter = \
torchtext.data.BPTTIterator.splits(
    (train,val,test),
    batch_size = BATCH_SIZE,
    device = 0,
    bptt_len = 50,# 反向传播往回传的长度,这里我暂时理解为一个样本有多少个单词传入模型
    repeat = False,
    shuffle= True)
# BPTTIterator可以连续地得到连贯的句子,BPTT的全称是back propagation through time。
'''
Iterator:标准迭代器

BucketIerator:相比于标准迭代器,会将类似长度的样本当做一批来处理,
因为在文本处理中经常会需要将每一批样本长度补齐为当前批中最长序列的长度,
因此当样本长度差别较大时,使用BucketIerator可以带来填充效率的提高。
除此之外,我们还可以在Field中通过fix_length参数来对样本进行截断补齐操作。

BPTTIterator: 基于BPTT(基于时间的反向传播算法)的迭代器,一般用于语言模型中。

观察生成的batch
'''
#观察生成的batch
print(next(iter(train_iter)))# 一个batch训练集维度
print(next(iter(val_iter)))
print(next(iter(test_iter)))

#next(iterator[, default]) 
#返回iterator.__next__()的值,还可指定默认值,它指定在到达了迭代 器末尾时将返回的值 
#iter(obj) 从可迭代对象创建一个迭代器 .简单地说,迭代器是包含方法__next__的对象,可用于迭代一组值。
#next(it) 让迭代器前进一步并返回下一个元素
#模型的输入是一串文字,模型的输出也是一串文字,他们之间相差一个位置,因为语言模型的目标是根据之前的单词预测下一个单词
it = iter(train_iter)
batch = next(it)
print(" ".join([TEXT.vocab.itos[i] for i in batch.text[:,1].data]))
print(" ".join([TEXT.vocab.itos[i] for i in batch.target[:,1].data]))
# string.join(sequence) 将string与sequence中的所有字符串元素合并,并返回结果 
# X[:,1] 就是取所有行的第1个数据
#这里要取的数据是 next(iter(train_iter)).text[:,1].data
#torchtext.data: Generic data loaders, abstractions, and iterators for text (including vocabulary and word vectors)
#TEXT.vocab 就是定义好的词汇表

  1. 定义模型torch.nn.RNN经典模板 背诵

经典模板_LSTM Word 语言模型上的(实验)动态量化

import torch
import torch.nn as nn


class RNNModel(nn.Module):
    """ 一个简单的循环神经网络"""

    def __init__(self, rnn_type, ntoken, ninp, nhid, nlayers, dropout=0.5):
        # rnn_type;有两个层供选择'LSTM', 'GRU'
        # ntoken:VOCAB_SIZE=50002
        # ninp:EMBEDDING_SIZE = 650,输入层维度
        # nhid:EMBEDDING_SIZE = 1000,隐藏层维度,这里是我自己设置的,用于区分ninp层。
        # nlayers:纵向有多少层神经网络

        ''' 该模型包含以下几层:
            - 词嵌入层
            - 一个循环神经网络层(RNN, LSTM, GRU)
            - 一个线性层,从hidden state到输出单词表
            - 一个dropout层,用来做regularization
        '''
        super(RNNModel, self).__init__()
        self.drop = nn.Dropout(dropout)
        self.encoder = nn.Embedding(ntoken, ninp)
        # 定义输入的Embedding层,用来把每个单词转化为词向量
        
        if rnn_type in ['LSTM', 'GRU']: # 下面代码以LSTM举例
            
            self.rnn = getattr(nn, rnn_type)(ninp, nhid, nlayers, dropout=dropout)
            # getattr(nn, rnn_type) 相当于 nn.rnn_type
            # nlayers代表纵向有多少层。还有个参数是bidirectional: 是否是双向LSTM,默认false
        else:
            try:
                nonlinearity = {'RNN_TANH': 'tanh', 'RNN_RELU': 'relu'}[rnn_type]
            except KeyError:
                raise ValueError( """An invalid option for `--model` was supplied,
                                 options are ['LSTM', 'GRU', 'RNN_TANH' or 'RNN_RELU']""")
            self.rnn = nn.RNN(ninp, nhid, nlayers, nonlinearity=nonlinearity, dropout=dropout)
        self.decoder = nn.Linear(nhid, ntoken)
        # 最后线性全连接隐藏层的维度(1000,50002)
      

        self.init_weights()

        self.rnn_type = rnn_type
        self.nhid = nhid
        self.nlayers = nlayers

    def init_weights(self):
        initrange = 0.1
        self.encoder.weight.data.uniform_(-initrange, initrange)
        self.decoder.bias.data.zero_()
        self.decoder.weight.data.uniform_(-initrange, initrange)

    def forward(self, input, hidden): 
        
        ''' Forward pass:
            - word embedding
            - 输入循环神经网络
            - 一个线性层从hidden state转化为输出单词表
        '''
        
        # input.shape = seg_length * batch = torch.Size([50, 32])
        # 如果觉得想变成32*50格式,可以在LSTM里定义batch_first = True
        # hidden = (nlayers * 32 * hidden_size, nlayers * 32 * hidden_size)
        # hidden是个元组,输入有两个参数,一个是刚开始的隐藏层h的维度,一个是刚开始的用于记忆的c的维度,
        # 这两个层的维度一样,并且需要先初始化,hidden_size的维度和上面nhid的维度一样 =1000,我理解这两个是同一个东西。
        emb = self.drop(self.encoder(input)) # 
        # emb.shape=torch.Size([50, 32, 650]) # 输入数据的维度
        # 这里进行了运算(50,50002,650)*(50, 32,50002)
        output, hidden = self.rnn(emb, hidden)
        # output.shape = 50 * 32 * hidden_size # 最终输出数据的维度,
        # hidden是个元组,输出有两个参数,一个是最后的隐藏层h的维度,一个是最后的用于记忆的c的维度,这两个层维度相同 
        # hidden = (h层维度:nlayers * 32 * hidden_size, c层维度:nlayers * 32 * hidden_size)


        output = self.drop(output)
        decoded = self.decoder(output.view(output.size(0)*output.size(1), output.size(2)))
        # output最后的输出层一定要是二维的,只是为了能进行全连接层的运算,所以把前两个维度拼到一起,(50*32,hidden_size)
        # decoded.shape=(50*32,hidden_size)*(hidden_size,50002)=torch.Size([1600, 50002])
        
        return decoded.view(output.size(0), output.size(1), decoded.size(1)), hidden
               # 我们要知道每一个位置预测的是哪个单词,所以最终输出要恢复维度 = (50,32,50002)
               # hidden = (h层维度:2 * 32 * 1000, c层维度:2 * 32 * 1000)

    def init_hidden(self, bsz, requires_grad=True):
        # 这步我们初始化下隐藏层参数
        weight = next(self.parameters())
        # weight = torch.Size([50002, 650])是所有参数的第一个参数
        # 所有参数self.parameters(),是个生成器,LSTM所有参数维度种类如下:
        # print(list(iter(self.parameters())))
        # torch.Size([50002, 650])
        # torch.Size([4000, 650])
        # torch.Size([4000, 1000])
        # torch.Size([4000]) # 偏置项
        # torch.Size([4000])
        # torch.Size([4000, 1000])
        # torch.Size([4000, 1000])
        # torch.Size([4000])
        # torch.Size([4000])
        # torch.Size([50002, 1000])
        # torch.Size([50002])
        if self.rnn_type == 'LSTM':
            return (weight.new_zeros((self.nlayers, bsz, self.nhid), requires_grad=requires_grad),
                    weight.new_zeros((self.nlayers, bsz, self.nhid), requires_grad=requires_grad))
                   # return = (2 * 32 * 1000, 2 * 32 * 1000)
                   # 这里不明白为什么需要weight.new_zeros,我估计是想整个计算图能链接起来
                   # 这里特别注意hidden的输入不是model的参数,不参与更新,就跟输入数据x一样
                   
        else:
            return weight.new_zeros((self.nlayers, bsz, self.nhid), requires_grad=requires_grad)
            # GRU神经网络把h层和c层合并了,所以这里只有一层。

初始化一个模型

nhid = 1000 # 我自己设置的维度,用于区分embeding_size=650
model = RNNModel("LSTM", VOCAB_SIZE, EMBEDDING_SIZE, nhid, 2, dropout=0.5)
if USE_CUDA:
    model = model.cuda()

model



定义评估模型的代码

  • 模型的评估和模型的训练逻辑基本相同,唯一的区别是我们只需要forward pass,不需要backward pass
# 先从下面训练模式看起,在看evaluate
def evaluate(model, data):
    model.eval() # 预测模式
    total_loss = 0.
    it = iter(data)
    total_count = 0.
    with torch.no_grad():
        hidden = model.init_hidden(BATCH_SIZE, requires_grad=False)
# 这里不管是训练模式还是预测模式,h层的输入都是初始化为0,hidden的输入不是model的参数
# 这里model里的model.parameters()已经是训练过的参数。
        for i, batch in enumerate(it):
            data, target = batch.text, batch.target
            # # 取出验证集的输入的数据和输出的数据,相当于特征和标签
            if USE_CUDA:
                data, target = data.cuda(), target.cuda()
            hidden = repackage_hidden(hidden) # 截断计算图
            with torch.no_grad(): # 验证阶段不需要更新梯度
                output, hidden = model(data, hidden)
                #调用model的forward方法进行一次前向传播,得到return输出值
            loss = loss_fn(output.view(-1, VOCAB_SIZE), target.view(-1))
            # 计算交叉熵损失
            
            total_count += np.multiply(*data.size()) 
# 上面计算交叉熵的损失是平均过的,这里需要计算下总的损失
# total_count先计算验证集样本的单词总数,一个样本有50个单词,一个batch32个样本
# np.multiply(*data.size()) =50*32=1600
            total_loss += loss.item()*np.multiply(*data.size())
# 每次batch平均后的损失乘以每次batch的样本的总的单词数 = 一次batch总的损失
            
    loss = total_loss / total_count # 整个验证集总的损失除以总的单词数
    model.train() # 训练模式
    return loss

import torch
import numpy as np
a = torch.ones((5,3))
print(a.size())
np.multiply(*a.size()) 


定义下面的一个function,帮助我们把一个hidden state和计算图之前的历史分离。

# Remove this part
def repackage_hidden(h):
    """Wraps hidden states in new Tensors, to detach them from their history."""
    if isinstance(h, torch.Tensor): 
        # 这个是GRU的截断,因为只有一个隐藏层
        # 判断h是不是torch.Tensor
        return h.detach() # 截断计算图,h是全的计算图的开始,只是保留了h的值
    #参考[https://blog.csdn.net/u013250416/article/details/81276671]
    else: # 这个是LSTM的截断,有两个隐藏层,格式是元组
        return tuple(repackage_hidden(v) for v in h)


定义loss function和optimizer

loss_fn = nn.CrossEntropyLoss() # 交叉熵损失
learning_rate = 0.001
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
scheduler = torch.optim.lr_scheduler.ExponentialLR(optimizer, 0.5)
# 每调用一次这个函数,lenrning_rate就降一半,0.5就是一半的意思


训练模型:

模型一般需要训练若干个epoch
每个epoch我们都把所有的数据分成若干个batch
把每个batch的输入和输出都包装成cuda tensor
forward pass,通过输入的句子预测每个单词的下一个单词
用模型的预测和正确的下一个单词计算cross entropy loss
清空模型当前gradient
backward pass
gradient clipping,防止梯度爆炸
更新模型参数
每隔一定的iteration输出模型在当前iteration的loss,以及在验证集上做模型的评估

import copy
GRAD_CLIP = 1.
NUM_EPOCHS = 2

val_losses = []
for epoch in range(NUM_EPOCHS):
    model.train() # 训练模式
    it = iter(train_iter) 
    # iter,生成迭代器,这里train_iter也是迭代器,不用iter也可以
    hidden = model.init_hidden(BATCH_SIZE) 
    # 得到hidden初始化后的维度
    for i, batch in enumerate(it):
        data, target = batch.text, batch.target
        # 取出训练集的输入的数据和输出的数据,相当于特征和标签
        if USE_CUDA:
            data, target = data.cuda(), target.cuda()
        hidden = repackage_hidden(hidden)
# 语言模型每个batch的隐藏层的输出值是要继续作为下一个batch的隐藏层的输入的
# 因为batch数量很多,如果一直往后传,会造成整个计算图很庞大,反向传播会内存崩溃。
# 所有每次一个batch的计算图迭代完成后,需要把计算图截断,只保留隐藏层的输出值。
# 不过只有语言模型才这么干,其他比如翻译模型不需要这么做。
# repackage_hidden自定义函数用来截断计算图的。
        model.zero_grad() # 梯度归零,不然每次迭代梯度会累加
        output, hidden = model(data, hidden)
        # output = (50,32,50002)
        loss = loss_fn(output.view(-1, VOCAB_SIZE), target.view(-1))
# output.view(-1, VOCAB_SIZE) = (1600,50002)
# target.view(-1) =(1600),关于pytorch中交叉熵的计算公式请看下面链接。
# https://blog.csdn.net/geter_CS/article/details/84857220
        loss.backward()#反向传播,计算当前梯度
        torch.nn.utils.clip_grad_norm_(model.parameters(), GRAD_CLIP)
        # 防止梯度爆炸,设定阈值,当梯度大于阈值时,更新的梯度为阈值
        optimizer.step()#根据梯度更新网络参数

        if i % 1000 == 0:
            print("epoch", epoch, "iter", i, "loss", loss.item())
    
        if i % 10000 == 0:
            val_loss = evaluate(model, val_iter)
            
            if len(val_losses) == 0 or val_loss < min(val_losses):
                # 如果比之前的loss要小,就保存模型
                print("best model, val loss: ", val_loss)
                torch.save(model.state_dict(), "lm-best.th")
            else: # 否则loss没有降下来,需要优化
                scheduler.step() # 自动调整学习率
                optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
                # 学习率调整后需要更新optimizer,下次训练就用更新后的
            val_losses.append(val_loss) # 保存每10000次迭代后的验证集损失损失

# 加载保存好的模型参数
best_model = RNNModel("LSTM", VOCAB_SIZE, EMBEDDING_SIZE, nhid, 2, dropout=0.5)
if USE_CUDA:
    best_model = best_model.cuda()
best_model.load_state_dict(torch.load("lm-best.th"))
# 把模型参数load到best_model里

使用最好的模型计算perplexity

val_loss = evaluate(best_model, val_iter)
print("perplexity: ", np.exp(val_loss))
test_loss = evaluate(best_model, test_iter)
print("perplexity: ", np.exp(test_loss))

Reference

邱锡鹏 深度学习
经典模板_LSTM Word 语言模型上的(实验)动态量化
5. LSTM Pytorch load
PyTorch中在反向传播前为什么要手动将梯度清零?@知乎 Pascal
七月在线面试题库

你可能感兴趣的:(nlp,python,神经网络,nlp)