Ensembling, Lagrange

Ensembling, Lagrange

  据我观察,名字带“拉”的人一般都很厉害,比如马拉多纳、希拉里、狄波拉、张娜拉、杜拉拉、陈法拉、尼古拉斯·赵四、地铁站的罗拉……

  但跟我们今天的大神拉格朗日(Lagrange)一比就都被秒成了渣渣,因为每一个上过高数和物理的我们都被拉大神无情碾压摩擦过啊~

  童鞋,请站起来回答拉格朗日中值定律是啥?

  童鞋,请站起来回答拉格朗日乘子法是啥?

  童鞋,请站起来回答拉格朗日内插公式是啥?

  童鞋,请站起来回答拉格朗日点是啥?

  童鞋,请站起来回答拉格朗日函数是啥?

  童鞋,请站起来回答拉格朗日方程是啥?

  ……

  童鞋,你肿么站不起来了童鞋?


  请原谅我召唤出封印在你们心底多年的梦魇,快跟我一起踏碎时空大逃亡,继续闪回到小米数据挖掘大赛那几天。

  上文中已经提到我们最终的模型不止一个,而是多个DNN和FM共七八个模型的融合。

  所谓模型融合,洋气一点叫Ensembling,在 http://mlwave.com/kaggle-ensembling-guide/ 一文中有详细介绍。简单来说就是综合多个单模型的输出来给出最终的结果,一般会比每个单模型的效果都好,现在已经是各大比赛的常规武器。但对于初涉江湖的小伙伴们还没有太多经验,一开始只是简单的平均,后来尝试了各个模型的输出做为LR的特征,发现没啥卵用,比赛已近尾声就没再折腾更复杂的方法,最终使用线性加权平均,即:

y^=w1y^1+w2y^2++wMy^M y^=w1y^1+w2y^2+…+wMy^M

  其中, w1+w2++wM=1 w1+w2+…+wM=1

  这里有个问题就是权重系数怎么定?

  一开始就是拍脑袋定,单模型效果好的权重就大一点,效果差的就小一点。后来小伙伴使用暴力网格搜索的方法,融合两三个模型还行,再多就已经慢到不可接受。

  我看在眼里急在心头,我们是模武双修的种族啊,怎么能只用暴力解决呢?这种目标如此鲜明灿若煌煌皓月的好问题,当然要祭出流光华丽的大模型才相得益彰呢!

第一式:混沌初分模型现!

  比如我们有M个单模型分类器,解决K分类问题,测试集包含N条样本, y^nmk y^nmk 表示第m个单模型对第n条样本属于第k类的预测概率。

  我们采用线性加权平均的方法,融合M个模型得到的预测概率

y^nk=m=1M1wmy^nmk+(1m=1M1wm)y^nMk y^nk=∑m=1M−1wmy^nmk+(1−∑m=1M−1wm)y^nMk

  比赛评价标准为logloss,我们希望融合后的模型在测试集上的logloss尽量小,所以优化目标如下:
minwf(w)=minw{1Nn=1Nk=1K1{yn=k}lny^nk}=minw{1Nn=1Nk=1K1{yn=k}ln(m=1M1wmy^nmk+(1m=1M1wm)y^nMk)} minwf(w)=minw{−1N∑n=1N∑k=1K1{yn=k}ln⁡y^nk}=minw{−1N∑n=1N∑k=1K1{yn=k}ln⁡(∑m=1M−1wmy^nmk+(1−∑m=1M−1wm)y^nMk)}

  其中 w=[w1,..,wM1]T w=[w1,..,wM−1]T是需要求解的权重参数,我们知道权重之和要归一,所以不需要 wM wM这一变量,用 1M1m=1wm 1−∑m=1M−1wm代替即可。

  问题已明确,下面来求解。

第二式:梯度杀!!

  这一招早已驾轻就熟,直接求梯度,然后上sgd或adagrad。单条样本的损失函数为

ln(w)=k=1K1{yn=k}ln(m=1M1wmy^nmk+(1m=1M1wm)y^nMk) ln(w)=−∑k=1K1{yn=k}ln⁡(∑m=1M−1wmy^nmk+(1−∑m=1M−1wm)y^nMk)

  梯度为
lnwm=k=1K1{yn=k}y^nmky^nMky^nk,m=1,,M1 ∂ln∂wm=−∑k=1K1{yn=k}y^nmk−y^nMky^nk,m=1,…,M−1

  adagrad版本的代码如下,注意其中变量名和上面的公式并不完全一致,类别编号是从0到K-1(我就是这么洒脱随性~)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import sys, math

classNum = int(sys.argv[1])
modelNum = int(sys.argv[2])
alfa = float(sys.argv[3])
beta = 1.0

print >> sys.stderr, "classNum=" + str(classNum)
print >> sys.stderr, "modelNum=" + str(modelNum)
print >> sys.stderr, "alfa=" + str(alfa)

w = [1.0] * modelNum
n = [0.0] * (modelNum-1)
for i in range(len(w)):
w[i] /= modelNum

print >> sys.stderr, w

for line in sys.stdin:
seg = line.rstrip().split(" ")
label = int(seg[0])
x = []
for tmp in seg[1:]:
seg2 = tmp.split(",")
for i in range(len(seg2)):
seg2[i] = float(seg2[i])
x.append(seg2)
pre = [0.0] * classNum
for m in range(modelNum):
for k in range(classNum):
pre[k] += w[m]*x[m][k]
g = [0.0] * (modelNum-1)
wsum = 0.0
for m in range(modelNum-1):
g[m] = (x[modelNum-1][label]-x[m][label])/pre[label]
n[m] += g[m] * g[m]
lr = alfa/(beta+math.sqrt(n[m]))
w[m] -= lr * g[m]
wsum += w[m]
w[modelNum-1] = 1-wsum

for x in w:
print x

  输入数据的格式如下:

label score11,score12,...,score1K score21,score22,...,score2K ... scoreM1,scoreM2,...,scoreMK

  举个例子,比如二分类4个模型的数据片段如下:

0 0.912427,0.0875725 0.905673,0.0943273 0.911398,0.088602 0.933166,0.066834
0 0.86061,0.139389 0.865925,0.134075 0.881552,0.118448 0.872196,0.127804
1 0.364299,0.635701 0.32974,0.67026 0.323839,0.676161 0.341047,0.658953
0 0.715563,0.284437 0.713995,0.286005 0.713599,0.286401 0.713383,0.286617
0 0.948186,0.0518137 0.925587,0.0744127 0.929451,0.0705489 0.939952,0.0600485
0 0.58531,0.41469 0.588321,0.411679 0.520456,0.479544 0.671846,0.328154
1 0.0154455,0.984554 0.0150741,0.984926 0.0118866,0.988113 0.0109413,0.989059
1 0.0472074,0.952793 0.0612501,0.93875 0.065446,0.934554 0.061947,0.938053
1 0.430228,0.569772 0.366636,0.633364 0.337206,0.662794 0.5487,0.4513
0 0.97958,0.0204195 0.980348,0.0196517 0.979461,0.0205394 0.985608,0.0143915

  我们的测试数据共200多万条,时间复杂度跟单模型数M呈线性关系,跑一次也就分分钟的事。为了验证算法,做了对比,对于3模型融合跑出的结果和暴力求解的结果一致。

  问题似乎就这么完美解决了,但人生就像直流充电桩,上一秒还是70A的电流,下一秒就可能跳电。当我们融合更多模型时问题出现了,有的权重算出了负值!就像下面这样:

0.120181463777    gender_cut_ftrl8000_val
0.0929962960391   gender_cut_ftrl_val
0.0245135332374   gender_nn_1005_val
0.269705618425    gender_cut_ftrl2000_val
0.422565675064    gender_cut_ftrl1000_val
0.233316720486    gender_newFM_val
0.0228310140994   gender_xxFM_val
-0.186110321128   gender_result_fm

导致最终模型的预测概率有可能大于1或小于0,这如何能忍,必须再创新招!

第三式:生死符!!!

  “这生死符一发作,一日厉害一日,奇痒剧痛递加九九八十一日,然后逐步减退,八十一日之后,又再递增,如此周而复始,永无休止。每年我派人巡行各洞各岛,赐以镇痛止痒之药,这生死符一年之内便可不发。”

  好啦,不吓你啦,这是金庸老先生笔下天山童姥的生死符。我这里的生死符简单得很,顾名思义,由权重符号决定单模型生死,正的留,负的弃,留下的单模型重新计算权重,有可能又有新的负值出现,再次使用生死符,直到剩下的单模型权重全部大于等于0。

  还以上面那次融合为例,最终把gender_xxFM_val和gender_result_fm都干掉了,剩下的权重为:

0.122788741689    gender_cut_ftrl8000_val
0.0782434412719   gender_cut_ftrl_val
0.0331138038226   gender_nn_1005_val
0.24358375583     gender_cut_ftrl2000_val
0.446465338463    gender_cut_ftrl1000_val
0.0758049189234   gender_newFM_val

  在测试集上的logloss = 0.435116,而其中单模型最好的logloss是0.436593。

  相应地,这批融合在7分类问题age上效果更加明显,从最好的单模型1.35416降到1.35061。


  以上便是比赛期间我们使用的全部招式。








  憋走,故事还没结束,忘了塞纳河畔的拉格朗日了吗?

  像我这等追求卓越的好青年岂能够留下不完美的话柄,虽然比赛结束了,我还是要把它彻底解决掉——快使出

第四式:铁锁横江!!!!

  分析上面的模型,出现负值,是因为少了对w的非负约束,那就加上:

minwf(w)=minw{1Nn=1Nk=1K1{yn=k}lny^nk}=minw{1Nn=1Nk=1K1{yn=k}ln(m=1M1wmy^nmk+(1m=1M1wm)y^nMk)} minwf(w)=minw{−1N∑n=1N∑k=1K1{yn=k}ln⁡y^nk}=minw{−1N∑n=1N∑k=1K1{yn=k}ln⁡(∑m=1M−1wmy^nmk+(1−∑m=1M−1wm)y^nMk)}

s.t.wm0,m=1,2,,M11m=1M1wm0 s.t.wm≥0,m=1,2,…,M−11−∑m=1M−1wm≥0

  这就成了带约束的优化问题,该怎么解呢?是时候请出拉格朗日大神了!大神蜜汁微笑,抛出一套对偶宝典,将带约束的极小化问题改造成极大极小问题,正是

第五式:沧海一粟!!!!!

  我们先把问题标准化,假定 f(x) f(x) ci(x) ci(x) hj(x) hj(x) 是定义在 Rn Rn上的连续可微函数,将下面约束最优化问题

minxf(x)s.t.ci(x)0,i=1,2,,khj(x)=0,j=1,2,,l minxf(x)s.t.ci(x)≤0,i=1,2,…,khj(x)=0,j=1,2,…,l

称为原始问题。

  然后引入广义拉格朗日函数

L(x,α,β)=f(x)+i=1kαici(x)+j=1lβjhj(x) L(x,α,β)=f(x)+∑i=1kαici(x)+∑j=1lβjhj(x)

  其中  αi αi 和  βj βj 是拉格朗日乘子, αi0 αi≥0。然后经过一番推来导去眉来眼去,可以证明
minxmaxα,β:αi0L(x,α,β) minxmaxα,β:αi≥0L(x,α,β)

与原始问题具有相同的x最优解,称作广义拉格朗日函数的极小极大问题。

  又有,当 f(x) f(x) ci(x) ci(x) hj(x) hj(x) 等满足一定条件时

maxα,β:αi0minxL(x,α,β) maxα,β:αi≥0minxL(x,α,β)

也与原始问题具有相同的x最优解,称作广义拉格朗日函数的极大极小问题,可以将此问题也改写成约束的形式:
maxα,βθD(α,β)=maxα,βminxL(x,α,β)s.t.αi0,i=1,2,,k maxα,βθD(α,β)=maxα,βminxL(x,α,β)s.t.αi≥0,i=1,2,…,k

称作原始问题的对偶问题。

  这一部分的具体细节懒得写了,估计你们也看不下去,真要有兴趣可以去看书,比如《凸优化》或者李航老师的《统计学习方法》,我上面的公式基本就是抄他的,但请注意书上附录C的KKT条件有误,(C.22)式和(C.23)式不应该包含在里面。

  好啦,照此框架,来解决我们的问题:

maxα,αm0minwL(w,α)=maxα,αm0minw{1Nn=1Nk=1K1{yn=k}ln(m=1M1wmy^nmk+(1m=1M1wm)y^nMk)m=1M1αmwmαM(1m=1M1wm)} maxα,αm≥0minwL(w,α)=maxα,αm≥0minw{−1N∑n=1N∑k=1K1{yn=k}ln⁡(∑m=1M−1wmy^nmk+(1−∑m=1M−1wm)y^nMk)−∑m=1M−1αmwm−αM(1−∑m=1M−1wm)}

  极大极小公式列出来很容易,如何求解才头疼。

  最早接触拉格朗日对偶是看SVM的推导,最近研究在线分配的shale算法又遇到它,发现基本都是依靠KKT条件推导,然而后面各有各的玩法,没有什么普适的方案。我也试着从KKT出发,但始终没找到什么高效的方法,一赌气,干脆舍弃KKT直接求解极大极小问题。如果哪位高人有更高明的招式,请一定传授小弟。

  我自己想的招式便是以不变应万变,依然按sgd的思路,每来一条样本,先固定  α α 不动,更新w,这是关于w的极小化问题,使用梯度下降(依然梯度杀);然后固定w,更新  α α,这是关于  α α 的极大化问题,使用梯度上升(可称梯云纵)。对  α α的非负约束也很简单,更新后如果小于0,则置为0。

  如此交错曲折,在解空间的山坡上忽上忽下苦苦寻觅,“路漫漫其修远兮”,正是

第六式:上下求索!!!!!!

  具体的梯度计算如下:

ln(w|α)wm=k=1K1{yn=k}y^nmky^nMky^nkαm+αM,m=1,,M1ln(α|w)αm=wm,m=1,,M1ln(α|w)αM=(1m=1M1wm) ∂ln(w|α)∂wm=−∑k=1K1{yn=k}y^nmk−y^nMky^nk−αm+αM,m=1,…,M−1∂ln(α|w)∂αm=−wm,m=1,…,M−1∂ln(α|w)∂αM=−(1−∑m=1M−1wm)

  adagrad代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import sys, math

classNum = int(sys.argv[1])
modelNum = int(sys.argv[2])
alfa = float(sys.argv[3])
beta = 1.0

print >> sys.stderr, "classNum=" + str(classNum)
print >> sys.stderr, "modelNum=" + str(modelNum)
print >> sys.stderr, "alfa=" + str(alfa)

w = [1.0] * modelNum
n = [0.0] * (modelNum-1)
A = [0.0] * modelNum
nA = [0.0] * modelNum

for i in range(len(w)):
w[i] /= modelNum

print >> sys.stderr, w

for line in sys.stdin:
seg = line.rstrip().split(" ")
label = int(seg[0])
x = []
for tmp in seg[1:]:
seg2 = tmp.split(",")
for i in range(len(seg2)):
seg2[i] = float(seg2[i])
x.append(seg2)
pre = [0.0] * classNum
for m in range(modelNum):
for k in range(classNum):
pre[k] += w[m]*x[m][k]
g = [0.0] * (modelNum-1)
wsum = 0.0
for m in range(modelNum-1):
g[m] = (x[modelNum-1][label]-x[m][label])/pre[label] - A[m] + A[modelNum-1]
n[m] += g[m] * g[m]
lr = alfa/(beta+math.sqrt(n[m]))
w[m] -= lr * g[m]
wsum += w[m]
gA = -w[m]
nA[m] += gA * gA
lrA = alfa/(beta+math.sqrt(nA[m]))
A[m] += lrA * gA
if A[m] < 0:
A[m] = 0
w[modelNum-1] = 1-wsum
gA = -w[modelNum-1]
nA[modelNum-1] += gA * gA
lrA = alfa/(beta+math.sqrt(nA[modelNum-1]))
A[modelNum-1] += lrA * gA
if A[modelNum-1] < 0:
A[modelNum-1] = 0

for x in w:
print x

  看一下效果,还是以上面的gender二分类为例,结果如下:

0.123262252313    gender_cut_ftrl8000_val
0.0782768467103   gender_cut_ftrl_val
0.0340907260294   gender_nn_1005_val
0.243746308063    gender_cut_ftrl2000_val
0.447138729994    gender_cut_ftrl1000_val
0.0627800144873   gender_newFM_val
0.00261889445352  gender_xxFM_val
0.00808622794875  gender_result_fm

  可以看到gender_xxFM_val和gender_result_fm的权重终于不再是负数,而且很小,接近于0,跟期望的一样,最后的logloss也基本一致,等于0.435125。

  说明我们的方法很给力啊!不能骄傲,继续前行。上面的方法对最后一项权重 wM wM 做了特殊处理,不够优雅,如果我们不专门处理它,而是把权重之和归一放在约束里呢?

第七式:万法归一!!!!!!!

  重新设计模型

y^nk=m=1Mwmy^nmk y^nk=∑m=1Mwmy^nmk

minwf(w)=minw{1Nn=1Nk=1K1{yn=k}lny^nk}=minw{1Nn=1Nk=1K1{yn=k}ln(m=1Mwmy^nmk)}s.t.wm0,m=1,2,,Mm=1Mwm=1 minwf(w)=minw{−1N∑n=1N∑k=1K1{yn=k}ln⁡y^nk}=minw{−1N∑n=1N∑k=1K1{yn=k}ln⁡(∑m=1Mwmy^nmk)}s.t.wm≥0,m=1,2,…,M∑m=1Mwm=1

L(w,α,β)=1Nn=1Nk=1K1{yn=k}ln(m=1Mwmy^nmk)m=1Mαmwm+β(m=1Mwm1) L(w,α,β)=−1N∑n=1N∑k=1K1{yn=k}ln⁡(∑m=1Mwmy^nmk)−∑m=1Mαmwm+β(∑m=1Mwm−1)

  梯度计算
ln(w|α,β)wm=k=1K1{yn=k}y^nmky^nkαm+β,m=1,,Mln(α|w,β)αm=wm,m=1,,Mln(β|w,α)β=m=1Mwm1 ∂ln(w|α,β)∂wm=−∑k=1K1{yn=k}y^nmky^nk−αm+β,m=1,…,M∂ln(α|w,β)∂αm=−wm,m=1,…,M∂ln(β|w,α)∂β=∑m=1Mwm−1

  代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import sys, math

classNum = int(sys.argv[1])
modelNum = int(sys.argv[2])
alfa = float(sys.argv[3])
beta = 1.0

print >> sys.stderr, "classNum=" + str(classNum)
print >> sys.stderr, "modelNum=" + str(modelNum)
print >> sys.stderr, "alfa=" + str(alfa)

w = [1.0] * modelNum
nw = [0.0] * modelNum
A = [0.0] * modelNum
nA = [0.0] * modelNum
B = 0.0
nB = 0.0

for i in range(len(w)):
w[i] /= modelNum

print >> sys.stderr, w

for line in sys.stdin:
seg = line.rstrip().split(" ")
label = int(seg[0])
x = []
for tmp in seg[1:]:
seg2 = tmp.split(",")
for i in range(len(seg2)):
seg2[i] = float(seg2[i])
x.append(seg2)
pre = [0.0] * classNum
for m in range(modelNum):
for k in range(classNum):
pre[k] += w[m]*x[m][k]
wsum = 0.0
for m in range(modelNum):
gw = -x[m][label]/pre[label] - A[m] + B
nw[m] += gw * gw
lrw = alfa/(beta+math.sqrt(nw[m]))
w[m] -= lrw * gw
wsum += w[m]
gA = -w[m]
nA[m] += gA * gA
lrA = alfa/(beta+math.sqrt(nA[m]))
A[m] += lrA * gA
if A[m] < 0:
A[m] = 0
gB = wsum - 1
nB += gB * gB
lrB = alfa/(beta+math.sqrt(nB))
B += lrB * gB

for x in w:
print x

  效果

0.117368795386    gender_cut_ftrl8000_val
0.0736857356383   gender_cut_ftrl_val
0.0387213072901   gender_nn_1005_val
0.236047170656    gender_cut_ftrl2000_val
0.463029142921    gender_cut_ftrl1000_val
0.069749655191    gender_newFM_val
0.00104464602712  gender_xxFM_val
0.00131312041348  gender_result_fm

  注意,这里权重之和并不是严格的1,而是1.000959573523,毕竟是个迭代算法,没法保证严格的约束啊……所以计算logloss还是要重新归一下,最后logloss = 0.43512,跟前面的结果基本一致。

  虽然最后两项的权重已经很接近于0,但看着还是很不爽,何不把很小的权重截成严格的0,就像生死符那样爽。对老司机来说都不是问题,上FTRL(为什么又是我)就好了嘛。

第八式:截权道!!!!!!!!

  FTRL的稀疏性就是专职干这的,将w的adagrad改成FTRL,代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import sys, math

classNum = int(sys.argv[1])
modelNum = int(sys.argv[2])
alfa = float(sys.argv[3])
beta = 1.0

l1 = float(sys.argv[4])
l2 = float(sys.argv[5])

print >> sys.stderr, "classNum=" + str(classNum)
print >> sys.stderr, "modelNum=" + str(modelNum)
print >> sys.stderr, "alfa=" + str(alfa)
print >> sys.stderr, "l1=" + str(l1)
print >> sys.stderr, "l2=" + str(l2)

w = [1.0] * modelNum
nw = [0.0] * modelNum
zw = [0.0] * modelNum
A = [0.0] * modelNum
nA = [0.0] * modelNum
B = 0.0
nB = 0.0

for i in range(len(w)):
w[i] /= modelNum

print >> sys.stderr, w

for line in sys.stdin:
seg = line.rstrip().split(" ")
label = int(seg[0])
x = []
for tmp in seg[1:]:
seg2 = tmp.split(",")
for i in range(len(seg2)):
seg2[i] = float(seg2[i])
x.append(seg2)
pre = [0.0] * classNum
avgp = [0.0] * classNum
for m in range(modelNum):
for k in range(classNum):
pre[k] += w[m]*x[m][k]
avgp[k] += x[m][k]/modelNum
wsum = 0.0
for m in range(modelNum):
p = pre[label]
if 0 == p:
p = avgp[label]
gw = -x[m][label]/p - A[m] + B
sw = (math.sqrt(nw[m]+gw*gw)-math.sqrt(nw[m]))/alfa
zw[m] += gw - sw * w[m]
nw[m] += gw * gw
if abs(zw[m]) < l1:
w[m] = 0
else:
sgnz = 1
if zw[m] < 0:
sgnz = -1
w[m] = (sgnz*l1 - zw[m])/((beta + math.sqrt(nw[m]))/alfa + l2)
wsum += w[m]
gA = -w[m]
nA[m] += gA * gA
lrA = alfa/(beta+math.sqrt(nA[m]))
A[m] += lrA * gA
if A[m] < 0:
A[m] = 0
gB = wsum - 1
nB += gB * gB
lrB = alfa/(beta+math.sqrt(nB))
B += lrB * gB

for x in w:
print x

  注意这里L1正则要设的很大才行,比如100,效果如下:

0.117563109491  gender_cut_ftrl8000_val
0.0734000016105 gender_cut_ftrl_val
0.0378296537773 gender_nn_1005_val
0.234723810866  gender_cut_ftrl2000_val
0.461201320728  gender_cut_ftrl1000_val
0.0747847726281 gender_newFM_val
0               gender_xxFM_val
0               gender_result_fm

  哈哈,最后两项终于被彻底干掉,最后的logloss = 0.435117,依然保持的很好。

  以上所有方法,在gender的7分类上也做了实验,同样有效,懒得再贴结果。


  故事终于讲完了,Ensembling met Lagrange, and they lived happily ever after.

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值