感知哈希与深度哈希:把图片压成一串指纹
前言
向量检索的方案库里图一多,存储和计算成本还是实打实的,有没有更极致的压缩?有,就是把图片直接压成一串二进制指纹,检索变成数比特位的事。这就是图像哈希检索,从去重到以图搜图都有它的一席之地。
先从最土的感知哈希说起
有个非常经典的小玩意叫感知哈希(pHash),思路简单到令人发指:
- 把图缩到32×32,转灰度,把像素层面的尺寸、色彩差异先抹掉
- 做二维DCT变换,把能量集中到低频区(对,就是JPEG压缩用的那个DCT)
- 取左上角8×8的低频块,这64个系数基本代表了一张图的"骨架"
- 拿每个系数和均值比大小,大于记1小于记0,64位指纹就出来了
两张图像不像,就看指纹的海明距离 $d_H(h_1, h_2) = \lVert h_1 \oplus h_2 \rVert$,异或之后数1的个数:
from PIL import Image
import imagehash
h1 = imagehash.phash(Image.open('original.jpg'))
h2 = imagehash.phash(Image.open('resized.jpg')) # 缩放/轻压缩过的版本
print(h1 - h2) # 海明距离: 0~5 基本同一张图, 超过10 就悬了缩放、轻微裁剪、重压缩、调色,这些操作动不了它的骨架,指纹基本不变;而内容不同的图指纹会迅速分叉。所以它是图库去重、盗图初筛的利器,而且64位指纹比对就是一次xor加一次popcount,一亿张图全扫一遍也就眨眨眼的功夫。
但它有个天花板
感知哈希只能回答"这两张图是不是同一张图被折腾过",回答不了"这两张图内容相似"。一张柯基和一张萨摩耶,在pHash眼里是两个无关的指纹,但用户搜柯基时显然希望萨摩耶多少也出来点。语义级的相似,得靠学习出来的哈希,也就是深度哈希。
深度哈希:语义塞进比特位
深度哈希这几年是检索领域的一个热门方向,核心思想一句话:拿CNN直接把图片映射成K位二进制码,并且用损失函数逼着"语义相近的图片编码距离近、语义无关的图片编码距离远"。
最典型的成对监督形式,训练时对标注了相似/不相似的图片对,优化这样的目标:
$$ \mathcal{L} = -\sum_{ij} \Big[ s_{ij} \log \sigma\big(d_{ij}\big) + (1 - s_{ij}) \log \big(1 - \sigma(d_{ij})\big) \Big] $$
其中 $s_{ij}$ 是语义标签(相似为1),$d_{ij}$ 是编码间的相似度量,$\sigma$ 是sigmoid。从2014年的CNNH、DNNH,到2015年把损失和特征学习端到端揉在一起的DPSH,再到2017年ICCV的HashNet用加权策略解决相似对极度不平衡的问题,一路看下来就是不断地把"离散的二进制码"这个不可导的讨厌性质驯服得更服帖——训练时用连续值放松,推理时再一刀切成0/1。
这套东西的实际收益账是这么算的(以2048维float特征换64位哈希码为例):
$$ \text{存储: } 8192 \text{B} \rightarrow 8 \text{B} \; (1/1024) \quad\quad \text{比对: 浮点点积} \rightarrow \text{异或+popcount} $$
千倍的存储压缩,比对指令也从SIMD浮点退化成位运算,配合哈希桶索引,亿级图库在普通机器上就能做实时检索。代价就是离散化那一刀会掉精度,鱼和熊掌的古老故事又演了一遍,所以工程上常见的玩法是哈希先粗筛召回一批候选,再用浮点特征精排,两头的好处都占。
结束语
从pHash到深度哈希,其实是把"像不像"这个问题一步步从像素层推向语义层的同一趟旅程,只是把终点从连续向量改成了离散比特。真要做产品,我的建议还是从上一篇的向量方案起步,量级和成本真到疼了再引入哈希做粗筛,一上来就All in哈希省下的钱,大概率会加倍付给调参和排查召回丢失。