scala-MLlib官方文档---spark.ml package--ML Pipelines+Collaborative Filtering+Frequent Pattern Mining

三、 ML Pipeline

Main concepts in Pipelines(管道中的主要概念)

MLlib对用于机器学习算法的API进行了标准化,从而使将多种算法组合到单个管道或工作流中变得更加容易。本节介绍了Pipelines API引入的关键概念,其中,管道概念主要受scikit-learn项目的启发。

  • DataFrame: 该ML API使用Spark SQL中的DataFrame作为ML数据集,该数据集可以保存各种数据类型。例如,DataFrame可以具有不同的列,用于存储文本,特征向量,真实标签和预测。
  • Transformer: 变压器是一种算法,可以将一个DataFrame转换为另一个DataFrame。例如,ML模型是一个Transformer,它将具有特征的DataFrame转换为具有预测的DataFrame。
  • Estimator: 估算器是一种算法,可以适合于DataFrame来生成Transformer。例如,学习算法是在DataFrame上进行训练并生成模型的Estimator。
  • Pipeline: 管道将多个“变形器”和“估计器”链接在一起,以指定ML工作流程。
  • Parameter: 现在,所有的“变形金刚”和“估计器”都共享一个用于指定参数的通用API。

1、DataFrame
机器学习可以应用于多种数据类型,例如矢量,文本,图像和结构化数据。该API采用Spark SQL中的DataFrame,以支持各种数据类型。
DataFrame支持许多基本类型和结构化类型。请参阅Spark SQL数据类型参考以获取受支持类型的列表。除了Spark SQL指南中列出的类型之外,DataFrame还可以使用ML Vector类型。
可以从常规RDD隐式或显式创建DataFrame。有关示例,请参见下面的代码示例和Spark SQL编程指南
命名DataFrame中的列。下面的代码示例使用诸如“文本”,“功能”和“标签”之类的名称。

2、Pipeline components
1)转换器
变形器是一种抽象,其中包括特征转换器和学习的模型。从技术上讲,Transformer实现了transform()方法,该方法通常通过附加一个或多个列将一个DataFrame转换为另一个。例如:

  • 特征转换器可以获取一个DataFrame,读取一列(例如,文本),将其映射到一个新列(例如,特征向量),然后输出一个新的DataFrame并附加映射的列。
  • 学习模型可能需要一个DataFrame,读取包含特征向量的列,预测每个特征向量的标签,然后输出带有预测标签的新DataFrame作为列添加。

2)估计器
估计器抽象学习算法或适合或训练数据的任何算法的概念。从技术上讲,估算器实现了fit()方法,该方法接受DataFrame并生成一个Model(即Transformer)。例如,诸如LogisticRegression之类的学习算法是Estimator,调用fit()会训练LogisticRegressionModel,后者是Model,因此是Transformer。

3)管道组件的属性
Transformer.transform()和Estimator.fit()都是无状态的。将来,可以通过替代概念来支持有状态算法。
每个Transformer或Estimator实例都有一个唯一的ID,该ID对指定参数(在下面讨论)很有用。

3、管道
在机器学习中,通常需要运行一系列算法来处理数据并从中学习。例如,简单的文本文档处理工作流程可能包括几个阶段:

  • 将每个文档的文本拆分为单词。
  • 将每个文档的单词转换为数字特征向量。
  • 使用特征向量和标签学习预测模型。

MLlib将这样的工作流表示为“管道”,它由要按特定顺序运行的一系列管道阶段(变形器和估计器)组成。在本节中,我们将使用此简单的工作流作为运行示例。
1)How it works
管线被指定为阶段序列,每个阶段可以是一个Transformer或Estimator。这些阶段按顺序运行,并且输入DataFrame在通过每个阶段时都会进行转换。对于Transformer阶段,在DataFrame上调用transform()方法。对于Estimator阶段,调用fit()方法以生成一个Transformer(它将成为PipelineModel或已拟合Pipeline的一部分),并且在DataFrame上调用该Transformer的transform()方法。
我们通过简单的文本文档工作流程对此进行说明。下图是管道的培训时间使用情况。
在这里插入图片描述
上方的第一行代表三个阶段的管道。前两个(令牌生成器和HashingTF)是“变形金刚”(蓝色),第三个(LogisticRegression)是“估计”(红色)。最下面的行表示流经管道的数据,圆柱体表示DataFrame。 在原始DataFrame上调用Pipeline.fit()方法,该DataFrame具有原始文本文档和标签。 Tokenizer.transform()方法将原始文本文档拆分为单词,并向DataFrame添加带有单词的新列。HashingTF.transform()方法将word列转换为特征向量,并将带有这些向量的新列添加到DataFrame。现在,由于LogisticRegression是Estimator,因此管道首先调用LogisticRegression.fit()来生成LogisticRegressionModel。如果管道中有更多估算器,则在将DataFrame传递到下一阶段之前,将在DataFrame上调用LogisticRegressionModel的transform()方法。
管道是估算器。因此,在运行Pipeline的fit()方法之后,它将生成PipelineModel,它是一个Transformer。该PipelineModel在测试时使用;下图说明了这种用法。
在这里插入图片描述
在上图中,PipelineModel具有与原始Pipeline相同的阶段数,但是原始Pipeline中的所有Estimator都已变为Transformers。在测试数据集上调用PipelineModel的transform()方法时,数据将按顺序通过拟合的管道。每个阶段的transform()方法都会更新数据集,并将其传递到下一个阶段。
管道和管道模型有助于确保训练和测试数据经过相同的特征处理步骤。
2)细节
DAG Pipelines(DAG管道):管道的阶段被指定为有序数组。此处给出的所有示例均适用于线性管道,即每个阶段使用前一阶段产生的数据的管道。只要数据流图形成有向非循环图(DAG),就可以创建非线性管道。当前根据每个阶段的输入和输出列名称(通常指定为参数)隐式指定该图。 If the Pipeline forms a DAG, then the stages must be specified in topological order.
Runtime checking: 由于管道可以对具有各种类型的DataFrame进行操作,因此它们不能使用编译时类型检查。相反,Pipelines和PipelineModels在实际运行Pipeline之前会进行运行时检查。此类型检查使用DataFrame架构完成,该架构是对DataFrame中列的数据类型的描述。
Unique Pipeline stages: 管道的阶段应该是唯一的实例。例如,同一实例myHashingTF不应两次插入到管道中,因为管道阶段必须具有唯一的ID。但是,由于将使用不同的ID创建不同的实例,因此可以将不同的实例myHashingTF1和myHashingTF2(均为HashingTF类型)放入同一管道中。
4、Parameters(参数)
MLlib估计器和变形器使用统一的API来指定参数。
参数是具有独立文件的命名参数。 ParamMap是一组(参数,值)对。
将参数传递给算法的主要方法有两种:

  • Set 设置实例的参数。例如,如果lr是LogisticRegression的实例,则可以调用lr.setMaxIter(10)以使lr.fit()最多使用10次迭代。该API与spark.mllib软件包中使用的API相似。
  • 将ParamMap传递给fit()或transform()。 ParamMap中的任何参数都将覆盖先前通过setter方法指定的参数。

参数属于估计器和变形器的特定实例。例如,如果我们有两个LogisticRegression实例lr1和lr2,则可以使用指定的两个maxIter参数来构建ParamMap:ParamMap(lr1.maxIter-> 10,lr2.maxIter-> 20)。如果管道中有两个带有maxIter参数的算法,这将很有用。

5、ML persistence : saving and loading pipelines
通常,将模型或管道保存到磁盘以供以后使用是值得的。在Spark 1.6中,模型导入/导出功能已添加到管道API。从Spark 2.3开始,spark.ml和pyspark.ml中基于DataFrame的API已有完整介绍。
ML持久性可跨Scala,Java和Python使用。但是,R当前使用修改后的格式,因此保存在R中的模型只能重新加载到R中。以后应该修复此问题,并在SPARK-15572中进行跟踪。
1)ML持久性的向后兼容性
通常,MLlib为ML持久性保持向后兼容性。也就是说,如果您将ML模型或管道保存在一个版本的Spark中,则应该能够将其重新加载并在以后的Spark版本中使用。但是,有极少数例外,如下所述。
模型持久性:是否可以通过Y版本的Spark加载使用X版本X中的Apache Spark ML持久性保存的模型或管道?

  • 主要版本:不保证,但尽力而为。
  • 次要版本和补丁程序版本:是;这些是向后兼容的。
  • 关于格式的注意事项:不能保证稳定的持久性格式,但是模型加载本身被设计为向后兼容。

模型行为:Spark版本X中的模型或管道在Spark版本Y中的行为是否相同?

  • 主要版本:不保证,但尽力而为。
  • 次要版本和补丁程序版本:除错误修复外,行为相同。

对于模型持久性和模型行为,在次要版本或补丁版本中的所有重大更改都将在Spark版本发行说明中报告。如果发行说明中未报告损坏,则应将其视为要修复的错误。

示例代码

本节提供了说明上述功能的代码示例。有关更多信息,请参阅API文档(Scala,Java和Python)。
1)Example : 估计器,变压器和参数

import org.apache.spark.ml.classification.LogisticRegression
import org.apache.spark.ml.linalg.{Vector, Vectors}
import org.apache.spark.ml.param.ParamMap
import org.apache.spark.sql.Row

// Prepare training data from a list of (label, features) tuples.
val training = spark.createDataFrame(Seq(
  (1.0, Vectors.dense(0.0, 1.1, 0.1)),
  (0.0, Vectors.dense(2.0, 1.0, -1.0)),
  (0.0, Vectors.dense(2.0, 1.3, 1.0)),
  (1.0, Vectors.dense(0.0, 1.2, -0.5))
)).toDF("label", "features")

// Create a LogisticRegression instance. This instance is an Estimator.
val lr = new LogisticRegression()
// Print out the parameters, documentation, and any default values.
println(s"LogisticRegression parameters:\n ${lr.explainParams()}\n")

// We may set parameters using setter methods.
lr.setMaxIter(10)
  .setRegParam(0.01)

// Learn a LogisticRegression model. This uses the parameters stored in lr.
val model1 = lr.fit(training)
// Since model1 is a Model (i.e., a Transformer produced by an Estimator),
// we can view the parameters it used during fit().
// This prints the parameter (name: value) pairs, where names are unique IDs for this
// LogisticRegression instance.
println(s"Model 1 was fit using parameters: ${model1.parent.extractParamMap}")

// We may alternatively specify parameters using a ParamMap,
// which supports several methods for specifying parameters.
val paramMap = ParamMap(lr.maxIter -> 20)
  .put(lr.maxIter, 30)  // Specify 1 Param. This overwrites the original maxIter.
  .put(lr.regParam -> 0.1, lr.threshold -> 0.55)  // Specify multiple Params.

// One can also combine ParamMaps.
val paramMap2 = ParamMap(lr.probabilityCol -> "myProbability")  // Change output column name.
val paramMapCombined = paramMap ++ paramMap2

// Now learn a new model using the paramMapCombined parameters.
// paramMapCombined overrides all parameters set earlier via lr.set* methods.
val model2 = lr.fit(training, paramMapCombined)
println(s"Model 2 was fit using parameters: ${model2.parent.extractParamMap}")

// Prepare test data.
val test = spark.createDataFrame(Seq(
  (1.0, Vectors.dense(-1.0, 1.5, 1.3)),
  (0.0, Vectors.dense(3.0, 2.0, -0.1)),
  (1.0, Vectors.dense(0.0, 2.2, -1.5))
)).toDF("label", "features")

// Make predictions on test data using the Transformer.transform() method.
// LogisticRegression.transform will only use the 'features' column.
// Note that model2.transform() outputs a 'myProbability' column instead of the usual
// 'probability' column since we renamed the lr.probabilityCol parameter previously.
model2.transform(test)
  .select("features", "label", "myProbability", "prediction")
  .collect()
  .foreach { case Row(features: Vector, label: Double, prob: Vector, prediction: Double) =>
    println(s"($features, $label) -> prob=$prob, prediction=$prediction")
  }

2)Example :管道

import org.apache.spark.ml.{Pipeline, PipelineModel}
import org.apache.spark.ml.classification.LogisticRegression
import org.apache.spark.ml.feature.{HashingTF, Tokenizer}
import org.apache.spark.ml.linalg.Vector
import org.apache.spark.sql.Row

// Prepare training documents from a list of (id, text, label) tuples.
val training = spark.createDataFrame(Seq(
  (0L, "a b c d e spark", 1.0),
  (1L, "b d", 0.0),
  (2L, "spark f g h", 1.0),
  (3L, "hadoop mapreduce", 0.0)
)).toDF("id", "text", "label")

// Configure an ML pipeline, which consists of three stages: tokenizer, hashingTF, and lr.
val tokenizer = new Tokenizer()
  .setInputCol("text")
  .setOutputCol("words")
val hashingTF = new HashingTF()
  .setNumFeatures(1000)
  .setInputCol(tokenizer.getOutputCol)
  .setOutputCol("features")
val lr = new LogisticRegression()
  .setMaxIter(10)
  .setRegParam(0.001)
val pipeline = new Pipeline()
  .setStages(Array(tokenizer, hashingTF, lr))

// Fit the pipeline to training documents.
val model = pipeline.fit(training)

// Now we can optionally save the fitted pipeline to disk
model.write.overwrite().save("/tmp/spark-logistic-regression-model")

// We can also save this unfit pipeline to disk
pipeline.write.overwrite().save("/tmp/unfit-lr-model")

// And load it back in during production
val sameModel = PipelineModel.load("/tmp/spark-logistic-regression-model")

// Prepare test documents, which are unlabeled (id, text) tuples.
val test = spark.createDataFrame(Seq(
  (4L, "spark i j k"),
  (5L, "l m n"),
  (6L, "spark hadoop spark"),
  (7L, "apache hadoop")
)).toDF("id", "text")

// Make predictions on test documents.
model.transform(test)
  .select("id", "text", "probability", "prediction")
  .collect()
  .foreach { case Row(id: Long, text: String, prob: Vector, prediction: Double) =>
    println(s"($id, $text) --> prob=$prob, prediction=$prediction")
  }

七、Collaborative Filtering

协作过滤通常用于推荐系统。这些技术旨在填充用户项关联矩阵的缺失条目。 spark.ml当前支持基于模型的协作过滤,其中通过一小部分潜在因素来描述用户和产品,这些潜在因素可用于预测缺少的条目。 spark.ml使用交替最小二乘(ALS)算法来学习这些潜在因素。 spark.ml中的实现具有以下参数:

  • numBlocks 是将用户和项目划分为块以并行计算的块数(默认为10)。
  • rank 是模型中潜在因子的数量(默认为10)。
  • maxIter 是要运行的最大迭代次数(默认为10)。
  • regParam在ALS中指定正则化参数(默认为1.0)。
    -implicitPrefs 指定是使用显式反馈ALS变体还是适用于隐式反馈数据的变体(默认为false,这意味着使用显式反馈)。
  • alpha是适用于ALS的隐式反馈变量的参数,用于控制偏好观察中的基线置信度(默认为1.0)。
  • nonnegative 指定是否对最小二乘使用非负约束(默认为false)。

注意:用于ALS的基于DataFrame的API当前仅支持用户和项目ID的整数。用户和项目ID列支持其他数字类型,但是ID必须在整数值范围内。

Explicit vs. Implicit feedback(显式与隐式反馈)

基于矩阵分解的协作过滤的标准方法将用户项矩阵中的条目视为用户对项目(例如,给电影评分的用户)赋予的明确偏好。
在许多实际用例中,通常只能访问隐式反馈(例如,视图,点击,购买,喜欢,分享等)。 spark.ml中用于处理此类数据的方法来自隐式反馈数据集的协作过滤。从本质上讲,此方法不是尝试直接对评分矩阵建模,而是将数据视为代表用户操作观察力的数字(例如,点击次数或某人观看电影的累积时间)。然后,这些数字与观察到的用户偏好的置信度有关,而不是与对商品的明确评分有关。然后,该模型尝试查找可用于预测用户对某项商品的期望偏好的潜在因素。

Scaling of the regularization parameter(正则化参数的缩放)

在解决每个最小二乘问题时,我们根据用户在更新用户因子时生成的评分数量或在更新产品因数中获得的产品评分数量来缩放正则化参数regParam。这种方法称为“ ALS-WR”,并在论文“ Netflix奖的大规模并行协作过滤”中进行了讨论。
它使regParam减少了对数据集规模的依赖,因此我们可以将从采样子集中学习的最佳参数应用于整个数据集,并期望获得类似的性能。

cold-start strategy(冷启动策略)

使用ALSModel进行预测时,通常会遇到训练模型期间不存在的用户和/或测试数据集中的项目。这通常在两种情况下发生:

  • 在生产中,适用于没有评级历史记录且尚未训练模型的新用户或新项目(这是“冷启动问题”)。
  • 交叉验证期间, 数据分为训练集和评估集。当在Spark的CrossValidator或TrainValidationSplit中使用简单的随机分割时,实际上很常见的是遇到评估集中未包含的用户和/或项目。

默认情况下,当模型中不存在用户和/或项目因子时,Spark在ALSModel.transform期间分配NaN预测。这在生产系统中可能很有用,因为它表明有新用户或新物品,因此系统可以做出一些后备决策以用作预测。
但是,这在交叉验证期间是不可取的,因为任何NaN预测值都将导致评估指标的NaN结果(例如,使用RegressionEvaluator时)。这使得无法选择模型。
Spark允许用户将coldStartStrategy参数设置为“ drop”,以便删除包含NaN值的预测数据框中的任何行。然后,将根据非NaN数据计算评估指标并将其有效。下例说明了此参数的用法。
注意:当前支持的冷启动策略是“ nan”(上述默认行为)和“ drop”。将来可能会支持其他策略。
示例代码
在以下示例中,我们从MovieLens数据集中加载收视率数据,每一行包括用户,电影,收视率和时间戳。然后,我们训练一个ALS模型,该模型默认情况下假设等级为显式(implicitPrefs为false)。我们通过测量评分预测的均方根误差来评估推荐模型。
有关该API的更多详细信息,请参阅ALS Scala文档。

import org.apache.spark.ml.evaluation.RegressionEvaluator
import org.apache.spark.ml.recommendation.ALS

case class Rating(userId: Int, movieId: Int, rating: Float, timestamp: Long)
def parseRating(str: String): Rating = {
  val fields = str.split("::")
  assert(fields.size == 4)
  Rating(fields(0).toInt, fields(1).toInt, fields(2).toFloat, fields(3).toLong)
}

val ratings = spark.read.textFile("data/mllib/als/sample_movielens_ratings.txt")
  .map(parseRating)
  .toDF()
val Array(training, test) = ratings.randomSplit(Array(0.8, 0.2))

// Build the recommendation model using ALS on the training data
val als = new ALS()
  .setMaxIter(5)
  .setRegParam(0.01)
  .setUserCol("userId")
  .setItemCol("movieId")
  .setRatingCol("rating")
val model = als.fit(training)

// Evaluate the model by computing the RMSE on the test data
// Note we set cold start strategy to 'drop' to ensure we don't get NaN evaluation metrics
model.setColdStartStrategy("drop")
val predictions = model.transform(test)

val evaluator = new RegressionEvaluator()
  .setMetricName("rmse")
  .setLabelCol("rating")
  .setPredictionCol("prediction")
val rmse = evaluator.evaluate(predictions)
println(s"Root-mean-square error = $rmse")

// Generate top 10 movie recommendations for each user
val userRecs = model.recommendForAllUsers(10)
// Generate top 10 user recommendations for each movie
val movieRecs = model.recommendForAllItems(10)

// Generate top 10 movie recommendations for a specified set of users
val users = ratings.select(als.getUserCol).distinct().limit(3)
val userSubsetRecs = model.recommendForUserSubset(users, 10)
// Generate top 10 user recommendations for a specified set of movies
val movies = ratings.select(als.getItemCol).distinct().limit(3)
val movieSubSetRecs = model.recommendForItemSubset(movies, 10)

如果评级矩阵是从其他信息源中得出的(即是从其他信号推断得出的),则可以将implicitPrefs设置为true以获得更好的结果:

val als = new ALS()
  .setMaxIter(5)
  .setRegParam(0.01)
  .setImplicitPrefs(true)
  .setUserCol("userId")
  .setItemCol("movieId")
  .setRatingCol("rating")

八、Frequent Pattern Mining

挖掘频繁项,项集,子序列或其他子结构通常是分析大规模数据集的第一步,而这是多年来数据挖掘中的活跃研究主题。我们建议用户参考Wikipedia的关联规则学习以获取更多信息。

FP-Growth

FP增长算法在Han等人的论文中进行了描述,该算法在不生成候选者的情况下挖掘频繁模式,其中“ FP”代表频繁模式。 给定交易数据集,FP增长的第一步是计算项目频率并识别频繁项目。与为相同目的设计的类似Apriori的算法不同,FP-growth的第二步使用后缀树(FP-tree)结构对交易进行编码,而无需显式生成候选集,这通常成本较高。第二步之后,可以从FP树中提取频繁项集。在spark.mllib中,我们实现了称为PFP的FP-growth的并行版本,如Li et al。,PFP:并行FP-growth用于查询推荐中所述。 PFP基于事务的后缀来分配增长的FP树的工作,因此比单机实现更具可伸缩性。我们请用户参考这些文件以获取更多详细信息。
spark.ml的FP-growth实现采用以下(超)参数:

  • minSupport: 对某个项目集的最低支持程度被确定为频繁。例如,如果某项出现在5个事务中的3个,则其支持率为3/5 = 0.6。
  • minConfidence: 生成关联规则的最低置信度。置信度表示发现关联规则为真的频率。例如,如果在事务项集中X出现4次,X和Y仅同时出现2次,则规则X => Y的置信度就是2/4 = 0.5。该参数不会影响频繁项目集的挖掘,但会指定从频繁项目集生成关联规则的最小置信度。
  • numPartitions: 用于分发作品的分区数。默认情况下,未设置参数,并且使用输入数据集的分区数。

FPGrowthModel提供:

  • freqItemsets:数据集格式的常用项目集(“ items” [Array],“ freq” [Long])
  • associationRules: 在minConfidence以上的置信度下生成的关联规则,格式为DataFrame(“ antecedent” [Array],“ consquent” [Array],“ confidence” [Double])。
  • transform: 对于itemsCol中的每个事务,转换方法都会将其项目与每个关联规则的前提进行比较。如果记录包含特定关联规则的所有前提,那么该规则将被视为适用,并且其结果将添加到预测结果中。变换方法将从所有适用规则中得出的结果总结为预测。预测列的数据类型与itemsCol相同,并且在itemsCol中不包含现有项目。
import org.apache.spark.ml.fpm.FPGrowth

val dataset = spark.createDataset(Seq(
  "1 2 5",
  "1 2 3 5",
  "1 2")
).map(t => t.split(" ")).toDF("items")

val fpgrowth = new FPGrowth().setItemsCol("items").setMinSupport(0.5).setMinConfidence(0.6)
val model = fpgrowth.fit(dataset)

// Display frequent itemsets.
model.freqItemsets.show()

// Display generated association rules.
model.associationRules.show()

// transform examines the input items against all the association rules and summarize the
// consequents as prediction
model.transform(dataset).show()

PrefixSpan

PrefixSpan是在Pei等人的《通过模式增长来挖掘顺序模式:PrefixSpan方法》中描述的顺序模式挖掘算法。我们为读者提供参考文献,以规范化顺序模式挖掘问题。
spark.ml的PrefixSpan实现采用以下参数:

  • minSupport:被视为频繁顺序模式所需的最低支持。
  • maxPatternLength:频繁顺序模式的最大长度。任何超出此长度的频繁模式都不会包含在结果中。
  • maxLocalProjDBSize: 在开始对投影数据库进行本地迭代处理之前,前缀投影数据库中允许的最大项目数。该参数应根据执行程序的大小进行调整。
  • sequenceCol: 数据集中的序列列的名称(默认为“序列”),此列中具有空值的行将被忽略。
import org.apache.spark.ml.fpm.PrefixSpan

val smallTestData = Seq(
  Seq(Seq(1, 2), Seq(3)),
  Seq(Seq(1), Seq(3, 2), Seq(1, 2)),
  Seq(Seq(1, 2), Seq(5)),
  Seq(Seq(6)))

val df = smallTestData.toDF("sequence")
val result = new PrefixSpan()
  .setMinSupport(0.5)
  .setMaxPatternLength(5)
  .setMaxLocalProjDBSize(32000000)
  .findFrequentSequentialPatterns(df)
  .show()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值