前言
现在绝大多数的推荐系统,采用的都是双塔模型,本质上是求解用户向量和item向量,在推荐的过程中进行相似度匹配。
如果你的系统保存有大量的用户信息和item信息,可以使用一些比较成熟的算法来进行特征的抽取,如YoutubeDNN
,阿里的SDM
那样。
当信息特别少的时候,或者说只有一个协同矩阵,则需要使用矩阵分解来求解两个稠密特征矩阵,即用户矩阵和物品矩阵。
这在数学上很好理解,用户矩阵和物品矩阵的乘积,则是用户对物品的偏好程度。
SVD 奇异值分解
SVD是矩阵分解常用的方式,原理我们这里忽略,只需要知道它能够将矩阵分解为 其他三个矩阵的乘积。
我们的目标是将一个用户偏好矩阵(稀疏矩阵),分解为用户矩阵和物品矩阵的乘积,并且通过后两个矩阵的乘积运算,将稀疏矩阵中为0的位置,填充上用户的偏好程度。那么我们就知道了每个用户对每个物品的偏好程度了。
所以我们需要使用SVD的升级版本:
- FunkSVD :一种基于机器学习的"伪"SVD,可直接求解两个矩阵。通过拟合让分解得到的矩阵的部分元素不断接近原矩阵的有效元素,最终使得原矩阵的无效元素(0元素)也能被拟合预测出来。这种SVD最早被Simon Funk在博客中发布,因此又叫做Funk-SVD。
- BiasSVD:
FunkSVD
的升级版,在其基础上加入了bias偏置 - SVD++ :
BiasSVD
算法基础上进行了改进,加入了隐式因素,如浏览时长、点击情况等.
代码实现
数据预处理
这里我们使用数据集steam-200k
,一个steam游戏平台,玩家游戏时长的数据集。
首先是数据清理,我们这里用游戏时长当做喜好程度,去掉购买信息。并且将游戏时长转换为喜好评分,因为头部游戏时长非常长,不利于评分计算。
python
data = pd.read_csv("steam-200k.csv",header=None,names=['userid','gametitle','behavior','value','other'])
data = data[data.behavior.isin(['play'])]
value = []
for i in data['value'].values:
if i <=1 :
value.append(1)
elif i>72:
value.append(7)
else:
if i/7 > 1:
value.append(i/7)
else:
value.append(1)
看一下数据分区情况,大部分是垃圾游戏,头部游戏少。
然后我们需要去掉 游戏和玩家过少的记录,因为记录太少会导致无法收敛。
python
user_value = {}
item_value = {}
for i in data[['userid','gametitle','value']].values:
if i[0] in user_value:
user_value[i[0]] += i[2]
else:
user_value[i[0]] = i[2]
if i[1] in item_value:
item_value[i[1]] += i[2]
else:
item_value[i[1]] = i[2]
user_valid = []
item_valid = []
for k,v in user_value.items():
if v >= MIN_USER_LIMIT: # 一个用户最少需要几条记录
user_valid.append(k)
for k,v in item_value.items():
if v >= MIN_ITEM_LIMIT: # 一个item最少需要几条记录
item_valid.append(k)
data = data[data.userid.isin(user_valid)]
data = data[data.gametitle.isin(item_valid)]
data[['userid','gametitle','value',]].to_csv("./steam_play_ready.csv",index=False,header=False)
用surprise进行SVD求解
准备好数据我们就可以进行SVD分解了,这里我们使用surprise,他是scikit的一个推荐相关的库。
python
import numpy as np
import pandas as pd
from surprise import SVD,KNNBasic,Dataset,accuracy,Reader
from surprise.model_selection import KFold,train_test_split,cross_validate
import matplotlib.pyplot as plt
rawdata = pd.read_csv("./steam_play_ready.csv",header=None,names=['uid','iid','value'])
print("加载数据集 大小:",rawdata.shape)
print(rawdata.head(5))
users = rawdata.uid.unique()
items = rawdata.iid.unique()
print("用户数量:",len(users)," 游戏数量:",len(items))
# rating_scale: 评分范围
reader = Reader(rating_scale=(1,10))
data = Dataset.load_from_df(rawdata,reader)
# 默认 biased=True 也就是我们这里用的biasSVD
algo = SVD(n_factors = 100,n_epochs = 20)
# 交叉验证
cross_validate(algo,data,measures=["RMSE", "MAE"],cv=5, verbose=True)
除了cross_validate
还可以自己手动拟合,并打印损失
python
algo.fit(trainset)
accuracy.rmse(algo.test(testset))
我们直接指定一个用户,查看预测值和实际值的比对效果
python
preds = []
PRED_USER_ID = '94088853'
items = []
reality = []
for i in rawdata.values:
if str(i[0]) == PRED_USER_ID:
items.append(i[1])
reality.append(i[2])
for i in items:
pred = algo.predict(PRED_USER_ID,i)
preds.append(pred.est)
result = pd.DataFrame({
"items":items,
"reality": reality,
"pred":preds,
})
result = result.sort_values("pred",ascending=False)
print("user--> ",PRED_USER_ID)
print(result.head(20))
这是我的运行结果,直接看起来效果一般。
pytorch embedding 分解矩阵
我这里演示了最简单的两个embedding乘积的方法分解。
- embedding 可以理解为一个m*n的矩阵,表示为共有m个事物(m个人 或者 m个物品),每个事务存在n个特征。
- 我这里假设每个用户有10个特征,每个游戏有10个特征。
python
import torch
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
stream_data = pd.read_csv("./steam_play_ready.csv",header=None,names=['uid','iid','rating'])
users = stream_data.uid.unique()
items = stream_data.iid.unique()
class StreamPreferenceNN(torch.nn.Module):
def __init__(self, users,items) -> None:
super().__init__()
self.user_ebd = torch.nn.Embedding(len(users),10)
self.item_ebd = torch.nn.Embedding(len(items),10)
# todo 可以加入一些更复杂的网络
self.user_map,self.item_map = {},{}
for i,uid in enumerate(users):
self.user_map[uid] = i
for i,iid in enumerate(items):
self.item_map[iid] = i
def predict(self,uid,iid):
user_feature = self.user_ebd(torch.tensor(self.user_map[uid]))
item_feature = self.item_ebd(torch.tensor(self.item_map[iid]))
return torch.mul(user_feature,item_feature).sum()
def forward(self,x):
ids = [[self.user_map[i[0]],self.item_map[i[1]]] for i in x]
ids = torch.tensor(ids)
user_features = self.user_ebd(ids[:,0])
item_features = self.item_ebd(ids[:,1])
return torch.mul(user_features,item_features).sum(dim=-1)
model =StreamPreferenceNN(users,items)
criterion = torch.nn.MSELoss()
optimize = torch.optim.Adam(model.parameters(),lr=0.1)
# 开始求解
loss_record = []
train_y = stream_data.rating.array
y = torch.tensor(train_y,dtype=torch.float32)
for i in range(0,100):
pred = model(stream_data.values)
loss = criterion(pred,y)
loss_record.append(loss.item())
loss.backward()
optimize.step()
optimize.zero_grad()
# 绘制收敛过程
plt.plot([i for i in range(0,len(loss_record))],loss_record)
plt.show()
# 这里只是简单求了一下前n个的评分,也可以和实际进行对比,因为收敛的还可以,我这里懒得写了。
userid = 11373749
user_item = []
for i in items:
pred = model.predict(userid,i)
user_item.append([i,pred])
result = pd.DataFrame(user_item,columns=['item','rating'])
result = result.sort_values("rating",ascending=False)
print(result.head(10))
看一下收敛的效果:
再看一下推荐的top:
尾语
在实际的推荐系统中,我们采用了更复杂的方法进行特征抽取,例如将用户特征,群体特征,历史行为 进行全连接,注意力机制,跨层的LSTM等网络 进行复杂变换。
如果你有兴趣,可以在上面的代码里加个全连接层(相当于bias)尝试一下。
其实,经典的推荐算法也好,基于深度学习的算法也好,都是求物品相似度。这在大多数的场景下是够用的。
但时代在进步,随着chatgpt的成功,在很多场景下,基于大模型推荐,正在颠覆传统推荐方法。
AI的时代迎面而来。