推荐系统中的隐语意分析方法: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的时代迎面而来。

相关推荐
昙鱼6 分钟前
springboot创建web项目
java·前端·spring boot·后端·spring·maven
eternal__day6 分钟前
数据结构(哈希表(中)纯概念版)
java·数据结构·算法·哈希算法·推荐算法
白宇横流学长12 分钟前
基于SpringBoot的停车场管理系统设计与实现【源码+文档+部署讲解】
java·spring boot·后端
18号房客13 分钟前
计算机视觉-人工智能(AI)入门教程一
人工智能·深度学习·opencv·机器学习·计算机视觉·数据挖掘·语音识别
APP 肖提莫15 分钟前
MyBatis-Plus分页拦截器,源码的重构(重构total总数的计算逻辑)
java·前端·算法
kirito学长-Java17 分钟前
springboot/ssm太原学院商铺管理系统Java代码编写web在线购物商城
java·spring boot·后端
OTWOL23 分钟前
两道数组有关的OJ练习题
c语言·开发语言·数据结构·c++·算法
QQ_77813297440 分钟前
基于深度学习的图像超分辨率重建
人工智能·机器学习·超分辨率重建
程序猿-瑞瑞1 小时前
24 go语言(golang) - gorm框架安装及使用案例详解
开发语言·后端·golang·gorm
qq_433554541 小时前
C++ 面向对象编程:递增重载
开发语言·c++·算法