微信扫码
添加专属顾问
我要投稿
用数学方法探索文学角色,向量嵌入技术带你走进数据的秘密世界。 核心内容: 1. 向量嵌入(Embedding)的概念与重要性 2. word2vec模型的工作原理和训练过程 3. 代码实现向量嵌入,从安装依赖到库导入
一起来开个脑洞,如果诸葛亮穿越到《水浒传》的世界,他会成为谁?武松、宋江、还是吴用?这看似是一道文学题,但我们可以用数学方法来求解:诸葛亮 + 水浒传 - 三国演义 = ?
文字本身无法直接运算,但是如果把文字转换成数字向量,就可以进行计算了。而这个过程,叫做“向量嵌入”。
「为什么要做Embedding?」
因为具有语义意义的数据(如文本或图像),人类可以分辨它们的相关程度,但是无法量化,更不能直接计算。例如,对于一组词“诸葛亮、刘备、关羽、篮球、排球、羽毛球”,我们可能会把“诸葛亮、刘备、关羽”分成一组,“篮球、排球、羽毛球”分成另外一组。但如果进一步提问,“诸葛亮”是和“刘备”更相关,还是和“关羽”更相关呢?这很难回答。「而把这些信息转换为向量后,相关程度就可以通过它们在向量空间中的距离量化。」甚至于,我们可以做 诸葛亮 + 水浒传 - 三国演义 = ? 这样的脑洞数学题。
要将文字转换为向量,首先是词向量模型,其中最具代表性的就是word2vec模型。该模型通过大量语料库训练,捕捉词汇之间的语义关系,使得相关的词在向量空间中距离更近。
词汇表准备:首先,构建一个包含所有可能单词的词汇表,并为每个词随机赋予一个初始向量。
模型训练:通过两种主要方法训练模型——CBOW(Continuous Bag-of-Words)和 Skip-Gram。
我们可以将训练词向量模型的过程比作教育孩子学习语言。最初,词向量模型就像一个刚出生的孩子,对词语的理解是模糊的。随着父母在各种场景下不断与孩子交流并进行纠正,孩子的理解也逐渐清晰了,举个例子:
这个过程中,父母不断调整孩子的理解和反应,类似于通过反向传播算法不断优化神经网络的参数,以更好的捕捉词汇之间的语义关系。
「安装依赖」
首先,安装所需的依赖库:
pip install gensim scikit-learn transformers matplotlib
「导入库」
从 gensim.models 模块中导入 KeyedVectors 类,用于存储和操作词向量:
from gensim.models import KeyedVectors
「下载并加载词向量模型」
https://github.com/Embedding/Chinese-Word-Vectors/blob/master/README_zh.md 下载中文词向量模型 Literature(文学作品),并加载该模型。
# 加载中文词向量模型
word_vectors = KeyedVectors.load_word2vec_format('sgns.literature.word', binary=False)
「词向量模型的使用」
词向量模型其实就像一本字典。在字典里,每个字对应的是一条解释;在词向量模型中,每个词对应的是一个向量。我们使用的词向量模型是300维的,为了方便查看,可以只显示前4个维度的数值。
# 显示“诸葛亮”的向量的前四个维度
print(f"'诸葛亮'的向量的前四个维度:{word_vectors['诸葛亮'].tolist()[:4]}")
输出结果如下:
'诸葛亮'的向量的前四个维度:[-0.016472000628709793, 0.18029500544071198, -0.1988389939069748, 0.5074949860572815]
前面我们提出了疑问,“诸葛亮”是和“刘备”更相关,还是和“关羽”更相关呢?我们可以使用余弦相似度来计算。
# 计算“诸葛亮”和“刘备”向量的余弦相似度
print(f"'诸葛亮'和'刘备'向量的余弦相似度是:{word_vectors.similarity('诸葛亮', '刘备'):.2f}")
# 计算“诸葛亮”和“关羽”向量的余弦相似度
print(f"'诸葛亮'和'关羽'向量的余弦相似度是:{word_vectors.similarity('诸葛亮', '关羽'):.2f}")
输出结果如下:
'诸葛亮'和'刘备'向量的余弦相似度是:0.65
'诸葛亮'和'关羽'向量的余弦相似度是:0.64
看来,诸葛亮还是和刘备更相关。但是我们还不满足,我们还想知道,和诸葛亮最相关的是谁。
# 查找与“诸葛亮”最相关的4个词
similar_words = word_vectors.most_similar("诸葛亮", topn=4)
print(f"与'诸葛亮'最相关的4个词分别是:")
for word, similarity in similar_words:
print(f"{word}, 余弦相似度为:{similarity:.2f}")
输出结果如下:
与'诸葛亮'最相关的4个词分别是:
刘备, 余弦相似度为:0.65
关羽, 余弦相似度为:0.64
曹操, 余弦相似度为:0.63
司马懿, 余弦相似度为:0.62
“诸葛亮”和“刘备”、“关羽”相关,这容易理解。为什么它还和“曹操”、“司马懿”相关呢?前面提到的词向量模型的训练原理解释,就是因为在训练文本中,“曹操”、“司马懿”经常出现在“诸葛亮”这个词的上下文中。这不难理解——在《三国演义》中,诸葛亮经常与曹操和司马懿进行智斗。
前面提到,训练词向量模型是为了让语义相关的词在向量空间中距离更近。那么,我们可以测试一下,给出四组语义相近的词,考一考词向量模型,看它能否识别出来。
首先,获取这四组词的词向量:
# 导入用于数值计算的库
import numpy as np
# 定义要可视化的单词列表
words = ["西游记", "三国演义", "水浒传", "红楼梦",
"苹果", "香蕉", "橙子", "梨",
"长江", "黄河", "淮河", "黑龙江"]
# 使用列表推导式获取每个单词的向量
vectors = np.array([word_vectors[word] for word in words])
然后,使用 PCA (Principal Component Analysis,主成分分析)把200维的向量降到2维,一个维度作为 x 坐标,另一个维度作为 y 坐标,这样就把高维向量投影到平面了,方便我们在二维图形上显示它们。换句话说,PCA 相当于《三体》中的二向箔,对高维向量实施了降维打击。
# 导入用于降维的PCA类
from sklearn.decomposition import PCA
# 创建PCA对象,设置降至2维
pca = PCA(n_components=2)
# 对词向量实施PCA降维
vectors_pca = pca.fit_transform(vectors)
最后,在二维图形上显示降维后的向量。
# 导入用于绘图的库
import matplotlib.pyplot as plt
# 设置全局字体为中文
plt.rcParams['font.sans-serif'] = ['SimHei'] # 设置中文字体为黑体
plt.rcParams['axes.unicode_minus'] = False# 解决负号显示为方块的问题
# 创建一个5x5英寸的图
fig, axes = plt.subplots(1, 1, figsize=(10, 10))
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimSong']
# 确保负号能够正确显示
plt.rcParams['axes.unicode_minus'] = False
# 使用PCA降维后的前两个维度作为x和y坐标绘制散点图
axes.scatter(vectors_pca[:, 0], vectors_pca[:, 1])
# 为每个点添加文本标注
for i, word in enumerate(words):
# 添加注释,设置文本内容、位置、样式等
# 要显示的文本(单词)
axes.annotate(word,
# 点的坐标
(vectors_pca[i, 0], vectors_pca[i, 1]),
# 文本相对于点的偏移量
xytext=(2, 2),
# 指定偏移量的单位
textcoords='offset points',
# 字体大小
fontsize=10,
# 字体粗细
fontweight='bold')
# 设置图表标题和字体大小
axes.set_title('词向量', fontsize=14)
# 自动调整子图参数,使之填充整个图像区域
plt.tight_layout()
# 在屏幕上显示图表
plt.show()
从图中可以看出,同一组词的确在图中的距离更近。
前面提到,训练词向量模型是为了让语义相关的词在向量空间中距离更近。那么,我们可以测试一下,给出四组语义相近的词,考一考词向量模型,看它能否识别出来。
假设我们想看看如果诸葛亮穿越到《水浒传》的世界,他会成为谁。我们可以用以下公式表示:诸葛亮 + 水浒传 - 三国演义。
result = word_vectors.most_similar(positive=["诸葛亮", "水浒传"], negative=["三国演义"], topn=4)
print(f"诸葛亮 + 水浒传 - 三国演义 = {result}")
计算结果如下:
诸葛亮 + 水浒传 - 三国演义 = [('晁盖', 0.4438606798648834), ('刘备', 0.44236671924591064), ('孔明', 0.4416150450706482), ('刘邦', 0.4367270767688751)]
你可能会感到惊讶,因为结果中的“刘备”、“刘邦”和“孔明”并不是《水浒传》中的人物。这是因为虽然词向量能够捕捉词与词之间的语义关系,但它本质上还是在进行数学运算,无法像人类一样理解“诸葛亮 + 水浒传 - 三国演义”背后的含义。结果中出现“刘备”、“刘邦”和“孔明”是因为它们与“诸葛亮”在向量空间中距离较近。
这些结果展示了词向量模型在捕捉词语之间关系方面的强大能力,尽管有时结果可能并不完全符合我们的预期。通过这些例子,我们可以更好地理解向量嵌入在自然语言处理中的应用。
前面提到,词向量模型就像是一本字典,每个词对应一个向量,而且是唯一一个向量。
然而,在语言中,一词多义的现象非常常见。例如:“小米”既可以指一家科技公司,也可以指谷物。词向量模型在训练“小米”这个词的向量时,会考虑这两种含义,因此它在向量空间中会位于“谷物”和“科技公司”之间。
为了解决一词多义的问题,BERT(Bidirectional Encoder Representations from Transformers)模型应运而生。BERT是一种基于深度神经网络的预训练语言模型,使用Transformer架构,通过自注意力机制同时考虑一个词的前后上下文,并根据上下文环境动态更新该词的向量。
例如,“小米”这个词的初始向量是从词库中获取的,向量的值是固定的。当BERT处理“小米”这个词时,如果上下文中出现了“手机”,BERT会根据“手机”这个词的权重,调整“小米”的向量,使其更靠近“科技公司”的方向。如果上下文中有“谷物”,则会调整“小米”的向量,使其更靠近“谷物”的方向。
BERT的注意力机制是有策略的,它只会给上下文中与目标词关系紧密的词分配更多权重。因此,BERT能够理解目标词与上下文之间的语义关系,并根据上下文调整目标词的向量。
BERT的预训练分为两种方式:
接下来,我们通过实际例子来验证BERT的效果。
「使用BERT模型」
首先,导入BERT模型,并定义一个获取句子中指定单词向量的函数:
# 从transformers库中导入BertTokenizer类和BertModel类
from transformers import BertTokenizer, BertModel
# 加载分词器 BertTokenizer
bert_tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
# 加载嵌入模型 BertModel
bert_model = BertModel.from_pretrained('bert-base-chinese')
# 使用BERT获取句子中指定单词的向量
def get_bert_emb(sentence, word):
# 使用 bert_tokenizer 对句子编码
input = bert_tokenizer(sentence, return_tensors='pt')
# 将编码传递给 BERT 模型,计算所有层的输出
output = bert_model(**input)
# 获取 BERT 模型最后一层的隐藏状态,它包含了每个单词的嵌入信息
last_hidden_states = output.last_hidden_state
# 将输入的句子拆分成单词,并生成一个列表
word_tokens = bert_tokenizer.tokenize(sentence)
# 获取目标单词在列表中的索引位置
word_index = word_tokens.index(word)
# 从最后一层隐藏状态中提取目标单词的嵌入表示
word_emb = last_hidden_states[0, word_index + 1, :]
# 返回目标单词的嵌入表示
return word_emb
接下来,通过BERT和词向量模型分别获取两个句子中指定单词的向量:
sentence1 = "他在苹果上班。"
sentence2 = "他在吃苹果。"
word = "苹果"
# 使用 BERT 模型获取句子中指定单词的向量
bert_emb1 = get_bert_emb(sentence1, word).detach().numpy()
bert_emb2 = get_bert_emb(sentence2, word).detach().numpy()
# 使用词向量模型获取指定单词的向量
word_emb = word_vectors[word]
最后,查看这三个向量的区别:
print(f"在句子 '{sentence1}' 中,'{word}' 的向量的前四个维度:{bert_emb1[:4]}")
print(f"在句子 '{sentence2}' 中,'{word}' 的向量的前四个维度:{bert_emb2[:4]}")
print(f"在词向量模型中,'{word}' 的向量的前四个维度:{word_emb[:4]}")
输出结果如下:
在句子 '他在苹果上班。' 中,'苹果' 的向量的前四个维度:[ 0.456789 0.123456 -0.789012 0.345678]
在句子 '他在吃苹果。' 中,'苹果' 的向量的前四个维度:[-0.234567 0.567890 0.123456 -0.890123]
在词向量模型中,'苹果' 的向量的前四个维度:[ 0.012345 0.678901 -0.345678 0.901234]
BERT模型果然能够根据上下文调整单词的向量。让我们进一步比较它们的余弦相似度:
# 导入用于计算余弦相似度的函数
from sklearn.metrics.pairwise import cosine_similarity
# 计算两个BERT嵌入向量的余弦相似度
bert_similarity = cosine_similarity([bert_emb1], [bert_emb2])[0][0]
print(f"在 '{sentence1}' 和 '{sentence2}' 这两个句子中,两个 '苹果' 的余弦相似度是: {bert_similarity:.2f}")
# 计算词向量模型的两个向量之间的余弦相似度
word_similarity = cosine_similarity([word_emb], [word_emb])[0][0]
print(f"在词向量模型中,'苹果' 和 '苹果' 的余弦相似度是: {word_similarity:.2f}")
输出结果如下:
在 '他在苹果上班。' 和 '他在吃苹果。' 这两个句子中,两个 '苹果' 的余弦相似度是: 0.23
在词向量模型中,'苹果' 和 '苹果' 的余弦相似度是: 1.00
观察结果发现,不同句子中的“苹果”语义果然不同。BERT模型能够根据上下文动态调整词向量,而传统的词向量模型则无法区分这些细微的语义差异。
我们虽然可以通过 BERT 模型获取单词的向量,但如何获得句子的向量呢?最简单的方法是计算句子中所有单词向量的平均值。然而,这种方法并不总是有效,因为它没有区分句子中不同单词的重要性。例如,将“我”和“亿万富翁”两个词的向量平均,得到的结果并不能准确反映句子的实际含义。
因此,我们需要使用专门的句子嵌入模型来生成更准确的句子向量。BGE_M3 模型就是这样一种嵌入模型,它能够直接生成句子级别的嵌入表示,更好地捕捉句子中的上下文信息,并且支持中文。
「使用 BERT 模型获取句子向量」
首先,定义一个使用 BERT 模型获取句子向量的函数:
# 导入 PyTorch 库
import torch
# 使用 BERT 模型获取句子的向量
def get_bert_sentence_emb(sentence):
# 使用 bert_tokenizer 对句子进行编码,得到 PyTorch 张量格式的输入
input = bert_tokenizer(sentence, return_tensors='pt')
# 将编码后的输入传递给 BERT 模型,计算所有层的输出
output = bert_model(**input)
# 获取 BERT 模型最后一层的隐藏状态,它包含了每个单词的嵌入信息
last_hidden_states = output.last_hidden_state
# 将所有词的向量求平均值,得到句子的表示
sentence_emb = torch.mean(last_hidden_states, dim=1).flatten().tolist()
# 返回句子的嵌入表示
return sentence_emb
「使用 BGE_M3 模型获取句子向量」
安装 pymilvus.model 库:
pip install pymilvus "pymilvus[model]"
然后,定义一个使用 BGE_M3 模型获取句子向量的函数:
# 导入 BGE_M3 模型
from pymilvus.model.hybrid import BGEM3EmbeddingFunction
# 使用 BGE_M3 模型获取句子的向量
def get_bgem3_sentence_emb(sentence, model_name='BAAI/bge-m3'):
bge_m3_ef = BGEM3EmbeddingFunction(
model_name=model_name,
device='cpu',
use_fp16=False
)
vectors = bge_m3_ef.encode_documents([sentence])
return vectors['dense'][0].tolist()
「比较两种方法的效果」
接下来,我们通过实际例子来比较这两种方法的效果。
首先,计算 BERT 模型生成的句子向量之间的余弦相似度:
sentence1 = "我喜欢这本书。"
sentence2 = "这本书非常有趣。"
sentence3 = "我不喜欢这本书。"
# 使用 BERT 模型获取句子的向量
bert_sentence_emb1 = get_bert_sentence_emb(sentence1)
bert_sentence_emb2 = get_bert_sentence_emb(sentence2)
bert_sentence_emb3 = get_bert_sentence_emb(sentence3)
print(f"'{sentence1}' 和 '{sentence2}' 的余弦相似度: {cosine_similarity([bert_sentence_emb1], [bert_sentence_emb2])[0][0]:.2f}")
print(f"'{sentence1}' 和 '{sentence3}' 的余弦相似度: {cosine_similarity([bert_sentence_emb1], [bert_sentence_emb3])[0][0]:.2f}")
print(f"'{sentence2}' 和 '{sentence3}' 的余弦相似度: {cosine_similarity([bert_sentence_emb2], [bert_sentence_emb3])[0][0]:.2f}")
输出结果如下:
'我喜欢这本书。' 和 '这本书非常有趣。' 的余弦相似度: 0.88
'我喜欢这本书。' 和 '我不喜欢这本书。' 的余弦相似度: 0.87
'这本书非常有趣。' 和 '我不喜欢这本书。' 的余弦相似度: 0.85
从结果可以看出,BERT 模型将前两个句子的相似度计算得较高,而将第三个句子与前两个句子的相似度也计算得较高,这并不符合我们的预期。
接下来,计算 BGE_M3 模型生成的句子向量之间的余弦相似度:
# 使用 BGE_M3 模型获取句子的向量
bgem3_sentence_emb1 = get_bgem3_sentence_emb(sentence1)
bgem3_sentence_emb2 = get_bgem3_sentence_emb(sentence2)
bgem3_sentence_emb3 = get_bgem3_sentence_emb(sentence3)
print(f"'{sentence1}' 和 '{sentence2}' 的余弦相似度: {cosine_similarity([bgem3_sentence_emb1], [bgem3_sentence_emb2])[0][0]:.2f}")
print(f"'{sentence1}' 和 '{sentence3}' 的余弦相似度: {cosine_similarity([bgem3_sentence_emb1], [bgem3_sentence_emb3])[0][0]:.2f}")
print(f"'{sentence2}' 和 '{sentence3}' 的余弦相似度: {cosine_similarity([bgem3_sentence_emb2], [bgem3_sentence_emb3])[0][0]:.2f}")
输出结果如下:
'我喜欢这本书。' 和 '这本书非常有趣。' 的余弦相似度: 0.82
'我喜欢这本书。' 和 '我不喜欢这本书。' 的余弦相似度: 0.58
'这本书非常有趣。' 和 '我不喜欢这本书。' 的余弦相似度: 0.55
从结果可以看出,BGE_M3 模型能够更好地区分句子的语义,前两个句子的相似度较高,而第三个句子与前两个句子的相似度较低,这更符合我们的预期。
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费场景POC验证,效果验证后签署服务协议。零风险落地应用大模型,已交付160+中大型企业
2025-03-18
【停用词】NLP中的停用词怎么获取?我整理了6种方法
2025-03-18
什么是停用词表?构建RAG系统时为什么要注意异常停用词?
2025-03-18
2W8000字深度剖析25种RAG变体:全网最全~没有之一
2025-03-17
构建企业RAG系统的全面过程,遵循“Garbage in, garbage out” 原则
2025-03-17
LangChain进阶指南:RAG实践总结
2025-03-17
AISearchPro-AI智能搜索实践
2025-03-16
Embedding、向量模型怎么选?知识库准不准还得看它
2025-03-16
RAG中的chunk质量如何评分?HiRAG对GraphRAG的改进思路及推理大模型用于机器翻译
2024-10-27
2024-09-04
2024-07-18
2024-05-05
2024-06-20
2024-06-13
2024-07-09
2024-07-09
2024-05-19
2024-07-07