AIQ | 技术宅如何用 ML 技术进化为女装大佬



转载请注明 AIQ - 最专业的机器学习大数据社区  http://www.6aiq.com

AIQ 机器学习大数据 知乎专栏 点击关注

五一假期大家有没有添置一件新女装呢(逃

说到买衣服,你应该有过这样的经历:走在大街上忽然看到某个人穿着很酷炫的衣(nv)服(zhuang),心里不禁感叹这么漂亮的衣服在哪买的,好想买一件穿穿,但你又不认识人家,只好望“衣”兴叹。但是假如现在有个方法能让你根据衣服的照片就能找到网上的卖家呢?

德国有个程序员小哥 Aleksandr Movchan 给这个问题专门起了个名字——“街道到商店”(Street-to-Shop )购物,并且决定用机器学习中的距离度量学习(Distance Metric Learning,即 DML)解决这个问题。(再也不怕买不到喜欢的女装了!)

度量学习

在介绍这篇“购物”教程前,先简单说一说度量学习。
度量学习(Metric Learning)也就是常说的相似度学习。如果需要计算两张图片之间的相似度,如何度量图片之间的相似度使得不同类别的图片相似度小而相同类别的图片相似度大就是度量学习的目标。

在数学中,一个度量(或距离函数)是一个定义集合中元素之间距离的函数。一个具有度量的集合被称为度量空间。例如如果我们的目标是识别人脸,那么就需要构建一个距离函数去强化合适的特征(如发色,脸型等);而如果我们的目标是识别姿势,那么就需要构建一个捕获姿势相似度的距离函数。为了处理各种各样的特征相似度,我们可以在特定的任务中通过选择合适的特征并手动构建距离函数。然而这种方法需要很大的人工投入,也可能对数据的改变非常不鲁棒(robust)。度量学习作为一个理想的替代,可以根据不同的任务来自主学习出针对某个特定任务的度量距离函数。

度量学习方法可以分为通过线性变换的度量学习和度量学习的非线性模型。一些很经典的非监督线性降维算法也可以看作非监督的马氏度量学习,如主成分分析、多维尺度变换等。

度量学习已应用于计算机视觉中的图像检索和分类、人脸识别、人类活动识别和姿势估计,文本分析和一些其他领域如音乐分析,自动化的项目调试,微阵列数据分析等。
下面我们就看看具体的教程。

构建数据集

首先,和任何机器学习问题一样,我们需要一个数据集。实际上,有天我在阿里巴巴速卖通上看到有海量的服装照片时,我就有了这么一个想法:可以用这些数据做一个根据照片进行搜索的功能。为了简单便捷一点,我决定重点关注女装,而且女孩子(和一部分男孩子)喜欢买衣服。

下面是我爬取图像的女装类别:

  • 裙子

  • 女装衬衫

  • 卫衣 & 运动衫

  • 毛衣

  • 夹克 & 外套

我使用了 requests requestsBeautifulSoup爬取图像。从服装品类的主页面上就可以获取卖家的服装照片,但是买家上传的照片,我们需要在评价区获取。在服装页面上有个“颜色”属性,它可以用来判断衣服是否是另一种颜色甚至是另外一种完全不同的服装。所以我们会把不同颜色的衣服视为不同的商品。

你可以点击这里 查看我用来获取一件服装所有信息的代码。

我们只需要按服装类别搜索服装页面,获取所有服装的 URL,使用上面的代码获得每件服装的信息。

最后,我们会得到每件服装的两个图像数据集:来自卖家的图像(item[‘colors’] 中每个元素的 URL 字段)和来自买家的图像(item[‘feedbacks’] 中每个元素的 URL 字段)。

对于每个颜色,我们只获取来自卖家的一张照片,但来自买家的照片可能不止一张,有时候甚至一张照片都没有(这和天猫上的买家秀一样,有的会大秀特秀,有的一张都不秀)。

很好!我们得到了想要的数据。但是,得到的数据集里有很多噪声数据:
来自买家的图像数据中有很多噪声,比如快递包装的照片,只秀衣服质地的照片而且只露出了一部分,还有些是刚撕开包裹时拍的照片。

为了减轻这个问题,我们将 5000 张图像标记成两个类别:良性照片和噪声照片。刚开始,我的计划是针对这两个类别训练一个分类器,然后用它来清洗数据集。但随后我决定将这个工作留在后面,仅仅将干净的数据添加进测试数据集和验证数据集中。

第二个问题是有时候好几个卖家卖同一样服装,而且有时候几家服装店展示的衣服照片都一样(或只是稍微做了些编辑工作)。怎么解决这种问题呢?

最容易的一种方法就是什么都不做,用距离度量学习中的一种鲁棒算法。不过这种方式会影响数据集的验证效果,因为我们在验证和训练数据中会有相同的服装。所以会导致数据外泄。另一种方法是寻找相似的(甚至完全相同的)服装,然后将它们合并为一件服装。我们可以用感知哈希(perceptual hashing)寻找相同的服装照片,或者也可以用噪声数据训练一个模型,将模型用于寻找相同的服装照片。我选择了第二种方法,因为它能让相同的照片合并为一张,哪怕是稍微编辑过的照片。

距离度量学习

最常用的一个距离度量学习方法是 Triplet loss:

其中 max(x,0)是 hinge 函数,d(x,y)是 x 和 y 之间的距离函数,F(x)是深度神经网络,M 是边界,a 是 anchor,p 是正点数,n 是负点数。
F(a), F(p), F(n) 是有深度神经网络生成的高维度空间(向量)中的点。有必要提一下,为了让模型在应对照片的光照和对比度变化时更加鲁棒,常常需要将向量进行正则化以获得相同的单元长度,例如 ||x|| = 1。Anchor 和正样本属于同一类比,负样本是另一个类别中的例子。

那么 Triplet loss 的主要理念就是用一个距离边界 M 将正例对(anchor 和 positive)的向量与负例对(anchor 和 negative)的向量进行区分。

但是怎样选择 triplet(a, p, n)呢?我们可以随机选择样本作为一个 triplet,但这会导致下面的问题。首先,会存在 N³ 个可能的 triplet。这意味着我们需要花很多时间来遍历所有可能的 triplet。但是实际上,我们不需要这么做,因为训练迭代几次后,很多元 triplet 已经符合 triplet 限制(比如 0 损失),也就是说这些 triplet 对于训练没有用处。

对于选择 triplet 的一个最常见方式是难分样本挖掘(hard negative mining):

选择最难的难样本在实际中会导致训练初期出现糟糕的局部最小值。具体来说,就是它会造成一个收缩的模型(例如 F(x) = 0)。要想减轻这种问题,我们可以用半难分样本挖掘(semi-hard negative mining)。

半难分样本要比正样本离 anchor 更远一些,但它们仍然难以区分(不符合 triplet 限制),因为它们在边界 M 内部。

生成半难分(和难分)样本有两种方式:在线和离线。

  • 在线方式是说我们可以从训练数据集中随机选择一些样本组成小批量数据,从这里面的样本中选择 triplet。但是用在线方式我们还需要有大批量的数据。在我们这个例子中,没法做到这一点,因为我只有一个仅 8 G RAM 的 GTX 1070。

  • 离线方式中,我们需要隔段时间暂停训练,为一定数量的样本预测向量,选择 triplet,并用这些 triplet 训练模型。这意味着我们需要进行两次正推计算,这也是使用离线方式要付出的一点代价。

很好!我们现在可以用 triplet loss 和离线的半难分样本挖掘方法训练模型了。但是!(每当好事将近时,都有出现“但是”,这回也没拿错剧本)我们还需要一些方法才能完美地解决“街道到商店”问题。我们的任务是找到卖家和买家相同程度最高的衣服照片。

但是,往往卖家的照片质量要比买家上传的照片质量好得太多(想想也是,网店发布的照片一般都经过了 N 道 PS 处理程序),所以我们会有两个域:卖家照片和买家照片。要想获得一个高效率模型,我们需要缩小这两个域的差距。这个问题就叫做域适应(domain adaptation)。

我提议一个很简单的方法来缩小这两个域的差距:我们从卖家照片中选择 anchor,从买家照片中选择正负样本。就这些!虽然简单但是很有效。

实现

为了实现我的想法,进行快速试验,我使用了 Keras 程序库和 TensorFlow 后端。

我选择了 Inception V3 作为我的模型的基本卷积神经网络。和正常操作一样,我用 ImageNet 权重初始化了卷积神经网络。然后在用 L2 正则化后在网络末端添加两个完全相连的层。向量大小为 128。

def get_model():
 no_top_model = InceptionV3(include_top=False, weights='imagenet', pooling='avg')

 x = no_top_model.output
 x = Dense(512, activation='elu', name='fc1')(x)
 x = Dense(128, name='fc2')(x)
 x = Lambda(lambda x: K.l2_normalize(x, axis=1), name='l2_norm')(x)
 return Model(no_top_model.inputs, x)

我们同样也需要实现 triplet 损失函数,可以将 anchor,正负样本作为一个单独的小批量数据传入函数,并将这个小批量数据在函数中分为 3 个张量。距离函数为欧式距离平方。

def margin_triplet_loss(y_true, y_pred, margin, batch_size):
 out_a = tf.gather(y_pred, tf.range(0, batch_size, 3))
 out_p = tf.gather(y_pred, tf.range(1, batch_size, 3))
 out_n = tf.gather(y_pred, tf.range(2, batch_size, 3))

 loss = K.maximum(margin
 + K.sum(K.square(out_a-out_p), axis=1)
 - K.sum(K.square(out_a-out_n), axis=1),
 0.0)
 return K.mean(loss)

并优化模型:

#utility function to freeze some portion of a function's arguments
from functools import partial, update_wrapper
def wrapped_partial(func, *args, **kwargs):
 partial_func = partial(func, *args, **kwargs)
 update_wrapper(partial_func, func)
 return partial_func

opt = keras.optimizers.Adam(lr=0.0001)
model.compile(loss=wrapped_partial(margin_triplet_loss, margin=margin, batch_size=batch_size),

实验结果

模型的性能衡量指标称为 R@K。
我们看看怎样计算 R@K。验证集中的每个买家照片作为一次查询,我们需要找到相应的卖家照片。我们每查询一次照片,就会计算嵌入向量并搜索该向量在所有卖家照片中的最近邻向量。我们不仅会用到验证集中的卖家照片,而且也会用到训练集中的卖家照片,因为这样可以增加干扰数量,让我们的任务更有挑战性。

所以我们会得到一张查询照片,以及一列最相似的卖家照片。如果在 K 个最相似照片中存在一个相应的卖家照片,我们为该查询返回 1,如果不是,返回 0。现在我们需要为验证集中的每一次查询返回这样一个结果,然后找到每次查询的平均得分。这就是 R@K。

正如我上文所说,我从噪声数据中清洗出了小部分买家照片。因而我用两个验证集测试了模型的质量:一个完整的验证集和一个仅有干净数据的子集。

模型的结果不是很理想,我们还可以这么做进行优化:

  • 将买家数据从噪声数据中清洗出来。在这方面,我已经做了第一步,清洗出了一个小数据集。

  • 更精准地合并服装照片(至少在验证集中这么做)。

  • 进一步缩小域之间的差距。我认为可以用特定域增强方法(例如增强图像的光照度)和其它特定方法完成(比如这篇论文中的方法)。

  • 使用另一种距离度量学习方法,我试了 这篇论文中的方法,但效果更糟了。

  • 当然还有收集更多的数据。

Demo,代码和训练后的模型

我给模型制作了一个 demo,可以点击 这里查看。

你可以上街拍张你喜欢的妹子的衣服照片(注意安全),或者从验证集中随机找一张,传到模型上,试试效果如何。

点击这里,查看本项目代码库。


更多高质资源 尽在AIQ 机器学习大数据 知乎专栏 点击关注

转载请注明 AIQ - 最专业的机器学习大数据社区  http://www.6aiq.com