adaboost-笔记(1)

1 - 加法模型

加法模型,就是通过训练集不断的得到不同的分类器(回归),然后将这些分类器组合成一个新的分类器的过程。

假设有\(N\)个样本,且我们的加法模型如下:
\[f(x)=\sum_{m=1}^M\beta_mb(x;\cal Y_m)\]
其中\(x\)为自变量,即样本;\(\cal Y_m\)为第\(m\)个分类器的参数;\(b(x;\cal Y_m)\)为分类器,也就是基函数;\(\beta_m\)为该分类器的系数。

可以看出,在给定这样一个加法模型的思路以及固定的训练集后,我们要做的就是最小化该模型的损失,即:
\[\min_{\beta_m,\cal Y_m}\sum_{i=1}^N\left(y_i,\sum_{m=1}^M\beta_mb(x_i;\cal Y_m)\right)\]
对于这样一个模型,训练的思路是:因为是个加法模型,所以可以从前向后,每一步只学习一个分类器(即基函数)和系数,让它逐步的逼近目标函数,直到达到我们能够接受的误差为止。该方法也叫做前向分步算法。

前向分布算法过程如下
假设目前训练集\(T={(x_1,y_1),(x_2,y_2),...(x_N,y_N)}\);损失函数\(L(y,f(x))\);分类器\({b(x;\cal Y)}\)。目标是得到最后的加法模型\(f(x)\)

1)得到第一个分类器\(f_0(x)=0\)
2)对于\(m=1,2,...M\)

i)如数学归纳法一样,假设目前已经得到了前\(m-1\)个分类器:
\[f(x)=f_{m-1}(x)=\sum_i^{m-1}\beta_ib(x;\cal Y_i)\]
那么,对于第\(m\)个分类器的参数及系数求法:
\[(\beta_m,\cal Y_m)=arg\min_{\beta,\cal Y}\sum_{i=1}^NL\left(y_i,f_{m-1}(x_i)+\beta b(x_i;\cal Y)\right)\]
这样就得到了第\(m\)个分类器的参数和系数;
ii)然后更新旧加法模型,得到新的加法模型:
\[f(x)=f_M(x)=\sum_{m=1}^M\beta_mb(x;\cal Y_m)\]
这样,就是将整个模型转换成了一步求得一个模型的参数和系数的优化问题上。

2 - adaboost

adaboost是加法模型的一个特例。假设目前有训练集\(T=\{(x_1,y_1),(x_2,y_2),...,(x_N,y_N)\}\), 其中样本都为\(n\)维向量\(x_i\in\bf R^n\), 标签\(y_i\in\{-1,+1\}\)

1)先设定初始时的训练样本的权值:
\[D_1 = (w_{11},...,w_{1i},...,w_{1N}),w_{1i}=\frac{1}{N},i=1,2,...N\]

2)对\(m=1,2,...,M\),即有\(M\)个分类器,则:

i)使用具有权值分布\(D_m\)的训练集学习,得到第\(m\)个基本分类器:
\[G_m(x):\rightarrow\{-1,+1\}\]

ii)然后计算基于当前分类器\(G_m(x)\)在训练集上的分类误差率:
\[e_m= P(G_m(x_i)\neq y_i)=\sum_{i=1}^Nw_{mi}I(G_m(x_i)\neq y_i)\]
ps:当然是选择当前权值基础上分类误差率最低的那个分类器作为当前分类器。所以其实是通过ii)来确定i)的分类器

iii)计算该分类器\(G_m(x)\)的系数:
\[\alpha_m=\frac{1}{2}log\frac{1-e_m}{e_m}\]
这里对数是自然对数。

ps:可以发现,当\(e_m\leq\frac{1}{2}\)时,\(\alpha_m\geq0\),并且\(\alpha_m\)随着\(e_m\)的减小而增大,也就是分类误差率越小的基本分类器在最终分类器中作用越大。

iv)更新训练集的权值为下一个分类器做准备:

\(D_{m+1}=(w_{m+1,1},...,w_{m+1,i},...,w_{m+1,N})\)

\(w_{m+1,i}=\frac{w_mi}{Z_m}exp(-\alpha_my_iG_m(x_i)),i=1,2,...N\)

这里 \(Z_m\)是归一化因子:
\[Z_m=\sum_{i=1}^Nw_{mi}exp(-\alpha_my_iG_m(x_i))\]

ps:权值更新的式子可以写成如下形式:
\[w_{m+1,i}= \begin{cases} \frac{w_{mi}}{Z_m}e^{-\alpha_m},& G_m(x_i)=y_i\\ \frac{w_{mi}}{Z_m}e^{\alpha_m},& G_m(x_i)\neq y_i \end{cases}\]
可以看出,当样本分类正确时,其权值在变小,而当样本分类错误时,其权值被放大,由此,误分类样本在下一轮学习中会起更大作用,也就是下一个分类器会更关注那些误分类的样本。

3)构建基本分类器的线性组合:
\[f(x)=\sum_{m=1}^M\alpha_mG_m(x)\]
得到最终分类器:
\[G(x)=sign(f(x))=sign\left(\sum_{m=1}^M\alpha_mG_m(x)\right)\]

2.1 这里举个例子

假设分类器是一个阈值分类器,即\(x>v,y=1\)\(x<v,y=-1\),训练集为简单的一维数据:

441382-20181011161729867-195744771.png
图2.1 李航书上表8.1

1)最开始每个样本的权重为相等,即:
\(D_1=(w_{11},w_{12},...w_{110})\)

\(w_{1i}=0.1,i=1,2,...,10\)

2)然后计算基于 \(v=\{1.5,2.5,3.5,4.5,5.5,6.5,7.5,8.5,9.5\}\)一共9个可选划分点基础上误差分类率最小的分类器:
\(e_{0.5}=0.5,错分类样本\{2,3,7,8,9\}\)
\(e_{1.5}=0.4,错分类样本\{3,7,8,9\}\)
\(e_{2.5}=0.3,错分类样本\{7,8,9\}\)
\(e_{3.5}=0.4,错分类样本\{4,7,8,9\}\)
\(e_{4.5}=0.5,错分类样本\{4,5,7,8,9\}\)
\(e_{5.5}=0.6,错分类样本\{4,5,6,7,8,9\}\)
\(e_{6.5}=0.5,错分类样本\{4,5,6,8,9\}\)
\(e_{7.5}=0.4,错分类样本\{4,5,6,9\}\)
\(e_{8.5}=0.3,错分类样本\{4,5,6\}\)
d1 = [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]
x = [0,1,2,3,4,5,6,7,8,9]
y = [1,1,1,-1,-1,-1,1,1,1,-1]

'''g1分类器,这里是划分点为2.5'''
def g1(x): return 1 if x < 2.5 else -1

y_prd = [g1(i) for i in x] # [1, 1, 1, -1, -1, -1, -1, -1, -1, -1]
e = sum([d1i*(0 if y1 == y2 else 1) for y1,y2,d1i in zip(y,y_prd,d1)] )  # 0.3

i)因为当2.5时,错误率最低,则第一个分类器:
\[G_1(x)=\begin{cases} \quad1,&x<2.5\\ -1,&x>2.5 \end{cases}\]
ii)当前误差率为0.3
iii)计算第一个分类器系数:
\[\alpha_1=\frac{1}{2}log\frac{1-e_1}{e_1}=0.4236\]
iv)更新训练集的权值分布:
\(D_2=(w_{21},...,w_{2i},...w_{210})\)
\(w_{2i}=\frac{w_{1i}}{Z_1}exp(-\alpha_iy_iG_1(x_i)),i=1,2,...10\)
\(D_2=(0.0715,0.0715,0.0715,0.0715,0.0715,0.0715,0.1666,0.1666,0.1666,0.0715)\)

d1 = [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]
y = [1,1,1,-1,-1,-1,1,1,1,-1]
z = []

'''g1分类器,这里是划分点为2.5'''
def g1(x):   return 1 if x < 2.5 else -1

for ind,(d1Item,yItem) in enumerate(zip(d1,y)):
    w = d1Item*np.exp(-0.4236*yItem*g1(ind))
    z.append(w)

d2 = [ zItem/sum(z) for zItem in z ]

\(f_1(x)=0.4236G_1(x)\)
此时分类器\(sign[f_1(x)]\)当前误分类3个\(\{7,8,9\}\)

d1 = [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]
x = [0,1,2,3,4,5,6,7,8,9]
y = [1,1,1,-1,-1,-1,1,1,1,-1]

'''g1分类器,这里是划分点为2.5'''
def g1(x): return 1 if x < 2.5 else -1
def f1(x): return 1 if 0.4236*g1(x) >0 else -1

y_prd = [ f1(i)  for i in x] # [1, 1, 1, -1, -1, -1, -1, -1, -1, -1]

3)接着处理\(m=2\)时。
同上述方法计算每个阈值\(v=\{1.5,2.5,3.5,4.5,5.5,6.5,7.5,8.5,9.5\}\)下分类误差率
\(e_{0.5}=0.6428,错分类样本\{2,3,7,8,9\}\)
\(e_{1.5}=0.5713,错分类样本\{3,7,8,9\}\)
\(e_{2.5}=0.4998,错分类样本\{7,8,9\}\)
\(e_{3.5}=0.5713,错分类样本\{4,7,8,9\}\)
\(e_{4.5}=0.6428,错分类样本\{4,5,7,8,9\}\)
\(e_{5.5}=0.7143,错分类样本\{4,5,6,7,8,9\}\)
\(e_{6.5}=0.5477,错分类样本\{4,5,6,8,9\}\)
\(e_{7.5}=0.3811,错分类样本\{4,5,6,9\}\)
\(e_{8.5}=0.2145,错分类样本\{4,5,6\}\)

最低的分类器:
\[G_2(x)=\begin{cases} \quad1,&x<8.5\\ -1,&x>8.5 \end{cases}\]
i)此时误差率为0.2145

# 可以看出当前样本的权重值,其中经过第一次分类之后错误分类的权重变大
d2 = [0.0715,0.0715,0.0715,0.0715,0.0715,0.0715,0.1666,0.1666,0.1666,0.0715]

x = [0,1,2,3,4,5,6,7,8,9]
y = [1,1,1,-1,-1,-1,1,1,1,-1]

'''g2分类器,这里是划分点为8.5'''
def g2(x): return 1 if x < 8.5 else -1

y_prd = [ g2(i) for i in x] # [1, 1, 1, -1, -1, -1, -1, -1, -1, -1]
# 计算预测值与实际值不相等的,即为误分类,将其乘以样本权重,得到误差率
e = sum([d1i*(0 if y1 == y2 else 1) for y1,y2,d1i in zip(y,y_prd,d2)] )  # 0.2145

ii)系数为0.6496
iii)训练集权值分布:
\(D_3=\{0.0455,0.0455,0.0455,0.1667,0.1667,0.1667,0.1060,0.1060,0.1060,0.0455\}\)
\(f_2(x)=0.4236G_1(x)+0.6496G_2(x)\)
iv)此时分类器\(sign[f_2(x)]\)训练集上错分类样本还是3个\(\{4,5,6\}\)

x = [0,1,2,3,4,5,6,7,8,9]
y = [1,1,1,-1,-1,-1,1,1,1,-1]
d3 = [0.0455,0.0455,0.0455,0.1667,0.1667,0.1667,0.1060,0.1060,0.1060,0.0455]

def g1(x): return 1 if x<2.5 else -1
def g2(x): return 1 if x<8.5 else -1

def f2(x): return 1 if (0.4236*g1(x) + 0.6496*g2(x)) > 0 else -1

y_prd = [ f2(i)  for i in x] # [1, 1, 1, 1, 1, 1, 1, 1, 1, -1]
isEqual = [1 if ypred == ygt else 0 for ypred,ygt in zip(y_prd, y)] # [1, 1, 1, 0, 0, 0, 1, 1, 1, 1]

4)如上述计算过程,第三轮得到的分类器:
\[G_3(x)=\begin{cases} \quad -1,&x<5.5\\ 1,&x>5.5 \end{cases}\]
i)此时误差率为0.1820

d3 = [0.0455,0.0455,0.0455,0.1667,0.1667,0.1667,0.1060,0.1060,0.1060,0.0455]
x = [0,1,2,3,4,5,6,7,8,9]
y = [1,1,1,-1,-1,-1,1,1,1,-1]

'''g3分类器,这里是划分点为5.5'''
def g3(x): return -1 if x < 5.5 else 1

y_prd = [ g3(i) for i in x] # [-1, -1, -1, -1, -1, -1, 1, 1, 1, 1]
# 计算预测值与实际值不相等的,即为误分类,将其乘以样本权重,得到误差率
e = sum([d1i*(0 if y1 == y2 else 1) for y1,y2,d1i in zip(y,y_prd,d3)] )  # 0.182

ii)系数为0.6496
iii)训练集权值分布:
\(D_4=\{0.125,0.125,0.125,0.102,0.102,0.102,0.065,0.065,0.065,0.125\}\)
\[f_3(x)=0.4236G_1(x)+0.6496G_2(x)+0.7514G_3(x)\]

x = [0,1,2,3,4,5,6,7,8,9]
y = [1,1,1,-1,-1,-1,1,1,1,-1]

def g1(x): return 1 if x<2.5 else -1
def g2(x): return 1 if x<8.5 else -1
def g3(x): return -1 if x<5.5 else 1

def f3(x): return 1 if (0.4236*g1(x) + 0.6496*g2(x) + 0.7514*g3(x)) > 0 else -1

y_prd = [ f3(i)  for i in x] # [1, 1, 1, -1, -1, -1, 1, 1, 1, -1]
isEqual = [1 if ypred == ygt else 0 for ypred,ygt in zip(y_prd, y)] # [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

4)如上述计算过程,第三轮得到的分类器:
此时误分类样本为0个,所以无需训练所谓第四个基分类器。
最终分类器为:
\[G(x)=sign[f_3(x)]=sign[0.4236G_1(x)+0.6496G_2(x)+0.7514G_3(x)]\]

3 - boosting tree

提升树是以回归树或者分类树作为基本分类器的模型,被认为统计学习中性能最好的方法之一(当然还有svm了)。
该类方法是采用了加法模型(基函数的线性组合)和前向分布算法,用决策树为基函数的提升方法叫提升树,分类问题 时,可采用二叉分类树,回归问题时,可采用二叉回归树。提升树模型可以表示为决策树的加法模型:
\[f_M(x)=\sum_{m=1}^MT(x;\Theta_m)\]
其中,\(T(x;\Theta_m)\)表示一棵决策树;\(\Theta_m\)表示第\(m\)棵决策树的参数;\(M\)为树的个数。
对于整个过程,如上面的加法模型一样,先确定初始提升树\(f_0(x)=0\),然后第\(m\)轮之后,整个的模型:
\[f_m(x)=f_{m-1}(x)+T(x;\Theta_m)\]
其中\(f_{m-1}(x)\)是当前求得的模型,然后通过经验风险最小化确定下一棵决策树的参数\(\Theta_m\)
\[\hat \Theta_m=arg\min_{\Theta_m}\sum_{i=1}^NL(y_i,f_{m-1}(x_I)+T(x_i;\theta_m))\]
ps:因为树的线性组合可以很好的拟合训练数据,即使数据中的输入与输出的关系很复杂也如此。所以提升树是一个高功能的学习算法

通常来说,不同的提升树学习算法,主要区别在于使用的损失函数:1)平方误差损失函数的回归问题;2)指数损失函数的分类问题;3)一般损失函数的一般决策问题。
对于二类分类问题,即将adaboost中的基本分类器限制为二类分类树(即树桩:一个根节点,两个叶子节点)。这里主要介绍回归问题的提升树。

假设训练集\(T=\{(x_1,y_1),(x_2,y_2),...,(x_N,y_N)\}\),且\(x_i\in \cal X \subseteq \bf R^n\),\(y_i\in \cal Y \subseteq \bf R\).对于一棵回归树,如果将输入空间划分成\(J\)个互不相交的区域\(R_1,R_2,...R_J\)并且在每个区域上确定输出常量\(c_j\),那么树可表示为:
\[T(x:\Theta)=\sum_{j=1}^Jc_jI(x\in R_j)\]
其中参数\(\Theta=\{(R_1,c_1),(R_2,c_2),...,(R_J,c_J)\}\)表示树的区域划分和各个区域上的常量输出\(J\)表示回归树的复杂度,即最终叶节点个数。

前向分步步骤
假设训练集\(T=\{(x_1,y_1),(x_2,y_2),...,(x_N,y_N)\}\),且\(x_i\in \cal X \subseteq \bf R^n\),\(y_i\in \cal Y \subseteq \bf R\)

1)初始化\(f_0(x)=0\)

2)对于轮数\(m=1,2,...,M\)

i)当第\(m\)步时,已知前\(m-1\)轮结果,需要求解:
\[\hat\Theta_m=arg\min_{\Theta_m}\sum_{i=1}^NL(y_i,f_{m-1}(x_i)+T(x_i;\Theta_m))\]
得到的\(\hat\Theta_m\)即为第\(m\)棵树的参数。当采用平方误差损失函数时,:
\[L(y,f(x))=(y-f(x))^2\]
代入得:
\[\begin{eqnarray} L(y,f_{m-1}(x)+T(x:\Theta_m)) &=&[y-f_{m-1}(x)-T(x;\Theta_m)]^2\\ &=&[r-T(x;\Theta_m)]^2 \end{eqnarray}\]
其中\(r=y-f_{m-1}(x)\)是前\(m-1\)轮后得到的模型拟合数据的残差,所以对于回归问题的提升树来说,就是拟合当前模型的残差。
ii)对于每个样本\(i=1,...N\)
求得他们的残差:
\[r_{mi}=y_i-f{m-1}(x_i),i=1,2,...N\]
iii)拟合这轮得到的残差\(r_{mi}\),基于整个训练集学习一棵新的回归树,得到\(T(x;\Theta_m)\)

iv)更新\(f_m(x)=f_{m-1}(x)+T(x; \Theta_m)\)

3)得到最后结果模型:
\[f_M(x)=\sum_{m=1}^MT(x;\Theta_m)\]

3.1 回归提升树例子

如下表数据:

441382-20181011161758946-65379539.png

图3.1.1 李航书上表8.2

\(x\)取值范围[0.5,10.5], \(y\)取值范围为[5.0,10.0]。为了简单起见,这里只采用树桩来生成每轮的回归树,且输入空间只划分成2个区域 \(\{R_1,R_2\}\)
这里略过了 \(f_0(x)\).直接生成 \(f_1(x)\):
通过优化下面的问题:
\[\min_{s}\left[\min_{c_1}\sum_{x_i\in R_1}(y_i-c_1)^2+\min_{x_i\in R_2}(y_i-c_2)^2\right]\tag{3.1.1}\]
来求解切分点 \(s\),使得:
\(R_1=\{x|x\leq s\}\), \(R_2=\{x|x>s\}\)
从而计算在当前切分点基础上,在 \(R_1\), \(R_2\)使平方损失误差达到最小值的 \(c_1\), \(c_2\):
\[c_1 = \frac{1}{N_1} \sum_{x_i\in R_1}y_i, \quad c_2 = \frac{1}{N_2} \sum_{x_i\in R_2}y_i\tag{3.1.2}\]
1)上述训练集样本的切分点有 \(\{1.5,2.5,3.5,4.5,5.5,6.5,7.5,8.5,9.5\}\)
对应的最小误差值如下:
441382-20181011161814025-1618615958.png

k发现当 \(s=6.5\)\(m(s)\)达到最小值。此时 \(R_1=\{1,2,...,6\}\); \(R_2=\{7,8,9,10\}\). \(c_1=6.24;c_2=8.91\)。所以回归树 \(T_1(x)\)为:
\[T_1(x)=\begin{cases} 6.24,& x<6.5\\ 8.91,&x\geq6.5 \end{cases}\]
从而 \(f_1(x)=T_1(x)\)
X = [1,2,3,4,5,6,7,8,9,10]
Y0 = [5.56, 5.70, 5.91, 6.40, 6.80, 7.05, 8.90, 8.70, 9.00, 9.05]
X_splits = [1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5]

def cost_split_point(ind,xs,YT):   
    R1 = YT[:ind+1]
    R2 = YT[ind+1:]
    c1 = round(mean(R1),3)
    c2 = round(mean(R2),3)
    
    ans0 = [(x-c1)*(x-c1) for x in R1]
    ans1 = [(x-c2)*(x-c2) for x in R2]
    
    return [sum(ans0+ans1), c1, c2]
    
cost = [[xs]+cost_split_point(ind,xs,Y0) for ind,xs in enumerate(X_splits)]
#[[1.5, 15.723, 5.559, 7.501],
# [2.5, 12.083, 5.629, 7.726],
# [3.5, 8.365, 5.723, 7.985],
# [4.5, 5.775, 5.892, 8.25],
# [5.5, 3.911, 6.073, 8.540],
# [6.5, 1.930, 6.236, 8.912],
# [7.5, 8.009, 6.617, 8.916],
# [8.5, 11.735, 6.877, 9.025],
# [9.5, 15.738, 7.113, 9.050]]

2)计算\(f_1(x)\)拟合训练集的残差计算公式\(r_{2i}=y_i-f_1(x_i),i=1,2,...,10\)

441382-20181011161832440-612752789.png
图3.1.3

\(f_1(x)\)拟合训练集后的平方损失为:
\[L(y,f_1(x))=\sum_{i=1}^{10}(y_i-f_1(x_i))^2=1.93\]
r_2i = [-0.68, -0.54, -0.33, -0.16, 0.56, 0.81, -0.01, -0.21, 0.09, 0.14]
r_2i = np.array(r_2i)
loss = (r_2i*r_2i).sum()#1.9301

3)在计算新的回归树时,选择的训练集是图3.1.3 而不是原来的3.1.1,即是基于上一轮模型结果之后的残差。从而得到这一轮的回归树:
\[T_2(x)=\begin{cases} -0.52,& x<3.5\\ 0.22,&x\geq3.5 \end{cases}\]

Y1 = [-0.68, -0.54, -0.33, 0.16, 0.56, 0.81, -0.01, -0.21, 0.09, 0.14]
cost = [[xs]+cost_split_point(ind,xs,Y1) for ind,xs in enumerate(X_splits)]
#[[1.5, 1.417, -0.680, 0.073],
# [2.5, 1.002, -0.609, 0.151],
# [3.5, 0.790, -0.517, 0.22],
# [4.5, 1.129, -0.347, 0.230],
# [5.5, 1.657, -0.166, 0.164],
# [6.5, 1.930, -0.003, 0.003],
# [7.5, 1.929, -0.004, 0.007],
# [8.5, 1.896, -0.029, 0.115],
# [9.5, 1.908, -0.017, 0.140]]

在得到这一轮的回归树之后,将前面得到的模型与当前模型相加,且重合区域需要注意相加减,如当\(x<3.5\)时,\(f_2(x)=-0.52+6.24=5.72\)
\[f_2(x)=f_1(x)+T_2(x)=\begin{cases} 5.72,& x<3.5\\ 6.46,& 3.5\leq x<6.5\\ 9.13,& x\geq 6.5 \end{cases}\]
\(f_2(x)\)拟合训练集损失为\(L(y,f_2(x))=\sum_{i=1}^{10}(y_i-f_2(x_i))^2=0.79\)

如此得到最后结果:
\[f_6(x)=f_5(x)+T_6(x)=\begin{cases} 5.63,& x<2.5\\ 5.82,& 2.5\leq x<3.5\\ 6.56,& 3.5\leq x<4.5\\ 6.83,& 4.5\leq x <6.5\\ 8.95,&x \geq6.5 \end{cases}\]
当前模型拟合训练集平方损失为0.17.假设此时误差已经满足要求,那么就可以将\(f_6(x)\)作为整个训练集的提升树

3.2 gradient boosting tree

当提升树中选取的损失函数是平方损失和指数损失函数时,训练过程如上述所属,可是当一般损失函数而言,优化相对就较为麻烦了,这时候可以用freidma提出的gradient boosting算法,这是与最速下降法相似的方法,使用了损失函数的负梯度:
\[r_{mi}=-\left[\frac{\rm dL(y,f(x_i))}{\rm df(x_i)}\right]_{f(x)=f_{m-1}(x)}\]
将其作为3.1中每一轮计算的平方损失残差值,即这一轮所谓的"训练集"
其他部分的计算不变。

参考资料:
[] 李航,统计学习方法

2017/04/07 第一次修改!

转载于:https://www.cnblogs.com/shouhuxianjian/p/9773174.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值