1 Star 6 Fork 4

OpenDocCN / apachecn-ml-zh

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
06.md 43.61 KB
一键复制 编辑 原始数据 按行查看 历史
布客飞龙 提交于 2022-01-02 01:27 . 2022-01-02 01:27:33

六、智能推荐系统

随着互联网上大量数字信息的出现,用户高效地获取物品成为一个挑战。推荐系统是信息过滤系统,它处理数字数据过载的问题,根据用户的偏好、兴趣和行为,从以前的活动中推断出项目或信息。

在本章中,我们将涵盖以下主题:

  • 介绍推荐系统
  • 基于潜在因子分解的协同过滤
  • 利用深度学习进行潜在因素协同过滤
  • 使用受限玻尔兹曼机器 ( RBM )构建推荐系统
  • 训练限制性商业惯例的对比分歧
  • 使用 RBMs 的协同过滤
  • 使用 RBMs 实现协同过滤应用程序

技术要求

读者应该具备 Python 3 和人工智能的基本知识,才能完成本章中的项目。

本章代码文件可在 GitHub: https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python/tree/master/chapter 06上找到

查看以下视频,查看正在运行的代码: http://bit.ly/2Sgc0R3

什么是推荐系统?

推荐系统在当今世界无处不在。无论是网飞的电影推荐还是亚马逊的产品推荐,推荐系统都产生了巨大的影响。推荐系统可以大致分为基于内容的过滤系统、协同过滤系统和基于潜在因素的过滤推荐系统。基于内容的过滤依赖于基于项目内容的手工编码特征。基于用户如何对现有项目进行评级,创建用户简档,并将用户提供的排名给予这些项目:

Figure 6.1: Content-based filtering illustration

从上图中我们可以看到(图 6.1 ),用户 A 购买了名为深度学习神经网络的书籍。由于人工智能这本书的内容与这两本书相似,基于内容的推荐系统已经向用户 A 推荐了这本书人工智能。正如我们所看到的,在基于内容的过滤中,根据用户的偏好向用户推荐项目。这不涉及其他用户如何评价这本书。

协同过滤尝试识别属于给定用户的相似用户,然后推荐相似用户喜欢、购买或评价较高的用户项目。这一般称为用户-用户协同过滤。相反,找到与给定项目相似的项目,并向同样喜欢、购买或高度评价其他相似项目的用户推荐项目。这个名字叫做物品-物品协同过滤:

Figure 6.2: item-item collaborative filtering illustration

在上图(图 6.2 )中,用户 A用户 B 的购书品味非常相似。用户 A 最近购买了书籍深度学习神经网络。由于用户 B用户 A 非常相似,因此用户-用户协同推荐系统也将这些书籍推荐给用户 B

基于潜在因子分解的推荐系统

基于潜在因子分解的过滤推荐方法试图通过分解评级来发现代表用户和项目简档的潜在特征。与基于内容的过滤特征不同,这些潜在特征是不可解释的,并且可以表示复杂的特征。例如,在电影推荐系统中,潜在特征之一可能代表幽默、悬疑和浪漫的特定比例的线性组合。通常,对于已经评级的项目,用户 i 对项目 j 给出的评级rijT3】可以表示为。其中 u i 是基于潜在因素的用户简档向量, v i 是基于相同潜在因素的项目向量:

Figure 6.3: Latent factor-based filtering illustration

上图所示(图 6.3 )是一种基于潜在因素的推荐方法,其中评分矩阵Rm x nT5】已经分解为用户简档矩阵Um x kT9】和项目简档矩阵Pn x kT13】的乘积,其中 k 是模型的潜在因素数。基于这些简档,我们可以通过计算用户简档和项目简档的内部产品来推荐到目前为止还没有被用户购买的项目。内部产品给出了用户在购买产品时可能给出的暂定评级。**

创建这些用户和项目简档的方法之一是,在根据用户和项目的某种形式的平均值填写缺失值后,对评分矩阵执行奇异值分解 ( 奇异值分解)。根据奇异值分解,评级矩阵 R 可以分解如下:

我们可以将用户档案矩阵取为 US 1/2 ,然后将物品档案矩阵转置为 S 1/2 V T 形成潜在因素模型。您可能会有一个问题,当分级矩阵中缺少与用户未分级的电影相对应的条目时,如何执行 SVD。常见的方法是在执行 SVD 之前,通过用户的平均评分或全球评分平均值来估算缺失的评分。

潜在因素协同过滤的深度学习

您可以利用深度学习方法来导出给定维度的用户和项目简档向量,而不是使用奇异值分解。

对于每个用户 i、可以通过一个嵌入层定义一个用户向量 u i ∈ R k 。同样,对于每个项目 j、可以通过另一个嵌入层定义一个项目向量vj∈Rk。然后,用户 i 对某个项目 j 的评分rijT19】可以表示为uIT27】和vjT31】的点积,如图所示:**

您可以修改神经网络,为用户和项目添加偏见。假设我们想要 k 潜在组件,那么 m 用户的嵌入矩阵 U 的尺寸将是 m x k 。类似地, n 项的嵌入矩阵 V 的尺寸为 n x k

基于深度学习的潜在因素模型部分,我们将使用这种嵌入方法来创建基于100K Movie Lens数据集的推荐系统。数据集可以从https://grouplens.org/datasets/movielens/下载。

我们将使用u1.base作为训练数据集,u1.test作为保持测试数据集。

基于深度学习的潜在因素模型

潜在因素协同过滤的深度学习部分讨论的基于深度学习的潜在因素模型可以按照图 6.4 所示进行设计:

Figure 6.4: Deep learning-based latent factor model on Movie Lens 100 K dataset

user_IDmovie_ID从它们对应的嵌入矩阵中提取用户和电影嵌入向量。在图中, embedding_1 代表用户标识的嵌入层, embedding_2 代表电影标识的嵌入层。用户嵌入向量和电影嵌入向量的点积在点 _1 层执行,以输出评分分数(一到五)。定义模型的代码如下所示:

def model(max_users,max_movies,latent_factors):
    user_ID = Input(shape=(1,))
    movie_ID = Input(shape=(1,))
    x = Embedding(max_users,latent_factors, input_length=1)(user_ID)
    y = Embedding(max_movies,latent_factors, input_length=1)(movie_ID)
    out = dot([x,y],axes=2,normalize=False)
    out= Reshape((1,))(out)
    model = Model(inputs=[user_ID,movie_ID],outputs=out)
    print(model.summary())
    return model

在前面的model功能中,max_usersmax_movies分别确定用户和电影嵌入矩阵的大小。模型的参数只不过是用户和电影嵌入矩阵的组成部分。所以如果我们有 m 用户和 n 电影,并且我们选择了 k 的潜在维度,那么我们就有 m x k + n x k = (m + n)k 参数需要学习。

数据处理功能可以编码如下:

data_dir = Path('/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/')
outdir = Path('/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/')

#Function to read data 
def create_data(rating,header_cols):
    data = pd.read_csv(rating,header=None,sep='\t')
    #print(data)
    data.columns = header_cols
    return data

#Movie ID to movie name dict 
def create_movie_dict(movie_file):
    print(movie_file)
    df = pd.read_csv(movie_file,sep='|', encoding='latin-1',header=None)
    movie_dict = {}
    movie_ids = list(df[0].values)
    movie_name = list(df[1].values)
    for k,v in zip(movie_ids,movie_name):
        movie_dict[k] = v 
    return movie_dict

# Function to create training validation and test data
def train_val(df,val_frac=None):
    X,y = df[['userID','movieID']].values,df['rating'].values
    #Offset the ids by 1 for the ids to start from zero
    X = X - 1 
    if val_frac != None:
        X_train, X_test, y_train, y_val = train_test_split(X, y, test_size=val_frac,random_state=0)
        return X_train, X_val, y_train, y_val
    else:
        return X,y

需要注意的一点是user_IDmovie_ID都减去了1,以确保标识从0开始,而不是从1开始,以便嵌入层可以正确引用它们。

调用数据处理和训练的代码如下:

#Data processing and model training 

train_ratings_df = create_data(f'{data_dir}/u1.base',['userID','movieID','rating','timestamp']) 
test_ratings_df = create_data(f'{data_dir}/u1.test',['userID','movieID','rating','timestamp']) 
X_train, X_val,y_train, y_val = train_val(train_ratings_df,val_frac=0.2)
movie_dict = create_movie_dict(f'{data_dir}/u.item')
num_users = len(train_ratings_df['userID'].unique())
num_movies = len(train_ratings_df['movieID'].unique())

print(f'Number of users {num_users}')
print(f'Number of movies {num_movies}')
model = model(num_users,num_movies,40)
plot_model(model, to_file='model_plot.png', show_shapes=True, show_layer_names=True)
model.compile(loss='mse',optimizer='adam')
callbacks = [EarlyStopping('val_loss', patience=2), 
             ModelCheckpoint(f'{outdir}/nn_factor_model.h5', save_best_only=True)]
model.fit([X_train[:,0],X_train[:,1]], y_train, nb_epoch=30, validation_data=([X_val[:,0],X_val[:,1]], y_val), verbose=2, callbacks=callbacks)

该模型的建立是为了存储关于验证错误的最佳模型。从训练日志中我们可以看到,该模型收敛于大约0.8872的验证 RMSE,如下所示:

Train on 64000 samples, validate on 16000 samples
Epoch 1/30
 - 4s - loss: 8.8970 - val_loss: 2.0422
Epoch 2/30
 - 3s - loss: 1.3345 - val_loss: 1.0734
Epoch 3/30
 - 3s - loss: 0.9656 - val_loss: 0.9704
Epoch 4/30
 - 3s - loss: 0.8921 - val_loss: 0.9317
Epoch 5/30
 - 3s - loss: 0.8452 - val_loss: 0.9097
Epoch 6/30
 - 3s - loss: 0.8076 - val_loss: 0.8987
Epoch 7/30
 - 3s - loss: 0.7686 - val_loss: 0.8872
Epoch 8/30
 - 3s - loss: 0.7260 - val_loss: 0.8920
Epoch 9/30
 - 3s - loss: 0.6842 - val_loss: 0.8959

我们现在在看不见的测试数据集上评估模型的性能。可以调用以下代码对测试数据集运行推理:

#Evaluate on the test dataset 
model = load_model(f'{outdir}/nn_factor_model.h5')
X_test,y_test = train_val(test_ratings_df,val_frac=None)
pred = model.predict([X_test[:,0],X_test[:,1]])[:,0]
print('Hold out test set RMSE:',(np.mean((pred - y_test)**2)**0.5))
pred = np.round(pred)
test_ratings_df['predictions'] = pred
test_ratings_df['movie_name'] = test_ratings_df['movieID'].apply(lambda x:movie_dict[x])

从日志中我们可以看到,RMSE 的保持测试大约在0.95左右,如下所示:

Hold out test set RMSE: 0.9543926404313371

现在,我们通过调用下面一行代码为测试数据集中 ID 为1的用户评估模型的性能:

#Check evaluation results for the UserID = 1 
test_ratings_df[test_ratings_df['userID'] == 1].sort_values(['rating','predictions'],ascending=False)

从以下结果中我们可以看出(图 6.5 )该模型在预测训练中未看到的电影的收视率方面做得很好:

Figure 6.5: Results of evaluation for UserID 1

与深度学习法潜在因素法相关的代码可以在https://github . com/packt publishing/Intelligent-Projects-use-Python/tree/master/chapter 06找到。

SVD++

一般来说,奇异值分解不会捕捉数据中可能存在的用户和项目偏差。一种被称为 SVD++的方法考虑了潜在因子分解方法中的用户和项目偏差,并且在诸如网飞挑战赛等比赛中非常流行。

进行基于潜在因素的推荐最常见的方式是将用户简档和偏好定义为 u i ∈ R kb i ∈ R ,将项目简档和偏好定义为*vI∈Rkbj*∈11 用户 i 对项目 j 提供的等级定义如下:

是所有收视率的全局均值。

用户简档和项目简档然后通过最小化在预测由用户评级的所有项目的评级时的误差平方和来确定。要优化的平方误差损失可以表示如下:

I ij 是一个指标功能,如果用户 i 有一个评级项目 *j,则该指标功能为 1;*否则为零。

相对于用户和项目简档的参数,成本被最小化。通常,这种优化会导致过度拟合,因此用户的规范和项目配置文件在成本函数中被用作规范,如下所示:

这里λ1T3λ2T7】是正则化常数。通常,使用一种流行的梯度下降技术交替最小二乘 ( ALS )进行优化,该技术通过保持项目参数固定来交替更新用户简档参数,反之亦然。**

surprise包很好的实现了 SVD++。在下一节中,我们将在100K movie lens数据集上用 SVD++训练一个模型,并查看性能指标。

电影镜头 100k 数据集的 SVD++训练模型

使用以下命令可通过conda下载surprise包:

conda install -c conda-forge scikit-surprise 

SVD++对应的算法在surprise中命名为SVDpp。我们可以按如下方式加载所有必需的包:

import numpy as np
from surprise import SVDpp # SVD++ algorithm
from surprise import Dataset
from surprise import accuracy
from surprise.model_selection import cross_validate
from surprise.model_selection import train_test_split

可以使用surprise中的Dataset.load_builtin实用程序下载100K Movie lens数据集并使其可供代码使用。我们按照8020的比例将数据分为训练集和保持测试集。数据处理代码行如下:

# Load the movie lens 10k data and split the data into train test files(80:20)
data = Dataset.load_builtin('ml-100k')
trainset, testset = train_test_split(data, test_size=.2)

接下来,我们将对数据进行5折叠交叉验证,并查看交叉验证结果。我们为随机梯度下降选择了0.008的学习速率。同样为了防止过度拟合,我们为 L1 和 L2 正则化选择了0.1正则化常数。这些代码行的详细信息如下:

#Perform 5 fold cross validation with all data 
algo = SVDpp(n_factors=40, n_epochs=40, lr_all=0.008, reg_all=0.1)
# Run 5-fold cross-validation and show results summary
cross_validate(algo,data, measures=['RMSE', 'MAE'], cv=5, verbose=True)

交叉验证的结果如下:

Evaluating RMSE, MAE of algorithm SVDpp on 5 split(s). Fold 1 Fold 2 Fold 3 Fold 4 Fold 5 Mean Std RMSE (testset) 0.9196 0.9051 0.9037 0.9066 0.9151 0.9100 0.0062 MAE (testset) 0.7273 0.7169 0.7115 0.7143 0.7228 0.7186 0.0058 Fit time 374.57 374.58 369.74 385.44 382.36 377.34 5.72 Test time 2.53 2.63 2.74 2.79 2.84 2.71 0.11   

从前面的结果可以看出,模型的5 fold cv RMSE0.91Movie Lens 100K数据集的结果令人印象深刻。

现在我们只在训练数据集trainset上训练模型,然后在测试集上评估模型。相关代码行如下:

model = SVDpp(n_factors=40, n_epochs=10, lr_all=0.008, reg_all=0.1)
model.fit(trainset)

一旦模型已经被训练,我们就在保持测试数据集测试集上评估模型。相关代码行如下:

#validate the model on the testset
pred = model.test(testset)
print("SVD++ results on the Test Set")
accuracy.rmse(pred, verbose=True)   

验证的输出如下:

SVD++ results on the test set
RMSE: 0.9320

从前面的结果中我们可以看到,SVD++模型在 RMSE 为0.93的测试数据集上做得非常好。结果与我们在此之前训练的基于深度学习的模型潜在因素模型(RMSE 的0.95)相当。

用于推荐的受限玻尔兹曼机器部分*、*中,我们将研究用于构建推荐系统的受限玻尔兹曼机器。该方法可以扩展到大规模数据集,因此在协同过滤中得到了广泛的应用。协同过滤领域的大部分数据集是稀疏的,导致非凸优化问题很难解决。与其他因子分解方法(如奇异值分解)相比,径向基函数在数据集上不太容易受到这种稀疏性问题的影响。

推荐使用受限玻尔兹曼机器

受限玻尔兹曼机器是一类属于无监督学习技术的神经网络。受限玻尔兹曼机器 ( RBMs ),因为它们广为人知,试图通过将输入数据投影到隐藏层来学习数据的隐藏结构。

隐藏层激活被期望编码输入信号并重新创建它。受限玻尔兹曼机器通常处理二进制数据:

Figure 6.6: Restricted Boltzmann machines for binary data

为了刷新我们的记忆,前面的图(图 6.6 )是一个有 m 输入或可见单位的 RBM。这是投射到一个隐藏层与 n 单位。给定可见层输入,隐藏单元相互独立,因此可以如下采样,其中表示 sigmoid 函数:

类似地,给定隐藏层激活,可见层单元是独立的,可以如下采样:

RBM 的参数是可见层单元 i 和隐藏层单元 j 之间的广义权重连接 w ij ∈ W m x n ,可见单元 i 处的偏置 c i ∈ b ,以及偏置 c j ∈ c

RBM 的这些参数是通过最大化可见输入数据的可能性来学习的。如果我们用表示组合参数集,并且我们有一组 T 训练输入数据点,那么在 RBM,我们尝试最大化似然函数:

我们通常不使用乘积形式,而是最大化可能性的对数,或者最小化对数可能性的负值,以使函数在数学上更加方便。如果我们将对数似然的负值表示为成本函数 C ,那么:

成本函数通常通过梯度下降最小化。成本函数相对于参数的梯度由预期项组成,表示如下:

术语表示隐藏单元和可见单元的联合概率分布上任何给定量的期望。此外,表示给定可见单位 v 的采样隐藏层输出。在梯度下降的每次迭代中计算联合概率分布的期望值在计算上是困难的。我们求助于一种叫做对比发散的智能方法来计算期望值,这将在下一节讨论。

对比分歧

计算联合概率分布期望值的方法之一是通过吉布斯抽样从联合概率分布中产生大量样本,然后将样本的平均值作为期望值。在吉布斯抽样中,联合概率分布中的每个变量都可以抽样,条件是其余变量。由于可见单元是独立的,给定隐藏单元,反之亦然,您可以将隐藏单元采样为,然后给定隐藏单元的可见单元激活为。然后,我们可以将样本作为从联合概率分布中采样的样本。这样,我们可以生成大量的样本,比如说 M ,取它们的平均值来计算期望的期望值。然而,在梯度下降的每一步中进行如此广泛的采样将使训练过程慢得不可接受,因此,我们不是在梯度下降的每一步中计算许多样本的平均值,而是从联合概率分布中仅生成一个样本,该样本应该代表整个联合概率分布上的期望:

Figure 6.7: Contrastive divergence illustration

从上图(图 6.7 )的图示可以看出,我们从看到的可见输入 v (t) 开始,基于条件概率分布 P(h/v = v (t) ) 对隐藏层激活进行采样。再次,使用条件概率分布P(v/h = h),我们对*v进行采样。基于条件概率分布P(h/v = v)*的隐藏单元下一次采样给我们,然后使用采样可见单元激活给我们。样本vh 整个联合概率分布的代表样本,即。同样用于计算任何包含 vh 的表达式的期望值。这种取样过程被称为对比发散。

从可见输入开始,然后从条件分布 P(v/h)P(v/h) 中依次采样,构成 Gibbs 采样的一个步骤,并从联合分布中给出一个样本 (v/h) 。我们可以选择从条件概率分布中选取连续几次采样迭代后的样本,而不是在吉布斯采样的每一步都选取样本 (v/h) 。如果在 Gibbs 抽样的 k 步之后,选择了代表元素,则对比散度称为 CD-k图 6.7 所示的对比差异可称为 CD-2、,因为我们是在两步吉布斯取样后选择样品的。

使用 RBMs 的协同过滤

限制性玻尔兹曼机器可用于在进行推荐时进行协同过滤。我们将使用这些资源管理系统向用户推荐电影。他们使用不同用户为不同电影提供的分级进行训练。用户不会观看或评价所有的电影,所以这个训练好的模型可以用来向用户推荐看不见的电影。

我们应该有的第一个问题是如何在 RBM 中处理等级,因为等级本质上是有序的,而 RBM 处理的是二进制数据。等级可以被视为二进制数据,代表等级的单位数量等于每个等级的唯一值的数量。例如:在评级系统中,级别从 1 到 5 不等,有五个二进制单位,其中一个对应的级别设置为 1,其余的为零。对 RBM 可见的单位是为用户提供的不同电影的等级。如所讨论的,每个等级将以二进制表示,并且对于每个可见单元,将存在来自所有二进制可见单元的权重连接,对应于电影分级。由于每个用户会对不同的电影集进行分级,因此每个用户的输入将会不同。然而,从电影分级单位到隐藏单位的权重连接对于所有用户来说都是通用的。

下图(图 6.8a**图 6.8b )所示为用户 A用户 B 的 RBM 视图。用户T10】A 和用户 B 对不同的一组电影进行了评分。然而,正如我们所看到的,对于每个用户来说,每个电影中隐藏单元的权重连接是相同的。关于用户 A 的 RBM 评级如下:

Figure 6.8a: RBM for collaborative filtering User A view

关于用户 B 的 RBM 评级如下:

Figure 6.8b: RBM for collaborative filtering User B view

还有一点需要注意的是,如果有 M 部电影,并且如果每部电影都可能有 k 个等级,那么 RBM 的可见单位数量就是 M * k 。同样,如果二进制隐藏单元的数量为 n ,那么*【W】*中的权重连接数等于 M * k * n 。给定可见层输入,每个隐藏单元 h j 可以独立于其他隐藏单元进行采样,如下所示:

这里, m = M * k.

与传统的 RBM 不同,在给定隐藏层激活的情况下,该网络中可见层的二进制单位不能被独立采样。关于电影等级的每一个 k 二进制单位通过 k 路软最大值激活功能联系在一起。如果给定隐藏单元的特定电影的可见单元的输入是,则电影的等级lI 的一般输入计算如下:

这里, (i - 1)k + l 是电影 i 的可视单元的索引,用于排名 l 。类似地,任何特定电影的可视单元都可以根据软最大值函数给出的概率进行采样,如下所示:

在定义隐藏和可见单元的输出时,还有一点很重要,那就是需要概率抽样,而不是默认输出是最大概率的输出。如果给定可见单位的隐藏单位激活概率为 P,,则统一生成范围【0,1】内的随机数 r ,如果 (P > r) ,则隐藏单位激活设置为真。该方案将确保在很长一段时间内激活设置为真的概率 P 。类似地,电影的可见单元是根据给定隐藏单元的概率从跨国发行中采样的。因此,如果对于一部特定的电影,给定隐藏单元激活的情况下,不同评级的概率范围从 1 到 5,分别是 (p 1 、p 2 、p 3 、p 4 、p 5 ) ,那么可以从多项式分布中对评级值进行采样,其概率质量函数如下:

这里:

我们现在具备了创建用于协同过滤的受限玻尔兹曼机器所需的所有技术知识。

使用 RBM 实现协同过滤

在接下来的几个部分中,我们将使用受限的玻尔兹曼机器来实现一个协作过滤系统,其技术原理在前面的部分中有所阐述。我们将使用的数据集是 MovieLens 100K 数据集,其中包含用户为不同电影提供的一到五个等级。数据集可以从https://grouplens.org/datasets/movielens/100k/下载。

这个协同过滤系统的 TensorFlow 实现将在接下来的几节中介绍。

处理输入

每行输入的评分文件记录包含字段userIdmovieIdratingtimestamp。我们处理每个记录以创建一个训练文件,其形式为一个具有与userIdmovieIdrating相关的三个维度的numpy数组。从 1 到 5 的等级是一个热编码的,因此沿着等级维度的长度是 5。我们用 80%的输入记录创建训练数据,而剩下的 20%保留用于测试目的。用户评分的电影数量为1682。训练文件包含943用户,因此训练数据的维度为(943,1682,5)。训练文件中的每个用户都是 RBM 的训练记录,将包含一些用户已评分的电影和一些用户未评分的电影。一些电影分级也被删除,以包括在测试文件中。RBM 将根据可用的分级进行训练,在隐藏单元中捕获输入数据的隐藏结构,然后尝试根据捕获的隐藏结构为每个用户重建所有电影的输入分级。我们还创建了几个字典来存储实际电影标识和它们在训练/测试数据集中的索引的交叉引用。以下是创建培训和测试文件的详细代码:

"""
@author: santanu
"""

import numpy as np
import pandas as pd
import argparse

'''
Ratings file preprocessing script to create training and hold out test datasets
'''

def process_file(infile_path):
    infile = pd.read_csv(infile_path,sep='\t',header=None)
    infile.columns = ['userId','movieId','rating','timestamp']
    users = list(np.unique(infile.userId.values))
    movies = list(np.unique(infile.movieId.values))

    test_data = []
    ratings_matrix = np.zeros([len(users),len(movies),5])
    count = 0 
    total_count = len(infile)
    for i in range(len(infile)):
        rec = infile[i:i+1]
        user_index = int(rec['userId']-1)
        movie_index = int(rec['movieId']-1)
        rating_index = int(rec['rating']-1)
        if np.random.uniform(0,1) < 0.2 :
            test_data.append([user_index,movie_index,int(rec['rating'])])

        else:
            ratings_matrix[user_index,movie_index,rating_index] = 1 

        count +=1 
        if (count % 100000 == 0) & (count>= 100000):
            print('Processed ' + str(count) + ' records out of ' + str(total_count))

    np.save(path + 'train_data',ratings_matrix)
    np.save(path + 'test_data',np.array(test_data))

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--path',help='input data path')
    parser.add_argument('--infile',help='input file name')
    args = parser.parse_args()
    path = args.path
    infile = args.infile
    process_file(path + infile)

训练文件是维度为 m x n x knumpy数组对象,其中 m 为用户总数, n 为电影总数, k 为离散评分值个数(一到五)。为了构建测试集,我们从训练数据集中随机选择 20%的 m x n 评分条目。因此,测试集评级样本的所有 k 评级值在训练数据集中标记为零。在测试集中,我们没有将数据扩展为三维 numpy 数组格式,因此它可以用于训练。相反,我们只是将useridmovieid和指定的评级保存在三列中。请注意,存储在列车和测试文件中的useridmovieid不是原始评级数据文件u.data中的实际标识。它们被1抵消,以适应从0而不是从1开始的 Python 和numpy索引

以下命令可用于调用数据预处理脚本:

python preprocess_ratings.py --path '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/' --infile 'u.data'

为协同过滤建立 RBM 网络

以下函数_network为协同过滤创建所需的 RBM 结构。首先,我们定义输入的权重、偏差和占位符。然后定义sample_hiddensample_visible函数,根据概率分别对隐藏激活和可见激活进行采样。隐藏单元从伯努利分布中采样,概率由 sigmoid 函数提供,而与每部电影相关的可见单元从多项式分布中采样,概率由 softmax 函数提供。不需要创建软最大概率,因为tf.multinomial函数可以直接从逻辑中采样,而不是实际概率。

我们通过定义基于吉布斯抽样的对比发散逻辑来跟进此事。gibbs_step函数实现 Gibbs 抽样的一个步骤,然后利用这个步骤实现顺序的对比发散 k

现在我们已经有了所有必要的函数,我们创建 TensorFlow ops 来对给定可见输入的隐藏状态self.h进行采样,并对给定采样隐藏状态的可见单位self.x进行采样。我们还使用对比散度从 v 和 h 的联合概率分布中抽取(self.x_s,self.h_s)作为代表性样本,即P(v,h/model),用于计算梯度中的不同期望项。

_network函数的最后一步是根据梯度更新 RBM 模型的权重和偏差。正如我们前面看到的,梯度是基于给定可见层输入的隐藏层激活self.h,以及通过对比发散得到的联合概率分布P(v,h/model)的代表性样本,即(self.x_s,self.h_s)

TensorFlow 操作self.x_,指的是给定隐藏层激活self.h的可见层激活,在推断过程中,该操作将有助于得出尚未被每个用户评级的电影的评级:

    def __network(self):

        self.x = tf.placeholder(tf.float32, [None,self.num_movies,self.num_ranks], name="x") 
        self.xr = tf.reshape(self.x, [-1,self.num_movies*self.num_ranks], name="xr") 
        self.W = tf.Variable(tf.random_normal([self.num_movies*self.num_ranks,self.num_hidden], 0.01), name="W") 
        self.b_h = tf.Variable(tf.zeros([1,self.num_hidden], tf.float32, name="b_h")) 
        self.b_v = tf.Variable(tf.zeros([1,self.num_movies*self.num_ranks],tf.float32, name="b_v")) 
        self.k = 2 

## Converts the probability into discrete binary states i.e. 0 and 1 
        def sample_hidden(probs):
            return tf.floor(probs + tf.random_uniform(tf.shape(probs), 0, 1)) 

        def sample_visible(logits):

            logits = tf.reshape(logits,[-1,self.num_ranks])
            sampled_logits = tf.multinomial(logits,1) 
            sampled_logits = tf.one_hot(sampled_logits,depth = 5)
            logits = tf.reshape(logits,[-1,self.num_movies*self.num_ranks])
            print(logits)
            return logits 

## Gibbs sampling step
        def gibbs_step(x_k):
          # x_k = tf.reshape(x_k,[-1,self.num_movies*self.num_ranks]) 
            h_k = sample_hidden(tf.sigmoid(tf.matmul(x_k,self.W) + self.b_h))
            x_k = sample_visible(tf.add(tf.matmul(h_k,tf.transpose(self.W)),self.b_v))
            return x_k
## Run multiple gives Sampling step starting from an initital point 
        def gibbs_sample(k,x_k):

            for i in range(k):
                x_k = gibbs_step(x_k)
# Returns the gibbs sample after k iterations
            return x_k

# Constrastive Divergence algorithm
# 1\. Through Gibbs sampling locate a new visible state x_sample based on the current visible state x 
# 2\. Based on the new x sample a new h as h_sample 
        self.x_s = gibbs_sample(self.k,self.xr)
        self.h_s = sample_hidden(tf.sigmoid(tf.matmul(self.x_s,self.W) + self.b_h))

# Sample hidden states based given visible states
        self.h = sample_hidden(tf.sigmoid(tf.matmul(self.xr,self.W) + self.b_h))
# Sample visible states based given hidden states
        self.x_ = sample_visible(tf.matmul(self.h,tf.transpose(self.W)) + self.b_v)

# The weight updated based on gradient descent 
        #self.size_batch = tf.cast(tf.shape(x)[0], tf.float32)
        self.W_add = tf.multiply(self.learning_rate/self.batch_size,tf.subtract(tf.matmul(tf.transpose(self.xr),self.h),tf.matmul(tf.transpose(self.x_s),self.h_s)))
        self.bv_add = tf.multiply(self.learning_rate/self.batch_size, tf.reduce_sum(tf.subtract(self.xr,self.x_s), 0, True))
        self.bh_add = tf.multiply(self.learning_rate/self.batch_size, tf.reduce_sum(tf.subtract(self.h,self.h_s), 0, True))
        self.updt = [self.W.assign_add(self.W_add), self.b_v.assign_add(self.bv_add), self.b_h.assign_add(self.bh_add)]

预处理步骤的数据可以在训练和推理过程中使用如下所示的read_data函数读取:

    def read_data(self):

        if self.mode == 'train':
           self.train_data = np.load(self.train_file)
           self.num_ranks = self.train_data.shape[2]
           self.num_movies = self.train_data.shape[1]
           self.users = self.train_data.shape[0]

        else:
           self.train_df = pd.read_csv(self.train_file)
           self.test_data = np.load(self.test_file)
           self.test_df = pd.DataFrame(self.test_data,columns=['userid','movieid','rating'])

           if self.user_info_file != None:
               self.user_info_df = pd.read_csv(self.user_info_file,sep='|',header=None)
               self.user_info_df.columns=['userid','age','gender','occupation','zipcode']

           if self.movie_info_file != None:
               self.movie_info_df = pd.read_csv(self.movie_info_file,sep='|',encoding='latin-1',header=None)
               self.movie_info_df = self.movie_info_df[[0,1]] 
               self.movie_info_df.columns = ['movieid','movie Title']

此外,在推断过程中,除了测试文件之外,我们还读取了所有电影和分级的预测文件 CSV(在前面代码的推断部分中为self.train_file),而不管它们是否已经被评级。一旦训练好模型,就进行预测。由于我们已经有了训练后预测的评分,我们在推断期间需要做的就是将评分预测信息与测试文件的实际评分信息相结合(更多细节请参见后面的traininference部分)。此外,我们从用户和电影元数据文件中读取信息供以后使用。

训练 RBM

这里说明的_train功能可以用来训练 RBM。在这个函数中,我们首先调用_network函数来构建 RBM 网络结构,然后在激活的 TensorFlow 会话中为指定数量的时期训练模型。使用张量流的saver功能以指定的时间间隔保存模型:

      def _train(self):

        self.__network()
       # TensorFlow graph execution

        with tf.Session() as sess:
            self.saver = tf.train.Saver()
            #saver = tf.train.Saver(write_version=tf.train.SaverDef.V2) 
            # Initialize the variables of the Model
            init = tf.global_variables_initializer()
            sess.run(init)

            total_batches = self.train_data.shape[0]//self.batch_size
            batch_gen = self.next_batch()
            # Start the training 
            for epoch in range(self.epochs):
                if epoch < 150:
                    self.k = 2

                if (epoch > 150) & (epoch < 250):
                    self.k = 3

                if (epoch > 250) & (epoch < 350):
                    self.k = 5

                if (epoch > 350) & (epoch < 500):
                    self.k = 9

                    # Loop over all batches
                for i in range(total_batches):
                    self.X_train = next(batch_gen)
                    # Run the weight update 
                    #batch_xs = (batch_xs > 0)*1
                    _ = sess.run([self.updt],feed_dict={self.x:self.X_train})

                # Display the running step 
                if epoch % self.display_step == 0:
                    print("Epoch:", '%04d' % (epoch+1))
                    print(self.outdir)
                    self.saver.save(sess,os.path.join(self.outdir,'model'), 
                                    global_step=epoch)
           # Do the prediction for all users all items irrespective of whether they 
             have been rated
            self.logits_pred = tf.reshape(self.x_,
           [self.users,self.num_movies,self.num_ranks])
            self.probs = tf.nn.softmax(self.logits_pred,axis=2)
            out = sess.run(self.probs,feed_dict={self.x:self.train_data})
            recs = []
            for i in range(self.users):
                for j in range(self.num_movies):
                    rec = [i,j,np.argmax(out[i,j,:]) +1]
                    recs.append(rec)
            recs = np.array(recs)
            df_pred = pd.DataFrame(recs,columns=
            ['userid','movieid','predicted_rating'])
            df_pred.to_csv(self.outdir + 'pred_all_recs.csv',index=False)

            print("RBM training Completed !")

在前面的函数中需要强调的一件重要事情是使用自定义的next_batch函数创建随机批次。该函数在下面的代码片段中定义,用于定义迭代器batch_gen,该迭代器可以由next方法调用来检索下一个小批量:

def next_batch(self):
    while True:
        ix = np.random.choice(np.arange(self.data.shape[0]),self.batch_size)
        train_X = self.data[ix,:,:] 
        yield train_X

需要注意的一点是,在培训结束时,我们会预测来自所有用户的所有电影的评分,无论它们是否被评分。具有最大概率的评级,将从五个可能的评级中给出(即从一到五)作为最终评级。因为在 Python 中索引是从零开始的,所以我们在使用argmax获得具有最高概率的评级位置后,添加一个来获得实际评级。因此,在培训结束时,我们有一个pred_all_recs.csv文件,其中包含所有培训和测试记录的预测评分。请注意,测试记录嵌入在培训记录中,1 到 5 的所有评分指标都设置为零。

然而,一旦我们从用户观看过的电影的隐藏表示中充分训练了模型,它就学会从用户没有看过的电影中生成评级。

可以通过调用以下命令来训练模型:

python rbm.py main_process --mode train --train_file '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/train_data.npy' --outdir '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/' --num_hidden 5 --epochs 1000

仅用5隐藏图层训练1000时代的模型需要大约52秒,从日志中我们可以看到:

RBM training Completed !
52.012 s: process RBM 

Note that the Restricted Boltzmann Machine Network has been trained on an Ubuntu machine with a GeForce Zotac 1070 GPU and 64 GB of RAM. Training time may vary based on the system used to train the network.

使用经过训练的 RBM 进行推理

鉴于我们已经在训练中生成了包含所有预测的文件pred_all_recs.csv,RBM 的推断非常简单。我们需要做的就是根据提供的测试文件从pred_all_recs.csv中提取测试记录。此外,我们通过将1添加到它们当前的值中来求助于原始的useridmovieid。返回原 ID 的目的是能够从u.useru.item文件中添加用户和电影信息。

推理块如下:

    def inference(self):

        self.df_result = self.test_df.merge(self.train_df,on=['userid','movieid'])
        # in order to get the original ids we just need to add 1 
        self.df_result['userid'] = self.df_result['userid'] + 1
        self.df_result['movieid'] = self.df_result['movieid'] + 1
        if self.user_info_file != None:
            self.df_result.merge(self.user_info_df,on=['userid'])
        if self.movie_info_file != None:
            self.df_result.merge(self.movie_info_df,on=['movieid'])
        self.df_result.to_csv(self.outdir + 'test_results.csv',index=False)

        print(f'output written to {self.outdir}test_results.csv')
        test_rmse = (np.mean((self.df_result['rating'].values - 
        self.df_result['predicted_rating'].values)**2))**0.5
        print(f'test RMSE : {test_rmse}')

推论可以如下调用:

 python rbm.py main_process --mode test --train_file '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/pred_all_recs.csv' --test_file '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/test_data.npy' --outdir '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/' --user_info_file '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/u.user' --movie_info_file '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/u.item'

通过仅使用 RBM 的5隐藏单元,我们实现了约1.19的 RMSE 测试,这是值得称赞的,因为我们选择了这样一个简单的网络。推理的输出日志在以下代码块中提供,以供参考:

output written to /home/santanu/ML_DS_Catalog-/Collaborating Filtering/test_results.csv
test RMSE : 1.1999306704742303
458.058 ms: process RBM

我们从test_results.csvuserid 1的推断结果如下(见图 6.9 ):

Figure 6.9: Holdout data validation results on userid 1

从前面截图中的预测(图 6.9 )我们可以看出,RBM 在预测userid 1的电影坚守集方面做得很好。

建议您将最终评分预测作为对每个电影评分预测的多项式概率分布的评分预期,并查看与我们将最终评分作为多项式分布的最高概率的方法相比的情况。协同过滤的 RBM 纸可以在https://www.cs.toronto.edu/~rsalakhu/papers/rbmcf.pdT2 找到。与受限玻尔兹曼机相关的代码可在https://github . com/PacktPublishing/Intelligent-Projects-use-Python/blob/master/chapter 06/RBM . py找到。

摘要

看完这一章,你现在应该能够使用受限的玻尔兹曼机器构建一个智能推荐系统,并根据你的领域和需求以有趣的方式扩展它。关于本章所述项目的详细实施,请参考https://github . com/PacktPublishing/Intelligent-Projects-use-Python/blob/master/chapter 06上该项目的 GiHub 链接。

在下一章中,我们将讨论如何创建一个移动应用程序来对电影评论进行情感分析。我期待你的参与。

1
https://gitee.com/OpenDocCN/apachecn-ml-zh.git
git@gitee.com:OpenDocCN/apachecn-ml-zh.git
OpenDocCN
apachecn-ml-zh
apachecn-ml-zh
master

搜索帮助