傳統與深度推薦系統重點技術演進圖
目標:在海量的信息中確定提供給用戶的具體內容,滿足用戶的個性化需求。
術語:
U2I:基於用戶歷史行為、給用戶直接推薦item
I2I:挖掘item之間的關聯信息。
U2U2I:給相似用戶群體推薦物品,基於用戶的協同過濾。
U2I2I:給用戶推薦相似的物品,基於item的協同過濾。
U2Tag2I:算出用戶tag偏好向量[科幻,恐怖,動作,...],匹配item列表。泛化性好。
地位:最早使用的推薦算法、年代久遠,但仍被廣泛使用,效果好。
定義:給用戶X推薦 之前喜歡過的物品 推薦 相似的物品。即U2I, 為瞭泛化的目的將中間的I進行聚合,得到Tag標簽,轉變為U2Tag2I問題。
如何實現一個基於內容的推薦系統,整體步驟:
餘弦相似度算法:
優點:
缺點:
核心:利用集體經驗推薦。
基於用戶的協同過濾U2U2I:給相似用戶群體推薦物品
ebcae072977f40a902e0d0d399081e78
基於物品的協同過濾U2I2I:給用戶推薦相似的物品(通過其他用戶發現)
基於用戶的協同過濾舉例,步驟
用戶相似度如下:
相似度問題:
已經得到瞭A和BC最相似,再計算用戶和新item的相似度:
技術挑戰:怎樣從海量的內容中,挑選出用戶感興趣的條目,並且能夠滿足系統50ms ~300ms的低延遲要求?
推薦系統通過對用戶興趣和內容進行建模、預測,最終實現用戶與內容的精準匹配。內容理解是建模的基礎,也是推薦系統的基石。
推薦內容平臺,涵蓋瞭圖文、圖集、短視頻、問答、直播、專題等各種內容形態,所有這些內容都可以歸結為文本、圖像和視頻三種數據形式。
內容理解針對文本、圖像和視頻三個大的方面進行內容分析和轉換,即將非結構化的內容(比如圖像、文本等)進行結構化的表示。
在自然語言處理中,使用詞嵌入(Embedding)的方式完成將數據映射為向量的變換,NLP任務中,一般把輸入文本映射成向量表示,以便神經網絡的處理。
在推薦系統領域,可定義為兩個大維度:通過內容本身來理解內容,通過行為反饋來理解內容。
前者:主要針對內容抽取靜態屬性標簽。
後者:通過行為積累的後驗數據、統計、或模型預估內容的知識、傾向性、投放目標以及抽象表達。
涉及到的子領域非常多,如下:
文本理解
視頻理解:即視頻的行為識別,常見算法:雙流網絡 / I3D等。
PaddleRec/models/contentunderstanding at master · PaddlePaddle/PaddleRec (github.com)
Tips:
目標:根據用戶的興趣和歷史行為,從千萬量級的候選物品,降低候選集規模,快速生成候選物品,然後交給排序環節。
特點:考慮到候選物品數量極大,所以要想速度快,就隻能使用簡單模型,使用少量特征,去保證泛化能力,盡量讓用戶感興趣的物品在這個階段能夠初步篩選出來;
推薦系統的召回階段是很關鍵的一個環節, 這個環節偏向策略型導向。目前工業界,標準召回結構一般是多路召回,即每一路召回采取一個不同的策略。
可以看出,上圖分成兩大類召回路:
上述圖片為單特征召回,如何進行將其多路融合?
以上效果依次變好,按照成本進行選擇
LR缺乏特征交叉例子1:
c53f36bc472b0167b72f593b3e5f9d40
LR是“線性模型+人工特征組合引入非線性”的模式。
如何解決表達能力受限的問題與挖掘隱藏特征呢?因此衍生出Ploy, FM, FFM等之類改進方法,用於解決特征交叉問題,以及發掘隱藏特征。
什麼是特征交叉?
如何發掘隱藏特征?矩陣分解MF、引入隱向量。
舉例,使用隱向量給每個用戶與每首音樂打上標簽,將[3, 3]的矩陣 分解為 [3, 4] * [4, 3]的矩陣。
泛化:
註意的是:
好處在於:
召回階段的技術趨勢總體而言:一切Embedding化以及有監督模型化,這是兩個相輔相成的總體發展趨勢。
embedding核心思想:將用戶特征和物品特征分離,各自通過某個具體的模型,分別打出用戶Embedding以及物品Embedding。embedding的具體方法有各種選擇,為不同的給用戶和物品打embedding的不同方法而已。
目標:排序環節融入更多特征,使用復雜模型,來精準地做根據用戶特征/物品特征/上下文特征對物品進行精準排名。
粗排:當每個用戶召回環節返回的物品數量還是太多,怕排序環節速度跟不上,所以可以在召回和精排之間加入一個粗排環節,通過少量用戶和物品特征,簡單模型,來對召回的結果進行個粗略的排序,粗排往往是可選的,可用可不同,跟場景有關。
精排:使用你能想到的任何特征,可以上你能承受速度極限的復雜模型,盡量精準地對物品進行個性化排序。
特點:
深度學習的排序模型目前看大的范式還是embedding + MLP,還沒有主流的精排模型能超越這一范式。
具體來說,特征主要有統計特征,id特征,用戶行為序列特征。對於這些特征不同的建模方式可以演化出deepFM, xDeepFM,DIN,autointent等等。
現狀:圖像領域裡有Resnet時刻,NLP裡面有Bert時刻,推薦領域暫時還停留在embedding + MLP階段。
(12 封私信 / 26 條消息) 目前工業界常用的推薦系統模型有哪些? - 知乎 (icpchaxun.com)
d71aae8dfa7d004eb5d9422bb6c18c39
目標:利用各種技術及業務策略,比如去已讀、去重、打散、多樣性保證、固定類型物品插入等等,以技術產品策略主導或者改進推薦體驗的。
註意:精排推薦結果出來瞭,一般並不會直接展示給用戶,可能還要上一些業務策略,比如去已讀,推薦多樣化,加入廣告等各種業務策略。之後形成最終推薦結果,將結果展示給用戶。
以排序模型(rank)文件夾下的dnn模型為例:
├── data #樣例數據 ├── sample_data #樣例數據 ├── train ├── sample_train.txt #訓練數據樣例├── __init__.py├── README.md #文檔├── config.yaml # sample數據配置├── config_bigdata.yaml # 全量數據配置├── net.py # 模型核心組網(動靜統一)├── criteo_reader.py #數據讀取程序# 為模型使用靜態圖的方式運行時的執行過程。其中定義瞭模型的具體運行方式:數據輸入方式,配置優化器、學習率、模型訓練和預測時的前向計算,損失的計算方式等。├── static_model.py # 構建靜態圖# 為模型使用動態圖的方式運行時的執行過程。定義內容同上├── dygraph_model.py # 構建動態圖
目標:1. 將數據集劃分為訓練集和測試集。2. 然後進行數據處理,生成可以直接輸入模型的數據。3. 並讀入模型。
該數據集包含三個數據文件,分別是:
根據ratings.dat中的評分時間(time),選取每個用戶最新的一條數據作為測試集,剩餘數據作為訓練集。
對於ratings.dat文件中每一條數據,我們根據userid和movieid去users.dat和movies.dat裡找到相應的用戶特征和電影特征,經過hash處理後拼接成一條可真實用於訓練或預估的數據。
拼接後數據格式如下所示,第一個數字是logid即數據的ID,各個數據間的分隔符為空格。
logid:10000004 time:971979246 userid:313460 gender:113660 age:144970 occupation:209266 movieid:466203 title:504 title:66476 title:0 title:0 genres:249254 genres:332172 genres:0 label:3
在每條數據中,電影的電影名title和電影風格genres可能需要拆分處理成多個詞,形成多組slot:feasign樣式的數據。
Slot:Feasign 是什麼? 推薦工程中,是指某一個寬泛的特征類別,比如用戶ID、性別、年齡就是Slot,Feasign則是具體值,比如:12345,男,20歲。 實踐過程中,很多特征槽位不是單一屬性,或無法量化並且離散稀疏的,比如某用戶興趣愛好有三個:遊戲/足球/數碼,且每個具體興趣又有多個特征維度,則在興趣愛好這個Slot興趣槽位中,就會有多個Feasign值。
PaddleRec在讀取數據時,每個Slot ID對應的特征,支持稀疏,且支持變長,可以非常靈活的支持各種場景的推薦模型訓練。
PaddleRec支持您使用自定義的格式進行輸入,根據需要自己定義數據讀取的邏輯,對以行為單位的數據進行截取,轉換等預處理,返回一個可以迭代的reader方法。
數據的輸出順序與我們在網絡中創建的inputs必須是嚴格一一對應的,並以np.array的形式輸入網絡中。
# 自定義的Reader中,您需要引入IterableDataset基類,創建一個名為RecDataset子類,繼承IterableDataset的基類。繼承並實現基類中的__iter__(self)函數,逐行讀取數據。from __future__ import print_functionimport numpy as np#引入IterableDataset基類from http://paddle.io import IterableDataset #創建一個子類,繼承IterableDataset的基類class RecDataset(IterableDataset): def __init__(self, file_list, config): super(RecDataset, self).__init__() self.file_list = file_list def __iter__(self): full_lines = [] self.data = [] for file in self.file_list: with open(file, "r") as rf: # 以行為單位,逐行讀取數據 for l in rf: output_list = [] line = l.strip().split(" ") sparse_slots = ["logid", "time", "userid", "gender", "age", "occupation", "movieid", "title", "genres", "label"] #logid和time這兩個特征,訓練模型時並不需要用到,故不必加入output_list logid = line[0].strip().split(":")[1] time = line[1].strip().split(":")[1] #向output_list中加入用戶特征:userid:1個數,gender:1個數,age:1個數,occupation:1個數 userid = line[2].strip().split(":")[1] output_list.append(np.array([float(userid)])) gender = line[3].strip().split(":")[1] output_list.append(np.array([float(gender)])) age = line[4].strip().split(":")[1] output_list.append(np.array([float(age)])) occupation = line[5].strip().split(":")[1] output_list.append(np.array([float(occupation)])) #向output_list中加入電影特征:movieid:1個數,title:4個數,genres:3個數 movieid = line[6].strip().split(":")[1] output_list.append(np.array([float(movieid)])) title = [] genres = [] for i in line: if i.strip().split(":")[0] == "title": title.append(float(i.strip().split(":")[1])) if i.strip().split(":")[0] == "genres": genres.append(float(i.strip().split(":")[1])) output_list.append(np.array(title)) output_list.append(np.array(genres)) #向output_list中加入標簽:label:1個數 label = line[-1].strip().split(":")[1] output_list.append(np.array([float(label)])) #返回一個可以迭代的reader方法 yield output_list
至此,我們已經將用戶和電影的特征用數字表示。
召回的目的是從大量電影庫中降低候選集規模,選出部分候選,輸入給排序模塊。召回通常采用簡單的模型,如ItemCF、UserCF、DSSM等。
本例采用DSSM模型的簡化版,即普通的雙塔模型。
所謂“雙塔”模型,是經典的DNN模型,從特征OneHot到特征Embedding,再經過幾層MLP隱層,兩個塔分別輸出用戶Embedding和Item Embedding編碼。訓練過程中,User Embedding和Item Embedding做Cosine相似度計算,使得用戶和正例Item在Embedding空間更接近,和負例Item在Embedding空間距離拉遠。損失函數則可用標準交叉熵損失,將問題當作一個分類問題。
現狀:對於絕大多數應用來說,雙塔模型的速度是足夠快瞭,但模型精度還有待提升。因此衍生出瞭,微軟雙塔DSSM,YouTube雙塔,Facebook雙塔, SENet雙塔模型。
Paddle Rec召回網絡結構:
class DNNLayer(nn.Layer): # 在使用動態圖時,針對一些比較復雜的網絡結構,可以使用Layer子類定義的方式來進行模型代碼編寫,即在__init__構造函數中進行組網Layer的聲明, # 在forward中使用聲明的Layer變量進行前向計算。子類組網方式也可以實現sublayer的復用,針對相同的layer可以在構造函數中一次性定義,在forward中多次調用。 def __init__(self, sparse_feature_number, sparse_feature_dim, fc_sizes): super(DNNLayer, self).__init__() self.sparse_feature_number = sparse_feature_number self.sparse_feature_dim = sparse_feature_dim self.fc_sizes = fc_sizes #聲明embedding層,建立emb表將數據映射為向量 self.embedding = paddle.nn.Embedding( self.sparse_feature_number, self.sparse_feature_dim, padding_idx=0, sparse=True, weight_attr=paddle.ParamAttr( name="SparseFeatFactors", initializer=paddle.nn.initializer.Uniform())) #使用循環的方式創建全連接層,可以在超參數中通過一個數組確定使用幾個全連接層以及每個全連接層的神經元數量。 #本例中使用瞭4個全連接層,並在每個全連接層後增加瞭relu激活層。 user_sizes = [36] + self.fc_sizes acts = ["relu" for _ in range(len(self.fc_sizes))] self._user_layers = [] for i in range(len(self.fc_sizes)): linear = paddle.nn.Linear( in_features=user_sizes[i], out_features=user_sizes[i + 1], weight_attr=paddle.ParamAttr( initializer=paddle.nn.initializer.Normal( std=1.0 / math.sqrt(user_sizes[i])))) self.add_sublayer('linear_user_%d' % i, linear) self._user_layers.append(linear) if acts[i] == 'relu': act = paddle.nn.ReLU() self.add_sublayer('user_act_%d' % i, act) self._user_layers.append(act) # 電影特征和用戶特征使用瞭不同的全連接層,不共享參數 movie_sizes = [27] + self.fc_sizes acts = ["relu" for _ in range(len(self.fc_sizes))] self._movie_layers = [] for i in range(len(self.fc_sizes)): linear = paddle.nn.Linear( in_features=movie_sizes[i], out_features=movie_sizes[i + 1], weight_attr=paddle.ParamAttr( initializer=paddle.nn.initializer.Normal( std=1.0 / math.sqrt(movie_sizes[i])))) self.add_sublayer('linear_movie_%d' % i, linear) self. _movie_layers.append(linear) if acts[i] == 'relu': act = paddle.nn.ReLU() self.add_sublayer('movie_act_%d' % i, act) self._movie_layers.append(act) def forward(self, batch_size, user_sparse_inputs, mov_sparse_inputs, label_input): # 對用戶特征建模, 所有用戶sparse特征查對應的emb表,獲得特征權重 user_sparse_embed_seq = [] for s_input in user_sparse_inputs: emb = self.embedding(s_input) emb = paddle.reshape(emb, shape=[-1, self.sparse_feature_dim]) user_sparse_embed_seq.append(emb) # 對電影特征建模, 所有電影sparse特征查對應的emb表,獲得特征權重 mov_sparse_embed_seq = [] for s_input in mov_sparse_inputs: s_input = paddle.reshape(s_input, shape=[batch_size, -1]) emb = self.embedding(s_input) emb = paddle.sum(emb, axis=1) emb = paddle.reshape(emb, shape=[-1, self.sparse_feature_dim]) mov_sparse_embed_seq.append(emb) # 查表結果拼接在一起,構成用戶特征權重向量 user_features = paddle.concat(user_sparse_embed_seq, axis=1) # 查表結果拼接在一起,構成電影特征權重向量 mov_features = paddle.concat(mov_sparse_embed_seq, axis=1) # 通過4層全鏈接層,獲得用於計算相似度的用戶特征和電影特征 for n_layer in self._user_layers: user_features = n_layer(user_features) for n_layer in self._movie_layers: mov_features = n_layer(mov_features) # 使用餘弦相似度算子,計算用戶和電影的相似程度 sim = F.cosine_similarity( user_features, mov_features, axis=1).reshape([-1, 1]) # 對輸入Tensor進行縮放和偏置,獲得合適的輸出指標 predict = paddle.scale(sim, scale=5) return predict
排序的目的是引入更多特征,進行更加精細化的預估。一般特點是模型較復雜,候選輸入集比較少。
此處采用DNN模型,模型組網如下圖所示。
相較於上述召回模型的雙塔結構,排序模型更早地完成瞭特征交叉,然後進一步利用DNN網絡進行訓練,旨在對候選集合進行更加精確的打分。
class DNNLayer(nn.Layer): def __init__(self, sparse_feature_number, sparse_feature_dim, fc_sizes): super(DNNLayer, self).__init__() self.sparse_feature_number = sparse_feature_number self.sparse_feature_dim = sparse_feature_dim self.fc_sizes = fc_sizes # 聲明embedding層,建立emb表將數據映射為向量 self.embedding = paddle.nn.Embedding( self.sparse_feature_number, self.sparse_feature_dim, padding_idx=0, sparse=True, weight_attr=paddle.ParamAttr( name="SparseFeatFactors", initializer=paddle.nn.initializer.Uniform())) # 使用循環的方式創建全連接層,可以在超參數中通過一個數組確定使用幾個全連接層以及每個全連接層的神經元數量。 # 本例中使用瞭4個全連接層,並在每個全連接層後增加瞭relu激活層。 sizes = [63] + self.fc_sizes + [1] acts = ["relu" for _ in range(len(self.fc_sizes))] + ["sigmoid"] self._layers = [] for i in range(len(self.fc_sizes) + 1): linear = paddle.nn.Linear( in_features=sizes[i], out_features=sizes[i + 1], weight_attr=paddle.ParamAttr( initializer=paddle.nn.initializer.Normal( std=1.0 / math.sqrt(sizes[i])))) self.add_sublayer('linear_%d' % i, linear) self._layers.append(linear) if acts[i] == 'relu': act = paddle.nn.ReLU() self.add_sublayer('act_%d' % i, act) self._layers.append(act) if acts[i] == 'sigmoid': act = paddle.nn.layer.Sigmoid() self.add_sublayer('act_%d' % i, act) self._layers.append(act) def forward(self, batch_size, user_sparse_inputs, mov_sparse_inputs, label_input): # 對用戶特征建模, 所有用戶sparse特征查對應的emb表,獲得特征權重 user_sparse_embed_seq = [] for s_input in user_sparse_inputs: emb = self.embedding(s_input) emb = paddle.reshape(emb, shape=[-1, self.sparse_feature_dim]) user_sparse_embed_seq.append(emb) # 對電影特征建模, 所有電影sparse特征查對應的emb表,獲得特征權重 mov_sparse_embed_seq = [] for s_input in mov_sparse_inputs: s_input = paddle.reshape(s_input, shape=[batch_size, -1]) emb = self.embedding(s_input) emb = paddle.sum(emb, axis=1) emb = paddle.reshape(emb, shape=[-1, self.sparse_feature_dim]) mov_sparse_embed_seq.append(emb) # 查表結果拼接在一起,混合用戶特征和電影特征,相比召回模型,排序模型更早地完成瞭特征交叉 features = paddle.concat( user_sparse_embed_seq + mov_sparse_embed_seq, axis=1) # 利用DNN網絡進行訓練,使用sigmoid激活的全鏈接層,旨在對候選集合進行更加精確的打分 for n_layer in self._layers: features = n_layer(features) # 對輸入Tensor進行縮放和偏置,獲得合適的輸出指標 predict = paddle.scale(features, scale=5) return predict
本PaddleRec demo系統一共啟動瞭5個在線服務,分別是用戶模型服務,內容模型服務,召回服務,排序服務,還有應用服務
用戶模型和內容模型分別是數據集當中的users.dat和movies.dat經過解析之後保存在redis當中,用戶模型以user_id作為key,內容模型以movie_id作為key。
召回服務目前分為兩個階段:
排序服務是用PaddleRec訓練好的CTR模型,用Paddle Serving啟動來提供預測服務能力,傳入一個用戶信息和一組內容信息,接下來就能經過特征抽取和排序計算,求得最終的打分,按從高到低排序返回給用戶。
整體應用服務流程
設計的流程是用戶傳入自己的用戶信息(性別,年齡,工作),從召回服務中得到用戶向量,近似查詢movie列表,接下來查詢到所有的內容模型信息,最終電影信息和用戶信息兩個結合在排序服務中得到所有候選電影的從高到低的打分,最終還原成原始的電影信息返回給用戶。
經過召回、正排(查電影)、排序rank之後,根據分數降序,從大到小,把最適合該用戶的電影信息返回回來,結果中是由大到小排序的電影信息,每個電影信息包含瞭電影的id,電影名和影片類型。
error { code: 200}item_infos { movie_id: "2670" title: "Run Silent, Run Deep (1958)" genre: "War"}item_infos { movie_id: "3441" title: "Red Dawn (1984)" genre: "Action, War"}item_infos { movie_id: "71" title: "Fair Game (1995)" genre: "Action"}item_infos { movie_id: "3066" title: "Tora! Tora! Tora! (1970)" genre: "War"}item_infos { movie_id: "3449" title: "Good Mother, The (1988)" genre: "Drama"}