全文共10848字,預計學習時長21分鐘或更長python
你也許天天都會逛一逛電子商務網站,或者從博客、新聞和媒體出版物上閱讀大量文章。算法
瀏覽這些東西的時候,最令讀者或者用戶煩惱的事情是什麼呢?json
——有太多的東西能夠看,反而會常常看不到本身正在搜索的東西。數組
是的,網上有太多的信息和文章,用戶須要一種方式來簡化他們的發現之旅。瀏覽器
若是你在經營一家電子商務網站或博客,你也許會問:有這個必要嗎?bash
嗯……你聽過漏斗嗎?服務器
用戶所用的漏斗越小,產品的轉換就越大。這是用戶體驗的基本原則。因此,若是減小步驟的數量能夠增長網站頁面的瀏覽量甚至是收入,爲何不這麼作呢?微信
推薦系統如何提供幫助?網絡
簡單來講,推薦系統就是一個發現系統,該系統可經過分析數據向用戶提供推薦。不須要用戶去專門搜索,系統自動帶來推薦商品。app
這聽起來像是魔法。
亞馬遜和Netflix幾十年前就開始使用這種魔法了。
一打開Spotify,它就已經爲用戶提供了一個推薦歌單(這種深度個性化推薦服務叫做Discover Weekly)。
深刻了解推薦系統
通常來講,咱們所知的推薦系統有兩種——固然並非全部的人都知道。
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個問題。
讓咱們開始吧!
使推薦系統可用於生產的終極指南
如何解決這些問題?機器自己存在限制,並且就算是根據常識,也不可能僅爲小小的需求就部署一個巨大的服務器。
推薦下面這本書:
這本書告訴咱們,對於一個可用於生產的系統,你不須要期望它在任何方面都具有最高精度。
在實際的用例中,一個有些不許確但又能夠接受的方法,一般是最有效的。
關於如何作到這一點,最有趣的部分是:
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的索引列。如如下界面所示:
這樣更加清晰。接下來的全部步驟只需使用訪問者和商品欄。
下一步:建立用戶-商品矩陣
噩夢來了……
一共有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一塊兒購買的三件商品。
太棒了!如今大量的資源需求已經不是問題了。那麼,另外兩個問題呢?
「冷啓動」問題:我不認識你
建立推薦系統時,最多見的就是冷啓動問題,由於系統中不會有新用戶的任何行爲記錄。
那麼,系統應該向他們推薦什麼呢?
請看咱們最近構建的推薦系統。你以爲這個結果有什麼異常嗎?
是的,結果只返回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
}
}
}複製代碼
隨機分數會給出使全部商品均勻分佈的隨機干擾。得分很小,這樣最受歡迎的推薦商品排名就不會降低。
好處在於用戶將瀏覽時沒必要滾動到第二或第三頁,只須要點擊瀏覽器上的刷新按鈕,就會獲得新的內容。
這很神奇。
推薦閱讀專題
留言 點贊 關注
咱們一塊兒分享AI學習與發展的乾貨
歡迎關注全平臺AI垂類自媒體 「讀芯術」
(添加小編微信:dxsxbb,加入讀者圈,一塊兒討論最新鮮的人工智能科技哦~)