不再只会调包:手撸 Embedding 算法实现向量搜索优化

前言:万物皆可向量化

在 RAG (Retrieval-Augmented Generation) 大火的今天,大家都在谈论 Vector Database(向量数据库)Embedding。你可能熟练使用 sentence-transformers 或者 OpenAI 的 text-embedding-3 接口,把一句话变成一个 1536 维的数组,然后扔进 ChromaDB 里做相似度搜索。

但是,这串数字背后的魔法到底是什么?为什么 King - Man + Woman = Queen

既然本博客主打“折腾”和“手搓”,今天我们就不调包,尝试从最基础的 Word2Vec 思想出发,用 Python + NumPy 手写一个简易的 Embedding 训练和搜索系统。只有亲手实现了它,你才能真正理解所谓的“语义空间”长什么样。

1. 词向量的直觉

计算机不认识字,只认识数字。
最简单的表示法是 One-Hot Encoding

  • Dictionary: [I, drink, coffee, love]
  • I: [1, 0, 0, 0]
  • coffee: [0, 0, 1, 0]

问题是:这种表示法里,“coffee”和“drink”的向量是正交的(点积为0),没有任何关联。但在人类逻辑里,喝咖啡是很搭配的词。

我们希望有一个 Dense Vector(稠密向量),比如:

  • coffee: [0.8, 0.1] (假设维度1是饮料属性,维度2是固体属性)
  • tea: [0.75, 0.05]
  • stone: [0.01, 0.9]

这样,coffeetea 在空间距离上就很近。

2. 训练的核心:Skip-Gram 模型

Word2Vec 的 Skip-Gram 模型思想简单粗暴:一个词的含义,由它周围的词(Context)决定。
“如果你经常出现在‘喝’和‘杯子’中间,那你大概率是一种饮料。”

我们需要训练一个神经网络:

  • 输入:中间词 (Today is sunny day) -> ‘sunny’
  • 输出:预测上下文词 -> ‘is’, ‘day’

定义模型结构

我们不用 PyTorch,直接用 NumPy 手写前向传播和反向传播。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import numpy as np

class SimpleWord2Vec:
def __init__(self, vocab_size, embed_dim, learning_rate=0.01):
self.vocab_size = vocab_size
self.embed_dim = embed_dim
self.lr = learning_rate

# W1 就是我们梦寐以求的 Embedding Matrix (V x D)
self.W1 = np.random.randn(vocab_size, embed_dim) * 0.01
# W2 是输出层的权重 (D x V)
self.W2 = np.random.randn(embed_dim, vocab_size) * 0.01

def softmax(self, x):
e_x = np.exp(x - np.max(x)) # 防止溢出
return e_x / e_x.sum(axis=0)

def forward(self, word_idx):
# 1. Look up embedding (其实就是取 W1 的一行)
h = self.W1[word_idx]
# 2. Output layer
u = np.dot(self.W2.T, h)
# 3. Softmax
y_pred = self.softmax(u)
return h, y_pred, word_idx

def backward(self, error, h, word_idx):
# 这是一个极简版的反向传播
# error = y_pred - y_true

# dW2
dW2 = np.outer(h, error)

# dW1 (只更新对应的那个 embedding vector)
dh = np.dot(self.W2, error)
dW1 = dh # 因为输入是 one-hot,所以梯度直接传给那一行

# Update weights
self.W2 -= self.lr * dW2
self.W1[word_idx] -= self.lr * dW1

看到了吗?训练过程极其类似一个分类任务。我们在不断调整 W1,使得它能基于当前词,最大概率地预测出上下文。当训练收敛时,W1 中的每一行,就是这个词的 Vector!

3. 实战:训练并可视化

假设我们有一小段语料:

“I like embedding”, “I like deep learning”, “deep learning is fun”

经过简单的预处理和训练(省略千字数据清洗代码):

1
2
3
4
vocab = ["I", "like", "embedding", "deep", "learning", "is", "fun"]
model = SimpleWord2Vec(len(vocab), embed_dim=2) # 用2维方便画图

# ... 训练 Loop 10000 次 ...

训练完成后,我们将 model.W1 里的向量画在二维坐标系上。

graph LR
    axis_x((X-axis)) --> axis_y((Y-axis))
    
    %% 这里模拟训练后的向量位置
    like((like)) 
    deep((deep))
    learning((learning))
    embedding((embedding))
    
    style like fill:#f96
    style deep fill:#69f
    style learning fill:#69f
    
    %% 我们可以看到 deep 和 learning 靠得很近
    %% embedding 可能也是个技术名词,离它们不远
    deep --- learning
    learning --- embedding
    like -.-> deep

你会发现神奇的现象:deeplearning 这种经常共现的词,向量距离会非常近。

4. 向量搜索与 Cosine Similarity

有了向量,怎么做搜索?比如我想搜“AI”,系统应该返回“Deep Learning”。

我们使用 余弦相似度 (Cosine Similarity) 来衡量距离。

$$ \text{Cosine}(A, B) = \frac{A \cdot B}{|A| |B|} $$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def cosine_similarity(v1, v2):
dot_product = np.dot(v1, v2)
norm_v1 = np.linalg.norm(v1)
norm_v2 = np.linalg.norm(v2)
return dot_product / (norm_v1 * norm_v2)

# 搜索函数
def search(query_word, model, vocab_map):
q_idx = vocab_map[query_word]
q_vec = model.W1[q_idx]

scores = {}
for word, idx in vocab_map.items():
if word == query_word: continue
score = cosine_similarity(q_vec, model.W1[idx])
scores[word] = score

# Sort by score
return sorted(scores.items(), key=lambda x: x[1], reverse=True)[:3]

5. 从 Demo 到生产环境

当然,我们手写的这个只是玩具:

  1. Context Window 也太小了:现代模型用 Transformer,能看 128k 甚至 1M 的 context。
  2. 静态 Embedding:Word2Vec 里,“Apple” (苹果) 和 “Apple” (公司) 的向量是一样的。BERT/GPT 解决了多义词问题(Dynamic Embedding)。
  3. 计算效率:当你有 10 亿个向量时,一个个算 Cosine 简直是自杀。你需要 HNSW (Hierarchical Navigable Small World) 算法或者 Faiss 这样的库来做近似最近邻搜索 (ANN)。

但无论技术如何迭代,核心思想从未改变:将人类的离散符号映射到连续的数学空间,计算距离。

下次当你直接调用 OpenAI 的 API 时,希望你能透过那枯燥的 JSON 响应,看到向量空间里那些璀璨的星辰。

这里是 QixYuan’s Tech Blog,我们下期见。


不再只会调包:手撸 Embedding 算法实现向量搜索优化
https://www.qixyuan.top/2025/03/20/4-handwritten-embeddings/
作者
QixYuan
发布于
2025年3月20日
许可协议