導讀:隨著電子商務規模的不斷擴大,電商平臺的商品數量和種類呈爆發式增長,使用者往往需要花費大量的時間才能找到自己想買的商品,這就是資訊超載問題。為了解決這個難題,“個性化推薦”技術應運而生,有效地節約使用者時間,提升電商成單率。本篇文章中,將為大家介紹個性化推薦系統的實現方法,並送上一份基於飛槳(PaddlePaddle)實現個性化推薦的程式碼教程。
1. 個性化推薦概述
日常生活中,當你開啟某電商購物APP後,可能會遇到以下情形:
為什麼我和好友是同時開啟的APP,但兩個人的首頁推薦商品不一致?
為什麼同樣是搜尋“T恤”,我和好友竟然出現了不一樣的商品列表?
為什麼在我瀏覽了某品牌的運動鞋之後,“猜你喜歡”模組的商品列表變了?
為什麼對比好友的APP介面,我更喜歡我自己介面中的商品呢?
這就是個性化推薦在電商應用的例子。
個性化推薦,是指通過分析、挖掘使用者行為,發現使用者的個性化需求與興趣特點,將使用者可能感興趣的資訊或商品推薦給使用者。
與搜尋引擎不同,個性化推薦系統不需要使用者準確地描述出自己的需求,而是根據使用者的歷史行為進行建模,主動提供滿足使用者興趣和需求的資訊。
個性化推薦幾乎涵蓋了電商系統、社交網路、廣告推薦、搜尋引擎等應用的方方面面。
個性化推薦技術的發展,可以分為傳統推薦方法和深度學習推薦方法兩個階段。
傳統推薦方法
1994年明尼蘇達大學推出了GroupLens系統,這被認為是個性化推薦系統成為一個相對獨立的研究方向的標誌。GroupLens系統首次提出了基於協同過濾來完成推薦任務的思想,此後,基於該模型的協同過濾推薦演算法引領了個性化推薦系統十幾年的發展方向。
傳統的個性化推薦系統方法主要有:
協同過濾推薦(Collaborative Filtering Recommendation):該方法是應用最廣泛的技術之一,需要收集和分析使用者的歷史行為、活動和偏好。它通常可以分為兩個子類:基於使用者(User-Based)的推薦和基於物品(Item-Based)的推薦。該方法的一個關鍵優勢是它不依賴於機器去分析物品的內容特徵,因此它無需理解物品本身也能夠準確地推薦諸如電影之類的複雜物品;缺點是對於沒有任何行為的新使用者存在冷啟動的問題,同時也存在使用者與商品之間的互動資料不夠多造成的稀疏問題。值得一提的是,社交網路或地理位置等上下文資訊都可以結合到協同過濾中去。
基於內容過濾推薦(Content-based Filtering Recommendation):該方法利用商品的內容描述,抽象出有意義的特徵,通過計算使用者的興趣和商品描述之間的相似度,來給使用者做推薦。優點是簡單直接,不需要依據其他使用者對商品的評價,而是通過商品屬性進行商品相似度度量,從而推薦給使用者所感興趣商品的相似商品;缺點是對於沒有任何行為的新使用者同樣存在冷啟動的問題。
組合推薦(Hybrid Recommendation):運用不同的輸入和技術共同進行推薦,以彌補各自推薦技術的缺點。
深度學習推薦方法
近些年來,深度學習在很多領域都取得了巨大的成功。學術界和工業界都在嘗試將深度學習應用於個性化推薦系統領域中。深度學習具有優秀的自動提取特徵的能力,能夠學習多層次的抽象特徵表示,並對異質或跨域的內容資訊進行學習,可以一定程度上處理個性化推薦系統冷啟動問題。
下面就為大家介紹個性化推薦的深度學習模型,以及如何使用PaddlePaddle實現該模型。
2. 效果展示
在正式開始之前,我們先看一下模型的最終效果:
我們使用包含使用者資訊、電影資訊與電影評分的資料集作為個性化推薦的應用場景。當訓練好模型後,只需要輸入對應的使用者ID和電影ID,就可以得出一個匹配的分數(範圍[0,5],分數越高視為興趣越大),然後根據所有電影的推薦得分排序,取得分最高的k個推薦給使用者。
Input movie_id: 1962
Input user_id: 1
Prediction Score is 4.25
3. 模型概覽
我們將首先介紹YouTube的視訊個性化推薦系統,然後再介紹融合推薦模型。
3.1.YouTube的深度神經網路個性化推薦系統
YouTube是世界上最大的視訊上傳、分享和發現網站,YouTube個性化推薦系統為超過10億使用者從不斷增長的視訊庫中推薦個性化的內容。整個系統由兩個神經網路組成:候選生成網路和排序網路。
候選生成網路從百萬量級的視訊庫中生成上百個候選。
排序網路對候選進行打分排序,輸出排名最高的數十個結果。
系統的整體結構如下:
3.1.1. 候選生成網路(Candidate Generation Network)
候選生成網路將推薦問題建模為一個類別數極大的多類分類問題:對於一個Youtube使用者,使用其觀看歷史(視訊ID)、搜尋詞記錄(searchtokens)、人口學資訊(如地理位置、使用者登入裝置)、二值特徵(如性別,是否登入)和連續特徵(如使用者年齡)等,對視訊庫中所有視訊進行多分類,得到每一類別的分類結果(即每一個視訊的推薦概率),最終輸出概率較高的幾百個視訊。
首先,將觀看歷史及搜尋詞記錄這類歷史資訊,對映為向量後取平均值得到定長表示;同時,輸入人口學特徵以優化新使用者的推薦效果,並將二值特徵和連續特徵歸一化處理到[0, 1]範圍。
接下來,將所有特徵表示拼接為一個向量,並輸入給非線形多層感知器(MLP)處理。
最後,訓練時將MLP的輸出給softmax做分類,預測時計算使用者的綜合特徵(MLP的輸出)與所有視訊的相似度,取得分最高的k個作為候選生成網路的篩選結果。
下圖是候選生成網路的結構:
對於一個使用者U,預測此刻使用者要觀看的視訊w為視訊i的概率公式為:
其中u為使用者U的特徵表示,V為視訊庫集合, 為視訊庫中第i個視訊的特徵表示。u和 為長度相等的向量,兩者點積可以通過全連線層實現。
考慮到softmax分類的類別數非常多,為了保證一定的計算效率,我們採取如下方式:
訓練階段,使用負樣本類別取樣將實際計算的類別數縮小至數千。
推薦(預測)階段,忽略softmax的歸一化計算(不影響結果),將類別打分問題簡化為點積(dot product)空間中的最近鄰(nearest neighbor)搜尋問題,取與u最近的k個視訊作為生成的候選。
3.1.2. 排序網路(Ranking Network)
排序網路的結構與候選生成網路類似,但是它的目標是對候選進行更細緻的打分排序。
和傳統廣告排序中的特徵抽取方法類似,這裡也構造了大量的用於視訊排序的相關特徵(如視訊ID、上次觀看時間等)。這些特徵的處理方式和候選生成網路類似,不同之處是排序網路的頂部是一個加權邏輯迴歸(weighted logisticregression),它對所有候選視訊進行打分,從高到底排序後將分數較高的一些視訊返回給使用者。
3.2.融合推薦模型
下面依次為大家介紹文字卷積神經網路和融合推薦模型。
3.2.1. 卷積神經網路(CNN)
卷積神經網路經常用來處理具有類似網格拓撲結構(grid-like topology)的資料。例如,影像可以視為二維網格的畫素點,自然語言可以視為一維的詞序列。卷積神經網路可以提取多種區域性特徵,並對其進行組合抽象得到更高階的特徵表示。實驗表明,卷積神經網路能高效地對影像及文字問題進行建模處理。
卷積神經網路主要由卷積(convolution)和池化(pooling)操作構成,其應用及組合方式靈活多變,種類繁多。
我們將重點講解下圖所示的卷積神經網路文字分類模型:
假設待處理句子的長度為n,其中第i個詞的詞向量為,k為維度大小。
首先,進行詞向量的拼接操作:將每h個詞拼接起來形成一個大小為h的詞視窗,記為,它表示詞序列xi,xi+1,…,xi:xi+h的拼接,其中,i表示詞視窗中第一個詞在整個句子中的位置,取值範圍從1到n-h+1,。
其次,進行卷積操作:把卷積核(kernel)應用於包含h各詞的視窗,得到特徵,其中為偏置項(bias),f為非線性啟用函式,如sigmoid。卷積核應用於句子中所有的詞視窗,產生一個特徵圖(feature map):
接下來,對特徵圖採用時間維度上的最大池化(max pooling over time)操作得到此卷積核對應的整句話的特徵,它是特徵圖中所有元素的最大值:
3.2.2. 融合推薦模型
在融合推薦模型的電影個性化推薦系統中,主要分為以下步驟:
[1] 首先,使用使用者特徵和電影特徵作為神經網路的輸入,其中:
使用者特徵融合了四個屬性資訊,分別是使用者ID、性別、職業和年齡。
電影特徵融合了三個屬性資訊,分別是電影ID、電影型別ID和電影名稱。
[2] 對使用者特徵,將使用者ID對映為維度大小為256的向量表示,輸入全連線層,並對其他三個屬性也做類似的處理。然後將四個屬性的特徵表示分別全連線並相加。
[3] 對電影特徵,將電影ID以類似使用者ID的方式進行處理,電影型別ID以向量的形式直接輸入全連線層,電影名稱用文字卷積神經網路得到其定長向量表示。然後將三個屬性的特徵表示分別全連線並相加。
[4] 得到使用者和電影的向量表示後,計算二者的餘弦相似度作為個性化推薦系統的打分。最後,用該相似度打分和使用者真實打分的差異的平方作為該回歸模型的損失函式。
融合推薦模型的主要架構如下:
4. 飛槳實戰
下面送上基於飛槳(PaddlePaddle)實現個性化推薦的程式碼教程。
本教程的原始碼和ipynb檔案位於book/recommender_system,初次使用請先參考Book文件使用說明。
4.1.資料準備
我們以MovieLens 百萬資料集(ml-1m)為例進行介紹。ml-1m 資料集包含了6,000位使用者對 4,000部電影的1,000,000條評價(評分範圍 1~5 分,均為整數),由 GroupLens Research 實驗室蒐集整理。
Paddle在API中提供了自動載入資料的模組。資料模組為 paddle.dataset.movielens
import paddle
movie_info = paddle.dataset.movielens.movie_info()
print movie_info.values()[0]
# Run this block to showdataset's documentation
# help(paddle.dataset.movielens)
在原始資料中包含電影的特徵資料,使用者的特徵資料,和使用者對電影的評分。
例如,其中某一個電影特徵為:
movie_info = paddle.dataset.movielens.movie_info()
print movie_info.values()[0]
<MovieInfo id(1), title(Toy Story ),categories(['Animation', "Children's", 'Comedy'])>
這表示,電影的id是1,標題是《Toy Story》,該電影被分為到三個類別中。這三個類別是動畫,兒童,喜劇。
user_info = paddle.dataset.movielens.user_info()
print user_info.values()[0]
<UserInfo id(1), gender(F), age(1), job(10)>
這表示,該使用者ID是1,女性,年齡比18歲還年輕。職業ID是10。
其中,年齡使用下列分佈:
"Under 18"
"18-24"
"25-34"
"35-44"
"45-49"
"50-55"
"56+"
職業是從下面幾種選項裡面選擇得出:
"other" or not specified
"academic/educator"
"artist"
"clerical/admin"
"college/grad student"
"customer service"
"doctor/health care"
"executive/managerial"
"farmer"
"homemaker"
"K-12 student"
"lawyer"
"programmer"
"retired"
"sales/marketing"
"scientist"
"self-employed"
"technician/engineer"
"tradesman/craftsman"
"unemployed"
"writer"
而對於每一條訓練/測試資料,均為 <使用者特徵>+ <電影特徵> + 評分。
例如,我們獲得第一條訓練資料:
train_set_creator = paddle.dataset.movielens.train()
train_sample = next(train_set_creator())
uid = train_sample[0]
mov_id = train_sample[len(user_info[uid].value())]
print "User %s rates Movie %swith Score %s"%(user_info[uid],movie_info[mov_id], train_sample[-1])
User <UserInfo id(1), gender(F), age(1), job(10)> rates Movie<MovieInfo id(1193), title(One Flew Over the Cuckoo's Nest ), categories(['Drama'])> with Score [5.0]
即使用者1對電影1193的評價為5分。
4.2.模型配置說明
下面我們開始根據輸入資料的形式配置模型。首先引入所需的庫函式以及定義全域性變數。
IS_SPARSE: embedding中是否使用稀疏更新
PASS_NUM: epoch數量
from __future__ import print_function
import math
import sys
import numpy as np
import paddle
import paddle.fluid as fluid
import paddle.fluid.layers as layers
import paddle.fluid.nets as nets
IS_SPARSE = True
BATCH_SIZE = 256
PASS_NUM = 20
然後為我們的使用者特徵綜合模型定義模型配置:
def get_usr_combined_features():
""" network definition for user part """
USR_DICT_SIZE =paddle.dataset.movielens.max_user_id() + 1
uid = layers.data(name='user_id', shape=[1], dtype='int64')
usr_emb = layers.embedding(
input=uid,
dtype='float32',
size=[USR_DICT_SIZE, 32],
param_attr='user_table',
is_sparse=IS_SPARSE)
usr_fc = layers.fc(input=usr_emb, size=32)
USR_GENDER_DICT_SIZE = 2
usr_gender_id = layers.data(name='gender_id', shape=[1], dtype='int64')
usr_gender_emb = layers.embedding(
input=usr_gender_id,
size=[USR_GENDER_DICT_SIZE, 16],
param_attr='gender_table',
is_sparse=IS_SPARSE)
usr_gender_fc =layers.fc(input=usr_gender_emb, size=16)
USR_AGE_DICT_SIZE =len(paddle.dataset.movielens.age_table)
usr_age_id = layers.data(name='age_id', shape=[1], dtype="int64")
usr_age_emb = layers.embedding(
input=usr_age_id,
size=[USR_AGE_DICT_SIZE, 16],
is_sparse=IS_SPARSE,
param_attr='age_table')
usr_age_fc = layers.fc(input=usr_age_emb,size=16)
USR_JOB_DICT_SIZE =paddle.dataset.movielens.max_job_id() + 1
usr_job_id = layers.data(name='job_id', shape=[1], dtype="int64")
usr_job_emb = layers.embedding(
input=usr_job_id,
size=[USR_JOB_DICT_SIZE, 16],
param_attr='job_table',
is_sparse=IS_SPARSE)
usr_job_fc = layers.fc(input=usr_job_emb,size=16)
concat_embed = layers.concat(
input=[usr_fc, usr_gender_fc,usr_age_fc, usr_job_fc], axis=1)
usr_combined_features =layers.fc(input=concat_embed, size=200, act="tanh")
return usr_combined_features
如上述程式碼所示,對於每個使用者,我們輸入4維特徵。其中包括user_id,gender_id,age_id,job_id。這幾維特徵均是簡單的整數值。為了後續神經網路處理這些特徵方便,我們借鑑NLP中的語言模型,將這幾維離散的整數值,變換成embedding取出。分別形成usr_emb, usr_gender_emb, usr_age_emb, usr_job_emb。
然後,我們對於所有的使用者特徵,均輸入到一個全連線層(fc)中。將所有特徵融合為一個200維度的特徵。
進而,我們對每一個電影特徵做類似的變換,網路配置為:
def get_mov_combined_features():
"""network definition for item(movie) part"""
MOV_DICT_SIZE =paddle.dataset.movielens.max_movie_id() + 1
mov_id = layers.data(name='movie_id', shape=[1], dtype='int64')
mov_emb = layers.embedding(
input=mov_id,
dtype='float32',
size=[MOV_DICT_SIZE, 32],
param_attr='movie_table',
is_sparse=IS_SPARSE)
mov_fc = layers.fc(input=mov_emb, size=32)
CATEGORY_DICT_SIZE = len(paddle.dataset.movielens.movie_categories())
category_id = layers.data(
name='category_id', shape=[1], dtype='int64', lod_level=1)
mov_categories_emb = layers.embedding(
input=category_id,size=[CATEGORY_DICT_SIZE, 32], is_sparse=IS_SPARSE)
mov_categories_hidden =layers.sequence_pool(
input=mov_categories_emb, pool_type="sum")
MOV_TITLE_DICT_SIZE =len(paddle.dataset.movielens.get_movie_title_dict())
mov_title_id = layers.data(
name='movie_title', shape=[1], dtype='int64', lod_level=1)
mov_title_emb = layers.embedding(
input=mov_title_id,size=[MOV_TITLE_DICT_SIZE, 32], is_sparse=IS_SPARSE)
mov_title_conv = nets.sequence_conv_pool(
input=mov_title_emb,
num_filters=32,
filter_size=3,
act="tanh",
pool_type="sum")
concat_embed = layers.concat(
input=[mov_fc, mov_categories_hidden,mov_title_conv], axis=1)
mov_combined_features =layers.fc(input=concat_embed, size=200, act="tanh")
return mov_combined_features
電影標題名稱(title)是一個序列的整數,整數代表的是這個詞在索引序列中的下標。這個序列會被送入 sequence_conv_pool 層,這個層會在時間維度上使用卷積和池化。因為如此,所以輸出會是固定長度,儘管輸入的序列長度各不相同。
最後,我們定義一個inference_program來使用餘弦相似度計算使用者特徵與電影特徵的相似性。
def inference_program():
"""the combined network"""
usr_combined_features =get_usr_combined_features()
mov_combined_features =get_mov_combined_features()
inference =layers.cos_sim(X=usr_combined_features, Y=mov_combined_features)
scale_infer = layers.scale(x=inference,scale=5.0)
return scale_infer
進而,我們定義一個train_program來使用inference_program計算出的結果,在標記資料的幫助下來計算誤差。我們還定義了一個optimizer_func來定義優化器。
def train_program():
"""define the cost function"""
scale_infer = inference_program()
label = layers.data(name='score', shape=[1], dtype='float32')
square_cost =layers.square_error_cost(input=scale_infer, label=label)
avg_cost = layers.mean(square_cost)
return [avg_cost,scale_infer]
def optimizer_func():
returnfluid.optimizer.SGD(learning_rate=0.2)
4.3.模型訓練
(1)定義訓練環境
定義您的訓練環境,可以指定訓練是發生在CPU還是GPU上。
use_cuda = False
place = fluid.CUDAPlace(0) if use_cuda elsefluid.CPUPlace()
(2)定義資料提供器
這一步是為訓練和測試定義資料提供器。提供器讀入一個大小為 BATCH_SIZE的資料。paddle.dataset.movielens.train每次會在亂序化後提供一個大小為BATCH_SIZE的資料,亂序化的大小為快取大小buf_size。
train_reader = paddle.batch(
paddle.reader.shuffle(
paddle.dataset.movielens.train(),buf_size=8192),
batch_size=BATCH_SIZE)
test_reader = paddle.batch(
paddle.dataset.movielens.test(),batch_size=BATCH_SIZE)
(3)構造訓練過程
我們這裡構造了一個訓練過程,包括訓練優化函式。
(4)提供資料
feed_order用來定義每條產生的資料和paddle.layer.data之間的對映關係。比如,movielens.train產生的第一列的資料對應的是user_id這個特徵。
feed_order = [ 'user_id', 'gender_id', 'age_id', 'job_id', 'movie_id', 'category_id', 'movie_title', 'score']
(5)構造訓練程式以及測試程式
分別構建訓練程式和測試程式,並引入訓練優化器。
main_program =fluid.default_main_program()
star_program =fluid.default_startup_program()
[avg_cost, scale_infer] =train_program()
test_program =main_program.clone(for_test=True)
sgd_optimizer =optimizer_func()
sgd_optimizer.minimize(avg_cost)
exe = fluid.Executor(place)
deftrain_test(program, reader):
count = 0
feed_var_list = [
program.global_block().var(var_name) for var_name in feed_order
]
feeder_test = fluid.DataFeeder(
feed_list=feed_var_list, place=place)
test_exe = fluid.Executor(place)
accumulated = 0
for test_data in reader():
avg_cost_np =test_exe.run(program=program,
feed=feeder_test.feed(test_data),
fetch_list=[avg_cost])
accumulated += avg_cost_np[0]
count += 1
return accumulated /count
(6)構造訓練主迴圈
我們根據上面定義的訓練迴圈數(PASS_NUM)和一些別的引數,來進行訓練迴圈,並且每次迴圈都進行一次測試,當測試結果足夠好時退出訓練並儲存訓練好的引數。
# Specify the directory pathto save the parameters
params_dirname = "recommender_system.inference.model"
from paddle.utils.plot import Ploter
train_prompt = "Train cost"
test_prompt = "Test cost"
plot_cost = Ploter(train_prompt,test_prompt)
deftrain_loop():
feed_list = [
main_program.global_block().var(var_name) for var_name in feed_order
]
feeder = fluid.DataFeeder(feed_list, place)
exe.run(star_program)
for pass_id in range(PASS_NUM):
for batch_id, datain enumerate(train_reader()):
# train a mini-batch
outs =exe.run(program=main_program,
feed=feeder.feed(data),
fetch_list=[avg_cost])
out = np.array(outs[0])
# get test avg_cost
test_avg_cost =train_test(test_program, test_reader)
plot_cost.append(train_prompt,batch_id, outs[0])
plot_cost.append(test_prompt,batch_id, test_avg_cost)
plot_cost.plot()
if batch_id == 20:
if params_dirname isnotNone:
fluid.io.save_inference_model(params_dirname, [
"user_id", "gender_id", "age_id", "job_id",
"movie_id", "category_id", "movie_title"
], [scale_infer], exe)
return
print('EpochID {0}, BatchID {1}, Test Loss {2:0.2}'.format(
pass_id + 1, batch_id + 1, float(test_avg_cost)))
if math.isnan(float(out[0])):
sys.exit("got NaN loss, training failed.")
(7)開始訓練
train_loop()
4.4.模型測試
(1)生成測試資料
使用create_lod_tensor(data, lod, place) 的API來生成細節層次的張量。data是一個序列,每個元素是一個索引號的序列。lod是細節層次的資訊,對應於data。比如,data = [[10, 2, 3], [2, 3]] 意味著它包含兩個序列,長度分別是3和2。於是相應地 lod = [[3, 2]],它表明其包含一層細節資訊,意味著 data 有兩個序列,長度分別是3和2。
在這個預測例子中,我們試著預測使用者ID為1的使用者對於電影'Hunchback of Notre Dame'的評分。
infer_movie_id = 783
infer_movie_name = paddle.dataset.movielens.movie_info()[infer_movie_id].title
user_id =fluid.create_lod_tensor([[np.int64(1)]], [[1]], place)
gender_id =fluid.create_lod_tensor([[np.int64(1)]], [[1]], place)
age_id =fluid.create_lod_tensor([[np.int64(0)]], [[1]], place)
job_id =fluid.create_lod_tensor([[np.int64(10)]], [[1]], place)
movie_id =fluid.create_lod_tensor([[np.int64(783)]], [[1]], place) # Hunchback ofNotre Dame
category_id =fluid.create_lod_tensor([np.array([10, 8, 9], dtype='int64')], [[3]], place) # Animation,Children's, Musical
movie_title =fluid.create_lod_tensor([np.array([1069, 4140, 2923, 710, 988], dtype='int64')], [[5]],
place) # 'hunchback','of','notre','dame','the'
(2)構建預測過程
與訓練過程類似,我們需要構建一個預測過程。其中, params_dirname是之前用來存放訓練過程中的各個引數的地址。
place = fluid.CUDAPlace(0) if use_cuda elsefluid.CPUPlace()
exe = fluid.Executor(place)
inference_scope =fluid.core.Scope()
(3)進行測試
現在我們可以進行預測了。我們要提供的feed_order應該和訓練過程保持一致。
with fluid.scope_guard(inference_scope):
[inferencer, feed_target_names,
fetch_targets] =fluid.io.load_inference_model(params_dirname, exe)
results = exe.run(inferencer,
feed={
'user_id': user_id,
'gender_id': gender_id,
'age_id': age_id,
'job_id': job_id,
'movie_id': movie_id,
'category_id': category_id,
'movie_title': movie_title
},
fetch_list=fetch_targets,
return_numpy=False)
predict_rating = np.array(results[0])
print("Predict Rating of user id 1 on movie \"" + infer_movie_name +
"\" is " +str(predict_rating[0][0]))
print("Actual Rating of user id 1 on movie \"" + infer_movie_name +
"\" is 4.")
趕快動手嘗試下吧!
想與更多的深度學習開發者交流,請加入飛槳官方QQ群:796771754。
如果您想詳細瞭解更多飛槳PaddlePaddle的相關內容,請參閱以下文件。
官網地址:
https://www.paddlepaddle.org.cn/
專案地址:
https://github.com/PaddlePaddle/book/blob/develop/05.recommender_system/README.cn.md