1. 程式人生 > >Python基於機器學習方法實現的電影推薦系統

Python基於機器學習方法實現的電影推薦系統

推薦演算法在網際網路行業的應用非常廣泛,今日頭條、美團點評等都有個性化推薦,推薦演算法抽象來講,是一種對於內容滿意度的擬合函式,涉及到使用者特徵和內容特徵,作為模型訓練所需維度的兩大來源,而點選率,頁面停留時間,評論或下單等都可以作為一個量化的 Y 值,這樣就可以進行特徵工程,構建出一個數據集,然後選擇一個合適的監督學習演算法進行訓練,得到模型後,為客戶推薦偏好的內容,如頭條的話,就是諮詢和文章,美團的就是生活服務內容。

可選擇的模型很多,如協同過濾,邏輯斯蒂迴歸,基於DNN的模型,FM等。我們使用的方式是,基於內容相似度計算進行召回,之後通過FM模型和邏輯斯蒂迴歸模型進行精排推薦,下面就分別說一下,我們做這個電影推薦系統過程中,從資料準備,特徵工程,到模型訓練和應用的整個過程。

我們實現的這個電影推薦系統,爬取的資料實際上維度是相對少的,特別是使用者這一側的維度,正常推薦系統涉及的維度,諸如頁面停留時間,點選頻次,收藏等這些維度都是沒有的,以及使用者本身的維度也相對要少,沒有地址、年齡、性別等這些基本的維度,這樣我們爬取的資料只有打分和評論這些資訊,所以之後我們又從這些資訊裡再拿出一些統計維度來用。我們爬取的電影資料(除電影詳情和圖片資訊外)是如下這樣的形式:

這裡的資料是有冗餘的,又通過如下的程式碼,對資料進行按維度合併,去除冗餘資料條目:

# 處理主函式,負責將多個冗餘資料合併為一條電影資料,將地區,導演,主演,型別,特色等維度資料合併
def mainfunc():
    try:
        unable_list = []
        with connection.cursor() as cursor:
            sql='select id,name from movie'
            cout=cursor.execute(sql)
            print("數量: "+str(cout))

            for row in cursor.fetchall():
                #print(row[1])
                movieinfo = df[df['電影名'] == row[1]]
                if movieinfo.shape[0] == 0:
                    disable_movie(row[0])
                    print('disable movie ' + str(row[1]))
                else:
                    g = lambda x:movieinfo[x].iloc[0]
                    types = movieinfo['型別'].tolist()
                    types = reduce(lambda x,y:x+'|'+y,list(set(types)))
                    traits = movieinfo['特色'].tolist()
                    traits = reduce(lambda x,y:x+'|'+y,list(set(traits)))
                    update_one_movie_info(type_=types, actors=g('主演'), region=g('地區'), director=g('導演'), trait=traits, rat=g('評分'), id_=row[0])
                          
        connection.commit()
    finally:
        connection.close()
   

之後開始準備使用者資料,我們從使用者打分的資料中,統計出每一個使用者的打分的最大值,最小值,中位數值和平均值等,從而作為使用者的一個附加屬性,儲存於userproex表中:

'insert into userproex(userid, rmax, rmin, ravg, rcount, rsum, rmedian) values(\'%s\', %s, %s, %s, %s, %s, %s)' % (userid, rmax, rmin, ravg, rcount, rsum, rmedium)


'update userproex set rmax=%s, rmin=%s, ravg=%s, rmedian=%s, rcount=%s, rsum=%s where userid=\'%s\'' % (rmax, rmin, ravg, rmedium, rcount, rsum, userid)

以上兩個SQL是最終插入表的時候用到的,代表準備使用者資料的最終步驟,其餘細節可以參考文末的github倉庫,不在此贅述,資料處理還用到了一些SQL,以及其他處理細節。

系統上線執行時,第一次是全量的資料處理,之後會是增量處理過程,這個後面還會提到。

我們目前把使用者資料和電影的資料的原始資料算是準備好了,下一步開始特徵工程。做特徵工程的思路是,對type, actors, director, trait四個型別資料分別構建一個頻度統計字典,用於之後的one-hot編碼,程式碼如下:

def get_dim_dict(df, dim_name):
  type_list = list(map(lambda x:x.split('|') ,df[dim_name]))
  type_list = [x for l in type_list for x in l]
  def reduce_func(x, y):
    for i in x:
      if i[0] == y[0][0]:
        x.remove(i)
        x.append(((i[0],i[1] + 1)))
        return x
    x.append(y[0])
    return x
  l = filter(lambda x:x != None, map(lambda x:[(x, 1)], type_list))
  type_zip = reduce(reduce_func, list(l))
  type_dict = {}
  for i in type_zip:
    type_dict[i[0]] = i[1]
  return type_dict

涉及到的冗餘資料也要刪除

df_ = df.drop(['ADD_TIME', 'enable', 'rat', 'id', 'name'], axis=1)

將電影資料轉換為字典列表,由於演員和導演均過萬維,實際計算時過於稀疏,當演員或導演只出現一次時,標記為冷門演員或導演

movie_dict_list = []
for i in df_.index:
  movie_dict = {}
  #type
  for s_type in df_.iloc[i]['type'].split('|'):
    movie_dict[s_type] = 1
  #actors
  for s_actor in df_.iloc[i]['actors'].split('|'):
    if actors_dict[s_actor] < 2:
      movie_dict['other_actor'] = 1
    else:
      movie_dict[s_actor] = 1
  #regios
  movie_dict[df_.iloc[i]['region']] = 1
  #director
  for s_director in df_.iloc[i]['director'].split('|'):
    if director_dict[s_director] < 2:
      movie_dict['other_director'] = 1
    else:
      movie_dict[s_director] = 1
  #trait
  for s_trait in df_.iloc[i]['trait'].split('|'):
    movie_dict[s_trait] = 1
  movie_dict_list.append(movie_dict)

使用DictVectorizer進行向量化,做One-hot編碼

v = DictVectorizer()
X = v.fit_transform(movie_dict_list)

這樣的資料,下面做餘弦相似度已經可以了,這是特徵工程的基本的一個處理,模型所使用的資料,需要將電影,評分,使用者做一個數據拼接,構建訓練樣本,並儲存CSV,注意這個CSV不用每次全量構建,而是除第一次外都是增量構建,通過mqlog中型別為'c'的訊息,增量構建以comment(評分)為主的訓練樣本,拼接之後的形式如下:

USERID  cf2349f9c01f9a5cd4050aebd30ab74f
movieid 10533913
type    劇情|奇幻|冒險|喜劇
actors  艾米·波勒|菲利絲·史密斯|理查德·坎德|比爾·哈德爾|劉易斯·布萊克
region  美國
director    彼特·道格特|羅納爾多·德爾·卡門
trait   感人|經典|勵志
rat 8.7
rmax    5
rmin    2
ravg    3.85714
rcount  7
rmedian 4
TIME_DIS    15

這個資料的actors等欄位和上面的處理是一樣的,為了之後libfm的使用,在這裡需要轉換為libsvm的資料格式

dump_svmlight_file(train_X_scaling, train_y_, train_file)

模型使用上遵循先召回,後精排的策略,先通過餘弦相似度計算一個相似度矩陣,然後根據這個矩陣,為使用者推薦相似的M個電影,在通過訓練好的FM,LR模型,對這個M個電影做偏好預估,FM會預估一個使用者打分,LR會預估一個點選概率,綜合結果推送給使用者作為推薦電影。

模組列表

  • recsys_ui: 前端技術(html5+JavaScript+jquery+ajax)
  • recsys_web: 後端技術(Java+SpringBoot+mysql)
  • recsys_spider: 網路爬蟲(python+BeautifulSoup)
  • recsys_sql: 使用SQL資料處理
  • recsys_model: pandas, libFM, sklearn. pandas資料分析和資料清洗,使用libFM,sklearn對模型初步搭建
  • recsys_core: 使用pandas, libFM, sklearn完整的資料處理和模型構建、訓練、預測、更新的程式
  • recsys_etl:ETL 處理爬蟲增量資料時使用kettle ETL便捷處理資料

為了能夠輸出一個可感受的系統,我們採購了阿里雲伺服器作為資料庫伺服器和應用伺服器,在線上搭建了電影推薦系統的第一版,地址是:

www.technologyx.cn

可以註冊,也可以使用已有使用者:

使用者名稱 密碼
gavin 123
gavin2 123
wuenda 123

歡迎登入使用感受一下。

設計思路

用簡單地方式表述一下設計思路,

1.後端服務recsys_web依賴於系統資料庫的推薦表‘recmovie’展示給使用者推薦內容
2.使用者對電影打分後(暫時沒有對點選動作進行響應),後臺應用會向mqlog表插入一條資料(訊息)。
3.新使用者註冊,系統會插入mqlog中一條新使用者註冊訊息
4.新電影新增,系統會插入mqlog中一條新電影新增訊息
5.推薦模組recsys_core會拉取使用者的打分訊息,並且並行的做以下操作:
a.增量的更新訓練樣本
b.快速(因伺服器比較卡,目前設定了延時)對使用者行為進行基於內容推薦的召回
c.訓練樣本更新模型
d.使用FM,LR模型對Item based所召回的資料進行精排
e.處理新使用者註冊訊息,監聽到使用者註冊訊息後,對該使用者的屬性初始化(統計值)。
f.處理新電影新增訊息,更新基於內容相似度而生成的相似度矩陣

注:

  • 由於線上資源匱乏,也不想使系統增加複雜度,所以沒有直接使用MQ元件,而是以資料庫表作為代替。
  • 專案原始碼地址: https://github.com/GavinHacker/recsys_core

模型相關的模組介紹

增量的處理使用者comment,即增量處理評分模組

這個模組負責監聽來自mqlog的訊息,如果訊息型別是使用者的新的comment,則對訊息進行拉取,並相應的把新的comment合併到總的訓練樣本集合,並儲存到一個臨時目錄
然後更新資料庫的config表,把最新的樣本集合(csv格式)的路徑更新上去

執行截圖

訊息佇列的截圖

把csv處理為libsvm資料

這個模組負責把最新的csv檔案,非同步的處理成libSVM格式的資料,以供libFM和LR模型使用,根據系統的效能確定任務的間隔時間

執行截圖

基於內容相似度推薦

當監聽到使用者有新的comment時,該模組將進行基於內容相似度的推薦,並按照電影評分推薦

執行截圖

libFM預測

http://www.libfm.org/

對已有的基於內容推薦召回的電影進行模型預測打分,呈現時按照打分排序

如下圖為打分更新

邏輯迴歸預測

對樣本集中的打分做0,1處理,根據正負樣本平衡,> 3分為喜歡 即1, <=3 為0 即不喜歡,這樣使用邏輯迴歸做是否喜歡的點選概率預估,根據概率排序

專案原始碼地址: https://github.com/GavinHacker/recsys_c