推荐系统组队学习——矩阵分解和FM

本文深入探讨了矩阵分解在推荐系统中的作用,包括隐语义模型、SVD和SVD++等算法,强调了优化方法如交替最小二乘法在处理稀疏数据集的优势。此外,还介绍了贝叶斯个性化排序(BPR)模型,该模型通过优化AUC来预测物品间的相对顺序。接着,文章转向因子分解机(FM)模型,解释了其如何通过二阶交叉项改进逻辑回归,并展示了FM在CTR预估和召回任务中的应用。最后,提供了FM模型的Python实现示例。
摘要由CSDN通过智能技术生成

一、矩阵分解

1.隐语义模型与矩阵分解

矩阵分解模型是在协同过滤共现矩阵的基础上, 使用更稠密的隐向量表示用户和物品,挖掘用户和物品的隐含兴趣和隐含特征, 在一定程度上弥补协同过滤模型处理稀疏矩阵能力不足的问题。

它的核心思想是通过隐含特征(latent factor)联系用户兴趣和物品(item), 基于用户的行为找出潜在的主题和分类, 然后对item进行自动聚类,划分到不同类别/主题(用户的兴趣)。

我们下面拿一个音乐评分的例子来具体看一下隐特征矩阵的含义。

假设每个用户都有自己的听歌偏好, 比如A喜欢带有小清新的, 吉他伴奏的, 王菲的歌曲,如果一首歌正好是王菲唱的, 并且是吉他伴奏的小清新, 那么就可以将这首歌推荐给这个用户。 也就是说是小清新, 吉他伴奏, 王菲这些元素连接起了用户和歌曲。 当然每个用户对不同的元素偏好不同, 每首歌包含的元素也不一样, 所以我们就希望找到下面的两个矩阵:

  1. 潜在因子——用户矩阵Q:这个矩阵表示不同用户对于不同元素的偏好程度
    在这里插入图片描述
  2. 潜在因子——音乐矩阵P:表示每种音乐含有各种元素的成分
    在这里插入图片描述

利用上面的这两个矩阵得到对应的两个隐向量, 我们就能得出张三对音乐A的喜欢程度:
在这里插入图片描述
根据隐向量其实就可以得到张三对音乐A的打分
在这里插入图片描述
按照这个计算方式, 最后就得到了我们的评分矩阵:
在这里插入图片描述
上面例子中的小清晰, 重口味, 优雅这些就可以看做是隐含特征, 而通过这个隐含特征就可以把用户的兴趣和音乐的进行一个分类, 其实就是找到了每个用户每个音乐的一个隐向量表达形式。但是,事实上, 我们有的只有用户的评分矩阵,这种矩阵非常的稀疏,如果直接基于用户相似性或者物品相似性去填充这个矩阵是不太容易的, 并且很容易出现长尾问题, 所以矩阵分解就可以比较容易的解决这个问题。

2.矩阵分解算法的原理

矩阵分解算法将 m* n 维的共享矩阵 R 分解成 m * k 维的用户矩阵 U 和 k * n维的物品矩阵 V 相乘的形式。 其中 m 是用户数量, n 是物品数量, k 是隐向量维度, 也就是隐含特征个数, 只不过这里的隐含特征要模型自己去学。 k 的大小决定了隐向量表达能力的强弱, k 越大, 表达信息就越强。

有了用户矩阵和物品矩阵就可以计算用户对物品的评分。

矩阵分解, 最常用的方法是特征值分解(EVD)或者奇异值分解(SVD)。EVD要求分解的矩阵是方阵, 显然用户-物品矩阵不满足这个要求。传统的SVD分解, 会要求原始矩阵是稠密的, 而我们这里的这种矩阵一般情况下是非常稀疏的, 如果想用奇异值分解, 就必须对缺失的元素进行填充, 而一旦补全, 空间复杂度就会非常高, 且补的不一定对。 然后就是SVD分解计算复杂度非常高, 而我们的用户-物品矩阵非常大, 所以基本上无法使用。

3.Basic SVD

Funk-SVD的思想很简单: 把求解上面两个矩阵的参数问题转换成一个最优化问题, 可以通过训练集里面的观察值利用最小化来学习用户矩阵和物品矩阵。

主要步骤包括:

  1. 随机初始化一个用户矩阵 U 和一个物品矩阵 V,计算猜测的评分
    在这里插入图片描述

  2. 计算出总的猜测的和真实值之间的误差平方和
    在这里插入图片描述

  3. 使用梯度下降算法进行训练, 把SSE降到最小
    在这里插入图片描述

在实际中, 仅仅设置用户矩阵和物品矩阵两个参数是不够的, 还要考虑其他的一些因素, 比如一个评分系统, 有些固有的属性和用户物品无关, 而用户也有些属性和物品无关, 物品也有些属性和用户无关。 因此, 另一种LFM, 在原来的基础上加了偏置项, 来消除用户和物品打分的偏差, 即预测公式如下:
在这里插入图片描述
三个偏置项的意义如下:

  • μ:训练集中所有记录的评分的全局平均数。不同网站的整体评分分布也会显示差异。 比如有的网站中用户就喜欢打高分, 有的网站中用户就喜欢打低分。 而全局平均数可以表示网站本身对用户评分的影响。
  • bu:用户偏差系数。这一项表示了用户的评分习惯中和物品没有关系的那种因素。 比如有些用户比较苛刻, 对什么东西要求很高, 那么他评分就会偏低, 而有些用户比较宽容, 对什么东西都觉得不错, 那么评分就偏高
  • bi:物品偏差系数。这一项表示了物品接受的评分中和用户没有关系的因素。 比如有些物品本身质量就很高, 因此获得的评分相对比较高, 有的物品本身质量很差,因此获得的评分相对较低。

SSE的变化如下:
在这里插入图片描述

4.SVD++

在推荐系统中,隐式反馈比显式反馈要多很多,在 SVD 中结合用户的隐式反馈行为和属性,这套模型叫做 SVD++。

隐式反馈加入的方法是:除了假设评分矩阵中的物品有一个隐因子向量外,用户有过行为的物品集合也都有一个隐因子向量,维度是一样的。把用户操作过的物品隐因子向量加起来,用来表达用户的兴趣偏好。

类似的,用户属性,全都转换成 0-1 型的特征后,对每一个特征也假设都存在一个同样维度的隐因子向量,一个用户的所有属性对应的隐因子向量相加,也代表了他的一些偏好。

综合两者,SVD++ 的目标函数中,只需要把推荐分数预测部分稍作修改,原来的用户向量那部分增加了隐式:
在这里插入图片描述
要学习的参数多了两个向量:x 和 y。一个是隐式反馈的物品向量,另一个用户属性的向量。

在 SVD 中考虑时间因素,有几种做法:

  1. 对评分按照时间加权,让久远的评分更趋近平均值;
  2. 对评分时间划分区间,不同的时间区间内分别学习出隐因子向量,使用时按照区间使用对应的隐因子向量来计算;
  3. 对特殊的期间,如节日、周末等训练对应的隐因子向量。

5.优化方法

优化方法除了梯度下降之外,还可以使用交替最小二乘(ALS)。

1.交替最小二乘原理 (ALS)

我们希望优化如下公式:
在这里插入图片描述
交替最小二乘通过迭代的方式进行优化:

  1. 初始化随机矩阵 Q 里面的元素值;
  2. 把 Q 矩阵当做已知的,直接用线性代数的方法求得矩阵 P;
  3. 得到了矩阵 P 后,把 P 当做已知的,故技重施,回去求解矩阵 Q;
  4. 上面两个过程交替进行,一直到误差可以接受为止。

交替最小二乘有这么几个好处:
5. 在交替的其中一步,也就是假设已知其中一个矩阵求解另一个时,要优化的参数是很容易并行化的;
6. 在不那么稀疏的数据集合上,交替最小二乘通常比随机梯度下降要更快地得到结果,事实上这一点就是我马上要说的,也就是关于隐式反馈的内容。

2.加权交替最小二乘(Weighted-ALS)

当我们从解决评分预测转变到解决行为预测的问题时,这种问题叫做One-Class问题。

如果把预测用户行为看成一个二分类问题,猜用户会不会做某件事,但实际上收集到的数据只有明确的一类:用户干了某件事,而用户明确“不干”某件事的数据却没有明确表达。所以这就是 One-Class 的由来,One-Class 数据也是隐式反馈的通常特点。

对于加权的概念,用户对物品的隐式反馈,通常是可以多次的,你有心心念念的衣服或者电子产品,但是刚刚剁完手的你正在吃土买不起,只能每天去看一眼。这样一来,查看次数越多,就代表你越喜欢这个。也就是说,行为的次数是对行为的置信度反应,也就是所谓的加权。

加权交替最小二乘这样对待隐式反馈:

  1. 如果用户对物品无隐式反馈则认为评分是 0;
  2. 如果用户对物品有至少一次隐式反馈则认为评分是 1,次数作为该评分的置信度。

那现在的目标函数在原来的基础上变成这样:
在这里插入图片描述
Cui为置信度,计算方式如下,C为次数,α是一个超参数,默认为40
在这里插入图片描述
这里又引出另一个问题,那些没有反馈的缺失值,取值为 0 的评分就非常多, 这会导致正负类别样本非常不平衡,严重倾斜到 0 评分这边。因此,不能使用所有的缺失值作为负类别。应对这个问题的做法就是负样本采样:挑一部分缺失值作为负类别样本即可。挑选方法:

  1. 随机均匀采样和正类别一样多;
  2. 按照物品的热门程度采样。

结论是,第一种不是很靠谱,第二种在实践中经过了检验。按照物品热门程度采样的思想就是:一个越热门的物品,用户越可能知道它的存在。那这种情况下,用户还没对它有反馈就表明:这很可能就是真正的负样本。

3.推荐计算

在得到了分解后的矩阵后,相当于每个用户得到了隐因子向量,这是一个稠密向量,用于代表他的兴趣。同时每个物品也得到了一个稠密向量,代表它的语义或主题。而且可以认为这两者是一一对应的,用户的兴趣就是表现在物品的语义维度上的。

看上去,让用户和物品的隐因子向量两两相乘,计算点积就可以得到所有的推荐结果了。但是实际上复杂度还是很高,尤其对于用户数量和物品数量都巨大的应用,如 Facebook,就更不现实。于是 Facebook 提出了两个办法得到真正的推荐结果:

  1. 利用一些专门设计的数据结构存储所有物品的隐因子向量,从而实现通过一个用户向量可以返回最相似的 K 个物品。

    Facebook 给出了自己的开源实现 Faiss,类似的开源实现还有 Annoy,KGraph,NMSLIB。其中 Facebook 开源的 Faiss 和 NMSLIB(Non-Metric Space Library)都用到了 ball tree 来存储物品向量。

  2. 对物品的隐因子向量先做聚类,海量的物品会减少为少量的聚类。然后再逐一计算用户和每个聚类中心的推荐分数,给用户推荐物品就变成了给用户推荐物品聚类。

    得到给用户推荐的聚类后,再从每个聚类中挑选少许几个物品作为最终推荐结果。这样做的好处除了大大减小推荐计算量之外,还可以控制推荐结果的多样性,因为可以控制在每个类别中选择的物品数量。

6.编程实现

class SVD():
	def __init__(self, rating_data, F=5, alpha=0.1, lmbda=0.1, max_iter=100):
		self.F = F # 这个表示隐向量的维度
		self.P = dict() # 用户矩阵P 大小是[users_num, F]
		self.Q = dict() # 物品矩阵Q 大小是[item_nums, F]
		self.bu = dict() # 用户偏差系数
		self.bi = dict() # 物品偏差系数
		self.mu = 0.0 # 全局偏差系数
		self.alpha = alpha # 学习率
		self.lmbda = lmbda # 正则项系数
		self.max_iter = max_iter # 最大迭代次数
		self.rating_data = rating_data # 评分矩阵
		
		# 初始化矩阵P和Q, 方法很多, 一般用随机数填充, 但随机数大小有讲究, 根据经验, 随机数需要和1/sqrt(F)成正比
		cnt = 0 # 统计总的打分数, 初始化mu用
		for user, items in self.rating_data.items():
			self.P[user] = [random.random() / math.sqrt(self.F) for x in range(0, F)]
			self.bu[user] = 0
			cnt += len(items)
			for item, rating in items.items():
				if item not in self.Q:
					self.Q[item] = [random.random() / math.sqrt(self.F) for x in range(0, F)]
					self.bi[item] = 0
		self.mu /= cnt

# 有了矩阵之后, 就可以进行训练, 这里使用随机梯度下降的方式训练参数P和Q
	def train(self):
		for step in range(self.max_iter):
			for user, items in self.rating_data.items():
				for item, rui in items.items():
					rhat_ui = self.predict(user, item) # 得到预测评分
					# 计算误差
					e_ui = rui - rhat_ui
					self.bu[user] += self.alpha * (e_ui - self.lmbda * self.bu[user])
					self.bi[item] += self.alpha * (e_ui - self.lmbda * self.bi[item])
					# 随机梯度下降更新梯度
					for k in range(0, self.F):
						self.P[user][k] += self.alpha * (e_ui*self.Q[item][k] - self.lmbda * self.P[user][k])
						self.Q[item][k] += self.alpha * (e_ui*self.P[user][k] - self.lmbda * self.Q[item][k])
			self.alpha *= 0.1 # 每次迭代步长要逐步缩小

	# 预测user对item的评分, 这里没有使用向量的形式
	def predict(self, user, item):
		return sum(self.P[user][f] * self.Q[item][f] for f in range(0, self.F)) + self.bu[user] + self.bi[item] + self.mu

def loadData():
	rating_data={1: {'A': 5, 'B': 3, 'C': 4, 'D': 4},
		2: {'A': 3, 'B': 1, 'C': 2, 'D': 3, 'E': 3},
		3: {'A': 4, 'B': 3, 'C': 4, 'D': 3, 'E': 5},
		4: {'A': 3, 'B': 3, 'C': 1, 'D': 5, 'E': 4},
		5: {'A': 1, 'B': 5, 'C': 5, 'D': 2, 'E': 1}
	}
	return rating_data

rating_data = loadData()
basicsvd = SVD(rating_data, F=10)
basicsvd.train()
for item in ['E']:
	print(item, basicsvd.predict(1, item))

7.贝叶斯个性化排序(BPR)

贝叶斯个性化排序直接预测物品两两之间的相对顺序(pair-wise问题),评价模型预测精准程度可以使用AUC。

AUC的计算公式如下:
在这里插入图片描述
分母是所有我们关心的那类样本,也就是正样本,有 M 个,以及其他样本有 N 个,这两类样本相对排序总共的组合可能性,是 M x N;
分子也不复杂,原本是这样算的:第一名的排序值是 r1,它在排序上不但比过了所有的负样本,而且比过了自己以外的正样本。

1.样本构造方法

BPR提出要关心物品之间对于用户的相对顺序,构造的样本是:用户、物品1、物品2、两个物品相对顺序。

相对顺序的含义:

  • 如果物品1消费过的,而物品不是,那么相对顺序取值1,是正样本
  • 如果物品1和物品2刚好相反,则是负样本
  • 样本中不包含其他情况:物品1和物品2都是消费过的,或者都是没消费过的

2.目标函数

物品1和物品2的似然概率是最大化交叉熵:
在这里插入图片描述
u12表示用户u,物品1和物品2的矩阵分解预测分数差,然后再用sigmoid把分数压缩到0-1之间。

通常还要加入L2正则项,正则项其实认为模型参数还有个先验概率,所以BPR名字中的贝叶斯的由来。BPR认为模型的先验概率符合正态分布。

目标函数:
在这里插入图片描述
最大化目标函数就能得到分解后的矩阵参数,其中θ是分解后的矩阵参数
这个目标函数化简和变形后就和AUC当初目标函数非常相似了,所以BPR模型作者宣称该模型是为AUC而生的。

3.训练方法

梯度下降又有批量梯度和随机梯度下降两个选择,前者收敛慢,后者训练快却不稳定。因此 BPR 的作者使用了一个介于两者之间的训练方法,结合重复抽样的梯度下降。具体来说是这样做的:

  1. 从全量样本中有放回地随机抽取一部分样本;
  2. 用这部分样本,采用随机梯度下降优化目标函数,更新模型参数;
  3. 重复步骤 1,直到满足停止条件。

这样,就得到了一个更符合推荐排序要求的矩阵分解模型了

8.优缺点分析

优点:

  • 泛化能力强: 一定程度上解决了稀疏问题
  • 空间复杂度低: 由于用户和物品都用隐向量的形式存放, 少了用户和物品相似度矩阵, 空间复杂度由降到了(n+m)*f
  • 更好的扩展性和灵活性:矩阵分解的最终产物是用户和物品隐向量, 这个深度学习的embedding思想不谋而合, 因此矩阵分解的结果非常便于与其他特征进行组合和拼接, 并可以与深度学习无缝结合。

但是, 矩阵分解算法依然是只用到了评分矩阵, 没有考虑到用户特征, 物品特征和上下文特征, 这使得矩阵分解丧失了利用很多有效信息的机会, 同时在缺乏用户历史行为的时候, 无法进行有效的推荐。 所以为了解决这个问题,逻辑回归模型及后续的因子分解机模型, 凭借其天然的融合不同特征的能力, 逐渐在推荐系统领域得到了更广泛的应用。

二、FM

1.FM模型理解

(1)逻辑回归模型及其缺点

FM模型其实是一种思路,具体的应用稍少。一般来说做推荐CTR预估时最简单的思路就是将特征做线性组合(逻辑回归LR),传入sigmoid中得到一个概率值,本质上这就是一个线性模型,因为sigmoid是单调增函数不会改变里面的线性模型的CTR预测顺序,因此逻辑回归模型效果会比较差。也就是LR的缺点有:

  1. 是一个线性模型
  2. 每个特征对最终输出结果独立,需要手动特征交叉

(2)二阶交叉项的改进

考虑所有的二阶交叉项,将目标函数改变为:
在这里插入图片描述
这个式子有一个问题,只有当 xi 与 xj 均不为0时这个二阶交叉项才会生效,为了解决这个问题,提出了FM算法。

FM模型使用了如下的优化函数:
在这里插入图片描述
实质上就是给每个 xi 计算一个embedding,然后将两个向量之间的embedding做内积得到之前所谓的wij, 好处就是这个模型泛化能力强 ,即使两个特征之前从未在训练集中同时出现,也可以进行计算。

(3)FM模型理解

从公式来看,模型前半部分就是普通的LR线性组合,后半部分的交叉项:特征组合。首先,单从模型表达能力上来看,FM是要强于LR的,至少它不会比LR弱,当交叉项参数wij全为0的时候,整个模型就退化为普通的LR模型。对于有n个特征的模型,特征组合的参数数量共有1+2+3+⋯+n−1=n(n−1)/2个,并且任意两个参数之间是独立的。所以说特征数量比较多的时候,特征组合之后,维度自然而然就高了。

定理:任意一个实对称矩阵(正定矩阵) W 都存在一个矩阵 V ,使得 W=V.VT 成立。

类似地,所有二次项参数 ωij 可以组成一个对称阵 W (为了方便说明FM的由来,对角元素可以设置为正实数),那么这个矩阵就可以分解为 W=VTV , V 的第 j 列( vj )便是第 j 维特征( xj )的隐向量。
在这里插入图片描述
需要估计的参数有 ω0∈R , ωi∈R , V∈R , <⋅,⋅> 是长度为 k 的两个向量的点乘,公式如下:
在这里插入图片描述
上面的公式中:

  • ω0 为全局偏置;
  • ωi 是模型第 i 个变量的权重;
  • ωij=<vi,vj> 特征 i 和 j 的交叉权重;
  • vi 是第 i 维特征的隐向量;
  • <⋅,⋅> 代表向量点积;
  • k(k<<n) 为隐向量的长度,包含 k 个描述特征的因子。

FM模型中二次项的参数数量减少为 kn 个,远少于多项式模型的参数数量。另外,参数因子化使得xhxi 的参数和 xixj 的参数不再是相互独立的,因此我们可以在样本稀疏的情况下相对合理地估计FM的二次项参数。具体来说,xhxi和 xixj 的系数分别为 <vh,vi> 和 <vi,vj> ,它们之间有共同项 vi 。也就是说,所有包含“ xi 的非零组合特征”(存在某个 j≠i ,使得 xixj≠0 )的样本都可以用来学习隐向量 vi ,这很大程度上避免了数据稀疏性造成的影响。而在多项式模型中, whi 和 wij 是相互独立的。

显而易见,FM的公式是一个通用的拟合方程,可以采用不同的损失函数用于解决regression、classification等问题,比如可以采用MSE(Mean Square Error)loss function来求解回归问题,也可以采用Hinge/Cross-Entropy loss来求解分类问题。当然,在进行二元分类时,FM的输出需要使用sigmoid函数进行变换,该原理与LR是一样的。直观上看,FM的复杂度是O(kn2) 。但是FM的二次项可以化简,其复杂度可以优化到 O(kn) 。由此可见,FM可以在线性时间对新样本作出预测。

2.FM模型应用

最直接的想法就是直接把FM得到的结果放进sigmoid中输出一个概率值,由此做CTR预估,事实上我们也可以做召回。

由于FM模型是利用两个特征的Embedding做内积得到二阶特征交叉的权重,那么我们可以将训练好的FM特征取出离线存好,之后用来做KNN向量检索

工业应用的具体操作步骤:

  1. 离线训练好FM模型(学习目标可以是CTR)
  2. 将训练好的FM模型Embedding取出
  3. 将每个uid对应的Embedding做avg pooling(平均)形成该用户最终的Embedding,item也做同样的操作
  4. 将所有的Embedding向量放入Faiss等
  5. 线上uid发出请求,取出对应的user embedding,进行检索召回

3.代码实现

pip install git+https://github.com/coreylynch/pyFM

安装pyfm包

(1)简单实现

from pyfm import pylibfm
from sklearn.feature_extraction import DictVectorizer
import numpy as np

train = [
	{"user": "1", "item": "5", "age": 19},
	{"user": "2", "item": "43", "age": 33},
	{"user": "3", "item": "20", "age": 55},
	{"user": "4", "item": "10", "age": 20},
]
v = DictVectorizer()
X = v.fit_transform(train)

y = np.repeat(1.0,X.shape[0])

fm = pylibfm.FM()
fm.fit(X,y)
fm.predict(v.transform({"user": "1", "item": "10", "age": 24}))

(2)回归问题实战

数据集

import numpy as np
from sklearn.feature_extraction import DictVectorizer
from pyfm import pylibfm

# 读入数据
def loadData(filename,path="ml-100k/"):
	data = []
	y = []
	users=set()
	items=set()
	with open(path+filename) as f:
		for line in f:
			(user,movieid,rating,ts)=line.split('\t')
			data.append({ "user_id": str(user), "movie_id": str(movieid)})
			y.append(float(rating))
			users.add(user)
			items.add(movieid)
	return (data, np.array(y), users, items)

# 导入训练集和测试集,并转换格式
(train_data, y_train, train_users, train_items) = loadData("ua.base")
(test_data, y_test, test_users, test_items) = loadData("ua.test")
v = DictVectorizer()
X_train = v.fit_transform(train_data)
X_test = v.transform(test_data)

# 训练模型并测试
fm = pylibfm.FM(num_factors=10, num_iter=100, verbose=True, task="regression",
initial_learning_rate=0.001, learning_rate_schedule="optimal")
fm.fit(X_train,y_train)

# 预测结果打印误差
preds = fm.predict(X_test)
from sklearn.metrics import mean_squared_error
print("FM MSE: %.4f" % mean_squared_error(y_test,preds))

(3)分类问题实战

import numpy as np
from sklearn.feature_extraction import DictVectorizer
from sklearn.cross_validation import train_test_split
from pyfm import pylibfm
from sklearn.datasets import make_classification

X, y = make_classification(n_samples=1000,n_features=100, n_clusters_per_class=1)
data = [ {v: k for k, v in dict(zip(i, range(len(i)))).items()} for i in X]

X_train, X_test, y_train, y_test = train_test_split(data, y, test_size=0.1, random_state=42)

v = DictVectorizer()
X_train = v.fit_transform(X_train)
X_test = v.transform(X_test)

fm = pylibfm.FM(num_factors=50, num_iter=10, verbose=True, task="classification",
initial_learning_rate=0.0001, learning_rate_schedule="optimal")
fm.fit(X_train,y_train)

from sklearn.metrics import log_loss
print("Validation log loss: %.4f" % log_loss(y_test,fm.predict(X_test)))

和回归问题主要改变的参数是误差函数、 num_factors 和 tasks

以上内容来源于datawhale组队学习

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值