语言模型|n-grams语言模型

语言是概率的。
语言模型(Language Models, LMs)就是要估计一个句子的概率!
概率是衡量语言流畅性的指标。

语言模型经历了规则模型→统计模型→神经网络模型。N-grams模型属于基于统计的语言模型,即通过统计来计算语言的概率。

举个简单的例子

现在有一句话:“我想要养一只小猫”,简便起见,我们把每个字当作一个元素,则这句话有8个元素。现在我们想要计算这句话的概率:

  • **当n=1时,n-grams模型即为unigram。**在这个模型中,我们假设每个词都是独立的,不受其他词的影响,所以句子的概率就是每个词出现的概率的乘积。P(sentence) = P(word1) * P(word2) * P(word3) * … * P(word8)。
  • **当n=2时,n-grams模型即为bigram。**在这个模型中,我们假设每个词的出现都受到前一个词的影响,所以句子的概率就是每个词在给定前一个词的条件下出现的概率的乘积。P(sentence) = P(word1) * P(word2|word1) * P(word3|word2) * … * P(word8|word7)
  • **当n=3时,n-grams模型即为trigram。**在这个模型中,我们假设每个词的出现都受到前两个词的影响,所以句子的概率就是每个词在给定前两个词的条件下出现的概率的乘积。P(sentence) = P(word1, word2) * P(word3|word1, word2) * P(word4|word2, word3) * … * P(word8|word6, word7)。

因此,n-grams语言模型中的n指的是:第n个词出现的概率只与前面n-1个词相关,可以想象成一个长度为n的窗口在文本上滑动。那么,上述概率是如何得到的呢?通过在语料库里统计得到!!例如,P(word1) 就是word1在整个语料库中出现的概率,即word1出现的次数/语料库的总词数;P(word2|word1) = P(word1word2)/P(word1),即当word1出现的时候,它后面是word2的概率。

正式的定义

对于一个句子 w 1 : N w_{1:N} w1:N = { w 1 , w 2 , w 3 , . . . , w N } \{w_{1}, w_{2}, w_{3}, ..., w_{N}\} {w1,w2,w3,...,wN}, w i w_{i} wi 代表词, i = 1 , 2 , . . . , N i = 1,2,...,N i=1,2,...,N。在真实的语言模型中, w i w_{i} wi也可以是token等形式,例如词语。句子 w 1 : N w_{1:N} w1:N 出现的概率的计算公式如下所示:
P n − g r a m s ( w 1 : N ) = ∏ i = n N C ( w i − n + 1 : i ) C ( w i − n + 1 : i − 1 ) P_{n-grams}(w_{1:N}) = \prod_{i=n}^{N} \frac{C(w_{i-n+1:i})}{C(w_{i-n+1:i-1})} Pngrams(w1:N)=i=nNC(win+1:i1)C(win+1:i)
其中:

  • P n − g r a m s ( w 1 : N ) P_{n-grams}(w_{1:N}) Pngrams(w1:N) 是句子 w 1 : N w_{1:N} w1:N 在n-grams模型下的概率。
  • C ( w i − n + 1 : i ) C(w_{i-n+1:i}) C(win+1:i) 是在语料库中,连续n个词 w i − n + 1 : i w_{i-n+1:i} win+1:i 出现的次数。
  • C ( w i − n + 1 : i − 1 ) C(w_{i-n+1:i-1}) C(win+1:i1) 是在语料库中,连续n-1个词 w i − n + 1 : i − 1 w_{i-n+1:i-1} win+1:i1 出现的次数。

下面通过bigrams语言模型来展示具体的计算方法,下图是一个示例语料库。
在这里插入图片描述
对文本“长颈鹿脖子长”(其由 {长颈鹿, 脖子, 长} 三个词构成)出现的概率进行计算,如下式所示:
P b i g r a m s ( 长颈鹿,脖子,长 ) = C ( 长颈鹿,脖子 ) C ( 长颈鹿 ) ⋅ C ( 脖子,长 ) C ( 脖子 ) P_{bigrams}(\text{长颈鹿,脖子,长}) = \frac{C(\text{长颈鹿,脖子})}{C(\text{长颈鹿})} \cdot \frac{C(\text{脖子,长})}{C(\text{脖子})} Pbigrams(长颈鹿,脖子,)=C(长颈鹿)C(长颈鹿,脖子)C(脖子)C(脖子,)
在此语料库中,C(长颈鹿) = 5,C(脖子) = 6,C(长颈鹿, 脖子) = 2,C(脖子, 长) =2,故有:
P b i g r a m s ( 长颈鹿,脖子,长 ) = 2 5 ⋅ 2 6 = 2 15 P_{bigrams}(\text{长颈鹿,脖子,长}) = \frac{2}{5} \cdot \frac{2}{6} = \frac{2}{15} Pbigrams(长颈鹿,脖子,)=5262=152
在此例中,我们可以发现虽然“长颈鹿脖子长”并没有直接出现在语料库中,但是 bigrams 语言模型仍可以预测出“长颈鹿脖子长”出现的概率。但是当n增大时,可能会出现下面的问题:例如,应用 trigrams 对文本“长颈鹿脖子长”出现的概率进行计算,将出现以下“零概率”的情况。
P t r i g r a m s ( 长颈鹿,脖子,长 ) = C ( 长颈鹿,脖子,长 ) C ( 长颈鹿,脖子 ) = 0 P_{trigrams}(\text{长颈鹿,脖子,长}) = \frac{C(\text{长颈鹿,脖子,长})}{C(\text{长颈鹿,脖子})} = 0 Ptrigrams(长颈鹿,脖子,)=C(长颈鹿,脖子)C(长颈鹿,脖子,)=0
这是因为语料库中没有”长颈鹿脖子长“!!但这句话的概率显然不应该是0。因此,在 n-grams 语言模型中,n 代表了拟合语料库的能力与对未知文本的泛化能力之间的权衡。当 n 过大时,语料库中难以找到与 n-gram 一模一样的词序列,可能出现大量“零概率”现象;在 n 过小时,n-gram 难以承载足够的语言信息,不足以反应语料库的特性。

上述的“零概率”现象可以通过平滑(Smoothing)技术进行改善。

数学原理(与极大似然估计的关系)

首先,我们需要理解一个基础概念:假设我们拥有一组样本数据,这些数据的生成是受到某些特定参数的影响。那么,我们可以推断出,这些参数应该能使得样本数据出现的概率达到最大,这就是极大似然似然估计的基本原理。
在这里插入图片描述
根据极大似然估计,在n-grams语言模型中,我们可以用频率来估计概率。概率就是参数,而频率就是用来估计参数的值。例如,在bigram中, C ( w i , w j ) / C ( w i ) C(w_{i}, w_{j})/C(w_{i}) C(wi,wj)/C(wi) 是对 P ( w j ∣ w i ) P(w_{j}|w_{i}) P(wjwi)的估计。

N-grams代码实现

教学来源:【神经网络从零到通-n元语法模型ngram-01-哔哩哔哩】 https://b23.tv/bglnOZc

任务介绍:语料库是英文名,实现字符级别的n-grams语言模型(即一个字母是一个元素)。

# 读取文件
words = open('data/names.txt').read().splitlines()

# 获取所有可能出现的字符,以及开始和结束的特殊标记(这里统一用.来表示开始和结束)
chars = ['.']
chars.extend(list(set("".join(words))))
chars_num = len(chars)
print('chars_num:', chars_num)  # 27个字符(26个字母加上.)

接下来,用torch中的tensor来给所有的gram计数,这里采用bigram。

import torch

# 用二维数组来表示二元gram:每一行的索引是第一个字符,每一列的索引是第二个字符,元素是二元gram的数量
N = torch.zeros(len(chars), len(chars),dtype=torch.int32)

# 在这里要先把字符和索引对应起来,形成一个查找表
stoi = {st:i for i,st in enumerate(chars)}
itos = {i:st for i,st in enumerate(chars)}

# 统计二元gram
for w in words:
    chs = ['.'] + list(w) +['.']
    for ch1, ch2 in zip(chs, chs[1:]):   # 一旦其中一个没了就停止
        row = stoi[ch1]  # 行号
        col = stoi[ch2]  # 列号
        N[row, col] += 1

在这里插入图片描述
为了更加清楚地展示tensor表示的计数情况,下面是可视化的结果:

import matplotlib.pyplot as plt
%matplotlib inline

# 可视化的效果
plt.figure(figsize=(16, 16))
plt.imshow(N, cmap='Blues')

for i in range(chars_num):
    for j in range(chars_num):
        bigram_str = itos[i] + itos[j]
        plt.text(j, i, bigram_str, ha='center', va='bottom')
        bigram_count = N[i, j].item()
        plt.text(j, i, bigram_count, ha='center', va='top')

在这里插入图片描述
以第二行为例,第二行的索引对应的字符是u,其中的每一个格子代表u后面跟着某个字符的计数情况,第二行的计数之和就是u出现的总次数。因此,若要计算u出现的情况下e出现的概率(对应第二行第三格),只需要用169除以第二行的计数之和。因此,我们可以进一步把这个矩阵里的计数转化为概率:

# 将计数转化成概率
P = N.float() / N.sum(1, keepdim=True)

# 可视化P
plt.figure(figsize=(16, 16))
plt.imshow(N, cmap='Blues')

for i in range(chars_num):
    for j in range(chars_num):
        bigram_str = itos[i] + itos[j]
        plt.text(j, i, bigram_str, ha='center', va='bottom')
        bigram_prob = round(P[i, j].item(),3)
        plt.text(j, i, bigram_prob, ha='center', va='top')

在这里插入图片描述
显然,每一行的概率之和应该为1。
在这里插入图片描述
接着,基于统计,我们已经得到了n-grams模型中的参数,即概率。我们可以用这些概率来完成生成的任务。

# 从P中采样, 生成新的名称
g = torch.Generator()
g.manual_seed(1234)
for i in range(5):   # 采样五个名称
    index = 0
    out = []
    while True:
        # 从P中采样,得到一个字符的索引
        p = P[index]
        # multinomial:根据p的概率分布,采样一个字符,可放回采样
        index = torch.multinomial(p,1,replacement=True,generator=g).item()
        out.append(itos[index])
        if index == 0:   # 到达结尾
            break
    print(''.join(out)[:-1])

前面在数学原理的部分,我们讲到对于一个语料库,最好的概率(参数)就是使其似然函数最大的参数。通常,为了避免下溢,我们会采用对数似然函数。因此,我们可以用对数似然函数来衡量一个模型的好坏,当对数似然函数越大,我们认为模型越好。为了和损失函数的大小方向一致(即越小越好),我们可以采用负对数似然函数(negative log likelihood,nll),将其视为损失函数,理论上,null能达到的最小值是0,损失函数的值越小,模型越好。

# 用一个指标来衡量模型的好坏:对数最大似然函数值(如果不用对数,会下溢)
log_likelihood = 0
n = 0  # 用数量来归一化,类似于MSE
for w in words:
    chs = ['.'] + list(w) +['.']
    for ch1,ch2 in zip(chs, chs[1:]):
        row = stoi[ch1]
        col = stoi[ch2]
        prob = P[row, col] 
        n +=1
        log_prob = torch.log(prob)
        log_likelihood += log_prob
        # print(f'{ch1}{ch2}: {prob:.4f} {log_prob:.4f}')
print(f'log likelihood: {log_likelihood:.5f}')

# nll是负对数似然函数,从损失函数的角度来看,我们希望函数越小越好,因此给对数似然函数加负号,得到负对数似然函数
# 理论上,null能达到的最小值是0,损失函数的值越小,模型越好
nll = -log_likelihood
print(f'nll: {nll:.5f}')
print(f'nll/n: {nll/n:.5f}')  # 平均负对数似然函数值

在这里插入图片描述
对于每个名称,我们都可以计算其nll,相当于计算这个名称的概率(概率越高说明越合理),相应地,nll越小,说明越合理。然而,此时有可能出现零概率问题,从而导致nll为无穷。为此我们可以采用平滑来避免,最简单的一种平滑方式是拉普拉斯平滑,即给每一种二元gram的数量都加上1,既可以避免对原始的概率分布有大的影响,也保证了不会有零概率的出现。
在这里插入图片描述
在这里插入图片描述

采用神经网络模型来实现

# 将二元语法模型变成神经网络模型(效果类似,但是方法不同)
# 输入:一个字符(转化为one-hot)
# 输出:下一个字符的概率分布
# 标签:例如有一个样本是emma,则输入e时,希望输出m的概率越高越好,[e,m]构成一个训练样本和标签
# 损失函数:平均负似然对数

import torch.nn.functional as F

# 构建数据集
xs,ys = [],[]
for w in words:
    chs = ['.'] + list(w) +['.']
    for ch1,ch2 in zip(chs, chs[1:]):
        row = stoi[ch1]
        col = stoi[ch2]
        xs.append(row)
        ys.append(col)
xs = torch.tensor(xs)
ys = torch.tensor(ys)
sample_num = xs.shape[0]

# 初始化chars_num个神经元的权重
g = torch.Generator().manual_seed(2147483647)
W = torch.randn((chars_num,chars_num),generator=g,requires_grad=True)  # 需要 requires_grad=True 才会计算梯度

epochs = 10000
learning_rate = 0.5

for i in range(epochs):
    # 前向传播   ps:所有的计算都是可微的,因此可以用反向传播算法
    xenc = F.one_hot(xs,num_classes=chars_num).float()
    xenc  # shape: [228146, 27] 228146是样本数量,27是字符数量
    logits = xenc @ W            # 对数计数
    counts = logits.exp()        # 这两行即softmax
    probs = counts / counts.sum(dim=1, keepdim=True)
    loss = -probs[torch.arange(len(xs)),ys].log().mean()  # 计算损失

    # 反向传播
    W.grad = None
    loss.backward()  # 计算梯度
    W.data += -learning_rate * W.grad  # 更新权重(梯度的反方向)

    # 打印
    if i % 10 == 0:
        print(f'epoch:{i}, loss:{loss.item():.4f}')
# 前面的二元模型的loss为2.45,用神经网络也能得到2.45的loss
# 虽然上面的二元模型更加简单,但是神经网络更加灵活,能够扩展模型的复杂性,有更明确的优化方式 

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值