elasticsearch演算法之推薦系統的相似度演算法(一)

無風聽海發表於2022-01-27

一、推薦系統簡介

推薦系統主要基於對使用者歷史的行為資料分析處理,尋找得到使用者可能感興趣的內容,從而實現主動向使用者推薦其可能感興趣的內容;

從物品的長尾理論來看,推薦系統通過發掘使用者的行為,找到使用者的個性化需求,從而將長尾商品準確地推薦給需要它的使用者,幫助使用者發現那些他們感興趣但很難發現的商品。

推薦系統使用的是基於鄰域的演算法,一類是基於使用者的協同過濾演算法,另一類是基於物品的協同過濾演算法;

二、資料集準備

我們採用GroupLens提供的MovieLens資料集

These files contain 1,000,209 anonymous ratings of approximately 3,900 movies 
made by 6,040 MovieLens users who joined MovieLens in 2000.

使用一下code載入評分檔案ml-1m/ratings.dat

def get_ratings():
    ratings = pd.read_csv('ml-1m/ratings.dat',
                          sep='::',
                          names=['UserID', 'MovieID', 'Rating', 'Timestamp'],
                          nrows=100000
                          )

    return ratings

我們可以檢視前五行的資料情況

rating = get_ratings()
print(rating.head())

   UserID  MovieID  Rating  Timestamp
0       1     1193       5  978300760
1       1      661       3  978302109
2       1      914       3  978301968
3       1     3408       4  978300275
4       1     2355       5  978824291

瞭解一下使用者感興趣的電影數量

rating = get_ratings()
plt.hist(rating['UserID'], bins=100, edgecolor='black')
plt.show()

image

三、基於使用者的協同過濾演算法

基於使用者的協同過濾演算法是推薦系統中最古老的演算法,該演算法首先需要找到跟當前使用者興趣相似的使用者,然後將找到的使用者感興趣卻不在當前使用者興趣列表的物品推薦給當前使用者;

基於使用者的協同過濾演算法主要分為兩步

  1. 找到和當前使用者興趣相似的使用者集合;

該演算法基於使用者對物品的歷史的正反饋行為計算使用者興趣相似度;我們給定使用者u、v,令N(u)表示使用者u曾經有過正反饋的物品集合,N(v)表示使用者v曾經有過正反饋的物品集合;我們可以通過餘弦相似度來計算使用者u和v的相似度:

w_{uv} = \frac{N(u) \cap N(v)}{\sqrt {N(u) \cup N(v)}}

例如使用者U對a、b、c有過正反饋記錄,使用者V對a、c有過正反饋記錄;

U  a b c
V  a c

我們利用餘弦相似度可以計算U和V的興趣相似度

\[w_{UV}=\frac{|\{a,b,c\} \cap \{a,c\}|}{\sqrt {|\{a,b,c\}| |\{a,c\}|}} = \frac{2}{\sqrt{6}} \]

我們使用如下的user_similarity來計算使用者相似性

def user_similarity(ratings):
    matrix = []
    rating_groups = ratings.groupby('UserID')
    for u_id, u_ratings in rating_groups:
        row = []
        matrix.append(row)
        u_movieIds = u_ratings['MovieID'].values
        for v_id, v_ratings in rating_groups:
            v_movieIds = v_ratings['MovieID'].values
            u_v_movieIds = np.intersect1d(u_movieIds, v_movieIds)
            similarity = len(u_v_movieIds)/math.sqrt(len(u_movieIds) * len(v_movieIds))
            row.append(similarity)

    result = pd.DataFrame(matrix, columns= rating_groups.groups.keys(), index=rating_groups.groups.keys())
    return result


rating = get_ratings()
similarity_matrix = user_similarity(rating)
print(similarity_matrix.head(10))

         1         2         3    ...       667       668       669
1   1.000000  0.084657  0.115406  ...  0.010504  0.068680  0.076194
2   0.084657  1.000000  0.147945  ...  0.087529  0.161416  0.048839
3   0.115406  0.147945  1.000000  ...  0.085666  0.070014  0.077674
4   0.119898  0.153704  0.152783  ...  0.083438  0.036370  0.000000
5   0.097618  0.125142  0.059708  ...  0.119562  0.142134  0.059131
6   0.163017  0.114939  0.099710  ...  0.063529  0.000000  0.032915
7   0.049341  0.284641  0.150899  ...  0.164817  0.179605  0.099627
8   0.116508  0.201633  0.083139  ...  0.090808  0.113092  0.023525
9   0.200125  0.162482  0.122407  ...  0.118842  0.178069  0.053877
10  0.240081  0.215441  0.216773  ...  0.126021  0.083229  0.096951

[10 rows x 669 columns]

在以上程式碼中直接遍歷所有的使用者進行計算相似性,很多時候由於物品的數量比較多或者每個使用者的興趣關注點比較少,這會導致大量使用者並不存在所謂的並集;我們可以先將資料結構反轉為產品使用者,然後計算不同使用者感興趣的產品總數和相關使用者之間感興趣的產品交集,最後再進行餘弦相似性計算;

import math
import numpy as np
import pandas as pd


def change_user_ratings(rating):
    grouped = rating.groupby('MovieID')
    result = {}
    for movieId,m_rating in grouped:
        result[movieId] = m_rating['UserID'].values

    df = pd.DataFrame({
        'MovieID': result.keys(),
        'UserIDs': result.values()
    })

    return df.set_index(df.columns.values[0])



def cal_count(product_users):
    user_counts = {}
    rel_user_counts = {}
    for movieId, row in product_users.iterrows():
        userIds = row['UserIDs']
        for uid in userIds:
            if uid not in user_counts:
                user_counts[uid] = 0
            user_counts[uid] += 1
            for vid in userIds:
                if (uid, vid) not in rel_user_counts:
                    rel_user_counts[(uid, vid)] = 0
                rel_user_counts[(uid, vid)] += 1



    user_counts = pd.DataFrame({'UserID': user_counts.keys(), 'Movie_Count': user_counts.values()})
    rel_user_counts = pd.DataFrame({'Rel_UserID':rel_user_counts.keys(), 'Movie_Count':rel_user_counts.values()})
    return  user_counts.set_index(user_counts.columns.values[0]), rel_user_counts.set_index(rel_user_counts.columns.values[0])




def cosin_similarity(user_counts, rel_user_counts):
    result = []
    for u, u_row in user_counts.iterrows():
        row = []
        result.append(row)
        u_count = u_row['Movie_Count']
        for v, v_row in user_counts.iterrows():
            v_count = v_row['Movie_Count']
            if rel_user_counts.index.isin([(u,v)]).any():
                count = rel_user_counts.at[(u,v), 'Movie_Count']
                row.append(count/math.sqrt(u_count * v_count))
            else:
                row.append(0)

    return pd.DataFrame(result, index=user_counts.index.values,  columns=user_counts.index.values)




def user_similarity(ratings):
    rating_users = change_user_ratings(ratings)
    user_counts, rel_user_counts = cal_count(rating_users)
    s = cosin_similarity(user_counts, rel_user_counts)
    return s


ratings = get_ratings()
similarity_matrix = user_similarity(ratings)
print(similarity_matrix.head(10))

         1         2         3    ...       667       668       669
1   1.000000  0.084657  0.115406  ...  0.010504  0.068680  0.076194
2   0.084657  1.000000  0.147945  ...  0.087529  0.161416  0.048839
3   0.115406  0.147945  1.000000  ...  0.085666  0.070014  0.077674
4   0.119898  0.153704  0.152783  ...  0.083438  0.036370  0.000000
5   0.097618  0.125142  0.059708  ...  0.119562  0.142134  0.059131
6   0.163017  0.114939  0.099710  ...  0.063529  0.000000  0.032915
7   0.049341  0.284641  0.150899  ...  0.164817  0.179605  0.099627
8   0.116508  0.201633  0.083139  ...  0.090808  0.113092  0.023525
9   0.200125  0.162482  0.122407  ...  0.118842  0.178069  0.053877
10  0.240081  0.215441  0.216773  ...  0.126021  0.083229  0.096951

[10 rows x 669 columns]

  1. 找到這個使用者集合感興趣的且當前使用者沒有聽說過的物品推推薦給他;

通過以上計算我們得到了使用者相似性的矩陣,接下來我們需要找到跟目標使用者興趣最相似的K個使用者,然後將只有這K個使用者喜歡的物品推薦給目標使用者;這裡我們需要進一步度量目標使用者對特定產品的感興趣程度

在這個公式中,S(u,K)包含和使用者u興趣最接近的K個使用者,N(i)是對物品i有過行為的使用者集合,Wuv 是使用者u和使用者v的興趣相似度,rvi 代表使用者v對物品i的興趣,這裡使用評分除以5來計算。

\[p(u,i) = \sum_{v \in s(u,K) \cap N(i)} w_{uv} r_{vi} \]

我們使用以下的recommend方法實現這個推薦

def recommend(ratings,u, matrix, k):
    result = pd.Series(dtype='float64');
    grouped = dict(list(ratings.groupby('UserID')))
    u_ratings = grouped[u][['MovieID','Rating']]
    row = matrix.loc[u].sort_values(ascending=False)
    for v in row.index:
        if u != v:
            similarity = row.loc[v]
            v_ratings = grouped[v][['MovieID','Rating']]
            diff = pd.concat([v_ratings, u_ratings, u_ratings]).drop_duplicates(subset=pd.Index(['MovieID']), keep=False)
            for movieId, s_rating in diff.set_index('MovieID').iterrows():
                like = similarity * (s_rating['Rating']/5)
                s_movieId = str(movieId)
                if movieId in result:
                    result[s_movieId] += like
                else:
                    result[s_movieId] = like

    return result.sort_values(ascending=False).head(k)
    

我們計算推薦給使用者A且其最感興趣的前三件商品;從計算結果可以看到是商品c和e;

ratings = get_ratings()
similarity_matrix = user_similarity(ratings)
recommend_movies = recommend(ratings, 1, similarity_matrix, 10)
print(recommend_movies.head(10))

2049    0.240081
3292    0.212965
1067    0.204131
2559    0.193922
3620    0.168068
963     0.168068
2179    0.165928
2211    0.165928
1817    0.165928
2227    0.165928
dtype: float64

注:由於rating.data的資料實際情況,每個使用者以及每個電影都會有對應的評分,所以第二種演算法並不具有什麼效能優勢,需要根據自己資料的實際情況進行選擇;

相關文章