(R语言)SVM的原理及入门使用(详细)

R语言入门LibSVM使用笔记

参考资料:
libsvm官方网站
R语言svm参考示例
机器学习与R语言 Brett Lantz(一本很好的R语言机器学习入坑书)

背景

LibSVM是由台湾大学林智仁教授领衔开发的一个用 C++ 编写的广泛使用的开源SVM程序。LibSVM是屡获殊荣的老牌SVM的程序(初代版本发行于2001年),在很多挑战赛中拿过奖。在一篇论文(2005,M. Pal)中,对遥感数据,检验了不同SVM包的效果,同样的参数,训练出的模型,在同等的精度下,LibSVM的运算时间少了一大截,这也是其内部算法的优势(下文会有个简单描述)。

SVM简介

+ 基本介绍

虽然作为SVM的应用者对SVM的原理是不需要深知,甚至不用知道。但是本文还是做一下简单的介绍,以下说法和一些例子不一定严谨,但是对于入门还是比较好懂。SVM的原理简单的一句话就是计算出多维空间中的最优决策分割,以二维空间为例,在二维空间中,假设有三类数据,这三类数据都有三种属性,一种是数据标识(标识他是哪一类),另外两种就是数据的特征(观测值),如下图所示(图片由LibSVM官网小程序提供),则SVM就会使用其算法输出一个能使每类间保留最大距离的边界如下图所示。
在这里插入图片描述
在这里插入图片描述

那么什么是最大距离线呢?可以简单理解为最凸包点的相连的垂直平分线。当然如果是个n维数据,SVM决策的边界是一个最大间隔超平面(Maximum Margin Hyperplane)。不过在很多情况下,数据并没有那么理想可分,也许就没有理想的边界能使SVM完全划分(非线性可分的),这就提到了SVM的最大特色,他通过以下方法解决:

  1. 直接上来说,即便不能找出一个完全分割每一类的平面,他也通过建立一个松弛变量,这样就创建了一个软间隔,允许一些点落在边界的不正确一边。
    min ⁡ w , b , ξ 1 2 w T w + C ∑ i = 1 l ξ i \min _{\mathbf{w}, b, \boldsymbol{\xi}} \quad \frac{1}{2} \mathbf{w}^{T} \mathbf{w}+C \sum_{i=1}^{l} \xi_{i} w,b,ξmin21wTw+Ci=1lξi
    这其中 C C C是一个成本参数(代价参数),修改这个值将调整对于落在超平面错误一边的案例的惩罚。成本参数越大,努力实现100%分离的优化就会越困难。另一方面,较小的成本参数将把重点放在更宽(广泛)的整体边缘
  2. 另外,svm通过核函数将数据映射到高维空间内可以使非线性数据变得线性可分。可以用下图直观的理解一下核函数的功能。在这里插入图片描述
    上图表示的是天气随经纬度的变化,恰巧数据没有探测到海拔的变化,显然左图的数据点是非线性可分的,通过核函数转化到了右图明显可以分开,通过核函数能使更多没有展现的数据特征被展现出来。常见的核函数有:
     linear:  K ( x i , x j ) = x i T x j  polynomial:  K ( x i , x j ) = ( γ x i T x j + r ) d , γ > 0  radial basis function  ( R B F ) : K ( x i , x j ) = exp ⁡ ( − γ ∥ x i − x j ∥ 2 ) , γ > 0  sigmoid:  K ( x i , x j ) = tanh ⁡ ( γ x i T x j + r ) \begin{aligned} &\text { linear: } K\left(\mathbf{x}_{i}, \mathbf{x}_{j}\right)=\mathbf{x}_{i}^{T} \mathbf{x}_{j}\\ &\text { polynomial: } K\left(\mathbf{x}_{i}, \mathbf{x}_{j}\right)=\left(\gamma \mathbf{x}_{i}{ }^{T} \mathbf{x}_{j}+r\right)^{d}, \gamma>0\\ &\text { radial basis function }(\mathrm{RBF}): K\left(\mathbf{x}_{i}, \mathbf{x}_{j}\right)=\exp \left(-\gamma\left\|\mathbf{x}_{i}-\mathbf{x}_{j}\right\|^{2}\right), \gamma>0\\ &\text { sigmoid: } K\left(\mathbf{x}_{i}, \mathbf{x}_{j}\right)=\tanh \left(\gamma \mathbf{x}_{i}{ }^{T} \mathbf{x}_{j}+r\right) \end{aligned}  linear: K(xi,xj)=xiTxj polynomial: K(xi,xj)=(γxiTxj+r)d,γ>0 radial basis function (RBF):K(xi,xj)=exp(γxixj2),γ>0 sigmoid: K(xi,xj)=tanh(γxiTxj+r)
    这里说明一下SVM模型的参数一般就是惩罚力度 C C C和核函数中的参数,其中比较常用的是线性核和RBF核,按照LibSVM的说明文档,他们极力推荐初学者使用RBF核函数。其理由如下:
  • RBF不像线性核函数可以处理标签与特征之间关系为非线性的情况
  • RBF核的通过特定的参数可以达到与线性核函数相同的效果
  • S形核函数有些特定的参数也与RBF核效果相同
  • 然后就是多项式核函数相对RBF有更多的参数,在参数确定上花的功夫要更多(因为参数确定基本是格网查找)
  • 最后一点就是RBF核函数映射出的值域为 [ 0 , 1 ] [0,1] [0,1],而多项式映射出的结果可能在 [ 0 , + ∞ ] [0,+∞] [0,+],所以RBF核不会带来数值问题。
+ 非基本问题

之前有提到,LibSVM有着异于其他SVM包的运算速度,这其实是有其算法原理的,SVM一开始只用于二分类问题,我之前描述的数学模型怎么看也不能分很多类的啊,所以学者们提出了 one against rest(一对其余) 和 one against one(一对一) 方法。

  • 一对其余的基本思想在于,将目标类为一类,其余的所有类划为另一类。这样就要对每个类别都进行一次‘一对其余’的分类,所以这种策略会产生n个分类器。
  • 一对一的基本思想是,将n个类别中的所有类别拉出来两两决斗,所以就有 C n 2 C_{n}^{2} Cn2种组合,即也会产生相应数量的分类器
  • 然后对于一个样本,模型会把所有的分类器都用一遍,然后所有分类器投票决定这个样本属于哪一类。

LibSVM就是使用的‘一对一’策略,且相比其他包的‘一对一’策略更快。

入门教程

接下来我将以一个简单的二分类问题向你展现R语言如何使用libSVM完成问题的,相应的优化流程也是LibSVM的初学者说明文档中一种推荐的使用策略。我们也会用数据对比这种策略与盲目的svm策略的优势。

+ 问题描述

在本次分析中,我们使用一个具有683个样本乳腺癌检查案例,每个数据除了是否确证乳腺癌一栏外还有10个特征。该数据来自UCI(UCI Machine Learning Data Repository),经过了LibSVM的人员的处理,去掉了缺省值。你可以在这个链接中找到该数据:
https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/

+ 初步方案

1、数据准备与探索
我将网站上的数据复制下来存在了一个文本文件中,首先我们使用read.table()将数据载入到R中,由于所有的变量都是数值型的,我们可以忽略stringAsFactor()的参数。

> b<-read.table('breast cancer_origin.txt') 
> str(b)
'data.frame':	683 obs. of  11 variables:
 $ V1 : num  2 2 2 2 2 4 2 2 2 2 ...
 $ V2 : chr  "1:1000025.000000" "1:1002945.000000" "1:1015425.000000" "1:1016277.000000" ...
 $ V3 : chr  "2:5.000000" "2:5.000000" "2:3.000000" "2:6.000000" ...
 $ V4 : chr  "3:1.000000" "3:4.000000" "3:1.000000" "3:8.000000" ...
 $ V5 : chr  "4:1.000000" "4:4.000000" "4:1.000000" "4:8.000000" ...
 $ V6 : chr  "5:1.000000" "5:5.000000" "5:1.000000" "5:1.000000" ...
 $ V7 : chr  "6:2.000000" "6:7.000000" "6:2.000000" "6:3.000000" ...
 $ V8 : chr  "7:1.000000" "7:10.000000" "7:2.000000" "7:4.000000" ...
 $ V9 : chr  "8:3.000000" "8:3.000000" "8:3.000000" "8:3.000000" ...
 $ V10: chr  "9:1.000000" "9:2.000000" "9:1.000000" "9:7.000000" ...
 $ V11: chr  "10:1.000000" "10:1.000000" "10:1.000000" "10:1.000000" ...

breast数据包含10个特征,和一个标签2、4(网站未说明哪种是阳性,经统计’4’类较少,所以下文认为’4’类是阳性)。
我们可以看到数据还存在冒号和列标号在前面,需要进一步的处理。除此之外svm要求将类别标识变量转换为因子。数据处理代码:

> a<-matrix(0,nrow(b),ncol(b))
> for (i in 1:nrow(b)){
+   for (j in 1:ncol(b)){
+     a[i,j]<-as.numeric(sub('.?.:','',b[i,j]))
+   }
+ }

其中sub是字符串替换函数,第一个参数为符合条件的正则表达式,.?.:的意思是:前面出现要么一个或者两个字符,这样就匹配了1~10:的情况。第二个参数为替换的目标,我们需要把冒号及其之前的所有字符都去掉,所以替换为空字符。

接下来是很关键的一步————标准化,大部分人模型精度怎么也不高的原因很可能是没有将数据标准化,标准化的原因这里不再赘述,另外切记标准化一定要在数据分块之前,也就是一定要将数据整体标准化。不统一的标准化,比如训练集和测试集分开标准化,也会导致模型精度降低。此外我们还要将模型分为训练集和测试集,以常规的3:1来分,使用sample函数可以保证随机抽取。

> x<-scale(a[,2:ncol(a)]);
> y<-as.factor(a[,1]);
> idx<-sample(nrow(x),nrow(x)*0.75);
> x_train<-x[idx,];
> y_train<-y[idx];
> x_test<-x[-idx,];
> y_test<-y[-idx];

2、模型的训练
为了使用LibSVM在R语言中提供的接口,该接口的提供者为David Meyer,我们需要下载e1071的包,除此之外我们需要进行简单的精度评定建议下载一个caret包,这个包提供很多基本机器学习函数,此外还提供了精度评定的函数。

e1071包中的svm算法用法如下:

svm(x, y = NULL, scale = TRUE, type = NULL, kernel =
"radial", degree = 3, gamma = if (is.vector(x)) 1 else 1 / ncol(x),
coef0 = 0, cost = 1, nu = 0.5, 
class.weights = NULL, cachesize = 40, tolerance = 0.001, epsilon = 0.1,
shrinking = TRUE, cross = 0, probability = FALSE, fitted = TRUE,
..., subset, na.action = na.omit)

作为入门学习,之前只介绍了径向基函数的核函数,知晓scale,kenerl,gamma,cost等参数就足够。原因如下:分类权重函数class.weights涉及用户的个性化分类要求,不是我们讨论的重点。其他的核函数的参数degree(多项式核函数),coef0(线性核函数)我们都用不到,然后关于一些系统参数,还有LibSVM提供的一些其他模型:如nu-SVC和SVM做回归的算法,笔者也并没有了解过。我们在此讨论问的是C-SVC方法,也就是我之前提到的模型。如需了解更多e1071的函数,可以在命令行输入??e1071,下载其说明文档。

目标参数的解释如下:

  • scale: 是否将数据进行标准化,此处标准化是将xy同时标准化。
  • kenerl: 核函数的类型
  • gamma: 径向基函数的唯一参数
  • cost: 代价函数的系数C

首先我们先进行一个简单的训练,模型参数是我随便打的两个参数,没有加上方法,看看精度如何。

> model<-svm(x_train, y_train, scale=FALSE, kernel='radial', gamma=2, cost= 0.2);
> y_pred <- predict(model, x_test);
> confusionMatrix(y_pred,y_test,positive = '4')
Confusion Matrix and Statistics

          Reference
Prediction   2   4
         2 102   0
         4  14  55
                                          
               Accuracy : 0.9181          
                 95% CI : (0.8664, 0.9545)
    No Information Rate : 0.6784          
    P-Value [Acc > NIR] : 6.53e-14        
                                          
                  Kappa : 0.8242          
                                          
 Mcnemar's Test P-Value : 0.000512        
                                          
            Sensitivity : 1.0000          
            Specificity : 0.8793          
         Pos Pred Value : 0.7971          
         Neg Pred Value : 1.0000          
             Prevalence : 0.3216          
         Detection Rate : 0.3216          
   Detection Prevalence : 0.4035          
      Balanced Accuracy : 0.9397          
                                          
       'Positive' Class : 4 

可以看到在测试集中我们训练的模型总体精度达到了0.918,且Kappa系数0.8242,结果还比较可观。但其中我们将14个‘2’类错分为了’4’类,也就是所谓的将并不是乳腺癌的患者诊断为了乳腺癌,这在机器学习中被称之为“假阳性”现象,实际来看会对患者造成额外的损失,所以我们考虑有什么其他方法将模型进一步优化。

+ 方案优化

其实刚刚那种SVM的做法是LibSVM guide中举出的一种反例,这种做法如果运气好,可以得出可观的结果(但并不是最优的),但是大多数情况效果是不如人意的。被推荐的做法是进行 交叉验证参数网格搜索 确定最优的模型,交叉验证可以解决模型的过拟合现象。
简单描述一下 交叉验证,将训练集分为K份,每次取出一份作为验证,其他剩余的(K-1)份作为训练,然后以此类推让每份数据都做一次验证,计算一次精度,最后精度取其平均来作为模型的好坏参数,这样下来就要计算K次模型。这样做的好处就是解决过拟合,接下来讲讲 过拟合 :打个比方,如果一个模型拟合的恰到好处,那么无论对于哪一折,精度应该都相当不错,所以取平均的话精度也不会差。但是如果是过拟合的话,对于某一折或很少几折精度可能非常高,但是对于其他的折精度就低了,取平均其精度就相对低,就会被淘汰。不进行交叉验证,训练集虽然精度很高,但是可能是过拟合,放在测试集精度就很低了,以下一张图可以帮助你理解SVM的过拟合:
在这里插入图片描述

这种做法具体步骤如下:

  1. 数据标准化
  2. 考虑核函数
  3. 用交叉验证加格网搜索找到最合适参数
  4. 将最合适的参数运用到整个训练集
  5. 检测

1、粗略网格查找
首先我们在训练集建立10折交叉验证和网格搜索参数,代码如下

> folds<-createFolds(y_train,k=10);
> comp1<-seq(-5,15,2);
> comp2<-seq(-15,3,2);
> C_crude<-2*10^comp1;
> gamma_crude<-2*10^comp2;
> accuracy_crude<-matrix(0,length(comp1),length(comp2)); 

其中createFolds()函数是caret包中提供的交叉验证的函数,该函数返回一个列表,列表的每一个元素是每一折的序号,gamma_crude与C_crude为建立的gamma和C的粗略查找网格查找范围分别为 [ 2 − 15 , 2 3 ] [2^{-15},2^{3}] [215,23] [ 2 − 5 , 2 15 ] [2^{-5},2^{15}] [25,215],指数间隔为2。先进行粗略查找,是为了减少计算,因为我们对每一个网格的参数都需要建立一个svm模型这个计算时间是比较长的,所以先进行粗略查找,再进行精确网格查找。accuracy矩阵是存放每个网格点的模型交叉验证的精度,此处精度以kappa系数度量,kappa2函数源自irr包,没有装的可以装一下,当然你也可以用整体精度等参数度量,取决于你的需求。

> str(folds)
List of 10
 $ Fold01: int [1:50] 8 9 11 29 74 86 95 96 105 121 ...
 $ Fold02: int [1:52] 2 25 33 46 68 82 89 91 92 93 ...
 $ Fold03: int [1:51] 1 3 10 31 32 44 47 54 58 67 ...
 $ Fold04: int [1:52] 4 7 19 26 34 40 41 45 60 79 ...
 $ Fold05: int [1:51] 16 23 48 52 55 57 77 112 117 120 ...
 $ Fold06: int [1:51] 35 36 51 63 69 104 118 130 147 155 ...
 $ Fold07: int [1:51] 30 38 53 65 70 76 87 88 97 103 ...
 $ Fold08: int [1:52] 5 12 27 37 66 81 83 90 98 109 ...
 $ Fold09: int [1:51] 13 15 21 22 24 56 61 62 72 73 ...
 $ Fold10: int [1:51] 6 14 17 18 20 28 39 42 43 49 ...

接下来就进行模型的建立:

>   for(i in 1:length(C_crude)){
+     C<-C_crude[i];
+     for(j in 1:length(gamma_crude)){
+        gamma<-gamma_crude[j];
+   
+       
+       cv_result<-lapply(folds,function(x){
+         xk_test<-x_train[x,]
+         yk_actual<-y_train[x]  #取一折做验证
+         xk_train<-x_train[-x,]
+         yk_train<-y_train[-x] #剩下九折训练
+         model<-svm(xk_train, yk_train, scale=FALSE, kernel='radial', gamma=gamma, cost= C);
+         yk_pred <- predict(model, xk_test);
+         
+         kappa<-kappa2(data.frame(yk_actual,yk_pred))$value
+         return(kappa);
+         
+       })
+         accuracy_crude[i,j]=mean(unlist(cv_result));#将精度存在一个矩阵里
+     }
+   }

建立完毕,可以看看结果,打开精度矩阵查看:
在这里插入图片描述

可以发现最高精度在网格中的位置,于是我们就找到精密网格精度的初步范围,这个范围是gamma在 [ 2 − 10 , 2 − 8 ] [2^{-10},2^{-8}] [210,28],cost在 [ 2 6 , 2 8 ] [2^{6},2^{8}] [26,28],得到这个结果的代码如下:

> temp<-which(accuracy_crude==max(accuracy_crude));
> col<-ceiling(temp/nrow(accuracy_crude));
> row<-temp-(col-1)*nrow(accuracy_crude);
> res_gamma<-gamma_crude[col];
> res_cost<-C_crude[row];
> num1<-log10(res_gamma/2)
> num2<-log10(res_cost/2)

2、精密网格查找与tune函数的使用
我们以0.25为指数的间隔建立精密的网格:

> gamma_fine<-2*10^seq((num1-1),num1+1,0.25)
> cost_fine<-2*10^seq((num2-1),num2+1,0.25)

其实交叉验证网格搜索不需要之前那么多行代码,因为e1071包中提供了一个自动参数网格查找的函数tune,还可以选择交叉验证的方法,相比之下使用tune()函数达到上面同样的效果只需要 一行代码。之前粗略查找网格是为了更好的让大家理解。

tune函数的语法如下:

tune(method, train.x, train.y = NULL, data = list(), validation.x =
NULL, validation.y = NULL, ranges = NULL, predict.func = predict,
tunecontrol = tune.control(), ...)

method是指运用的核函数方法,如果是径向基核函数,直接输入svm即可,参数x.train可以输入表达式也可以输入x数据,当x.train为表达式时,y.train可以忽略,另外表达式使用的是列名,所以data一定要指明是哪个变量,否则忽略dataranges是参数检验的范围,tunecontrol是根据tune.control函数生成的内部参数,他可以控制计算方法是bootstrap还是crossvalidation,还可以控制交叉验证的折数等参数。如,我们可以输入:

obj <- tune(svm, Species~., data = iris,
ranges = list(gamma = 2^(-1:1), cost = 2^(2:4)),
tunecontrol = tune.control(sampling = "fix")
)

此处我使用了另一种比较好理解的形式,未使用tunecontrol就是默认设置:

> obj <- tune.svm(x=x_train,y=y_train, gamma = gamma_fine, cost = cost_fine)

tune()的返回值是一个list,其中检验出来最好的参数被保存在了best.parameters中list中的每个值可以参考e1071的说明文档
3、运用最合适的参数到训练集

> model_best<-svm(x_train, y_train, scale=FALSE, kernel='radial', gamma=obj$best.parameters$gamma,
                cost= obj$best.parameters$cost);

4、模型检验

> y_pred <- predict(model_best, x_test);
> result<-confusionMatrix(y_test,y_pred,positive = '4')
> result
Confusion Matrix and Statistics

          Reference
Prediction   2   4
         2 114   2
         4   1  54
                                          
               Accuracy : 0.9825          
                 95% CI : (0.9496, 0.9964)
    No Information Rate : 0.6725          
    P-Value [Acc > NIR] : <2e-16          
                                          
                  Kappa : 0.96            
                                          
 Mcnemar's Test P-Value : 1               
                                          
            Sensitivity : 0.9643          
            Specificity : 0.9913          
         Pos Pred Value : 0.9818          
         Neg Pred Value : 0.9828          
             Prevalence : 0.3275          
         Detection Rate : 0.3158          
   Detection Prevalence : 0.3216          
      Balanced Accuracy : 0.9778          
                                          
       'Positive' Class : 4          

可以发现我们的整体精度从0.91到了0.98,kappa系数也从粗略方法的0.82提升到了0.96。证明我们运用交叉验证和网格搜索的方法最终使模型的精度得到了提高。
所有代码整合在一起,供大家参考:

library('e1071');
library('caret');
library('irr');
setwd('C:\\Users\\lenovo\\Desktop');
b<-read.table('breast cancer_origin.txt')
#--------------处理数据-------------------
a<-matrix(0,nrow(b),ncol(b))
for (i in 1:nrow(b)){
  for (j in 1:ncol(b)){
    a[i,j]<-as.numeric(sub('.?.:','',b[i,j]))
  }
}
#--------------标准化--------------------
x<-scale(a[,2:ncol(a)]);
y<-as.factor(a[,1]);
#--------------分离训练集和测试集----------------
idx<-sample(nrow(x),nrow(x)*0.75);
x_train<-x[idx,];
y_train<-y[idx];
x_test<-x[-idx,];
y_test<-y[-idx];
#--------------初步方案的演示---------------------
model<-svm(x_train, y_train, scale=FALSE, kernel='radial', gamma=2, cost= 0.2);
y_pred <- predict(model, x_test);
confusionMatrix(y_pred,y_test,positive = '4')

#--------------在训练集模型建立10折交叉验证------------------
folds<-createFolds(y_train,k=10);
comp1<-seq(-5,15,2);
comp2<-seq(-15,3,2);
C_crude<-2*10^comp1;
gamma_crude<-2*10^comp2;
accuracy_crude<-matrix(0,length(comp1),length(comp2)); 
#--------------展开粗略格网搜索-----------------------

  for(i in 1:length(C_crude)){
    C<-C_crude[i];
    for(j in 1:length(gamma_crude)){
       gamma<-gamma_crude[j];
  
      
      cv_result<-lapply(folds,function(x){
        xk_test<-x_train[x,]
        yk_actual<-y_train[x]  #取一折做验证
        xk_train<-x_train[-x,]
        yk_train<-y_train[-x] #剩下九折训练
        model<-svm(xk_train, yk_train, scale=FALSE, kernel='radial', gamma=gamma, cost= C);
        yk_pred <- predict(model, xk_test);
        
        kappa<-kappa2(data.frame(yk_actual,yk_pred))$value
        return(kappa);
        
      })
        accuracy_crude[i,j]=mean(unlist(cv_result));#将精度存在一个矩阵里
    }
  }
#--------------找出精度最高的参数-------
temp<-which(accuracy_crude==max(accuracy_crude));
col<-ceiling(temp/nrow(accuracy_crude));
row<-temp-(col-1)*nrow(accuracy_crude);
res_gamma<-gamma_crude[col];
res_cost<-C_crude[row];
num1<-log10(res_gamma/2)
num2<-log10(res_cost/2)
#--------------展开精密格网搜索-----------------------
#精密网格搜索将会用e1071包中的tune函数展开
gamma_fine<-2*10^seq((num1-1),num1+1,0.25)
cost_fine<-2*10^seq((num2-1),num2+1,0.25)
obj <- tune.svm(x=x_train,y=y_train, gamma = gamma_fine, cost = cost_fine)
#--------------运用测试集对模型进行检验--------------------------------
model_best<-svm(x_train, y_train, scale=FALSE, kernel='radial', gamma=obj$best.parameters$gamma,
                cost= obj$best.parameters$cost);
y_pred <- predict(model_best, x_test);
result<-confusionMatrix(y_test,y_pred,positive = '4')





结尾

此文是我学习时的笔记,谬误甚多还恳请大家指出,大家一起学习,如果有英文阅读水平的强烈推荐看libsvm guide,在参考文献的网站中有列出。
感谢阅读!
👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊👊

  • 28
    点赞
  • 139
    收藏
    觉得还不错? 一键收藏
  • 20
    评论
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值