『论文复现系列』1.NNLM

354 阅读12分钟

一个神经概率语言模型

论文 | A Neural Probabilistic Language Model

链接 | proceedings.neurips.cc/paper/2000/…

作者 | Yoshua Bengio, Réjean Ducharme, Pascal Vincent

发布时间 | 2003

由于比较喜欢做思维导图,所以就采用思维导图来梳理自己的精读思路了,需要SVG格式的朋友可以联系我获取

论文解读部分

A Neural Probabilistic Language Model

论文十问

Q1:论文试图解决什么问题?

  • 找到一种比统计学习更好的办法,学习单词序列的联合概率函数

Q2:这是否是一个新的问题?

  • 不是,是为了改善原有的n-gram模型,减少训练时长,破解维度诅咒,得到更多上下文的信息

Q3:这篇文章要验证一个什么科学假设?

  • 1、借助单词的分布式表示,可以更好的对单词的相似性进行建模
  • 2、通过训练单词向量和神经网络,虽然耗费资源和时间较多,但可行,能得到一个相对更好的结果

Q4:有哪些相关研究?如何归类?谁是这一课题在领域内值得关注的研究员?

  • 在下文阅读过程中进行了归纳

Q5:论文中提到的解决方案之关键是什么?

  • 1、与每个词关联的是一个分布式的单词特征向量
  • 2、用序列中的每个单词特征向量来表示该序列的联合概率函数
  • 3、使用神经网络学习单词特征向量和概率函数的参数

Q6:论文中的实验是如何设计的?

  • 选择了两个数据集训练模型,并用困惑度当做指标对不同模型实验结果进行比较,具体在实验结果部分

Q7:用于定量评估的数据集是什么?代码有没有开源?

  • 数据集。 布朗语料库 美联社(AP)新闻 本文的代码未公开提供

Q8:论文中的实验及结果有没有很好地支持需要验证的科学假设?

  • 有,单词的分布式和大规模的NN网络确实有助于学习一个更好的语言模型

    • NN与n-grams混合,效果提升
    • 采用RNN等时序网络,考虑更多上下文,有提升
    • 隐藏单元数量越多越有帮助
    • 输入输出层直接连接虽然更快收敛,但会导致效果下降

Q9:这篇论文到底有什么贡献?

  • 在本篇文章之前,SOTA LM基于n-gram模型,由于离散建模而遭受高维诅咒,此外,泛化也并不容易

  • 从结果看,本文的贡献有:

    • 验证了大型语料库上训练大规模神经网络可以考虑更长上下文

    • 分布式表示建模单词使得泛化更容易

      • 训练中每个句子会告知模型相邻语义句子指数数量
      • 测试中生词序列只要是相似词组成,特征向量应该分布相近

Q10:下一步呢?有什么工作可以继续深入?

  • 作者提出的观点在未来展望部分,如果说更多的话

    • 初始化

      • word2vec
    • 更深的结构与连接

      • resnet
    • 不同长度句子的训练

      • rnn
    • 词的上下文表示

      • bert

1、引言

N-grams模型

  • 传统语言模型求联合概率分布的代表

  • 语言统计模型可以用该公式表示下一个单词出现的条件概率

    • P^(w1T)=t=1TP^(wtw1t1)\hat{P}\left(w_1^T\right)=\prod_{t=1}^T \hat{P}\left(w_t \mid w_1^{t-1}\right)
      • W_t是第t个单词

      • wij=(wi,wi+1,,wj1,wj)w_i^j=\left(w_i, w_{i+1}, \cdots, w_{j-1}, w_j\right)
  • N-grams利用词序和邻近词在序列中的依赖性构建

    • P^(wtw1t1)P^(wtwtn+1t1)\hat{P}\left(w_t \mid w_1^{t-1}\right) \approx \hat{P}\left(w_t \mid w_{t-n+1}^{t-1}\right)
    • 简化了条件,不用考虑大量单词之间的独立对最终效果的影响

引出问题

  • 问题一:n个单词的新组合未出现在语料库,但不想要对该组合赋值为0,有何解决方法?

    • Katz, 1987

      Estimation of probabilities from sparse data for the language model component of a speech recognizer

      • back-off trigram models
      • 使用更小的context size(使得新组合的个数减少)
    • Jelinek and Mercer, 1980

      Estimation of probabilities from sparse data for the language model component of a speech recognizer

      • smoothed(or interpolated) trigram models
      • 使用插值或者拉普拉斯平滑等方式给予新组合小概率
  • 问题二:模型如何从看到的单词序列得到新的单词序列的?

    • 一个解决办法是去思考问题一中出现的模型的生成模型

      • 生成方法能学习到生成模型,生成方法由数据学习联合概率分布P(X, Y),然后求出条件概率分布P(Y|X)作为预测的模型,即生成模型:

      • P(YX)=P(X,Y)P(X)P(Y \mid X)=\frac{P(X, Y)}{P(X)}
      • 从本质讲,新的序列是通过对训练集中经常出现的短词组(一般取长度为3的单词最佳)进行“粘合”

  • 问题三:如何对传统模型进行进一步提升?

    • 显然除了前两个单词的信息,也有更多的信息可以加强预测的可靠性

    • Goodman, 2001

      A Bit of Progress in Language Modeling

      • 可以考虑超过一两个单词的上下文

      • 可以考虑单词之间的"相似性"

        • 例如"The cat is walking in the bedroom"与"A dog was running in a room"有相似的语义和语法,可能前者的信息能对预测后者产生帮助

符号定义

  • v表示列向量,v’表示其转置
  • A_j表示矩阵A的第j行
  • x.y = x′y

分布式对抗维度诅咒

  • 步骤

    • 1、将一个分布式单词特征向量(R^m维的真实向量)与词表中的每一个单词相关联
    • 2、依据序列中单词的特征向量来表达单词序列的联合概率函数
    • 3、学习单词特征向量和该概率函数的参数。
  • 单词特征向量代表单词的不同方面

2、NNLM模型提出

训练集

  • w1...wT的单词序列(每个单词均属于一个大而有限的词表V)

目标函数:

f(wt,,wtn+1)=P^(wtw1t1)f\left(w_t, \cdots, w_{t-n+1}\right)=\hat{P}\left(w_t \mid w_1^{t-1}\right)
  • 1、构建实数向量C(i)

    • C表示与词汇表中每个单词相关的分布式特征向量

    • 实战中C为|V| × m维的自由参数矩阵

    • C为V中任意向量i的映射向量函数

C(i)RmC(i) \in \mathbb{R}^m
  • 2、用C表示单词的概率函数,g函数则将上下文中单词的特征向量关联起来,并映射为w_t的条件概率向量

    • g可由前馈or循环神经网络or一个参数化函数实现,参数设置为w,总体参数集为θ = (C, ω)
P^(wt=iw1t1)\hat{P}\left(w_t=i \mid w_1^{t-1}\right)
  • 3、f函数则是C映射与g映射的组合

    • 对任意i,映射C函数相同

      • 因为映射C的参数即是特征向量本身
      • |V| × m维的C,则C(i)为第i字的特征向量
f(i,wt1,,wtn+1)=g(i,C(wt1),,C(wtn+1))f\left(i, w_{t-1}, \cdots, w_{t-n+1}\right)=g\left(i, C\left(w_{t-1}\right), \cdots, C\left(w_{t-n+1}\right)\right)
  • 函数图像
    • g是神经网络,C(i)是第i个词的特征向量

训练

  • 一个训练指标:困惑度(perplexity)
1/P^(wtw1t1)的几何平均值1 / \hat{P}\left(w_t \mid w_1^{t-1}\right)的几何平均值
  • 训练函数

    • 核心函数

      • tanh是对每一个输入的X依次运用,W可为0(没有直接连接),X则为所有输入向量C(i)的连接
y=b+Wx+Utanh(d+Hx)y = b +W x +U tanh(d + Hx)
  • X的定义

    • x=(C(wt1)C(wt2)C(wtn+1))x = (C(wt−1),C(wt−2),···,C(wt−n+1))
  • Softmax层

    • P^(wtwt1,wtn+1)=eywtieyi\hat{P}\left(w_t \mid w_{t-1}, \cdots w_{t-n+1}\right)=\frac{e^{y_{w_t}}}{\sum_i e^{y_i}}
  • 损失函数

    • θ = (b, d,W,U, H,C)
L=1Ttlogf(wt,wt1,,wtn+1;θ)+R(θ)L=\frac{1}{T} \sum_t \log f\left(w_t, w_{t-1}, \cdots, w_{t-n+1} ; \theta\right)+R(\theta)
  • 迭代更新

    • 随机梯度上升
      • ε为学习率
θθ+εlogP^(wtwt1,wtn+1)θ\theta \leftarrow \theta+\varepsilon \frac{\partial \log \hat{P}\left(w_t \mid w_{t-1}, \cdots w_{t-n+1}\right)}{\partial \theta}
  • 参数设置

    • 设h为隐藏单元个数
    • 设m为每个单词的相关特征个数
    • 不需要单词特征向量与输出层直连时,矩阵W设置为0
    • 输出偏差b为自由参数(随意设置)
    • 隐层偏向d(每个隐藏单元有一个)
    • 隐层到输出权重矩阵U(|V| × m)
    • 单词特征向量到输出的权重矩阵W(|V| × (n-1)m)
    • 隐藏层权重矩阵H(h × (n-1)m
    • 单词特征矩阵C(|V| × m)
    • 自由度|V|(1 + nm + h) + h(1 + (n−1)m)

约束条件

w1t1的任意取值w_1^{t-1}的任意取值
i=1Vf(i,wt1,,wtn+1)=1\sum_{i=1}^{|V|} f\left(i, w_{t-1}, \cdots, w_{t-n+1}\right)=1 \\
s.t.f>0s.t. f>0

3、实验结果

实验数据集

  • PS:论文中的Parallel Implementation是基于CPU的,现已被GPU取代,因此不再记录

  • 实验基于Brown corpus,共1181041个单词,其中80w作为训练集,20w作为测试集,剩下181041为测试

    • 基于各种英文文本和书籍
    • 共47578个不同词汇
    • 频次小于3的稀有词合并为一个符号,词汇量减少到 |V| = 16383

参数设置

  • 初始学习率εo = 10^−3
  • 迭代函数为
εt=εo1+rt\varepsilon_t=\frac{\varepsilon_o}{1+r t}
  • 启发式参数r设置为10^-8

主要的结论

  • 与n-grams的最佳结果相比,使用神经网络明显可以获得更好的结果,困惑度差异约24%

    • 隐藏单元是有用的(MLP3 vs MLP1和MLP4 vs MLP2)
    • 从2个单词的上下文到4个单词带来了神经网络的改进
    • 神经网络的输出和interpolated trigram混合能减少困惑度
    • 从输入直连到输出,提供了更多的容量,可以更快学习,但推荐所有连接都通过隐层,这样会导致训练时间加长很多,但会形成一个瓶颈,模型泛化能力会更好

实验参数选择

    • 结果表明,MLP10效果为最好,其中mix为是否与interpolated trigram混合,train,valid,test为困惑度,direct为是否存在输入与输出直连,m是MLP中的词特征数,h为隐藏单元数量,c是n-grams中的词类,n为阶数(隐层层数)

4、未来展望

An Energy Minimization Network

Training products of experts by minimizing contrastive divergence

  • 现在只使用了"输入"单词,而没有用"输出"单词(下一个单词)

  • 很多的参数都在输出层扩展

    • 且词与词之间的语义与句法相似性没有利用
  • Hinton, 2000

Out-of-vocabulary words

  • 首先猜测一个词的初始特征向量

    • 通过对出现在同一上下文的其他单词特征向量进行加权凸组合(加权平均)
  • 当真正出现新词时采用上述方式进行模拟特征向量权重,后通过重新归一化所有词(除了新加入的词)

更多

  • 将网络进行分解,用聚类等方式

  • 用树状结构表示条件概率,其中神经网络应用于每个节点,每个节点表示给定上下文的单词类的概率,叶表示给定上下文的单词的概率。

    • Bengio,2002

      New distributed probabilistic language models.

  • 只从输出词子集传播梯度

    • Schwenk and Gauvain, 2002

      Connectionist language modeling for large vocabulary continuous speech recognition

  • 引入先验知识

    • 语义信息(Word Net)
    • 低级语法信息(parts-of-speech)
    • high-level grammatical information
    • 引入更多结构和参数共下行、利用RNN来捕捉更长期上下文的影响,拓展输入窗口的大小以至于包括整个段落
  • 解释神经网络学习到的单词特征表示,例如为何m = 2

  • 需要一个服务于多义词的模型,这个模型只是为每个词分配了连续语义空间中的一个点

5、复现模型结构

基于Paddle

想一键体验Paddle版本可点击这里

核心网络

# NNLM 

import paddle
import paddle.nn as nn
from paddle.nn import Layer

# hyperparameters
m = 2  # dimension of word vector
n_step = 2  # num of preceding words 
h = 10  # hidden size

# model
class NNLM(Layer):
    def __init__(self):
        super(NNLM, self).__init__()
        self.C = nn.Embedding(V, m)
        self.H = paddle.create_parameter(shape=ranH.shape, dtype=str(ranH.numpy().dtype), default_initializer=nn.initializer.Assign(ranH))
        self.d = paddle.create_parameter(shape=zerH.shape, dtype=str(zerH.numpy().dtype), default_initializer=nn.initializer.Assign(zerH))
        self.W = paddle.create_parameter(shape=ranV.shape, dtype=str(ranV.numpy().dtype), default_initializer=nn.initializer.Assign(ranV))
        self.b = paddle.create_parameter(shape=zerV.shape, dtype=str(zerV.numpy().dtype), default_initializer=nn.initializer.Assign(zerV))
        self.U = paddle.create_parameter(shape=ranVH.shape, dtype=str(ranVH.numpy().dtype), default_initializer=nn.initializer.Assign(ranVH))

    def forward(self, input):
            # input -> B x n_step

            x = paddle.reshape(self.C(input), shape=[-1, n_step * m])  # B x (n_step x m)
            # x = self.C(input)

            hidden_out = paddle.tanh(x.matmul(self.H.transpose([1, 0])) + self.d)  # B x h
            
            output = x.matmul(self.W.transpose([1, 0])) + self.b + hidden_out.matmul(self.U.transpose([1, 0]))  # B x V

            return output
            
model = NNLM()
print(model)

运用Demo

import paddle
import paddle.nn as nn
from paddle.nn import Layer
import paddle.optimizer as optimizer
import paddle.io as Data

sentences = ['i like cat', 'i love coffee', 'i hate milk']
sentences_list = " ".join(sentences).split()  # ['i', 'like', 'cat', 'i', 'love'. 'coffee',...]
vocab = list(set(sentences_list))
word2idx = {w: i for i, w in enumerate(vocab)}
idx2word = {i: w for i, w in enumerate(vocab)}

# 7
V = len(vocab)


def make_data(sentences):
    input_data = []
    target_data = []
    for sen in sentences:
        sen = sen.split()  # ['i', 'like', 'cat']
        input_tmp = [word2idx[w] for w in sen[:-1]]
        target_tmp = word2idx[sen[-1]]

        input_data.append(input_tmp)
        target_data.append(target_tmp)
    return input_data, target_data


input_data, target_data = make_data(sentences)
input_data, target_data = paddle.to_tensor(input_data), paddle.to_tensor(target_data)
input_data
dataset = Data.TensorDataset([input_data, target_data])

# loader = Data.DataLoader.from_dataset(dataset, 2, True)
loader = Data.DataLoader(dataset)
# loader

# parameters
m = 2
n_step = 2
h = 10

ranH = paddle.randn([h, n_step * m])
zerH = paddle.zeros([h])
ranV = paddle.randn([V, n_step * m])
zerV = paddle.zeros([V])
ranVH = paddle.randn([V, h])

# model
class NNLM(Layer):
    def __init__(self):
        super(NNLM, self).__init__()
        self.C = nn.Embedding(V, m)
        self.H = paddle.create_parameter(shape=ranH.shape, dtype=str(ranH.numpy().dtype), default_initializer=nn.initializer.Assign(ranH))
        self.d = paddle.create_parameter(shape=zerH.shape, dtype=str(zerH.numpy().dtype), default_initializer=nn.initializer.Assign(zerH))
        self.W = paddle.create_parameter(shape=ranV.shape, dtype=str(ranV.numpy().dtype), default_initializer=nn.initializer.Assign(ranV))
        self.b = paddle.create_parameter(shape=zerV.shape, dtype=str(zerV.numpy().dtype), default_initializer=nn.initializer.Assign(zerV))
        self.U = paddle.create_parameter(shape=ranVH.shape, dtype=str(ranVH.numpy().dtype), default_initializer=nn.initializer.Assign(ranVH))

    def forward(self, input):
            # input -> B x n_step

            x = paddle.reshape(self.C(input), shape=[-1, n_step * m])  # B x (n_step x m)
            # x = self.C(input)

            hidden_out = paddle.tanh(x.matmul(self.H.transpose([1, 0])) + self.d)  # B x h
            
            output = x.matmul(self.W.transpose([1, 0])) + self.b + hidden_out.matmul(self.U.transpose([1, 0]))  # B x V

            return output

model = NNLM()
optimizer = optimizer.Adam(parameters=model.parameters(), learning_rate=1e-3)
criterion = nn.CrossEntropyLoss()

for epoch in range(1, 5000 + 1):
    for batch_x, batch_y in loader:
        optimizer.clear_grad()

        # batch_x -> B x n_step
        # batch_y -> B
        batch_x = batch_x.cuda()
        batch_y = batch_y.cuda()

        pred = model(batch_x)  # B x V

        loss = criterion(pred, batch_y)

        if epoch % 1000 == 0:
            print("epoch:{}, loss:{}".format(epoch, loss.item()))

        loss.backward()
        optimizer.step()


pred = model(input_data.cuda()).max(1, keepdim=True)[1]  # B x 1

# print([idx2word[idx] for idx in pred])

# print([idx2word[idx.item()] for idx in pred.squeeze()])  # ['cat', 'coffee', 'milk']

基于pytorch

核心网络

class NNLM(nn.Module):
    def __init__(self):
        super(NNLM, self).__init__()
        self.C = nn.Embedding(V, m)
        self.H = nn.Parameter(torch.randn(h, n_step * m), requires_grad=True)
        self.d = nn.Parameter(torch.zeros(h), requires_grad=True)
        self.W = nn.Parameter(torch.randn(V, n_step * m), requires_grad=True)
        self.b = nn.Parameter(torch.zeros(V), requires_grad=True)
        self.U = nn.Parameter(torch.randn(V, h), requires_grad=True)

    def forward(self, input):
        # input -> B x n_step

        x = self.C(input).view(-1, n_step * m)  # B x (n_step x m)

        hidden_out = torch.tanh(x.matmul(self.H.transpose(0, 1)) + self.d)  # B x h

        output = x.matmul(self.W.transpose(0, 1)) + self.b + hidden_out.matmul(self.U.transpose(0, 1))  # B x V
        # print(x.shape)
        # print(self.H.transpose(0, 1).shape)
        # print(x.matmul(self.H.transpose(0, 1)).shape)
        # print(self.d.shape)
        # print(output.shape)
        return output

运用Demo

import torch
import torch.nn as nn
import torch.optim as optimizer
import torch.utils.data as Data

sentences = ['i like cat', 'i love coffee', 'i hate milk']
sentences_list = " ".join(sentences).split()  # ['i', 'like', 'cat', 'i', 'love'. 'coffee',...]
vocab = list(set(sentences_list))
word2idx = {w: i for i, w in enumerate(vocab)}
idx2word = {i: w for i, w in enumerate(vocab)}

V = len(vocab)


def make_data(sentences):
    input_data = []
    target_data = []
    for sen in sentences:
        sen = sen.split()  # ['i', 'like', 'cat']
        input_tmp = [word2idx[w] for w in sen[:-1]]
        target_tmp = word2idx[sen[-1]]

        input_data.append(input_tmp)
        target_data.append(target_tmp)
    return input_data, target_data


input_data, target_data = make_data(sentences)
input_data, target_data = torch.LongTensor(input_data), torch.LongTensor(target_data)
dataset = Data.TensorDataset(input_data, target_data)
loader = Data.DataLoader(dataset, 2, True)

# parameters
m = 2
n_step = 2
h = 10


class NNLM(nn.Module):
    def __init__(self):
        super(NNLM, self).__init__()
        self.C = nn.Embedding(V, m)
        self.H = nn.Parameter(torch.randn(h, n_step * m), requires_grad=True)
        self.d = nn.Parameter(torch.zeros(h), requires_grad=True)
        self.W = nn.Parameter(torch.randn(V, n_step * m), requires_grad=True)
        self.b = nn.Parameter(torch.zeros(V), requires_grad=True)
        self.U = nn.Parameter(torch.randn(V, h), requires_grad=True)

    def forward(self, input):
        # input -> B x n_step

        x = self.C(input).view(-1, n_step * m)  # B x (n_step x m)

        hidden_out = torch.tanh(x.matmul(self.H.transpose(0, 1)) + self.d)  # B x h

        output = x.matmul(self.W.transpose(0, 1)) + self.b + hidden_out.matmul(self.U.transpose(0, 1))  # B x V
        # print(x.shape)
        # print(self.H.transpose(0, 1).shape)
        # print(x.matmul(self.H.transpose(0, 1)).shape)
        # print(self.d.shape)
        # print(output.shape)
        return output


model = NNLM().cuda()

optimizer = optimizer.Adam(model.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss()


for epoch in range(1, 5000 + 1):
    for batch_x, batch_y in loader:
        optimizer.zero_grad()

        # batch_x -> B x n_step
        # batch_y -> B
        batch_x = batch_x.cuda()
        batch_y = batch_y.cuda()

        pred = model(batch_x)  # B x V

        loss = criterion(pred, batch_y)

        if epoch % 1000 == 0:
            print("epoch:{}, loss:{}".format(epoch, loss.item()))

        loss.backward()
        optimizer.step()


pred = model(input_data.cuda()).max(1, keepdim=True)[1]  # B x 1

# print([idx2word[idx] for idx in pred])

# print([idx2word[idx.item()] for idx in pred.squeeze()])  # ['cat', 'coffee', 'milk']

6、总结

改进的主要原因

  • 利用已学习的分布式表示,用它自己的武器来对抗维数的诅咒
  • 每个训练句子都将其他句子的组合数量告知模型

尾言

  • 本文为改进统计语言模型打开了一扇门,基于分布式表示的更紧凑、更流畅的表示取代"条件概率表",容纳更多的条件变量
  • 通过付出更多训练时长的方式来改善条件变量数量过多从而导致过拟合

By:jjyaoao