1.3 创建一个Bigram字符预测模型

这个Bigram字符预测模型程序的整体结构如下图所示。

Bigram字符预测模型程序结构

第1步 构建实验语料库

首先建一个非常简单的数据集。

  In  

# 构建一个玩具数据集
corpus = [ "我喜欢吃苹果",
        "我喜欢吃香蕉",
        "她喜欢吃葡萄",
        "他不喜欢吃香蕉",
        "他喜欢吃苹果",
        "她喜欢吃草莓"]

这个玩具数据集,你可以把它想象成中文的一个简单缩影,抑或文明曙光初现时我们祖先发明的第一批语言和文字,人们每天反反复复就只说这么几句话。这就是我们的实验语料库。

第2步 把句子分成N个“Gram”(分词)

定义一个分词函数,用它将文本分割成单个汉字字符,针对字符来计算Bigram词频。

  In  

# 定义一个分词函数,将文本转换为单个字符的列表
def tokenize(text):
    return [char for char in text] # 将文本拆分为字符列表

第3步 计算每个Bigram在语料库中的词频

定义计算N-Gram词频的函数,并在数据集上应用这个函数,指定参数n为2,以生成Bigram,然后把所有的词频都显示出来。

  In  

# 定义计算N-Gram词频的函数
from collections import defaultdict, Counter # 导入所需库
def count_ngrams(corpus, n):
    ngrams_count = defaultdict(Counter)  # 创建一个字典,存储N-Gram计数
    for text in corpus:  # 遍历语料库中的每个文本
        tokens = tokenize(text)  # 对文本进行分词
        for i in range(len(tokens) - n + 1):  # 遍历分词结果,生成N-Gram
            ngram = tuple(tokens[i:i+n])  # 创建一个N-Gram元组
            prefix = ngram[:-1]  # 获取N-Gram的前缀
            token = ngram[-1]  # 获取N-Gram的目标单字
            ngrams_count[prefix][token] += 1  # 更新N-Gram计数
    return ngrams_count
bigram_counts = count_ngrams(corpus, 2) # 计算Bigram词频
print("Bigram词频:") # 打印Bigram词频
for prefix, counts in bigram_counts.items():
    print("{}: {}".format("".join(prefix), dict(counts))) 

 Out 

Bigram词频:
我: {'喜': 2}
喜: {'欢': 6}
欢: {'吃': 6}
吃: {'苹': 2, '香': 2, '葡': 1, '草': 1}
苹: {'果': 2}
香: {'蕉': 2}
她: {'喜': 2}
葡: {'萄': 1}
他: {'不': 1, '喜': 1}
不: {'喜': 1}
草: {'莓': 1}

从输出中可以看到,每一个二元组在整个语料库中出现的次数都被统计得清清楚楚,这就是我们所构造的模型的基础信息。比如说,“吃苹”这个Bigram,在语料库中出现了2次;而“吃葡”则只出现过1次。

第4步 计算每个Bigram的出现概率

根据词频计算每一个Bigram出现的概率。也就是计算给定前一个词时,下一个词出现的可能性,这是通过计算某个Bigram词频与前缀词频之比得到的。

  In  

# 定义计算N-Gram出现概率的函数
def ngram_probabilities(ngram_counts):
    ngram_probs = defaultdict(Counter)  # 创建一个字典,存储N-Gram出现的概率
    for prefix, tokens_count in ngram_counts.items():  # 遍历N-Gram前缀
        total_count = sum(tokens_count.values())  # 计算当前前缀的N-Gram计数
        for token, count in tokens_count.items():  # 遍历每个前缀的N-Gram
            ngram_probs[prefix][token] = count / total_count  # 计算每个N-Gram出现的概率
    return ngram_probs
bigram_probs = ngram_probabilities(bigram_counts) # 计算Bigram出现的概率
print("\nbigram出现的概率:") # 打印Bigram概率
for prefix, probs in bigram_probs.items():
    print("{}: {}".format("".join(prefix), dict(probs)))

 Out 

Bigram出现的概率:
我: {'喜': 1.0}
喜: {'欢': 1.0}
欢: {'吃': 1.0}
吃: {'苹': 0.3333333333333333, '香': 0.3333333333333333, '葡': 0.16666666666666666, '草': 0.16666666666666666}
苹: {'果': 1.0}
香: {'蕉': 1.0}
她: {'喜': 1.0}
葡: {'萄': 1.0}
他: {'不': 0.5, '喜': 0.5}
不: {'喜': 1.0}
草: {'莓': 1.0}

这样,我们拥有了全部Bigram出现的概率,也就拥有了一个N-Gram模型。可以用这个模型来进行文本生成。也就是说,你给出一个字,它可以为你预测下一个字,方法就是直接选择出现概率最高的一个词进行生成。

第5步 根据Bigram出现的概率,定义生成下一个词的函数

定义生成下一个词的函数,基于N-Gram出现的概率计算特定前缀出现后的下一个词。

  In  

# 定义生成下一个词的函数
def generate_next_token(prefix, ngram_probs):
    if not prefix in ngram_probs:  # 如果前缀不在N-Gram中,返回None
        return None
    next_token_probs = ngram_probs[prefix]  # 获取当前前缀的下一个词的概率
    next_token = max(next_token_probs, 
                     key=next_token_probs.get)  # 选择概率最大的词作为下一个词
    return next_token

这段代码接收一个词序列(称为前缀)和一个包含各种可能的下一个词及其对应概率的词典。首先,检查前缀是否在词典中。如果前缀不存在于词典中,那么函数返回None,表示无法生成下一个词。如果前缀存在于词典中,该函数就会从词典中取出这个前缀对应的下一个词的概率。接着,函数会在其中找到概率最大的词,然后将这个词作为下一个词返回。

有了这个函数,给定一个前缀词之后,我们就可以调用它,生成下一个词。

第6步 输入一个前缀,生成连续文本

先定义一个生成连续文本的函数。

  In  

# 定义生成连续文本的函数
def generate_text(prefix, ngram_probs, n, length=6):
    tokens = list(prefix)  # 将前缀转换为字符列表
    for _ in range(length - len(prefix)):  # 根据指定长度生成文本 
        # 获取当前前缀的下一个词
        next_token = generate_next_token(tuple(tokens[-(n-1):]), ngram_probs) 
        if not next_token: # 如果下一个词为None,跳出循环
            break
        tokens.append(next_token) # 将下一个词添加到生成的文本中
    return "".join(tokens) # 将字符列表连接成字符串

这个函数首先将前缀字符串转化为字符列表tokens,以便后续操作。然后进入一个循环,循环的次数等于生成文本的目标长度length减去前缀的长度。循环的目的是生成足够长度的文本。在循环中,函数会调用之前定义的generate_next_token函数,以获取下一个词。

这个函数会考虑到当前的n-1个词(也就是前缀的最后n-1个词),以及所有可能的下一个词及其对应的概率。如果generate_next_token函数返回的下一个词是None(也就是没有找到合适的下一个词),那么循环会提前结束,不再生成新的词。如果函数成功找到了下一个词,那么这个词会被添加到字符列表tokens的尾部。当循环结束时,函数将使用Python的join方法,将字符列表连接成一个字符串,也就是函数生成的一段连续文本。

有了这个函数,我们就可以调用它,生成连续文本。一个简单的语言模型就做好了!

小冰问道:咖哥,那我们能否用这个模型生成句子呢?

咖哥回答:当然可以,加入一个前缀,模型立刻就能生成一个句子。

  In  

# 输入一个前缀,生成文本
generated_text = generate_text("我", bigram_probs, 2)
print("\n生成的文本:", generated_text) # 打印生成的文本

 Out 

生成的文本: 我喜欢吃苹果

这个Bigram字符预测模型,也就是简单的N-Gram模型虽然有局限性,但它对后来许多更加强大的自然语言处理技术都有很大的启发意义。你看,我们只是通过一个玩具数据集和二元组模型,就能生成简单的句子。

在N-Gram模型中,我们预测一个词出现的概率,只需考虑它前面的N-1个词。这样做的优点是计算简单,但缺点也很明显:它无法捕捉到距离较远的词之间的关系。而下面我给你介绍的这个和N-Gram差不多同时出现的早期语言模型——Bag-of-Words模型(也称“词袋模型”),则并不考虑哪个词和哪个词临近,而是通过把词看作一袋子元素的方式来把文本转换为能统计的特征。