DNN召回
這里指的是youtube的Deep Neural Network for YouTube Recommendation論文里提到的模型,論文里同時(shí)提出了召回和排序,這里先只介紹召回精刷。大廠畢竟還是nb啊揭措,看起來(lái)其實(shí)挺簡(jiǎn)單胆数,為什么別人提不出來(lái)呢?細(xì)節(jié)滿滿黎泣,意味著坑也滿滿恕刘。1.論文鏈接 2.簡(jiǎn)書(shū)上的靠譜分享 3.我之前在簡(jiǎn)書(shū)上的簡(jiǎn)單一提
直接上圖:
模型很簡(jiǎn)單,先看輸入抒倚。左下藍(lán)色的是item embedding褐着,是用item的id從random初始化的emb_matrix里look_up出來(lái)的,一個(gè)用戶(或者說(shuō)一條樣本更準(zhǔn)確些)的若干個(gè)item平均一下托呕,進(jìn)入dnn含蓉;
綠色的是用戶搜索的query,跟item本質(zhì)上沒(méi)什么不同项郊,也是平均一下進(jìn)入dnn馅扣;
再其他concat到一起的是各種各樣的特征,這里主要是用戶的特征着降。想一下為什么不用item的特征差油?因?yàn)閕tem這么多,你怎么知道加哪個(gè)item的特征叭蛋厌殉!當(dāng)然也許是有什么巧妙特征的,只是實(shí)踐里沒(méi)這么麻煩過(guò)侈咕。有了輸入公罕,經(jīng)過(guò)三層網(wǎng)絡(luò),就是輸出耀销,把這個(gè)模型當(dāng)做是超大規(guī)模的分類(lèi)模型楼眷,每一個(gè)item可以理解為一個(gè)類(lèi)。
問(wèn)題來(lái)了熊尉。
輸入的emb直接做均值合理嗎罐柳?
我隱約記得這是論文里指出的,好像是嘗試過(guò)add/concat效果都不如average狰住。在實(shí)踐中還是用了加權(quán)平均(但沒(méi)有跟直接平均對(duì)照試驗(yàn)過(guò)张吉,每次改動(dòng)的地方都不止一個(gè)),權(quán)重的設(shè)計(jì)思想就是跟(觀看后的)天數(shù)成反比催植、跟觀看完成率成正比肮蛹,公式是:
weight_avg_watch = tf.exp(-self.batch_watchDays/7.0) * self.batch_watchProp
那求均值的視頻/query個(gè)數(shù)要固定嗎勺择?
其實(shí)沒(méi)必要固定。在實(shí)踐中伦忠,為了簡(jiǎn)單起見(jiàn)省核,訓(xùn)練的時(shí)候樣本都是固定長(zhǎng)度的,而推理(后面會(huì)再說(shuō)一下)的時(shí)候就是不固定長(zhǎng)度的了昆码,有多少算多少(但設(shè)置max)气忠,然后求均值。
有梯度消失或梯度爆炸的問(wèn)題嗎赋咽?
這是有的旧噪,一看三層relu就知道有問(wèn)題了,實(shí)踐中改成了leakyrelu冬耿,有改善舌菜。其他的方法比如clip、BN亦镶、lr decay等都可以嘗試日月。
對(duì)時(shí)間怎么建模?example age是什么缤骨?
example age就是target item在“當(dāng)前”的年齡( the age of the training example )爱咬,沒(méi)有那么多彎彎繞,因?yàn)閥outube用戶對(duì)新視頻更加偏好绊起,即使相關(guān)性不太好也可以.
超大規(guī)模的分類(lèi)精拟,計(jì)算資源夠用嗎?
超大規(guī)模的分類(lèi)虱歪,實(shí)踐中通常是幾百萬(wàn)蜂绎,計(jì)算資源開(kāi)銷(xiāo)太大,光算loss都要半年笋鄙,所以不能這樣干师枣。我們用采樣的方式構(gòu)造負(fù)樣本,TensorFlow牛逼萧落,tf.nn.log_uniform_candidate_sampler這個(gè)函數(shù)的思想就是給定正樣本践美,在剩下的樣本里,越是靠前的越容易被抽出來(lái)當(dāng)負(fù)樣本找岖,這叫按照 log-uniform (Zipfian) 分布采樣陨倡。一般采五六百吧。注:vocab是按照頻率排的许布,因此越靠前兴革,就是越熱門(mén)的item。vocab怎么得到的蜜唾?可以在產(chǎn)出樣本的時(shí)候順便產(chǎn)出杂曲。貼一段代碼:
import org.apache.spark.ml.feature.{CountVectorizer, CountVectorizerModel}
...
...
val watchDF = sqlContext.createDataFrame(sampleWatch).toDF("uid", "watch")
val watchCvModel: CountVectorizerModel = new CountVectorizer()
.setInputCol("watch")
.setOutputCol("watch_fea")
.setVocabSize(watchVocabSize)
.setMinDF(watchVocabMinFreq)
.fit(watchDF)
val watchVocab = watchCvModel.vocabulary.zipWithIndex
val formatWatchVocab = watchVocab.map(x => s"${x._1}\t${x._2}")
sc.parallelize(formatWatchVocab, 1).saveAsTextFile(outputWatchVocab)
那直接用展示未點(diǎn)擊的來(lái)做負(fù)樣本不好嗎箕昭?
不好。一個(gè)是麻煩解阅。另一個(gè)是,展示未點(diǎn)擊的泌霍,其實(shí)是排序模型認(rèn)為很好的選項(xiàng)了货抄,直接用label=0來(lái)打擊它并不好,一般用戶對(duì)它是有一定的興趣的朱转。還有就是蟹地,要predicting future watch,而不是predicting held-out watch藤为,不要偷窺未來(lái)怪与。
要如何構(gòu)造樣本?上面兩段說(shuō)明了負(fù)樣本如何選擇缅疟,其實(shí)就是為了在這里說(shuō)樣本如何選擇分别。負(fù)樣本是采樣得到的,那構(gòu)造樣本的時(shí)候只要記錄input和正樣本就好了存淫。正樣本是什么耘斩?點(diǎn)擊的?觀看的桅咆?還是觀看完成度高于某個(gè)值的括授?這里需要根據(jù)業(yè)務(wù)來(lái)定,比如長(zhǎng)視頻岩饼,甚至還需要你將用戶分段的觀看累計(jì)起來(lái)荚虚。input是什么?我們有了用戶的觀看或者行為歷史籍茧,既可以一個(gè)用戶產(chǎn)出多條樣本版述,也可以一個(gè)用戶產(chǎn)出一條樣本,這兩個(gè)選項(xiàng)又分別有多種不同的方法硕糊。比如院水,一個(gè)用戶的正向行為list是abcdefg,那我的樣本可以是(abcd->e,abcd->f,abcd->g,bcde->f,bcde->g)简十。這個(gè)選擇比較自由檬某,也沒(méi)有對(duì)比過(guò)優(yōu)劣,應(yīng)該是差不多的螟蝙。
用戶特征選什么比較好恢恼?
用戶特征使用靜態(tài)特征(年齡、性別胰默、居住地...)比較好场斑,因?yàn)榻y(tǒng)計(jì)特征本質(zhì)上只是區(qū)分了用戶是否活躍漓踢,這不是我們想要的÷┮或者其他各種途徑得到的用戶emb特征喧半,也可以加進(jìn)去試試。
有什么其他的改進(jìn)嗎青责?
- 例如加入attention挺据,這個(gè)實(shí)踐中效果有限,也有分享說(shuō)效果很好脖隶,應(yīng)該是業(yè)務(wù)不同導(dǎo)致的扁耐。
- 例如將item的某些屬性(uploader、tag等)與item的emb concat到一起产阱,再計(jì)算平均值婉称。這其實(shí)相當(dāng)于又加了若干個(gè)query。
- 使用LSTM替換average是否有效构蹬?基本沒(méi)效果王暗。
網(wǎng)絡(luò)結(jié)構(gòu)的代碼可見(jiàn) youtube-dnn-recall-structure.py
問(wèn)題又來(lái)了。
上面一節(jié)說(shuō)的主要是訓(xùn)練怎燥。這一節(jié)主要說(shuō)線上服務(wù)瘫筐。
那我要怎么提供線上服務(wù)啊铐姚?
- 這其實(shí)還有一個(gè)內(nèi)部隱含的問(wèn)題策肝,就是誰(shuí)是item的emb,誰(shuí)是user的emb隐绵?我們看模型結(jié)構(gòu)示意圖之众,覺(jué)得item emb不是輸入的嗎?其實(shí)不是依许。網(wǎng)絡(luò)上有人將輸入的emb當(dāng)做item棺禾,也取得了不錯(cuò)的效果,這是有可能的峭跳,但膘婶,確實(shí)不太對(duì)。說(shuō)結(jié)論蛀醉,user的emb是最后一層隱藏層h(激活后)悬襟,item的emb就是這個(gè)隱藏層到softmax之前的權(quán)重矩陣W。為什么呢拯刁?因?yàn)閥 = Wh + b脊岳,如此才能顯示出W和h之間的聯(lián)系(這里其實(shí)有個(gè)問(wèn)題,b存在的意義是什么?實(shí)踐中前人的代碼里寫(xiě)了b割捅,但我覺(jué)得可能不需要b吧奶躯,只是后來(lái)沒(méi)做實(shí)驗(yàn)了。)亿驾。另外嘹黔,input的emb其實(shí)可以用其他方式得到的emb來(lái)初始化,例如item2vec的emb莫瞬;并且参淹,input的emb可以與W共享,同時(shí)更新乏悄,這樣就不用矛盾選擇哪個(gè)emb了。
- 線上召回可以有兩種恳不,一是II召回檩小,也就是用item emb計(jì)算相似度,得到倒排烟勋,進(jìn)而召回规求,這里可以看出我們?cè)谏弦还?jié)的優(yōu)化中concat了uploader等屬性的好處,那就是同一個(gè)up的item之間天然的具有一定相似度卵惦;二是UI召回阻肿,也就是,將user存到couchbase/Aerospike里沮尿,將item用faiss訓(xùn)練為一個(gè)index丛塌,然后線上根據(jù)user查找相近的item。
加一個(gè)問(wèn)題畜疾,UI召回里的相似度怎么算赴邻?為什么?II呢啡捶?
UI召回的相似度是用內(nèi)積的姥敛,而不是余弦,這是因?yàn)榫W(wǎng)絡(luò)訓(xùn)練的時(shí)候就是內(nèi)積計(jì)算瞎暑。II召回的相似度彤敛,內(nèi)積、余弦了赌、歐氏距離墨榄,都可以嘗試,可以根據(jù)實(shí)際情況來(lái)決定揍拆,我在實(shí)踐中是都用過(guò)渠概,效果差不多,都還不錯(cuò)。但從理解上來(lái)說(shuō)播揪,余弦可能更科學(xué)一點(diǎn)贮喧,畢竟是同一空間?
再加一個(gè)問(wèn)題猪狈,softmax的W要不要?dú)w一化箱沦?hidden要不要?dú)w一化?也就是說(shuō)雇庙,user和item的emb需要?dú)w一化嗎谓形?從上文的理解來(lái)說(shuō),都行疆前,這個(gè)意思是寒跳,你在訓(xùn)練的時(shí)候怎么做的,在用的時(shí)候就怎么做竹椒。如果你網(wǎng)絡(luò)里歸一了童太,使用的時(shí)候就歸一,如果沒(méi)有胸完,那就不用书释,不然會(huì)起反效果(試驗(yàn)過(guò)的)。
插一個(gè)問(wèn)題赊窥,faiss訓(xùn)練index的具體原理爆惧?faiss是一種高效的k-means聚類(lèi)實(shí)現(xiàn),facebook 牛逼(破音)锨能!具體的我也沒(méi)太了解過(guò)扯再。安裝可以看久遠(yuǎn)的過(guò)去,使用可以看簡(jiǎn)書(shū)大佬的分享址遇。貼幾句使用示例吧:
index = faiss.index_factory(dim, factory)
index.nprobe = nprobe
index.train(embedding_all)
index.add_with_ids(embedding_all, fid_all)
faiss.write_index(index, out_index)
好了叔收,現(xiàn)在知道item的emb要訓(xùn)練index索引了,當(dāng)然這個(gè)item emb在訓(xùn)練結(jié)束時(shí)候可以存下來(lái)傲隶。user的emb要怎么弄饺律?一種方法是python來(lái)load模型然后推理一次,一種方法是手寫(xiě)前向然后推理跺株。python的優(yōu)點(diǎn)是簡(jiǎn)單复濒,手寫(xiě)一般是用scala/java,優(yōu)點(diǎn)是可以為后續(xù)的實(shí)時(shí)化做準(zhǔn)備乒省。貼一小段代碼:
import breeze.linalg.{DenseMatrix, DenseVector, normalize}
...
...
def feedForward(x: DenseVector[Double],
w: Array[DenseMatrix[Double]],
b: Array[DenseVector[Double]]): DenseVector[Double] = {
val layer0 = w(0) * x + b(0)
val relu0 = layer0.map { e => if (e > 0) e else 0.0 }
val layer1 = w(1) * relu0 + b(1)
val relu1 = layer1.map { e => if (e > 0) e else 0.0 }
val layer2 = w(2) * relu1 + b(2)
val relu2 = layer2.map { e => if (e > 0) e else 0.2 * e }
relu2
}
dnn召回的效果指標(biāo)都挺不錯(cuò)巧颈,展示占比也不低,是主要的召回源之一袖扛。實(shí)踐中砸泛,user的emb存到cb十籍,通常只存最新的,而item的index需要訓(xùn)練新舊兩個(gè)版本唇礁,避免cb沒(méi)刷完時(shí)user找不到item勾栗。