推荐系统中的隐语意分析方法:SVD 和深度学习的比较,全是白话,没有公式

前言

现在绝大多数的推荐系统,采用的都是双塔模型,本质上是求解用户向量和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的时代迎面而来。

相关推荐
秋夫人3 分钟前
B+树(B+TREE)索引
数据结构·算法
鸽芷咕19 分钟前
【Python报错已解决】ModuleNotFoundError: No module named ‘paddle‘
开发语言·python·机器学习·bug·paddle
梦想科研社38 分钟前
【无人机设计与控制】四旋翼无人机俯仰姿态保持模糊PID控制(带说明报告)
开发语言·算法·数学建模·matlab·无人机
Milo_K40 分钟前
今日 leetCode 15.三数之和
算法·leetcode
Darling_0043 分钟前
LeetCode_sql_day28(1767.寻找没有被执行的任务对)
sql·算法·leetcode
AlexMercer101244 分钟前
【C++】二、数据类型 (同C)
c语言·开发语言·数据结构·c++·笔记·算法
Greyplayground1 小时前
【算法基础实验】图论-BellmanFord最短路径
算法·图论·最短路径
Adolf_19931 小时前
Flask-JWT-Extended登录验证, 不用自定义
后端·python·flask
蓑 羽1 小时前
力扣438 找到字符串中所有字母异位词 Java版本
java·算法·leetcode
源代码:趴菜1 小时前
LeetCode63:不同路径II
算法·leetcode·职场和发展