语言模型

要点:

  1. 什么是语言模型
  2. 语言模型有哪些种类
  3. 如何构造和评价语言模型
  4. 语言模型的应用场景

什么是语言模型

语言是灵活的,语言也是有规律的。了解一门语言的人可以判断一句话是否“合理”。

什么是语言模型

通俗来讲,语言模型评价一句话是否“合理”或“是人话”。数学上讲,语言模型用于计算文本的成句概率。

语言模型的主要用途

1. 语言识别

语言识别:声音 -> 文本
声音的本质是一种波,将波按时间切分成很多帧,如25ms一段,之后进行声学特征提取,将每一帧转化为一个向量。
以声学特征提取后的向量作为输入,经过声学模型,预测得到音素。音素与拼音类似,但要考虑声调。音素序列对应多条文本序列,由语言模型挑选出成句概率最高的序列,使用beam search或维特比的方式解码。

2. 手写识别

识别模型将图片中文字转化为候选汉字(一般分定位和识别两步),再由语言模型挑选出成句概率最高的序列。

2. 输入法

  • 输入即为拼音序列,每个拼音自然的有多个候选汉字,根据语言模型挑选高概率序列。
  • 输入法是一个细节繁多的任务,在语言模型这一基础算法上,需要考虑常见的打字手误,常见误读,拼音缩略,中英缩略,中英混杂,输出符号,用户习惯等能力。
  • 手写输入法,语音输入法同理。

语言模型分类

1. 基于统计语言模型

  • 对于一份语料进行词频、词序、词共现的统计
  • 计算相关概率得到语言模型
  • 发表:N-gram语言模型

2. 基于神经网络的语言模型

  • 根据设定好的网络结构使用语料进行模型训练
  • 代表:LSTM语言模型,bert模型

1. 自回归(auto regression)语言模型

  • 在训练时由上文预测下文(或反过来)
  • 单向模型,仅使用单侧序列信息
  • 代表:N-gram、ELMO

2. 自编码(auto encoding)语言模型

  • 在训练时预测序列中任意位置的字符
  • 双向模型,吸收上下文信息
  • 代表:bert

N-gram语言模型

  • 如何计算成句概率?
    用S代表句子,w代表单个字或词:

\[S = W_1W_2W_3W_4...W_n P(S) = P(W_1,W_2,W_3,...,W_n) \]

成句概率 -> 词\(W_1\)~\(W_n\)按顺序出现的概率

\[P(W_1,W_2,W_3,...,W_n) = P(W_1)P(W_2|W_1)P(W_3|W_2W_1)...P(W_n|W_{n-1}W_{n-2}...W_1) \]

以字为单位
P(今天天气不错) = P(今)*P(天|今) *P(天|今天) *P(气|今天天) *P(不|今天天气) *P(错|今天天气不)

以词为单位
P(今天 天气 不错) = P(今天)*P(天气|今天) *P(不错|今天 天气)

如何计算P(今天)?
P(今天) = Count(今天) / Count_total 语料总词数

P(天气|今天) = Count(今天 天气) / Count(今天)

P(不错|今天 天气) = Count(今天 天气 不错) / Count(今天 天气)

二元组:今天 天气 2 gram
三元组:今天 天气 不错 3 gram

困难:句子太多
对于任意一门语言,N-gram数量都非常庞大,无法穷举,需要简化。

马尔科夫假设:\(P(W_n|W_1,...,W_{n-1}) \approx P(W_n|W_{n-1},W_{n-2},W_{n_3})\)
假设第n个词出现的概率仅受其前面有限个词影响
P(今天天气不错) = P(今)*P(天|今) *P(天|今天) *P(气|天天) *P(不|天气) *P(错|气不)

马尔科夫假设的缺陷:

  1. 影响第n个词的因素可能出现在前面很远的地方
  2. 影响第n个词的因素可能出现在其后面
  3. 影响第n个词的因素可能并不在文中

但是马尔科夫假设下依然可以得到非常有效的模型。

如何给出语料中没出现过的词或ngram的概率?

用S代表句子,w代表单个字或词

\[S = W_1W_2W_3...W_n \]

\[P(S) = P(W_1,W_2,W_3,..,W_n) \]

成句概率 -> 词W1~Wn按顺序出现的概率
P(今天 天气 糟糕) = P(今天)*P(天气|今天) *P(糟糕|今天)
平滑问题:理论上,任意的词组合成的句子,概率都不应当为零。如何给没见过的词或ngram分配概率即为平滑问题,也称折扣问题。

1. 回退

当三元组abc不存在时,退而寻找bc二元组的概率:\(P(c|ab) = P(c|b)*Bow(ab)\),Bow(ab)称为二元组ab的回退概率。回退概率有很多计算方式,甚至可以设定为常数。
回退可以迭代进行,如序列abcd:
\(P(d|abc) = P(d|bc)*Bow(abc)\)
\(P(d|bc) = P(d|b)*Bow(bc)\)
\(P(d|c) = P(d)*Bow(c)\)

当P(word)不存在时如何处理?
加1平滑:\(P(word) = \frac{count(word)+1}{count(total_{word})+V}\),V为词表大小
对于高阶概率同样可以:\(P_{Add-1}(w_t|w_{t-1}) = \frac{c(w_{t-1},w_t)+1}{c(w_{t-1})+V}\)

2. 将低频词替换为

预测中遇到的未见过的词,也可以用代替,这是一种nlp中处理为登陆词的常见方法。

3. 插值

受到回退平滑的启发,在计算高阶ngram概率是同时考虑低阶的ngram概率值,以插值给出最终结果。

\[P(w_n|w_{n-1}w_{n-2}) = \lambda_1P(W_n|W_{n-1}W_{n-2}) + \lambda_2P(W_n|W_{n-1}) + \lambda_3P(W_n) \]

实践证明,这种方式效果有提升,\(\lambda\)可以在验证集上调参确定。

import math,jieba
from collections import defaultdict


class NgramLanguageModel:
    def __init__(self, corpus=None, n=3):
        self.n = n
        self.sep = "_"     # 用来分割两个词,没有实际含义,只要是字典里不存在的符号都可以
        self.sos = "<sos>"    #start of sentence,句子开始的标识符
        self.eos = "<eos>"    #end of sentence,句子结束的标识符
        self.unk_prob = 1e-5  #给unk分配一个比较小的概率值,避免集外词概率为0
        self.fix_backoff_prob = 0.4  #使用固定的回退概率
        self.ngram_count_dict = dict((x + 1, defaultdict(int)) for x in range(n))
        self.ngram_count_prob_dict = dict((x + 1, defaultdict(int)) for x in range(n))
        self.ngram_count(corpus)
        self.calc_ngram_prob()

    #将文本切分成词或字或token
    def sentence_segment(self, sentence):
        #return sentence.split()
        return jieba.lcut(sentence)

    #统计ngram的数量
    def ngram_count(self, corpus):# 此函数的作用在于:如果是3gram,那么就分别计算1个词,两个词,三个词在一起的次数,为后面计算他们共同出现的频率做准备
        for sentence in corpus:
            word_lists = self.sentence_segment(sentence)
            word_lists = [self.sos] + word_lists + [self.eos]  #前后补充开始符和结尾符
            for window_size in range(1, self.n + 1):           #按不同窗长扫描文本
                for index, word in enumerate(word_lists):
                    #取到末尾时窗口长度会小于指定的gram,跳过那几个
                    if len(word_lists[index:index + window_size]) != window_size:
                        continue
                    #用分隔符连接word形成一个ngram用于存储
                    ngram = self.sep.join(word_lists[index:index + window_size])# 用“_”符号将词连接在一起,方便后面计算概率
                    self.ngram_count_dict[window_size][ngram] += 1
        #计算总词数,后续用于计算一阶ngram概率
        self.ngram_count_dict[0] = sum(self.ngram_count_dict[1].values())# 单个词的总数 = 1gram
        return

    #计算ngram概率
    def calc_ngram_prob(self):
        for window_size in range(1, self.n + 1):
            for ngram, count in self.ngram_count_dict[window_size].items():
                if window_size > 1:
                    ngram_splits = ngram.split(self.sep)              #ngram        :a b c,前面用“_”将词连接在一起,现在把词分开
                    ngram_prefix = self.sep.join(ngram_splits[:-1])   #ngram_prefix :a_b,回退过后,我们要计算a,b同时出现的次数
                    ngram_prefix_count = self.ngram_count_dict[window_size - 1][ngram_prefix] #Count(a,b),取出a,b同时出现的次数
                    if ngram_prefix_count == 0: # 如果是新词,为了避免概率为零需要做平滑处理
                        ngram_prefix_count = 1
                    #print(0)
                else:
                    ngram_prefix_count = self.ngram_count_dict[0]     #count(total word)
                    #print(1)
                #word = ngram_splits[-1]
                #self.ngram_count_prob_dict[word + "|" + ngram_prefix] = count / ngram_prefix_count
                self.ngram_count_prob_dict[window_size][ngram] = count / ngram_prefix_count # 计算概率
        return

    #获取ngram概率,其中用到了回退平滑,回退概率采取固定值
    def get_ngram_prob(self, ngram):
        # 此处在计算此时计算的是几gram
        n = len(ngram.split(self.sep))
        if ngram in self.ngram_count_prob_dict[n]:
            #尝试直接取出概率
            return self.ngram_count_prob_dict[n][ngram]
        elif n == 1:
            #一阶gram查找不到,说明是集外词,不做回退
            return self.unk_prob
        else:
            #高于一阶且在训练集中未出现的gram可以回退
            ngram = self.sep.join(ngram.split(self.sep)[1:])
            return self.fix_backoff_prob * self.get_ngram_prob(ngram)


    #回退法预测句子概率
    def calc_sentence_ppl(self, sentence): # 此处的sentence是要计算PPL的句子
        word_list = self.sentence_segment(sentence)
        word_list = [self.sos] + word_list + [self.eos]
        sentence_prob = 0
        for index, word in enumerate(word_list):
            # 从句子最左端开始计算每个ngram的概率
            ngram = self.sep.join(word_list[max(0, index - self.n + 1):index + 1])
            prob = self.get_ngram_prob(ngram)
            # print(ngram, prob)
            # 计算PPL
            sentence_prob += math.log(prob)
        return 2 ** (sentence_prob * (-1 / len(word_list)))



if __name__ == "__main__":
    corpus = open("corpus.txt", encoding="utf8").readlines()
    lm = NgramLanguageModel(corpus, 3)
    print("词总数:", lm.ngram_count_dict[0])
    #print(lm.ngram_count_prob_dict)
    print(lm.calc_sentence_ppl("饭来中午我吃"))
    print(lm.calc_sentence_ppl("中午来我家吃我"))
    print(lm.calc_sentence_ppl("这个月的资金情况"))

神经网络语言模型

Bengio et al. 2001:与ngram模型相似,使用前n个词预测下一个词,输出在字表上的概率分布,且得到了词向量这一副产品。

随着相关研究的发展,隐含层模型结构的复杂程度不断提升:DNN -> CNN/RNN -> LSTM/GRU -> transformer

Devlin et al. 2018:BERT诞生。主要特点:不再使用预测下一个字的方式训练语言模型,转而预测文本中被随机遮盖的某个字。这种方式被称为MLM(masked language model)。实际上这种方式被提出的时间非常早,并非bert原创。

自回归语言模型:前n个字预测下一个字,单向且从左向右预测或反向使用,有利于生成式任务。
自编码语言模型:用整段文本,预测文本中的某一个字。双向且更好的利用文本中的信息。引入了[mask],在fine-tune中不出现。

语言模型的评价指标:

困惑度(perplexity):

\[PP(S) = P(w_1w_2w_3...w_n)^{-\frac{1}{N}} = \sqrt[N]{ \frac{1}{P(w_1w_2...w_N)} } = \sqrt[N]{\sum_{i=1}^{N}\frac{1}{P(w_i|w_1w_2...w_{i-1})}} \]

一般使用合理的目标文本计算PPL,若PPL值低,则说明成句概率高,也就说明由此语言模型来判断,该句子的合理性高,这样是一个好的语言模型。PPL值与成句概率成反比。

另一种PPL,用对数求和代替小数乘积:

\[PP(S) = 2^{-\frac{1}{N}\sum log(P(w_i))} \]

本质是相同的,与成句概率成反比。

思考:PPL越小,语言模型效果越好,这一结论是否正确?
成句概率是一个相对值。不同语言模型对于同一篇文章的成句概率越低则模型越好。

#coding:utf8

import torch
import torch.nn as nn
import numpy as np
import math
import random
import os
import re
import matplotlib.pyplot as plt

"""
基于pytorch的LSTM语言模型
"""

class LanguageModel(nn.Module):
    def __init__(self, input_dim, vocab):
        super(LanguageModel, self).__init__()
        # len(vocab) + 1 是为了给<UNK>占一个位置
        self.embedding = nn.Embedding(len(vocab) + 1, input_dim)
        self.layer = nn.LSTM(input_dim, input_dim, num_layers=2)
        self.classify = nn.Linear(input_dim, len(vocab) + 1)
        self.dropout = nn.Dropout(0.1)
        self.loss = nn.functional.cross_entropy

    #当输入真实标签,返回loss值;无真实标签,返回预测值
    def forward(self, x, y=None):
        x = self.embedding(x)  #output shape:(batch_size, sen_len, input_dim)
        x, _ = self.layer(x)      #output shape:(batch_size, sen_len, input_dim)
        x = x[:, -1, :]        #output shape:(batch_size, input_dim)
        x = self.dropout(x)
        y_pred = self.classify(x)   #output shape:(batch_size, input_dim)
        if y is not None:
            return self.loss(y_pred, y)
        else:
            return torch.softmax(y_pred, dim=-1)

#读取语料获得字符集
#输出一份
def build_vocab_from_corpus(path):
    vocab = set()
    with open(path, encoding="utf8") as f:
        for index, char in enumerate(f.read()):
            vocab.add(char)
    vocab.add("<UNK>") #增加一个unk token用来处理未登录词
    writer = open("vocab.txt", "w", encoding="utf8")
    for char in sorted(vocab):
        writer.write(char + "\n")
    return vocab

#加载字表
def build_vocab(vocab_path):
    vocab = {}
    with open(vocab_path, encoding="utf8") as f:
        for index, line in enumerate(f):
            char = line[:-1]        #去掉结尾换行符
            vocab[char] = index + 1 #留出0位给pad token
        vocab["\n"] = 1
    return vocab

#加载语料
def load_corpus(path):
    return open(path, encoding="utf8").read()

#随机生成一个样本
#从文本中截取随机窗口,前n个字作为输入,最后一个字作为输出
def build_sample(vocab, window_size, corpus):
    start = random.randint(0, len(corpus) - 1 - window_size)
    end = start + window_size
    window = corpus[start:end]
    target = corpus[end]
    # print(window, target)
    x = [vocab.get(word, vocab["<UNK>"]) for word in window]   #将字转换成序号
    y = vocab[target]
    return x, y

#建立数据集
#sample_length 输入需要的样本数量。需要多少生成多少
#vocab 词表
#window_size 样本长度
#corpus 语料字符串
def build_dataset(sample_length, vocab, window_size, corpus):
    dataset_x = []
    dataset_y = []
    for i in range(sample_length):
        x, y = build_sample(vocab, window_size, corpus)
        dataset_x.append(x)
        dataset_y.append(y)
    return torch.LongTensor(dataset_x), torch.LongTensor(dataset_y)

#建立模型
def build_model(vocab, char_dim):
    model = LanguageModel(char_dim, vocab)
    return model

#文本生成测试代码
def generate_sentence(openings, model, vocab, window_size):
    reverse_vocab = dict((y, x) for x, y in vocab.items())
    model.eval()
    with torch.no_grad():
        pred_char = ""
        #生成了换行符,或生成文本超过20字则终止迭代
        while pred_char != "\n" and len(openings) <= 20:
            openings += pred_char
            x = [vocab.get(char, vocab["<UNK>"]) for char in openings[-window_size:]]
            x = torch.LongTensor([x])
            if torch.cuda.is_available():
                x = x.cuda()
            y = model(x)[0]
            y = torch.argmax(y)
            pred_char = reverse_vocab[int(y)]
    return openings

#计算文本ppl
def calc_perplexity(sentence, model, vocab, window_size):
    prob = 0
    model.eval()
    with torch.no_grad():
        for i in range(1, len(sentence)):
            start = max(0, i - window_size)
            window = sentence[start:i]
            x = [vocab.get(char, vocab["<UNK>"]) for char in window]
            x = torch.LongTensor([x])
            target = sentence[i]
            target_index = vocab.get(target, vocab["<UNK>"])
            if torch.cuda.is_available():
                x = x.cuda()
            pred_prob_distribute = model(x)[0]
            target_prob = pred_prob_distribute[target_index]
            prob += math.log(target_prob, 10)
    return 2 ** (prob * ( -1 / len(sentence)))


def train(corpus_path, save_weight=True):
    epoch_num = 10        #训练轮数
    batch_size = 128       #每次训练样本个数
    train_sample = 10000   #每轮训练总共训练的样本总数
    char_dim = 128        #每个字的维度
    window_size = 6       #样本文本长度
    vocab = build_vocab("vocab.txt")       #建立字表
    corpus = load_corpus(corpus_path)     #加载语料
    model = build_model(vocab, char_dim)    #建立模型
    if torch.cuda.is_available():
        model = model.cuda()
    optim = torch.optim.Adam(model.parameters(), lr=0.001)   #建立优化器
    for epoch in range(epoch_num):
        model.train()
        watch_loss = []
        for batch in range(int(train_sample / batch_size)):
            x, y = build_dataset(batch_size, vocab, window_size, corpus) #构建一组训练样本
            if torch.cuda.is_available():
                x, y = x.cuda(), y.cuda()
            optim.zero_grad()    #梯度归零
            loss = model(x, y)   #计算loss
            watch_loss.append(loss.item())
            loss.backward()      #计算梯度
            optim.step()         #更新权重
        print("=========\n第%d轮平均loss:%f" % (epoch + 1, np.mean(watch_loss)))
        print(generate_sentence("现在银行", model, vocab, window_size))
    if not save_weight:
        return
    else:
        base_name = os.path.basename(corpus_path).replace("txt", "pth")
        model_path = os.path.join("model", base_name)
        torch.save(model.state_dict(), model_path)
        return

#训练corpus文件夹下的所有语料,根据文件名将训练后的模型放到莫得了文件夹
def train_all():
    for path in os.listdir("corpus"):
        corpus_path = os.path.join("corpus", path)
        train(corpus_path)


if __name__ == "__main__":
    # build_vocab_from_corpus("corpus/all.txt")
    # train("corpus.txt", True)
    train_all()

应用:

#coding:utf8

import torch
import torch.nn as nn
import numpy as np
import math
import random
import re
import os
from nnlm import build_model, build_vocab


"""
使用训练好的语言模型
"""

def load_trained_language_model(path):
    char_dim = 128        #每个字的维度,与训练时保持一直
    window_size = 6       #样本文本长度,与训练时保持一直
    vocab = build_vocab("vocab.txt")      # 加载字表
    model = build_model(vocab, char_dim)  # 加载模型
    model.load_state_dict(torch.load(path))  #加载训练好的模型权重
    model.eval()
    if torch.cuda.is_available():
        model = model.cuda()
    model.window_size = window_size
    model.vocab = vocab
    return model

#计算文本ppl
def calc_perplexity(sentence, model):
    prob = 0
    with torch.no_grad():
        for i in range(1, len(sentence)):
            start = max(0, i - model.window_size)
            window = sentence[start:i]
            x = [model.vocab.get(char, model.vocab["<UNK>"]) for char in window]
            x = torch.LongTensor([x])
            target = sentence[i]
            target_index = model.vocab.get(target, model.vocab["<UNK>"])
            if torch.cuda.is_available():
                x = x.cuda()
            pred_prob_distribute = model(x)[0]
            target_prob = pred_prob_distribute[target_index]
            # print(window , "->", target, "prob:", float(target_prob))
            prob += math.log(target_prob, 10)
    return 2 ** (prob * ( -1 / len(sentence)))

#加载训练好的所有模型
def load_models():
    model_paths = os.listdir("model")
    class_to_model = {}
    for model_path in model_paths:
        class_name = model_path.replace(".pth", "")
        model_path = os.path.join("model", model_path)
        class_to_model[class_name] = load_trained_language_model(model_path)
    return class_to_model

#基于语言模型的文本分类伪代码
#class_to_model: {"class1":<language model obj1>, "class2":<language model obj2>, ..}
#每个语言模型,用对应的领域语料训练
def text_classification_based_on_language_model(class_to_model, sentence):
    ppl = []
    for class_name, class_lm in class_to_model.items():
        #用每个语言模型计算ppl
        ppl.append([class_name, calc_perplexity(sentence, class_lm)])
    ppl = sorted(ppl, key=lambda x:x[1])
    print(sentence)
    print(ppl[0: 3])
    return ppl

sentence = ["在全球货币体系出现危机的情况下",
            "点击进入双色球玩法经典选号图表",
            "慢时尚服饰最大的优点是独特",
            "做处女座朋友的人真的很难",
            "网戒中心要求家长全程陪护",
            "在欧巡赛扭转了自己此前不利的状态",
            "选择独立的别墅会比公寓更适合你",
            ]

# model = load_trained_language_model("language_model.pth")
#
# for s in sentence:
#     prob = calc_perplexity(s, model)
#     print(s, prob)

class_to_model = load_models()
for s in sentence:
    text_classification_based_on_language_model(class_to_model, s)
Ngram语言模型 NN语言模型
解码速度
消耗内存
是否需要调参 不需要 需要
模型大小
长距离依赖 无法处理 相对有效
词义关系
泛化能力 较弱 较强

语言模型的应用

话者分离

根据说话内容判断说话人,常用于语言识别系统中,判断录音对话中的角色。如:客服对话录音,判断坐席和客户。
根据不同腔调判断说话人。

本质上是文本分类任务:

  1. 对每个类别,使用类别语料训练语言模型。
  2. 对于一个新输入的文本,用所有语言模型计算成句概率。
  3. 选取概率最高的类别为预测类别。

相对于一般文本分类模型(贝叶斯,rf,神经网络等)

优势:

  1. 每个类别模型互相独立,样本不均衡或样本有错误对其他模型没有影响
  2. 可以随时增加新的类别,而不影响旧的类别的效果

效果上讲:一般不会有显著优势
效率上讲:一般会低于统一的分类模型

文本纠错

纠正文本中的错误

  1. 对每一个字建立一个混淆字集合
  2. 计算整句话成句概率
  3. 用混淆字集合中的词替代原句中的字,重新计算概率
  4. 选取得分最高的一个候选句子,如果这个句子比原句的得分增长超过一定的阈值
  5. 对下一个字重复步骤3-4,直至句子末尾

同音字有完整字表
形似字可以通过ocr收集
也可以依照实际情况手动添加一些

缺陷

  1. 无法解决字多字少问题
  2. 阈值的设置非常难把握,如果设置过大,达不到纠错效果;如果设置过小,造成大量替换,有可能改变句子的原意
  3. 混淆字字表难以完备
  4. 语言模型的领域性会影响修改效果
  5. 连续的错字会大幅提升纠正难度

一般工业做法:

限定一个修改白名单,只判断特定的字词是否需要修改。如限定只对所有发音为shang wu的片段,计算是否修改为“商务”,其余一概不作处理。
对于深度学习模型而言,错别字是可以容忍的,所以纠错本身的重要性在下降,一般只针对展示任务。

from ngram_language_model import NgramLanguageModel


# 从事先准备的语料库找同音字
def detect_words(word):
    with open('tongyin.txt', encoding='utf8') as f:
        for line in f:
            tongyin = line.split(" ")
            if word == tongyin[0]:
                tongyin[1] = tongyin[1].replace("\n",'')
                return tongyin

# 找到一句话中每个字的同音字
def search_replace(sentence):
    word_replace = {}
    for word in sentence:
        word_replace[word] = detect_words(word)

    print(word_replace)
    return word_replace


#输出ppl
def output_ppls(word_replace):
    ppl_dict = {}
    for key,words_list in word_replace.items():
        print(words_list)
        ppl = lm.calc_sentence_ppl(sentence)
        ppl_dict[sentence] = ppl
        if words_list != None:
            for word in words_list[1]:
                # 将句子中的每个字都替换为同音字,并计算他们的ppl
                p_sentence = sentence.replace(words_list[0],word)
                ppl = lm.calc_sentence_ppl(p_sentence)
                ppl_dict[p_sentence] = ppl
        else:
            continue
    return ppl_dict


def output_sentence(ppl_dict,k=1):
    sentences = sorted(ppl_dict.items(),key=lambda x:x[1])[0:k]
    return sentences


if __name__ == "__main__":
    corpus = open("corpus.txt", encoding="utf8").readlines()
    lm = NgramLanguageModel(corpus, 3)
    while True:
        sentence = input("输入句子:")
        word_replace = search_replace(sentence)
        ppl_dict = output_ppls(word_replace)
        sentences = output_sentence(ppl_dict,k=3)
        print(sentences)

数字归一化

将一个文本中的数字部分转化为对读者友好的样式。

  1. 找到数字形式符合规范的文本作为原始材料
  2. 用正则表达式找到数字部分
  3. 将数字部分依照其格式替换为<阿拉伯数字><汉字数字><汉字连读>等token
  4. 使用带token文本训练语言模型
  5. 对新输入的文本,同样使用正则表达式找到数字部分,之后分别带入各个token,使用语言模型计算概率。
  6. 选取概率最高的token作为最终数字形式,按照规律转化后填入原文本。

例:中国党第<汉字数字>次全国代表大会于<阿拉伯数字>年<阿拉伯数字>月<阿拉伯数字>日召开,各地方代表<汉字连续>发言。
训练时,将当成一个字训练语言模型
预测时,判断那种token的概率最高,若需要转化格式则通过规则完成,模型只起到判断是否转化的作用。

文本打标

给文本添加标点或语气停顿,可以理解为一种粗粒度的分词。常用于语音合成任务中,辅助做出发音的停顿。

带token训练语言模型,需要有标注数据,在停顿处添加token: < s >
例:我最近 < s > 抽了点时间 < s > 读了一本 < s > 关于 < s > 马尔可夫生平 < s > 的书
预测过程:

  1. 选定一个窗口长度,首先预测第一次停顿位置
    我< s >最近抽了点时间 ppl:10
    我最< s >近抽了点时间 ppl:20
    我最近< s >抽了点时间 ppl:5 <- 此处的PPL最小,选择此处作为第一次停顿
  2. 之后从“抽了点时间”开始向后重复此过程

其本质为序列标注任务,可以依照类似方式,处理分词、文本加标点、文本段落切分等任务。分词或切分段落只需要一种token;达标点时可以用多种分隔token代表不同标点。

总结

  1. 语言模型的核心能力是计算成句概率,依赖这一能力可以完成大量不同类型的NLP任务。
  2. 基于统计模型的语言模型和基于神经网络的语言模型各有使用的场景。大体上讲,基于统计模型优势在于解码速度,而神经网络的模型效果较好。
  3. 单纯通过PPL评价语言模型是有局限性的,通过下游任务效果进行整体评价更好。
  4. 深入理解一种算法,有助于发现更多的应用方式。
  5. 看似简单的假设,也能带来有意义的结果。事实上,这是简化问题的常见方式。
posted @ 2022-03-14 23:26  蒋古诚  阅读(744)  评论(0)    收藏  举报