【TensorFlow-windows】学习笔记七——生成对抗网络

前言

既然学习了变分自编码(VAE),那也必须来一波生成对抗网络(GAN)。

国际惯例,参考网址:

论文: Generative Adversarial Nets

PPT:Generative Adversarial Networks (GANs)

Generative Adversarial Nets in TensorFlow

GAN原理学习笔记
GAN — Ways to improve GAN performance

理论

粗略点的讲法就说:一个生成器 G G G,一个判别器 D D D,前者用来将噪声输入转换成图片,后者判别当前输入图片是真实的还是生成的。

为了更详细地了解GAN,还是对论文进行简要的组织、理解吧。有兴趣直接看原始论文,这里进行部分关键内容的摘抄。

任意的 G G G D D D函数空间都存在特定解, G G G要能表示训练集的分布,而 D D D一定是等于 1 2 \frac{1}{2} 21,也就是说判别器无法分辨当前输入是真的还是假的,这样就达到了鱼目混珠的效果。在GAN中,使用多层感知器构建 G G G D D D,整个模型可以使用反向传播算法学习。

论文里面有一句很好的话解释了GAN的动机:目前深度学习在判别模型的设计中取得了重大成功,但是在生成模型却鲜有成效,主要原因在于在极大似然估计和相关策略中有很多难以解决的概率计算难题(想想前一篇博客的变分自编码的理论,阔怕),而且丢失了生成背景下的分段线性单元的优势,因此作者就提出了新的生成模型估计方法,避开这些难题,也就是传说中的GAN。它的训练完全不需要什么鬼似然估计,只需要使用目前炒鸡成功的反传和dropout算法。

为了让生成器学到数据分布 p g p_g pg,需要定义一个先验的噪声输入 p z ( z ) p_z(z) pz(z),然后使用 G ( z ; θ g ) G(z;\theta_g) G(z;θg)将其映射到数据空间,这里的 G G G是具有参数 θ g \theta_g θg的多层感知器。然后定义另一个多层感知器 D ( x ; θ d ) D(x;\theta_d) D(x;θd),输出一个标量。 D ( x ) D(x) D(x)代表的是 x x x来自于真实样本而非生成的样本 p g p_g pg的概率,我们训练 D D D去最大化将正确标签同时赋予训练集和 G G G生成的样本的概率,也就是 D D D把真的和假的图片都当成真的了。同时要去训练 G G G去最小化 log ⁡ ( 1 − D ( G ( z ) ) ) \log (1-D(G(z))) log(1D(G(z))),是为了让生成的图片被赋予正样本标签的概率大点,损失函数就是:
min ⁡ G max ⁡ D V ( D , G ) = E x ∼ p d a t a ( x ) [ log ⁡ D ( x ) ] + E z ∼ p z ( z ) [ log ⁡ ( 1 − D ( G ( z ) ) ) ] \min_G \max_D V(D,G)=E_{x\sim p_{data}(x)}[\log D(x)]+E_{z\sim p_z(z)}[\log(1-D(G(z)))] GminDmaxV(D,G)=Expdata(x)[logD(x)]+Ezpz(z)[log(1D(G(z)))]
在优化 D D D的时候,在训练的内循环中是无法完成的,计算上不允许,并且在有限数据集上会导致过拟合,因此可以以 k : 1 k:1 k:1 的训练次数比例分别优化 D D D G G G。这能够让 D D D保持在最优解附近,只要 G G G变化比较缓慢。

而且在实际中,上式可能无法提供足够的梯度让 G G G很好地学习,在训练早期,当 G G G很差的时候, D D D能够以很高的概率将其归为负样本,因为生成的数据与训练数据差距很大,这样 log ⁡ ( 1 − D ( G ( z ) ) ) \log(1-D(G(z))) log(1D(G(z)))就饱和了,与其说最小化 log ⁡ ( 1 − D ( G ( z ) ) ) \log(1-D(G(z))) log(1D(G(z)))不如去最大化 log ⁡ ( D ( G ( z ) ) ) \log(D(G(z))) log(D(G(z))),这个目标函数对 G G G D D D的收敛目标不变,但是能早期学习具有更强的梯度。

训练算法:

外层一个大循环就不说了,对所有的批数据迭代,内层有一个小循环,控制上面说的判别器 D D D与生成器 G G G的训练比例为 k : 1 k:1 k:1的:

  • 以下步骤执行 k k k次:

    • 从噪声先验 p g ( z ) p_g(z) pg(z)中采样 m m m个噪声样本 { z ( 1 ) , ⋯   , z ( m ) } \{z^{(1)},\cdots,z^{(m)}\} {z(1),,z(m)}

    • 从原始样本分布 p d a t a ( x ) p_{data}(x) pdata(x)中选取 m m m个样本 x ( 1 ) ⋯ x ( m ) {x^{(1)}\cdots x^{(m)}} x(1)x(m),说这么复杂,原始样本的分布不就是原始样本么,直接从原始样本里面选一批数据就行了

    • 更新判别器
      ∇ θ d 1 m ∑ i = 1 m [ log ⁡ D ( x ( i ) ) + log ⁡ ( 1 − D ( G ( z ( i ) ) ) ) ] \nabla_{\theta_d}\frac{1}{m}\sum_{i=1}^m \left[\log D\left(x^{(i)}\right)+\log \left(1-D\left(G\left(z^{(i)}\right)\right)\right)\right] θdm1i=1m[logD(x(i))+log(1D(G(z(i))))]

  • 从噪声先验 p g ( z ) p_g(z) pg(z)中采样 m m m个噪声样本 { z ( 1 ) , ⋯   , z ( m ) } \{z^{(1)},\cdots,z^{(m)}\} {z(1),,z(m)}

  • 更新生成器
    ∇ θ g 1 m ∑ i = 1 m log ⁡ ( 1 − D ( G ( z ( i ) ) ) ) \nabla \theta_g \frac{1}{m}\sum_{i=1}^m \log\left(1-D\left(G \left( z^{(i)}\right) \right)\right) θgm1i=1mlog(1D(G(z(i))))

后面作者又证明了两个内容:

  • p g = p d a t a p_g=p_{data} pg=pdata的全局最优
  • 训练算法的收敛性

身为一个合格的程序猿,还是很有必要看看数学推导的o(╯□╰)o虽然不一定能懂

先看一个简单的式子: y → a log ⁡ ( y ) + b log ⁡ ( 1 − y ) y\to a\log(y)+b\log(1-y) yalog(y)+blog(1y),这个式子在 [ 0 , 1 ] [0,1] [0,1]范围取得最大值的点是在 a a + b \frac{a}{a+b} a+ba,证明很简单,直接两边求导,令 y ′ = 0 y'=0 y=0就能算出来。

再看看我们的优化目标,当给定生成器 G G G的时候,也就是 G G G固定的时候:
V ( G , D ) = ∫ x p d a t a ( x ) log ⁡ D ( x ) d x + ∫ z p z ( z ) log ⁡ ( 1 − D ( g ( z ) ) ) d z = ∫ x p d a t a ( x ) log ⁡ ( D ( x ) ) + p g ( x ) log ⁡ ( 1 − D ( x ) ) d x V(G,D)=\int_x p_{data}(x)\log D(x)dx+\int _zp_z(z)\log(1-D(g(z)))dz\\ =\int _xp_{data}(x)\log(D(x))+p_g(x)\log(1-D(x))dx V(G,D)=xpdata(x)logD(x)dx+zpz(z)log(1D(g(z)))dz=xpdata(x)log(D(x))+pg(x)log(1D(x))dx
长得挺像,那么 D D D的最优解就是:
D G ∗ ( x ) = p d a t a ( x ) p d a t a ( x ) + p g ( x ) D_G^*(x)=\frac{p_{data}(x)}{p_{data}(x)+p_g(x)} DG(x)=pdata(x)+pg(x)pdata(x)
D D D的训练目标可以看成最大化对数似然去估算条件概率 P ( Y = y ∣ x ) P(Y=y|x) P(Y=yx),这里 Y Y Y表示 x x x来自于原始数据 p d a t a p_{data} pdata(此时 y = 1 y=1 y=1)还是生成数据 p g p_g pg(此时 y = 0 y=0 y=0),所以损失函数又可以写成:
C ( G ) = max ⁡ D V ( G , D ) = E x ∼ p d a t a [ log ⁡ D G ∗ ( x ) ] + E z ∼ p z [ log ⁡ ( 1 − D G ∗ ( G ( z ) ) ) ] = E x ∼ p d a t a [ log ⁡ D G ∗ ( x ) ] + E z ∼ p g [ log ⁡ ( 1 − D G ∗ ( x ) ) ] = E x ∼ p d a t a [ log ⁡ p d a t a ( x ) p d a t a ( x ) + p g ( x ) ] + E x ∼ p g [ log ⁡ p g ( x ) p d a t a ( x ) + p g ( x ) ] \begin{aligned} C(G)&=\max_D V(G,D)\\ &=E_{x\sim p_{data}}\left[\log D_G^*(x)\right]+E_{z\sim p_z}\left[\log(1-D^*_G(G(z)))\right]\\ &=E_{x\sim p_{data}}\left[\log D_G^*(x)\right]+E_{z\sim p_g}\left[\log(1-D^*_G(x))\right]\\ &=E_{x\sim p_{data}}\left[\log\frac{p_{data}(x)}{p_{data}(x)+p_g(x)}\right]+E_{x\sim p_g}\left[\log\frac{p_g(x)}{p_{data}(x)+p_g(x)}\right] \end{aligned} C(G)=DmaxV(G,D)=Expdata[logDG(x)]+Ezpz[log(1DG(G(z)))]=Expdata[logDG(x)]+Ezpg[log(1DG(x))]=Expdata[logpdata(x)+pg(x)pdata(x)]+Expg[logpdata(x)+pg(x)pg(x)]
理论1 当且仅当 p g = p d a t a p_g=p_{data} pg=pdata的时候,训练目标 C ( G ) C(G) C(G)达到全局最小,此时, C ( G ) C(G) C(G)收敛到值 − log ⁡ 4 -\log 4 log4

证明:当 p g = p d a t a p_g=p_{data} pg=pdata的时候, D G ∗ ( x ) = 1 2 D^*_G(x)=\frac{1}{2} DG(x)=21,因此 C ( G ) = log ⁡ 1 2 + log ⁡ 1 2 = − log ⁡ 4 C(G)=\log \frac{1}{2}+\log\frac{1}{2}=-\log 4 C(G)=log21+log21=log4,感觉正常应该是:
E x ∼ p d a t a [ − log ⁡ 2 ] + E x ∼ p g [ − log ⁡ 2 ] E_{x\sim p_{data}}[-\log 2]+E_{x\sim p_g}[-\log 2] Expdata[log2]+Expg[log2]
但是作者貌似让 E x ∼ p d a t a = E x ∼ p g = 1 E_{x\sim p_{data}}=E_{x\sim p_g}=1 Expdata=Expg=1了,我估计是因为收敛到最终解的时候,理想状态是判别器无法分辨哪个真哪个假,所以都当成正样本了。这样还能将 C ( G ) C(G) C(G)变形:
C ( G ) = − log ⁡ 4 + K L ( p d a t a ∥ p d a t a + p g 2 ) + K L ( p g ∥ p d a t a + p g 2 ) C(G)=-\log 4+KL\left(p_{data}\parallel\frac{p_{data}+p_g}{2}\right)+KL\left(p_g\parallel \frac{p_{data}+p_g}{2}\right) C(G)=log4+KL(pdata2pdata+pg)+KL(pg2pdata+pg)
其实最终理想状态下后面两个KL距离是等于0的,代表衡量的两个分布一样。

这里作者提到了一个表达式称为Jensen-Shannon divergence,衡量模型分布和数据生成过程:
C ( G ) = − log ⁡ 4 + 2 ⋅ J S D ( p d a t a ∥ p g ) C(G)=-\log 4+2\cdot JSD(p_{data}\parallel p_g) C(G)=log4+2JSD(pdatapg)
这个 J S D JSD JSD始终是非负的,当且仅当 p d a t a = p g p_{data}=p_g pdata=pg的时候取 0 0 0,意思就是生成模型能够完美生成数据分布。

接下来看看算法收敛性

理论2 如果 G G G D D D有足够的容量,并且在训练算法的每一步,给定 G G G时判别器都能达到它的最优,那么 p g p_g pg的更新便可以通过优化
E x ∼ p d a t a [ log ⁡ D G ∗ ( x ) ] + E x ∼ p g [ log ⁡ ( 1 − D G ∗ ( x ) ) ] E_{x\sim p_{data}}\left[\log D_G^*(x)\right]+E_{x\sim p_g}\left[\log (1-D_G^*(x))\right] Expdata[logDG(x)]+Expg[log(1DG(x))]
然后 p g p_g pg就收敛到了 p d a t a p_{data} pdata

证明就不看了,因为我不是特别懂作者那一段文字,我们只需要知道收敛结果就是 p g = p d a t a p_g=p_{data} pg=pdata就行了。

代码实现-模型训练与保存

老流程:读数据、初始化相关参数、定义数据接受接口、初始化权重和偏置、构建基本模块(生成器和判别器)、构建模型、定义损失和优化器、训练

读取数据

这个就不说了,全博客通用:

IMG_HEIGHT=28
IMG_WIDTH=28
CHANNELS=3
#读取数据集
def read_images(dataset_path,batch_size):
    imagepaths,labels=list(),list()
    data=open(dataset_path,'r').read().splitlines()
    for d in data:
        imagepaths.append(d.split(' ')[0])
        labels.append(int(d.split(' ')[1]))
    imagepaths=tf.convert_to_tensor(imagepaths,dtype=tf.string)
    labels=tf.convert_to_tensor(labels,dtype=tf.int32)
    image,label=tf.train.slice_input_producer([imagepaths,labels],shuffle=True)

    image=tf.read_file(image)
    image=tf.image.decode_jpeg(image,channels=CHANNELS)
    image=tf.image.rgb_to_grayscale(image)    

    image=tf.reshape(image,[IMG_HEIGHT*IMG_WIDTH])
    image=tf.cast(image,tf.float32)
    image = image / 255.0
    image=tf.convert_to_tensor(image)

    inputX,inputY=tf.train.batch([image,label],
    batch_size=batch_size,capacity=batch_size*8,num_threads=4)
    return inputX,inputY

定义相关参数

主要是学习率,训练次数,输入单元、隐单元、输出单元的神经元个数

#定义相关参数
learning_rate=0.0002
num_steps=1000
batch_size=128
disp_step=1000
num_class=10
gen_hid_num=256
dis_hid_num=256
noise_dim=100
num_input=IMG_HEIGHT*IMG_WIDTH

定义数据接收接口

需要注意GAN主要有两类接口,生成器接收的是噪声输入,判别器接收的是真实图片或者生成的图片

#建立生成器、判别器的接收接口
gen_input=tf.placeholder(tf.float32,shape=[None,noise_dim],name='gen_input')
dis_input=tf.placeholder(tf.float32,shape=[None,num_input],name='dis_input')

初始化权重

#定义权重
def glorot_init(shape):
    return tf.random_normal(shape=shape,stddev=1/tf.sqrt(shape[0]/2.0))

weights={
    'gen_hidden1':tf.Variable(glorot_init([noise_dim,gen_hid_num])),
    'gen_out':tf.Variable(glorot_init([gen_hid_num,num_input])),
    'dis_hidden1':tf.Variable(glorot_init([num_input,dis_hid_num])),
    'dis_out':tf.Variable(glorot_init([dis_hid_num,1]))
}
biases={
    'gen_hidden1':tf.Variable(tf.zeros([gen_hid_num])),
    'gen_out':tf.Variable(tf.zeros([num_input])),
    'dis_hidden1':tf.Variable(tf.zeros([dis_hid_num])),
    'dis_out':tf.Variable(tf.zeros([1]))
}

基本模块:生成器和判别器

#定义基本模块
def generator(x):
    hidden_layer=tf.add(tf.matmul(x,weights['gen_hidden1']),biases['gen_hidden1'])
    hidden_layer=tf.nn.relu(hidden_layer)
    out_layer=tf.add(tf.matmul(hidden_layer,weights['gen_out']),biases['gen_out'])
    out_layer=tf.nn.sigmoid(out_layer)
    return out_layer

def discriminator(x):
    hidden_layer=tf.add(tf.matmul(x,weights['dis_hidden1']),biases['dis_hidden1'])
    hidden_layer=tf.nn.relu(hidden_layer)
    out_layer=tf.add(tf.matmul(hidden_layer,weights['dis_out']),biases['dis_out'])
    out_layer=tf.nn.sigmoid(out_layer)
    return out_layer

构建模型

注意我们的测试函数是生成器,最终需要它来生成图片,所以需要加入函数中

#生成器
gen_sample=generator(gen_input)
tf.add_to_collection('generation',gen_sample)
#判别器
dis_real=discriminator(dis_input)
dis_fake=discriminator(gen_sample)

定义损失和优化器

损失函数包含生成器和判别器,但是针对生成器,我们上面说过,最小化 log ⁡ ( 1 − D ( G ( z ) ) ) \log (1-D(G(z))) log(1D(G(z)))不如最大化 log ⁡ ( D ( G ( z ) ) ) \log(D(G(z))) log(D(G(z))),所以生成器的损失可以定义为负对数,这样就把最大化又变成最小化了:

gen_loss = -tf.reduce_mean(tf.log(disc_fake))

当然你也可以使用最小化的方法,此博文即用最小化 log ⁡ ( 1 − D ( G ( z ) ) ) \log(1-D(G(z))) log(1D(G(z)))的方法:

G_loss = tf.reduce_mean(tf.log(1-prob_artist1))

判别器还是老样子最大化 log ⁡ ( D ( x ) ) − log ⁡ ( D ( G ( z ) ) ) \log (D(x))-\log(D(G(z))) log(D(x))log(D(G(z))),加个负号也是最小化了:

disc_loss = -tf.reduce_mean(tf.log(disc_real) + tf.log(1. - disc_fake))

【注】其实有的时候也可以直接用交叉熵来定义损失,让判别器对真实图片的标签接近1,对假图片的判别标签接近0

d_loss_real = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits = D, labels = tf.ones_like(D)))
d_loss_fake = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits = _D, labels = tf.zeros_like(_D)))
d_loss = d_loss_real + d_loss_fake

而对于生成器,希望判别器对假图片的判别标签接近1:

g_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits = _D, labels = tf.ones_like(_D)))

话不多说,接下来定义优化器:

optimizer_gen=tf.train.AdamOptimizer(learning_rate=learning_rate)
optimizer_dis=tf.train.AdamOptimizer(learning_rate=learning_rate)

但是因为采用的类似于固定梯度下降法,即在更新生成器时,判别器参数不动,同理更新判别器时生成器参数不动,所以需要先指定分开训练的时候分别对谁求梯度:

#因为采用了固定梯度下降,所以必须知道每个优化器需要优化什么
gen_var=[weights['gen_hidden1'],weights['gen_out'],biases['gen_hidden1'],biases['gen_out']]
dis_var=[weights['dis_hidden1'],weights['dis_out'],biases['dis_hidden1'],biases['dis_out']]

这样就可以针对性求解了:

#优化
train_gen=optimizer_gen.minimize(gen_loss,var_list=gen_var)
train_dis=optimizer_dis.minimize(dis_loss,var_list=dis_var)

训练模型与保存

#初始化
init=tf.global_variables_initializer()
saver=tf.train.Saver()
input_image,input_label=read_images('./mnist/train_labels.txt',batch_size)
with tf.Session() as sess:
    sess.run(init)
    coord=tf.train.Coordinator()
    tf.train.start_queue_runners(sess=sess,coord=coord)
    for step in range(1,num_steps):
        time_start = time.time()
        batch_x,batch_y=sess.run([input_image,tf.one_hot(input_label,num_class,1,0)])
        z=np.random.uniform(-1.0,1.0,size=(batch_size,noise_dim))
        sess.run([train_gen,train_dis],feed_dict={dis_input:batch_x,gen_input:z})
        if step%1000==0 or step==1:                        
            g_loss,d_loss=sess.run([gen_loss,dis_loss],feed_dict={gen_input:z,dis_input:batch_x})
            time_end=time.time()
            print('step:%i----Generator loss:%f-----Discriminator Loss:%f' %(step,g_loss,d_loss))
    coord.request_stop()
    coord.join()
    print('optimization finished')
    saver.save(sess,'./GAN_mnist_model/GAN_mnist')

我没有训练多少次,有兴趣的可以多训练,最终让判别器的损失接近 0.5 0.5 0.5就说明接近最优解了,我的训练结果:

step:1----Generator loss:0.393680-----Discriminator Loss:1.626469
step:1000----Generator loss:3.580971-----Discriminator Loss:0.078812
step:2000----Generator loss:4.907338-----Discriminator Loss:0.037951
step:3000----Generator loss:5.269949-----Discriminator Loss:0.015779
step:4000----Generator loss:3.202836-----Discriminator Loss:0.119377
step:5000----Generator loss:3.977841-----Discriminator Loss:0.140365
step:6000----Generator loss:3.546029-----Discriminator Loss:0.111060
step:7000----Generator loss:3.723459-----Discriminator Loss:0.099416
step:8000----Generator loss:4.479396-----Discriminator Loss:0.130558
step:9000----Generator loss:4.041896-----Discriminator Loss:0.132201
step:10000----Generator loss:3.873767-----Discriminator Loss:0.241299
step:11000----Generator loss:4.237263-----Discriminator Loss:0.162134
step:12000----Generator loss:3.463274-----Discriminator Loss:0.223905
step:13000----Generator loss:3.941289-----Discriminator Loss:0.261881
step:14000----Generator loss:3.292896-----Discriminator Loss:0.356275
optimization finished

【更新日志】2018-8-27
还是依据论文流程,把判别器的训练放在前面

d_loss,g_loss=sess.run([dis_loss,gen_loss],feed_dict={dis_input:batch_x,gen_input:z})

代码实现-模型调用

还是老套路:

  • 载入模型

    sess=tf.Session()
    new_saver=tf.train.import_meta_graph('./GAN_mnist_model/GAN_mnist.meta')
    new_saver.restore(sess,'./GAN_mnist_model/GAN_mnist')
    
  • 载入运算图

    graph=tf.get_default_graph()
    print(graph.get_all_collection_keys())
    #['generation', 'queue_runners', 'summaries', 'train_op', 'trainable_variables', 'variables']
    
  • 获取预测函数和数据接收接口

    gen=graph.get_collection('generation')
    gen_input=graph.get_tensor_by_name('gen_input:0')
    
  • 随便丢个噪声给生成器

    noise_input=np.random.uniform(-1.0,1.0,size=[1,100])
    g=sess.run(gen,feed_dict={gen_input:noise_input})
    gen_img=g[0]*255.0
    gen_img=gen_img.reshape(28,28)
    plt.imshow(gen_img)
    plt.show()
    

    这里写图片描述

后记

效果貌似不是特别好呢,可能训练次数不是特别够,也可能传统的GAN结构对手写数字的生成能力不够,需要加深层数或者使用更好的GAN变种算法,后续打算再找几个GAN算法研究研究。这里先贴一下这篇博客关于GAN的损失函数的对比
这里写图片描述

训练代码:链接:https://pan.baidu.com/s/12_DNKILTtletYbDDhHDi6Q 密码:vyu7

测试代码:链接:https://pan.baidu.com/s/1rvAKjBnazzKRiL7nPiufVQ 密码:nreb
【更新日志】2018-8-27
从论文来看,我们一般需要先训练判别器,再训练生成器,但是TensorFlow-Examples中给的例子是

_, _, gl, dl = sess.run([train_gen, train_disc, gen_loss, disc_loss],
                                feed_dict=feed_dict)

建议还是改一下:

_, _, gl, dl = sess.run([train_disc,train_gen,  disc_loss, gen_loss],
                                feed_dict=feed_dict)

但是收敛度遇到问题了,目前正在解决。
【更新日志2018-8-29日】
找到未收敛或者收敛程度不好的原因了,要使用AdamOptimizer优化器,不要使用AdagradOptimizer优化器

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风翼冰舟

额~~~CSDN还能打赏了

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值