基於深度學習的個性化召回推薦算法item2vec
item2vec是基於word2vec的原理,word2vec的詳細內容請參考Tensorflow深度學習算法整理(二)
我們先將原始數據文件轉換成訓練數據集文件,訓練數據文件包含每一個用戶點擊過的物品id。在這裏,我們是把用戶點擊過的Itemid當成上下文的詞向量,再來根據用戶的喜好來挑選出出現概率最大的物品來進行推薦。
INPUT = "../item2vec_data/" import os def product_train_data(input_file, output_file): ''' 獲取訓練數據集文件 :param input_file: :param output_file: :return: ''' if not os.path.exists(input_file): return # 用戶點擊過的商品{用戶id:[商品id1,商品id2,...]} record = {} linenum = 0 score_thr = 4. with open(input_file, 'r') as fp: for line in fp: if linenum == 0: linenum += 1 continue item = line.strip().split(',') if len(item) < 4: continue userid, itemid, rating = item[0], item[1], float(item[2]) if rating < score_thr: continue if userid not in record: record.setdefault(userid, []) record[userid].append(itemid) with open(output_file, 'w') as fw: for userid in record: fw.write(" ".join(record[userid]) + "\n") if __name__ == "__main__": product_train_data(INPUT + "ratings.txt", INPUT + "train_data.txt")
運行結果
現在我們便對訓練數據進行訓練,然後再根據物品的embedding相似度進行推薦
INPUT_PATH = "../item2vec_data/" from gensim.models import word2vec import logging from util.reader import get_user_click import pprint def get_sentence(data_path): # 預處理 sentence = [] with open(data_path, 'r') as f: for line in f: sentence.append(line.strip()) return sentence def cal_recom_result(distance, user_click): model = word2vec.Word2Vec.load(distance) recent_click_num = 3 topk = 5 recom_info = {} for user in user_click: click_list = user_click[user] recom_info.setdefault(user, {}) for itemid in click_list[:recent_click_num]: try: recom_info[user].setdefault(itemid, []) recom_info[user][itemid].append(model.wv.most_similar(itemid, topn=topk)) except Exception: continue return recom_info if __name__ == "__main__": sentence = get_sentence(INPUT_PATH + "train_data.txt") train_data = [list(item.split(" ")) for item in sentence] logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', level=logging.INFO) model = word2vec.Word2Vec(train_data, vector_size=200, sg=0, workers=-1, min_count=1, epochs=3) model.save(INPUT_PATH + "distance") user_click, user_click_time = get_user_click(INPUT_PATH + "ratings.txt") pprint.pprint(cal_recom_result(INPUT_PATH + "distance", user_click)["1"])
運行結果(省略日誌)
{'2': [[('3527', 0.2799011170864105),
('111115', 0.26561179757118225),
('26444', 0.25397157669067383),
('432', 0.25121787190437317),
('53766', 0.24429647624492645)]],
'29': [[('2178', 0.2700711488723755),
('347', 0.2577595114707947),
('6666', 0.2471877485513687),
('26339', 0.24447323381900787),
('83468', 0.23756802082061768)]],
'32': [[('7171', 0.26247233152389526),
('97834', 0.26078081130981445),
('7348', 0.25278741121292114),
('1654', 0.2401883602142334),
('7885', 0.22451621294021606)]]}
現在我們來擴展一下,將物品的相關內容也放入進來進行相似度的推薦。
基於內容的推薦算法content based
- 個性化召回算法Content Based背景介紹
基於內容的推薦不同於之前任何一種個性化召回算法,它屬於獨立的分支。像之前的CF、LFM、Personal Rank都同屬於基於鄰域的推薦。Item2vec屬於深度學習的推薦。
- Content Based算法主體流程介紹
在這個算法的主體流程大部分並不屬於個性化推薦的範疇,實際上應該從屬於NLP或者用戶畫像的內容範疇。只有極少數的一部分屬於個性化推薦算法的內容範疇。
背景
- 思路極簡,可解釋性強。
任何一個推薦系統的初衷都是爲了推薦出用戶喜歡的Item。而基於內容的推薦恰是刻畫出用戶的喜好之後給予用戶推薦這個喜好的物品。如果某一個用戶訪問系統的時候經常點擊體育類的新聞,在這個用戶下一次訪問系統的時候,自然而然的系統更加傾向性的給他推薦體育類型的新聞。對於推薦結果可解釋性非常的強。
- 用戶推薦的獨立性
基於內容的推薦結果只與該用戶本身的行爲有關係,其餘用戶的行爲是影響不到該用戶的推薦結果。但是無論是CF、LFM、Personal Rank以及Item2vec,其餘用戶的行爲都會一定程度上或多或少地干預到最後的推薦結果。
- 問世較早,流行度高
基於內容推薦的極簡性、可解釋性,所以它出現的非常早,並且無論是在工業界還是研究界都作爲一種基礎的召回算法,流行度非常高。但是任何事物都是有兩面性的,基於內容的推薦並不完美,它有非常明顯的缺點:1、它對於推薦的擴展性是比較差的,也就是說如果一個用戶之前經常訪問體育類的新聞,那麼在之後的推薦中,傾向於在體育範圍內不斷的挖掘,而很難完成跨領域的物品推薦。2、需要積累一定量的用戶的行爲,才能夠完成基於內容的推薦。
Content Based算法主流程
- Item Profile
針對於基於內容推薦下,Item的刻畫大體可以分爲兩大類:1、關鍵詞刻畫;2、類別刻畫。無論在什麼場景下,都是這兩個類的刻畫。譬如信息流場景下,我們需要刻畫出一件新聞屬於財經還是娛樂。這件新聞講的是某個球星還是某個明星。在電商場景下,我們需要刻畫出這個物品它屬於圖書還是屬於玩具,具體的關鍵詞上,也會有是講這個物品是講機器學習的還是講人文情感的。這個物品是參與滿減的還是參與包郵的。
- User Profile
第一步,我們完成了物品的內容刻畫之後,第二步我們需要對用戶進行刻畫。傳統範疇的用戶畫像是比較寬泛的,它不僅包含了用戶的一些動態特徵,還包含了它的一些靜態特徵。而我們用在基於內容推薦裏的,更多的聚焦在用戶長期短期的行爲,繼而通過這個行爲的分析,將用戶感興趣的類別予以刻畫。
- Online Recommendation
有了Item的刻畫,有了User的刻畫,便是在線上完成個性化推薦的過程。也就是說給用戶推薦他最感興趣的一些類別。假使某個用戶經常點擊某個球星的新聞,當這個用戶訪問系統的時候,我們應該將該球星最新的新聞最及時的消息推薦給這個用戶,這樣點擊率自然會很高。
經過這三步的介紹,我們發現前兩步是從屬於NLP和用戶畫像的範疇,第三步是個性化內容推薦的範疇。
Item Profile技術要點
- Topic FInding
針對於Topic發現,我們首先要選定特徵,這裏的特徵是title和內容主體的詞語的分詞。我們得到了這個詞語的分詞之後,針對於Topic的發掘,我們採用命名實體識別的方式,這個方式可以去匹配關鍵詞詞表。得到了這些關鍵詞之後,我們需要對這些關鍵詞進行一定的排名。將排名最高的Top 3或Top 5給Item完成Label。至於這裏的排名,我們會用一些算法加一些規則,算法諸如像TF-IDF(關於TF-IDF的內容請參考Tensorflow深度學習算法整理(二) ),規則是基於我們自己的場景總結出來的一些來修正錯誤keys的一些規則。
- Genre Classify
對於類別的劃分,我們同樣是首先選定好特徵,這裏同樣是利用一些文本信息,比如說title或者正文中所有的去過標點之後的分詞得到的詞向量,這裏詞向量可以直接在淺層模型中進行one-hot編碼。在深層模型中可以進行embedding。這裏使用的分類模型主要是像早期的邏輯迴歸,像中期的FastText以及近期的Text CNN等等。這些分類器我們在使用的時候,只使用多種分類器分別在不同的權重,然後對結果進行一個線性的加權,從而得到正確的分類。
以上我們是針對於文檔的Topic發掘或者是類別的分類進行的敘述。對於短視頻,實際上現在引入了一些更多的特徵,比如關鍵幀所對應圖像的分類識別以及音頻所對應語音識別後文字的處理等一些有益的嘗試。
User Profile技術要點
- Genre/Topic
用戶對哪些種類的新聞或者說物品感興趣,另一個層面就是說對哪些關鍵詞感興趣。現在多是基於統計的方式,也同時做一些有益的嘗試,比如引入一些分類器等等。
- Time Decay
對於用戶的刻畫,我們一定要注意時間衰減。也就是不同時期的行爲所佔權重是不同的。最終我們想刻畫得到的結果是針對某個用戶,我們想得到用戶對於不同種類Item的傾向性,譬如某個用戶對於娛樂的傾向性是0.7,對於財經的傾向性是0.3。
Online Recommendation技術要點
- Find top k Genre/Topic
基於用戶的刻畫,找到用戶最感興趣的top k個分類。
- Get the best n item from fixed Genre/Topic
由於這top k個分類都是帶有權重的,相應的給每個分類得到n個最好的這個分類下的item。這裏有兩點需要說明,由於權重的不同,不同種類下召回的n個數目是不同的,譬如某人對財經感興趣對娛樂也感興趣,但是對娛樂感興趣的程度更高,那麼這裏對娛樂召回的數目就要多於對財經的召回的數目。這裏的best對於不是新item來講,就是它後面CTR;如果是新的item,我們在入庫的時候,都會給出一個預估的CTR,那麼我們就用這個預估的CTR來作爲item是否好的衡量標準。
代碼實現
我們先來實現物品平均評分和均分物品類別權重以及物品類別的倒排
import os def get_ave_score(input_file): ''' 物品平均評分 :param input_file: 評分文件 :return: ''' if not os.path.exists(input_file): return {} linenum = 0 record = {} ave_score = {} with open(input_file, 'r') as fp: for line in fp: if linenum == 0: linenum += 1 continue item = line.strip().split(",") if len(item) < 4: continue userid, itemid, rating = item[0], item[1], float(item[2]) if itemid not in record: record[itemid] = [0, 0] # 這裏0爲物品被評價的總分數,1爲物品爲物品被總點擊的次數 record[itemid][0] += rating record[itemid][1] += 1 for itemid in record: # 計算物品的平均評價分數 ave_score[itemid] = round(record[itemid][0] / record[itemid][1], 3) return ave_score def get_item_cate(ave_score, input_file): ''' 均分類別權重和物品類別倒排 :param ave_score: 物品平均評分 :param input_file: 物品詳情文件 :return: ''' if not os.path.exists(input_file): return {}, {} linenum = 0 item_cate = {} record = {} cate_item_sort = {} topk = 100 with open(input_file, 'r') as fp: for line in fp: if linenum == 0: linenum += 1 continue item = line.strip().split(",") if len(item) < 3: continue itemid = item[0] cate_str = item[-1] # 獲取物品的分類列表 cate_list = cate_str.strip().split("|") # 物品的分類權重 ratio = round(1 / len(cate_list), 3) if itemid not in item_cate: item_cate.setdefault(itemid, {}) for fix_cate in cate_list: # 儲存該物品的分類權重{物品id:{物品分類:分類權重}} item_cate[itemid][fix_cate] = ratio for itemid in item_cate: # 遍歷每一個物品的分類 for cate in item_cate[itemid]: if cate not in record: record.setdefault(cate, {}) # 獲取該物品的平均評價分數 itemid_rating_score = ave_score.get(itemid, 0) # 按照物品分類來儲存該分類內各個物品的平均評價分數{物品分類:{物品id:平均評價分數}} record[cate][itemid] = itemid_rating_score for cate in record: if cate not in cate_item_sort: cate_item_sort.setdefault(cate, []) # 對每一個物品分類中按照平均評價分數對物品id進行排序 for combine in sorted(record[cate].items(), key=lambda x: x[1], reverse=True)[:topk]: # 儲存排序後的各個物品分類中物品的平均評價分數{物品分類:[(物品id1,平均評價分數1), (物品id2,平均評價分數2)]} # 這裏平均評價分數1>=平均評價分數2 cate_item_sort[cate].append((combine[0], combine[1])) return item_cate, cate_item_sort if __name__ == "__main__": ave_score = get_ave_score("../data/ratings.txt") print(len(ave_score)) print(ave_score['31']) item_cate, cate_item_sort = get_item_cate(ave_score, "../data/movies.txt") print(item_cate['1']) print(cate_item_sort['Children'])
運行結果
4382
3.167
{'Adventure': 0.2, 'Animation': 0.2, 'Children': 0.2, 'Comedy': 0.2, 'Fantasy': 0.2}
[('250', 5.0), ('1030', 5.0), ('2091', 5.0), ('2102', 5.0), ('2430', 5.0), ('4519', 5.0), ('26084', 5.0), ('27790', 5.0), ('156025', 5.0), ('2046', 4.75), ('2138', 4.5), ('3034', 4.5), ('6350', 4.5), ('7164', 4.5), ('85736', 4.5), ('5971', 4.4), ('262', 4.375), ('26662', 4.333), ('81564', 4.333), ('1033', 4.25), ('3213', 4.25), ('78499', 4.222), ('2005', 4.167), ('76093', 4.143), ('1148', 4.125), ('60069', 4.1), ('745', 4.083), ('2987', 4.059), ('8', 4.0), ('314', 4.0), ('837', 4.0), ('917', 4.0), ('953', 4.0), ('1009', 4.0), ('1011', 4.0), ('1021', 4.0), ('1023', 4.0), ('1031', 4.0), ('1223', 4.0), ('1566', 4.0), ('2014', 4.0), ('2034', 4.0), ('2037', 4.0), ('2083', 4.0), ('2096', 4.0), ('2099', 4.0), ('2141', 4.0), ('2687', 4.0), ('2846', 4.0), ('3086', 4.0), ('3159', 4.0), ('3189', 4.0), ('3564', 4.0), ('5159', 4.0), ('6559', 4.0), ('6753', 4.0), ('6793', 4.0), ('6951', 4.0), ('8537', 4.0), ('27253', 4.0), ('27731', 4.0), ('37857', 4.0), ('50601', 4.0), ('56171', 4.0), ('68954', 4.0), ('79091', 4.0), ('84944', 4.0), ('86298', 4.0), ('87222', 4.0), ('95858', 4.0), ('106022', 4.0), ('110461', 4.0), ('1282', 3.929), ('1907', 3.917), ('59784', 3.917), ('50872', 3.885), ('919', 3.861), ('531', 3.857), ('596', 3.85), ('3396', 3.833), ('1', 3.829), ('2804', 3.818), ('1073', 3.812), ('1097', 3.808), ('34', 3.795), ('4886', 3.792), ('2081', 3.773), ('986', 3.75), ('2033', 3.75), ('2052', 3.75), ('2080', 3.75), ('2087', 3.75), ('5103', 3.75), ('6316', 3.75), ('8961', 3.75), ('38038', 3.75), ('44022', 3.75), ('46948', 3.75), ('65261', 3.75), ('103335', 3.75)]
現在我們來進行用戶刻畫。
import os def get_up(item_cate, input_file): ''' 用戶刻畫 獲取某用戶點擊過的某物品分類佔該用戶點擊過的所有物品分類的評分佔比 :param item_cate: 物品分類權重 :param input_file: 用戶對物品的評分文件 :return: ''' if not os.path.exists(input_file): return {} record = {} up = {} linenum = 0 score_thr = 4. topk = 2 with open(input_file, 'r') as fp: for line in fp: if linenum == 0: linenum += 1 continue item = line.strip().split(',') if len(item) < 4: continue userid, itemid, rating, timestamp = item[0], item[1], float(item[2]), int(item[3]) if rating < score_thr: continue # 如果該物品沒有分類 if itemid not in item_cate: continue # 獲取用戶對物品的時間評分 time_score = get_time_score(timestamp) if userid not in record: record.setdefault(userid, {}) # 遍歷該物品的所有分類 for fix_cate in item_cate[itemid]: if fix_cate not in record[userid]: record[userid].setdefault(fix_cate, 0) # 記錄該用戶點擊過的物品分類的總評分{用戶id:{物品分類:總評分}} # 物品分類總評分爲用戶對物品的評分*該分類的權重*時間間隔權值 record[userid][fix_cate] += rating * time_score * item_cate[itemid][fix_cate] for userid in record: if userid not in up: up.setdefault(userid, []) total_score = 0 # 對物品分類總評分進行排序,並對排前2位的物品分類總評分進行遍歷 # 這裏combine[0]是物品分類 # combine[1]是某用戶點擊過的物品分類總評分 for combine in sorted(record[userid].items(), key=lambda x: x[1], reverse=True)[:topk]: up[userid].append((combine[0], combine[1])) # 累加該用戶點擊過的所有物品分類的總評分 total_score += combine[1] for index in range(len(up[userid])): # 存儲某用戶點擊過的單個物品分類總評分佔所有物品分類總評分的比率 # {用戶id:{物品分類:該分類的評分佔比}} up[userid][index][1] = round(up[userid][index][1] / total_score, 3) return up def get_time_score(timestamp): ''' 獲取時間間隔權值,如果時間間隔越大,該權值越低 :param timestamp: 用戶對物品評分的時間戳 :return: ''' fix_timestamp = 1476086345 total_sec = 24 * 60 * 60 delta = (fix_timestamp - timestamp) / total_sec return round(1 / (1 + delta), 3)
此時再進行我們的推薦算法。在實際的項目中,基於內容的推薦實際上我們只需要將用戶刻畫存入到Redis K:V當中,將物品倒排存入到搜索引擎Elastic Search當中,這個推薦的過程實際上是在線實時的分別去請求K:V和搜索引擎獲取物品倒排的。現在我們來模擬這個過程。
import os from util.content_read import get_item_cate, get_ave_score import pprint def get_up(item_cate, input_file): ''' 用戶刻畫 獲取某用戶點擊過的某物品分類佔該用戶點擊過的所有物品分類的評分佔比 :param item_cate: 物品分類權重 :param input_file: 用戶對物品的評分文件 :return: ''' if not os.path.exists(input_file): return {} record = {} up = {} linenum = 0 score_thr = 4. topk = 2 with open(input_file, 'r') as fp: for line in fp: if linenum == 0: linenum += 1 continue item = line.strip().split(',') if len(item) < 4: continue userid, itemid, rating, timestamp = item[0], item[1], float(item[2]), int(item[3]) if rating < score_thr: continue # 如果該物品沒有分類 if itemid not in item_cate: continue # 獲取用戶對物品的時間評分 time_score = get_time_score(timestamp) if userid not in record: record.setdefault(userid, {}) # 遍歷該物品的所有分類 for fix_cate in item_cate[itemid]: if fix_cate not in record[userid]: record[userid].setdefault(fix_cate, 0) # 記錄該用戶點擊過的物品分類的總評分{用戶id:{物品分類:總評分}} # 物品分類總評分爲用戶對物品的評分*該分類的權重*時間間隔權值 record[userid][fix_cate] += rating * time_score * item_cate[itemid][fix_cate] for userid in record: if userid not in up: up.setdefault(userid, []) total_score = 0 # 對物品分類總評分進行排序,並對排前2位的物品分類總評分進行遍歷 # 這裏combine[0]是物品分類 # combine[1]是某用戶點擊過的物品分類總評分 for combine in sorted(record[userid].items(), key=lambda x: x[1], reverse=True)[:topk]: up[userid].append((combine[0], combine[1])) # 累加該用戶點擊過的所有物品分類的總評分 total_score += combine[1] for index in range(len(up[userid])): # 存儲某用戶點擊過的單個物品分類總評分佔所有物品分類總評分的比率 # {用戶id:{物品分類:該分類的評分佔比}} up[userid][index] = (up[userid][index][0], round(up[userid][index][1] / total_score, 3)) return up def get_time_score(timestamp): ''' 獲取時間間隔權值,如果時間間隔越大,該權值越低 :param timestamp: 用戶對物品評分的時間戳 :return: ''' fix_timestamp = 1476086345 total_sec = 24 * 60 * 60 delta = (fix_timestamp - timestamp) / (total_sec * 100) return round(1 / (1 + delta), 3) def recom(cate_item_sort, up, userid, topk=10): ''' 推薦結果 :param cate_item_sort: 物品倒排 :param up: 用戶刻畫 :param userid: 用戶id :param topk: :return: ''' if userid not in up: return {} recom_result = {} if userid not in recom_result: recom_result.setdefault(userid, []) for combine in up[userid]: cate = combine[0] ratio = combine[1] num = int(topk * ratio) + 1 if cate not in cate_item_sort: continue recom_list = cate_item_sort[cate][:num] recom_result[userid] += recom_list return recom_result def run_main(): ave_score = get_ave_score("../data/ratings.txt") item_cate, cate_item_sort = get_item_cate(ave_score, "../data/movies.txt") up = get_up(item_cate, "../data/ratings.txt") print(len(up)) print(up['1']) pprint.pprint(recom(cate_item_sort, up, '1')) if __name__ == "__main__": run_main()
運行結果
100
[('Drama', 0.6), ('Action', 0.4)]
{'1': [('30', 5.0),
('149', 5.0),
('156', 5.0),
('178', 5.0),
('279', 5.0),
('280', 5.0),
('290', 5.0),
('611', 5.0),
('667', 5.0),
('1224', 5.0),
('2344', 5.0),
('2826', 5.0)]}
根據結果,我們可以看到總共有100條用戶刻畫,而對於用戶1來說,它喜歡的物品類型爲Drama、權重爲0.6和Action、權重爲0.4。我們再來看一下推薦結果,我們將原始文件中的物品信息依次列出來
30,Shanghai Triad (Yao a yao yao dao waipo qiao) (1995),Crime|Drama
149,Amateur (1994),Crime|Drama|Thriller
156,Blue in the Face (1995),Comedy|Drama
178,Love & Human Remains (1993),Comedy|Drama
279,My Family (1995),Drama
280,Murder in the First (1995),Drama|Thriller
290,Once Were Warriors (1994),Crime|Drama
611,Hellraiser: Bloodline (1996),Action|Horror|Sci-Fi
667,Bloodsport 2 (a.k.a. Bloodsport II: The Next Kumite) (1996),Action
1224,Henry V (1989),Action|Drama|Romance|War
2344,Runaway Train (1985),Action|Adventure|Drama|Thriller
2826,"13th Warrior, The (1999)",Action|Adventure|Fantasy
這些物品的類別確實都屬於Drama和Action類別。
個性化召回算法總結與評估方法
這個圖是工業界多種召回算法並存的推薦系統架構,首先Match是召回,召回完了之後是Rank排序,排序完了之後是Strategy策略調整。然後將結果返回給Web層。
我們來看一下在召回階段是如何多種算法並存的,比如說這裏算法A召回了兩個Item a與b,算法B召回了三個Item a、c、d,算法C召回了四個Item e、f、d、c。每一種算法召回的Item數量是如何確定的呢?這裏有兩種形式:
- 爲了滿足Rank排序階段的性能要求,這裏指定召回階段召回的數目,比如說是50個。以往的算法根據表現來平分這50個。每一種算法指定一個比例,比如說算法A是0.2,算法B是0.3,算法C是0.5。這樣每一個算法也就有了自己召回數目的上限。
- 如果Rank階段毫無性能壓力,則在各算法中寫了多少推薦,那麼就全部召回。在召回完成之後需要進行合併。合併完成之後得到了Item a到f。將重複召回的進行去重,但是我們會標記各個Item是同時屬於哪個算法召回的。召回完成之後的Item進入排序階段。
個性化召回算法的評價
- 離線評價准入
在我們新增一組個性化召回算法的時候,我們離線選取了一部分訓練文件來訓練了我們個性化召回算法的模型,我們根據這個模型得到了一些推薦結果。同時我們有必要保留一些測試集,在測試集上評價一下推薦結果的可靠程度。這個可靠程度首先讓我們有一個預期,這個算法會給線上帶來正向或負向的收益。
- 在線評價收益
最終結果還是需要到線上生產環境去評價真實的收益。
Offline(離線)評價方法
- 評測新增算法推薦結果在測試集上的表現。
如果我們新增的某種個性化召回算法對於用戶A給出了推薦結果爲Item a、b、c。
我們獲得了該算法在測試集上的展現數據,對用戶推薦的爲Item a、b、c、m。我們發現有3個跟訓練數據集是重合的,就是a、b、c。那麼這三個Item就是分母。
如果我們得到了用戶A在測試集上的點擊物品,就爲Item a和c。那麼a、c就是分子,那麼點擊率就是2/3。如果這個數據是高於基線的點擊率的話,那麼我們便可以將這種推薦結果放到線上去進行ab測試。當然線下評價的結果和線上真實環境的結果是有差異的,但是這種方式能夠給我們一個最基礎、直觀的評判,是否可以准入到線上。
Online(在線)評價方法
- 定義指標
我們需要根據不同的場景,比如說在信息流場景下,我們最關心的就是點擊率,平均閱讀時長等等指標;在電商系統中,我們可能更加關注的是轉化率以及總的交易額度。總之我們要根據自己的產品來找到最能夠評價產品的核心指標。
- 生產環境ab測試
這裏我們往往採用以劃分userid尾號的形式,比如說分出1%的流量在原有的個性化召回體系上增加我們要實驗的個性化召回算法。試驗幾天之後,與基線去比較核心指標的優劣,如果收益是正向的,我們就保留。
學習排序綜述
- 什麼是Learn To Rank
排序是在搜索場景以及推薦場景中應用的最爲廣泛。傳統的排序方法是構造相關度函數,使相關度函數對每一個文檔進行打分。得分較高的文檔,排的位置就靠前。但是隨着相關度函數中特徵的增多,使調參變的及其困難,所以便將排序這一過程引入了機器學習的概念,也就變成了學習排序,是指的對與單獨的文檔來預估點擊率,將預估的點擊率最大的文檔排到前面。所以特徵的選擇與模型的訓練就變得至關重要。
將個性化召回的物品候選集根據物品本身的屬性結合用戶的屬性,上下文等信息給出展現優先級的過程。
假設有一個用戶A,基於他的歷史行爲給出了召回,可能是很多種召回算法經過合併之後的得到了6個Item。然後經過排序將這6個Item的優先級固定爲c、a、f、d、b、e。這個得到優先級的過程就是由排序得到的,分別根據了Item本身的屬性以及用戶當前的一些上下文和用戶固定的一些屬性得到最佳的順序,以保證點擊率最高。
- 排序在個性化推薦系統中的重要作用
在個性化推薦系統中,後端的主要流程是召回、排序和策略調整。召回決定了推薦效果的天花板,排序就是決定了逼近這個天花板的程度。排序決定了最終的推薦效果。
由於推薦算法後端的主流程是召回、排序、策略調整。策略調整部分是幾乎不會改變最終展現給用戶的物品的順序的,用戶看到的順序幾乎是由排序這一部分來決定的,如果用戶在首屏的前面的位置就能夠看到自己想要的物品,用戶就會在我們的推薦系統中停留的時間更長。反之,如果需要用戶幾次刷新之後才能得到自己想要的物品,那麼用戶下一次必然不會再信任我們的推薦效果。必然在系統的停留時長也不會很長。
在工業界,排序又分爲三個步驟:1、PreRank,排序之前的部分。由於排序的模型有淺層模型切換到深層模型的時候,耗時在不停的增加,比如之前召回可以允許有5000個物品去過淺層模型,比如說邏輯迴歸。我們就是訓練出了一組參數,這個整體的打分過程耗時很短。但是當我們的排序模型切換到了深層模型,比如說DNN,整體需要請求一次深度學習的服務,那麼這5000個Item去請求的時間顯然是我們不能承受的,所以我們先有一個粗排。這個粗排會將這5000個召回的物品進行第一次排序,將候選集縮小到一定範圍之內,這樣使得排序模型的總處理時間滿足系統的性能要求。粗排往往以一些簡單的規則爲主,比如說使用後驗CTR(點擊通過率),或者說對於新的物品,在入庫時的預估CTR等等。2、Rank,主排序部分。現在業界比較流行的還有一次重排ReRank,這個重排是將主排序結果再放入一個強化學習的模型裏面去進行一個重排序,這種主要突出的用戶最近幾次行爲的一些特徵,將與最近幾次用戶行爲相近的Item給優先的展示。以便獲取用戶行爲的延續性。由於單一Item在重排模型的耗時要比主模型的耗時要長很多,所以重排部分只是會影響主排序產生的頭部的一些結果,比如說Top 50的結果進行一個重排。最能影響結果的還是主排序模型。
Rank解析
單一的淺層模型,在學習排序初期是非常受歡迎的,因爲模型線上處理時間較短,所以它支持特徵的維度就會非常的高。但是也存在很多的問題,比如像邏輯迴歸模型,它需要研發者具有很強的樣本篩選以及特徵處理能力。這裏包含了像特徵的歸一化,離散化,特徵的組合等等。所以後期發展到了淺層模型的組合,比如像樹模型gbdt,包括LR與gbdt的組合,這一類模型不需要特徵的歸一化,離散化,能夠較強的發現特徵之間的規律。所以相較於單一的淺層模型,是有一定的優勢的。隨着深度學習在工業界應用的不斷成熟,以及像tensorflow深度學習框架的開源,現在大部分工業界的主排序模型都已經切換到了深度學習模型。
- 工業界推薦系統中排序架構解析
算法後端的主流程是召回之後排序,召回完了將Item集合傳給排序部分,排序部分會調用打分框架,得到每一個Item在當前的上下文下對當前用戶的一個得分。進而我們根據得分決定展現順序。在打分框架內部,首先會讓每一個Item以及用戶去提取特徵,這裏提取的特徵要與離線訓練的模型特徵保持一致。提取完特徵之後向排序服務發出請求,排序服務會返回給我們一個得分,推薦引擎基於此得分完成排序。經過簡單的策略調整之後,展現給用戶。這裏需要特別注意的是排序服務與離線訓練好的排序模型之間的通信。如果是單一的淺層模型,像LR,我們可以直接將訓練好的模型參數存入內存,當排序服務需要對外提供服務的時候,直接加載內存中的模型參數即可。像FM,gbdt等等,我們只需要離線訓練好模型,將模型實例化到硬盤當中,在在線服務當中,由於這些模型都有相應的庫函數,它們提供模型的加載和模型對外預測等一系列的接口,我們便可以完成打分。但是對於像深度學習模型,我們在訓練完了模型之後,還需要提供一個深度學習的服務供排序服務調用。