数据挖掘实验一:分类技术——二分网络上的链路预测
实验一:分类技术——二分网络上的链路预测实验内容采用二分网络模型,对ml-1m文件夹中的“用户—电影”打分数据进行建模,考虑将用户信息、电影详细信息、以及打分分值作为该网络上的边、点的权重;根据网络结构特征给出节点相似性度量指标;基于相似性在二分网络上进行链路预测;画出ROC曲线来度量预测方法的准确性。分析及设计导入数据并初步分析处理数据:观察所给的文件类型为.dat格式,即纯文本格式,pytho
实验一:分类技术——二分网络上的链路预测
实验内容
-
采用二分网络模型,对ml-1m文件夹中的“用户—电影”打分数据进行建模,考虑将用户信息、电影详细信息、以及打分分值作为该网络上的边、点的权重;
-
根据网络结构特征给出节点相似性度量指标;
-
基于相似性在二分网络上进行链路预测;
-
画出ROC曲线来度量预测方法的准确性。
分析及设计
-
导入数据并初步分析处理数据:
-
观察所给的文件类型为.dat格式,即纯文本格式,python中有专门处理该类型文件的包和函数;
-
观察源数据包中的数据,都是拿“::“分隔的,所以我们就可以根据“::”分隔号进行分割每一行数据;
-
用pandas包中的read_table函数读取ratings.dat文件,得到一个DataFrame形式的数据(输出形式类似于表);
- 原始数据(前5条):
1::1193::5::978300760 1::661::3::978302109 1::914::3::978301968 1::3408::4::978300275 1::2355::5::978824291
- 输出数据(前5条):
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
-
DataFrame数据可以轻易的通过点操作符和列名取到当中的任意一个数据,同时它还可以被遍历,并且Pandas中有丰富的函数来操作DataFrame数据,所以此时数据包中的数据已经变成可操作,可处理,可修改的变量;
-
对于导入的ratings.dat数据,我们需要把它转化成矩阵的形式,这样它就可以参与直接计算了;
-
划分训练集和测试集,90%用于训练集;
-
观察数据,以训练集的UserID为行、MovieID为列、Rating为值,建立一个users_size*movies_size的矩阵;
-
-
建立小型二部图网络结构模型,计算相似度关系:
- 假设有三个用户,三部电影,可以构建如下二部图:
-
传统的二部图网络边没有权值,只是用1和0来代表连通和不连通,可以在边上加一个关于评分的权值,实现有权重的二部图网络推荐:
-
把用户和电影看成抽象的节点,算法利用的信息都隐藏在用户和电影的连通关系中;
-
s i m ( i , j ) = 1 d ( I j ) ∑ a = 1 m w i a w j a d ( u a ) sim(i,j)=\frac{1}{d(I_j)}\sum_{a=1}^m{\frac{w_{ia}w_{ja}}{d(u_a)}} sim(i,j)=d(Ij)1a=1∑md(ua)wiawja
-
s i m ( i , j ) sim(i,j) sim(i,j):代表电影 i i i和电影 j j j的相似度;
-
w w w:代表的是权重,根据用户的打分计算获得,权重并不一定是一个均匀分布的量,因为单纯把评分5星的权重设置成评分1星权重的5倍是不太合理的。权重更应该呈现出是打分的下凸函数的形式,设 w = f ( r a t i n g ) w=f(rating) w=f(rating),1、2星时的 w w w应该都很接近0,5星时的 w w w应该为1;
-
w i a w j a w_{ia}w_{ja} wiawja:代表用户 a a a对电影 i i i权重乘以用户 a a a对电影 j j j的权重;
- 如果用户 a a a同时对电影 i i i和 j j j都产生行为,那就说明电影 i i i和电影j可能是相似的,但有多相似?取决于用户 a a a对电影 i i i、 j j j的评价(即权值)。如果两个电影的评价都很高,则该项就会较大,即代表用户喜欢电影 i i i时,有较大的可能性也喜欢电影 j j j;
-
d ( I j ) = ∑ k = 1 l w k j d(I_j)=\sum_{k=1}^{l}w_{kj} d(Ij)=∑k=1lwkj:代表所有用户给电影 j j j权重的和,即电影 j j j的所有评价分;
- 如果该项很高,可以从一定程度上认为电影 j j j是流行的、受欢迎的。一个受欢迎的电影可以和大多电影产生关联,但这并不能说明它们就是相似的。什么样的行为在相似度矩阵里作用大呢?应该是小众的且打分高的电影,因为根据这些电影我们可以较为准确得定位一类电影。而受欢迎的电影显然是要被抑制的,因为它不能代表一类电影或用户,所以乘以 d ( I j ) d(I_j) d(Ij)的倒数来抑制流行电影的影响;
-
d ( u a ) = ∑ k = 1 m w a k d(u_a)=\sum_{k=1}^{m}{w_{ak}} d(ua)=∑k=1mwak :代表用户 a a a的全部打分和,用于判断用户 a a a的选择是否有代表性;
- 一个有选择性看电影的用户对相似度矩阵的帮助更大。如果一个用户看过很多电影并且给很多电影打了高分,则该项值很大,但我们此时不能从该用户的行为中得到较大的区分电影的帮助,所以我们要抑制这种行为,故乘以 d ( u a ) d(u_a) d(ua)的倒数;
-
-
-
-
将数据带入到上述公式中,我们就可以得到一个依据评分的电影相似度矩阵;事实上,我们还可以计算其他角度的相似度矩阵,比如根据电影的年龄受众建立依据年龄的相似度矩阵,将这些相似度矩阵乘以某些加权系数然后相加就可以得到一个更全面的相似度矩阵,但是本次实验并没有实现。
-
此时,我们就有了从一部电影得到另一部电影的依据——依据评分的相似度矩阵。
-
根据相似度关系,训练:
- 训练过程:
F
i
=
(
S
i
m
∗
W
i
T
)
T
F_i=(Sim*W_{i}^{T})^{T}
Fi=(Sim∗WiT)T
- 通过评价相似度矩阵和训练集:用户-电影矩阵的转置相乘后转置,就可以得到推荐分数矩阵 F i F_i Fi;
- F i F_i Fi的每一行代表一个电影,每一列代表一个用户,值为用户对该电影的推荐分数;此时我们就有了向用户推荐没看过电影的依据;
- 通过python内置函数以及循环等操作对 F i F_i Fi进行排序,得到推荐排序矩阵,此时矩阵中( j j j, i i i)中的值代表用户 i i i认为电影 j j j在所有电影中的排名;
- 训练结果:排序后的F_sort矩阵;
- 训练过程:
F
i
=
(
S
i
m
∗
W
i
T
)
T
F_i=(Sim*W_{i}^{T})^{T}
Fi=(Sim∗WiT)T
-
测试结果输出及ROC曲线:
-
测试集中的评分权重如何取?我们将评分转换为一个权重数组movies_w,权值取值范围[0,1],数组里的值代表我们有多大程度希望推荐该星级的电影;
例如:movies_w[1],movies_w[2],movies_w[3]都设置为0,代表我们不希望推荐和1到3星的类似的电影;movies_w[4]=0.2,movie_w[5]=1,代表我们很希望推荐5星的电影,同时对推荐4星电影的期望程度只有5星的 1 5 \frac{1}5 51;如果可推荐的电影足够多的话,只给用户推荐类似于5星显然更优;;
-
我们设置一个值为分界线,在该值之前的排名为推荐排名,在该值之后的排名为不推荐排名;
-
即对F_sort矩阵进行划分,定一个排名分界线,小于分界线的设置为1,大于的设置为0;
-
假设测试集中用户给一个电影打了高分,且排名靠前,我们就认为预测正确;假设测试集值中用户没给一个电影打分,而排名靠前,我们就认为预测错误;如何表示这两种情况?
-
# 如果一个电影被评价了,且排名靠前(预测正确),则取(电影评价权值*1)相加; # 正样本中被预测为正例的; TP = np.sum(Test * F_out) # 如果一个电影没有被评价,且排名靠前(预测错误),则取(负样本权值1*1)=1相加; # 负样本中被预测为正例的; FP = np.sum((1 - Test) * F_out)
-
Test 1-Test F_out 正样本 负样本 正例
-
-
-
TPR:正例中被预测对的;即: T P 测 试 集 1 的 个 数 \frac{TP}{测试集1的个数} 测试集1的个数TP;
-
FPR:负例中被预测错的;即: F P 测 试 集 中 0 的 个 数 \frac{FP}{测试集中0的个数} 测试集中0的个数FP;
-
根据TPR和FPR绘出ROC曲线,计算AUC值;
-
详细实现
-
导入数据包.py
import pandas as pd ''' 该文件是将数据包中的ratings.dat导入到python中; 导入后在python中的保存形式:<class 'pandas.core.frame.DataFrame'>; 该文件仅对ratings.dat文件做了数据导入处理,因为后面仅用了这一个文件的信息(电影打分相似度)来推荐电影; 如果想要得到更精确的结果,可以导入其他两个文件,计算出不同的相似度矩阵(例如:性别相似度,电影类别相似度等); ''' # 用字符串保存文件路径 ratings_path = './数据包/ratings.dat' # pandas中的read_table函数可以读取.dat文件;(read_csv函数貌似也可以,结果似乎没什么不同) # 设置列名:"UserID(用户ID)", "MovieID(电影ID)", "Rating(评分)", "Timestamp(时间戳)" ratings_names = ["UserID", "MovieID", "Rating", "Timestamp"] all_ratings = pd.read_table(ratings_path, sep='::', engine='python', names=ratings_names) # 时间戳可以用pd中的to_datetime处理,虽然本实验没有用到; # all_ratings['Datetime'] = pd.to_datetime(all_ratings['Timestamp']) # 测试输出,输出读取数据的前五个; # print(all_ratings[:5]) # 另一种输出方式:打印前n条数据, 缺省默认是5; # print(all_ratings.head(6))
-
数据处理.py
import numpy as np from 实验一电影推荐 import 导入数据包 as sj ''' 该文件实现功能; 对导入的数据进行处理,最终生成权重矩阵W; 矩阵W:行代表用户i,列代表电影j,值代表用户i给电影j的打分分值转换的权重; ''' # rand()返回一组均匀分布(服从[0,1)分布)的随机数; random()返回一个随机生成的实数,范围在[0,1); # mask是一个只包含True和False的矩阵,这个数组是让你指定需要掩盖的值的,标记为True的数据会被掩盖掉。 mask = np.random.rand(len(sj.all_ratings)) < .9 # print(mask) # 90%数据用于训练,10%数据用于测试,用mask掩盖矩阵进行划分; train_ratings = sj.all_ratings[mask] test_ratings = sj.all_ratings[~mask] ''' 一共有多少部电影和多少个用户; all_ratings中存有有效的(评过分的)UserID和MovieID 但是all_ratings中包含是重复的ID,若使用需要先用unique()去重后再len(); ''' r_users = sj.all_ratings["UserID"].unique() r_movies = sj.all_ratings.MovieID.unique() users_size = len(r_users) movies_size = len(r_movies) ''' 对用户ID和电影建立索引; 原因:原始数据中用户和电影并不是按顺序存储的(甚至ID都不是按顺序存在的,可能某一处99之后就是101); k代表索引,uid、mid代表ID,enumerate是建立索引的函数,{}里的写法类似与三元表达式; ''' uid2idx = {uid: k for k, uid in enumerate(r_users)} mid2idx = {mid: k for k, mid in enumerate(r_movies)} ''' 用户对电影的评分(score)范围为[1,5],没看过的电影可以认为评分为0,可以将评分转化为权重w; 一种权重转换方法: w = score / 5;w 范围为[0,1];(AUC = 0.85左右,效果不太理想) (可能是因为太过于均分权重,五个评分1星的电影相当于一个评分五星的电影,这显然是不合理的) 另一种转换方法: 将w设计为一个score的函数(下凸的)或者用一个列表来存储score和w的转化; 建立W矩阵,行为用户i,列为电影j,值为权重w; ''' # 用一个列表存score和w的对应关系(可以修改列表的值来追求更高的AUC) movies_w = [0, 0.000001, 0.0001, 0.01, 0.1, 1] # AUC = 0.911 # movies_w = [0, 0, 0, 0, 0, 1] # AUC = 0.9088 # 初始化一个我们需要的shape的,值全为0的矩阵W W = np.zeros((users_size, movies_size)) for _, rating in train_ratings.iterrows(): W[uid2idx[rating.UserID], mid2idx[rating.MovieID]] = movies_w[rating.Rating] # W[uid2idx[rating.UserID], mid2idx[rating.MovieID]] = 15 ** (rating.Rating - 5.0) ''' 改变score对应的w值,发现评分为4星的权重越低,AUC反而越好(AUC最大和最小差距在0.05左右); 从结果来看,似乎代表:一个电影被评4星,代表用户就喜欢类似的电影程度不高;一个电影被评5星,才能较大程度说明用户的喜好。 但是按照常理来说,4星的评分已经相对较高了,应该可以很大程度代表用户的喜好了。 这里面的原因不太了解。 '''
-
相似度设定.py
import numpy as np from 实验一电影推荐 import 数据处理 as sjcl ''' 相似度矩阵是预测的关键,可以从多个维度建立相似度矩阵; Sim_1是从评分的角度建立的相似度关系,也就是电影的评分相似度; 用矩阵乘法根据推导出的Sim_1的公式进行计算; 我们还可以尝试在性别、电影类别等角度建立相似度矩阵(多个二部图),最后将所有的相似度矩阵分别乘以系数相加,得到总体的预测率更高的相似度矩阵; ''' # 计算第一个相似度 sim_1 W = sjcl.W # 关于sum函数:axis=0为列相加,axis=1为行相加; # movieJ_w 代表电影j被用户评价的全部权重w的和,userI_w 代表用户i评价的全部电影的权重w的和; movieJ_w = W.sum(axis=0) userI_w = W.sum(axis=1) # 相似度矩阵Sim应该是电影i和电影j的相似度的矩阵,所以初始化时行和列都是movies_size; Sim_1 = np.zeros((sjcl.movies_size, sjcl.movies_size)) # reshape()用于转化数组为列向量; W1 = W / userI_w.reshape((-1, 1)) # 会出现0/0的情况,所以用isnan转化Nan为0 W1[np.isnan(W1)] = 0 # dot():矩阵乘法,该处代表:矩阵W1的转置乘以矩阵W Sim_1 = np.dot(W1.T, W) Sim_1 = Sim_1 / movieJ_w Sim_1[np.isnan(Sim_1)] = 0 # print("Sim_1= ", Sim_1)
-
训练和测试.py
import numpy as np from 实验一电影推荐 import 数据处理 as sjcl, 相似度设定 as xsd ''' 该文件功能: 通过训练集生成"电影排序矩阵"; 生成测试集; 电影排序矩阵:行代表用户i,列代表电影j,值代表在用户i眼里电影j在所有电影里的排名; ''' u_size = sjcl.users_size m_size = sjcl.movies_size Sim = xsd.Sim_1 W = xsd.W # 建立推荐矩阵F和排序后的推荐矩阵F_sort ''' argsort()例子: F = [[0.1, 1, 1], [1, 0.2, 1]] F_sort_index = [[0 1 2], [1 0 2]] F_sort = [[3. 2. 1.], [2. 3. 1.]] ''' F = np.dot(Sim, W.T).T F_sort_index = np.argsort(F, axis=1) F_sort = np.zeros((u_size, m_size)) for i in range(u_size): for j in range(m_size): F_sort[i, F_sort_index[i][j]] = m_size - j # 初始化测试集(与初始化训练集类似); ''' 测试集中的评分我们转换为一个权重数组movies_w,数组里的值代表我们有多大程度希望推荐该星级的电影; 例如:movies_w[1],movies_w[2],movies_w[3]都设置为0,代表我们不希望推荐和1到3星的类似的电影; movies_w[4]=0.2,movie_w[5]=1,代表我们很希望推荐5星的电影; 同时对推荐4星电影的期望程度只有5星的五分之一; 如果可推荐的电影足够多的话,给用户推荐类似于5星显然更优; ''' movies_w = [0, 0, 0, 0, 0, 1] Test = np.zeros((u_size, m_size)) for _, rating in sjcl.test_ratings.iterrows(): # Test[sjcl.uid2idx[rating.UserID], sjcl.mid2idx[rating.MovieID]] = sjcl.movies_w[rating.Rating] Test[sjcl.uid2idx[rating.UserID], sjcl.mid2idx[rating.MovieID]] = movies_w[rating.Rating] # 指数函数的底:AUC值 —— 7:0.897288 8:0.8986 10:0.90186 15:0.9041 20.0.9057 # Test[sjcl.uid2idx[rating.UserID], sjcl.mid2idx[rating.MovieID]] = 15 ** (rating.Rating - 5.0) # print(Test)
-
效果验证.py
from 实验一电影推荐 import 训练和测试 as tfianl import numpy as np import matplotlib.pyplot as plt ''' 输出ROC曲线; 输出AUC值; ''' Test = tfianl.Test F = tfianl.F_sort m_size = tfianl.m_size # TPR代表正例中被预测为正例的 # FPR代表负例中被预测为正例的 TPR, FPR = [], [] # 正样本,测试集的全部值相加,即全部打过分的电影的分数和; T_all = np.sum(Test) # 负样本,测试集中没被打过分的电影的数量,在负样本中,没被打过分的电影权值为1; F_all = np.sum(Test == False) for threshold in np.arange(0, 1, 0.005): F_out = F < (m_size * threshold) # 0,1矩阵,用于判断是排名靠前的还是靠后的,靠前为1,靠后为0,排名在threshold*movie_size之前的为靠前 # 由于threshold在改变,故分界线也在移动,一开始0多,后来1多; # F_out = F_out.astype(int) # 如果一个电影被评价了,且排名靠前(预测正确),则取(电影评价权值*1)相加;(正例中被预测为正例的) TP = np.sum(Test * F_out) # 如果一个电影没有被评价,且排名靠前(预测错误),则取(负样本权值1*1)=1相加;(负例中被预测为正例的) FP = np.sum((1 - Test) * F_out) TPR.append(TP / T_all) FPR.append(FP / F_all) # 画图(横坐标为FPR,纵坐标为TPR) plt.plot(FPR, TPR) plt.xlim(0, 1) plt.ylim(0, 1) plt.xlabel("FPR") plt.ylabel("TPR") plt.show() # ROC曲线的积分 # AUC值越接近1,预测效果越好; AUC = np.sum([0.005 * tpr for tpr in TPR]) print(AUC)
实验结果
(数据量为100万条左右的情况下,大概跑一次代码十分钟左右)
- AUC输出值:
- 红色部分只是一个警告,因为在数组运算时出现了0/0=NaN的情况,可以忽略;
- ROC曲线:
更多推荐
所有评论(0)