NLP入门——RNN、LSTM模型的搭建、训练与预测

在卷积语言模型建模时,我们选取上下文长度ctx_len进行训练,预测时选取句子的最后ctx_len个分词做预测,这样句子的前0~seql-1-ctx_len个词对于预测没有任何帮助,这对于语言处理来说显然是不利的。
在词袋语言模型建模时,我们舍弃ctx_len的概念,利用前缀和,用前seql-1个词作为训练数据,后seql-1个词作为标签训练。最终用这句话所有的分词求前缀和做预测,这样句子中所有的分词都参与了预测,是更准确的。
但是,词袋语言模型没有考虑到分词的顺序带来的影响,例如:0 1 2 -> 3,将0 1 2装入词袋、张量求前缀和的结果和 0 2 1、 1 0 2、 1 2 0等等没有区别,没有办法区分分词的顺序。

在此基础上,如果考虑分词间的前后次序对预测的影响,有:
h ( x t ) = C e l l ( h ( x ( t − 1 ) + x t ) h(x_t) = Cell(h(x(t-1) + x_t) h(xt)=Cell(h(x(t1)+xt)
这样类似于二叉树结构顺序输入的模型,输入的不同次序带来的结果也不同。
在这里插入图片描述
如上图所示的二叉树结构,输入X_0 、X_1和X_2次序的不同,对于每层的影响也不同。这样就有效的保留了原句中分词间次序对于预测的影响。

RNN模型的搭建

#RNNLM.py
#encoding: utf-8
import torch
from torch import nn

class RNNCell(nn.Module):
    def __init__(self, isize, hsize, osize, dropout, **kwargs):
    
        super(RNNCell, self,).__init__()   ##调用父类的初始化函数
        self.net = nn.Sequential(nn.Linear(isize + osize, hsize),
        nn.ReLU(inplace=True), #设置relu激活函数,inplace=True在原始张量上进行
        nn.Dropout(p=dropout, inplace=False),#设置丢弃率防止过拟合,同时创建一个新的张量
        nn.Linear(hsize, osize, bias=False))       
    
    # x_t:(bsize, isize)
    def forward(self, x_t, h_p): # x_t为当前词向量,h_p是h_x(t-1)即上一步的结果
        
        return self.net(torch.cat([h_p, x_t], dim=-1))
    
    
    
class RNNLayer(nn.Module):
    def __init__(self, isize, hsize, dropout,norm_residual=True, **kwargs):
        super(RNNLayer, self,).__init__()   ##调用父类的初始化函数
        self.init_hx = nn.Parameter(torch.zeros(1, isize)) #h_0即最初初始化状态
        #初始化时第一维加1维,便于后续的拼接
        self.cell = RNNCell(isize, hsize, isize, dropout)
        self.drop = nn.Dropout(p=dropout, inplace=True)
        self.normer = nn.LayerNorm(isize) #做归一化
        self.norm_residual = norm_residual #设置变量存储做判断
        
    # input: (bsize, seql-1, isize)
    def forward(self, input, hx=None):#hx是None说明在训练,hx不是None是Tensor说明在解码
                                      #解码时候就用前一步的hx隐状态
        
        _ = self.normer(input) #稳定之后的结果 
        bsize = input.size(0)   #存一下bsize
        _hx = hx if isinstance(hx, torch.Tensor) else self.init_hx.expand(bsize, -1)
        #如果hx已经是一个张量不是None说明已开始计算,否则用初始化的值
        #expand函数将init_hx:(1,isize) -> (bsize, isize),设置参数为-1表示大小保留原状
        rs = []
        for xu in input.unbind(1): #将input在第一维展开 xu:(bsize, isize)
            _hx = self.cell(xu, _hx) #xu为新分词,hx为前一步隐状态
            rs.append(_hx)
            
        rs = (_ if self.norm_residual else input) + self.drop(torch.stack(rs, dim=1))
        #将rs在指定维度拼起来:rs:(bsize, seq-1, isize)
        #如果参数初始化做的好,就用LayerNorm后的值,否则用原始值
        if hx is None:
            return rs
        else:
            return (rs, _hx)   

class NNLM(nn.Module):
    
    def __init__(self, vcb_size, isize, hsize, dropout,
    nlayer, bindemb=True, **kwargs): #有多少个词,就分多少类,类别数为vcb_size
        super(NNLM, self).__init__()
        self.emb = nn.Embedding(vcb_size, isize,
        padding_idx=0)                #<pad>的索引为0
        #self.comp = nn.Linear(ctx_len * isize, isize, bias=False) #将4个词的向量降维为isize
        self.drop = nn.Dropout(p=dropout, inplace=True) #embedding后dropout
        self.nets = nn.Sequential(*[RNNLayer(isize, hsize, dropout)
        for _ in range(nlayer)])
        self.classifier = nn.Linear(isize, vcb_size)
        if bindemb:
            self.classifier.weight = self.emb.weight#将emb的权重赋给分类器
        self.normer = nn.LayerNorm(isize)
        self.out_normer = nn.LayerNorm(isize)
                
    # input: (bsize, seql-1) 句数,句长-1 由于最后一个词是预测不作为输入
    def forward(self, input):
        
        out = self.emb(input)
        # out: (bsize, seql-1, isize)   
        out = self.drop(out) 
        out = self.normer(out) #使用归一化,使模长均匀
        out = self.out_normer(self.nets(out))
        return self.classifier(out) #分类产生参数

RNN模型的训练

#RNNtrain.py
#encoding: utf-8

import torch
from torch import nn
from RNNLM import NNLM #导入模型
from h5py import File as h5File #读训练数据
from math import sqrt
from random import shuffle #使输入数据乱序,使模型更均衡
from lrsch import SqrtDecayLR
from tqdm import tqdm

train_data = "train.h5"#之前已经张量转文本的h5文件
isize = 64              
hsize = isize * 2       #设置初始参数
dropout = 0.3           #设置丢弃率
nlayer = 4              #设置层数
gpu_id = -1             #设置是否使用gpu
lr = 1e-3               #设置初始学习率
max_run = 8           #设置训练轮数

nreport = 5000          #每训练5000个batch打印一次
tokens_optm = 25000     #设置更新参数的词数阈值

def init_model_parameters(modin): #初始化模型参数
    with torch.no_grad():         #不是训练不用求导
        for para in modin.parameters():
            if para.dim() > 1:          #若维度大于1,说明是权重参数
                _ = 1.0 / sqrt(para.size(-1))
                para.uniform_(-_,_)     #均匀分布初始化
        for _m in modin.modules():      #遍历所有小模型
            if isinstance(_m, nn.Linear):#如果小模型是linear类型
                if _m.bias is not None: #初始化bias
                    _m.bias.zero_()
                elif isinstance(_m, nn.LayerNorm):#初始化LayerNorm参数
                    _m.weight.fill_(1.0)
                    _m.bias.zero_()
                elif isinstance(_m, nn.Embedding): #如果是嵌入层则进入初始化权重矩阵,将<pad>的向量初始化为零向量
                    if _m.padding_idx >= 0:
                        _m.weight[_m.padding_idx].zero_()
    return modin

def train(train_data, tl, model, lossf, optm, cuda_device,
 nreport=nreport, tokens_optm=tokens_optm):#nreport为每训练一部分词打一次epoch
    
    model.train() #设置模型在训练的模式
    src_grp = train_data["src"] #从输入数据中取出句子
    _l = 0.0  #_l用来存当前loss
    _t = 0    #_t用来存句子数
    _lb = 0.0
    _tb = 0
    _tom = 0
    
    for _i, _id in tqdm(enumerate(tl, 1)):
        seq_batch = torch.from_numpy(src_grp[_id][()])
        #seq_batch:[bsize, seql]
        _seqlen = seq_batch.size(-1)  #取出每个batch的句长

        if cuda_device is not None:
            seq_batch = seq_batch.to(cuda_device, non_blocking=True)
             #将数据放在同一gpu上
        seq_batch = seq_batch.long()   #数据转换为long类型
        seq_i = seq_batch.narrow(1, 0, _seqlen - 1) #训练数据读取前seql-1的数据
        #seq_i:[bsize, seql-1]
        seq_o = seq_batch.narrow(1, 1, _seqlen - 1) #预测数据读取后seql-1的数据做标签
        #seq_o:[bsize, seql-1]
        
        
        out = model(seq_i)          #获得模型结果
        #out: {bsize, seql-1, vcb_size} vcb_size即预测类别数
        
        loss = lossf(out.view(-1, out.size(-1)), seq_o.contiguous().view(-1)) 
        #转换out维度为[bsize*(seql-1),vcb_size],seq_o:[bsize*(seql-1)]
        _lossv = loss.item()
        _l += _lossv        #整个训练集的loss
        _lb += _lossv       #每个batch的loss
        _n = seq_o.ne(0).int().sum().item() #seq_o中不是<pad>的位置的数量
        _t += _n            #整个训练集的分词数
        _tb += _n           #每个batch的分词数
        _tom += _n
        loss.backward()                 #反向传播求导
        if _tom > tokens_optm:          #当词数大于时更新参数
            optm.step()                     #参数的更新
            optm.zero_grad(set_to_none=True)#参数更新后清空梯度
            _tom = 0
        if _i % nreport == 0:   #每训练5000个batch打印一次
            print("Average loss over %d tokens: %.2f"%(_tb, _lb/_tb))
            _lb = 0.0
            _tb = 0
            save_model(model, "checkpoint.rnn.pt") #暂存检查点模型
            
        
    return _l / _t #返回总的loss


def save_model(modin, fname): #保存模型所有内容 权重、偏移、优化
    
    torch.save({name: para.cpu() for name, para in
    model.named_parameters()}, fname)

t_data = h5File(train_data, "r")#以读的方式打开训练数据

vcb_size = t_data["nword"][()].tolist()[0] #将返回的numpy的ndarray转为list
#在我们的h5文件中存储了nword: 总词数

model = NNLM(vcb_size, isize, hsize, dropout, nlayer)
model = init_model_parameters(model) #在cpu上初始化模型
lossf = nn.CrossEntropyLoss(reduction='sum', ignore_index=0,
label_smoothing=0.1)
#设置ignore_index=0,即忽略<pad>的影响

if (gpu_id >= 0) and torch.cuda.is_available(): #如果使用gpu且设备支持cuda
    cuda_device = torch.device("cuda", gpu_id)  #配置gpu
    torch.set_default_device(cuda_device)
else:
    cuda_device = None

if cuda_device is not None:                     #如果要用gpu
    model.to(cuda_device)                       #将模型和损失函数放在gpu上
    lossf.to(cuda_device)

optm = torch.optim.Adam(model.parameters(), lr=lr, 
betas=(0.9, 0.98), eps=1e-08)
#使用model.parameters()返回模型所有参数,
lrm = SqrtDecayLR(optm, lr) #将优化器和初始学习率传入

tl = [str(_) for _ in range(t_data["ndata"][()].item())] #获得字符串构成的训练数据的list

#save_model(model, "eva.rnn.pt")
for i in range(1, max_run + 1):
    shuffle(tl)         #使数据乱序
    _tloss = train(t_data, tl, model, lossf, optm,
    cuda_device)  #获取每轮训练的损失
    print("Epoch %d: train loss %.2f"%(i, _tloss)) #打印日志
    save_model(model, "eva.rnn.pt")
    lrm.step() #每轮训练后更新学习率
    
t_data.close()

在命令行输入:

:~/nlp/lm$ python RNNtrain.py 

RNN模型的解码与预测

模型的解码

#RNNLM.py
#encoding: utf-8
 # input: (bsize, seql)
    def decode(self, input, maxlen=50): #设置最多生成50个分词
        
        rs = [input]
        bsize =input.size(0)
     
        hxd = {} #存每一层产生的hx
        out = self.normer(self.drop(self.emb(input)))
        # out:(bsize, seql, isize)
        for i, layer in enumerate(self.nets):
            out, hxd[i] = layer(out, hx=hxd.get(i, "init")) #手动遍历每一层
            #得到了每层的结果rs以及隐状态hxd[i]
        # out:(bsize, seql, isize)
        out = self.classifier(self.out_normer(out.narrow(
        1, out.size(1) - 1, 1))).argmax(-1)
        # narrow:(bsize, 1, isize) -> (bsize, 1, vcb_size) 
        # -> (bsize, 1)
        rs.append(out)
        done_trans = out.squeeze(1).eq(2)   #记录是否完成生成
        # done_trans:(bsize)
        
        if not done_trans.all().item():
            for i in range(maxlen - 1): #已经生成一个词并添加到rs中了
                
                out = self.normer(self.drop(self.emb(out)))
                # out:(bsize, 1, isize)
                for _, layer in enumerate(self.nets): #将前一层结果给下一层做输入
                    out, hxd[_] = layer(out, hx=hxd[_])
                    #将之前的隐状态输入生成下一层的隐状态
                
                out = self.classifier(self.out_normer(out)).argmax(-1) #最后的输出预测下一个分词
                # out:(bsize, 1, vcb_size) -> (bsize, 1)
                rs.append(out) #将新预测分词添加到结果

                done_trans |= out.squeeze(1).eq(2) 
                if done_trans.all().item(): #当全都为True,说明此batch中所有句子预测都为<eos>,即解码完成
                    break
        
        return torch.cat(rs, dim=1)

模型的预测

预测脚本与前面的词袋模型脚本基本一样,我们在命令行中输入:

:~/nlp/lm$ python RNNpredict.py test.h5 zh.vcb pred.txt checkpoint.rnn.pt 
:~/nlp/lm$ less pred.txt 

在这里插入图片描述
我们可以看到经过若干个epoch训练出的rnn模型对于文本的预测续写。

LSTM模型的搭建

在这里插入图片描述

但是,由于上图这样的结构,h_3是由h_2和X_2得出,而h_2是由h_1和X_1得出。那么对于h_3来说,X_2的影响就要比X_1大很多。于是在进行预测时,句末的分词对预测的影响就要大于句子中间或者开头的分词。
而在实际句子中,

回顾 安理会 主席 以 安理会 名义 在 1994 年 4 月 7 日 发表 的 声明 ( S / PRST / 1994 / 16 ) 和 在 1994 年 4 月 30 日 发表 的 声明 ( S / PRST / 1994 / 21 ) ,

上句中“声明”的预测很依赖于动词“发表”,这符合我们的模型的偏好即句末的分词影响更大。但是像本句句尾的“1994”是该年发表声明的时间,显然前面的“1994”对它影响更大,而不是句末的分词。
这就不符合我们模型的偏好,因此我们需要重新设计cell,使得模型的偏好更均衡,而不是句末的分词影响最大。

sigmoid激活函数

S i g m o i d Sigmoid Sigmoid 函数的公式如下所示,其可以将神经网络的输出映射到(0,1)之中,公式如下所示。
s i g m o i d ( x ) = 1 1 + e − x sigmoid(x)=\frac{1}{1+e^{-x}} sigmoid(x)=1+ex1
函数图像如下:
在这里插入图片描述
如图上图所示,当输入的值趋于正无穷或负无穷时,梯度会趋近零,神经网络学习不到特征,从而导致深度神经网络无法进行训练。

>>> import torch
>>> a=torch.randn(4,5)
>>> a.sigmoid()
tensor([[0.4421, 0.6359, 0.5781, 0.4000, 0.4741],
        [0.4456, 0.4710, 0.0643, 0.3690, 0.4005],
        [0.4287, 0.7616, 0.6746, 0.1963, 0.6259],
        [0.4268, 0.8297, 0.4883, 0.4696, 0.6682]])
>>> b=a.exp()
>>> b/(1+b)
tensor([[0.4421, 0.6359, 0.5781, 0.4000, 0.4741],
        [0.4456, 0.4710, 0.0643, 0.3690, 0.4005],
        [0.4287, 0.7616, 0.6746, 0.1963, 0.6259],
        [0.4268, 0.8297, 0.4883, 0.4696, 0.6682]])

我们利用sigmoid函数来使得学习的权重进行变化,通过0或1的变化有助于更新或忘记信息。
我们通过门来控制变化,要么是1则记住,要么是0则忘掉。因记忆能力有限,记住重要的,忘记无关紧要的。
LSTM的基本结构如下:

h t , c t = L S T M C e l l ( x t , h t − 1 , c t − 1 ) h_t, c_t = LSTMCell(x_t, h_{t-1}, c_{t-1}) htct=LSTMCell(xt,ht1,ct1)
h = x t ∣ h t − 1 _h = x_t | h_{t-1} h=xtht1
h i d d e n = a c t ( L i n e a r ( h ) ) hidden=act(Linear(_h)) hidden=act(Linear(h))
i g a t e = s i g m o i d ( L i n e a r ( h ) ) igate=sigmoid(Linear(_h)) igate=sigmoid(Linear(h))
o g a t e = s i g m o i d ( L i n e a r ( h ) ) ogate=sigmoid(Linear(_h)) ogate=sigmoid(Linear(h))
f g a t e = s i g m o i d ( L i n e a r ( h ) ) fgate=sigmoid(Linear(_h)) fgate=sigmoid(Linear(h))
c t = c t − 1 ∗ f g a t e + h i d d e n ∗ i g a t e c_t=c_{t-1}*fgate+hidden*igate ct=ct1fgate+hiddenigate
h t = c t ∗ o g a t e h_t=c_t*ogate ht=ctogate

#LSTMLM.py
#encoding: utf-8
import torch
from torch import nn

class LSTMCell(nn.Module):
    def __init__(self, isize, osize, dropout, **kwargs):
    
        super(LSTMCell, self,).__init__()   
        self.net = nn.Linear(isize + osize, 4 * osize) #4*osize后分块,分别得到hidden、igate、ogate和fgate
        self.drop = nn.Dropout(p=dropout, inplace=False)
        
    def forward(self, x_t, h_p):
        
        hx, cx = h_p
        out = self.net(torch.cat([hx, x_t], dim=-1))#将h_x和x_t向量拼接
    
        # out:(bsize, 4, osize)
        out = out.view(out.size(0), 4, -1)
        hidden = out.select(1, 0).relu() #取out第1维的第0个张量过relu激活函数
        igate, fgate, ogate = out.narrow(1, 1, 3).sigmoid().unbind(1) 
        #取out第1维的1,2,3个张量过sigmoid激活函数再按照第一维拆分
        #i/f/o gate:(bsize, 1, osize) hidden(bsize, 1, osize)
        cx = (cx * fgate).addcmul(hidden, igate) #cx*fgate+hidden*igate
        hx = cx * ogate
        
        return hx, cx #hx是本层输出,cx是向后传的cell
        
class LSTMLayer(nn.Module):
    def __init__(self, isize, dropout,norm_residual=True, **kwargs):
        super(LSTMLayer, self,).__init__()   
        self.init_hx = nn.Parameter(torch.zeros(1, isize))
        self.init_cx = nn.Parameter(torch.zeros(1, isize))
        self.cell = LSTMCell(isize, isize, dropout)
        self.drop = nn.Dropout(p=dropout, inplace=True)
        self.normer = nn.LayerNorm(isize) #做归一化
        self.norm_residual = norm_residual #设置变量存储做判断
        
    # input: (bsize, seql-1, isize)
    def forward(self, input, hx=None): #hx若不为None则为(hx, cx)
        
        _ = self.normer(input) #稳定之后的结果 
        bsize = input.size(0)
        _hx = hx if isinstance(hx, (tuple, list)) else (self.
        init_hx.expand(bsize, -1), self.init_cx.expand(bsize, -1))
        rs = []
        for xu in input.unbind(1):
            _hx = self.cell(xu, _hx)
            rs.append(_hx[0]) #只添加_hx中的hx
        rs = (_ if self.norm_residual else input) + self.drop(torch.stack(rs, dim=1))
        
        if hx is None:
            return rs
        else:
            return (rs, _hx)
        #如果参数初始化做的好,就用LayerNorm后的值,否则用原始值

class NNLM(nn.Module):
    
    def __init__(self, vcb_size, isize, hsize, dropout,
    nlayer, bindemb=True, **kwargs): #有多少个词,就分多少类,类别数为vcb_size
        super(NNLM, self).__init__()
        self.emb = nn.Embedding(vcb_size, isize,
        padding_idx=0)                #<pad>的索引为0
        #self.comp = nn.Linear(ctx_len * isize, isize, bias=False) #将4个词的向量降维为isize
        self.drop = nn.Dropout(p=dropout, inplace=True) #embedding后dropout
        self.nets = nn.Sequential(*[LSTMLayer(isize, dropout)
        for _ in range(nlayer)])
        self.classifier = nn.Linear(isize, vcb_size)
        if bindemb:
            self.classifier.weight = self.emb.weight#将emb的权重赋给分类器
        self.normer = nn.LayerNorm(isize)
        self.out_normer = nn.LayerNorm(isize)
        
        
    # input: (bsize, seql-1) 句数,句长-1 由于最后一个词是预测不作为输入
    def forward(self, input):
        
        out = self.emb(input)
        # out: (bsize, seql-1, isize)   
        out = self.drop(out) 
        out = self.normer(out) #使用归一化,使模长均匀
        out = self.out_normer(self.nets(out))
        return self.classifier(out) #分类产生参数
    
    # input: (bsize, seql)
    def decode(self, input, maxlen=50): #设置最多生成50个分词
        
        rs = [input]
        bsize =input.size(0)
     
        hxd = {}
        out = self.normer(self.drop(self.emb(input)))
        # out:(bsize, seql, isize)
        for i, layer in enumerate(self.nets):
            out, hxd[i] = layer(out, hx=hxd.get(i, "init"))
        out = self.classifier(self.out_normer(out.narrow(
        1, out.size(1) - 1, 1))).argmax(-1)
        # narrow:(bsize, 1, isize) -> (bsize, 1, vcb_size) 
        # -> (bsize, 1)
        rs.append(out)
        done_trans = out.squeeze(1).eq(2)   #记录是否完成生成
        # done_trans:(bsize)
        
        if not done_trans.all().item():
            for i in range(maxlen - 1):
                
                out = self.normer(self.drop(self.emb(out)))
                # out:(bsize, 1, isize)
                for _, layer in enumerate(self.nets):
                    out, hxd[_] = layer(out, hx=hxd[_])
                out = self.classifier(self.out_normer(out)).argmax(-1)
                # out:(bsize, 1, vcb_size) -> (bsize, 1)
                rs.append(out)

                done_trans |= out.squeeze(1).eq(2) 
                if done_trans.all().item(): #当全都为True,说明此batch中所有句子预测都为<eos>,即解码完成
                    break
        
        return torch.cat(rs, dim=1)

LSTM模型的预测

我们可以看出,LSTM的解码与RNN一样,只是在cell的设计上不同,训练脚本与预测脚本都是相同的,我们在命令行输入:

:~/nlp/lm$ python LSTMpredict.py test.h5 zh.vcb pred.txt checkpoint.lstm.pt 
:~/nlp/lm$ less pred.txt 

在这里插入图片描述

相关推荐

  1. Linux下深度学习虚拟环境模型训练

    2024-07-17 07:58:01       31 阅读
  2. NLP训练模型

    2024-07-17 07:58:01       29 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-17 07:58:01       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-17 07:58:01       72 阅读
  3. 在Django里面运行非项目文件

    2024-07-17 07:58:01       58 阅读
  4. Python语言-面向对象

    2024-07-17 07:58:01       69 阅读

热门阅读

  1. React基础学习-Day06

    2024-07-17 07:58:01       22 阅读
  2. Oracle(6)什么是重做日志文件(Redo Log File)?

    2024-07-17 07:58:01       17 阅读
  3. el-table template slot-scope=“scope“ 不显示内容

    2024-07-17 07:58:01       25 阅读
  4. PICO,迷途VR?

    2024-07-17 07:58:01       24 阅读
  5. ubuntu 18 cuda 11.8 安装 vllm

    2024-07-17 07:58:01       23 阅读
  6. LLM大语言模型研究方向总结剖析

    2024-07-17 07:58:01       21 阅读
  7. 如何在SpringCloud中优雅实现服务注册与发现

    2024-07-17 07:58:01       22 阅读