🏡作者主页:点击!
🤖编程探索专栏:点击!
⏰️创作时间:2024年12月23日10点0分
神秘男子影,
秘而不宣藏。
泣意深不见,
男子自持重,
子夜独自沉。
多模态推荐系统 (Multi-modal Recommendation System)
什么是多模态推荐系统?
多模态推荐系统是一种利用多种不同类型的数据源(例如文本、图像、视频、音频等)来进行推荐的系统。传统的推荐系统通常只依赖于单一模态的数据,例如用户的评分或点击行为,而多模态推荐系统则结合了来自多个模态的信息,从而可以提供更准确和个性化的推荐。
优点
- 提高推荐准确性:通过结合多种数据源,可以更全面地了解用户的偏好。
- 丰富的用户体验:多模态数据可以为用户提供更多样化的推荐内容。
- 处理冷启动问题:在用户数据不足的情况下,可以利用其他模态的数据进行推荐。
示例
假设我们有一个电商平台,用户在平台上浏览和购买商品。我们可以使用以下多模态数据来构建推荐系统:
- 文本:商品的描述和用户的评论。
- 图像:商品的图片。
- 行为:用户的点击和购买记录。
超图 (Hypergraph)
什么是超图?
超图(Hypergraph)是一种广义的图结构,用于表示关系更复杂的数据。在传统的图中,边仅连接两个节点,而在超图中,超边(Hyperedge)可以连接两个或多个节点。因此,超图能够更自然地表示多元关系。
超图的表示
- 节点 (Nodes):与传统图一样,超图也包含一组节点。
- 超边 (Hyperedges):每条超边可以连接任意数量的节点。
应用场景
- 社交网络分析:在社交网络中,一个群聊或小组可以看作是连接多个用户的超边。
- 生物信息学:超图可以用来表示基因之间的复杂相互作用。
- 推荐系统:在推荐系统中,超图可以表示用户、物品和标签之间的多元关系。
示例
假设我们有一个超图,表示多个学生选修多门课程的情况。每个学生和每门课程都是节点,选修同一门课程的学生形成一个超边。
超图神经网络 (Hypergraph Neural Network, HGNN)
什么是HGNN?
超图神经网络(HGNN)是一种专门用于处理超图结构数据的神经网络。HGNN通过聚合节点及其超边的特征来学习节点的表示。与传统的图神经网络(GNN)不同,HGNN在聚合过程中考虑了超边连接的多个节点,从而能够捕捉更复杂的关系。
关键组件
- 节点特征 (Node Features):每个节点都有一个特征向量。
- 超边特征 (Hyperedge Features):每条超边也可以有一个特征向量。
- 聚合函数 (Aggregation Function):用于聚合节点和超边的特征,常见的聚合函数包括加权和、均值等。
HGNN的工作流程
- 特征初始化:初始化节点和超边的特征向量。
- 特征聚合:对每个节点,通过其连接的超边聚合邻居节点的特征。
- 特征更新:利用聚合后的特征更新节点的表示。
- 迭代更新:重复特征聚合和更新的过程,直到达到预定的迭代次数或收敛。
应用场景
- 推荐系统:通过HGNN,可以更好地捕捉用户、物品和标签之间的多元关系,从而提高推荐的准确性。
- 社交网络分析:HGNN可以用于分析社交网络中群体的行为模式和影响力。
- 生物信息学:HGNN可以用于分析基因之间复杂的相互作用,帮助发现潜在的基因功能模块。
示例
假设我们有一个学术引用网络,论文、作者和机构都可以看作节点,共同作者关系和引用关系可以看作超边。我们可以使用HGNN来分析网络中的学术影响力和合作模式。
问题提出
- 耦合问题(Coupling):现有的多模态推荐系统研究通常共享用户ID嵌入(user ID embeddings),这导致了协作信号和多模态信号之间的耦合。这意味着,尽管协作信号和多模态内容提供了探索用户兴趣的不同途径,它们在嵌入学习中却常常被混合在一起,限制了用户嵌入的稳定更新。
- 局部性问题(Locality):大多数现有方法只从交互图(interaction graph)中学习局部用户兴趣,缺乏对用户全局兴趣的探索。由于用户-项目交互的稀疏性,这些方法在建模鲁棒的用户兴趣方面受限。
-
- 耦合问题(Coupling):
-
-
- 假设有两个用户,Alice 和 Bob,他们在电影推荐平台上的行为(如购买、点击)被用来生成他们的兴趣嵌入。
- 如果系统使用相同的用户ID嵌入来同时考虑他们的购买行为(协作信号)和他们观看的电影类型(多模态信号),这可能会导致一个问题:Alice 可能因为喜欢动作片中的特效而喜欢某部电影,而 Bob 可能因为喜欢同一部电影的剧情而喜欢它。
- 在这种情况下,如果两个信号通过相同的嵌入更新,系统可能难以准确捕捉到他们对电影的不同偏好,因为更新可能会在不同的信号之间产生冲突,导致推荐不够个性化。
-
-
- 局部性问题(Locality):
-
-
- 继续以 Alice 和 Bob 为例,假设他们都在电影推荐平台上与少数几部电影有过交互。
- 一个只考虑局部兴趣的系统可能会基于这些有限的交互来推荐电影,而不考虑用户可能对电影中不同属性(如颜色、风格、形状)的全局兴趣。
- 例如,Alice 可能喜欢色彩鲜艳的服装设计,而 Bob 可能偏好简约风格。如果系统没有能力捕捉到这些全局兴趣,它可能会推荐一些基于局部交互相似性的电影,而不是真正符合他们个人偏好的电影。
- 论文中的图1(d)展示了用户全局兴趣通常与项目属性标签相关,这些属性并不依赖于局部交互。这意味着,如果一个推荐系统只考虑局部交互,可能会错过更全面理解用户兴趣的机会。
-
解决方案
当然可以。LGMRec模型通过以下几个关键技术部分来解决推荐系统中的耦合和局部性问题:
- 局部图嵌入模块(Local Graph Embedding Module):
-
- 协作图嵌入(Collaborative Graph Embedding, CGE):
-
-
- 使用图神经网络在用户-项目交互图上进行消息传递,通过公式 El+1=CGPROG(El)=D^−12A^D^−12ElE l +1=C G P R O G (E l )=D ^−21A ^D ^−21E l 来捕获高阶连接性,其中 A^A ^ 是邻接矩阵,D^D^ 是度矩阵。
-
-
- 模态图嵌入(Modality Graph Embedding, MGE):
-
-
- 独立地对每种模态(如视觉和文本)的特征进行处理,将原始模态特征通过变换矩阵 WmW m 投影到统一的嵌入空间,也就是MLPM L P,然后通过类似CGE的过程来学习模态相关的嵌入。
-
- 全局超图嵌入模块(Global Hypergraph Embedding Module):
-
- 超图依赖性构建:
-
-
- 定义隐式属性向量作为超边嵌入,通过公式 Hmi=Emi⋅Vm⊤H m i =E m i ⋅V m ⊤ 和 Hmu=Au×(Hmi)TH m u =A u ×(H m i )T 来获得项目-超边和用户-超边依赖性矩阵。
-
-
- 超图消息传递:
-
-
- 通过超边作为中间枢纽进行消息传递,使用公式 Em,h+1i=DROP(H^mi)⋅DROP(H^mi⊤)⋅Em,hiE m ,h +1i =DROP(H ^m i )⋅DROP(H ^m i ⊤)⋅E m ,h i 来更新全局嵌入,其中 DROPDROP 表示dropout函数。
-
- 融合与预测(Fusion and Prediction):
-
- 使用公式 E∗=Eidlge+∑m∈MNORM(Emlge)+α⋅NORM(Eghe)E ∗=E idl g e +∑m ∈M NORM(E m l g e )+α ⋅NORM(E ghe) 来融合局部和全局嵌入,其中 NORMNORM 是归一化函数,αα 是控制全局嵌入融合的可调因子。
- 超图对比学习(Hypergraph Contrastive Learning, HCL):
-
- 采用InfoNCE损失函数来增强不同模态之间的全局语义一致性,使用余弦相似度作为相似性度量。
- 优化:
-
- 使用贝叶斯个性化排名(BPR)损失和超图对比损失的组合来优化模型参数,公式为 L=LBPR+λ2⋅(LuHCL+LiHCL)L =L B P R +λ 2⋅(L u H C L +L i H C L ),其中 λ2λ2 是损失项权重。
框架图
这张框架图描述了LGMRec模型的主要组件和流程。我逐一分析每个部分:
- ID Embeddings: 这部分表示用户和物品的初始嵌入向量,通常用于表示用户和物品的特征。
- Local Graph Embedding Module: 局部图嵌入模块,负责捕捉局部的用户兴趣。它包括:
-
- Collaborative Graph Embedding: 通过用户-物品的交互矩阵来学习用户和物品的协作嵌入。
- Modality Graph Embedding: 利用物品的模态特征(如文本和视觉特征)来学习嵌入。
- Global Hypergraph Embedding Module: 全局超图嵌入模块,用于捕捉全局的用户兴趣和物品属性之间的关系。它包括:
-
- Hypergraph Learning: 通过超图结构来学习用户和物品的全局依赖性。
- Fusion and Prediction: 融合和预测模块,将局部和全局嵌入向量结合起来,用于预测用户对物品的偏好分数。这通常涉及到:
-
- Nomalize: 对嵌入向量进行归一化处理,以确保不同嵌入向量之间的可比性。
- Fusion: 融合不同来源的嵌入向量,可能包括加权和或更复杂的融合策略。
- Prediction: 使用融合后的嵌入向量来计算用户对物品的偏好分数。
- Adjacency Matrix: 邻接矩阵,用于表示图结构中的连接关系,是图嵌入中的关键组成部分。
- Interaction Matrix: 交互矩阵,记录了用户和物品之间的交互行为,如点击、购买等。
- Modal Features: 模态特征,指物品的不同类型特征,如文本描述、视觉图像等。
- LBPR: 贝叶斯个性化排名损失函数(Bayesian Personalized Ranking Loss),用于模型的优化。
整个框架的流程可以概括为:
- 使用局部图嵌入模块学习基于用户-物品交互的局部兴趣。
- 使用全局超图嵌入模块学习基于物品属性和用户偏好的全局兴趣。
- 将局部和全局嵌入向量进行融合。
- 通过融合后的嵌入向量预测用户对物品的偏好,并使用损失函数进行模型优化。
实验分析
环境部署
git clone https://github.com/georgeguo-cn/LGMRec
环境配置
pip install -r requirements.txt
conda install --file requirements.txt
数据集配置
通过这个地址-->dataset下载> baby
\clothing
\sports
这三个数据集,然后将这些文件放入源码的data文件夹下。
代码运行
python main.py -m LGMRec -d baby -g 0
python main.py -m LGMRec -d sports -g 0
python main.py -m LGMRec -d clothing -g 0
运行截图
数据集的相关统计数据
模型结构以及数据维度
部分运行结果
代码分析
提示
除mian文件的分析外,在视频讲解中将会对模型进行一些简单的代码分析,主要是模型的结构,以便帮助初学者理解模型的搭建,帮助读者进行自我创新。
模型代码部分结构分析
def __init__(self, config, dataset):
super(LGMRec, self).__init__(config, dataset) # 调用父类的构造函数,传入配置和数据集
self.embedding_dim = config['embedding_size'] # 设置嵌入向量的维度大小
self.feat_embed_dim = config['feat_embed_dim'] # 设置特征嵌入的维度大小
self.cf_model = config['cf_model'] # 协同过滤模型的类型
self.n_mm_layer = config['n_mm_layers'] # 多模态图嵌入的层数
self.n_ui_layers = config['n_ui_layers'] # 用户-项目图嵌入的层数
self.n_hyper_layer = config['n_hyper_layer'] # 超图神经网络层数
self.hyper_num = config['hyper_num'] # 超图的数量
self.keep_rate = config['keep_rate'] # dropout保留率
self.alpha = config['alpha'] # 全局超图嵌入的权重系数
self.cl_weight = config['cl_weight'] # 对比学习损失的权重
self.reg_weight = config['reg_weight'] # 正则化项的权重
self.tau = 0.2 # Gumbel softmax的温度参数
self.n_nodes = self.n_users + self.n_items # 总节点数(用户数 + 项目数)
self.hgnnLayer = HGNNLayer(self.n_hyper_layer) # 初始化超图神经网络层
# 加载数据集信息
self.interaction_matrix = dataset.inter_matrix(form='coo').astype(np.float32) # 获取用户-项目交互矩阵,并转换为COO格式
self.adj = self.scipy_matrix_to_sparse_tenser(self.interaction_matrix,
torch.Size((self.n_users, self.n_items))) # 将交互矩阵转换为稀疏张量
self.num_inters, self.norm_adj = self.get_norm_adj_mat() # 计算并归一化邻接矩阵
self.num_inters = torch.FloatTensor(1.0 / (self.num_inters + 1e-7)).to(self.device) # 计算交互次数的倒数并转换为张量
# 初始化用户和项目ID的嵌入向量
self.user_embedding = nn.Embedding(self.n_users, self.embedding_dim) # 初始化用户嵌入矩阵
self.item_id_embedding = nn.Embedding(self.n_items, self.embedding_dim) # 初始化项目嵌入矩阵
nn.init.xavier_uniform_(self.user_embedding.weight) # 使用Xavier均匀分布初始化用户嵌入矩阵
nn.init.xavier_uniform_(self.item_id_embedding.weight) # 使用Xavier均匀分布初始化项目嵌入矩阵
self.drop = nn.Dropout(p=1 - self.keep_rate) # 初始化Dropout层,用于防止过拟合
# 加载项目的多模态特征并定义超图嵌入向量
if self.v_feat is not None:
self.image_embedding = nn.Embedding.from_pretrained(self.v_feat, freeze=True) # 加载预训练的图像特征嵌入矩阵,并冻结参数
self.item_image_trs = nn.Parameter(
nn.init.xavier_uniform_(torch.zeros(self.v_feat.shape[1], self.feat_embed_dim))) # 初始化图像特征转换矩阵
self.v_hyper = nn.Parameter(
nn.init.xavier_uniform_(torch.zeros(self.v_feat.shape[1], self.hyper_num))) # 初始化图像超图嵌入矩阵
if self.t_feat is not None:
self.text_embedding = nn.Embedding.from_pretrained(self.t_feat, freeze=True) # 加载预训练的文本特征嵌入矩阵,并冻结参数
self.item_text_trs = nn.Parameter(
nn.init.xavier_uniform_(torch.zeros(self.t_feat.shape[1], self.feat_embed_dim))) # 初始化文本特征转换矩阵
self.t_hyper = nn.Parameter(
nn.init.xavier_uniform_(torch.zeros(self.t_feat.shape[1], self.hyper_num))) # 初始化文本超图嵌入矩阵
def forward(self):
# 超图依赖性构建
if self.v_feat is not None:
iv_hyper = torch.mm(self.image_embedding.weight, self.v_hyper) # 计算图像模态的项目超图嵌入
uv_hyper = torch.mm(self.adj, iv_hyper) # 根据项目超图嵌入计算用户超图嵌入
iv_hyper = F.gumbel_softmax(iv_hyper, self.tau, dim=1, hard=False) # 对项目超图嵌入进行Gumbel Softmax计算
uv_hyper = F.gumbel_softmax(uv_hyper, self.tau, dim=1, hard=False) # 对用户超图嵌入进行Gumbel Softmax计算
if self.t_feat is not None:
it_hyper = torch.mm(self.text_embedding.weight, self.t_hyper) # 计算文本模态的项目超图嵌入
ut_hyper = torch.mm(self.adj, it_hyper) # 根据项目超图嵌入计算用户超图嵌入
it_hyper = F.gumbel_softmax(it_hyper, self.tau, dim=1, hard=False) # 对项目超图嵌入进行Gumbel Softmax计算
ut_hyper = F.gumbel_softmax(ut_hyper, self.tau, dim=1, hard=False) # 对用户超图嵌入进行Gumbel Softmax计算
# CGE: 协同图嵌入
cge_embs = self.cge() # 计算协同图嵌入
if self.v_feat is not None and self.t_feat is not None:
# MGE: 模态图嵌入
v_feats = self.mge('v') # 计算图像模态的嵌入
t_feats = self.mge('t') # 计算文本模态的嵌入
# 本地嵌入 = 协同相关嵌入 + 模态相关嵌入
mge_embs = F.normalize(v_feats) + F.normalize(t_feats) # 对模态图嵌入进行归一化,并相加
lge_embs = cge_embs + mge_embs # 本地嵌入为协同图嵌入与模态图嵌入之和
# GHE: 全局超图嵌入
uv_hyper_embs, iv_hyper_embs = self.hgnnLayer(self.drop(iv_hyper), self.drop(uv_hyper),
cge_embs[self.n_users:]) # 计算用户和项目的图像模态超图嵌入
ut_hyper_embs, it_hyper_embs = self.hgnnLayer(self.drop(it_hyper), self.drop(ut_hyper),
cge_embs[self.n_users:]) # 计算用户和项目的文本模态超图嵌入
av_hyper_embs = torch.concat([uv_hyper_embs, iv_hyper_embs], dim=0) # 将用户和项目的图像模态超图嵌入拼接
at_hyper_embs = torch.concat([ut_hyper_embs, it_hyper_embs], dim=0) # 将用户和项目的文本模态超图嵌入拼接
ghe_embs = av_hyper_embs + at_hyper_embs # 全局超图嵌入为图像和文本模态超图嵌入之和
# 本地嵌入 + alpha * 全局嵌入
all_embs = lge_embs + self.alpha * F.normalize(ghe_embs) # 结合本地和全局嵌入,并乘以alpha系数进行组合
else:
all_embs = cge_embs # 如果没有模态特征,则仅使用协同图嵌入作为最终嵌入
u_embs, i_embs = torch.split(all_embs, [self.n_users, self.n_items], dim=0) # 将所有嵌入分割为用户嵌入和项目嵌入
return u_embs, i_embs, [uv_hyper_embs, iv_hyper_embs, ut_hyper_embs, it_hyper_embs] # 返回用户嵌入、项目嵌入和超图嵌入
def calculate_loss(self, interaction):
ua_embeddings, ia_embeddings, hyper_embeddings = self.forward()
users = interaction[0]
pos_items = interaction[1]
neg_items = interaction[2]
u_g_embeddings = ua_embeddings[users]
pos_i_g_embeddings = ia_embeddings[pos_items]
neg_i_g_embeddings = ia_embeddings[neg_items]
batch_bpr_loss = self.bpr_loss(u_g_embeddings, pos_i_g_embeddings, neg_i_g_embeddings)
[uv_embs, iv_embs, ut_embs, it_embs] = hyper_embeddings
batch_hcl_loss = self.ssl_triple_loss(uv_embs[users], ut_embs[users], ut_embs) + self.ssl_triple_loss(iv_embs[pos_items], it_embs[pos_items], it_embs)
batch_reg_loss = self.reg_loss(u_g_embeddings, pos_i_g_embeddings, neg_i_g_embeddings)
loss = batch_bpr_loss + self.cl_weight * batch_hcl_loss + self.reg_weight * batch_reg_loss
return loss
参数代码分析
# 创建 ArgumentParser 对象,用于处理命令行参数
parser = argparse.ArgumentParser()
# 添加 'model' 参数,接受一个字符串类型的值,默认值为 'LGMRec',用于指定模型名称
parser.add_argument('--model', '-m', type=str, default='LGMRec', help='name of models')
# 添加 'dataset' 参数,接受一个字符串类型的值,默认值为 'baby',用于指定数据集名称,可选的数据集还有clothing,sports。
parser.add_argument('--dataset', '-d', type=str, default='baby', help='name of datasets')
# 添加 'gpu_id' 参数,接受一个整数类型的值,默认值为 0,表示使用的 GPU 编号
parser.add_argument('--gpu_id', '-g', type=int, default=0, help='gpu number')
# 解析命令行参数
args, _ = parser.parse_known_args()
# 创建一个配置字典,包含 GPU 编号
config_dict = {
'gpu_id': args.gpu_id,
}
# 调用 quick_start 函数,传入模型名称、数据集名称和配置字典,并设置保存模型为 True
quick_start(model=args.model, dataset=args.dataset, config_dict=config_dict, save_model=True)
相关文件功能分析
LGMRec/
├── data/ # 数据目录
│ ├── baby/ # 婴儿数据目录
│ ├── clothing/ # 服装数据目录
│ └── sports/ # 运动数据目录
│
├── models/ # 模型目录
│ └── Igmrec.py
│ # LGMRec模型代码
├── utils/ # 模型工具目录
│ ├── configurator.py # 配置文件生成器
│ ├── data_utils.py # 数据处理工具
│ ├── dataloader.py # 数据加载工具
│ ├── dataset.py # 数据集处理工具
│ ├── logger.py # 日志记录工具
│ ├── metrics.py # 性能评估指标工具
│ ├── quick_start.py # 快速开始脚本
│ └── topk_evaluator.py # Top-K评估工具
│
├── common/ # 公共模块目录
│ └── overall.yaml # 通用配置文件
├── README.md # 项目说明文件
└── train.sh # 训练脚本
代码算法复现结果
提示
由于领域特殊性,其他预测数据无法展示,只能对模型预测的相关准确率指标数据进行分享,请见谅,同时由于不同服务器以及相应python包版本存在差异,会存在复现结果不尽相同,这是正常现象。该结果在RTX 4090得到。
在LGMRec模型中复现的数据结果
|----------|----------|------------|--------------|
| 指标 | Baby | Sports | Clothing |
| R@10 | 0.0640 | 0.0715 | 0.0553 |
| R@20 | 0.1001 | 0.1065 | 0.0825 |
| N@10 | 0.0345 | 0.0388 | 0.0550 |
| N@20 | 0.0441 | 0.0476 | 0.0356 |
创新思路方向
- 通过对现有数据集进行统计我们可以发现,现在大部分数据集都存在交互稀疏性的问题
,这样的稀疏性将会严重影响模型的性能以及准确率,该论文从全局领域进行相应的特征提取,但是此处依旧有进步空间。 - 目前在用户与物品的交互信息存在大量的噪声,许多交互并不能表明用户对于该物品存在兴趣,并且由时序问题,对于在不同的时间段,同一个用户的兴趣点可能发生变化,在前的大量的交互可能对改变后的兴趣产生影响。可以关注该点进行创新。
总结
LGMRec论文返现多模态推荐系统在建模用户兴趣时存在两个主要问题:用户ID嵌入的共享更新导致协作信号与多模态信号之间的耦合,以及缺乏对用户全局兴趣的探索,这限制了对稀疏交互问题的处理能力。为了解决这些问题,论文提出了一种名为LGMRec的新型多模态推荐器,它通过局部图嵌入模块独立学习与协作和模态相关的用户局部兴趣,并通过全局超图嵌入模块捕获用户和项目的全局嵌入,以模拟全局依赖关系。LGMRec将这两种嵌入有效融合,不仅提高了推荐系统的准确性和鲁棒性,而且在三个基准数据集上的广泛实验中展示了其优越性。