代碼詳解:建立一個百分百懂你的產品推薦系統

全文共10848字,預計學習時長21分鐘或更長python

圖片來源:Unsplash/Susan Yin

你也許天天都會逛一逛電子商務網站,或者從博客、新聞和媒體出版物上閱讀大量文章。算法

瀏覽這些東西的時候,最令讀者或者用戶煩惱的事情是什麼呢?json

——有太多的東西能夠看,反而會常常看不到本身正在搜索的東西。數組

是的,網上有太多的信息和文章,用戶須要一種方式來簡化他們的發現之旅。瀏覽器

若是你在經營一家電子商務網站或博客,你也許會問:有這個必要嗎?bash

嗯……你聽過漏斗嗎?服務器

用戶所用的漏斗越小,產品的轉換就越大。這是用戶體驗的基本原則。因此,若是減小步驟的數量能夠增長網站頁面的瀏覽量甚至是收入,爲何不這麼作呢?微信

推薦系統如何提供幫助?網絡

簡單來講,推薦系統就是一個發現系統,該系統可經過分析數據向用戶提供推薦。不須要用戶去專門搜索,系統自動帶來推薦商品。app

這聽起來像是魔法。

亞馬遜和Netflix幾十年前就開始使用這種魔法了。

一打開Spotify,它就已經爲用戶提供了一個推薦歌單(這種深度個性化推薦服務叫做Discover Weekly)。

圖片來源:Unsplash/Samuel Zeller

深刻了解推薦系統

通常來講,咱們所知的推薦系統有兩種——固然並非全部的人都知道。

1. 基於內容的推薦系統

這類推薦系統很容易被咱們的大腦消化,並且不會出現短路或爆炸的跡象。

例如,你是一個狂熱的小說迷,喜歡阿加莎·克里斯蒂的《無人生還》,並從網上書店買了這本書。

那麼,當你下次再打開網站時,網上書店就會給你推薦《ABC謀殺案》。

爲何呢?

由於它們都是阿加莎·克里斯蒂的做品。

所以,基於內容的推薦模型會向你推薦這本書。

就是這麼簡單!那就來用一用吧!

等等……

雖然這種基於內容的推薦很容易被咱們的大腦消化,看起來也很簡單,但它沒法預測用戶的真實行爲。

例如,你不喜歡偵探赫丘裏·波羅,但喜歡阿加莎·克里斯蒂小說中的其餘偵探。在這種狀況下,網站就不該該向你推薦《ABC謀殺案》。

2. 協同過濾推薦系統

這種類型的推薦系統克服了上面的問題。本質上,該系統記錄了用戶在網站上的全部交互,並基於這些記錄提出建議。

它是什麼原理呢?

請看下面的場景:

這裏有兩個用戶,用戶A和用戶B。

用戶A購買了商品1

用戶A購買了商品2

用戶A購買了商品3

用戶B購買了商品1

用戶B購買了商品3

那麼協同過濾系統將會向用戶B推薦商品2,由於有另一個用戶也購買了商品1和商品3,同時還購買了商品2。

你也許會說,得了吧,他們多是偶然才一塊兒買了那些巧合的商品。

可是,若是有100個用戶都與用戶A有相同的購買行爲呢?

這就是所謂的羣衆的力量。

那麼,你還在等什麼呢?讓咱們開始在你的生產環境中建立協同過濾推薦系統吧!

等等,先彆着急!

雖然這個系統性能極佳,但在嘗試建立可用於生產的系統時,它還存在幾個嚴重問題。

協同過濾推薦系統的不足

1. 它不知道用戶的購物習慣。基於內容的推薦系統會根據用戶的購物記錄推薦類似商品,與此相反,協同過濾推薦系統的推薦並非基於類似性。若是你關心這一問題的話,解決方案就是將兩種方法混合起來,結合使用。

2. 由於須要存儲用戶項矩陣,因此係統須要大量的硬件資源。假設你的電子商務網站有10萬用戶;與此同時,你的網站提供1萬種產品。在這種狀況下,你將須要10000 x 100000的矩陣,每一個元素包含4個字節的整數。是的,光是存儲矩陣,不作其餘事,你就須要4GB的內存。

3. 「冷啓動」(冰冷的開始),該系統並不會爲新用戶帶來好處,由於系統並不瞭解新用戶。

4. 不變性。若是用戶沒有在網站上進行搜索或購物,系統的推薦將一成不變。因而用戶就會認爲網站上沒有什麼新鮮東西,從而退出網站。

經過混合使用兩種推薦系統能夠輕易解決第1個問題,然而,其餘問題仍然使人頭痛。

本文的目的就是解決第二、第3和第4個問題。

讓咱們開始吧!


使推薦系統可用於生產的終極指南

如何解決這些問題?機器自己存在限制,並且就算是根據常識,也不可能僅爲小小的需求就部署一個巨大的服務器。

推薦下面這本書:

Ted Dunning 和Ellen Friedman的《實用性機器學習》

這本書告訴咱們,對於一個可用於生產的系統,你不須要期望它在任何方面都具有最高精度。

在實際的用例中,一個有些不許確但又能夠接受的方法,一般是最有效的。

關於如何作到這一點,最有趣的部分是:

1. 對通用推薦指標進行批量計算。

2. 實時查詢,不使用用戶-商品矩陣,而是獲取用戶的最新交互並向系統查詢。

下面咱們邊構建系統邊解釋。

Python的推薦系統

爲何選擇python? 由於python的語言簡單易學,只須要幾個小時就能理解它的語法。

for item in the_bag:
    print(item)複製代碼

經過上面代碼,你能夠打印包裏的全部項。

請訪問Python官網(https://www.python.org/downloads/),根據操做系統下載並安裝相應安裝包。

本教程須要用到如下幾個安裝包。

pip install numpy

pip install scipy

pip install pandas

pip install jupyter

pip install requests

Numpy和Scipy是處理數學計算的python包,建構矩陣時須要用到它們。Pandas 用於數據處理。Requests用於http調用。Jupyter是一個能夠交互運行python代碼的網絡應用程序。

輸入Jupyter Notebook,你會看到以下界面


在提供的單元格上編寫代碼,代碼將以交互方式運行。

開始以前須要幾個工具。

1. Elasticsearch(彈性搜索)。這是一個開源搜索引擎,能夠幫助快速搜索到文檔。這個工具可用於保存計算指標,以便實時查詢。

2. Postman。這是一個API開發工具,可用來模擬彈性搜索中的查詢,由於彈性搜索能夠經過http訪問。

下載並安裝這兩個工具,接着就能夠開始了。

數據

先來看看Kaggle中的數據集:電子商務網站行爲數據集(http://www.baidu.com/link?url=-uZgHHgYJmRlBX5WL_ufkLSb0S5eXU0j43iPMLh3XNtXbLq5uNoqe3Oje7AUt0PK)。下載並提取Jupyter 筆記本目錄中的數據。

在這些文件中,本教程只須要用到events.csv。

該文件由用戶對電子商務網站上的商品進行的數百萬次操做組成。

開始探索數據吧!

import pandas as pd
import numpy as np複製代碼

將輸入寫在Jupyter Notebook上,就能夠開始了。

df = pd.read_csv('events.csv')
df.shape複製代碼

它會輸出(2756101,5),這意味着你有270萬行和5列。

讓咱們來看看

df.head()

它有5欄。

1. 時間戳(Timestamp),事件的時間戳

2. 訪問者ID(Visitorid),用戶的身份

3. 商品ID(Itemid), 商品的名稱

4. 事件(Event), 事件

5. 交易ID(Transactionid),若是事件是交易,則爲交易ID

下面檢查一下,哪些事件是可用的

df.event.unique()

你將得到三個事件:瀏覽、添加到購物車和交易。

你可能嫌麻煩,不想處理全部事件,因此本教程中只需處理交易。

因此,咱們只過濾交易。

trans = df[df['event'] == 'transaction']
trans.shape複製代碼

它將輸出(22457, 5)

也就是說你將有22457個交易數據能夠處理。這對新手來講已經足夠了。

下面來進一步看看數據

visitors = trans['visitorid'].unique()
items = trans['itemid'].unique()

print(visitors.shape)
print(items.shape)複製代碼

你將得到11719個獨立訪問者和12025個獨立商品。

建立一個簡單而有效的推薦系統,經驗之談是在不損失質量的狀況下對數據進行抽樣。這意味着,對於每一個用戶,你只需獲取50個最新交易數據,卻仍然能夠得到想要的質量,由於顧客行爲會隨着時間的推移而改變。

trans2 = trans.groupby(['visitorid']).head(50)
trans2.shape複製代碼

如今你只有19939筆交易。這意味着2000筆左右的交易已通過時。

因爲訪問者ID和商品ID是一長串的數字,你很難記住每一個ID。

trans2['visitors'] = trans2['visitorid'].apply(lambda x :
 np.argwhere(visitors == x)[0][0])
trans2['items'] = trans2['itemid'].apply(lambda x : np.argwhere(items
 == x)[0][0])

trans2複製代碼

你須要其餘基於0的索引列。如如下界面所示:

這樣更加清晰。接下來的全部步驟只需使用訪問者和商品欄。

圖片來源:Unsplash/Andrei Lazarev

下一步:建立用戶-商品矩陣

噩夢來了……

一共有11719個獨立訪問者和12025個商品,因此須要大約500MB的內存來存儲矩陣。

稀疏矩陣(Sparse matrix)這時候就派上用場了。

稀疏矩陣是大多數元素爲零的矩陣。這是有意義的,由於不可能全部的用戶都購買全部的商品,不少鏈接都將爲零。

from scipy.sparse import csr_matrix

Scipy頗有用。

occurences = csr_matrix((visitors.shape[0], items.shape[0]),
 dtype='int8')

def set_occurences(visitor, item):
    occurences[visitor, item] += 1

trans2.apply(lambda row: set_occurences(row['visitors'], row['items']),
 axis=1)

occurences複製代碼

對數據中的每一行應用set_occurences函數。

會輸出以下結果:

<11719x12025 sparse matrix of type '<class 'numpy.int8'>'

with 18905 stored elements in Compressed Sparse Row format>

在矩陣的1.4億個單元格中,只有18905個單元格是用非零數據填充的。

因此,實際上只須要把這18905個值存儲到內存中,效率就能提升99.99%。

但稀疏矩陣有一個缺點,想要實時檢索數據的話,須要很大的計算量。因此,到這裏尚未結束。

共現矩陣

下面建構一個商品-商品矩陣,其中每一個元素表示用戶同時購買兩個商品的次數,咱們稱之爲共現矩陣。

要建立共現矩陣,你須要將共現矩陣的轉置與自身作點積。

有人試過在沒有稀疏矩陣的狀況下這樣作,結果電腦死機了。因此,千萬不要重蹈覆轍。

cooc = occurences.transpose().dot(occurences)

cooc.setdiag(0)

電腦立馬輸出了一個稀疏矩陣。

setdiag函數將對角線設置爲0,這意味着你不想計算商品1的值,而商品1的位置都在一塊兒,由於它們是相同的項目。

異常行爲

共現矩陣包含同時購買兩種商品的次數。

但也可能會存在一種商品,購買這種商品自己和用戶的購物習慣沒有任何關係,多是限時搶購之類的商品。

在現實中,你想要捕捉的是真正的用戶行爲,而非像限時搶購那樣很是規行爲。

爲了消除這些影響,你須要對共現矩陣的得分進行扣除。

Ted Dunnings在前一本書中提出了一種算法,叫作對數似然比(Log-Likelihood Ratio, LLR)。

def xLogX(x):
    return x * np.log(x) if x != 0 else 0.0

def entropy(x1, x2=0, x3=0, x4=0):
    return xLogX(x1 + x2 + x3 + x4) - xLogX(x1) - xLogX(x2) - xLogX(x3)
 - xLogX(x4)

def LLR(k11, k12, k21, k22):
    rowEntropy = entropy(k11 + k12, k21 + k22)
    columnEntropy = entropy(k11 + k21, k12 + k22)
    matrixEntropy = entropy(k11, k12, k21, k22)
    if rowEntropy + columnEntropy < matrixEntropy:
        return 0.0
    return 2.0 * (rowEntropy + columnEntropy - matrixEntropy)

def rootLLR(k11, k12, k21, k22):
    llr = LLR(k11, k12, k21, k22)
    sqrt = np.sqrt(llr)
    if k11 * 1.0 / (k11 + k12) < k21 * 1.0 / (k21 + k22):
        sqrt = -sqrt
    return sqrt複製代碼

LLR函數計算的是A和B兩個事件同時出現的可能性。

參數有

1.k11, 兩個事件同時發生的次數

2.k12, 事件B 單獨發生的次數

3.k21, 事件A單獨發生的次數

4.k22, 事件A和事件B都沒有發生的次數

如今計算LLR函數並將其保存到pp_score矩陣中。

row_sum = np.sum(cooc, axis=0).A.flatten()
column_sum = np.sum(cooc, axis=1).A.flatten()
total = np.sum(row_sum, axis=0)

pp_score = csr_matrix((cooc.shape[0], cooc.shape[1]), dtype='double')
cx = cooc.tocoo()
for i,j,v in zip(cx.row, cx.col, cx.data):
    if v != 0:
        k11 = v
        k12 = row_sum[i] - k11
        k21 = column_sum[j] - k11
        k22 = total - k11 - k12 - k21
        pp_score[i,j] = rootLLR(k11, k12, k21, k22)複製代碼

對結果進行排序,使每種商品LLR得分最高的位於每行的第一列。

result = np.flip(np.sort(pp_score.A, axis=1), axis=1)

result_indices = np.flip(np.argsort(pp_score.A, axis=1), axis=1)

推薦系統的指標

結果矩陣中的第一項指標若是足夠高的話,能夠被視爲該項的指標。

來看一下其中的一個結果

result[8456]

你會獲得

array([15.33511076, 14.60017668, 3.62091635, ..., 0. ,

0. , 0. ])

再看看指標

result_indices[8456]

你會獲得

array([8682, 380, 8501, ..., 8010, 8009, 0], dtype=int64)

能夠有把握地說,商品8682和商品380的LLR分數很高,能夠做爲商品8456的指標。而商品8501分數不是那麼高,可能不能做爲商品8456的指標。

這意味着,若是有用戶購買了商品8682和商品380,你能夠向他推薦商品8456。

這很簡單。

可是,根據經驗,你可能想給LLR分數施加一些限制,這樣能夠刪除可有可無的指標。

minLLR = 5
indicators = result[:, :50]
indicators[indicators < minLLR] = 0.0

indicators_indices = result_indices[:, :50]

max_indicator_indices = (indicators==0).argmax(axis=1)
max = max_indicator_indices.max()

indicators = indicators[:, :max+1]
indicators_indices = indicators_indices[:, :max+1]複製代碼

如今,已經準備好將它們組合到彈性搜索中了,這樣就能夠實時查詢推薦。

import requests

import json

好了,如今能夠把以前準備好的東西放到彈性搜索中了。

可是,請注意。若是你想用 /_create/<id> API一個個地添加數據,將會花費很長時間。你固然能夠這麼作,可是可能須要花費半個小時到一個小時才能把12025個商品轉移到彈性搜索中。

那怎麼解決這個問題呢?

批量更新

幸運的是,彈性搜索擁有批量API,能夠輕鬆地同時發送多個文檔。

所以,建立一個新索引(items2),讓咱們來嘗試一下:

actions = []
for i in range(indicators.shape[0]):
    length = indicators[i].nonzero()[0].shape[0]
    real_indicators = items[indicators_indices[i,
 :length]].astype("int").tolist()
    id = items[i]

action = { "index" : { "_index" : "items2", "_id" : str(id) } }    data = {        "id": int(id),        "indicators": real_indicators    }
    actions.append(json.dumps(action))
    actions.append(json.dumps(data))
    if len(actions) == 200:    actions_string = "\n".join(actions) + "\n"    actions = []url = "http://127.0.0.1:9200/_bulk/"       headers = {           "Content-Type" : "application/x-ndjson"       }       requests.post(url, headers=headers, data=actions_string)if
 len(actions) > 0:    actions_string = "\n".join(actions) + "\n"    actions = [] url = "http://127.0.0.1:9200/_bulk/"    headers = {        "Content-Type" : "application/x-ndjson"    }    requests.post(url, headers=headers, data=actions_string)複製代碼

瞧,只須要幾秒鐘就能完成。

在Postman中點擊這個API

127.0.0.1:9200/items2/_count

你就存儲了數據

{
    "count": 12025,
    "_shards": { 
       "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    }
}複製代碼

用/items2/240708檢查一下商品數據

{
    "id": 240708,
    "indicators": [
        305675,
        346067,
        312728
    ]
}複製代碼

Id是商品的Id,而指標則是成爲該商品推薦指標的其餘商品。

實時查詢

建立的最棒的部分就是實時查詢

{
  "query": {
    "bool": {
     "should": [
      { "terms": {"indicators" : [240708], "boost": 2}}
     ]
    }
  }
}複製代碼

發送請求到127.0.0.1:9200/items2/_search

你會獲得三個結果。商品312728, 商品305675和 商品346067。正是會與商品240708一塊兒購買的三件商品。

太棒了!如今大量的資源需求已經不是問題了。那麼,另外兩個問題呢?

圖片來源:Unsplash/Sean O.

「冷啓動」問題:我不認識你

建立推薦系統時,最多見的就是冷啓動問題,由於系統中不會有新用戶的任何行爲記錄。

那麼,系統應該向他們推薦什麼呢?

請看咱們最近構建的推薦系統。你以爲這個結果有什麼異常嗎?

是的,結果只返回3個推薦項——只有3個。你打算如何向客戶展現這三個可憐的推薦項呢?

爲了更好的用戶體驗,讓咱們將未受推薦的商品放在列表末尾。

{
  "query": {
    "bool": {
     "should": [
      { "terms": {"indicators" : [240708]}},
      { "constant_score": {"filter" : {"match_all": {}}, "boost" :
 0.000001}}
     ]
    }
  }
}複製代碼

你可使用常數分數來返回全部其餘項。

可是,你也須要對全部未受推薦的項目進行排序,這樣即便沒有再用戶的行爲中捕捉到,也有多是用戶會喜歡的商品。

多數狀況下,受歡迎的商品很是好用。

如何肯定一個商品是否受歡迎呢?

popular = np.zeros(items.shape[0])

def inc_popular(index):
    popular[index] += 1

trans2.apply(lambda row: inc_popular(row['items']), axis=1)複製代碼

這很簡單,逐個數商品的出現次數,出現次數最多的就最流行。讓咱們建立另外一個索引items3。批量插入

actions = []
for i in range(indicators.shape[0]):
    length = indicators[i].nonzero()[0].shape[0]
                    real_indicators = items[indicators_indices[i,
 :length]].astype("int").tolist()
    id = items[i]

        action = { "index" : { "_index" : "items3", "_id" : str(id) } }
    # url = "http://127.0.0.1:9200/items/_create/" + str(id)
    data = {
        "id": int(id),
        "indicators": real_indicators,
        "popular": popular[i]
    }
        actions.append(json.dumps(action))
        actions.append(json.dumps(data))

        if len(actions) == 200:
           actions_string = "\n".join(actions) + "\n"
           actions = []
      
           url = "http://127.0.0.1:9200/_bulk/"
           headers = {
               "Content-Type" : "application/x-ndjson" 
       }
                requests.post(url, headers=headers, data=actions_string)if
 len(actions) > 0:
    actions_string = "\n".join(actions) + "\n"
    actions = []url = "http://127.0.0.1:9200/_bulk/"
    headers = {
        "Content-Type" : "application/x-ndjson"
    }
    requests.post(url, headers=headers, data=actions_string)複製代碼

這個索引階段中也包括流行字段。因此數據會是這樣的

{
    "id": 240708,
    "indicators": [
        305675,
        346067,
        312728
    ],
    "popular": 3.0
}複製代碼

你將會有三個字段。ID,指標(與前面相似),以及流行字段(也就是用戶購買的商品數量)。

在前面的查詢中加入popular。

函數得分:組合得分的方法

因此,如今有多個得分來源,即指標分數和流行分數,那麼如何將分數組合起來呢?

能夠用彈性搜索的功能評分。

{
  "query": {
    "function_score":{
     "query": {
      "bool": { 
      "should": [
        { "terms": {"indicators" : [240708], "boost": 2}},
        { "constant_score": {"filter" : {"match_all": {}}, "boost" :
 0.000001}}
       ]
      }
    }, 
    "functions":[ 
     { 
      "filter": {"range": {"popular": {"gt": 0}}},
       "script_score" : {
                 "script" : {
                   "source": "doc['popular'].value * 0.1"
                 }
             }
      }
     ],
     "score_mode": "sum",
     "min_score" : 0
    }
  }
}複製代碼

修改查詢,並添加一個功能評分,將流行值的0.1倍添加到上面的常量分數中。沒必要執着於0.1,也可使用其餘函數,甚至天然對數。像這樣:

Math.log(doc['popular'].value)

如今,能夠看到最受歡迎的商品461686排在第四位,僅低於推薦商品。

下面依次是其它受歡迎的商品。

不變的、靜態的推薦

如你所見,每次實時查詢時,推薦結果都保持不變。一方面這很好,由於咱們的技術是可複製的;但另外一方面,用戶可能對此並不滿意。

Ted Dunnings在他的書中說,在推薦的第20個商品後,點擊率將會很是低。這意味着在那以後咱們推薦的任何商品都不會被用戶知道。

怎麼解決這個問題呢?

有一種技術叫作抖動。它會在查詢時產生一種隨機干擾,使最不受推薦的商品的排名提早,但同時又保證受到強烈推薦的商品仍然在推薦列表的前幾位。

{
  "query": {
    "function_score":{
     "query": {
      "bool": {
       "should": [
        { "terms": {"indicators" : [240708], "boost": 2}},
        { "constant_score": {"filter" : {"match_all": {}}, "boost" :
 0.000001}}
       ]
      }
    },
     "functions":[
      {
       "filter": {"range": {"popular": {"gt": 1}}},
       "script_score" : {
                 "script" : {
                   "source": "0.1 * Math.log(doc['popular'].value)"
                 }
             }
      },
      {
       "filter": {"match_all": {}},
       "random_score": {}
      }
     ],
     "score_mode": "sum",
     "min_score" : 0
    }
  }
}複製代碼

隨機分數會給出使全部商品均勻分佈的隨機干擾。得分很小,這樣最受歡迎的推薦商品排名就不會降低。

好處在於用戶將瀏覽時沒必要滾動到第二或第三頁,只須要點擊瀏覽器上的刷新按鈕,就會獲得新的內容。

這很神奇。

圖片來源:Unsplash/Marvin Meyer

推薦閱讀專題


留言 點贊 關注

咱們一塊兒分享AI學習與發展的乾貨
歡迎關注全平臺AI垂類自媒體 「讀芯術」


(添加小編微信:dxsxbb,加入讀者圈,一塊兒討論最新鮮的人工智能科技哦~)

相關文章
相關標籤/搜索