目录
- N-Gram 概述
- N-Gram 构建过程
- Token
- N-Gram 实例
- 第1步 构建实验语料库
- 第2步 把句子分成 N 个 “Gram”
- 第3步 计算每个 Bigram 在语料库中的词频
- 第4步 计算出现的概率
- 第5步 生成下一个词
- 第6步:输入前缀,生成连续文本
- 上述实例完整代码
- N-Gram 的局限性
N-Gram 概述
N-Gram 诞生于统计学 NLP 初期,为解决词序列冗长导致的高复杂性概率计算。其通过分割文本为连续 N 个词的组合,来预测下一个词。
e . g . e.g. e.g. 我喜欢大模型
根据分词结果,文本中有三个词:“我”、“喜欢”、“大模型”
- N=1,组合成一元组(Unigram):“我”、“喜欢”、“大模型”
- N=2,组合成二元组(Bigram):“我喜欢”、“喜欢大模型”
- N=3,组合成三元组(Trigram):“我喜欢大模型”
N-Gram 构建过程
第一步:分割文本为连续 N 个词的组合(N-Gram)
- 以二元组(Bigram)为例,将语料库中文本进行分割。
- e . g . e.g. e.g. 我爱吃香菜
第二步:统计每个 N-Gram 在文本中出现的次数,即词频
- 在语料库
["我爱吃香菜", "我爱吃涮", "我爱吃汉堡", "我喜欢你", "我也爱吃水果"]
中,Bigram “我爱” 出现了 3 次。
第三步:计算下一个词出现的概率
- 二元组 “我爱” 出现了 3 次,而其前缀 “我” 在语料库中出现了 5 次,则给定 “我” 为前缀时,下一个词为 “爱” 的概率为 60%
第四步:迭代上述过程,生成整段文本内容
Token
上述内容中,我们将文本 “我爱吃香菜” 分为了 4 个词。但是标准的说法,是分成了 4 个 Token。
在 NLP 中,
- 英文分词方法通常使用 NLTK、spaCy 等自然语言处理库。
- 中文分词则通常使用 jieba 库。
- 在预训练模型在 BERT 中,使用 Tokenizer 库。
分词是预处理的一个重要环节,其他还包括文本清洗、去停用词、词干提取、词性标注等环节。
N-Gram 实例
整体流程一览图如下:
第1步 构建实验语料库
# 构建语料库
corpus = ["我喜欢吃苹果", "我喜欢吃香蕉", "她喜欢吃葡萄", "他不喜欢吃香蕉", "他喜欢吃苹果", "她喜欢吃草莓"]
第2步 把句子分成 N 个 “Gram”
import jiebadef generate_bigrams(corpus):bigram_list = []for sentence in corpus:# 使用jieba分词words = list(jieba.cut(sentence))bigrams = [(words[i] , words[i + 1]) for i in range(len(words) - 1)]bigram_list.extend(bigrams)return bigram_listbigrams = generate_bigrams(corpus)
print(bigrams)
结果:
[('我', '喜欢'), ('喜欢', '吃'), ('吃', '苹果'), ('我', '喜欢'), ('喜欢', '吃'), ('吃', '香蕉'), ('她', '喜欢'), ('喜欢', '吃'), ('吃', '葡萄'), ('他', '不'), ('不', '喜欢'), ('喜欢', '吃'), ('吃', '香蕉'), ('他', '喜欢'), ('喜欢', '吃'), ('吃', '苹果'), ('她', '喜欢'), ('喜欢', '吃'), ('吃', '草莓')]
第3步 计算每个 Bigram 在语料库中的词频
from collections import defaultdict, Counterdef count_bigrams(bigrams):# 创建字典存储biGram计数bigrams_count = defaultdict(Counter)for bigram in bigrams:prefix = bigram[:-1]token = bigram[-1]bigrams_count[prefix][token] += 1return bigrams_countbigrams_counts = count_bigrams(bigrams)
for prefix, counts in bigrams_counts.items():print("{}: {}".format("".join(prefix), dict(counts)))
结果:
我: {'喜欢': 2}
喜欢: {'吃': 6}
吃: {'苹果': 2, '香蕉': 2, '葡萄': 1, '草莓': 1}
她: {'喜欢': 2}
他: {'不': 1, '喜欢': 1}
不: {'喜欢': 1}
第4步 计算出现的概率
def bigram_probabilities(bigrams_count):bigrams_prob = defaultdict(Counter)for prefix, tokens_count in bigrams_count.items():total_count = sum(tokens_count.values())for token, count in tokens_count.items():bigrams_prob[prefix][token] = count / total_countreturn bigrams_probbigrams_prob = bigram_probabilities(bigrams_count)
for prefix, probs in bigrams_prob.items():print("{}: {}".format("".join(prefix), dict(probs)))
结果:
我: {'喜欢': 1.0}
喜欢: {'吃': 1.0}
吃: {'苹果': 0.3333333333333333, '香蕉': 0.3333333333333333, '葡萄': 0.16666666666666666, '草莓': 0.16666666666666666}
她: {'喜欢': 1.0}
他: {'不': 0.5, '喜欢': 0.5}
不: {'喜欢': 1.0}
第5步 生成下一个词
def generate_token(prefix, bigram_probs):if not prefix in bigram_probs:return Nonenext_token_probs = bigram_probs[prefix]next_token = max(next_token_probs, key=next_token_probs.get)return next_token
第6步:输入前缀,生成连续文本
def generate_text(prefix, bigram_probs, length=6):tokens = list(prefix)for _ in range(length - len(prefix)):next_token = generate_token(tuple(tokens[-1:]), bigram_probs)if not next_token:breaktokens.append(next_token)return "".join(tokens)generate_text("我", bigram_probs)
结果:
'我喜欢吃苹果'
上述实例完整代码
import jieba
from collections import defaultdict, Counter# 构建语料库
corpus = ["我喜欢吃苹果", "我喜欢吃香蕉", "她喜欢吃葡萄", "他不喜欢吃香蕉", "他喜欢吃苹果", "她喜欢吃草莓"]# 二元组切词
def generate_bigrams(corpus):bigram_list = []for sentence in corpus:# 使用jieba分词words = list(jieba.cut(sentence))bigrams = [(words[i] , words[i + 1]) for i in range(len(words) - 1)]bigram_list.extend(bigrams)return bigram_list# 计算二元组词频
def count_bigrams(bigrams):# 创建字典存储biGram计数bigrams_count = defaultdict(Counter)for bigram in bigrams:prefix = bigram[:-1]token = bigram[-1]bigrams_count[prefix][token] += 1return bigrams_count# 计算二元组概率
def bigram_probabilities(bigrams_count):bigram_probs = defaultdict(Counter)for prefix, tokens_count in bigrams_count.items():total_count = sum(tokens_count.values())for token, count in tokens_count.items():bigram_probs[prefix][token] = count / total_countreturn bigram_probs# 生成内容
def generate_token(prefix, bigram_probs):if not prefix in bigram_probs:return Nonenext_token_probs = bigram_probs[prefix]next_token = max(next_token_probs, key=next_token_probs.get)return next_tokendef generate_text(prefix, bigram_probs, length=6):tokens = list(prefix)for _ in range(length - len(prefix)):next_token = generate_token(tuple(tokens[-1:]), bigram_probs)if not next_token:breaktokens.append(next_token)return "".join(tokens)if __name__ == '__main__':bigrams = generate_bigrams(corpus)print(bigrams)bigrams_count = count_bigrams(bigrams)for prefix, counts in bigrams_count.items():print("{}: {}".format("".join(prefix), dict(counts)))bigram_probs = bigram_probabilities(bigrams_count)for prefix, probs in bigram_probs.items():print("{}: {}".format("".join(prefix), dict(probs)))res = generate_text("我", bigram_probs)print(res)
N-Gram 的局限性
N-Gram 模型具有很大的启发意义和价值,我们只需要一个简单的语料库,结合二元组模型,即可生成一段话。
N-Gram 模型中,我们预测一个词出现的频率,只考虑其之前的 N-1 个词,其优点是计算简单,但是缺点也很明显,那就是它无法捕捉到距离较远的词之间的关系。
下一节,将介绍于 N-Gram 同时代产物,词袋模型(Bag-of-Words)。词袋模型不考虑哪个词和哪个词接近,而是通过把词看作一袋子元素的方式来把文本转换为能统计的特征。
2024.09.07