2.3-基于RNN的语言模型的学习与评价

1引言

所有代码位于:https://1drv.ms/f/s!AvF6gzVaw0cNjpx9BAtQYGAHFT3gZA?e=jycNfH;

  1. 前面实现了各个Time xxx层;这样一来,我们就可以将RNNLM表示为下图:

    在这里插入图片描述

2 RNNLM的代码实现

代码位于:RNN_model/RNNLM.py

2.1初始化

  1. 关于权重初始化的初始值选择问题,这个系列的书中有提到:https://1drv.ms/b/s!AvF6gzVaw0cNjqRJgUyoQUQEFg7P2w?e=6pPag5;
  1. 初始化主要就是初始化各个层使用的权重以及生成相应的Time xxx层;

  2. 其中初始化权重需要注意的是:

    1. Embedding层的初始化权重的分布是标准差为0.01的高斯分布,没有使用标准差为0的高斯分布;因为标准差为0的高斯分布在后续神经网络的前向计算过程中产生有分布偏向的数据,此时若使用如sigmoid函数这种S型函数,会导致导数值趋近于0,即梯度变小,存在梯度消失的风险;如下图所示:

      在这里插入图片描述

    2. 而当使用标准差为0.01的高斯分布时,是下图这样的,数据不再具有偏向性;

      在这里插入图片描述

    3. RNN层和Affine层权重的初始化使用的是Xavier初始值;这个初始值适用于线性函数;RNN层中的tanh函数左右对称,且中央附近可以视作线性函数,所以适合使用Xavier初始值;Affine层是线性变换,自然适用;

      1. 本书中简单起见,只根据当前结点上一个层的节点数来进行Xavier初始化;如下图所示:

        在这里插入图片描述

      2. 在上一层的节点数是 n 的情况下,使用标准差为 1 n \frac{1}{\sqrt{n}} n 1的分布作为Xavier初始值;例如 W x \boldsymbol{W}_x Wx维度为(D,H),因此上一个层的节点数就是D;所以 1 n \frac{1}{\sqrt{n}} n 1就是 1 D \frac{1}{\sqrt{D}} D 1​;

  3. 再就是生成对应的层;

  4. 初始化代码如下:

    class SimpleRnnlm:
        def __init__(self, vocab_size, wordvec_size, hidden_size):
            V, D, H = vocab_size, wordvec_size, hidden_size
            rn = np.random.randn
    
            # 初始化权重
            embed_W = (rn(V, D) / 100).astype('f') # 标准差0.01消除数据分布的偏向性
            rnn_Wx = (rn(D, H) / np.sqrt(D)).astype('f') # 使用Xavier初始值
            rnn_Wh = (rn(H, H) / np.sqrt(H)).astype('f') # 使用Xavier初始值
            rnn_b = np.zeros(H).astype('f')
            affine_W = (rn(H, V) / np.sqrt(H)).astype('f') # 使用Xavier初始值
            affine_b = np.zeros(V).astype('f')
    
            # 生成层
            self.layers = [
                TimeEmbedding(embed_W),
                TimeRNN(rnn_Wx, rnn_Wh, rnn_b, stateful=True), # stateful=True:前向传播信息流不中断
                TimeAffine(affine_W, affine_b)
            ]
            self.loss_layer = TimeSoftmaxWithLoss()
            self.rnn_layer = self.layers[1]
    
            # 将所有的权重和梯度整理到列表中
            self.params, self.grads = [], []
            for layer in self.layers:
                self.params += layer.params
                self.grads += layer.grads
    

2.2前向和反向传播

  1. 前面以及实现了每一个Time xxx层,因此RNNLM的前向传播就是依次调用TEmbedding、TRNN、TAffine、TSoftmaxwithLoss层即可得到模型损失;

  2. 前向传播的代码如下:

    def forward(self, xs, ts):
        '''
        @param xs: 输入数据,即单词ID;形状:(N,T)
        @param ts: 监督标签;形状:(N,T)时不是独热编码形式;
        @return loss: 损失函数值'''
        for layer in self.layers:
            xs = layer.forward(xs)
        # 传入损失函数的xs维度为(N,T,V);
        loss = self.loss_layer.forward(xs, ts)
        return loss
    
  3. 反向传播则是反过来调用各个层的backward函数即可;代码如下:

    def backward(self, dout = 1):
        dout = self.loss_layer.backward(dout)
        # 此时dout的维度为(N, T, V)
        for layer in reversed(self.layers):
            dout = layer.backward(dout)
        return dout
    

3语言模型的一个评价指标-困惑度

困惑度(perplexity)常被用作评价语言模型的预测性能的指标。

  1. 语言模型基于给定的已经出现的单词(信息)输出将要出现的单词的概率分布

3.1数据量为一时困惑度的计算

以you say goodbye and i say hello.为例。

这里数据量为一表示输入一个单词;不是输入一个长度为T的时序数据(或句子);

  1. 现有两个模型,传入单词you,模型输出如下的预测结果:

    在这里插入图片描述

  2. 分别取倒数,得到困惑度:模型一是1.25;模型二是5;很显然0.8的概率对应的模型更好;

  3. 因此可以说明:模型预测概率越大,困惑度越小,模型性能越好

    1. 当模型预测概率为1时,困惑度为1;其余时候困惑度都大于1;
    2. 困惑度可以理解为分叉度,即可供选择的候选单词个数;困惑度1.25说明候选的单词基本上只有一个;而困惑度5表示候选的有5个,这当然不是我们希望看到的;

3.2数据量为多个的时候困惑度的计算

  1. 先放公式:
    L = − 1 N ∑ n ∑ k t n k log ⁡ y n k (1) L=-\frac1N\sum_n\sum_kt_{nk}\log y_{nk} \tag{1} L=N1nktnklogynk(1)

    困惑度 = e L (2) 困惑度=e^L \tag{2} 困惑度=eL(2)

    这里,假设数据量为 N N N个。 t n \boldsymbol{t}_n tn是 one-hot 向量形式的正确解标签, t n k t_{nk} tnk表示第 n n n个数据的第 k k k个值, y n k y_{nk} ynk表示概率分布(神经网络中的 Softmax 的输出); L L L是神经网络的损失,即前面说的交叉熵损失。

  2. 接下来,我们假设 N = 2 N=2 N=2;这表示我们总共计算了两个单词的损失之和作为 L L L

  3. 由于独热编码向量中只有一个元素为1,因此我们可以将(1)和(2)式变成下式:
    e L = e − 1 2 ( t 11 l o g y 11 + t 21 l o g y 21 ) (3) e^L=e^{-\frac{1}{2}(t_{11}logy_{11}+t_{21}logy_{21})} \tag{3} eL=e21(t11logy11+t21logy21)(3)
    我们假设 t 11 = 1 t_{11}=1 t11=1 t 21 = 1 t_{21}=1 t21=1为两个单词的真是单词ID对应的独热编码向量中的值;其余的 t t t值都是0,就可以省掉了;且假设对数的底数是 e e e

  4. 更进一步,有:
    e L = e − 1 2 l o g y 11 + e − 1 2 l o g y 21 = y 11 − 1 2 + y 21 − 1 2 = 1 y 11 1 y 21 (4) \begin{aligned} e^L&=e^{-\frac{1}{2}logy_{11}}+e^{-\frac{1}{2}logy_{21}}\\ &=y_{11}^{-\frac{1}{2}}+y_{21}^{-\frac{1}{2}}\\ &=\sqrt{\frac{1}{y_{11}}\frac{1}{y_{21}}} \end{aligned} \tag{4} eL=e21logy11+e21logy21=y1121+y2121=y111y211 (4)

  5. 在信息论领域,困惑度也称为“平均分叉度”。这可以解释为,数据量为 1 时的分叉度是数据量为N时的分叉度的**平均值**:

    1. 关键是平均分叉度这个词;
    2. 数据量为一时不存在平均;
    3. 数据量大于1时,由式(4)可以看到,这里的平均就是几何平均;根号下是每个单词预测概率的倒数;相加是算术平均,相乘是几何平均;因此开了根号;
    4. 式(1)(2)推导一下就是式(4),那就好理解了。

4 基于PTB数据集的RNNLM的学习的实现

不过这里仅使用 PTB 数据集 (训练数据)的前 1000 个单词。这是因为在本节实现的 RNNLM 中,即便 使用所有的训练数据,也得不出好的结果。后面将对它进行改进。

代码位于:RNN_model/RNNLM.py

  1. 数据的读取就不说了;详见代码;

  2. 数据的准备(使得数据能够输入到RNNLM中):由于RNN是根据前面的单词预测下一个单词,得有下一个单词让模型预测,因此这里输入要去掉最后一个单词:

    # 构建输入和标签
    xs = corpus[:-1]  # 输入
    ts = corpus[1:]  # 输出(监督标签)
    data_size = len(xs)
    
  3. 最初我们在读取PTB数据集时,是把所有的句子都连在一起读进来的;现在我们用的时候因为是mini-batch,那么batch_size就代表着句子数;这里的实际输入corpus是连续的999个单词;我们根据batch_sizecorpus进行划分,划分成batch_size个句子,每个句子的长度是99;由于没法整除,最后一个句子的长度大于99:

    # 计算读入mini-batch的各笔样本数据的开始位置
    jump = (corpus_size - 1) // batch_size # 99
    offsets = [i * jump for i in range(batch_size)] # 列表中每个元素表示每个句子的第一个单词
    
  4. 句子分好之后,就进入到每个epoch的每个step进行学习;关键是读取对应的数据的过程需要理解一下;都在注释中;代码如下:

    for epoch in range(max_epoch):
        for iter in range(max_iters):
            # 获取mini-batch
            # 先初始化
            batch_x = np.empty((batch_size, time_size), dtype='i') # (10,5)
            batch_t = np.empty((batch_size, time_size), dtype='i') # (10,5)
            # 然后从xs和ts选择相应的数据
            for t in range(time_size):
                for i, offset in enumerate(offsets):
                    # 这两个循环嵌套实现的就是:
                    # 为这个批次里面每一个句子的每个t时刻选择输入和标签
                    # offset决定哪个句子;time_idx决定这次取的时间步
                    batch_x[i, t] = xs[(offset + time_idx) % data_size] # % data_size用来放置句子长度不够;但其实这里用不着
                    batch_t[i, t] = ts[(offset + time_idx) % data_size]
                time_idx += 1 # 一直不清空;
    
  5. 获取到一个step的数据之后,就用之前实现的RNNLM的前向传播和反向传播方法计算损失和梯度,并调用优化器进行参数更新:

    # 计算梯度,更新参数
    loss = model.forward(batch_x, batch_t)
    model.backward()
    optimizer.update(model.params, model.grads)
    total_loss += loss
    loss_count += 1
    
  6. 一个epoch计算完之后,计算一下这个epoch的困惑度,并保存下来:

    # 各个epoch的困惑度评价
    ppl = np.exp(total_loss / loss_count)
    print('| epoch %d | perplexity %.2f'
        % (epoch+1, ppl))
    ppl_list.append(float(ppl))
    total_loss, loss_count = 0, 0
    
  7. 然后可以绘制一下困惑度的变化情况折线图:

    # 绘制ppl_list折线图
    import matplotlib.pyplot as plt
    x = np.arange(len(ppl_list))
    plt.plot(x, ppl_list, label='train')
    # 保存图片
    plt.savefig('ppl.png')
    

    在这里插入图片描述

  8. 接着,将上述过程进行封装:

    1. 训练入口位于:RNN_model/RNNLM_train.py
    2. 训练类位于:RNN_model/rnnTrainer.py
  • 13
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值