目录
- 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 jieba
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
bigrams = generate_bigrams(corpus)
print(bigrams)
结果:
[('我', '喜欢'), ('喜欢', '吃'), ('吃', '苹果'), ('我', '喜欢'), ('喜欢', '吃'), ('吃', '香蕉'), ('她', '喜欢'), ('喜欢', '吃'), ('吃', '葡萄'), ('他', '不'), ('不', '喜欢'), ('喜欢', '吃'), ('吃', '香蕉'), ('他', '喜欢'), ('喜欢', '吃'), ('吃', '苹果'), ('她', '喜欢'), ('喜欢', '吃'), ('吃', '草莓')]
第3步 计算每个 Bigram 在语料库中的词频
from collections import defaultdict, Counter
def count_bigrams(bigrams):
# 创建字典存储biGram计数
bigrams_count = defaultdict(Counter)
for bigram in bigrams:
prefix = bigram[:-1]
token = bigram[-1]
bigrams_count[prefix][token] += 1
return bigrams_count
bigrams_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_count
return bigrams_prob
bigrams_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 None
next_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:
break
tokens.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] += 1
return 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_count
return bigram_probs
# 生成内容
def generate_token(prefix, bigram_probs):
if not prefix in bigram_probs:
return None
next_token_probs = bigram_probs[prefix]
next_token = max(next_token_probs, key=next_token_probs.get)
return next_token
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:
break
tokens.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