C-02 推薦系統

更新、更全的《機器學習》的更新網站,更有python、go、數據結構與算法、爬蟲、人工智能教學等着你:http://www.javashuo.com/article/p-vozphyqp-cm.htmlpython

推薦系統

目前推薦系統被應用於各個領域,例如淘寶的商品推薦、b站的視頻推薦、網易雲音樂的每日推薦等等,這些都是基於用於往日在平臺的行爲模式給用戶推薦他們可能喜歡的商品、視頻、音樂。算法

下面咱們將以電影推薦系統舉例,一步一步經過Python實現一個簡單的電影推薦系統。數組

因爲數據量的緣由,咱們可能沒法作到精度較高的推薦系統,可是作一個差很少能實現推薦功能的電影推薦系統是徹底沒有問題的。數據結構

1、導入模塊

import io
import os
import sys
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
from sklearn.impute import SimpleImputer
from sklearn.metrics.pairwise import cosine_similarity
%matplotlib inline
font = FontProperties(fname='/Library/Fonts/Heiti.ttc')

2、收集數據

早期講構造機器學習系統的時候說到,咱們第一步每每是須要收集數據。app

因爲本次要作的推薦系統是和電影有關的,而爲了給用戶推薦他喜歡的電影,通常會收集每一個用戶對本身看過的電影的評分。這裏咱們假設用戶看完電影必定會給電影評分,而且評分範圍爲\(\{1,2,3,4,5\}\),即評分最低爲1分,最高爲5分。若是用戶沒有評分則意味着用戶沒有看過該電影。dom

若是是大型的推薦系統,每每須要經過不少途徑得到各類數據,如爬蟲、平臺合做,而且可能還會考慮用戶自己的各類信息,如身高、體重、年齡、興趣愛好……和電影的各類信息,如篇名、導演、演員陣容、上映時間……,一般這種大型系統針對的數據維度每每都是上萬,上十萬的。因爲數據限制,所以這裏只假設用戶喜歡看的電影和他對電影的評分有關。機器學習

因爲這是一個簡單的推薦系統版本,因此咱們假設咱們已經獲取了用戶對本身看過的電影的評分,即數據在movie.xlsx表格中。學習

# 收集數據
# 沒有表格文件時自定義數據
csv_data = '''
《肖申克的救贖》,《控方證人》,《這個殺手不太冷》,《霸王別姬》,《美麗人生》,《阿甘正傳》,《辛德勒的名單》,姓名
,4.0,,4.0,,5.0,,'劉一'
4.0,,5.0,3.0,5.0,,,'陳二'
3.0,4.0,,3.0,2.0,3.0,3.0,'張三'
2.0,3.0,,3.0,,,,'李四'
3.0,4.0,,5.0,3.0,3.0,,'王五'
,,4.0,,4.0,2.0,,'趙六'
3.0,,1.0,5.0,3.0,3.0,2.0,'孫七'
2.0,,2.0,,1.0,,,'周八'
1.0,2.0,,,,2.0,,'吳九'
,5.0,,4.0,,3.0,3.0,'鄭十'
'''
if not os.path.exists('datasets/movie.xlsx'):
    # 將文件讀入內存
    csv_data = io.StringIO(csv_data)
    df = pd.read_csv(csv_data, header=0)
    df.index = df['姓名'].tolist()
else:
    # 從表格中獲取數據
    df = pd.read_excel('datasets/movie.xlsx', header=0)
    df.index = df['姓名'].tolist()
df
《肖申克的救贖》 《控方證人》 《這個殺手不太冷》 《霸王別姬》 《美麗人生》 《阿甘正傳》 《辛德勒的名單》 姓名
劉一 NaN 4.0 NaN 4.0 NaN 5.0 NaN 劉一
陳二 4.0 NaN 5.0 3.0 5.0 NaN NaN 陳二
張三 3.0 4.0 NaN 3.0 2.0 3.0 3.0 張三
李四 2.0 3.0 NaN 3.0 3.0 2.0 NaN 李四
王五 3.0 4.0 NaN 5.0 3.0 3.0 NaN 王五
趙六 NaN NaN 4.0 NaN 4.0 2.0 NaN 趙六
孫七 3.0 NaN 1.0 5.0 3.0 3.0 2.0 孫七
周八 2.0 NaN 2.0 NaN 1.0 NaN 2.0 周八
吳九 1.0 2.0 2.0 NaN 1.0 1.0 NaN 吳九
鄭十 NaN 5.0 NaN 4.0 NaN 3.0 3.0 鄭十

3、數據預處理

3.1 無評分電影處理

從上述表格中能夠看到不少用戶有些電影是沒有看過的,這至關於數據裏的缺失值,《數據預處理》那一篇詳細講到了缺失值的處理,這裏再也不贅述,在此處可能使用中位數、平均數、衆數恐怕都不行。測試

這種狀況的缺失值爲用戶沒有看過此電影,而咱們之後推薦電影也定是推薦用戶沒有看過的電影,若是你把他沒有看過的電影就這樣輕易只依據你們對電影的平均評分,你就給他推薦電影,這可能並不合理。所以我在這裏統一用\(0\)替換全部的沒有打分的電影,即\(0\)表示用戶沒有看過這部電影,而後使用其餘算法給用戶推薦電影。

# 數據缺失值處理
def imputer_data(np_arr):
    # 中位數strategy=median,平均數strategy=mean,衆數strategy=most_frequent
    imputer = SimpleImputer(missing_values=np.nan,
                            strategy='constant', fill_value=0)
    # 找到用戶沒有評分的電影
    imr = imputer.fit(np_arr.iloc[:, :-1])
    # 將用戶沒有評分的電影評分用0填充
    imputed_df = imr.transform(np_arr.iloc[:, :-1])

    return imputed_df


imputed_df = imputer_data(df)
imputed_df
array([[0., 4., 0., 4., 0., 5., 0.],
       [4., 0., 5., 3., 5., 0., 0.],
       [3., 4., 0., 3., 2., 3., 3.],
       [2., 3., 0., 3., 3., 2., 0.],
       [3., 4., 0., 5., 3., 3., 0.],
       [0., 0., 4., 0., 4., 2., 0.],
       [3., 0., 1., 5., 3., 3., 2.],
       [2., 0., 2., 0., 1., 0., 2.],
       [1., 2., 2., 0., 1., 1., 0.],
       [0., 5., 0., 4., 0., 3., 3.]])

獲取了數據並對數據進行了預處理,接下來就是從數據中獲得咱們想要的信息,然而咱們想要獲取什麼信息呢?

通常咱們想要獲取兩個用戶,或兩個電影之間的類似度,如下兩點將是咱們要從數據中得到的信息。

  1. 獲取用戶之間的類似度,而後基於其餘用戶算出沒看過電影的用戶可能會給電影的評分。
  2. 獲取類似度最高的電影,而後基於用戶看過的電影算出用戶可能會給電影的評分。

經過以上兩種方式,設定一個評分閾值,若是用戶可能會給該電影的評分大於該閾值,則能夠放心的給用戶推薦電影,反之則不推薦給用戶。

4、協同過濾算法-基於用戶的推薦

若是咱們想要獲取用戶user_1和用戶user_2之間的類似度時,能夠考慮使用餘弦類似度來評估二者之間的類似度。餘弦類似度的取值範圍爲\((-1,1)\),餘弦類似度越大,則用戶user_1與用戶user_2之間的類似度越大,反之則二者之間的類似度越小。

4.1 餘弦類似度

餘弦類似度:
\[ w_{uv}=\frac{|N(u)\bigcap{N(v)}|}{\sqrt{|N(u)||{N(v)}|}} \]

# 數據標準化前用戶之間的類似度
user_1 = imputed_df[[0]]
user_2 = imputed_df[[1]]
user_8 = imputed_df[[7]]
user_9 = imputed_df[[8]]
print('電影名:{}'.format(df.columns[:-1].values))
print('劉一:{}'.format(user_1))
print('陳二:{}'.format(user_2))
print('周八:{}'.format(user_8))
print('吳九:{}'.format(user_9))

print('劉一和陳二的餘弦類似度:{}'.format(cosine_similarity(user_1, user_2)))
print('陳二和周八的餘弦類似度:{}'.format(cosine_similarity(user_2, user_8)))
print('周八和吳九的餘弦類似度:{}'.format(cosine_similarity(user_8, user_9)))
電影名:['《肖申克的救贖》' '《控方證人》' '《這個殺手不太冷》' '《霸王別姬》' '《美麗人生》' '《阿甘正傳》' '《辛德勒的名單》']
劉一:[[0. 4. 0. 4. 0. 5. 0.]]
陳二:[[4. 0. 5. 3. 5. 0. 0.]]
周八:[[2. 0. 2. 0. 1. 0. 2.]]
吳九:[[1. 2. 2. 0. 1. 1. 0.]]
劉一和陳二的餘弦類似度:[[0.18353259]]
陳二和周八的餘弦類似度:[[0.73658951]]
周八和吳九的餘弦類似度:[[0.58536941]]

從上面的輸出的結果能夠看到

  1. 劉一和陳二隻對電影《霸王別姬》都共同評分過,其餘的都沒有共同評分過,兩我的之間有很低的類似度是很正常的。
  2. 陳二和周八雖然對不少電影都共同評分過,可是他們的評分中陳二的評分都是偏高的,而周八都是偏低的,可是二者之間卻有着較高的類似度,這明顯是不合理的。
  3. 周八和吳九對不少電影都共同評分過,而且兩我的的評分都較偏低,可是應該遠沒有達到\(0.70\)的類似度,通常\(0.70\)的類似度是極高的。

對於上述狀況是由於評分爲\(0\)在餘弦類似度看來是有意義的,也就是較之評分爲\(1\)而言評分爲\(0\)更差。即餘弦類似度認爲兩我的對某一部電影評分都是\(0\)的話,那兩我的給電影評分都很低,二者天然會有很高的類似性,這也是爲何劉一和陳二的類似度很低,而陳二和周八類似度及周八和吳九的類似度都很高的緣由。

4.2 數據標準化處理

上一節講到了餘弦類似度可能會把電影評分爲\(0\)較於評分\(1\)認爲用戶可能討厭某部電影,即把\(0\)變得有意義。然而這部電影評分爲\(0\)即用戶沒有看過此電影,它並沒有實際意義,即在餘弦類似度中的\(0\)是應該是中性,也就說不能經過用戶對電影的評分爲\(0\)就所以判斷用戶喜不喜歡這部電影,所以咱們須要對數據作標準化處理。

首先考慮的多是熱編碼處理,它可以讓數據都變成中性,可是熱編碼以後,用戶對其餘電影的評分可能也變得沒有意義。而其餘的標準化處理,如歸一化是爲了作統一尺度處理。所以咱們能夠自定義一個標準化的方式。

該自定義的標準化方式是從新生成全部用戶的評分,使得用戶對全部電影的平均評分爲\(0\),這樣在用戶把用戶沒有打分的電影設爲\(0\)的時候這個\(0\)就成了一箇中性值,即\(0\)在餘弦類似度看來是中性的。而使得用戶的平均評分爲\(0\)的方法也很簡單,能夠對某個用戶的非零評分的每個評分值使用以下公式
\[ s_{std}^{(i)} = x^{(i)}-\mu \]
其中\(x^{(i)}\)表示用戶對某部電影的評分,\(\mu\)表示用戶對全部電影評分的平均評分。

# 標準化評分
def nonzero_mean(np_arr):
    """計算矩陣內每一行不爲0元素的平均數"""
    # 找到數組內評分不爲0的即非0元素
    exist = (np_arr != 0)
    # 行非0元素總和
    arr_sum = np_arr.sum(axis=1)
    # 行非0元素總個數
    arr_num = exist.sum(axis=1)

    return arr_sum/arr_num


def standard_data(np_arr):
    standardized_df = np_arr.copy()
    # 非0元素行下標
    nonzero_rows = np.nonzero(np_arr)[0]
    # 非0元素列下標
    nonzero_columns = np.nonzero(np_arr)[1]
    # 非0元素行平均值
    nonzero_rows_mean = nonzero_mean(np_arr)
    # 遍歷並修改全部非0元素
    for ind in range(len(nonzero_rows)):
        # 第ind個元素的行標和列標肯定一個元素
        i = nonzero_rows[ind]
        j = nonzero_columns[ind]
        standardized_df[i, j] = round(
            np_arr[i, j]-nonzero_rows_mean[i], 2)

    return standardized_df


standardized_df = standard_data(imputed_df)
# 數據標準化後用戶之間的類似度
user_1 = standardized_df[[0]]
user_2 = standardized_df[[1]]
user_8 = standardized_df[[7]]
user_9 = standardized_df[[8]]
print('電影名:{}'.format(df.columns[:-1].values))
print('劉一:{}'.format(user_1))
print('陳二:{}'.format(user_2))
print('周八:{}'.format(user_8))
print('吳九:{}'.format(user_9))

print('劉一和陳二的餘弦類似度:{}'.format(cosine_similarity(user_1, user_2)))
print('陳二和周八的餘弦類似度:{}'.format(cosine_similarity(user_2, user_8)))
print('周八和吳九的餘弦類似度:{}'.format(cosine_similarity(user_8, user_9)))
電影名:['《肖申克的救贖》' '《控方證人》' '《這個殺手不太冷》' '《霸王別姬》' '《美麗人生》' '《阿甘正傳》' '《辛德勒的名單》']
劉一:[[ 0.   -0.33  0.   -0.33  0.    0.67  0.  ]]
陳二:[[-0.25  0.    0.75 -1.25  0.75  0.    0.  ]]
周八:[[ 0.25  0.    0.25  0.   -0.75  0.    0.25]]
吳九:[[-0.4  0.6  0.6  0.  -0.4 -0.4  0. ]]
劉一和陳二的餘弦類似度:[[0.30464382]]
陳二和周八的餘弦類似度:[[-0.3046359]]
周八和吳九的餘弦類似度:[[0.36893239]]

從上面輸出的結果能夠看到劉一和陳二的類似度提升了,而且陳二和周八的類似度明顯下降了,這是符合咱們心理預期的,因爲\(0\)變成了中性,周八和吳九的類似度也有着大幅的下降。

5、預測

上述過程其實咱們已經自定義了一個模型,只是這個模型可能並無用到咱們以前學習的傳統機器學習算法,他用到的是另外一種算法,即一個推薦算法——《協同過濾算法》。

如今咱們能夠測試咱們的模型,固然因爲沒有真實數據,只能預測某個新用戶對某一部他沒看過的電影給多少評分。如今假設咱們從某個不知名網站弄來了王二麻子和爛穀子對電影的評分,咱們能夠作一個模型基於上述十我的對電影的評分預測他們會給他們沒看過的電影評多少分。

該模型主要是獲取某我的如王二麻子觀看了全部的電影的評分,而後計算王二麻子與其餘全部人之間的餘弦類似度,以後經過王二麻子與其餘用戶的類似度\(*\)類似度對應用戶對王二麻子未看電影的評分\(/\)王二麻子與其餘用戶的總類似度。假設王二麻子與張三的類似度爲\(0.2\),與李四的類似度爲\(0.6\),王二麻子沒看的電影爲《霸王別姬》,而張三對《霸王別姬》的評分爲\(5\),李四對《霸王別姬》的評分爲\(2\),則王二麻子對《霸王別姬》的加權評分爲
\[ {\frac{0.2*5+0.6*2}{0.2+0.6}}=2.75 \]

# 預測
# 對新數據處理成np.array數組
new_user = '''
1,2,,2,2,2,,王二麻子
2,1,2,,5,1,4,爛穀子
1,2,2,2,3,,4,大芝麻
'''


def predict(new_user, similarity_tool='cosine_similarity'):
    rating_list = []
    movie_list = []

    # 對於輸入數據爲或str和numpy數組作不一樣的處理
    if isinstance(new_user, str):
        new_df = pd.read_csv(io.StringIO(new_user), header=None)
    else:
        new_df = pd.DataFrame(new_user)

    new_df.columns = ['《肖申克的救贖》', '《控方證人》', '《這個殺手不太冷》',
                      '《霸王別姬》', '《美麗人生》', '《阿甘正傳》', '《辛德勒的名單》', '姓名']

    # 填充數據並對數據進行標準化
    imputed_new_df = imputer_data(new_df)
    standardized_new_user = standard_data(imputed_new_df)

    # 經過餘弦類似度計算預測用戶與已有樣本之間的類似度
    user_similarity_list = []
    for ind in range(len(standardized_df)):
        user = standardized_df[[ind]]
        mod = sys.modules['__main__']
        file = getattr(mod, similarity_tool, None)
        user_similarity_list.append(cosine_similarity(
            user, standardized_new_user)[0])

    # 將餘弦類似度列表構形成numpy數組方便計算
    users_similarity_arr = np.array(user_similarity_list).reshape(
        standardized_df.shape[0], standardized_new_user.shape[0])

    # 遍歷全部用戶對她沒有評分的電影計算預測評分值
    for ind in range(len(new_df)):
        empty_rating_ind = []
        # 獲取名字信息
        name = new_df['姓名'][ind]
        nonzero_list = np.nonzero(imputed_new_df[[ind], :])[1]
        user_similarity_arr = users_similarity_arr[:, [ind]].reshape(1, -1)

        # 找到預測用戶沒有評分電影的索引
        for j in range(standardized_new_user.shape[1]):
            if j not in nonzero_list:
                empty_rating_ind.append(j)

        # 遍歷預測用戶沒有評分的電影計算預測評分值
        for rating_ind in empty_rating_ind:
            # 計算用戶的加權評分總和
            user_rating_list = imputed_df[:, rating_ind].reshape(1, -1)
            rating_arr = user_similarity_arr*user_rating_list
            rating_sum = rating_arr.sum(axis=1)
            # 計算用戶的總類似度
            user_similarity_sum = user_similarity_arr.sum(axis=1)

            # 當用戶總類似度爲0時打印提示消息
            if rating_sum == 0:
                print('親,{}不怎麼看電影我實在無能爲力!'.format(name))
            else:
                rating = rating_sum/user_similarity_sum
                print('*{}*可能會給電影{}評分{}'.format(name,
                                                df.columns[:-1].values[rating_ind], round(rating[0], 2)))
            # 統計評分
            rating_list.append(round(rating[0], 2))

        empty_rating_ind = []

    return rating_list


rating_list = predict(new_user)
*王二麻子*可能會給電影《這個殺手不太冷》評分-0.21
*王二麻子*可能會給電影《辛德勒的名單》評分-0.31
*爛穀子*可能會給電影《霸王別姬》評分2.79
*大芝麻*可能會給電影《阿甘正傳》評分3.13

從上面的預測的數據看到,其實預測效果還很不錯。

上面講到了其實還有一種方法,即基於電影(項目)的推薦,即獲取類似度較高的電影。流程爲計算全部被你評分的電影與你未評分的電影之間的餘弦類似度,而後經過與基於用戶推薦相同的方法就能夠實現電影選擇你。實現方法即對上述給出的表格轉置而後修改代碼中的參數便可,此處很少贅述。

6、測試

l = np.random.randint(0, 6, size=(50, 8))
l[:, [-1]] = 0

rating_list = predict(l)
*0*可能會給電影《這個殺手不太冷》評分0.09
*0*可能會給電影《肖申克的救贖》評分2.59
*0*可能會給電影《控方證人》評分1.9
*0*可能會給電影《肖申克的救贖》評分4.62
*0*可能會給電影《控方證人》評分19.66
*0*可能會給電影《肖申克的救贖》評分3.34
*0*可能會給電影《美麗人生》評分1.26
*0*可能會給電影《阿甘正傳》評分9.15
*0*可能會給電影《控方證人》評分0.27
*0*可能會給電影《控方證人》評分2.42
*0*可能會給電影《這個殺手不太冷》評分0.36
*0*可能會給電影《美麗人生》評分3.18
*0*可能會給電影《控方證人》評分0.46
*0*可能會給電影《這個殺手不太冷》評分1.67
*0*可能會給電影《控方證人》評分3.79
*0*可能會給電影《美麗人生》評分2.18
*0*可能會給電影《霸王別姬》評分1.67
*0*可能會給電影《美麗人生》評分2.36
*0*可能會給電影《阿甘正傳》評分-1.8
*0*可能會給電影《這個殺手不太冷》評分1.89
*0*可能會給電影《霸王別姬》評分2.5
*0*可能會給電影《辛德勒的名單》評分0.31
*0*可能會給電影《霸王別姬》評分0.87
*0*可能會給電影《美麗人生》評分2.98
*0*可能會給電影《肖申克的救贖》評分1.95
*0*可能會給電影《美麗人生》評分1.37
*0*可能會給電影《肖申克的救贖》評分-0.26
親,0不怎麼看電影我實在無能爲力!
親,0不怎麼看電影我實在無能爲力!
親,0不怎麼看電影我實在無能爲力!
親,0不怎麼看電影我實在無能爲力!
*0*可能會給電影《肖申克的救贖》評分2.36
*0*可能會給電影《霸王別姬》評分-6.45
*0*可能會給電影《控方證人》評分1.23
*0*可能會給電影《這個殺手不太冷》評分0.41
*0*可能會給電影《霸王別姬》評分-3.71
*0*可能會給電影《美麗人生》評分1.7
*0*可能會給電影《這個殺手不太冷》評分0.8
*0*可能會給電影《美麗人生》評分1.82
*0*可能會給電影《肖申克的救贖》評分2.37
*0*可能會給電影《這個殺手不太冷》評分-0.43
*0*可能會給電影《霸王別姬》評分1.25
*0*可能會給電影《肖申克的救贖》評分1.73
*0*可能會給電影《這個殺手不太冷》評分1.02
*0*可能會給電影《阿甘正傳》評分2.18
*0*可能會給電影《辛德勒的名單》評分-0.66
*0*可能會給電影《美麗人生》評分3.2
*0*可能會給電影《這個殺手不太冷》評分-0.83
*0*可能會給電影《肖申克的救贖》評分2.59
*0*可能會給電影《美麗人生》評分2.14
*0*可能會給電影《辛德勒的名單》評分2.2
*0*可能會給電影《美麗人生》評分2.9
*0*可能會給電影《美麗人生》評分1.86
*0*可能會給電影《阿甘正傳》評分0.82
*0*可能會給電影《美麗人生》評分7.91
*0*可能會給電影《控方證人》評分10.1
*0*可能會給電影《這個殺手不太冷》評分-4.76
*0*可能會給電影《這個殺手不太冷》評分9.9
*0*可能會給電影《美麗人生》評分2.69
*0*可能會給電影《辛德勒的名單》評分2.41
*0*可能會給電影《肖申克的救贖》評分2.07
*0*可能會給電影《辛德勒的名單》評分2.55
*0*可能會給電影《肖申克的救贖》評分1.78
*0*可能會給電影《霸王別姬》評分1.12
*0*可能會給電影《肖申克的救贖》評分1.67
*0*可能會給電影《美麗人生》評分1.59
*0*可能會給電影《肖申克的救贖》評分1.7
*0*可能會給電影《這個殺手不太冷》評分3.54
*0*可能會給電影《美麗人生》評分2.0

測試結果能夠看出模型其實還行,由於評分太高和無評分的現象不多,即符合正態分佈。

相關文章
相關標籤/搜索