fastText与GloVe原理

fastText

fasttext是facebook开源的一个词向量与文本分类工具,在2016年开源,典型应用场景是“带监督的文本分类问题”。提供简单而高效的文本分类和表征学习的方法,性能比肩深度学习而且速度更快。

在此之前word2vec将上下文关系转化为多分类任务,进而训练逻辑回归模型,这里的类别数量是 |V| 词库大小。通常的文本数据中,词库少则数万,多则百万,在训练中直接训练多分类逻辑回归并不现实。word2vec中提供了两种针对大规模多分类问题的优化手段, negative sampling 和 hierarchical softmax。在优化中,negative sampling 只更新少量负面类,从而减轻了计算量。hierarchical softmax 将词库表示成前缀树,从树根到叶子的路径可以表示为一系列二分类器,一次多分类计算的复杂度从|V|降低到了树的高度。

1. fastText原理

fastText 模型输入一个词的序列(一段文本或者一句话),输出这个词序列属于不同类别的概率。序列中的词和词组组成特征向量,特征向量通过线性变换映射到中间层,中间层再映射到标签。fastText 在预测标签时使用了非线性激活函数,但在中间层不使用非线性激活函数。fastText 模型架构和 Word2Vec 中的 CBOW 模型很类似。不同之处在于,fastText 预测标签,而 CBOW 模型预测中间词。

2. 模型架构

其中 x 1 , x 2 , . . . , x N − 1 , x N x_1,x_2,...,x_{N−1},x_N x1,x2,...,xN1,xN 表示一个文本中的n-gram向量,每个特征是词向量的平均值。这和cbow相似,cbow用上下文去预测中心词,而此处用全部的n-gram去预测指定类别。
在这里插入图片描述

目标函数

在这里插入图片描述

  • N N N: 样本个数
  • y n y_n yn: 第n个样本对应的类别
  • f f f: 损失函数 softmax/ns(negative sampling)/hs(hierarchical softmax)
  • x n x_n xn: 第n个样本的归一化特征
  • A A A: 权重矩阵(构建词,embedding层)
  • B B B: 权重矩阵(隐层到输出层)

3. 层次SoftMax

对于有大量类别的数据集,fastText使用了一个分层分类器(而非扁平式架构)。不同的类别被整合进树形结构中(想象下二叉树而非 list)。在某些文本分类任务中类别很多,计算线性分类器的复杂度高。为了改善运行时间,fastText 模型使用了层次 Softmax 技巧。层次 Softmax 技巧建立在哈弗曼编码的基础上,对标签进行编码,能够极大地缩小模型预测目标的数量。

fastText 也利用了类别(class)不均衡这个事实(一些类别出现次数比其他的更多),通过使用 Huffman 算法建立用于表征类别的树形结构。因此,频繁出现类别的树形结构的深度要比不频繁出现类别的树形结构的深度要小,这也使得进一步的计算效率更高。

4. N-gram子词特征

fastText方法不同与word2vec方法,引入了两类特征并进行embedding。其中n-gram颗粒度是词与词之间,n-char是单个词之间。用hashing来减少N-gram的存储

  1. fastText 可以用于文本分类和句子分类。不管是文本分类还是句子分类,我们常用的特征是词袋模型。但词袋模型不能考虑词之间的顺序,因此 fastText 还加入了 N-gram 特征。两类特征的存储均通过计算hash值的方法实现。
  2. 在 fastText中,每个词被看做是 n-gram字母串包。为了区分前后缀情况,"<", ">"符号被加到了词的前后端。除了词的子串外,词本身也被包含进了 n-gram字母串包。以 where 为例,n=3 的情况下,其子串分别为<wh, whe, her, ere, re>,以及其本身 。

好处

  • 对于低频词生成的词向量效果会更好。因为它们的n-gram可以和其它词共享。
  • 对于训练词库之外的单词,仍然可以构建它们的词向量。我们可以叠加它们的字符级n-gram向量。

5. fastText 词向量与word2vec对比

FastText= word2vec中 cbow + h-softmax的灵活使用

相似处:

  • 图模型结构很像,都是采用embedding向量的形式,得到word的隐向量表达。
  • 都采用很多相似的优化方法,比如使用Hierarchical softmax优化训练和预测中的打分速度。

不同处:

  • 模型的输入层:word2vec的输入层,是 context window 内的term;而fasttext 对应的整个sentence的内容,包括term,也包括 n-gram的内容。
  • 模型的输出层:word2vec的输出层,对应的是每一个term,计算某term的概率最大;而fasttext的输出层对应的是分类的label。不过不管输出层对应的是什么内容,其对应的vector都不会被保留和使用。
  • 两者本质的不同,体现在 h-softmax的使用:Word2vec的目的是得到词向量,该词向量最终是在输入层得到,输出层对应的 h-softmax
    也会生成一系列的向量,但最终都被抛弃,不会使用。fastText则充分利用了h-softmax的分类功能,遍历分类树的所有叶节点,找到概率最大的label(一个或者N个)

6. 不平衡分类

把原来的softmax看做深度为1的树,词表V中的每一个词语表示一个叶子节点。如果把softmax改为二叉树结构,每个word表示叶子节点,那么只需要沿着通向该词语的叶子节点的路径搜索,而不需要考虑其它的节点。这就是为什么fastText可以解决不平衡分类问题,因为在对某个节点进行计算时,完全不依赖于它的上一层的叶子节点(即权重大于它的叶结点),也就是数目较大的label不能影响数目较小的label(即图5中B无法影响A和C)。

7. fastText 实现

pip install fasttext

分类

import fasttext
// 模型训练
classifier = fasttext.supervised("fasttext_train.txt","fasttext.model",label_prefix = "__label__")

fasttext.supervised():

  • 第一个参数为训练集,即用来拟合模型的数据
  • 第二个参数为模型存储的绝对路径
  • 第三个为文本与标签的分隔符
import fasttext
// 加载模型
classifier = fasttext.load_model("fasttext.model.bin",label_prefix = "__label__")
// 测试模型 其中 fasttext_test.txt 就是测试数据,格式和 fasttext_train.txt 一样
result = classifier.test("fasttext_test.txt")
print "准确率:",result.precision
print "回归率:",result.recall

// 使用模型,以测试集中第一个文档为例
f = open("fasttext_test.txt")
line = f.readlines()[0]
f.close()
result = classifier.predict([line])
print result
from fastText import train_supervised, load_model
train_supervised(input, lr=0.1, dim=100, 
                   ws=5, epoch=5, minCount=1, 
                   minCountLabel=0, minn=0, 
                   maxn=0, neg=5, wordNgrams=1, 
                   loss="softmax", bucket=2000000, 
                   thread=12, lrUpdateRate=100,
                   t=1e-4, label="__label__", 
                   verbose=2, pretrainedVectors=""):
  """
  训练一个监督模型, 返回一个模型对象
  @param input: 训练数据文件路径
  @param lr:              学习率
  @param dim:             向量维度
  @param ws:              cbow模型时使用
  @param epoch:           次数
  @param minCount:        词频阈值, 小于该值在初始化时会过滤掉
  @param minCountLabel:   类别阈值,类别小于该值初始化时会过滤掉
  @param minn:            构造subword时最小char个数
  @param maxn:            构造subword时最大char个数
  @param neg:             负采样
  @param wordNgrams:      n-gram个数
  @param loss:            损失函数类型, softmax, ns: 负采样, hs: 分层softmax
  @param bucket:          词扩充大小, [A, B]: A语料中包含的词向量, B不在语料中的词向量
  @param thread:          线程个数, 每个线程处理输入数据的一段, 0号线程负责loss输出
  @param lrUpdateRate:    学习率更新
  @param t:               负采样阈值
  @param label:           类别前缀
  @param verbose:         ??
  @param pretrainedVectors: 预训练的词向量文件路径, 如果word出现在文件夹中初始化不再随机
  @return model object
  """

模型的保存与加载

// 模型的保存与加载
from fastText import load_model
model.save_model(path)
load_model(path)

词向量训练

fasttext不仅可以进行文本分类,也可以训练词向量,准备语料时,只需要去掉原始数据中的label标签即可。

from fastText import train_supervised, load_model
train_unsupervised(input, model="skipgram", lr=0.05, dim=100, 
                   ws=5, epoch=5, minCount=5, 
                   minCountLabel=0, minn=3, 
                   maxn=6, neg=5, wordNgrams=1, 
                   loss="ns", bucket=2000000, 
                   thread=12, lrUpdateRate=100,
                   t=1e-4, label="__label__", 
                   verbose=2, pretrainedVectors=""):
  """
  训练词向量,返回模型对象
  输入数据不要包含任何标签和使用标签前缀

  @param model: 模型类型, cbow/skipgram两种
  其他参数参考train_supervised()方法
  @return model
  """
  pass

参数方面的建议:

  1. loss function 选用 hs(hierarchical softmax)要比 ns(negative sampling)训练速度更快,准确率也更高
  2. wordNgram 默认为1,建议设置为 2 或以上更好
  3. 如果词数不是很多,可以把 bucket 设置小一些,否则会预留太多的 bucket 使模型太大

参考:
https://www.cnblogs.com/tsdblogs/p/10479660.html
https://www.cnblogs.com/huangyc/p/9768872.html
https://blog.csdn.net/qq_32023541/article/details/80839800
https://blog.csdn.net/ymaini/article/details/81489599

GloVe

GloVe: Global Vectors for Word Representation
GloVe的全称叫Global Vectors for Word Representation,它是一个基于全局词频统计(count-based & overall statistics)的词表征(word representation)工具,它可以把一个单词表达成一个由实数组成的向量,这些向量捕捉到了单词之间一些语义特性,比如相似性(similarity)、类比性(analogy)等。我们通过对向量的运算,比如欧几里得距离或者cosine相似度,可以计算出两个单词之间的语义相似性。

1. 共现矩阵

根据语料库(corpus)构建一个共现矩阵(Co-ocurrence Matrix)X,矩阵中的每一个元素 X i j X_{ij} Xij 代表单词 i i i 和上下文单词 j j j 在特定大小的上下文窗口(context window)内共同出现的次数。
例子:
语料库:i love you but you love him i am sad
采用一个窗口宽度为5(左右长度都为2)的统计窗口,那么就有以下窗口内容:
在这里插入图片描述
以窗口5为例说明如何构造共现矩阵:中心词为love,语境词为 but、you、him、i;则执行: X l o v e , b u t + = 1 X_{love,but}+=1 Xlove,but+=1 X l o v e , y o u + = 1 X_{love,you}+=1 Xlove,you+=1 X l o v e , h i m + = 1 X_{love,him}+=1 Xlove,him+=1 X l o v e , i + = 1 X_{love,i}+=1 Xlove,i+=1使用窗口将整个语料库遍历一遍,即可得到共现矩阵X。一般而言,这个 X i j X_{ij} Xij 次数的最小单位是1,但是GloVe不这么认为:它根据两个单词在上下文窗口的距离 d d d ,提出了一个衰减函数(decreasing weighting)用于计算权重,也就是说距离越远的两个单词所占总计数(total count)的权重越小: d e c a y = 1 / d decay = 1/d decay=1/d

2. GloVe模型推导

变量定义

  • X i j X_{ij} Xij :表示单词 j j j 出现在单词 i i i 的上下文中的次数;
  • X i X_{i} Xi :表示单词 i i i 的上下文中所有单词出现的总次数,即 X i = ∑ k X i k X_{i} = \sum^{k} X_{ik} Xi=kXik
  • P i j = P ( j ∣ i ) = X i j X i P_{ij} = P(j|i) = \frac{X_{ij}}{X_i} Pij=P(ji)=XiXij:即表示单词 j j j 出现在单词 i i i 的上下文中的概率
  • r a t i o i , j , k = P i , k P j , k ratio_{i,j,k} = \frac{P_{i,k}}{P_{j,k}} ratioi,j,k=Pj,kPi,k:两个概率的比值,可以使用它观察出两个单词 i i i j j j 相对于单词 k k k 哪个更相关(relevant)

在这里插入图片描述
上图中,ice和solid更相关,而stream和solid明显不相关, r a t i o ratio ratio 指标规律:

在这里插入图片描述
假设我们已经得到了词向量,如果我们用词向量通过某种函数计算ratio,能够同样得到这样的规律的话,就意味着我们词向量与共现矩阵具有很好的一致性,也就说明我们的词向量中蕴含了共现矩阵中所蕴含的信息。以上推断可以说明通过概率的比例而不是概率本身去学习词向量可能是一个更恰当的方法

公式推导

loss function

设用词向量 v i 、 v j 、 v k v_i、v_j、v_k vivjvk 计算 r a t i o i , j , k ratio_{i,j,k} ratioi,j,k 的函数为 g ( v i , v j , v k ) g(v_i,v_j,v_k) g(vi,vj,vk)(先不去管具体的函数形式),那么应该有: P i , k P j , k = r a t i o i , j , k = g ( v i , v j , v k ) \frac{P_{i,k}}{P_{j,k}}=ratio_{i,j,k}=g(v_{i},v_{j},v_{k}) Pj,kPi,k=ratioi,j,k=g(vi,vj,vk)根据我们的设想,两者应该要可能得接近,因此利用两者的均方差来作为代价函数: J = ∑ i , j , k N ( P i , k P j , k − g ( v i , v j , v k ) ) 2 J=\sum_{i,j,k}^N(\frac{P_{i,k}}{P_{j,k}}-g(v_{i},v_{j},v_{k}))^2 J=i,j,kN(Pj,kPi,kg(vi,vj,vk))2

关联函数

设计关联函数 g ( v i , v j , v k ) g(v_{i},v_{j},v_{k}) g(vi,vj,vk)

  1. 考虑单词 i i i 和单词 j j j 之间的关系,因为向量空间是线性结构的,所以要表达出两个概率的比例差,最简单的办法是作差: v i − v j v_i-v_j vivj
  2. r a t i o i , j , k ratio_{i,j,k} ratioi,j,k是个标量,则 g ( v i , v j , v k ) g(v_{i},v_{j},v_{k}) g(vi,vj,vk)最后生成的值也应该是个标量,即做内积生成标量: ( v i − v j ) T v k (v_{i}-v_{j})^Tv_{k} (vivj)Tvk
  3. 作者往 ( v i − v j ) T v k (v_i−v_j)^Tv_k (vivj)Tvk的外面套了一层指数运算exp(),得到 g ( v i , v j , v k ) = e x p ( ( v i − v j ) T v k ) g(v_{i},v_{j},v_{k})=exp((v_{i}-v_{j})^Tv_{k}) g(vi,vj,vk)=exp((vivj)Tvk) ,套上之后,我们的目标是让以下公式尽可能地成立: P i , k P j , k = e x p ( ( v i − v j ) T v k ) = e x p ( ( v i T v k − v j T v k ) ) \frac{P_{i,k}}{P_{j,k}}=exp((v_{i}-v_{j})^Tv_{k})=exp((v_{i}^Tv_{k}-v_{j}^Tv_{k})) Pj,kPi,k=exp((vivj)Tvk)=exp((viTvkvjTvk))即: P i , k P j , k = e x p ( v i T v k ) e x p ( v j T v k ) \frac{P_{i,k}}{P_{j,k}}=\frac{exp(v_{i}^Tv_{k})}{exp(v_{j}^Tv_{k})} Pj,kPi,k=exp(vjTvk)exp(viTvk)此时,只需要让上式分子对应相等,分母对应相等,即: P i , k = e x p ( v i T v k ) P_{i,k}=exp(v_{i}^Tv_{k}) Pi,k=exp(viTvk) P j , k = e x p ( v j T v k ) P_{j,k}=exp(v_{j}^Tv_{k}) Pj,k=exp(vjTvk)即统一形式为: P i , j = e x p ( v i T v j ) P_{i,j}=exp(v_{i}^Tv_{j}) Pi,j=exp(viTvj) 取对数: l o g ( P i , j ) = v i T v j log(P_{i,j})=v_{i}^Tv_{j} log(Pi,j)=viTvj 代价函数就可以简化为: J = ∑ i , j N ( l o g ( P i , j ) − v i T v j ) 2 J=\sum_{i,j}^N(log(P_{i,j})-v_{i}^Tv_{j})^2 J=i,jN(log(Pi,j)viTvj)2
  4. 共现矩阵是个对称矩阵,单词和上下文单词其实是相对的,即有: v i T v j = v j T v i v_{i}^Tv_{j}=v_{j}^Tv_{i} viTvj=vjTvi ,但现在的公式是不满足 l o g ( P i , j ) = l o g ( P j , i ) log(P_{i,j})=log(P_{j,i}) log(Pi,j)=log(Pj,i)。现将代价函数中的条件概率展开: l o g ( P i , j ) = l o g X i , j X i = l o g ( X i , j ) − l o g ( X i ) = v i T v j log(P_{i,j}) = log\frac{X_{i,j}}{X_i}=log(X_{i,j})-log(X_i)=v_{i}^Tv_{j} log(Pi,j)=logXiXi,j=log(Xi,j)log(Xi)=viTvj将其变为: l o g ( X i , j ) = v i T v j + b i log(X_{i,j})=v_{i}^Tv_{j}+b_{i} log(Xi,j)=viTvj+bi还是不满足对称性,于是我们针对 v j v_j vj 也添加一个偏置: l o g ( X i , j ) = v i T v j + b i + b j log(X_{i,j})=v_{i}^Tv_{j}+b_{i}+b_{j} log(Xi,j)=viTvj+bi+bj词是代价函数变为: J = ∑ i , j N ( v i T v j + b i + b j − l o g ( X i , j ) ) 2 J=\sum_{i,j}^N(v_{i}^Tv_{j}+b_{i}+b_{j}-log(X_{i,j}))^2 J=i,jN(viTvj+bi+bjlog(Xi,j))2
  5. 基于出现频率越高的词对权重应该越大的原则,在代价函数中添加权重项,于是代价函数进一步完善: J = ∑ i , j N f ( X i , j ) ( v i T v j + b i + b j − l o g ( X i , j ) ) 2 J=\sum_{i,j}^Nf(X_{i,j})(v_{i}^Tv_{j}+b_{i}+b_{j}-log(X_{i,j}))^2 J=i,jNf(Xi,j)(viTvj+bi+bjlog(Xi,j))2首先权重函数应该是非减的;其次当词频过高时,权重不应过分增大;最后如果两个单词没有在一起出现,也就是 X i , j = 0 X_{i,j}=0 Xi,j=0 那么他们应该不参与到loss function的计算当中去,也就是要满足 f ( 0 ) = 0 f(0)=0 f(0)=0 。作者通过实验确定权重函数为( x m a x = 100 x_{max}=100 xmax=100): f ( x ) = { ( x / x m a x ) 0.75 , if  x &lt; x m a x 1 , if  x &gt; = x m a x f(x) = \begin{cases} (x/x_{max})^{0.75}, &amp; \text{if $x\lt x_{max}$} \\ 1, &amp; \text{if $x\gt= x_{max}$} \end{cases} f(x)={(x/xmax)0.75,1,if x<xmaxif x>=xmax

3. GloVe模型训练词向量

虽然很多人声称GloVe是一种无监督(unsupervised learing)的学习方式(因为它确实不需要人工标注label),但其实它还是有label的,这个label就是公式中的 l o g ( X i , j ) log(X_{i,j}) log(Xi,j),向量 v i , v j v_i,v_j vivj 就是要不断更新/学习的参数,所以本质上训练方式跟监督学习的训练方法一样,都是基于梯度下降的。这篇论文里的实验是这么做的:采用了AdaGrad的梯度下降算法,对矩阵X中的所有非零元素进行随机采样,学习曲率(learning rate)设为0.05,在vector size小于300的情况下迭代了50次,其他大小的vectors上迭代了100次,直至收敛。最终学习得到的是两个vector是 v i , v j v_i,v_j vivj ,因为X是对称的(symmetric),所以从原理上讲 v i 和 v j v_i和v_j vivj 是也是对称的,他们唯一的区别是初始化的值不一样,而导致最终的值不一样。所以这两者其实是等价的,都可以当成最终的结果来使用。但是为了提高鲁棒性,我们最终会选择两者之和 v i + v j v_i + v_j vi+vj 作为最终的vector(两者的初始化不同相当于加了不同的随机噪声,所以能提高鲁棒性)

4. Glove与LSA、word2vec的比较

  1. LSA(Latent Semantic Analysis)是一种比较早的count-based的词向量表征工具,它也是基于共现矩阵的,只不过采用了基于奇异值分解(SVD)的矩阵分解技术对大矩阵进行降维,而SVD的复杂度是很高的,所以它的计算代价比较大。还有一点是它对所有单词的统计权重都是一致的。而这些缺点在GloVe中被一一克服了。
  2. word2vec最大的缺点则是没有充分利用所有的语料,Glove融入全局的先验统计信息,可以加快模型的训练速度,又可以控制词的相对权重。

参考:
https://www.fanyeong.com/2018/02/19/glove-in-detail/
https://blog.csdn.net/u014665013/article/details/79642083

5. Glove 实现

Glove训练词向量

https://github.com/stanfordnlp/GloVe
进入glove/build文件夹下,里面主要介绍这个程序包括了四部分子程序,按步骤各自是vocab_count、cooccur、shuffle、glove。

  1. vocab_count:用于计算原文本的单词统计(生成vocab.txt,每一行为:单词 词频)
  2. cooccur:用于统计词与词的共现。目測类似与word2vec的窗体内的随意两个词(生成的是cooccurrence.bin,二进制文件)
  3. shuffle:对于2中的共现结果又一次整理(一看到shuffle瞬间想到hadoop,生成的也是二进制文件cooccurrence.shuf.bin)
  4. glove:glove算法的训练模型,会运用到之前生成的相关文件,终于会输出vectors.txt和vectors.bin
CORPUS=text8
VOCAB_FILE=vocab.txt
COOCCURRENCE_FILE=cooccurrence.bin
COOCCURRENCE_SHUF_FILE=cooccurrence.shuf.bin
BUILDDIR=build
SAVE_FILE=vectors
VERBOSE=2
MEMORY=4.0
VOCAB_MIN_COUNT=5
VECTOR_SIZE=50
MAX_ITER=15
WINDOW_SIZE=15
BINARY=2
NUM_THREADS=8
X_MAX=10

$BUILDDIR/vocab_count -min-count $VOCAB_MIN_COUNT -verbose $VERBOSE < $CORPUS > $VOCAB_FILE
$BUILDDIR/cooccur -memory $MEMORY -vocab-file $VOCAB_FILE -verbose $VERBOSE -window-size $WINDOW_SIZE < $CORPUS > $COOCCURRENCE_FILE
$BUILDDIR/shuffle -memory $MEMORY -verbose $VERBOSE < $COOCCURRENCE_FILE > $COOCCURRENCE_SHUF_FILE
$BUILDDIR/glove -save-file $SAVE_FILE -threads $NUM_THREADS -input-file $COOCCURRENCE_SHUF_FILE -x-max $X_MAX -iter $MAX_ITER -vector-size $VECTOR_SIZE -binary $BINARY -vocab-file $VOCAB_FILE -verbose $VERBOSE

在生成的 vectors.txt 的最开头,加上两个数,第一个数指明一共有多少个向量,第二个数指明每个向量有多少维,就能直接用 word2vec 的 load 函数加载了

 model=gensim.models.Word2Vec.load_word2vec_format('vectors.txt',binary=False)

利用 embeddings 调用 Glove向量

Embeddings is a python package that provides pretrained word embeddings for natural language processing and machine learning.

Instead of loading a large file to query for embeddings, embeddings is backed by a database and fast to load and query:
https://github.com/vzhong/embeddings

pip install embeddings  # from pypi
pip install git+https://github.com/vzhong/embeddings.git  # from github

Upon first use, the embeddings are first downloaded to disk in the form of a SQLite database. This may take a long time for large embeddings such as GloVe. Further usage of the embeddings are directly queried against the database. Embedding databases are stored in the $EMBEDDINGS_ROOT directory (defaults to ~/.embeddings). Note that this location is probably undesirable if your home directory is on NFS, as it would slow down database queries significantly.

from embeddings import GloveEmbedding
from embeddings import FastTextEmbedding
from embeddings import KazumaCharEmbedding
from embeddings import ConcatEmbedding

g = GloveEmbedding('common_crawl_840', d_emb=300, show_progress=True)
f = FastTextEmbedding()
k = KazumaCharEmbedding()
c = ConcatEmbedding([g, f, k])
for w in ['canada', 'vancouver', 'toronto']:
    print('embedding {}'.format(w))
    print(g.emb(w))
    print(f.emb(w))
    print(k.emb(w))
    print(c.emb(w))
from embeddings import GloveEmbedding, KazumaCharEmbedding
embeddings = [GloveEmbedding(), KazumaCharEmbedding()]
for w in tqdm(word_vocab):
    e = []
    for emb in embeddings:
        e += emb.emb(w, default='zero')
    E.append(e)

参考:https://www.cnblogs.com/gcczhongduan/p/5198169.html

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值