从 LSH 到 K-center-greedy:语义嵌入如何做数据去重、清洗与样本筛选
博客列表 主页

从 LSH 到 K-center-greedy:语义嵌入如何做数据去重、清洗与样本筛选

如果手里只有一堆 embedding 向量,没有别的先验信息,许多下游任务都可以改写成同一个问题:如何在高维空间里比较点与点之间的关系。 去重是在找“过近的点”,检索是在找“离查询最近的点”,聚类是在找“自然形成团块的点”,样本筛选是在找“最能覆盖全局的点”,推荐则是在找“和用户向量最匹配的点”。这篇文章不再只罗列工具,而是从最基本的数学对象出发,解释这些方法在向量空间里做了什么。

从一个向量开始:相似度、距离和归一化

设一条文本经过 embedding 模型后得到向量

\[x_i \in \mathbb{R}^d\]

最常用的第一步是做 L2 归一化:

\[\hat{x}_i = \frac{x_i}{\|x_i\|_2}\]

归一化之后,两个文本的余弦相似度就可以直接写成点积:

\[s(i,j)=\cos(\theta_{ij})=\hat{x}_i^\top \hat{x}_j\]

如果你更喜欢距离,也可以直接用欧氏距离。对单位向量来说,两者几乎是同一回事:

\[\|\hat{x}_i-\hat{x}_j\|_2^2 = 2 - 2\hat{x}_i^\top \hat{x}_j\]

这条公式很有用:只要向量先归一化,后面无论你用“最大相似度”还是“最小距离”,做的都是同一类比较。于是文本数据就从“字符串集合”变成了“点云”,许多数据清洗与检索问题,也变成了对这片点云做搜索、分组、连边和覆盖。

LSH:先把不可能的 O(n^2) 比较变成候选召回

假设你有 n 条文本,如果直接对所有样本做两两比较,复杂度接近

\[O(n^2)\]

n 到几十万甚至百万时,这基本不可用。LSH 的想法很朴素:不要先精确比所有点,而是先用一个很便宜的哈希函数,把“可能接近”的向量送进同一个桶里。

p.s. 很多工程算法都在处理 pairwise 带来的平方复杂度。LSH 处理向量相似,linear attention 处理 attention,Tournament 机制处理 reward 比较。目标不同,但压力都来自两两比较。

以最常见的随机超平面 LSH 为例,先随机采样一个向量,也就是一个高维空间的超平面。

\[r \sim \mathcal{N}(0, I)\]

然后定义一个 1 bit 哈希函数:

\[h_r(x)= \begin{cases} 1, & r^\top x \ge 0 \\ 0, & r^\top x < 0 \end{cases}\]

它的直觉很简单:看向量 x 落在超平面的哪一侧。若两个向量方向接近,它们落在同一侧的概率更大。对于角距离,还有一个很好记的结论:

\[\Pr[h_r(x)=h_r(y)] = 1 - \frac{\theta(x,y)}{\pi}\]

夹角越小,碰撞概率越高。实际工程里不会只用一个超平面,而会把多个 bit 拼成签名,再建多张哈希表。这样做的效果是:先用哈希召回候选,再在候选集合里做精确相似度计算。

这一步解决的问题不是“最终谁和谁重复”,而是“谁值得被拿去精查”。它特别适合初步清洗网页抓取语料、论坛帖子、模板化描述,因为它能快速把近似重复样本缩到一小批候选里。代价是它会有误召回和漏召回,所以 LSH 适合做第一层过滤,不适合独立做最终判定。

Faiss + 近邻图:把语义去重写成图问题

候选集合出来以后,下一步才是更精细的语义比较。这里最常见的动作是:给每个样本向量找 top-k 最近邻。记样本 i 的邻居集合为

\[N_k(i)=\operatorname{TopK}_j \ s(i,j)\]

如果你对每个 i 都算出 N_k(i),整批数据就可以被看成一张 kNN 图。Faiss 的作用,就是高效地完成这件事。小规模时可以直接做精确搜索;更大规模时,Faiss 会用 IVFPQHNSW 之类的近似近邻结构,把“全库逐一扫描”变成“在少量候选簇里搜索”,从而降低检索成本。

去重通常不是“搜到一个最像的样本就删”,而是先定义一条边:

\[E=\{(i,j)\mid s(i,j)\ge \tau\}\]

这里 $\tau$ 是相似度阈值。于是整批数据被写成图

\[G=(V,E)\]

图中连在一起的节点,就是一组高度相似的文本。接下来可以做两种事:

第一种是连通块去重。如果一组样本互相都很近,就把它们视为一个簇,只保留一个代表样本。第二种是边过滤 + 人工抽检。也就是只把相似度极高的边当成“硬重复”,对边界样本再做人工复核。这个流程比单纯看一对文本稳得多,因为它利用了局部几何结构,而不只是孤立的相似度值。

这里还有一个工程细节:最好先按类别或来源分桶,再在桶内建 Faiss 索引。因为 embedding 表达的是语义近,但业务上关心的常常是“同类语义近”。如果商品评论和商品标题混在一起建图,很容易出现语义很近但不该互相去重的误伤。

K-center-greedy:不是删最像的,而是留最有代表性的

去重解决的是冗余,但数据集还会有另一个问题:即使没有重复样本,数据也可能高度集中在几个密集区域。比如一万个样本几乎都在讲“某个类型漏洞”,去重后仍然会把训练或标注预算耗在同一片区域里。语义嵌入去重处理的是语义重复;如果 RAG 会把主题相近的内容大面积召回再重排,就还需要覆盖式采样来降低标注开销。K-center-greedy 可以被理解为一种由嵌入向量本身驱动的训练集采样;与之对应,人工标注中的分类与重采样技术也都在尝试减少随机抽样的弊端。

K-center 问题的目标是:用 k 个中心点,尽量覆盖全部样本,让最远样本到最近中心的距离尽可能小:

\[\min_{S:|S|=k} \max_{x \in X} \min_{c \in S} \|x-c\|_2\]

这个目标直接求最优很难,所以工程上常用 K-center-greedy。它的规则很直观:假设当前已经选出一个集合 $S_t$,下一步总是选那个离当前集合最远的点:

\[x_{t+1} = \arg\max_{x \in X \setminus S_t} \min_{c \in S_t} \|x-c\|_2\]

这条公式可以直接翻译成人话:每次都去找当前最没有被代表到的样本。 这样选出来的子集不会只盯着高频区域,而会自动把边界区域、长尾区域和孤立区域都照顾到。

所以 K-center-greedy 解决的不是“删重复”,而是“保覆盖”。如果你的下游任务是主动学习、人工标注、训练集压缩,它往往比随机抽样更有效,因为随机抽样会反复抽到密集区域,而 K-center-greedy 会主动把预算投向当前表示不足的区域。

检索、聚类、RAG、推荐,本质上都在复用同一套向量运算

一旦理解了“语义向量就是空间里的点”,许多看起来不同的任务只是换了目标函数。无论对象是查询、文档、用户、商品还是句子片段,第一步通常都是先把原始对象编码成 embedding;后面反复复用的,主要是几类基础操作:算相似度、找近邻、做聚合、控覆盖。任务名称不同,往往只是拿这些运算去优化不同指标,执行的操作在数学上差别并不大。

从数学上看,最常见的相似度要么是内积

\[s(x,y)=x^\top y\]

要么是余弦相似度

\[\cos(x,y)=\frac{x^\top y}{\|x\|_2\|y\|_2}\]

如果向量事先做了归一化,那么两者几乎就是同一个量。于是,大量系统表面上属于不同赛道,底层却都在围绕同一个分数做 Top-K、最近邻搜索、簇分配、中心更新和覆盖优化。也正因为如此,你为了语义去重搭好的那套 embedding + ANN 检索基础设施,稍微换个目标函数,往往就能继续拿去做搜索、RAG 甚至推荐召回。

检索 是最直接的一种。给定一个查询向量 $q$,在文档向量集合 ${d_i}$ 中寻找分数最高的几个:

\[\operatorname{TopK}_i \ q^\top d_i\]

这就是最基本的语义搜索。它不死盯词面重合,而是在向量空间里找离查询最近的文档。倒排索引擅长显式关键词;向量检索补上同义表达、上下位概念和改写说法。工程流程也固定:文档离线编码建索引,线上把查询编码成 $q$,再用 FaissHNSW 之类的近似近邻算法捞出 top-k 候选。需要更精确时,再接 cross-encoder 或规则层重排。所谓语义检索系统,核心就是“向量化 + 相似度排序 + 近邻搜索”。

聚类 问的则不是谁和查询最像,而是哪些点天然应该被归成一团。最经典的是 k-means

\[\min_{\{c_i\},\{\mu_j\}} \sum_i \|x_i-\mu_{c_i}\|_2^2\]

其中 $\mu_j$ 是第 $j$ 个簇中心,$c_i$ 表示样本属于哪个簇。把这个式子拆开看,里面还是几步老操作:先把每个点分配给最近的中心,再把每个中心更新成簇内样本的平均值。聚类没有脱离相似度计算,只是把一对一比较改成点对中心比较,再反复迭代。它在语料分析里很有用:某一团特别大、特别密,往往说明有很多模板化表达、重复样本或语义接近的改写;某个簇半径很大,则说明这个主题内部本身就多样,不能简单只保留一条。对去重来说,聚类最大的价值是先缩小搜索空间:先按簇组织,再在簇内做更细的相似度判断。

RAG 从结构上看,其实就是“检索 + 上下文组装 + 生成”。先把文档切成 chunk,得到向量 ${c_i}$;查询进来之后,先取

\[\mathcal{C}(q)=\operatorname{TopK}_i \ q^\top c_i\]

再把这些 chunk 拼进提示词里交给生成模型。这里真正新增的,是最后一步把检索结果喂给 LLM;前面的召回部分仍然是标准向量检索。因此,RAG 的质量上限不只取决于模型会不会说,也取决于你能不能挑出“相关、不重复、互补”的上下文。如果 chunk 库里有大量近重复片段,top-k 很容易返回五段几乎在讲同一件事的内容,浪费上下文窗口;如果切块过细,系统又可能捞回一堆局部相关但缺少完整事实链的片段。所以在 RAG 里,去重、聚类、覆盖优化都很关键。去重清掉明显冗余,聚类把相近 chunk 归在一起,覆盖策略避免 top-k 只盯住同一个高密度区域。很多“RAG 特有问题”追到最后,还是同一个旧问题:向量空间组织得好不好,近邻搜索结果是不是过于拥挤、过于重复。

推荐 也是同理。若用户有一个向量 $u$,物品有向量 $v_i$,最基础的打分函数就是

\[\operatorname{score}(u,i)=u^\top v_i\]

在内容推荐里,$u$ 可以是用户历史点击内容向量的平均、加权和,也可以由用户塔实时生成;$v_i$ 则来自物品塔或内容 encoder。无论模型包装得多复杂,召回阶段的核心问题依然是:把“当前用户”也看成一个查询向量,然后去物品向量库里找最近邻。 这和查询向量找最相似文档在数学上几乎是同一件事,只是查询来源不同:检索系统来自用户输入的文本,推荐系统来自用户行为序列。很多工业系统里,搜索召回和推荐召回甚至会共享同一套向量索引与 ANN 基建。推荐里的多样性控制、去重打散、长尾探索,也离不开前面的操作:在相似内容簇内限流,避免首页刷出一排相似短视频;加入覆盖约束,让结果保留一点探索空间。

所以,如果把这些任务放在同一个坐标系里看,会发现它们共享的是一组很朴素的几何原语:相似度决定谁更近,近邻搜索决定先看谁,聚合决定“代表点”长什么样,覆盖决定结果是否只扎堆在局部。 检索关注“相关性”,聚类关注“结构”,RAG 关注“相关性 + 信息密度”,推荐关注“偏好匹配 + 多样性控制”;这些差异更多发生在目标层,而不是底层算子层。一旦把 embedding 空间、近邻索引、相似度阈值、去重策略这些基础模块搭扎实,许多上层应用也就有了生长空间。

结语

从公式角度看,这些方法并没有看起来那么分散。LSH 是在高维空间里先做近似分桶,解决候选召回;Faiss 是在向量空间里高效找近邻,再把高相似样本写成图,解决语义去重;K-center-greedy 是在同一个空间里做覆盖优化,解决样本选择。至于检索、聚类、RAG、推荐,也都可以看成“围绕相似度、距离、近邻和覆盖”的不同任务变体。

所以,当你手里只有 embedding 向量时,并不是什么都做不了。只要先把相似度和距离定义清楚,再把搜索、连边、聚类和覆盖这些基本操作搭起来,就已经足够支撑一整套数据清洗与检索系统了。