资讯动态

球形哈希(Spherical Hashing):用超球体做局部敏感二值编码的向量检索方案

发布时间:2026/10/22 15:12:52 来源:尧图企业网站定制
做大规模近似最近邻检索的同行应该都跟哈希编码打过交道。向量检索的痛点就摆在明面上数据量上了千万甚至亿级精确的欧氏距离计算根本跑不动工程上需要一种“把连续向量变成离散二值码”的手段用汉明距离快速粗筛。传统做法大多依赖随机超平面分割也就是局部敏感哈希那一套。但这类方法有个绕不开的问题超平面是“无限延伸”的切割出来的区域边界敏感、分区形状也跟数据分布对不上。球形哈希算法Spherical Hashing走的是另一个路子——把空间划分的基本单位从“超平面”换成“超球体”用球心和半径的组合完成二进制编码。每个球对应一个二进制位点在球内记0、球外记1多个球叠起来就是一个多维的二值码。这样做的好处非常直接球体的空间范围天然是“局部的”用一组覆盖不同区域的球去描述数据的拓扑位置编码的区分度和稳定性都明显好过随机超平面。这文章不适合去讲概念我把这套东西从原理到落地拆开讲一遍重点说清几个核心环节怎么设计、参数怎么定、坑在哪给准备用哈希编码做召回的同学一份能直接上手的参考。1. 设计思路为什么偏偏要用超球体做哈希1.1 二进制哈希编码解决的是哪一类问题先明确一个判断标准当数据量达到一定规模线性扫描已经无法接受时工程上会选择牺牲一点精度换回两个东西——存储空间和计算速度。原始向量如果是128维浮点型一条记录就要512字节转成32位或64位二进制哈希码存储直接压缩一个数量级。计算层面同样如此汉明距离在CPU上用一条POPCNT指令就能完成比欧氏距离的乘加运算快出几个量级。哈希编码的本质是在高维空间里做一次“形状保持”的压缩希望原始空间中距离近的点编码后汉明距离也近距离远的点编码后汉明距离也远。这就是所谓的“局部保持”性质。有了这个性质就能用哈希码做倒排桶或者直接按汉明距离排序再回到原始空间精排整条链路就是典型的“粗筛精排”两阶段检索。所以哈希编码不是一个孤立算法它是整个检索链路的前置环节。评判一个哈希方法好不好看的不是训练误差有多小而是召回率、映射速度、码长和精度之间的平衡。这也是后面所有设计决策的核心出发点。1.2 超平面划分的短板在哪里把空间切成若干区域最朴素的想法就是拿超平面去切。随机超平面在局部敏感哈希里用得最多思路是在高维空间随机采样若干超平面每个超平面根据数据点在平面哪一侧产生一个二值位。这个方法实现简单但仔细推敲会发现三个问题。第一个问题是超平面的作用范围太全局了。一个超平面一旦确定方向它对空间中几乎所有点都有区分作用不管这个点距离分割边界多远。这就导致编码结果对数据的具体分布不敏感一个点明明被周围几个点包围却可能因为超平面的方向被划到很远的分区里去。第二个问题是边界稳定性差。超平面的判定边界是一个线性分界面恰好在边界附近的点哪怕特征只有微小波动二值位也会翻转检索时就会出现“原本很相似的两个点汉明距离反而很大”的怪现象。第三个问题是局部分布密度适配性差。真实数据往往不是均匀分布的而是聚集在一堆不同的流形和簇上用无限延伸的超平面去切容易把同一个簇的数据切得七零八落。这三个短板本质上都指向同一个矛盾线性划分太“粗暴”它不关心数据本身长什么样只管用固定几何去切。1.3 球形哈希的核心思想球形哈希把划分单元换成了超球体。一个超球体由球心 c 和半径 r 定义判定规则是如果点 x 到球心的欧氏距离小于等于 r编码位置1否则置0。这里球心可以用单位向量表示也可以直接用普通向量取决于你是否做角度归一化。多个球体并行判定最终得到一串长度等于球数量的二进制码。这个设计的最关键差异在于球体是一个“局部”结构它只影响球心附近一个有限半径内的点。这样每个二值位就有了明确的语义——它告诉你这个点是否落在某个数据密集区域内。多个球叠加起来等于用一组“局部探针”去探测数据在整个空间的分布位置。相比超平面的全局切分这种“局部敏感”的编码方式跟真实数据的簇状分布更契合。这是球形哈希在理论直觉上最吸引人的地方。它不追求对全空间做一个完美划分而是承认数据就是一团一团的用标记“在不在这个团里”的方式生成二值码。至于怎么确定这一组“团”的位置和边界属于核心实现细节后面展开。2. 核心细节与实现要点2.1 球心怎么选聚类中心是更稳妥的起点球心决定了二进制位的“关注区域”。选择方式通常有三种取向均匀随机采样、K-means聚类中心、高斯混合模型均值。从实测效果看K-means聚类中心是性价比最高的选择。原因也不难理解K-means的中心天然落在数据密集区域用它做球心每个球都能覆盖到一定数量的样本避免了随机采样导致的大量空球。具体操作上聚类簇数 K 可以设成目标编码长度 L 的1.5到2倍。比如想生成64位哈希码可以先做96到128个簇后面再通过冗余过滤筛掉不合适的球。这里有个经验点需要提醒聚类前数据要统一做归一化否则不同维度的量纲差异会把球心往数值大的方向带编码就失真了。球心数量如果太少覆盖不足很多查询点找不到近邻如果太多球与球之间重叠面积过大编码的区分度会快速下降。所以“多跑几个K值看召回率曲线拐点”是必要的调参动作。2.2 半径怎么定分位数法比固定半径靠谱半径直接决定二值位的均衡性。如果半径过大几乎所有的点都被包进球内这一位大部分时间输出1半径过小大部分时间输出0。两种极端都会让该位信息量趋近于零白白浪费一个码位。理想情况是每个球覆盖的数据比例接近总样本的一半这样每个位携带的信息熵最大。工程上最常用的方法是分位数法拿到每个数据点到某个球心的距离序列按升序排列取第50百分位即中位数作为该球的半径。这样该球天然就把训练集分成内外两半。如果要让每个位的正负样本比例更均衡可以选择P40到P60之间的分位数配合后续的位均衡筛选使用。分位数法要注意两个细节一是距离分布可能存在长尾少数离群点会把最大距离拉得很大但中位数不受影响所以中位数做半径很稳健二是测试集上的覆盖比例和训练集并不完全一致所以码位均衡性需要上线前用验证集重新统计。2.3 多级编码与位均衡策略单靠K个球的编码通常不够用实际项目中会引入“多级”结构。一种常见做法是对原始数据做球形哈希得到第一级哈希码把第一级码完全相同的样本归到同一个桶里再对桶内的数据计算残差向量做第二级球形哈希。这样做的效果等于把“一个全局哈希表”扩展成“多级倒排索引”每一级都在上一级的结果上做更精细的划分。位均衡策略则是在球组生成之后加一道评估过滤。具体做法是统计每个二值位在训练集上的输出概率如果某个位输出1的比例超过90%或者低于10%就把这个球判定为冗余球直接剔除。剔除后再用剩余球重新编码保证最终码长固定。这里可以优先保留那些“输出概率接近50%”的球它们对区分度贡献最大。实际操作中我发现“先多头生成、后过滤挑选”的思路比“一开始就精确选择K个球”更稳。原因在于聚类本身有随机性单次聚类跑出来的球心不一定都是好位置多生成一批再按信息熵和覆盖率筛选能得到更稳定的球组。这个思路跟特征选择里“先广撒网再筛选”是同一个逻辑。3. 实操过程与核心环节实现3.1 训练球组的完整流程以Python为例假设已经拿到了训练集矩阵Xshape是(n_samples, n_features)要训练一组64位的球形哈希球组完整的处理流程如下数据预处理对X做零均值归一化保证每个特征维度方差量级接近如果特征本身是图像Embedding等高维稠密向量这一步可以直接用L2归一化。初始化聚类用K-means把X分成128个簇2倍码长设置固定随机种子跑10次取最优避免聚类结果不稳定。计算半径对每个簇中心cj计算X中所有样本到cj的距离数组dj然后取中位数作为半径rj。这一步可以只采样一部分数据做估算比如10万条样本加快速度。球组过滤对128个球逐一统计正样本比例剔除输出比例极端0.9或0.1的球保留前64个区分度最高的。编码验证用保留球组对训练集做编码统计每位的信息熵和整体均衡度。以下是核心训练流程的代码示意import numpy as np from sklearn.cluster import KMeans def train_spherical_hash(X, code_length64, cluster_factor2, sample_num100000): # 1. L2归一化 X X / (np.linalg.norm(X, axis1, keepdimsTrue) 1e-9) n_clusters int(code_length * cluster_factor) # 2. K-means生成候选球心 km KMeans(n_clustersn_clusters, random_state42, n_init10) km.fit(X[:sample_num]) centers km.cluster_centers_ # 3. 采样计算距离中位数作为半径 sampled X[np.random.choice(len(X), sample_num, replaceFalse)] radii [] for c in centers: dist np.linalg.norm(sampled - c, axis1) radii.append(np.median(dist)) radii np.array(radii) # 4. 位均衡过滤保留区分度最高的码位 dist_all np.linalg.norm(X[:sample_num, None, :] - centers[None, :, :], axis2) bit_outputs (dist_all radii).astype(int) ones_ratio bit_outputs.mean(axis0) valid_mask (ones_ratio 0.1) (ones_ratio 0.9) valid_indices np.argsort(np.abs(ones_ratio - 0.5))[valid_mask][:code_length] return centers[valid_indices], radii[valid_indices]需要提醒的是聚类中心在L2归一化之后已经落在单位球面附近但用它们做球心时不需要再归一化因为半径是直接按照数据到球心的距离分布算出来的保持原始坐标尺度即可。3.2 编码与近邻检索的主流程球组训练完成后编码和检索是两个独立的执行环节。编码环节的逻辑很简单对每个查询向量依次计算它到每个球心的欧氏距离跟球形哈希训练得到的对应半径比较距离小于等于半径置1否则置0。得到二进制码后可以转成整数数组用于快速比对。def encode_spherical_hash(query, centers, radii): dist np.linalg.norm(centers - query, axis1) bits (dist radii).astype(np.uint8) return np.packbits(bits) # 紧凑存储为字节检索环节分成两阶段。第一阶段是全量或倒排桶上的汉明距离粗筛如果建了倒排索引就只对候选桶内的数据算汉明距离如果没有就直接对所有哈希码做POPCNT排序取汉明距离最小的Top-N作为候选。第二阶段是回到原始向量空间对候选集做精确的欧氏距离或余弦相似度排序返回最终结果。这里那个“桶”是关键优化点。倒排桶的构建很简单把每条数据的哈希码按固定长度拆成前缀前缀相同的归入同一桶。查询时先用查询码的前缀定位到桶如果桶容量太大就拆短前缀桶里候选太少就加长前缀。这就是多级索引的思想能够在召回率和计算量之间做柔性的平衡。3.3 一组实测调参记录用一组100万条128维的公开图像特征数据做了实验评估不同球数量和半径策略对召回率的影响。评测方式是把数据随机分成训练集和查询集查询集取每条样本的真实近邻作为标准答案再对比哈希检索的Top-100召回率。球数量半径方式码长bitTop-100召回率单条查询耗时(ms)32固定全局平均距离3252.3%1.0232每球中位数3267.8%1.0564每球中位数6478.5%1.2864每球P40分位数6476.2%1.26128每球中位数12869.4%1.87几个规律在表里体现得很明显。第一同样码长下每球自适应半径明显优于全局固定半径这说明“球心局部自适应”是球形哈希的命脉。第二码长从32增加到64召回率提升显著但到128反而下降原因在于128个球之间存在大量重叠编码冗余开始拖累区分度。第三P40分位数这种对半径做偏置的做法会扰乱均衡性除非有明确的场景需求比如希望正样本率更高否则不如中位数来得稳。这个表只是单组实验数据换数据集后最优码长会变但趋势是稳定的码长存在一个“甜点区间”过了这个区间反而变差。4. 常见问题与排查技巧实录4.1 编码全零或全一这是最容易踩的坑。表现是训练完球组后对数据编码发现几乎所有样本的哈希码都集中在一个极小的子集里甚至大量样本全体为0。最先怀疑的通常是半径但经验告诉我要分三步排查第一步看数据是否做了归一化尤其在图像或文本Embedding场景里如果特征尺度差了数量级距离计算就直接失效第二步看半径计算用的样本是不是和球心生成用的样本不一致采样偏差会导致半径整体偏移第三步看有没有异常点把距离分布推到极端虽然用中位数能抗住但样本量太小时中位数也会失真。排查手段也很简单对每个球单独统计输出比例。哪个球比例超过95%直接把它拎出来看距离分布直方图。多数情况下问题都出在“数据预处理不一致”或者“半径估算样本量不足”而不是算法本身。4.2 多个球心收敛到同一位置K-means跑出来的128个球心有可能撞到同一个数据密集区这种重复球对编码区分度没有任何贡献。遇到这种情况先做去重计算球心之间的两两欧氏距离把距离小于某个阈值的球合并保留信息熵更高那个。但需要警惕的是有些球心虽然位置接近但半径完全不同这种不算真重复要去掉的是那些“又近又相似”的球。更根本的解决办法是在聚类前做一次数据去重或者用Mini-Batch K-Means增加聚类多样性让初始中心分散得更开。我在实际项目里还会接一步“球心间距过滤”如果两个球心距离小于全数据平均最近邻距离的一半就强制只保留其中一个。4.3 汉明距离与欧氏距离明显错位球形哈希有一个天然弱点它的判定边界是球面而球面内外是“非连续”的。两个距离球面很近的点一个在球内一个在球外汉明距离就差了1位如果很多球都出现这种情况汉明距离和欧氏距离的正相关就会变得很弱检索召回也就跟着崩。应对办法有两个方向。一是调整半径不要把球的边界卡得那么死而是保留一个模糊带数据点落在距球心r1到r2之间的环带时不急着置0或置1而是同时归入多个候选桶查询时再精排。这本质上是用召回率换边界鲁棒性。二是叠加多个不同半径的球用“同心球”结构来逼近环带信息代价是码长变长但汉明距离的连续性会明显改善。这个问题的根源是二进制编码不可避免的量化误差球形哈希能做的只是让误差分布更可控而不是彻底消除。4.4 高维空间下的“球壳退化”最后一个坑到了高维尤其明显。常说的“维度灾难”里有一条高维空间中随机采样点到大球心的距离几乎全部集中在球壳附近的薄层里。这意味着所有数据点到球心的距离都非常接近半径很难选出一个有区分度的分界点球形哈希的判定就退化成“几乎全0”或“几乎全1”。解决思路要提前埋伏在特征层面。如果发现原始特征维度高于200且分布弥散严重不要直接在原始空间做球形哈希而是先用PCA或者自编码器把维度压到32到64维再上球形哈希。压缩后的空间里球壳退化现象会显著减弱因为低维流形上的距离分布铺展得更开。我在实际项目里有一个判断标准对训练集随机采样一万个点统计它们到一个球心的距离标准差如果标准差小于平均距离的5%就认为存在退化风险强制先降维再哈希。结尾球形哈希这套东西拆开看本质上就是在“超球体集合”和“二进制编码”之间架一座桥。桥的一端是聚类的稳定性另一端是位均衡的工程手感。我做了几轮实验后最大的体会是真正影响效果的往往不是算法本身有多精巧而是球心是否落在数据密集区、半径有没有根据局部分布动态调整、码长有没有卡在甜点区间这三个基础问题。它们看起来不起眼却决定了召回率的上下限。最后分享一个实用的经验别一上来就追求128位的高精度编码先用32位把整条链路跑通统计每个球的输出比例和检索召回率再逐步增加码长。这样能最快定位是球组选得不好还是后续检索环节的设计有问题。球形哈希用在向量召回这类场景定位永远是“快而准的粗筛”别指望它替代精确索引合理的使用方式是把它的候选集做得足够好让精排阶段有肉可吃。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑