Python 现代开发学习笔记

(五)用向量比较问题与材料

1. 从 NumPy 手算余弦相似度

先用第二章的字符串计数做一个能够手算的编码器。词表固定为“切片、共享、复制”,每个文本依次统计这三个词出现的次数。同一列始终表示同一个词,因此问题和文档可以放到相同的坐标系里比较。

"""先用可手算的关键词计数,解释文本怎样变成定长向量。"""
import numpy as np

vocabulary = ["切片", "共享", "复制"]
texts = ["切片介绍:切片共享数据。", "共享数据;复制后得到复制品。", "切片。"]
question = "切片共享"
# 每列的含义固定:第 0 列数切片,第 1 列数共享,第 2 列数复制。
documents = np.array([[text.count(word) for word in vocabulary] for text in texts], dtype=float)
query = np.array([question.count(word) for word in vocabulary], dtype=float)
assert documents.tolist() == [[2., 1., 0.], [0., 1., 2.], [1., 0., 0.]]
assert query.tolist() == [1., 1., 0.]
print("列:", vocabulary, "文档:", documents.tolist(), "问题:", query.tolist())

第一篇材料中“切片”出现两次、“共享”出现一次,“复制”没出现,因此是 (2,1,0);第二篇是 (0,1,2),第三篇是 (1,0,0)。问题“切片共享”得到 q=(1,1,0)。这就得到了接下来手算所用的三个文档向量。

用第十一章的点积计算,q·d0=1×2+1×1+0×0=3。向量长度是各分量平方和的平方根,所以 ||q||=√(1²+1²+0²)=√2,||d0||=√5;除以双方长度,得到 3/√10≈0.948683。另外两项分别是 1/√10≈0.316228、1/√2≈0.707107;排序得到 d0、d2、d1。

cosine⁡(q,d)=q⋅d∥q∥∥d∥ \operatorname{cosine}(q,d)=\frac{q\cdot d}{\|q\|\|d\|}

"""逐项核对向量相似度:点积、长度、归一化与排序。"""
import numpy as np

query = np.array([1., 1., 0.])
documents = np.array([[2., 1., 0.], [0., 1., 2.], [1., 0., 0.]])
# 每一行是一篇文档,axis=1 得到三个文档各自的长度。
unit_query = query / np.linalg.norm(query)
unit_documents = documents / np.linalg.norm(documents, axis=1, keepdims=True)
scores = unit_documents @ unit_query
order = np.argsort(-scores)
np.testing.assert_allclose(scores, [3 / np.sqrt(10), 1 / np.sqrt(10), 1 / np.sqrt(2)])
assert order.tolist() == [0, 2, 1]
print("得分:", scores.round(6).tolist(), "前两项:", order[:2].tolist())
逐项算点积和向量长度,再推导三个文档的排序

keepdims=True 让三个文档长度保留为 (3,1),可以逐行除以自己的长度。归一化后,每个向量长度为 1,余弦比较就能用矩阵乘法完成。这里的得分不是正确率,也不是答案可信度。

np.linalg.norm 计算这里的向量长度;np.argsort 返回排序后的下标。给 scores 加负号后,从小到大排负数,就把较大的原始得分放到了前面。最后使用这些下标取回原文,而不只是输出一串分数。

换成问题“数据库”,这个三词词表会得到全零计数,说明当前表示没有保存它的信息。完整资料需要更丰富的表示,下面继续看项目怎样替换编码器。

2. 文本向量与语义向量

完整项目提供 CharacterEmbeddings:先转小写、去空白,把相邻两个字符组成一组,固定哈希到 4096 个位置,累计后归一化。这个实现让不同进程得到相同表示,用来观察检索的数据流。

它按字面重合比较文本。“副本”和“独立数据”语义接近,但字符不同,未必得到相近向量;哈希位置也可能碰撞。语义向量模型通过训练学习文本表示,适合进一步比较近义表达。两者都可以实现 Embeddings 接口:embed_documents 接收文档列表,embed_query 接收单个问题。

已经在本机准备向量模型时,可以通过 --embedding YOUR_EMBEDDING_MODEL 替换。项目用 langchain_ollama.OllamaEmbeddings 同时编码文档与问题。更换模型后重新建立文档索引,保持维度、模型版本与预处理一致。Ollama 向量接口

笔记正文与全部题解 · Ctrl / ⌘ K