前言
随着深度学习近些年的发展,在cv领域图像搜索的表现也越来越好,拍照购物的功能渐渐成为了主流电商的标配。其整个过程牵扯较多技术方向,本文将从一个较为全面的角度为大家介绍,作为后端开发我会略微偏向描述工程方面的问题,同时为便于理解和串联上下文,有关深度学习算法方面也会做简单介绍,算法同学或者对深度学习已有较多涉猎的同学可选择性跳过。
另外本文是基于我在之前公司负责开发的拍照购物应用的基础上来的,过程中参考了大量业界和学术界公开较久的方案,因此可能此时已经与前沿技术存在差距。
术语
为了便于理解,我们先以最通俗的方式介绍几个概念,这个介绍并不严谨,只是便于后续理解。
想象一下场景,我们现在要求自由落体时物体下落的时间和下落的距离之间的关系,
于是,我们做了很多次实验,得到了非常多组物体做自由落体运动时 (下落时间,下落距离)的数据,我们通过这些大量实验数据,推测出 下落距离h = 1/2*gt^2 t为下落时间,g为重力系数。
| feature | 特征,可以理解为一个已知条件、自变量等等,对上面例子就是时间 |
|---|---|
| groudtruth | 真值,每次实验的下落距离h 就是groundtruth |
| model、train(训练)、loss(损失)、inference(推断) | 模型,可以理解为一个函数,一般的 f(所有feature) = groudtruth模型包含结构(骨架)和参数(肌肉),比如 f(下落时间) = 下落距离,这个f 我们站在上帝视角知道是 f(t) = 1/2*gt^2, 是个二次多项式函数,这个二次多项式函数就是模型的骨架,只有骨架,我们确定不了这个函数,因为有无数多个二次多项式函数,还需要确定二次多项式的三个系数, f(t) = a*t^2 + b*t + c。a,b,c;通过一堆的实验数据 (下落时间,下落距离)求出最优的a,b,c的过程就是训练,训练过程中需要不断更新a,b,c的猜测值,每次猜测值对应的函数输出的下落距离猜测值 与真实的实验数据下落距离(groudtruth)之间的差异就是loss,衡量这个差异的方式又多种多样,直接取两者之差就是最简单的一种了,这个衡量方式就是loss函数模型训练好后,得到了最优的a,b,c,肯定要拿去用的,输入一个时间到模型函数中,输出一个预测的下落距离,这就是inference |
| alexnet,resnet,xxxnet | 上面提到了model(模型)是什么意思,那xxxnet就是一个具体的模型了,模型包含结构和参数,结构是模型设计阶段就确定了,参数则是模型训练阶段才确定的。xxxnet就是一些公开设计好的模型结构,你要拿来用,就得在具体业务数据下去训练,得到适合你的模型参数。alexnet是辛顿的学生alex参加2012年imagenet比赛用的,凭借这个掀翻了之前svm的最好成绩。resnet何凯明提出的残差网络,就对于理解本文来说,我们只要知道xxxnet都是大牛们设计好的网络结构,哪些是最近提出的,效果好的,我们可以直接拿来用就行。 |
| pretrain(预训练)、finetune(微调) | 预训练,顾名思义就是预先进行的训练,各种xxxnet有非常多的参数,训练起来很麻烦很耗资源也很耗时间,但是深度学习的深度有一个好处就是越前面的层学到的越是接近像素级的特征,比如哪里有线、哪里有菱角、哪里有圆圈,越往后学到的是越接近语义级的特征,比如哪些线哪些菱角怎么组合在一起是个车轮,怎么组合在一起是个车窗。所以呢,一个模型前面的层的参数训练好后,是可以复用的,毕竟不管什么业务,什么样的像素组在一起是线,是角还是圈这个事不变的,我们只需要固定好前面层的参数,然后喂自己业务的数据,训练最后一层或者两层的参数就可以了。那这个前面参数训练好的网络就叫pretrain model,固定前面层训练最后面的过程就叫finetune。 |
| 数据集(dataset) | 对应上面说的那一堆实验数据 |
| imagenet、cifar10 | 数据集通常来说获取都比较麻烦,大部分时候都是有监督学习,需要人工标注,所以就有了一些开源的数据集,别人整理好的,造福大家,同时对于一些模型评测 大家也有了统一的标准,imagenet是斯坦菲李飞飞那个,一千多万真实生活中的图片,几千类吧;cifar10小数据集,图片少,这两个都是用来做图片分类的;同样对其他任务,比如目标检测什么的,也都有对应的开源数据集 |
| 神经网络、deeplearning | 听起来高大上,其实可以理解成一个比较复杂的函数,只不过这个函数不太好描述出来;deeplearning是神经网络的一种,之所以叫深度,可以权当做是个嵌套函数 比如3层,f=a3(f3(a2(f2(a1(f1(x)))))),并且嵌套的层数比较深。。。 |
| cnn(卷积神经网络) | 既然也是神经网络,那肯定也是一种函数,区别在于它的嵌套不是简单的激活函数和线性函数的嵌套。。。 |

问题拆解
整个拍照购物归根到底是一个 推荐问题。
因此遵循两大步骤:召回(recall)、排序(rank)
- 所谓召回可以理解为:从一大堆商品里找到N个和搜索的图片较像的商品,当做候选
- 所谓排序:就是除了相似度外在综合考虑下其他因素,对N个候选排排号,得到最终的推荐列表
拍照购物和一般推荐问题又有些不同:
一般推荐问题影响排序的feature有很多,且各个feature之间无非常明显的主次之分;而拍照购物虽然也有较多feature,但是最主要的一定是视觉上的相似度
基于这个原因,单纯以提升点击率为目标, 拍照购物的两大步骤可以调整为
视觉相似搜索(召回)、多因素重排(rerank)

视觉相似搜索
(
召回
)
搜的像
理论
计算机只会处理数值,那要判定搜的像不像,首先要将像的程度这个概念量化。
我们做个类比
| 平面坐标系下三个点: A点 B点 C点 | 三张图片 A图 B图 C图 |
|---|---|
| C点比B点到A点的距离更近 | C图比B图更像A图 |

图片相似度可以由 图片间的距离 来度量,而要计算距离,首相要能够把图片放到N维向量空间上的一个点,即要能将图片表示成类似点的坐标那样的形式,称之为向量。
由此, 搜的像 等价于 对于特定的**** 距离度量方式 ****, 找到 最适合当前业务的**** 向量表示方式。

所以关键要素:
- 距离
- 向量表示方式
距离
我们先来看距离,这个牛人们已经给我们研究好了,就那么几种,我这里列两种,相似搜索里常用的。
| 欧式距离 | 上学期间经常打交道的,二维平面里就是两个点之间的距离sqrt((x1-x2)^2 + (y1-y2)^2)推广到n维 |
|
|
| — | — | — | — |
| 余弦距离 | 两个向量之间的夹角余弦值 |
|
|
两种方式各有优劣,余弦距离对方向敏感,对绝对大小不敏感;欧式距离则相反。举个实际的例子,
三个学生的各科成绩组成的向量 (语文成绩,数学成绩)
A同学 = (68,100)
B同学 =(90,86)
C同学 = (32,65)

以欧式距离来衡量,A同学和B同学更像, 以余弦距离来衡量,A同学和C同学更像。
从总分班级排名来看,A和B总分排名很接近,都属于比较靠前的同学,所以欧氏距离的相似度更合理。
从学生的各科发展来看,A同学和C同学都属于偏科学生,语文都不擅长,所以余弦距离的相似度更合理。
还有一种距离方式叫inner_product,内积 X.Y = x1*x2+y1*y2 = cos距离*||X||*||Y||,综合考虑了绝对大小和夹角,且只需要计算两次乘法和一次加法,非常省钱,很适合我们。
向量表示方式
好了,距离度量方式大牛们都研究的差不多了,我们没什么发挥空间。
所以最终,要想搜的像,最核心问题,变成了寻找在当前业务情形下的最优的向量表示方式。
所谓向量的表示方式,其实归根到底就是一个映射。
y=f(x) x是输入的图片,y是输出的图片向量,能把图片变成向量,最容易想到的方式,是什么,当然是图片的像素值,不考虑通道,一张图片是 h*w大小的,一个像素有r、g、b三个数,拼在一起3*h*w位的向量有了
[r1,g1,b1, r2,g2,b2…..]
不要觉得这种方式很傻,它确实是一种图片到向量的映射方式,我们就直接用它在cifar10数据集上试试相似搜索的效果。
import numpy as np
import pickle
def unpickle(file):
import pickle
with open(file, 'rb') as fo:
dict = pickle.load(fo, encoding='bytes')
return dict
data = np.zeros(50000,3072)
for i in range(1,6):
data[(i-1)*10000:i*10000] = unpickle("/Users/zhangzongchao/Downloads/cifar-10-batches-py/data_batch_%d"%i)[b'data']
def search(n):
"""
搜索和第n张图最像的100张图的编号
"""
return np.argsort(np.sqrt(np.sum((data-data[n])*(data-data[n]),axis=1)))[:100]


可以看到 一眼望去 还是有点像的,至少总体色彩上是比较像的。
效果如实反映了我们构造的图像向量的本质,即完全使用像素值拼接,结果自然就是整体色彩上比较像。
有些图片里的内容根本风马牛不相及,但是就因为图片与被搜图片的大部分对应像素差别小,最后算出的向量距离就接近了。
所以呢就不能用这种图像的低层feature(如像素),而应该用更高阶的feature,如上面概念所说,于是我们自然能想到用深度学习模型最后一层的输出,作为图片的向量。

前面【术语】里有提到cnn就是一种特殊的神经网络,当然也就是一种特殊的函数,以个人经验来说,这个cnn还是很多人读关于深度学习文章的障碍,当然把它当作一个黑盒并不影响对其他部分的阅读。这里我还是尝试以一个外行人的理解来讲一下cnn——卷积神经网络
关键字肯定就是"卷积"二字,它是一种运算,类比于 加减乘除,区别在与普通的加减乘除是两个数之间的,卷积是两个矩阵之间的(二维卷积),并且普通加减乘除是一次性的,卷积是一个过程的。
借用一个经典的动图来说话

中间彩色的叫卷积核,一般卷积核都有很多层,这个图只画了一层。
经过这个卷积核的一次操作,生成的一个数 = x11*w11 + x12*w12 + x21*w21+x22*w22
然后卷积核会在左边的矩阵上依次左到右、上到下移动,移动的步长叫做stride
最后滑动完,产生的结果为一个新的矩阵。
做个类比
|
| 单变量线性模型f(x) = ax+b | 卷积神经网络 |
|---|---|
| 输入 | 一个数 | 一个矩阵,比如图片,如果是灰度图就是二维矩阵,如果是彩色,三维矩阵 |
| 运算 | a*x+b | 很多个上面的那个动图组合在一起 |
| 要学习的 | a和b,训练模型就是为了得到最优的a和b | 组成模型的n多个卷积核上的数上面动图上就是 w11, w12, w21, w22 |
关于cnn网络的基本结构,可以看这个演示
https://poloclub.github.io/cnn-explainer/
前面那些奇奇怪怪的长方体,可以先不用去关注,他其实是深度模型各个层的输出,一般可以叫featureMap,模型如前所述,当做一个比较复杂的函数来看就行了,一个黑盒,反正最后一层能输出1000个数值,这就够了,我们就有了一个1000位的一维向量。


效果看起来好了很多,也就是说对于一张图片来说这1000个数,要比那3*32*32=3072个rgb数值拼在一起,更加能代表它。
为什么会好呢?
因为rgb拼在一起这个函数,我们是拍脑袋想出来的;而alexnet这个pretrain模型是实实在在在imagenet上千万张图片上训练出来的。
之前有说过,训练就是不断的更新模型里的参数,以减小loss,那alexnet这个pretrain模型训练的时候的loss是什么样的呢?首先他为了解决的是分类问题,也就是把图片尽量正确的归到imagenet那1000个类里,所以loss肯定是分错了的loss大,正确的loss小嘛,至于具体是什么,反正不影响我们理解。
既然这个pretrain模型是在分类的问题上训出来的,那其实并不完全贴合我们想找相似的这个场景,毕竟分类问题会让最后提取出的图片特征过多的关心影响图片类别的那些feature,而忽略了其他不影响类别但是在找相似中很重要的feature,比如说颜色,他不影响分类,但却很影响相似性。
所以loss的选择是可以优化的,对于度量学习,我们使用triplet loss。给分类和tripplet两个来做个类比
|
| 分类 | tripplet |
|---|---|
| 训练数据 | (图片,图片类别) | (A图片,跟A图片相似的图片, 跟A图片不像的图片) |
| 训练目标 | 尽可能的让更多的图片分到正确的类里 | 尽可能拉近相似的(正向)两张图片,尽可能推远不像的(负向)的两张图片 |
到此本来我们的模型是差不多敲定了,alexnet+tripplet loss,但是呢,alexnet本身在imagenet1000类识别比赛上也只能做到85%左右的准确率,而后面出现的resnet系的模型都能达到95%了,那还用说当然用resnet了,我们就把他当作是另外一个黑盒函数就好了,它主要是解决了怎么让模型变得更深的问题,一般来说深度学习嘛,越深能力越强,但是越难训练也越容易过拟合,但是resnet比较好的解决了这些问题。
ok最后敲定了 resnet+tripplet loss。
实践
上面其实都算是在选型,就像我们在做后端开发的时候,需要用mq选了rabbitmq,需要用缓存选了redis。真正工作其实还没开始呢。选型选的是大牛们给我们搞好的架子,真正拿到这架子干活,还是会碰到各种实际的问题的。
从**** 0 ****到**** 1 ****,没人力标注,训练数据怎么来
首先我们选了tripplet训练方式,训练数据需要大量的 正负向三元组,也就是 (基准图片、正向图、负向图),这些图从何而来呢?
人工标注?这个是最容易想到也是最靠谱的,当然也是最费钱费时间的,作为一个预算有限的项目,大规模的人工标注,这个就不要想了。
- 自己商品图
- 自家商品评论图、口碑图(优)
-
天猫、京东、淘宝商品图
- 天猫、京东、淘宝评论图(优)
电商的商品图,每个商品都会维护多张不同角度不同色号的图片,彼此之间构成正向图。
电商的用户评论里,有些用户会晒图,晒的图与当前商品的图构成正向图。
天猫淘宝京东上的评论图片量可观,难点是要解决反爬虫问题。
数据清洗
上述数据中,除了自身商品图来源相对正规,有审核,质量较高外,商品的评论图质量比较差,有很多噪声数据。
需要通过一些手段进行清洗
- pretrain模型+传统cv方法计算距离卡阈值进行清洗
- 利用商业试用api(比如百度)辅助清洗
-
人工抽检
流程优化

之前讲的都是默认对整张图的一系列操作,实际上商品图通常都比较干净,商品处于图片主体部分;而用户拍的搜索图就非常随意,大量的杂乱背景,目标的位置也可能不在主体,更有可能拍的图里有多个可选目标,不能确定用户要搜索的到底是哪个东西。
所以实际中我们要在图片搜索前,先做一步目标检测(object detection),这又是一个cv领域的大方向,这里我们只需要知道有这一步就好。提一下,我们最终选用的模型是maskrcnn模型。
有了这一步之后,用户在搜之前,界面上就会最有可能的那个候选框,下面的搜索流程就只送入这个框里的图片部分了。当然用户也可以拖动这个框,找自己需要的目标。
框加权调整 choose_bbox = w1*离中心距离 + w2*maskrcnn模型输出的score + w3*框的面积
线上数据反哺模型,变得更强
有了上面的工作,我们就基于自己准备的大量训练数据,训练了一个模型,就叫baseline吧,通常情况,它的效果还是可以接受的,老板看到之后会允许我们上线,或者开一部分流量灰度。
有了用户使用后,我们就有了宝贵的真实数据了,因为不管是商品评论还是商品图片,给我们带来的数据集feature的分布还是与真实场景有些不同的。
有了真实的数据,通过前期的埋点,我们得到了
(用户拍的搜索图、展示给用户的搜索结果商品图、用户点击了的商品图)
这个来组成我们新的三元组进行训练,因为这个是生产环境源源不断产生的,所以我们就可以形成一种,定时finetune的机制,
不断地拿线上的数据来微调我们的模型,以达到更好的效果。
但有一点要考虑的就是,推荐系统中的信息茧房问题,会导致搜索结果中经典款式越来越优先,这种情况可以在后面的多因素重排中适当加大一点新款商品的权重,尤其对fashion类别(服饰鞋包)。
视觉相似的延伸用处
- 电商比价系统,作为一种找同款的手段
- 商品上架,作为一个判别商家盗图的手段
搜的快
理论
上面的搜的像的部分,比较粗略的介绍了我们在算法层面的一些推动过程。总结起来就是三步走
- paper,顶会cvpr、iccv
- github,paper上只有模型的理论研究和结构,还有最终的表现,但是没有代码实现,一般paper发表后,马上就会有业界雷锋大牛,帮我们在tensorflow或者pytorch上实现xxxnet,并复现结果
-
找数据、清洗数据、微调、小trick,大多数我们说的算法工程师大部分时间都在搞这个
经过上面那些算法层面的过程后,搜索的结果是比较像了,但是如果性能不行,每次搜索都要几秒,那也白搭。
来看下整个流程

具体的耗时分布目前拿不到了,颜色越深表示越耗时。
图片上传都是对的cdn,边缘接入,耗时基本在10ms的量级内解决。值得注意的是,上传的图片需要在手机端进行预先的降分辨率处理,目前的手机拍的照都很高清,不仅占用传输,而且对于模型的运算来说徒增压力。
rerank虽然也需要运行模型,但是是非深度模型,模型复杂度低,参数少运算快,个位数的ms内解决。
瓶颈在目标检测和相似搜索这两步,细分一下其实是三步
目标检测(跑maskrcnn)模型, 特征提取(跑resnet)模型, 相似搜索
前两步都是跑深度模型
后一步是相似搜索,再说的直接点就是,超大规模向量的距离计算和topN问题
让模型快
模型我们是利用tensorflow serving,来构建模型服务平台
- cpu指令集优化、矩阵运算包英特尔®MKL-DNN
- xla jit
-
花钱上gpu
这几条基本是硬件和指令集上的硬优化,gpu加速明显但是成本是个问题,其他指令集上的优化效果天花板明显。
从模型本身考虑,还有两个方面
- 模型压缩
1.1 剪枝
1.2 替换全连接层
1.3 单精度
- 边缘计算
中心训练,边缘推断,tensorflow lite, tensorflow.js (须考虑 硬件环境)
让**** topN ****问题快
- 降低向量的位数
模型层面降
通常resnet最后一层输出2048位,我们可以改成1024位,然后重新基于训练数据finetune一下模型,效果并不会太多变差,但是topN性能却能大幅提升。
向量层面降
1.PCA
2.乘积量化
- 选择更优的距离的度量类型
前文有提过,最常用的欧氏距离、cos距离、内积,内积是已经比较合适了,更高效的是汉明距离。
将图片向量先进行二值化,然后再计算汉明距离衡量相似度。
如2048位的二值向量,汉明距离范围是[0,2048],距离范围很小,再排序的时候就可以用到一些优化的办法,比如bucket。
缺点就是效果有可能会变差,尤其是在寻找同款商品的时候。
- 先聚类,后搜索
一个搜索向量来了之后,到一堆向量里去找最近的N个,假如这一堆向量能提前分成m类,每类有一个类中心,那我们搜索的时候只需要到和搜索向量最近的几个类中心所在到类搜索就行了。
比如以二维空间的一批向量为例

每个蓝点代表二维空间一个向量(x,y)
现在要搜索(xs, ys)的最近N个向量
先使用kmeans聚类到10个, 然后搜索的时候只搜最近的3个

蓝色的点为带搜索向量, 其他圆圈都是每个聚类的聚类中心,三个亮蓝色标注的是与代搜索向量最近的三个聚类中心。
由上图的情况,搜3个的效果基本上是没有误差的,回到图片搜索的问题上,向量空间的维数很高,上千维,向量数量级也非常大,千万甚至上亿,所以聚类的个数和搜索的类中心数量,就需要通过对比实验,效果和性能上的tradeoff。
以我们的经验,最终是1000个类,搜索最近的50个聚类中心。
- gpu
实践
一开始,我们是直接用java的,按上面说的几个思路,用java去写一个类似计算引擎的东西,实现质量难搞。
后来还是回归"选型"。。。
faiss,https://github.com/facebookresearch/faiss
它几乎都很好的支持了上面提到的几个优化思路,(其实乘积量化这些我们也没自己想出来,是用了faiss之后)
而且有Facebook的背书,经过一些demo的验证,发现效果非常好。基本上千万级别的topN问题耗时在个位数的毫秒。

多因素重排
(rerank)
经过上面的视觉相似搜索后,找出来的的确是视觉上最像的,但是因为我们的电商规模有限,大多数用户搜索的图片内的东西,在我们平台上没有对应的同款商品。
这就导致搜索结果的前几名都和搜索图片比较像,但都不是同款。因为距离比较产生先后顺序,但其实差距也很小,肉眼上来观察也很难说得清谁比谁更像。
这时候就需要结合特定用户的一些偏好,调整结果的排序,我们叫这一步为rerank。
因素
人+货

本质上是一个ctr预估问题
方案 lr/gbdt + lr
整体架构

汤不热吧