足够详细、足够简单的 Python 版推荐系统入门级—理论篇(下)|8月更文挑战

693 阅读5分钟

代码实现

import numpy as np
import pandas as pd

我们将研究 MovieLens 数据集,基于并创建模型用于向用户推荐电影。这个数据是由明尼苏达大学的 GroupLens 研究项目收集的。该数据集可以从这里下载。在这个数据集包括了来自 943 个用户对 1682 部电影的 100,000 个评分(1-5)。用户的信息包括年龄、性别、职业等信息。

导入数据集

首先,我们将导入我们的标准库并在 Python 中读取数据集。这个数据集包含1682部电影的属性。其中有 24 列,最后 19 列指定了某部电影的类型。这些都是二进制的列,也就是说,数值为 1 表示电影属于该类型,否则为 0。

数据集已经被 GroupLens 分为训练数据集和测试数据集两部分,其中测试数据数据集,对每个用户有10个评分,即总共有9,430行。我们将把这两个文件读入我们的Python环境。

r_cols = ['user_id','movie_id','rating','unix_timestamp']
ratings_train = pd.read_csv('data/ml-100k/ua.base',sep='\t',names=r_cols,encoding='latin-1')
ratings_train.head()
user_id movie_id rating unix_timestamp
0 1 1 5 874965758
1 1 2 3 876893171
2 1 3 4 878542960
3 1 4 3 876893119
4 1 5 3 889751712
ratings_test = pd.read_csv('data/ml-100k/ua.test',sep='\t',names=r_cols,encoding='latin-1')
ratings_test.shape,ratings_train.shape
((9430, 4), (90570, 4))
ratings_test.head()
user_id movie_id rating unix_timestamp
0 1 20 4 887431883
1 1 33 4 878542699
2 1 61 4 878542420
3 1 117 3 874965739
4 1 155 2 878542201

创建协同过滤模型

生成评分矩阵

协同过滤计算都是基于评分矩阵接下来我们就要利用上面 DataFrame 数据来生成而一个 2 维评分矩阵,行是用户列是电影,数值是用户对电影评分,没有评分的用 0 来填充。

rating_df = ratings_train.pivot(index='user_id', columns='movie_id', values='rating').fillna(0)
data_matrix = rating_df.to_numpy()
data_matrix.shape
(943, 1680)
计算用户到用户、项目到项目的相似性

现在计算出相似度,可以使用 sklearn 提供的 pairwise_distance 函数来计算余弦相似度。

from sklearn.metrics.pairwise import pairwise_distances 
user_similarity = pairwise_distances(data_matrix, metric='cosine')
item_similarity = pairwise_distances(data_matrix.T, metric='cosine')
实现预测方法

这就给了我们数组形式的项目-项目和用户-用户的相似性。下一步是根据这些相似性来进行预测。让我们定义一个函数来做这件事。

def predict(ratings, similarity, type='user'):
    if type == 'user':
        mean_user_rating = ratings.mean(axis=1)
        #We use np.newaxis so that mean_user_rating has same format as ratings
        ratings_diff = (ratings - mean_user_rating[:, np.newaxis])
        pred = mean_user_rating[:, np.newaxis] + similarity.dot(ratings_diff) / np.array([np.abs(similarity).sum(axis=1)]).T
    elif type == 'item':
        pred = ratings.dot(similarity) / np.array([np.abs(similarity).sum(axis=1)])
    return pred
user_prediction = predict(data_matrix, user_similarity, type='user')
item_prediction = predict(data_matrix, item_similarity, type='item')
user_prediction
array([[ 1.81349209,  0.70700463,  0.61698708, ...,  0.39276591,         0.39226752,  0.39200766],
       [ 1.49898583,  0.34098239,  0.18310294, ..., -0.08555358,        -0.08404725, -0.08377072],
       [ 1.51740786,  0.29296796,  0.15029285, ..., -0.12609608,        -0.12431009, -0.12410346],
       ...,
       [ 1.36707183,  0.23452991,  0.09185339, ..., -0.17162167,        -0.17056838, -0.17063272],
       [ 1.54450965,  0.36817316,  0.25677137, ..., -0.01115452,        -0.01046112, -0.01008175],
       [ 1.59370125,  0.45494826,  0.37321426, ...,  0.14561441,         0.14514214,  0.14528961]])

矩阵分解(MF)

接下来通过一个例子来理解矩阵分解。考虑一个由不同用户给不同电影的用户-电影评分矩阵。因式分解我们大家都并不陌生,在初中时候我接触过,

x21=0(x1)(x+1)=0x^2 -1 =0 \rightarrow (x-1)(x+1) = 0

因式分解的目的就是便于简化

这里评分矩阵是一个稀疏的矩阵,所谓稀疏矩阵就是矩阵上大部分位置都是 0 ,其实这是因为通常用户只会看到一小部分的电影。通过矩阵分解将这个稀疏矩阵为 0 位置的值进行预测,来填充那些缺失的评分。使用矩阵分解法,可以找到一些潜在的特征,我们通常因为这个潜在的特征存在于潜在空间,用户和项目都可以通过隐含特征来共存在同一个潜在空间,他们具有了共同的隐含特征,方便我们来计算他们之间相似性。基于这些潜在的特征可以推测用户如何对电影进行评分。分解后的矩阵相乘需要可以还原原有矩阵。

在评分矩阵中,行是一个一个用户通过 User Id 来区分不同用户,而项目是列,通过 Item Id 来区分不同项目。通过矩阵分解将一个用户-项目的评分矩阵分解为两个矩阵,然后通过这两个矩阵可以返回一个矩阵,这样做的好处,得到矩阵的用户和项目交叉的位置会有数值,也就是

通常潜在特征 k 维度要远远小于用户维度 M 或者商品维度 N,可以把我们的评级矩阵R(M×N)R_{(M \times N)}分成 PM×KP_{M \times K}Q(N×K)Q_{(N \times K)},这样PxQTP x Q^T(这里QTQ^T是 Q 矩阵的转置)就接近于R矩阵。

R=PΣQTR = P \Sigma Q^T
  • M 用户的数量
  • N 项目(电影)的数量
  • K 隐含空间特征数量
  • RM×NR_{M \times N} 用户-电影的评分矩阵
  • PM×KP_{M \times K} 用户特征矩阵表示每个用户隐含特征
  • QN×KQ_{N \times K} 项目特征矩阵表示每个项目隐含特征
  • Σk×k\Sigma_{k \times k}对角特征权重矩阵表示特征

通过矩阵分解可以消除数据中的噪音。通常会移除哪些那些和用户评价电影关系不大的特征,其实也就是将现在用户对物品的评分数值(标量)分解为两个向量,这两个向量分别是用户pukp_{uk} 和项目的向量qikq_{ik}。可以计算两个向量的点积就是用户对项目的具体评分值。

rui=k=1kpukσqikr_{ui} = \sum_{k=1}^k p_{uk}\sigma q_{ik}

当一个新的用户进入系统,并且给电影进行评分,那么这些新增用户行为数据如何添加到这个矩阵来更新既有的矩阵。当新增用户都某一个商品进行评价,这是对角线矩阵 Σ\Sigma 不会发生变化,项目-特征矩阵,唯一变换的是用户-特征的变换。

R=PΣQTRQ=PΣQTQQTQ=1RQ=PΣRQΣ1=PR = P\Sigma Q^T\\ RQ = P\Sigma Q^TQ\\ Q^TQ = 1\\ RQ=P\Sigma\\ RQ\Sigma^{-1}=P
  • 在等号两次都乘以矩阵 QQ
  • 因为 QQ 矩阵是正交矩阵,得到 QTQ=1Q^TQ = 1 所以有 RQ=PΣRQ = P\Sigma
  • 从而得出 $$

所以当一个矩阵 Q 发生变化了,我们就可以通过上面公式来计算出 P 矩阵。同理在 P 矩阵发生变换了同时可以更新 Q 矩阵。还有就是将R矩阵分解为P和Q,我们还需要让 P 和 Q 矩阵相乘后形成矩阵越接近 R 矩阵就越好。可以使用梯度下降算法来做这件事。目标函数最小化实际评分和和 P和Q 估计的评分之间的平方误差

eui2=(ruir^ui)2=(ruik=1Kpukσkqki)2e_{ui}^2 = (r_{ui} - \hat{r}_{ui})^2 = (r_{ui} - \sum_{k=1}^K p_{uk}\sigma_k q_{ki})^2
  • euie_{ui} 是误差值,u 表示用户下标而 i 表示物品的下标
  • ruir_{ui} 是实际 u 用户对 i 项目的评分
  • r^ui\hat{r}_{ui} 是通过矩阵分解对 u 用户对 i 商品的预测值
(eui2)puk=2(ruir^ui)qki=2euiqki(eui2)qki=2(ruir^ui)puk=2euipuk\frac{\partial (e_{ui}^2)}{\partial p_{uk}} = -2(r_{ui} - \hat{r}_{ui})q_{ki} = -2e_{ui}q_{ki}\\ \frac{\partial (e_{ui}^2)}{\partial q_{ki}} = -2(r_{ui} - \hat{r}_{ui})p_{uk} = -2e_{ui}p_{uk}\\

有了损失函数,我们就可以用梯度下降来更新qkiq_{ki}pukp_{uk}

puk=puk=a(eui2)puk=puk+2euiqkiqki=qki=a(eui2)qki=puk+2euipukp^{\prime}_{uk} = p_{uk} = a^*\frac{\partial (e_{ui}^2)}{\partial p_{uk}} = p_{uk} + 2 e_{ui}q_{ki}\\ q^{\prime}_{ki} = q_{ki} = a^*\frac{\partial (e_{ui}^2)}{\partial q_{ki}} = p_{uk} + 2 e_{ui}p_{uk}\\

这里 α\alpha 是学习率,用于确定每次更新参数的步伐大小。上述更新可以重复进行,直到误差最小化,来实现训练过程。

class MF():

    
    # 初始化用户-电影评分矩阵,这里隐含特征,以及 alpha 和 beta 参数    
    def __init__(self, R, K, alpha, beta, iterations):
        self.R = R
        self.num_users, self.num_items = R.shape
        self.K = K
        self.alpha = alpha
        self.beta = beta
        self.iterations = iterations

    # 初始化用户-特征和电影-特征矩阵
    def train(self):
        self.P = np.random.normal(scale=1./self.K, size=(self.num_users, self.K))
        self.Q = np.random.normal(scale=1./self.K, size=(self.num_items, self.K))

        # Initializing the bias terms 初始化偏置
        self.b_u = np.zeros(self.num_users)
        self.b_i = np.zeros(self.num_items)
        self.b = np.mean(self.R[np.where(self.R != 0)])

        # List of training samples 
        self.samples = [
            (i, j, self.R[i, j])
            for i in range(self.num_users)
            for j in range(self.num_items)
            if self.R[i, j] > 0
        ]

        # 在指定迭代过程中随机梯度下降
        training_process = []
        for i in range(self.iterations):
            np.random.shuffle(self.samples)
            self.sgd()
            mse = self.mse()
            training_process.append((i, mse))
            if (i+1) % 20 == 0:
                print("Iteration: %d ; error = %.4f" % (i+1, mse))

        return training_process

    # 计算批量的 MSE
    def mse(self):
        xs, ys = self.R.nonzero()
        predicted = self.full_matrix()
        error = 0
        for x, y in zip(xs, ys):
            error += pow(self.R[x, y] - predicted[x, y], 2)
        return np.sqrt(error)

    # 随机梯度下降来对 P 和 Q 矩阵进行优化
    def sgd(self):
        for i, j, r in self.samples:
            prediction = self.get_rating(i, j)
            e = (r - prediction)

            self.b_u[i] += self.alpha * (e - self.beta * self.b_u[i])
            self.b_i[j] += self.alpha * (e - self.beta * self.b_i[j])

            self.P[i, :] += self.alpha * (e * self.Q[j, :] - self.beta * self.P[i,:])
            self.Q[j, :] += self.alpha * (e * self.P[i, :] - self.beta * self.Q[j,:])

    # j 获取 i 用户对 j 电影的评价
    def get_rating(self, i, j):
        prediction = self.b + self.b_u[i] + self.b_i[j] + self.P[i, :].dot(self.Q[j, :].T)
        return prediction

    # 填充用户-电影评分矩阵
    def full_matrix(self):
        return mf.b + mf.b_u[:,np.newaxis] + mf.b_i[np.newaxis:,] + mf.P.dot(mf.Q.T)

评估推荐系统引擎

大家可能更专注模型定义,构建过程和其背后的算法,而对如何评价一个模型的好坏还不太了解,不了解各种评估模型的指标,以及他们都代表什么以及如何使用。对一些机器学习或者深度学习任务,接受任务后设定一个实现指标显得比较重要。

其实要训练出一个好的模型,首先要知道什么样模型才是好的模型。这样就需要通过一些指标来真正反映模型好坏。也是今天重点的内容。

真实值\预测值TF
TTPFN
FFPTN
  • TP(True Positive) 真正类 测试集真实标签为 T, 预测值也为 T的总数
  • FN(False Negative) 漏报 测试集真实标签为 F,测试集却为 F 的总数
  • FP(False Positive) 误报 测试集真实标签为 F,测试集却为 T 的总数
  • TN(True Negative) 真负类

准确率

准确率(Accuracy):所有正确分类的样本与总样本数比例 准确度是正确预测和总数的比值,从混淆矩阵中,TP 和 TN 之和就是正确的预测数。 Acc=NpredNtotalAcc = \frac{N_{pred} }{N_{total}}

Npred=TP+TNN_{pred} = TP + TN Ntotal=TP+TN+FP+FNN_{total} = TP + TN + FP + FN

下面的精准度和召回率有点绕,但是并不难只要大家留心然后在自己做点练习就能够很好理解和运用这两个指标来衡量模型。

精准度(Precision)

精准度(Precision):就是我们预测为正样本中有多少是正确的概率 正确类数和真正类数与漏报数之和的比值,

Precision=TPTP+FPPrecision = \frac{TP}{TP + FP}

召回率(recall)

也叫查全率,反映正样本被预测为正的比例。

Recall=TPTP+FNRecall = \frac{TP}{TP+FN}

召回率体现了分类模型H对正样本的识别能力,recall 越高,说明模型对正样本的识别能力越强。

假设有 20 任务其中 10 个被按时完成,10 个逾期完成

  • 第 1 种
预测完成任务预测逾期完成任务
实际按时完成任务28
实际逾期完成任务010
名称
准确率(2+10)/20 = 0.6
精准度2/(2+0) = 1
召回率2/(2+8) = 0.2
  • 第 2 种预测情况
预测完成任务预测逾期完成任务
实际按时完成任务100
实际逾期完成任务100
名称
准确率10/20 = 0.5
精准率10/(10+10) = 0.5
召回率10/(2+8) = 1

我们看到虽然召回率是 100% 但是精准率确很低只有 50% 也就是我们在做题时候全部孤注一掷压一个选择。

Recall=tptp+fnRecall = \frac{tp}{tp + fn}