跳转至

Embedding 与相似度

演示文本向量化的核心概念:将文本映射为定长向量,并通过余弦相似度衡量语义接近程度。 为支持离线测试,提供基于哈希的确定性向量器 HashEmbedder;真实场景应使用 OpenAI Embedding API 或开源模型(如 bge-m3)。 对应教程:第1卷《AI 基础》第4章 Embedding。

文件路径: examples/volume-01-ai-fundamentals\embedding_demo.py


源代码

"""Embedding 与相似度示例。

演示文本向量化的核心概念:将文本映射为定长向量,并通过余弦相似度衡量语义接近程度。
为支持离线测试,提供基于哈希的确定性向量器 ``HashEmbedder``;真实场景应使用 OpenAI
Embedding API 或开源模型(如 bge-m3)。

对应教程:第1卷《AI 基础》第4章 Embedding。
"""

from __future__ import annotations

import hashlib
import math
import re
from dataclasses import dataclass


def tokenize(text: str) -> list[str]:
    """简易分词:英文按单词、中文按字。

    Args:
        text: 原始文本。

    Returns:
        token 列表(小写化)。
    """
    text = text.lower()
    # 提取英文单词
    words = re.findall(r"[a-z]+", text)
    # 提取中文字符
    chars = re.findall(r"[\u4e00-\u9fff]", text)
    return words + chars


@dataclass
class EmbeddingResult:
    """Embedding 结果。

    Attributes:
        text: 原始文本。
        vector: 向量。
        dim: 向量维度。
    """

    text: str
    vector: list[float]
    dim: int


class HashEmbedder:
    """基于哈希的确定性文本向量器。

    通过分词后对每个 token 哈希到固定维度并累加,构造稀疏向量。
    再做 L2 归一化,使其可直接用于余弦相似度(归一化后点积 = 余弦)。

    特点:
        - 确定性:相同文本始终得到相同向量。
        - 无需训练、无需外部模型。
        - 仅用于教学,语义能力远弱于真实 Embedding 模型。
    """

    def __init__(self, dim: int = 64, seed: int = 0) -> None:
        """初始化向量器。

        Args:
            dim: 输出向量维度。
            seed: 哈希种子,用于产生不同投影。

        Raises:
            ValueError: 维度非正时。
        """
        if dim <= 0:
            raise ValueError("维度必须为正整数")
        self.dim = dim
        self.seed = seed

    def _hash_token(self, token: str) -> tuple[int, float]:
        """将 token 哈希为一个维度索引与一个符号(+1/-1)。

        Args:
            token: 输入 token。

        Returns:
            ``(index, sign)`` 元组。
        """
        digest = hashlib.md5(f"{self.seed}:{token}".encode()).hexdigest()  # noqa: S324
        index = int(digest[:8], 16) % self.dim
        sign = 1.0 if int(digest[8:16], 16) % 2 == 0 else -1.0
        return index, sign

    def embed(self, text: str) -> EmbeddingResult:
        """将文本转换为向量。

        Args:
            text: 原始文本。

        Returns:
            ``EmbeddingResult``,向量已 L2 归一化。
        """
        vec = [0.0] * self.dim
        tokens = tokenize(text)
        for tok in tokens:
            index, sign = self._hash_token(tok)
            vec[index] += sign
        norm = math.sqrt(sum(v * v for v in vec))
        if norm > 0:
            vec = [v / norm for v in vec]
        return EmbeddingResult(text=text, vector=vec, dim=self.dim)

    def embed_batch(self, texts: list[str]) -> list[EmbeddingResult]:
        """批量向量化。

        Args:
            texts: 文本列表。

        Returns:
            Embedding 结果列表。
        """
        return [self.embed(t) for t in texts]


def cosine_similarity(vec_a: list[float], vec_b: list[float]) -> float:
    """计算两个向量的余弦相似度。

    Args:
        vec_a: 向量 A。
        vec_b: 向量 B。

    Returns:
        余弦相似度,范围 ``[-1, 1]``。

    Raises:
        ValueError: 维度不一致或向量为零向量时。
    """
    if len(vec_a) != len(vec_b):
        raise ValueError(f"维度不一致: {len(vec_a)} vs {len(vec_b)}")
    dot = sum(a * b for a, b in zip(vec_a, vec_b, strict=True))
    norm_a = math.sqrt(sum(a * a for a in vec_a))
    norm_b = math.sqrt(sum(b * b for b in vec_b))
    if norm_a == 0 or norm_b == 0:
        raise ValueError("零向量无法计算余弦相似度")
    return dot / (norm_a * norm_b)


def find_most_similar(
    query: str,
    candidates: list[str],
    embedder: HashEmbedder,
) -> list[tuple[str, float]]:
    """在候选文本中检索与 query 最相似的文本(按相似度降序)。

    Args:
        query: 查询文本。
        candidates: 候选文本列表。
        embedder: 向量器实例。

    Returns:
        ``(text, score)`` 列表,按相似度降序排列。
    """
    query_vec = embedder.embed(query).vector
    scored: list[tuple[str, float]] = []
    for cand in candidates:
        cand_vec = embedder.embed(cand).vector
        score = cosine_similarity(query_vec, cand_vec)
        scored.append((cand, score))
    scored.sort(key=lambda x: x[1], reverse=True)
    return scored


def main() -> None:
    """演示 Embedding 与相似度检索。"""
    embedder = HashEmbedder(dim=128)
    candidates = [
        "AI Agent 能够自主决策并执行任务",
        "今天天气不错,适合出门散步",
        "RAG 通过检索增强大模型生成",
        "机器学习是人工智能的一个分支",
    ]
    query = "智能体可以自己做决定"
    print(f"查询: {query}\n")
    for text, score in find_most_similar(query, candidates, embedder):
        print(f"  {score:.4f}  {text}")


if __name__ == "__main__":
    main()