推荐系统(Recommender System)笔记 06:推荐系统实例

在本节中,我们将具体来看几家世界级的巨头公司对于推荐系统的应用实例

Facebook 的深度学习推荐系统

Facebook 于 2014 年提出了经典的 GBDT + LR 的 CTR 模型结构,之后又在 2019 年发布了最新的深度学习模型 DLRM (Deep Learning Recommender System)

Facebook 的推荐系统应用场景

Facebook 的推荐系统主要用于广告推荐,这是一个标准的 CTR 预估场景,系统输入用户 (User)、广告 (Ad)、上下文 (Context) 的相关特征,预估 CTR (即用户点击该广告的概率),以此作为广告排序和推荐的依据。

*如果考虑更具体的营收模式,Facebook 广告推荐系统还需要使用其他模块来根据 CTR 计算广告出价、回报率等一系列指标,不过我们在这里只关心 CTR 预估模型这一推荐系统的主体部分

GBDT + LR 组合模型

在此之前我们已经介绍过了 GBDT + LR 模型,该模型简单来说就是使用 GBDT 自动组合特征,生成新的离散型特征向量,再将该特征向量输入给 LR 模型进行 CTR 预估。 由于 GBDT 以及 LR 这两个部分使用同样的优化目标进行训练,所以在训练的过程中,不需要考虑将 LR 的梯度回传给 GBDT,而是将它们完全作为一个整体来训练。

该模型在实际应用中,GBDT 子树规模这一超参数会对效果产生显著影响,Facebook 给出的子树规模与模型损失 (Loss) 的关系曲线如下所示:

在这里插入图片描述

根据这张关系图,我们可以清楚看到当子树规模超过 500 时,继续增大子树规模对于损失的减少作用微乎其微。因此 500 左右应该是一个比较合理的子树规模取值。

实时数据流框架

为了协助模型的准实时训练以及特征的准实时更新,Facebook 基于 Scribe 构建了 online data joiner 模块:

在这里插入图片描述

根据该模块的名字就能知晓其具体的职责:准实时地把来自各不同数据流的数据整合在一起,组成训练样本,并最终与点击数据进行整合,形成完整有标签的样本数据。 在这一过程中,有 3 点需要注意:

  1. waiting window (数据等待窗口) : 所谓的在 DIEN 的 基础上,将 用户 的兴趣细 分 为 不 同 兴趣 通道 , 进一步模 拟用 户在
    不 同兴趣通道上 的 演 化过程 , 生 成不同 兴趣通道的 记忆向 量 , 再利用 注意 力机制
    作用于多层神经网络数据等待窗口指的是在曝光发生之后,要等待多久才能够判定发生了对应的点击行为。
    因此如果这个窗口过长,会影响数据实时性,如果过短,则会有一部分点击数据来不及和曝光数据进行关联。(单纯从数据操作角度来看,假设该时间窗口为 40 分钟,则一个广告在曝光之后的 40 分钟之内有点击数据,则可以将曝光数据与此点击数据相关联,若超出 40 分钟,即使该广告有点击数据,则也无法关联)
  2. 分布式架构与全局统一的 ID (行为 ID): Facebook 为每个行为建立了全局统一的请求 ID (Request ID),并且建立了 Hash Queue 来缓存曝光记录。在这个队列中,如果某条曝光记录在等待窗口过去后还未匹配到点击数据,就会被判定为负样本 (Negative Sample)
  3. 数据流保护机制: Facebook 专门开发的保护机制,为了避免 online data joiner 出现问题,导致所有样本变为负样本

降采样和模型校正

为了控制数据规模,Facebook 选择使用负采样,即保留所有正样本,对负样本进行降采样,提升模型训练效率,同时平衡正负样本(因为根据经验来说,负样本的数量远远高于正样本)。 根据 Facebook 的实验,负采样频率选择 0.025 会比较理想。

但是负采样有可能会导致真实 CTR 从 0.1% 漂移到 10%。为了确保 CTR 的准确性,就需要进行校正,校正公式为:

在这里插入图片描述

q 就是校正后的 CTR,p 是模型得到的预估 CTR,w 是采样频率。

DLRM

DLRM 的模型结构图如下所示:

在这里插入图片描述
模型各层的作用分别为:

  • 特征工程: 共有两类特征,第一类是类别、id 类特征,这类特征使用 one-hot 编码生成稀疏特征 (Sparse Feature);第二类是数值型连续特征,这是稠密特征 (Dense Feature)
  • Embedding 层: 该层的作用就在于将类别型特征的 one-hot 向量转换为稠密的 n 维 Embedding 向量。至于那些连续型的特征则被拼接为一个特征向量,输入给黄色的 MLP 以生成 n 维向量。换言之,无论类别型还是连续型特征,经过 Embedding 层之后都被转换为 n 维 Embedding 向量
  • 神经网络层 (NNs)
  • 特征交互层 (Interaction): 这一层会将之前的 Embedding 两两做内积,再与之前连续型特征对应的 Embedding 连接,输入后续的 MLP
  • 目标拟合层: 一个全链接多层神经网络,最后一层使用 sigmoid 进行最终的 CTR 预估

DLRM 的并行训练

DLRM 融合了模型并行和数据并行(在之前的笔记 推荐系统(Recommender System)04:推荐系统工程实现 中我们已经对这两种并行训练方式进行了比较细致的了解,这里只做简单介绍):

  • 对于 Embedding 部分采用了模型并行。每一个计算结点(设备)仅保存一部分 Embedding 层参数,每个节点(设备)进行并行 mini batch 梯度更新时,仅更新自己节点上的部分 Embedding 层参数。 以此减轻大量 Embedding 层参数导致的内存不足
  • 对于 MLP 部分以及特征交互层使用了数据并行。即每个计算结点(设备)拥有全部模型参数,各节点仅用一部分数据去计算梯度,之后汇总所有的梯度来更新模型参数。 方便并行进行前向和反向传播

Airbnb 基于 Embedding 的实时搜索推荐系统

回顾一下 Embedding 的定义,Embedding 不仅能把大量的稀疏的特征向量转换为稠密向量,同时它也会把物品/对象的语义特征纳入其中,这也是我们可以通过直接计算 Embedding 之间的内积来得到相似度的原因。Airbnb 灵活地使用了这一性质。

Airbnb 的推荐系统应用场景

Airbnb 作为一个短租平台,它所提供的服务相当直观。用户 (User) 通过键入价位、位置等信息以寻求合适的房源。此时,Airbnb 会为用户提供一张房源搜索推荐列表。根据一般的租房经验,在得到房源信息之后,用户 (User) 可能会有几种不同的行为:点击查看 (Click);立即预定 (Booking);与房东取得联系看房,这实际上表达了一种租房的意愿和请求 (Book Request)。

为了同时捕捉用户的 “长期” 与 “短期” 兴趣,Airbnb 选择首先分别对用户和房源进行 Embedding,然后根据 Embedding 来构造特征,输入排序模型。在具体的 Embedding 上,也细分了用户短期兴趣 Embedding 和用户长期兴趣 Embedding,前者是为了进行房源相似性推荐以及在 Session 内进行实时个性化推荐;后者是为了生成最终推荐结果时考虑到用户之前的预订偏好,进行成功率(用户预订率)更高的个性化推荐。

基于短期兴趣的房源 Embedding 方法

Airbnb 使用一个 Session 内点击数据对房源进行 Embedding,以此捕获用户于一次搜索过程中的短期兴趣。所谓的一个 Session 内点击数据就是一位用户在一次搜索过程中点击的房源序列。 这样的一个序列要满足 2 条件:

  • 只有在房源的详情页停留超过 30 秒才算一个 valid 的数据点(为了清洗噪声点)
  • 用户超过 30 分钟没有动作,那么该序列就会被打断(为了避免非相关/不连续序列的产生)

在得到这个点击房源序列之后,就可以使用 Item2vec 将这个序列作为一个 “句子” 样本进行 Embedding。Airbnb 选择使用 Word2vec 的 skip-gram 模型来进行 Embedding。 我们之前已经了解了 skip-gram 模型的目标函数为:

在这里插入图片描述

在使用负样本的训练方式之后,目标函数变为:

在这里插入图片描述
该式中的 σ 函数代表常见的 sigmoid 函数 , D 是正样本集合 , D’ 是负样本集合 。 因此我们如果将 Sigmoid 函数直接写明,则该目标函数应为:

在这里插入图片描述

式子的前半部分是正样本的形式,后半部分是负样本的形式。这里需要解释一下 Airbnb 的 Embedding 过程中,什么是正样本,什么是负样本:

  • 正样本:取自 Session 内的点击序列中的房源。表明用户对这些房源有兴趣,因此是正样本
  • 负样本:在确定了中心房源 (central listing) 之后,从所有房源的集合中随机选取一个房源作为负样本

为了适应真实的业务场景,Airbnb 也希望能将用户的预订 (Booking) 信息引入 Embedding,这就可以让 Airbnb 提供的艘噢所列表和相似房源列表更倾向于去推荐之前预订成功 Session 中的房源。 为此,Airbnb 把点击序列分为两种:

  1. 最终产生了预订结果的预订会话 (Booking Session)
  2. 最终没有产生预订的探索性会话 (Exploratory Session)

对于每个预订会话 (Booking Session) 来说,只有最后一个房源是被预订房源 (Booked Listing),我们现在要做的就是把这个预订信息引入 Word2vec 目标函数。为此,不管这个被预订房源在不在 Word2vec 滑动窗口之中,我们都要假设该被预订房源是与滑动窗口的中心房源 (Central Listing) 相关的,换言之,我们引入了一个全局上下文信息 (Global Context) 到目标函数中:

在这里插入图片描述
这里多出来的最后一项中,lb 就表示被预订房源 (Booked Listing),预订和点击同为正样本行为,所以和之前的正样本一样,加上了一个负号。这个最后一项没有 ∑ 是因为被预订的房源只有一个,所以中心房源只与这一个被预订房源有关。

除此以外,为了更好地发现同一市场内部房源的差异性,Airbnb 还加入了另一组负样本,即在与中心房源同一市场的房源集合中进行随机抽样,得到一组新的副样本,同样也加入到目标函数中:

在这里插入图片描述
在这最后一项中,Dmn 就表示同一地区的负样本集合。上面的这个式子也就是房源 Embedding 的最终目标函数。而对于冷启动问题,Aribnb 则选择使用附近 3 个同类型、价格相似的房源向量进行平均来应对。

基于长期兴趣的用户 Embedding 和房源 Embedding

上面基于短期兴趣的房源 Embedding 主要基于用户的点击序列数据,但该方法难以捕捉用户的长期兴趣。为了能捕获用户的长期偏好,Airbnb 选择使用预订会话序列 (Booking Session Sequence),换言之,这是一个记录了用户历史预订的序列。

比如用户 i 在过去一年预订了 5 个房源,那么预订会话序列就是 si = (li1, li2, li3, li4, li5)

但需要格外注意的是,这个序列无法像之前的点击会话 (Click Session) 那样直接使用 Word2vec 来进行 Embedding,这是因为:

  1. 一位用户预订行为发生的数量远远小于点击行为,这就会导致很多用户在过去一段时间内,预订会话序列的长度甚至只有 1
  2. 大部分房源被预订的次数也非常少,要想使用 Word2vec 训练出可靠的 Embedding,物品最少要出现 5~10 次

因此会有严重的数据稀疏问题。为了应对该问题,Airbnb 选择基于某些属性规则做相似用户和相似房源的聚合。

比如房源属性如下表所示:
在这里插入图片描述
此时就可以使用属性名称与对应的分桶 id 组成一个属性标识。比如某个房源的国家是美国 (US),房源属性为 Ent (Buckets = 1),每晚价格为 56~69 (Buckets = 3),则可以使用 US_lt1_pn3 来表示该房源的属性标识,对于用户也采用同样的方法。

在得到用户属性和房源属性之后,就可以将原本的预订序列转化为如下形式:

((utype1, ltype1), (utype2, ltype2), …, (utypeN, ltypeN))

该序列中,utype1 就表示用户在预订房源 l1 时的用户属性,因为用户的属性会随着时间变化,因此 utype1utype2 未必相同。在得到这个全新的序列之后,接下来要做的就是训练生成 Embedding,使用户和房源的属性都处于同一个空间中,这里可以完全沿用之前生成短期兴趣房源 Embedding 的目标函数,唯一的不同在于使用 (user type, listing type) 这样的一个元组替代了原本的房源。

在这里插入图片描述
其中,Dbook 是中心词附近的用户属性与房源属性的集合。所以实际上,在训练过程中,用户属性与房源属性完全被同等对待, 这样的方式保证了 二 者自然而然地在一个向量空间中生成。虽然整个过程浪费了一些信息,但从工程角度来看并没有问题。

搜索词的 Embedding

除了计算用户和房源的 Embedding,Airbnb 还在其搜索推荐系统中对搜索词 (query) 进行了 Embedding, 与用户 Embedding 的方法类似,通过把搜索词和房源置于同一向量空间进行 Embedding,再通过二者之间的余弦相似度进行排序。而引入 Embedding 之后,搜索结果甚至能够捕捉到搜索词的语义信息。

实时排序模型及其特征工程

在之前的步骤中,Airbnb 已经得到了短期和长期兴趣的用户和房源 Embedding,但是 Airbnb 并没有直接去使用 Embedding 相似度排名作为搜索推荐结果,而是基于 Embedding 进一步得到新的用户房源相关特征:

在这里插入图片描述
上表中最后一个特征:UserTypeListingTypeSim - 用户属性与房源属性相似度。该特征相似度就是使用用户属性与房源属性的长期兴趣 Embedding 计算得到的。而 EmbClickSIm 指的是候选房源与用户最近点击过的房源的相似度,也正是这些与最近点击相关的特征,加强了 Airbnb 系统的 “实时” 特性

在得到这些 Embedding 特征之后,会和其他特征一起输入搜索排序模型进行训练。Airbnb 选择的是 GBDT 模型。

YouTube 深度学习视频推荐系统

YouTube 的推荐系统应用场景

YouTube 与 Netflix 和国内的爱奇艺这样的流媒体平台很不相同,它没有自制的电影、电视剧这类的头部内容,同时因为各类视频数量和种类庞杂,用户很难找到自己真正感兴趣的内容。

至于 YouTube 的商业模式,我们在此之前已经说过不止一次了:利润主要来自视频广告,而广告的曝光机会与用户观看时长成正比。

所以 YouTube 的推荐团队构建了两个深度学习网络分别考虑召回率 (Recall) 和准确率 (Precision) 的要去,并构筑以用户观看市场为优化目标的排序模型,最大化用户观看时长,以此保证更高的广告曝光量。

YouTube 的推荐系统架构

由于 YouTube 的视频基数巨大,在如此规模下进行个性化推荐,就不得不考虑 “实时性” 的问题,为了降低在线系统推荐的延迟,不宜使用复杂的网络结构。因此 YouTube 选择使用两级深度学习模型完成推荐:

在这里插入图片描述
实际上这两级模型分别就对应推荐漏斗中的召回层以及排序层。事实上这两级模型的任务也确实和对应的层级完全一致。

候选集生成模型

对应推荐漏斗中的召回层,主要就是进行快速筛选,将候选视频集合从原本的百万量级降低到几百量级。

在这里插入图片描述
自底而上来看这个网络,最底层的输入是用户历史观看视频 Embedding 以及搜索词 Embedding 向量。这部分 Embedding 的生成同样也是利用 Word2vec 方法。在得到这两个 Embedding 之后,再把用户的地理属性特征 Embedding、年龄、性别等特征都连接起来,输入给上层的 ReLU 神经网络进行训练。

整个模型最后的输出层是一个 softmax 层,所以很显然,YouTube 将这个应用场景看作是一个多分类问题,模型最后的输出就是一个在所有候选集视频上的概率分布,也就是推断用户会喜欢每个候选视频的概率。

但是,具体到实际的业务场景中,我们不可能对于每一次推荐请求都完整运行一遍这样的一个候选集生成网络,这样的成本实在是太高了。因此,YouTube 选择在通过这个 “候选集生成模型” 得到用户和视频的 Embedding 之后,使用 Embedding 之间的 NN 方法来提供服务。这么做的好处就是不需要将整个模型都放在线上服务器,只需要将用户 Embedding 和视频 Embedding 都存储在 Redis 这样的线上内存数据库就好了。那么,我们是如何从这个模型中得到用户 Embedding 以及视频 Embedding 的呢?

我们之前已经知道,模型最后是一个 softmax 输出层,该层的参数实际上是一个 m x n 的矩阵,m 就是最后一个 ReLU 层的维度,n 则是最后分类的个数,这里也就是候选集视频的数量。因此,本质上该矩阵的每个列向量就是每个候选视频对应的 Embedding,这和 Word2vec 中词向量的生成过程完全一样。

那么,用户 Embedding 来自哪里呢?因为输入的特征向量全部都是用户相关的特征,所以在使用某用户 u 的特征向量作为模型输入时, 最后一层 ReLU 层的输出向量就可以当作该用户的 Embedding 向量。

得到这两类 Embedding 之后,将他们都导入到线上 Redis 数据库,在预测某用户的视频候选集时,先得到该用户的 Embedding 向量,再在视频 Embedding 向量空间中利用局部敏感哈希等方法搜索该用户 Embedding 向量的 TopK 近邻,就可以快速得到 k 个候选视频集合。

排序模型

对应推荐漏斗中的排序层,对几百量级的候选集视频进行排序,根据排序结果确定最终的推荐列表。

在这里插入图片描述
由于待操作的视频量级相比召回时已经从百万降低到几百,因此,在进行排序时就可以引入更多的特征进行精排序。

比如上图中,从左至右特征分别为:

  • 当前候选视频的 Embedding
  • 用户观看的最后 N 个视频的平均 Embedding

根据上面的结构图,我们可以看到,尽管模型结构和候选集生成模型比较类似,但是在输出层上差异非常明显,而且,在模型训练和提供服务时使用的函数也不一样。这么做的原因也是从业务角度出发进行的考量。

我们在分析 YouTube 的营收模式时已经说过,增加用户观看时长才是推荐系统的最主要的优化目标。所以在排序模型训练时,每次曝光期望观看时长 (Expected duration per expose) 是一个比较合理的优化目标。为了直接预估观看时长,将正样本的观看时长作为其样本权重,用加权逻辑回归进行训练,可以让模型学习到 “用户观看时长” 这一信息。

而在服务中使用 eWx+b 作为输出,则涉及到一个新的概念:机会比 (Odds)。机会比用以表示一件事发生与不发生概率的比值。在逻辑回归中,我们想要得到一件事发生的概率 p,就需要使用 Sigmoid 函数:

在这里插入图片描述
那么此时使用概率 p 计算 Odds 的话,结果为:

在这里插入图片描述
这就验证了模型服务过程中的输出就是 Odds。而之所以使用 Odds 作为输出,原因也不难理解,我们只需要结合加权逻辑回归进行理解即可,我们之前说过,为了能让模型学习到 “用户观看时长” 这一信息,我们设定的权值就是观看时长,因此正样本的发生概率就是原本的 Ti 倍,所以 Odds 应变为:

在这里插入图片描述
而由于在当前的推荐环境中,用户打开一个视频的概率 p 是相当低的,因此可以对上面的式子进行进一步的简化和近似:

在这里插入图片描述
所以变量 Odds 本质上就是每次曝光期望观看时长,正符合排序模型优化的目标。

训练和测试样本的处理

YouTube 在处理训练样本的工程中有以下 3 点值的注意:

  1. 在候选集生成模型中,输出层使用了 softmax,把任务变相转换为一个多分类任务,每个候选视频就是一个类别,但如此一来,类别会显得过多。为此,YouTube 选择使用负采样训练方法减少每次预测的分类数量,从而加快模型的收敛速度。
  2. 不采用原始的用户日志,而是对每个用户提取等量的训练样本,以此避免模型过分受到活跃用户的影响
  3. 处理测试集时,不采用经典的随机留一法,而是以用户最近一次观看作为测试集,这样可以避免引入未来信息,造成数据穿越问题
  4. 为了处理用户对于新视频的偏好。YouTube 引入了 Example Age 特征,它定义了训练样本产生的时刻距离当前时刻的时间。 比如 24 小时之前生成的训练样本,Example Age 的值就是 24;而做模型服务时,不管候选视频是哪个,该值都可以被设为 0。

阿里巴巴深度学习推荐系统

阿里巴巴的推荐系统应用场景

这可能是目前为止我们在日常生活中最常使用,最熟悉的推荐系统。从登录网站到购买成功,一般有以下几个步骤:

  • 登录
  • 搜索
  • 浏览
  • 点击
  • 添加购物车
  • 支付
  • 购买成功
    以上每个步骤都有用户流失,我们之前介绍的多目标优化模型 ESMM 就可以解决 “是否为各阶段单独建立模型” 的问题。

阿里巴巴的推荐模型体系

在这里插入图片描述

阿里巴巴深度学习推荐模型的进化

基础深度学习模型

基于经典的 Embedding + MLP 深度学习 模型架构,将用户行为历史的 Embedding 简单地通过加和池化操作叠加,再与其他用户特征、广告特征、场景特征连接后输入上层神经网络进行训练

DIN 模型

利用注意力机制替换基础模型的 Sum Pooling 操作,根据候选广告和用户历史行为之间的关系确定每个历史行为的权重

DIEN 模型

在 DIN 的基础上, 进一步改进对用户行为历史的建模,使用序列模型在用户行为历史之上抽取用户兴趣并模拟用户兴趣的演化过程

MIMN 模型

在 DIEN 的 基础上,将用户的兴趣细分为不同兴趣通道,进一步模拟用户在不同兴趣通道上的演化过程,生成不同兴趣通道的记忆向量,再利用注意力机制作用于多层神经网络

在这里插入图片描述
上图中每一副图片表明一位用户购买的一件商品,我们可以用这张图来理解阿里巴巴的几个推荐模型。可以看到,基础深度学习模型对于用户的行为一视同仁,不分重点。DIN 模型引入了注意力机制,此时每个商品就有了各自的权重,该权重就是基于当前商品与候选商品的关系,借助注意力机制学习得到的,因而模型拥有了有重点地看待用户行为的能力。DIEN 开始考虑用户行为与兴趣会随着时间变化,让模型拥有了下次购买的预测能力。最后的 MIMN 模型中,开始对用户的多个 “兴趣通道” 进行建模,根据商品的种类分配了多个序列,以此更精准地把握用户兴趣的变迁过程,避免不同兴趣之间的相互干扰。

因此,“用户兴趣” 始终是指引阿里巴巴发展其推荐模型的一杆大旗。

模型服务模块的技术架构

在这里插入图片描述
上图中展现了两种不同的架构,用虚线分割了线下和线上环境。这两个架构的主要区别在于对用户行为的处理:

  1. 用户兴趣表达模块:b 架构把 a 架构的 “用户行为特征 (User Behavior Features) 在线数据库” 替换为 “用户兴趣表达 (User Interest Representation) 在线数据库”。 因为 DIEN 以及 MIMN 表达用户兴趣的最终形式都是兴趣 Embedding 向量。如果在线服务过程中获取的是 “用户行为特征”,那还需要运行比较复杂的序列模型再生成 “用户兴趣向量”。因此,为了避免这个转换的步骤,选择直接把兴趣向量存储在线上,这样模型可以直接获取
  2. 用户兴趣中心模块:b 架构新增的模块 (UIC),它会根据用户行为序列生成用户兴趣向量。 我们上面所说的 DIEN 和 MIMN 其用户兴趣向量生成模型就是这部分。另外,因为 UIC 的存在,实时用户行为时间 (Realtime User Behavior Event) 的更新方式也会发生变化。在 a 架构中,一个新的用户行为时间发生,该事件就会被插入用户行为特征数据库;在 b 架构中,新的用户行为时间会触发 UIC 更新逻辑,UIC 会利用该事件更新对应用户的兴趣向量

撇除这两个模块,剩余模块在两个架构中的作用基本一致,我们可以直接分为离线部分和在线部分来看具体的运行逻辑:

  • 离线部分:学习模块 (Learner) 定期利用日志 (Logs) 训练和更新模型 (Model)。在 a 架构中,训练完成的模型直接部署上线;在 b 架构中,将模型拆分,用户兴趣向量生成部分部署在 UIC,其余部分部署在服务器
  • 在线部分:在线部分主要看它如何处理请求,具体的步骤为
    1. 请求携带用户 ID 以及待排序的候选商品的 ID 到来
    2. 实时预估服务根据用户 ID 、候选商品 ID 获取用户和商品特征。这里的用户特征在 a/b 架构中有所不同
    3. 实时预估服务使用用户以及商品特征进行预估和排序,返回最终的排序结果
  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值