协同过滤 改进:加权平均CF-GBRT —— 偏好与共性结合

1 协同过滤Collaborative Filtering(CF)

1.1 协同过滤算法解释:
https://blog.csdn.net/wangdong2017/article/details/87872351?utm_medium=distribute.pc_relevant.none-task-blog-baidujs-7

1.2 协同过滤的任务
协同过滤 改进:加权平均CF-GBRT —— 偏好与共性结合_第1张图片
假设现在需要对用户会喜欢什么电影进行预测,在上图(用户x电影)矩阵中,黄格子是用户看过的电影且里面的数字是他们对电影的评分,协同过滤算法的任务是预测白格子的评分。

1.3 Latent Factor模型
现实使用协同过滤中,由于庞大的user数量和item数量,会导致Rating矩阵非常庞大,非常难处理。这里的处理方法是对矩阵进行分解。

运用到Latent Factor模型,将R矩阵分解为Q矩阵和P矩阵(如下图),即item因素矩阵和user因素矩阵。利用交替最小二乘法ALS(Alternating Least Squares)对factors(即矩阵Q和矩阵P)进行求解,因此基于ALS的协同过滤,被简称为ALS。模型预测user对item的评分值为Q矩阵和P矩阵的乘积。
https://blog.csdn.net/antkillerfarm/article/details/53734658
协同过滤 改进:加权平均CF-GBRT —— 偏好与共性结合_第2张图片
有说法认为其中的factors能够作为假设用户和商品之间存在的隐藏的若干关联维度(比如有说法认为能够假设为用户年龄、性别、受教育程度和商品的外观、价格等)。当然这里也只是假设,实际上,因为协同过滤算法需要的输入变量只有user,item,rating三项,因此并不会凭空得出用户年龄、性别、受教育程度和商品的外观、价格等因素的影响。

1.4 协同过滤偏好性强,但缺乏共性
根据CF算法的三个输入变量user;item;rating,注定了算法无法侦测出(比如用户年龄、性别、受教育程度和商品的外观、价格)等共性因素的影响,如下图左侧
协同过滤 改进:加权平均CF-GBRT —— 偏好与共性结合_第3张图片
文章认为,ALS能够学习到的是User的偏好和Item的偏好,Latent Factors也即是User偏好因素以及Item偏好因素。那么算法就缺少共性的影响,即上图右侧,缺少共性因素对Rating的影响。

因此,为协同过滤算法添加共性因素,是本文的目的。

2 梯度提升回归树GBRT

2.1 GBRT(Gradient Boost Regression Tree)解释:
https://www.cnblogs.com/cxchanpin/p/7275516.html

2.2 GBRT的任务
协同过滤 改进:加权平均CF-GBRT —— 偏好与共性结合_第4张图片
同样运用的用户对电影的评分的历史数据去预测新样本的评分,算法能够将用户和电影的详细信息展开,利用这些信息去学习并最终预测新样本。如图,根据包含了年龄;性别;职业;电影发布日期;是否为动作类电影;是否问冒险类电影;是否为动物类电影的因素,以及他们(这类人)对该部电影(这类电影)的评分,运用GBRT算法进行学习,并最终预测新的样本(如某类人对某类电影的评分)。算法能够有效捕获用户的共性,以及电影的共性,从而完善协同过滤缺乏共性的不足之处。

2.3 使用GBRT的原因
GBRT是常用的非线性回归算法,准确率高。虽然GBRT集成方法为Boosting,但现spark支持GBRT,因此本文也采用GBRT算法对上文提到共性因素进行探索。

3 加权平均CF GBRT

3.1 公式
最终预测结果 = α x CF预测结果 + (1 - α) x GBRT预测结果
其中 0 ≤ α ≤ 1

3.1 求解
由于α定义域较窄,利用循环迭代求出最优解

4 实例论证

4.1 数据集
http://files.grouplens.org/datasets/movielens/
ml-100k

4.2 导入API及数据
Spark支持协同过滤ALS算法。Spark主要运用在大数据平台,但在本地也是可以使用。GBRT算法则借用SKlearn。

import numpy as np
import pandas as pd
import time
from datetime import datetime
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
from sklearn.model_selection import train_test_split
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.metrics import mean_squared_error
from pyspark.sql import SparkSession
from pyspark.ml.recommendation import ALS
import matplotlib.pyplot as plt

data = pd.read_csv('u.data', sep='\t', header=None, names=['user','item','rating'], usecols=[0,1,2])
item = pd.read_csv('u.item', sep='|', header=None, names=['item','date','Action','Adventure','Animation',
              "Children's",'Comedy','Crime','Documentary','Drama','Fantasy',
              'Film-Noir','Horror','Musical','Mystery','Romance','Sci-Fi',
              'Thriller','War','Western'], usecols=[0,2,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23])
user = pd.read_csv('u.user', sep='|', header=None, names=['user','age','gender','occupation'], usecols=[0,1,2,3])

4.3 数据预处理
预处理好数据后,将数据进行八二分成训练集和测试集,其中训练集中,再八二分为小训练集和小测试集。目的:小训练集用来训练,得到CF和GBRT模型后,利用小测试集测试结果并计算出最优加权系数α。最后将CF、GBRT和α系数带入测试集进行最后的检验。

#检查缺失值
data.isnull().any()
user.isnull().any()
item.isnull().any()
item.fillna(method='pad', inplace = True)#用前面的值替换缺失值

#将item.date改为时间戳
def timestamp(date):
    '''python自带的时间戳函数只能转换1970年之后的,之前的转换需要自行定义函数'''
    curTime = datetime.strptime(str(date), "%d-%b-%Y") 
    strTime =  curTime.strftime('%Y-%m-%d %H:%M:%S')
    utcTime1 = datetime.strptime(strTime, '%Y-%m-%d %H:%M:%S')    
    #这个时间之后为正 之前为负
    utcTime2 = datetime.strptime("1970-01-01 00:00:00", '%Y-%m-%d %H:%M:%S')
    metTime =  utcTime1 - utcTime2 #两个日期的 时间差
    timeStamp = metTime.days*24*3600 + metTime.seconds
    return timeStamp

#将date改为时间戳
item.date = [int(time.mktime(time.strptime(str(dates), "%d-%b-%Y"))) if (datetime.strptime(str(dates), "%d-%b-%Y") - datetime.strptime('01-Jan-1970', "%d-%b-%Y")).days > 0 else timestamp(dates) for dates in item.date]

'''将user进行labelencoder,注意这里用到GBRT梯度提升树模型,树模型不需要转换哑变量和归一化'''
le_gender = LabelEncoder()
user.gender = le_gender.fit_transform(user.gender)
le_gender.classes_

le_occupation = LabelEncoder()
user.occupation = le_occupation.fit_transform(user.occupation)
le_occupation.classes_

'''新建一个表:将data中的user转变为user表的内容,data中的item转变为item表的内容,并分为训练集和测试集'''
d_list = []
for index in data.index:
    #print(index)
    temp_u = user[user['user']==data.loc[index,'user']].iloc[:,1:].values.ravel().tolist()
    temp_i = item[item['item']==data.loc[index,'item']].iloc[:,1:].values.ravel().tolist()
    temp_list = temp_u + temp_i + [data.loc[index, 'rating']]
    d_list = d_list + [temp_list]

detail = pd.DataFrame(d_list, columns=list(user.columns)[1:]+list(item.columns)[1:]+['rating'], dtype=int)
detail.to_csv('detail_rating.data', header=True, index=False)
#detail = pd.read_csv('detail_rating.data')

#不使用sklearn的train_test_split,这里将前80000样本作为训练集,后20000作为最终测试集,其中80000训练集中将64000分为训练集,16000分为测试集
train = detail.iloc[:80000, :]
test = detail.iloc[80000:, :]

X2_train = train.iloc[16000:, :-1]
y2_train = train.iloc[16000:, -1]
X2_test = train.iloc[:16000, :-1]
y2_test = train.iloc[:16000, -1]

X_test = test.iloc[:, :-1]
y_test = test.iloc[:, -1]

4.4 GBRT模型 及模型评估

gbr = GradientBoostingRegressor(n_estimators=100, learning_rate=0.1, max_depth=8, random_state=0, loss='ls').fit(X2_train, y2_train)
gbrt_mse = mean_squared_error(y2_test, gbr.predict(X2_test))   
gbrt_test_pred = gbr.predict(X2_test)

4.5 ALS模型

spark = SparkSession\
    .builder\
    .appName("loadDatas")\
    .master("local[*]")\
    .enableHiveSupport()\
    .getOrCreate()

train_data = data.iloc[:80000, :]
test_data = data.iloc[80000:, :]

train_data2 = train_data.iloc[16000:, :]
test_data2 = train_data.iloc[:16000, :]

df = spark.createDataFrame(train_data2, list(data.columns))
#df.show()

als = ALS(rank=10, maxIter=7, seed=None, userCol="user", itemCol="item", ratingCol="rating")
model = als.fit(df)
#model.userFactors.orderBy("id").collect()

asl_test_pred_data = spark.createDataFrame(test_data2.iloc[:,:-1], ["user", "item"])
#predictions = sorted(model.transform(test).collect(), key=lambda r: r[0])
predictions = model.transform(asl_test_pred_data).collect()
#predictions是乱序的,需要匹配回test_data对应user和item
asl_test_pred = test_data2.copy(deep=True)
asl_test_pred['y_pred'] = 0
for i in predictions:
    temp_index = asl_test_pred[(asl_test_pred.user==i.user)&(asl_test_pred.item==i.item)].index
    asl_test_pred.loc[temp_index, 'y_pred'] = i.prediction

co_gbrt_asl = pd.DataFrame(columns=['actual_rating', 'gbrt_pred', 'asl_pred'])
co_gbrt_asl.actual_rating = asl_test_pred.rating
co_gbrt_asl.gbrt_pred = gbrt_test_pred
co_gbrt_asl.asl_pred = asl_test_pred.y_pred

co_gbrt_asl.isnull().any()#检测到asl预测结果存在nan,用该列的平均值填补
for column in list(co_gbrt_asl.columns[co_gbrt_asl.isnull().sum() > 0]):
    mean_val = co_gbrt_asl[column].mean()
    co_gbrt_asl[column].fillna(mean_val, inplace=True)

#利用MSE及RMSE评估两模型
MSE_test2_gbrt = mean_squared_error(co_gbrt_asl.iloc[:,0], co_gbrt_asl.iloc[:,1])
MSE_test2_asl = mean_squared_error(co_gbrt_asl.iloc[:,0], co_gbrt_asl.iloc[:,2])
RMSE_test2_gbrt = mean_squared_error(co_gbrt_asl.iloc[:,0], co_gbrt_asl.iloc[:,1])**0.5
RMSE_test2_asl = mean_squared_error(co_gbrt_asl.iloc[:,0], co_gbrt_asl.iloc[:,2])**0.5

4.6 加权平均CF和GBRT

def adjusted_pred(df, a):
    '''
    ASL预测结果为偏好得分,GBRT预测结果为共性得分
    调整预测结果公式:
    a*ASL + (1-a)*GBRT               0<=a<=1
    '''
    adjusted_result = []
    for i in range(len(df.index)):
        adjusted_result.append(a*df.iloc[i,2] + (1-a)*df.iloc[i,1])
    mse = np.sum(np.power((df.iloc[:,0].values.reshape(-1,1) - np.array(adjusted_result).reshape(-1,1)),2))/len(df.iloc[:,0])
    return adjusted_result, mse

adjusted_mse = []
for i in range(0, 101):
    i = i/100
    _, temp_mse = adjusted_pred(df=co_gbrt_asl, a=i)
    adjusted_mse.append([i, temp_mse])

#可视化系数α与MSE关系
plt.plot([i[0] for i in adjusted_mse],[i[1] for i in adjusted_mse])
plt.title("a : MSE",fontsize=18)
plt.xlabel("a",fontsize=14)
plt.ylabel("MSE",fontsize=14)
plt.tick_params(axis='both',labelsize=14)
plt.show()

#α取MSE最小对应的值
a = sorted(adjusted_mse, key=lambda x: x[1])[0][0]

其中系数α与均方误差MSE的关系如图
协同过滤 改进:加权平均CF-GBRT —— 偏好与共性结合_第5张图片

4.7 预测最终测试集并进行检验

#预测最终测试集
#GBRT
final_gbrt_pred = gbr.predict(X_test)
#ASL
final_asl_test_data = spark.createDataFrame(test_data.iloc[:,:-1], ["user", "item"])
final_predictions = model.transform(final_asl_test_data).collect()
#predictions是乱序的,需要匹配回test_data对应user和item
final_asl_pred = test_data.copy(deep=True)
final_asl_pred['y_pred'] = 0
for i in final_predictions:
    temp_index = final_asl_pred[(final_asl_pred.user==i.user)&(final_asl_pred.item==i.item)].index
    final_asl_pred.loc[temp_index, 'y_pred'] = i.prediction

final_co = pd.DataFrame(columns=['actual_rating', 'gbrt_pred', 'asl_pred'])
final_co.actual_rating = final_asl_pred.rating
final_co.gbrt_pred = final_gbrt_pred
final_co.asl_pred = final_asl_pred.y_pred

final_co.isnull().any()#检测到asl预测结果存在nan,用该列的平均值填补
for column in list(final_co.columns[final_co.isnull().sum() > 0]):
    mean_val = final_co[column].mean()
    final_co[column].fillna(mean_val, inplace=True)

#新增最终预测结果
final_co['final_pred'] = 0
for index in final_co.index:
    final_co.loc[index, 'final_pred'] = a*final_co.loc[index, 'asl_pred'] + (1-a)*final_co.loc[index, 'gbrt_pred']


MSE_final_gbrt = mean_squared_error(final_co.iloc[:,0], final_co.iloc[:,1])
MSE_final_asl = mean_squared_error(final_co.iloc[:,0], final_co.iloc[:,2])
MSE_final_pred = mean_squared_error(final_co.iloc[:,0], final_co.iloc[:,3])
RMSE_final_gbrt = mean_squared_error(final_co.iloc[:,0], final_co.iloc[:,1])**0.5
RMSE_final_asl = mean_squared_error(final_co.iloc[:,0], final_co.iloc[:,2])**0.5
RMSE_final_pred = mean_squared_error(final_co.iloc[:,0], final_co.iloc[:,3])**0.5

最终验证结果:
MSE(GBRT): 1.0057495636543246
MSE(ASL): 0.8915632655698295
MSE(加权CF-GBRT): 0.8623720861105352

RMSE(GBRT): 1.0028706614785001
RMSE(ASL): 0.9442262787964701
RMSE(加权CF-GBRT): 0.9286399119737074

4.8 结论
加权平均CF-GBRT最终预测结果的均方误差MSE以及标准误差RMSE低于ALS模型的MSE和RMSE,也低于GBRT模型MSE和RMSE。

加权平均CF-GBRT结合CF的偏好性和GBRT的共性,能够弥补CF缺乏数据共性的不足,以及弥补GBRT偏好性的不足。

你可能感兴趣的:(笔记,算法,python,机器学习,数据挖掘,spark)