语言模型雏形 - 词袋模型

本文为对《GPT 图解 - 大模型是怎样构建的》一书的学习笔记,所有的例子和代码均来源于本书。

基本介绍

Bag-of-Words 模型为早期的语言模型,诞生于 1954 年。

核心思想:将文本中的词看作一个个独立的个体,不考虑在句子中的顺序,只关心词出现频次。

应用场景:文本分析、情感分析

代码实践

准备数据集

1
2
3
4
5
corpus = ["我特别特别喜欢看电影",  
"这部电影真的是很好看的电影",
"今天天气真好是难得的好天气",
"我今天去看了一部电影",
"电影院的电影都很好看"]

对数据集进行分词

1
2
3
4
import jieba
corpus_tokenized = [list(jieba.cut(sentence)) for sentence in corpus]

print(corpus_tokenized)

分词完成的结果如下:

1
[['我', '特别', '特别', '喜欢', '看', '电影'], ['这部', '电影', '真的', '是', '很', '好看', '的', '电影'], ['今天天气', '真好', '是', '难得', '的', '好', '天气'], ['我', '今天', '去', '看', '了', '一部', '电影'], ['电影院', '的', '电影', '都', '很', '好看']]

创建词汇表

1
2
3
4
5
6
7
8
word_dict = {} # 初始化词汇表  
# 遍历分词后的语料库
for sentence in corpus_tokenized:
for word in sentence:
# 如果词汇表中没有该词,则将其添加到词汇表中
if word not in word_dict:
word_dict[word] = len(word_dict) # 分配当前词汇表索引
print(" 词汇表:", word_dict) # 打印词汇表

获取到如下结果,其中 key 为对应的词,value 为词出现的频率。

1
词汇表: {'我': 0, '特别': 1, '喜欢': 2, '看': 3, '电影': 4, '这部': 5, '真的': 6, '是': 7, '很': 8, '好看': 9, '的': 10, '今天天气': 11, '真好': 12, '难得': 13, '好': 14, '天气': 15, '今天': 16, '去': 17, '了': 18, '一部': 19, '电影院': 20, '都': 21}

生成词袋表示

1
2
3
4
5
6
7
8
9
10
11
12
# 根据词汇表将句子转换为词袋表示  
bow_vectors = [] # 初始化词袋表示
# 遍历分词后的语料库
for sentence in corpus_tokenized:
# 初始化一个全 0 向量,其长度等于词汇表大小
sentence_vector = [0] * len(word_dict)
for word in sentence:
# 将对应词的索引位置加 1,表示该词在当前句子中出现了一次
sentence_vector[word_dict[word]] += 1
# 将当前句子的词袋向量添加到向量列表中
bow_vectors.append(sentence_vector)
print(" 词袋表示:", bow_vectors) # 打印词袋表示

输出如下结果:

1
2
3
4
5
6
7
[
[1, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 2, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1]
]

输出结果需要稍微理解一下

  • 数组中的每一行表示语料中的一句话
  • 每个元素表示该词在当前句子中出现的次数。例如第一行的第二个元素 2 为“特别”,说明“特别”在第一句话“我特别特别喜欢看电影”总一共出现了两次。
  • 每一行一共有 22 个元素,说明所有的语料一共有 22 个词。

可以看到整个的输出结果为一个稀疏矩阵,尤其是当词汇量变大后,矩阵会更加稀疏。

计算余弦相似度

该步骤需要有一点数学基础。

余弦相似度:用来衡量两个向量的相似程度。值在 -1 到 1 之间,值越接近 1,两个向量越相似。越接近 -1,表示两个向量越不相似。值为 0 时,表示没有明显的相似性。

公式如下:

1
(A * B) / (||A|| * ||B||)

(A * B) 为向量的点积,||A|| 和 ||B|| 表示向量的长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 导入 numpy 库,用于计算余弦相似度  
import numpy as np
# 定义余弦相似度函数
def cosine_similarity(vec1, vec2):
dot_product = np.dot(vec1, vec2) # 计算向量 vec1 和 vec2 的点积
norm_a = np.linalg.norm(vec1) # 计算向量 vec1 的范数
norm_b = np.linalg.norm(vec2) # 计算向量 vec2 的范数
return dot_product / (norm_a * norm_b) # 返回余弦相似度
# 初始化一个全 0 矩阵,用于存储余弦相似度
similarity_matrix = np.zeros((len(corpus), len(corpus)))
# 计算每两个句子之间的余弦相似度
for i in range(len(corpus)):
for j in range(len(corpus)):
similarity_matrix[i][j] = cosine_similarity(bow_vectors[i],
bow_vectors[j])

可视化余弦相似度

1
2
3
4
5
6
7
8
9
10
11
12
13
# 导入 matplotlib 库,用于可视化余弦相似度矩阵  
import matplotlib.pyplot as plt
plt.rcParams["font.family"]=['SimHei'] # 用来设定字体样式
plt.rcParams['font.sans-serif']=['SimHei'] # 用来设定无衬线字体样式
plt.rcParams['axes.unicode_minus']=False # 用来正常显示负号
fig, ax = plt.subplots() # 创建一个绘图对象
# 使用 matshow 函数绘制余弦相似度矩阵,颜色使用蓝色调
cax = ax.matshow(similarity_matrix, cmap=plt.cm.Blues)
fig.colorbar(cax) # 条形图颜色映射
ax.set_xticks(range(len(corpus))) # x 轴刻度
ax.set_yticks(range(len(corpus))) # y 轴刻度
ax.set_xticklabels(corpus, rotation=45, ha='left') # 刻度标签 ax.set_yticklabels(corpus) # 刻度标签为原始句子
plt.show() # 显示图形

最终获取到如下的结果:
image.png
每个单元格表示两个句子之间的相似度。

总结

缺点:

  1. 采用了稀疏矩阵,每个单词是一个维度,计算效率较低。
  2. 忽略了文本的上下文信息。