TensorFlow 深度学习第二版:6~10(3)https://developer.aliyun.com/article/1426804
预测用户的电影评级
为此,我编写了一个名为prediction()
的函数。它采用有关用户和项目(在本例中为电影)的示例输入,并按名称从图创建 TensorFlow 占位符。然后它求值这些张量。在以下代码中,需要注意的是 TensorFlow 假定检查点目录已存在,因此请确保它已存在。有关此步骤的详细信息,请参阅run.py
文件。请注意,此脚本不显示任何结果,但在main.py
脚本中进一步调用此脚本中名为prediction
的函数进行预测:
def prediction(users=predicted_user, items=predicted_item, allow_soft_placement=allow_soft_placement,\ log_device_placement=log_device_placement, checkpoint_dir=checkpoint_dir): rating_prediction=[] checkpoint_prefix = os.path.join(checkpoint_dir, "model") graph = tf.Graph() with graph.as_default(): session_conf = tf.ConfigProto(allow_soft_placement=allow_soft_placement,log_device_placement=log_device_placement) with tf.Session(config = session_conf) as sess: new_saver = tf.train.import_meta_graph("{}.meta".format(checkpoint_prefix)) new_saver.restore(sess, tf.train.latest_checkpoint(checkpoint_dir)) user_batch = graph.get_operation_by_name("id_user").outputs[0] item_batch = graph.get_operation_by_name("id_item").outputs[0] predictions = graph.get_operation_by_name("svd_inference").outputs[0] pred = sess.run(predictions, feed_dict={user_batch: users, item_batch: items}) pred = clip(pred) sess.close() return pred
我们将看到如何使用此方法来预测电影的前 K 部电影和用户评级。在前面的代码段中,clip()
是一个用户定义的函数,用于限制数组中的值。这是实现:
def clip(x): return np.clip(x, 1.0, 5.0) # rating 1 to 5
现在让我们看看,我们如何使用prediction()
方法来制作用户的一组电影评级预测:
def user_rating(users,movies): if type(users) is not list: users=np.array([users]) if type(movies) is not list: movies=np.array([movies]) return prediction(users,movies)
上述函数返回各个用户的用户评级。它采用一个或多个数字的列表,一个或多个用户 ID 的列表,以及一个或多个数字的列表以及一个或多个电影 ID 的列表。最后,它返回预测电影列表。
寻找前 K 部电影
以下方法提取用户未见过的前 K 个电影,其中 K 是任意整数,例如 10.函数的名称是top_k_movies()
。它返回特定用户的前 K 部电影。它需要一个用户 ID 列表和评级数据帧。然后它存储这些电影的所有用户评级。输出是包含用户 ID 作为键的字典,以及该用户的前 K 电影列表作为值:
def top_k_movies(users,ratings_df,k): dicts={} if type(users) is not list: users = [users] for user in users: rated_movies = ratings_df[ratings_df['user']==user].drop(['st', 'user'], axis=1) rated_movie = list(rated_movies['item'].values) total_movies = list(ratings_df.item.unique()) unseen_movies = list(set(total_movies) - set(rated_movie)) rated_list = [] rated_list = prediction(np.full(len(unseen_movies),user),np.array(unseen_movies)) useen_movies_df = pd.DataFrame({'item': unseen_movies,'rate':rated_list}) top_k = list(useen_movies_df.sort_values(['rate','item'], ascending=[0, 0])['item'].head(k).values) dicts.update({user:top_k}) result = pd.DataFrame(dicts) result.to_csv("user_top_k.csv") return dicts
在前面的代码段中,prediction()
是我们之前描述的用户定义函数。我们将看到一个如何预测前 K 部电影的例子(更多或见后面的部分见Test.py
)。
预测前 K 类似的电影
我编写了一个名为top_k_similar_items()
的函数,它计算并返回与特定电影类似的 K 个电影。它需要一个数字列表,数字,电影 ID 列表和评级数据帧。它存储这些电影的所有用户评级。它还将 K 作为自然数。
TRAINED
的值可以是TRUE
或FALSE
,它指定是使用受过训练的用户还是使用电影表或未经训练的用户。最后,它返回一个 K 电影列表,类似于作为输入传递的电影:
def top_k_similar_items(movies,ratings_df,k,TRAINED=False): if TRAINED: df=pd.read_pickle("user_item_table_train.pkl") else: df=pd.read_pickle("user_item_table.pkl") corr_matrix=item_item_correlation(df,TRAINED) if type(movies) is not list: return corr_matrix[movies].sort_values(ascending=False).drop(movies).index.values[0:k] else: dict={} for movie in movies: dict.update({movie:corr_matrix[movie].sort_values(ascending=False).drop(movie).index.values[0:k]}) pd.DataFrame(dict).to_csv("movie_top_k.csv") return dict
在前面的代码中,item_item_correlation()
函数是一个用户定义的函数,它计算在预测前 K 个类似电影时使用的电影 - 电影相关性。方法如下:
def item_item_correlation(df,TRAINED): if TRAINED: if os.path.isfile("model/item_item_corr_train.pkl"): df_corr=pd.read_pickle("item_item_corr_train.pkl") else: df_corr=df.corr() df_corr.to_pickle("item_item_corr_train.pkl") else: if os.path.isfile("model/item_item_corr.pkl"): df_corr=pd.read_pickle("item_item_corr.pkl") else: df_corr=df.corr() df_corr.to_pickle("item_item_corr.pkl") return df_corr
计算用户 - 用户相似度
为了计算用户 - 用户相似度,我编写了user_similarity()
函数,它返回两个用户之间的相似度。它需要三个参数:用户 1,用户 2;评级数据帧;并且TRAINED
的值可以是TRUE
或FALSE
,并且指的是是否应该使用受过训练的用户与电影表或未经训练的用户。最后,它计算用户之间的 Pearson 系数(介于 -1 和 1 之间的值):
def user_similarity(user_1,user_2,ratings_df,TRAINED=False): corr_matrix=user_user_pearson_corr(ratings_df,TRAINED) return corr_matrix[user_1][user_2]
在前面的函数中,user_user_pearson_corr()
是一个计算用户 - 用户 Pearson 相关性的函数:
def user_user_pearson_corr(ratings_df,TRAINED): if TRAINED: if os.path.isfile("model/user_user_corr_train.pkl"): df_corr=pd.read_pickle("user_user_corr_train.pkl") else: df =pd.read_pickle("user_item_table_train.pkl") df=df.T df_corr=df.corr() df_corr.to_pickle("user_user_corr_train.pkl") else: if os.path.isfile("model/user_user_corr.pkl"): df_corr=pd.read_pickle("user_user_corr.pkl") else: df = pd.read_pickle("user_item_table.pkl") df=df.T df_corr=df.corr() df_corr.to_pickle("user_user_corr.pkl") return df_corr
评估推荐系统
在这个小节中,我们将通过绘制它们以评估电影如何在不同的簇中传播来评估簇。
然后,我们将看到前 K 部电影,并查看我们之前讨论过的用户 - 用户相似度和其他指标。现在让我们开始导入所需的库:
import tensorflow as tf import pandas as pd import readers import main import kmean as km import numpy as np
接下来,让我们定义用于评估的数据参数:
DATA_FILE = "Input/ratings.dat" # Data source for the positive data. K = 5 #Number of clusters MAX_ITERS = 1000 #Maximum number of iterations TRAINED = False # Use TRAINED user vs item matrix USER_ITEM_TABLE = "user_item_table.pkl" COMPUTED_CLUSTER_CSV = "clusters.csv" NO_OF_PCA_COMPONENTS = 2 #number of pca components SVD_SOLVER = "randomized" #svd solver -e.g. randomized, full etc.
让我们看看加载将在k_mean_clustering()
方法的调用调用中使用的评级数据集:
ratings_df = readers.read_file("Input/ratings.dat", sep="::") clusters,movies = km.k_mean_clustering(ratings_df, K, MAX_ITERS, TRAINED = False) cluster_df=pd.DataFrame({'movies':movies,'clusters':clusters})
做得好!现在让我们看一些简单的输入簇(电影和各自的簇):
print(cluster_df.head(10)) >>> clusters movies 0 0 0 1 4 1 2 4 2 3 3 3 4 4 4 5 2 5 6 4 6 7 3 7 8 3 8 9 2 9 print(cluster_df[cluster_df['movies']==1721]) >>> clusters movies 1575 2 1721 print(cluster_df[cluster_df['movies']==647]) >>> clusters movies 627 2 647
让我们看看电影是如何分散在群集中的:
km.showClusters(USER_ITEM_TABLE, COMPUTED_CLUSTER_CSV, NO_OF_PCA_COMPONENTS, SVD_SOLVER) >>>
图 6:类似电影的簇
如果我们查看该图,很明显数据点更准确地聚集在簇 3 和 4 上。但是,簇 0,1 和 2 更分散并且不能很好地聚类。
在这里,我们没有计算任何准确率指标,因为训练数据没有标签。现在让我们为给定的相应电影名称计算前 K 个类似的电影并打印出来:
ratings_df = readers.read_file("Input/ratings.dat", sep="::") topK = main.top_k_similar_items(9,ratings_df = ratings_df,k = 10,TRAINED = False) print(topK) >>> [1721, 1369, 164, 3081, 732, 348, 647, 2005, 379, 3255]
上述结果为电影9 ::Sudden Death (1995)::Action
计算了 Top-K 类似的电影。现在,如果您观察movies.dat
文件,您将看到以下电影与此类似:
1721::Titanic (1997)::Drama|Romance 1369::I Can't Sleep (J'ai pas sommeil) (1994)::Drama|Thriller 164::Devil in a Blue Dress (1995)::Crime|Film-Noir|Mystery|Thriller 3081::Sleepy Hollow (1999)::Horror|Romance 732::Original Gangstas (1996)::Crime 348::Bullets Over Broadway (1994)::Comedy 647::Courage Under Fire (1996)::Drama|War 2005::Goonies, The (1985)::Adventure|Children's|Fantasy 379::Timecop (1994)::Action|Sci-Fi 3255::League of Their Own, A (1992)::Comedy|Drama
现在让我们计算用户 - 用户 Pearson 相关性。当运行此用户相似度函数时,在第一次运行时需要时间来提供输出但在此之后,其响应是实时的:
print(main.user_similarity(1,345,ratings_df)) >>> 0.15045477803357316 Now let's compute the aspect rating given by a user for a movie: print(main.user_rating(0,1192)) >>> 4.25545645 print(main.user_rating(0,660)) >>> 3.20203304
让我们看一下用户的 K 电影推荐:
print(main.top_k_movies([768],ratings_df,10)) >>> {768: [2857, 2570, 607, 109, 1209, 2027, 592, 588, 2761, 479]} print(main.top_k_movies(1198,ratings_df,10)) >>> {1198: [2857, 1195, 259, 607, 109, 2027, 592, 857, 295, 479]}
到目前为止,我们已经看到如何使用电影和评级数据集开发简单的 RE。但是,大多数推荐问题都假设我们有一个由(用户,项目,评级)元组集合形成的消费/评级数据集。这是协同过滤算法的大多数变体的起点,并且已经证明它们可以产生良好的结果;但是,在许多应用中,我们有大量的项目元数据(标签,类别和流派)可用于做出更好的预测。
这是将 FM 用于特征丰富的数据集的好处之一,因为有一种自然的方式可以在模型中包含额外的特征,并且可以使用维度参数 d 对高阶交互进行建模(参见下面的图 7)更多细节)。
最近的一些类型的研究表明,特征丰富的数据集可以提供更好的预测:i)Xiangnan He 和 Tat-Seng Chua,用于稀疏预测分析的神经分解机。在 SIGIR '17 的论文集中,2017 年 8 月 7 日至 11 日,日本东京新宿。ii)Jun Xiao,Hao Ye,Xiantian He,Hanwang Zhang,Fei Wu 和 Tat-Seng Chua(2017)Attentional Factorization Machines:Learning the Learning 通过注意网络的特征交互的权重 IJCAI,墨尔本,澳大利亚,2017 年 8 月 19 - 25 日。
这些论文解释了如何将现有数据转换为特征丰富的数据集,以及如何在数据集上实现 FM。因此,研究人员正在尝试使用 FM 来开发更准确和更强大的 RE。在下一节中,我们将看到一些使用 FM 和一些变体的示例。
分解机和推荐系统
在本节中,我们将看到两个使用 FM 开发更强大的推荐系统的示例 。我们将首先简要介绍 FM 及其在冷启动推荐问题中的应用。
然后我们将看到使用 FM 开发真实推荐系统的简短示例。之后,我们将看到一个使用称为神经分解机(NFM)的 FM 算法的改进版本的示例。
分解机
基于 FM 的技术处于个性化的前沿。它们已经被证明是非常强大的,具有足够的表达能力来推广现有模型,例如矩阵/张量分解和多项式核回归。换句话说,这种类型的算法是监督学习方法,其通过结合矩阵分解算法中不存在的二阶特征交互来增强线性模型的表现。
现有的推荐算法需要(用户,项目和评级)元组中的消费(产品)或评级(电影)数据集。这些类型的数据集主要用于协同过滤(CF)算法的变体。 CF 算法已得到广泛采用,并已证明可以产生良好的结果。但是,在许多情况下,我们有大量的项目元数据(标签,类别和流派),可以用来做出更好的预测。不幸的是,CF 算法不使用这些类型的元数据。
FM 可以使用这些特征丰富的(元)数据集。 FM 可以使用这些额外的特征来模拟指定维度参数 d 的高阶交互。最重要的是,FM 还针对处理大规模稀疏数据集进行了优化。因此,二阶 FM 模型就足够了,因为没有足够的信息来估计更复杂的交互:
图 7:表示具有特征向量 x 和目标 y 的个性化问题的示例训练数据集。这里的行指的是导演,演员和流派信息的电影和专栏
假设预测问题的数据集由设计矩阵X ∈ R^nxp
描述,如图 7 所示。在图 1 中,X
的第i
行X ∈ R^p
描述了一种情况,其中p
是实数估值变量。另一方面,y[i]
是第i
个情况的预测目标。或者,我们可以将此集合描述为元组(x, y)
的集合S
,其中(同样)x ∈ R^p
是特征向量,y
是其对应的目标或标签。
换句话说,在图 7 中,每行表示特征向量x[i]
与其相应的目标y[i]
。为了便于解释,这些特征分为活跃用户(蓝色),活动项目(红色),同一用户评级的其他电影(橙色),月份时间(绿色)和最后一部电影评级指标(棕色)。然后,FM 算法使用以下分解的交互参数来模拟x
中p
输入变量之间的所有嵌套交互(直到d
阶):
在等式中,v
表示与每个变量(用户和项目)相关联的 K 维潜在向量,并且括号运算符表示内积。具有数据矩阵和特征向量的这种表示在许多机器学习方法中是常见的,例如,在线性回归或支持向量机(SVM)中。
但是,如果您熟悉矩阵分解(MF)模型,则前面的等式应该看起来很熟悉:它包含全局偏差以及用户/项目特定的偏差,并包括用户项目交互。现在,如果我们假设每个x(j)
向量在位置u
和i
处仅为非零,我们得到经典的 MF 模型:
然而,用于推荐系统的 MF 模型经常遭受冷启动问题。我们将在下一节讨论这个问题。
冷启动问题和协同过滤方法
冷启动这个问题听起来很有趣,但顾名思义,它源于汽车。假设你住在阿拉斯加状态。由于寒冷,您的汽车发动机可能无法顺利启动,但一旦达到最佳工作温度,它将启动,运行并正常运行。
在推荐引擎的领域中,术语冷启动仅仅意味着对于引擎来说还不是最佳的环境以提供最佳结果。在电子商务中,冷启动有两个不同的类别:产品冷启动和用户冷启动。
冷启动是基于计算机的信息系统中的潜在问题,涉及一定程度的自动数据建模。具体而言,它涉及的问题是系统无法对尚未收集到足够信息的用户或项目进行任何推断。
冷启动问题在推荐系统中最为普遍。在协同过滤方法中,推荐系统将识别与活动用户共享偏好的用户,并提出志同道合的用户喜欢的项目(并且活跃用户尚未看到)。由于冷启动问题,这种方法将无法考虑社区中没有人评定的项目。
通过在基于内容的匹配和协同过滤之间采用混合方法,通常可以减少冷启动问题。尚未收到用户评级的新项目将根据社区分配给其他类似项目的评级自动分配评级。项目相似性将根据项目的基于内容的特征来确定。
使用基于 CF 的方法的推荐引擎根据用户操作推荐每个项目。项目具有的用户操作越多,就越容易分辨哪个用户对其感兴趣以及其他项目与之类似。随着时间的推移,系统将能够提供越来越准确的建议。在某个阶段,当新项目或用户添加到用户项目矩阵时,会出现此问题:
图 8:用户与项目矩阵有时会导致冷启动问题
在这种情况下,RE 还没有足够的知识来了解这个新用户或这个新项目。类似于 FM 的基于内容的过滤方法是可以结合以减轻冷启动问题的方法。
前两个方程之间的主要区别在于,FM 在潜在向量方面引入了高阶相互作用,潜在向量也受分类或标签数据的影响。这意味着模型超越了共现,以便在每个特征的潜在表示之间找到更强的关系。
问题的定义和制定
给定用户在电子商务网站上的典型会话期间执行的点击事件的序列 ,目标是预测用户是否购买或不购买,如果他们正在购买,他们会买什么物品。因此,这项任务可分为两个子目标:
- 用户是否会在此会话中购买物品?
- 如果是,那么将要购买的物品是什么?
为了预测在会话中购买的项目的数量,强大的分类器可以帮助预测用户是否将购买该项目的 。在最初实现 FM 后,训练数据的结构应如下:
图 9:用户与项目/类别/历史表可用于训练推荐模型
为了准备这样的训练集,我们可以使用 pandas 中的get_dummies()
方法将所有列转换为分类数据,因为 FM 模型使用表示为整数的分类数据。
我们使用两个函数TFFMClassifier
和TFFMRegressor
来进行预测(参见items.py
)并分别计算 MSE(参见来自tffm
库的quantity.py
脚本(在 MIT 许可下))。tffm
是基于 TensorFlow 的 FM 和 pandas 实现,用于预处理和结构化数据。这个基于 TensorFlow 的实现提供了一个任意顺序(>= 2
)分解机,它支持:
- 密集和稀疏的输入
- 不同的(基于梯度的)优化方法
- 通过不同的损失函数进行分类/回归(logistic 和 mse 实现)
- 通过 TensorBoard 记录
另一个好处是推理时间相对于特征数量是线性的。
我们要感谢作者并引用他们的工作如下:Mikhail Trofimov,Alexander Novikov,TFFM:TensorFlow 实现任意顺序分解机,GitHub 仓库,2016。
要使用此库,只需在终端上发出以下命令:
$ sudo pip3 install tffm # For Python3.x $ sudo pip install tffm # For Python 2.7.x
在开始实现之前,让我们看一下我们将在本例中使用的数据集。
数据集描述
例如,我将使用 RecSys 2015 挑战数据集来说明如何拟合 FM 模型以获得个性化推荐。该数据包含电子商务网站的点击和购买事件,以及其他项目类别数据。数据集的大小约为 275MB,可以从此链接下载。
有三个文件和一个自述文件;但是,我们将使用youchoose-buys.dat
(购买活动)和youchoose-clicks.dat
(点击活动):
youchoose-clicks.dat
:文件中的每条记录/行都包含以下字段:
- 会话 ID:一个会话中的一次或多次点击
- 时间戳:发生点击的时间
- 项目 ID:项目的唯一标识符
- 类别:项目的类别
youchoose-buys.dat
:文件中的每条记录/行都包含以下字段:
- 会话 ID:会话 ID:会话中的一个或多个购买事件
- 时间戳:购买发生的时间
- 物料 ID:物品的唯一标识符
- 价格:商品的价格
- 数量:购买了多少件商品
youchoose-buys.dat
中的会话 ID 也存在于youchoose-clicks.dat
文件中。这意味着具有相同会话 ID 的记录一起形成会话期间某个用户的点击事件序列。
会话可能很短(几分钟)或很长(几个小时),可能只需点击一下或点击几百次。这一切都取决于用户的活动。
实现工作流程
让我们开发一个预测并生成solution.data
文件的推荐模型。这是一个简短的工作流程:
- 下载并加载 RecSys 2015 挑战数据集,并复制到本章代码库的
data
文件夹中 - 购买数据包含会话 ID,时间戳,项目 ID,类别和数量。此外,
youchoose-clicks.dat
包含会话 ID,时间戳,项目 ID 和类别。我们不会在这里使用时间戳。我们删除时间戳,对所有列进行单热编码, 合并买入和点击数据集以使数据集特征丰富。在预处理之后,数据看起来类似于图 11 中所示的数据。 - 为简化起见,我们仅考虑前 10,000 个会话,并将数据集拆分为训练(75%)和测试(25%)集。
- 然后,我们将测试分为正常(保留历史数据)和冷启动(通过删除历史数据),以区分具有历史记录或没有历史记录的用户/项目的模型。
- 然后我们使用
tffm
训练我们的 FM 模型,这是 TensorFlow 中 FM 的实现,并使用训练数据训练模型。 - 最后,我们在正常和冷启动数据集上评估模型。
图 10:使用 FM 预测会话中已购买项目列表的工作流程
预处理
如果我们想充分利用类别和扩展的历史数据,我们需要加载数据并将其转换为正确的格式。因此,在准备训练集之前,必须进行一些预处理。让我们从加载包和模块开始:
import tensorflow as tf import pandas as pd from collections import Counter from tffm import TFFMClassifier from sklearn.metrics import mean_squared_error from sklearn.model_selection import train_test_split import numpy as np from sklearn.metrics import accuracy_score import os
我是 ,假设您已经从前面提到的链接下载了数据集。现在让我们加载数据集:
buys = open('data/yoochoose-buys.dat', 'r') clicks = open('data/yoochoose-clicks.dat', 'r')
现在为点击创建 pandas 数据帧并购买数据集:
initial_buys_df = pd.read_csv(buys, names=['Session ID', 'Timestamp', 'Item ID', 'Category', 'Quantity'], dtype={'Session ID': 'float32', 'Timestamp': 'str', 'Item ID': 'float32','Category': 'str'}) initial_buys_df.set_index('Session ID', inplace=True) initial_clicks_df = pd.read_csv(clicks, names=['Session ID', 'Timestamp', 'Item ID', 'Category'],dtype={'Category': 'str'}) initial_clicks_df.set_index('Session ID', inplace=True)
我们不需要在这个例子中使用时间戳,所以让我们从数据帧中删除它们:
initial_buys_df = initial_buys_df.drop('Timestamp', 1) print(initial_buys_df.head()) # first five records print(initial_buys_df.shape) # shape of the dataframe >>>
initial_clicks_df = initial_clicks_df.drop('Timestamp', 1) print(initial_clicks_df.head()) print(initial_clicks_df.shape) >>>
由于在此示例中我们不使用时间戳,因此从数据帧(df
)中删除Timestamp
列:
initial_buys_df = initial_buys_df.drop('Timestamp', 1) print(initial_buys_df.head(n=5)) print(initial_buys_df.shape) >>>
initial_clicks_df = initial_clicks_df.drop('Timestamp', 1) print(initial_clicks_df.head(n=5)) print(initial_clicks_df.shape) >>>
让我们选取前 10,000 名购买用户:
x = Counter(initial_buys_df.index).most_common(10000) top_k = dict(x).keys() initial_buys_df = initial_buys_df[initial_buys_df.index.isin(top_k)] print(initial_buys_df.head()) print(initial_buys_df.shape) >>>
initial_clicks_df = initial_clicks_df[initial_clicks_df.index.isin(top_k)] print(initial_clicks_df.head()) print(initial_clicks_df.shape) >>>
现在让我们创建索引的副本,因为我们还将对其应用单热编码:
initial_buys_df['_Session ID'] = initial_buys_df.index print(initial_buys_df.head()) print(initial_buys_df.shape) >>>
正如我们之前提到的 ,我们可以将历史参与数据引入我们的 FM 模型。我们将使用一些group_by
魔法生成整个用户参与的历史记录。首先,我们对所有列进行单热编码以获得点击和购买:
transformed_buys = pd.get_dummies(initial_buys_df) print(transformed_buys.shape) >>> (106956, 356) transformed_clicks = pd.get_dummies(initial_clicks_df)print(transformed_clicks.shape) >>> (209024, 56)
现在是时候汇总项目和类别的历史数据了:
filtered_buys = transformed_buys.filter(regex="Item.*|Category.*") print(filtered_buys.shape) >>> (106956, 354) filtered_clicks = transformed_clicks.filter(regex="Item.*|Category.*") print(filtered_clicks.shape) >>> (209024, 56) historical_buy_data = filtered_buys.groupby(filtered_buys.index).sum() print(historical_buy_data.shape) >>> (10000, 354) historical_buy_data = historical_buy_data.rename(columns=lambda column_name: 'buy history:' + column_name) print(historical_buy_data.shape) >>> (10000, 354) historical_click_data = filtered_clicks.groupby(filtered_clicks.index).sum() print(historical_click_data.shape) >>> (10000, 56) historical_click_data = historical_click_data.rename(columns=lambda column_name: 'click history:' + column_name)
然后我们合并每个user_id
的历史数据:
merged1 = pd.merge(transformed_buys, historical_buy_data, left_index=True, right_index=True) print(merged1.shape) merged2 = pd.merge(merged1, historical_click_data, left_index=True, right_index=True) print(merged2.shape) >>> (106956, 710) (106956, 766)
然后我们将数量作为目标并将其转换为二元:
y = np.array(merged2['Quantity'].as_matrix())
现在让我们将y
转换为二元:如果购买发生,为1
;否则为0
:
for i in range(y.shape[0]): if y[i]!=0: y[i]=1 else: y[i]=0 print(y.shape) print(y[0:100]) print(y, y.shape[0]) print(y[0]) print(y[0:100]) print(y, y.shape) >>>
训练 FM 模型
由于我们准备了数据集,下一个任务是创建 MF 模型。首先,让我们将数据分成训练和测试集:
X_tr, X_te, y_tr, y_te = train_test_split(merged2, y, test_size=0.25)
然后我们将测试数据分成一半,一个用于正常测试,一个用于冷启动测试:
X_te, X_te_cs, y_te, y_te_cs = train_test_split(X_te, y_te, test_size=0.5)
现在让我们在数据帧中包含会话 ID 和项目 ID:
test_x = pd.DataFrame(X_te, columns = ['Item ID']) print(test_x.head()) >>>
test_x_cs = pd.DataFrame(X_te_cs, columns = ['Item ID']) print(test_x_cs.head()) >>>
然后我们从数据集中删除不需要的特征:
X_tr.drop(['Item ID', '_Session ID', 'click history:Item ID', 'buy history:Item ID', 'Quantity'], 1, inplace=True) X_te.drop(['Item ID', '_Session ID', 'click history:Item ID', 'buy history:Item ID', 'Quantity'], 1, inplace=True) X_te_cs.drop(['Item ID', '_Session ID', 'click history:Item ID', 'buy history:Item ID', 'Quantity'], 1, inplace=True)
然后我们需要将DataFrame
转换为数组:
ax_tr = np.array(X_tr) ax_te = np.array(X_te) ax_te_cs = np.array(X_te_cs)
既然 pandas DataFrame
已经转换为 NumPy 数组,我们需要做一些null
处理。我们简单地用零替换 NaN:
ax_tr = np.nan_to_num(ax_tr) ax_te = np.nan_to_num(ax_te) ax_te_cs = np.nan_to_num(ax_te_cs)
然后我们用优化的超参数实例化 TF 模型进行分类:
model = TFFMClassifier( order=2, rank=7, optimizer=tf.train.AdamOptimizer(learning_rate=0.001), n_epochs=100, batch_size=1024, init_std=0.001, reg=0.01, input_type='dense', log_dir = ' logs/', verbose=1, seed=12345 )
在我们开始训练模型之前,我们必须为冷启动准备数据:
cold_start = pd.DataFrame(ax_te_cs, columns=X_tr.columns)
和前面提到的一样,如果我们只能访问类别而没有历史点击/购买数据,我们也有兴趣了解会发生什么。让我们删除cold_start
测试集的历史点击和购买数据:
for column in cold_start.columns: if ('buy' in column or 'click' in column) and ('Category' not in column): cold_start[column] = 0
现在让我们训练模型:
model.fit(ax_tr, y_tr, show_progress=True)
其中一项最重要的任务是预测会议中的购买事件:
predictions = model.predict(ax_te) print('accuracy: {}'.format(accuracy_score(y_te, predictions))) print("predictions:",predictions[:10]) print("actual value:",y_te[:10]) >>> accuracy: 1.0 predictions: [0 0 1 0 0 1 0 1 1 0] actual value: [0 0 1 0 0 1 0 1 1 0] cold_start_predictions = model.predict(ax_te_cs) print('Cold-start accuracy: {}'.format(accuracy_score(y_te_cs, cold_start_predictions))) print("cold start predictions:",cold_start_predictions[:10]) print("actual value:",y_te_cs[:10]) >>> Cold-start accuracy: 1.0 cold start predictions: [1 1 1 1 1 0 1 0 0 1] actual value: [1 1 1 1 1 0 1 0 0 1]
然后让我们将预测值添加到测试数据中:
test_x["Predicted"] = predictions test_x_cs["Predicted"] = cold_start_predictions
现在是时候找到测试数据中每个session_id
的所有买入事件并检索相应的项目 ID:
sess = list(set(test_x.index)) fout = open("solution.dat", "w") print("writing the results into .dat file....") for i in sess: if test_x.loc[i]["Predicted"].any()!= 0: fout.write(str(i)+";"+','.join(s for s in str(test_x.loc[i]["Item ID"].tolist()).strip('[]').split(','))+'\n') fout.close() >>> writing the results into .dat file....
然后我们对冷启动测试数据做同样的事情:
sess_cs = list(set(test_x_cs.index)) fout = open("solution_cs.dat", "w") print("writing the cold start results into .dat file....") for i in sess_cs: if test_x_cs.loc[i]["Predicted"].any()!= 0: fout.write(str(i)+";"+','.join(s for s in str(test_x_cs.loc[i]["Item ID"].tolist()).strip('[]').split(','))+'\n') fout.close() >>> writing the cold start results into .dat file.... print("completed..!!") >>> completed!!
最后,我们销毁模型以释放内存:
model.destroy()
另外,我们可以看到文件的示例内容:
11009963;214853767 10846132;214854343, 214851590 8486841;214848315 10256314;214854125 8912828;214853085 11304897;214567215 9928686;214854300, 214819577 10125303;214567215, 214853852 10223609;214854358
考虑到我们使用相对较小的数据集来拟合我们的模型, 实验结果很好。正如预期的那样,如果我们可以通过项目购买和点击访问所有信息集,则更容易生成预测,但我们仍然只使用汇总类别数据获得冷启动建议的预测。
既然我们已经看到客户将在每个会话中购买,那么计算两个测试集的均方误差将会很棒。TFFMRegressor
方法可以帮助我们解决这个问题。为此,请使用quantity.py
脚本。
首先,问题是如果我们只能访问类别而没有历史点击/购买数据会发生什么。让我们删除cold_start
测试集的历史点击和购买数据:
for column in cold_start.columns: if ('buy' in column or 'click' in column) and ('Category' not in column): cold_start[column] = 0
让我们创建 MF 模型。您可以使用超参数:
reg_model = TFFMRegressor( order=2, rank=7, optimizer=tf.train.AdamOptimizer(learning_rate=0.1), n_epochs=100, batch_size=-1, init_std=0.001, input_type='dense', log_dir = ' logs/', verbose=1 )
在前面的代码块中,随意放入您自己的日志记录路径。现在是时候使用正常和冷启动训练集训练回归模型:
reg_model.fit(X_tr, y_tr, show_progress=True)
然后我们计算两个测试集的均方误差:
predictions = reg_model.predict(X_te) print('MSE: {}'.format(mean_squared_error(y_te, predictions))) print("predictions:",predictions[:10]) print("actual value:",y_te[:10]) cold_start_predictions = reg_model.predict(X_te_cs) print('Cold-start MSE: {}'.format(mean_squared_error(y_te_cs, cold_start_predictions))) print("cold start predictions:",cold_start_predictions[:10]) print("actual value:",y_te_cs[:10]) print("Regression completed..!!") >>>MSE: 0.4897467853668941 predictions: [ 1.35086 0.03489107 1.0565269 -0.17359206 -0.01603088 0.03424695 2.29936886 1.65422797 0.01069662 0.02166392] actual value: [1 0 1 0 0 0 1 1 0 0] Cold-start MSE: 0.5663486183636738 cold start predictions: [-0.0112379 1.21811676 1.29267406 0.02357371 -0.39662406 1.06616664 -0.10646269 0.00861482 1.22619736 0.09728943] actual value: [0 1 1 0 1 1 0 0 1 0] Regression completed..!!
最后,我们销毁模型以释放内存:
reg_model.destroy()
因此,从训练数据集中删除类别列会使 MSE 更小,但这样做意味着我们无法解决冷启动建议问题。考虑到我们使用相对较小的数据集的条件,实验结果很好。
正如预期的那样,如果我们可以通过项目购买和点击访问完整的信息设置,则更容易生成预测,但我们仍然只使用汇总的类别数据获得冷启动建议的预测。
改进的分解机
Web 应用的许多预测任务需要对分类变量(例如用户 ID)和人口统计信息(例如性别和职业)进行建模。为了应用标准 ML 技术,需要通过单热编码(或任何其他技术)将这些分类预测变换器转换为一组二元特征。这使得得到的特征向量高度稀疏。要从这种稀疏数据中有效地学习,考虑特征之间的相互作用是很重要的。
在上一节中,我们看到 FM 可以有效地应用于模型二阶特征交互。但是,FM 模型以线性方式进行交互,如果您想捕获真实世界数据的非线性和固有复杂结构,则这种方式是不够的。
Xiangnan He 和 Jun Xiao 等。为了克服这一局限,我们提出了一些研究计划,如神经因子分解机(NFM)和注意因子分解机(AFM)。
有关更多信息,请参阅以下文章:
- Xiangnan He 和 Tat-Seng Chua,用于稀疏预测分析的神经分解机。在 SIGIR '17,新宿,东京,日本,2017 年 8 月 7 日至 11 日的会议录。
- Jun Xiao,Hao Ye,Xiantian He,Hanwang Zhang,Fei Wu 和 Tat-Seng Chua(2017),注意分解机:通过注意网络学习特征交互的权重 IJCAI,墨尔本,澳大利亚,2017 年 8 月 19 - 25 日。
通过在建模二阶特征相互作用中无缝地组合 FM 的线性度和在建模高阶特征相互作用中神经网络的非线性,NFM 可用于在稀疏设置下进行预测。
另一方面,即使所有特征交互具有相同的权重,AFM 也可用于对数据建模,因为并非所有特征交互都同样有用且具有预测性。
在下一节中,我们将看到使用 NFM 进行电影推荐的示例。
神经分解机
使用原始 FM 算法,其表现可能受到建模方式阻碍,它使用相同权重建模所有特征交互,因为并非所有特征交互都同样有用且具有预测性。例如,与无用特征的交互甚至可能引入噪声并对表现产生不利影响。
最近,Xiangnan H.等。提出了一种称为神经分解机(NFM)的 FM 算法的改进版本。 NFM 在建模二阶特征相互作用中无缝地结合了 FM 的线性度,在建模高阶特征相互作用时无缝地结合了神经网络的非线性。从概念上讲,NFM 比 FM 更具表现力,因为 FM 可以被看作是没有隐藏层的 NFM 的特例。
数据集描述
我们使用 MovieLens 数据进行个性化标签推荐。它包含电影上 668,953 个用户的标签应用。使用单热编码将每个标签应用(用户 ID,电影 ID 和标签)转换为特征向量。这留下了 90,445 个二元特征,称为ml-tag
数据集。
我使用 Perl 脚本将其从.dat
转换为.libfm
格式。转换程序在此链接(第 2.2.1 节)中描述。转换后的数据集包含用于训练,验证和测试的文件,如下所示:
ml-tag.train.libfm
ml-tag.validation.libfm
ml-tag.test.libfm
有关此文件格式的更多信息,请参阅此链接。
NFM 和电影推荐
我们使用 TensorFlow 并复用来自这个 GitHub 的扩展 NFM 实现。这是 FM 的深度版本,与常规 FM 相比更具表现力。仓库有三个文件,即NeuralFM.py
,FM.py
和LoadData.py
:
FM.py
用于训练数据集。这是 FM 的原始实现。NeuralFM.py
用于训练数据集。这是 NFM 的原始实现,但有一些改进和扩展。LoadData.py
用于以 libfm 格式预处理和加载数据集。
模型训练
首先,我们使用以下命令训练 FM 模型。该命令还包括执行训练所需的参数:
$ python3 FM.py --dataset ml-tag --epoch 20 --pretrain -1 --batch_size 4096 --lr 0.01 --keep 0.7 >>> FM: dataset=ml-tag, factors=16, #epoch=20, batch=4096, lr=0.0100, lambda=0.0e+00, keep=0.70, optimizer=AdagradOptimizer, batch_norm=1 #params: 1537566 Init: train=1.0000, validation=1.0000 [5.7 s] Epoch 1 [13.9 s] train=0.5413, validation=0.6005 [7.8 s] Epoch 2 [14.2 s] train=0.4927, validation=0.5779 [8.3 s] … Epoch 19 [15.4 s] train=0.3272, validation=0.5429 [8.1 s] Epoch 20 [16.6 s] train=0.3242, validation=0.5425 [7.8 s]
训练结束后,训练好的模型将保存在主目录的pretrain
文件夹中:
将模型保存为预训练文件。
此外,我已尝试使用以下代码进行验证和训练损失的训练和验证误差:
# Plot loss over time plt.plot(epoch_list, train_err_list, 'r--', label='FM training loss per epoch', linewidth=4) plt.title('FM training loss per epoch') plt.xlabel('Epoch') plt.ylabel('Training loss') plt.legend(loc='upper right') plt.show() # Plot accuracy over time plt.plot(epoch_list, valid_err_list, 'r--', label='FM validation loss per epoch', linewidth=4) plt.title('FM validation loss per epoch') plt.xlabel('Epoch') plt.ylabel('Validation loss') plt.legend(loc='upper left') plt.show()
前面的代码生成绘图,显示 FM 模型中每次迭代的训练与验证损失:
图 11:FM 模型中每次迭代的训练与验证损失
如果查看前面的输出日志,最佳训练(即验证和训练)将在第 20 次和最后一次迭代时进行。但是,您可以进行更多迭代以改进训练,这意味着评估步骤中的 RMSE 值较低:
Best Iter(validation)= 20 train = 0.3242, valid = 0.5425 [490.9 s]
现在让我们使用以下命令训练 NFM 模型(但也使用参数):
$ python3 NeuralFM.py --dataset ml-tag --hidden_factor 64 --layers [64] --keep_prob [0.8,0.5] --loss_type square_loss --activation relu --pretrain 0 --optimizer AdagradOptimizer --lr 0.01 --batch_norm 1 --verbose 1 --early_stop 1 --epoch 20 >>> Neural FM: dataset=ml-tag, hidden_factor=64, dropout_keep=[0.8,0.5], layers=[64], loss_type=square_loss, pretrain=0, #epoch=20, batch=128, lr=0.0100, lambda=0.0000, optimizer=AdagradOptimizer, batch_norm=1, activation=relu, early_stop=1 #params: 5883150 Init: train=0.9911, validation=0.9916, test=0.9920 [25.8 s] Epoch 1 [60.0 s] train=0.6297, validation=0.6739, test=0.6721 [28.7 s] Epoch 2 [60.4 s] train=0.5646, validation=0.6390, test=0.6373 [28.5 s] … Epoch 19 [53.4 s] train=0.3504, validation=0.5607, test=0.5587 [25.7 s] Epoch 20 [55.1 s] train=0.3432, validation=0.5577, test=0.5556 [27.5 s]
此外,我尝试使用以下代码使验证和训练损失的训练和验证误差可见:
# Plot test accuracy over time plt.plot(epoch_list, test_err_list, 'r--', label='NFM test loss per epoch', linewidth=4) plt.title('NFM test loss per epoch') plt.xlabel('Epoch') plt.ylabel('Test loss') plt.legend(loc='upper left') plt.show()
前面的代码在 NFM 模型中产生每次迭代的训练与验证损失:
图 12:NFM 模型中每次迭代的训练与验证损失
对于 NFM 模型,最佳训练(用于验证和训练)发生在第 20 次和最后一次迭代。但是,您可以进行更多迭代以改进训练,这意味着评估步骤中的 RMSE 值较低:
Best Iter (validation) = 20 train = 0.3432, valid = 0.5577, test = 0.5556 [1702.5 s]
模型评估
现在,要评估原始 FM 模型,请执行以下命令:
$ python3 FM.py --dataset ml-tag --epoch 20 --batch_size 4096 --lr 0.01 --keep 0.7 --process evaluate Test RMSE: 0.5427
注意
对于 TensorFlow 上的 Attentional Factorization Machines 实现,感兴趣的读者可以参考此链接中的 GitHub 仓库。但请注意,某些代码可能无效。我将它们更新为兼容 TensorFlow v1.6。因此,我强烈建议您使用本书提供的代码。
要评估 NFM 模型,只需将以下行添加到NeuralFM.py
脚本中的main()
方法,如下所示:
# Model evaluation print("RMSE: ") print(model.evaluate(data.Test_data)) #evaluate on test set >>> RMSE: 0.5578330373003925
因此,RMSE 几乎与 FM 模型相同。现在让我们看看每次迭代的测试误差:
# Plot test accuracy over time plt.plot(epoch_list, test_err_list, 'r--', label='NFM test loss per epoch', linewidth=4) plt.title('NFM test loss per epoch') plt.xlabel('Epoch') plt.ylabel('Test loss') plt.legend(loc='upper left') plt.show()
前面的代码绘制了 NFM 模型中每次迭代的测试损失:
图 13:NFM 模型中每次迭代的测试损失
总结
在本章中,我们讨论了如何使用 TensorFlow 开发可扩展的推荐系统。我们已经看到推荐系统的一些理论背景,并在开发推荐系统时使用协同过滤方法。在本章后面,我们看到了如何使用 SVD 和 K 均值来开发电影推荐系统。
最后,我们了解了如何使用 FM 和一种称为 NFM 的变体来开发更准确的推荐系统,以便处理大规模稀疏矩阵。我们已经看到处理冷启动问题的最佳方法是使用 FM 协同过滤方法。
下一章是关于设计由批评和奖励驱动的 ML 系统。我们将看到如何应用 RL 算法为现实数据集制作预测模型。
十、OpenAI Gym
OpenAI Gym 是一个开源 Python 框架,由非营利性 AI 研究公司 OpenAI 开发,作为开发和评估 RL 算法的工具包。它给我们提供了一组测试问题,称为环境,我们可以编写 RL 算法来解决。这使我们能够将更多的时间用于实现和改进学习算法,而不是花费大量时间来模拟环境。此外,它为人们提供了一种比较和审查其他算法的媒介。
OpenAI 环境
OpenAI Gym 拥有一系列环境。在编写本书时,可以使用以下环境:
- 经典控制和玩具文本:来自 RL 文献的小规模任务。
- 算法:执行计算,例如添加多位数和反转序列。这些任务中的大多数都需要记忆,并且可以通过改变序列长度来改变它们的难度。
- Atari:经典 Atari 游戏,使用街机学习环境,屏幕图像或 RAM 作为输入。
- 棋盘游戏:目前,我们已经将围棋游戏包括在
9x9
和19x19
板上,而 Pachi 引擎则作为对手。 - 2D 和 3D 机器人:允许在模拟中控制机器人。这些任务使用 MuJoCo 物理引擎,该引擎专为快速准确的机器人仿真而设计。一些任务改编自 RLLab。
env
类
OpenAI Gym 允许使用env
类,它封装了环境和任何内部动态。此类具有不同的方法和属性,使您可以实现创建新环境。最重要的方法名为reset
,step
和render
:
reset
方法的任务是通过将环境初始化为初始状态来重置环境。在重置方法中,必须包含构成环境的元素的定义(在这种情况下,机械臂的定义,要抓取的对象及其支持)。step
方法是用于在时间上推进环境的 。它需要输入操作并将新观察结果返回给智能体。在该方法中,必须定义运动动态管理,状态和奖励计算以及剧集完成控制。- 最后一种方法是
render
, 用于显示当前状态。
使用框架提出的env
类作为新环境的基础,它采用工具包提供的通用接口。
这样,构建的环境可以集成到工具包的库中,并且可以从 OpenAI Gym 社区的用户所做的算法中学习它们的动态。
安装并运行 OpenAI Gym
有关如何使用和运行 OpenAI Gym 的更多详细说明,请参阅(此链接)的官方文档页面。使用以下命令可以实现 OpenApp Gym 的最小安装:
git clone https://github.com/openai/gym cd gym pip install -e
安装 OpenAI Gym 之后,您可以在 Python 代码中实例化并运行环境:
import gym env = gym.make('CartPole-v0') obs = env.reset() for step_idx in range(500): env.render() obs, reward, done, _ = env.step(env.action_space.sample())
此代码段将首先导入gym
库。然后它创建了 Cart-Pole 环境的实例 ,这是 RL 中的经典问题。 Cart-Pole 环境模拟安装在推车上的倒立摆。钟摆最初是垂直的,你的目标是保持其垂直平衡。控制摆锤的唯一方法是选择水平方向让推车移动(向左或向右)。
上面的代码运行500
时间步的环境,并选择随机操作在每一步执行。因此,您在下面的视频中看到,杆不能长时间保持稳定。奖励是通过在杆距垂直方向超过 15 度之前经过的时间步数来衡量的。您保持在此范围内的时间越长,您的总奖励就越高。
Q-Learning 算法
解决 RL 问题需要在学习过程中估计评估函数。该函数必须能够通过奖励的总和来评估策略的成功。
Q-Learning 的基本思想是算法学习整个状态和动作空间的最优评估函数(S×A
)。这种所谓的Q
函数以Q: S×A -> R
的形式提供匹配,其中R
是在状态s ∈ S
中执行的动作a ∈ A
的未来奖励的期望值。一旦智能体学会了最佳函数Q
,它就能够识别哪种行为将导致某种状态下最高的未来奖励。
实现 Q-Learning 算法的最常用示例之一涉及使用表。该表的每个单元格是值Q(s; a) = R
并且它被初始化为 0.由智能体执行的动作a ∈ A
是使用相对于Qε
的贪婪策略来选择的。
Q-Learning 算法的基本思想是训练规则,它更新表格元素Q(s; a) = R
。
该算法遵循以下基本步骤:
- 任意初始化
Q(s; a) = R
。 - 对每个剧集重复以下:
- 初始化
s
。 - 重复(对于剧集的每一步):
- 使用从
Q
派生的策略从s ∈ S
中选择一个动作a ∈ A
。 - 选取动作
a
,观察r
,s'
:
Q(s; a) <= Q(s; a) + a · (r + γ · max Q(s'; a) - Q(s; a)) s': s <- s'
- 继续直到
s
的终点。
我们在下图中描述了算法:
图 2:Q-Learning 算法
让我们总结一下 Q 值更新过程中使用的参数:
α
是学习率,设置在 0 和 1 之间。将其设置为 0 意味着 Q 值永远不会更新,因此不会学习任何内容。设置较高的值(如 0.9)意味着可以快速进行学习。γ
是折扣因子,设置在 0 和 1 之间。这模拟了未来奖励的值低于直接奖励的事实。在数学上,需要将折扣因子设置为小于 1 以使算法收敛。max Q(s'; a)
是在当前状态之后的状态下可获得的最大奖励,即之后采取最佳行动的奖励。
FrozenLake 环境
智能体控制角色在4×4
网格世界中的移动。网格的一些瓷砖是可行走的,而其他瓷砖则导致落入水中。另外,智能体的移动方向是不确定的,并且仅部分地取决于所选择的方向。智能体因找到目标图块的可行走路径而获得奖励:
图 3:Frozen-Lake v0 网格字的表示
使用如下网格描述上面所示的表面:
SFFF (S: starting point, safe) FHFH (F: frozensurface, safe) FFFH (H: hole, fall to yourdoom) HFFG (G: goal, where the frisbee islocated)
当我们到达目标或陷入一个洞时,这一集结束。如果达到目标,我们会收到1
的奖励,否则会收到0
。
针对 FrozenLake 问题的 Q-Learning
在为高度结构化数据提供良好功能方面,神经网络非常强大。
为了解决 FrozenLake 问题,我们将构建一个单层网络,该网络采用1×16
向量中编码的状态并学习最佳移动(动作),在向量中映射可能的动作长度为四。
TensorFlow 深度学习第二版:6~10(5)https://developer.aliyun.com/article/1426806