五一假期大家有沒有添置一件新女裝呢(逃
說到買衣服,你應該有過這樣的經歷:走在大街上忽然看到某個人穿著很酷炫的衣(nv)服(zhuang),心裡不禁感嘆這麼漂亮的衣服在哪買的,好想買一件穿穿,但你又不認識人家,只好望“衣”興嘆。但是假如現在有個方法能讓你根據衣服的照片就能找到網上的賣家呢?
德國有個程式設計師小哥 Aleksandr Movchan 給這個問題專門起了個名字——“街道到商店”(Street-to-Shop )購物,並且決定用機器學習中的距離度量學習(Distance Metric Learning,即 DML)解決這個問題。(再也不怕買不到喜歡的女裝了!)
度量學習
在介紹這篇“購物”教程前,先簡單說一說度量學習。 度量學習(Metric Learning)也就是常說的相似度學習。如果需要計算兩張圖片之間的相似度,如何度量圖片之間的相似度使得不同類別的圖片相似度小而相同類別的圖片相似度大就是度量學習的目標。
在數學中,一個度量(或距離函式)是一個定義集合中元素之間距離的函式。一個具有度量的集合被稱為度量空間。例如如果我們的目標是識別人臉,那麼就需要構建一個距離函式去強化合適的特徵(如髮色,臉型等);而如果我們的目標是識別姿勢,那麼就需要構建一個捕獲姿勢相似度的距離函式。為了處理各種各樣的特徵相似度,我們可以在特定的任務中通過選擇合適的特徵並手動構建距離函式。然而這種方法需要很大的人工投入,也可能對資料的改變非常不魯棒(robust)。度量學習作為一個理想的替代,可以根據不同的任務來自主學習出針對某個特定任務的度量距離函式。
度量學習方法可以分為通過線性變換的度量學習和度量學習的非線性模型。一些很經典的非監督線性降維演算法也可以看作非監督的馬氏度量學習,如主成分分析、多維尺度變換等。
度量學習已應用於計算機視覺中的影象檢索和分類、人臉識別、人類活動識別和姿勢估計,文字分析和一些其他領域如音樂分析,自動化的專案除錯,微陣列資料分析等。 下面我們就看看具體的教程。
構建資料集
首先,和任何機器學習問題一樣,我們需要一個資料集。實際上,有天我在阿里巴巴速賣通上看到有海量的服裝照片時,我就有了這麼一個想法:可以用這些資料做一個根據照片進行搜尋的功能。為了簡單便捷一點,我決定重點關注女裝,而且女孩子(和一部分男孩子)喜歡買衣服。
下面是我爬取影象的女裝類別:
-
裙子
-
女裝襯衫
-
衛衣&運動衫
-
毛衣
-
夾克&外套
我使用了 requests requests和 BeautifulSoup爬取影象。從服裝品類的主頁面上就可以獲取賣家的服裝照片,但是買家上傳的照片,我們需要在評價區獲取。在服裝頁面上有個“顏色”屬性,它可以用來判斷衣服是否是另一種顏色甚至是另外一種完全不同的服裝。所以我們會把不同顏色的衣服視為不同的商品。
你可以點選這裡 檢視我用來獲取一件服裝所有資訊的程式碼。
我們只需要按服裝類別搜尋服裝頁面,獲取所有服裝的 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,可以點選 這裡檢視。
你可以上街拍張你喜歡的妹子的衣服照片(注意安全),或者從驗證集中隨機找一張,傳到模型上,試試效果如何。
點選這裡,檢視本專案程式碼庫。