从 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. 非常多的工程算法都在去解决类似的数学问题,LSH希望解决向量相似中的平方复杂度,linear attention希望解决attention中的平方复杂度,Tournament机制希望去解决reward中的平方复杂度. 他们都源于pairwise机制,但pairwise依旧很强.

以最常见的随机超平面 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 向量时,并不是什么都做不了。恰恰相反,只要你先把相似度和距离定义清楚,再把搜索、连边、聚类和覆盖这些基本操作搭起来,就已经足够支撑一整套数据清洗与检索系统了。