【生成对抗网络系列】二、DCGAN


参考资料

论文

  Deep Convolution Generative Adversarial Networks

博客

  GANs系列:DCGAN原理简介与基础GAN的区别对比

  【生成对抗网络】Deep Convolution GAN (DCGAN) 详细解读

  基于pytorch的DCGAN代码实现(DCGAN基本原理+代码讲解)

  torch学习 (三十七):DCGAN详解

  DCGAN pytorch代码实现及代码详解


第1章 前言

DCGAN 全称为:Deep Convolution Generative Adversarial Networks(深度卷积生成对抗网络),DCGAN将深度卷积神经网络CNN与生成对抗网络GAN结合用于无监督学习领域
 DCGAN的出发点并不是更改损失函数或者是对GAN的原理进行剖析,而是直接的对网络结构施加限制从而实现更为强大的生成模型,所以我们可以直接将DCGAN的设计方案嵌入到自己的GAN网络中。


第2章 转置卷积(Transposed Convolution)

2.1 前言

转置卷积(Transposed Convolution) 在语义分割或者对抗神经网络(GAN)中比较常见,其主要作用就是做上采样(UpSampling)。

 在有些地方转置卷积又被称作 fractionally-strided convolution或者deconvolution,但 deconvolution 具有误导性,不建议使用。

 对于转置卷积需要注意的是:

  • 转置卷积不是卷积的逆运算
  • 转置卷积也是卷积;

 讲解视频参考:转置卷积(transposed convolution)

 讲解博客参考:转置卷积(Transposed Convolution)


2.2 卷积

 首先回顾下普通卷积,下图以stride=1,padding=0,kernel_size=3为例,假设输入特征图大小是4x4的(假设输入输出都是单通道),通过卷积后得到的特征图大小为2x2。

一般使用卷积的情况中,要么特征图变小(stride > 1),要么保持不变(stride = 1),当然也可以通过四周padding让特征图变大但没有意义。

 关于卷积的详细介绍可以参考博文

在这里插入图片描述


2.3 转置卷积操作

转置卷积主要作用就是起到 上采样 的作用。但转置卷积不是卷积的逆运算(一般卷积操作是不可逆的),它只能恢复到原来的大小(shape/size),数值与原来不同。转置卷积的运算步骤可以归为以下几步:

  • (1)在输入特征图元素间填充 s-1行、列0(其中s表示转置卷积的步距);
  • (2)在输入特征图四周填充 k-p-1 行、列0(其中k表示转置卷积的kernel_size大小,p为转置卷积的padding,注意这里的padding和卷积操作中有些不同);
  • (3)将卷积核参数上下、左右翻转
  • (4)做正常卷积运算(填充0,步距1);

 下面假设输入的特征图大小为2x2(假设输入输出都为单通道),通过转置卷积后得到4x4大小的特征图。这里使用的转置卷积核大小为k=3,stride=1,padding=0的情况(忽略偏执bias)。

  • 首先在元素间填充s-1=0行、列0(等于0不用填充);
  • 然后在特征图四周填充k-p-1=2行、列0;
  • 接着对卷积核参数进行上下、左右翻转;
  • 最后做正常卷积(填充0,步距1);

在这里插入图片描述

 下图展示了转置卷积中不同 sp 的情况:

在这里插入图片描述

 转置卷积操作后特征图的大小可以通过如下公式计算:
H o u t = ( H i n − 1 ) × s t r i d e [ 0 ] − 2 × p a d d i n g [ 0 ] + k e r n e l _ s i z e [ 0 ] W o u t = ( W i n − 1 ) × s t r i d e [ 1 ] − 2 × p a d d i n g [ 1 ] + k e r n e l _ s i z e [ 1 ] H_{out}=(H_{in}−1)×stride[0]−2×padding[0]+kernel\_size[0] \\ W_{out}=(W_{in}−1)×stride[1]−2×padding[1]+kernel\_size[1] Hout=(Hin1)×stride[0]2×padding[0]+kernel_size[0]Wout=(Win1)×stride[1]2×padding[1]+kernel_size[1]

 其中stride[0]表示高度方向的stride,padding[0]表示高度方向的padding,kernel_size[0]表示高度方向的kernel_size,索引[1]都表示宽度方向上的。

 通过上面公式可以看出padding越大,输出的特征矩阵高、宽越小,你可以理解为正向卷积过程中进行了padding然后得到了特征图,现在使用转置卷积还原到原来高、宽后要把之前的padding减掉。


2.4 Pytorch中的转置卷积

Pytorch官方关于转置卷积ConvTranspose2d的文档

 官方对转置卷积使用到的参数介绍:

在这里插入图片描述

 上面讲的例子中已经介绍了in_channels, out_channels, kernel_size, stride, padding这几个参数了,在官方提供的方法中还有:

  • output_padding:在计算得到的输出特征图的高、宽方向各填充几行或列0(注意,这里只是在上下以及左右的一侧one side填充,并不是两侧都填充,有兴趣自己做个实验看下),默认为0不使用。
  • groups:当使用到组卷积时才会用到的参数,默认为1即普通卷积。
  • bias:是否使用偏执bias,默认为True使用。
  • dilation:当使用到空洞卷积(膨胀卷积)时才会使用到的参数,默认为1即普通卷积。

 输出特征图宽、高计算:

  • Input: ( N , C i n , H i n , W i n )   o r   ( C i n , H i n , W i n ) (N, C_{in}, H_{in}, W_{in})\ or\ (C_{in}, H_{in}, W_{in}) (N,Cin,Hin,Win) or (Cin,Hin,Win)
  • Output: ( N , C o u t , H o u t , W o u t )   o r   ( C o u t , H o u t , W o u t ) (N, C_{out}, H_{out}, W_{out})\ or\ (C_{out}, H_{out}, W_{out}) (N,Cout,Hout,Wout) or (Cout,Hout,Wout)

H o u t = ( H i n − 1 ) × s t r i d e [ 0 ] − 2 × p a d d i n g [ 0 ] + d i l a t i o n [ 0 ] × ( k e r n e l _ s i z e [ 0 ] − 1 ) + o u t p u t _ p a d d i n g [ 0 ] + 1 W o u t = ( W i n − 1 ) × s t r i d e [ 1 ] − 2 × p a d d i n g [ 1 ] + d i l a t i o n [ 1 ] × ( k e r n e l _ s i z e [ 1 ] − 1 ) + o u t p u t _ p a d d i n g [ 1 ] + 1 H_{out}=(H_{in}−1)×stride[0]−2×padding[0]+dilation[0]×(kernel\_size[0]−1)+output\_padding[0]+1 \\ W_{out}=(W_{in}-1)×stride[1]−2×padding[1]+dilation[1]×(kernel\_size[1]−1)+output\_padding[1]+1 Hout=(Hin1)×stride[0]2×padding[0]+dilation[0]×(kernel_size[0]1)+output_padding[0]+1Wout=(Win1)×stride[1]2×padding[1]+dilation[1]×(kernel_size[1]1)+output_padding[1]+1


第3章 DCGAN网络架构

3.1 网络改进

 DCGAN主要是在网络架构上改进了原始GAN,DCGAN的生成器与判别器都利用CNN架构替换了原始GAN的全连接网络,主要改进之处有如下几个方面:

  • (1)全卷积网络(all convolutional net):用步幅卷积(strided convolutions)替代确定性空间池化函数(deterministic spatial pooling functions)(比如最大池化),让网络自己学习下采样方式。作者对 GeneratorDiscriminator 都采用了这种方法。

  • (2)取消全连接层: 比如,使用 全局平均池化(global average pooling)替代 fully connected layerGAP 会降低收敛速度,但是可以提高模型的稳定性。

  • (3)批归一化(Batch Normalization)BN 被证明是深度学习中非常重要的 加速收敛减缓过拟合 的手段,这样有助于解决 poor initialization 问题并帮助梯度流向更深的网络。

 但是实践表明,将所有层都进行Batch Normalization,会导致样本震荡和模型不稳定,因此 只对生成器(G)的输出层和鉴别器(D)的输入层使用BN

  • (4)Generator 中除输出层使用 Tanh 激活函数,其余层全部使用 ReLu 激活函数
  • (5)Discriminator 所有层都使用 LeakyReLU 激活函数

 概括来说,DCGAN的主要的tricks如下图所示:

在这里插入图片描述


原论文中的DCGAN生成器 G 的结构如下图所示:

原文中生成器的输入是一个 100 100 100 维的噪声,中间会通过 4 4 4 层卷积层(转置卷积),每通过一个卷积层通道数减半,长宽扩大一倍 ,最终产生一个 64 × 64 × 3 64\times64\times3 64×64×3 大小的图片输出。

在这里插入图片描述


3.2 训练参数

  • 所有模型均采用小批量随机梯度下降(SGD)训练,Batch_size大小为128。
  • 所有权重均根据零中心正态分布进行初始化,标准偏差为0.02。
  • 在LeakyReLU中,斜率均设置为0.2。
  • 使用了Adam优化器并调整超参数。基础GAN中建议的学习率0.001太高了,改为使用0.0002。
  • 此外,将动量项β1保留在建议值0.9会导致训练振荡和不稳定性,而将其降低到0.5有助于稳定训练。

第4章 Pytorch实现DCGAN

参考

  PyTorch-GAN


 首先附上一张GAN网络的结构图,DCGAN结构也一样,只是内部细节不一样。

在这里插入图片描述
DCGAN的结构如下:

在这里插入图片描述


4.1 生成器Generator

 这里使用的是手写数据集Mnist,图片大小resize成 32 × 32 × 1 32\times32\times1 32×32×1,DCGAN的生成器部分采用CNN架构,并且使用转置卷积,输入噪声为100维的向量,输出为 32 × 32 × 1 32\times32\times1 32×32×1 的图片,具体网络结构如下所示:

在这里插入图片描述

 对照着代码看比较清晰:

"""
    1.生成器Generator
"""
class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()

        # Python中两个斜杠即双斜杠(//)表示地板除,即先做除法(/),然后向下取整(floor)
        # opt.img_size = 32
        # init_size= 8
        self.init_size = opt.img_size // 4
        self.l1 = nn.Sequential(
            # opt.latent_dim是噪声维度,值为100
            # 128 * self.init_size ** 2 = 128x64 = 8192
            nn.Linear(opt.latent_dim, 128 * self.init_size ** 2),
        )

        self.conv_blocks = nn.Sequential(
            nn.BatchNorm2d(128),

            # (128, 8, 8) -> (128, 16, 16)
            nn.Upsample(scale_factor=2),    # 上采样,将图片放大两倍
            # (128, 16, 16) -> (128, 16, 16)
            nn.Conv2d(128, 128, 3, stride=1, padding=1),
            nn.BatchNorm2d(128, 0.8),
            nn.LeakyReLU(0.2, inplace=True),

            # (128, 16, 16) -> (128, 32, 32)
            nn.Upsample(scale_factor=2),
            # (128, 32, 32) -> (64, 32, 32)
            nn.Conv2d(128, 64, 3, stride=1, padding=1),
            nn.BatchNorm2d(64, 0.8),
            nn.LeakyReLU(0.2, inplace=True),

            # (64, 32, 32) -> (1, 32, 32)
            nn.Conv2d(64, opt.channels, 3, stride=1, padding=1),
            nn.Tanh(),
        )

    def forward(self, z):
        # l1函数进行的是Linear变换
        out = self.l1(z)
        # view是维度变换函数,可以看到out数据变成了四维数据:
        # (batch,8192) -> (batch, 128, 8, 8)
        out = out.view(out.shape[0], 128, self.init_size, self.init_size)
        img = self.conv_blocks(out)
        return img

4.2 判别器Discriminator

在这里插入图片描述

"""
    2.判别器Discriminator
"""
class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()

        def discriminator_block(in_filters, out_filters, bn=True):
            # Conv卷积,Relu激活,Dropout将部分神经元失活,进而防止过拟合
            block = [nn.Conv2d(in_filters, out_filters, 3, 2, 1),
                     nn.LeakyReLU(0.2, inplace=True),
                     nn.Dropout2d(0.25)]
            # 如果bn这个参数为True,那么就需要在block块里面添加上BatchNorm的归一化函数
            if bn:
                block.append(nn.BatchNorm2d(out_filters, 0.8))
            return block

        self.model = nn.Sequential(
            # (batch, 1, 32, 32) -> (batch, 16, 16, 16),输入层不使用BN
            *discriminator_block(opt.channels, 16, bn=False),
            # (batch, 16, 16, 16) -> (batch, 32, 8, 8)
            *discriminator_block(16, 32),
            # (batch, 32, 8, 8) -> (batch, 64, 4, 4)
            *discriminator_block(32, 64),
            # (batch, 64, 4, 4) -> (batch, 128, 2, 2)
            *discriminator_block(64, 128)
        )

        # opt.img_size = 32
        # ds_size= 32 / 16 = 2
        ds_size = opt.img_size // 2 ** 4
        self.adv_layer = nn.Sequential(
            nn.Linear(128 * ds_size ** 2, 1),   # (batch, 512) -> (batch, 1)
            nn.Sigmoid())

    def forward(self, img):
        out = self.model(img)               # (batch, 1, 32, 32) -> (batch, 128, 2, 2)
        out = out.view(out.shape[0], -1)    # (batch, 128, 2, 2) -> (batch, 512)
        validity = self.adv_layer(out)      # (batch, 512) -> (batch, 1)
        return validity

4.3 完整代码

import argparse
import os
import numpy as np
from tqdm.autonotebook import tqdm

import torchvision.transforms as transforms
from torchvision.utils import save_image

from torch.utils.data import DataLoader
from torchvision import datasets
from torch.autograd import Variable

import torch.nn as nn
import torch.nn.functional as F
import torch

os.makedirs("images", exist_ok=True)

# 命令行选项、参数和子命令解析器
parser = argparse.ArgumentParser()
parser.add_argument("--n_epochs", type=int, default=200, help="迭代次数")
parser.add_argument("--batch_size", type=int, default=64, help="batch大小")
parser.add_argument("--lr", type=float, default=0.0002, help="adam: 学习率")
parser.add_argument("--b1", type=float, default=0.5, help="adam: 动量梯度下降第一个参数")
parser.add_argument("--b2", type=float, default=0.999, help="adam: 动量梯度下降第二个参数")
parser.add_argument("--n_cpu", type=int, default=8, help="CPU个数")
parser.add_argument("--latent_dim", type=int, default=100, help="噪声数据生成维度")
parser.add_argument("--img_size", type=int, default=32, help="输入数据的维度")
parser.add_argument("--channels", type=int, default=1, help="输出数据的通道数")
parser.add_argument("--sample_interval", type=int, default=400, help="保存图像的迭代数")
opt = parser.parse_args()
print(opt)

# 判断GPU可用,有GPU用GPU,没有用CPU
cuda = True if torch.cuda.is_available() else False

"""
    自定义初始化参数
"""
def weights_init_normal(m):
    classname = m.__class__.__name__   # 获得类名
    if classname.find("Conv") != -1:   # 在类classname中检索到了Conv
        torch.nn.init.normal_(m.weight.data, 0.0, 0.02)
    elif classname.find("BatchNorm2d") != -1:
        torch.nn.init.normal_(m.weight.data, 1.0, 0.02)
        torch.nn.init.constant_(m.bias.data, 0.0)

"""
    1.生成器Generator
"""
class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()

        # Python中两个斜杠即双斜杠(//)表示地板除,即先做除法(/),然后向下取整(floor)
        # opt.img_size = 32
        # init_size= 8
        self.init_size = opt.img_size // 4
        self.l1 = nn.Sequential(
            # opt.latent_dim是噪声维度,值为100
            # 128 * self.init_size ** 2 = 128x64 = 8192
            nn.Linear(opt.latent_dim, 128 * self.init_size ** 2),
        )

        self.conv_blocks = nn.Sequential(
            nn.BatchNorm2d(128),

            # (128, 8, 8) -> (128, 16, 16)
            nn.Upsample(scale_factor=2),    # 上采样,将图片放大两倍
            # (128, 16, 16) -> (128, 16, 16)
            nn.Conv2d(128, 128, 3, stride=1, padding=1),
            nn.BatchNorm2d(128, 0.8),
            nn.LeakyReLU(0.2, inplace=True),

            # (128, 16, 16) -> (128, 32, 32)
            nn.Upsample(scale_factor=2),
            # (128, 32, 32) -> (64, 32, 32)
            nn.Conv2d(128, 64, 3, stride=1, padding=1),
            nn.BatchNorm2d(64, 0.8),
            nn.LeakyReLU(0.2, inplace=True),

            # (64, 32, 32) -> (1, 32, 32)
            nn.Conv2d(64, opt.channels, 3, stride=1, padding=1),
            nn.Tanh(),
        )

    def forward(self, z):
        # l1函数进行的是Linear变换
        out = self.l1(z)
        # view是维度变换函数,可以看到out数据变成了四维数据:
        # (batch,8192) -> (batch, 128, 8, 8)
        out = out.view(out.shape[0], 128, self.init_size, self.init_size)
        img = self.conv_blocks(out)
        return img

"""
    2.判别器Discriminator
"""
class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()

        def discriminator_block(in_filters, out_filters, bn=True):
            # Conv卷积,Relu激活,Dropout将部分神经元失活,进而防止过拟合
            block = [nn.Conv2d(in_filters, out_filters, 3, 2, 1),
                     nn.LeakyReLU(0.2, inplace=True),
                     nn.Dropout2d(0.25)]
            # 如果bn这个参数为True,那么就需要在block块里面添加上BatchNorm的归一化函数
            if bn:
                block.append(nn.BatchNorm2d(out_filters, 0.8))
            return block

        self.model = nn.Sequential(
            # (batch, 1, 32, 32) -> (batch, 16, 16, 16),输入层不使用BN
            *discriminator_block(opt.channels, 16, bn=False),
            # (batch, 16, 16, 16) -> (batch, 32, 8, 8)
            *discriminator_block(16, 32),
            # (batch, 32, 8, 8) -> (batch, 64, 4, 4)
            *discriminator_block(32, 64),
            # (batch, 64, 4, 4) -> (batch, 128, 2, 2)
            *discriminator_block(64, 128)
        )

        # opt.img_size = 32
        # ds_size= 32 / 16 = 2
        ds_size = opt.img_size // 2 ** 4
        self.adv_layer = nn.Sequential(
            nn.Linear(128 * ds_size ** 2, 1),   # (batch, 512) -> (batch, 1)
            nn.Sigmoid())

    def forward(self, img):
        out = self.model(img)               # (batch, 1, 32, 32) -> (batch, 128, 2, 2)
        out = out.view(out.shape[0], -1)    # (batch, 128, 2, 2) -> (batch, 512)
        validity = self.adv_layer(out)      # (batch, 512) -> (batch, 1)
        return validity


# BCE损失函数
adversarial_loss = torch.nn.BCELoss()

# Initialize generator and discriminator
generator = Generator()
discriminator = Discriminator()

if cuda:                                #初始化,将数据放在cuda上
    generator.cuda()
    discriminator.cuda()
    adversarial_loss.cuda()

# Initialize weights
generator.apply(weights_init_normal)
discriminator.apply(weights_init_normal)

# Configure data loader
os.makedirs("../../data/mnist", exist_ok=True)
dataloader = torch.utils.data.DataLoader(     #显卡加速
    datasets.MNIST(
        "../../data/mnist",                  #进行训练集下载
        train=True,
        download=True,
        transform=transforms.Compose(
            [transforms.Resize(opt.img_size), transforms.ToTensor(), transforms.Normalize([0.5], [0.5])]
        ),
    ),
    batch_size=opt.batch_size,
    shuffle=True,
)

# 定义神经网络的优化器
optimizer_G = torch.optim.Adam(generator.parameters(), lr=opt.lr, betas=(opt.b1, opt.b2))
optimizer_D = torch.optim.Adam(discriminator.parameters(), lr=opt.lr, betas=(opt.b1, opt.b2))

Tensor = torch.cuda.FloatTensor if cuda else torch.FloatTensor

# ----------
#  Training
# ----------
for epoch in range(1, opt.n_epochs):
    loop = tqdm(dataloader, colour='red', unit='img')
    for i, (imgs, _) in enumerate(loop):

        # Adversarial ground truths
        # 因为 batch 是64,所以在这里 imgs.size(0) = 64
        # 这两句话本质就是为64张真图片和64张假图片打上标签,真图片标签为1,假图片标签为0
        valid = Variable(Tensor(imgs.size(0), 1).fill_(1.0), requires_grad=False)
        fake = Variable(Tensor(imgs.size(0), 1).fill_(0.0), requires_grad=False)

        # Configure input
        real_imgs = Variable(imgs.type(Tensor))

        # -----------------
        #  训练 Generator
        # -----------------

        # 梯度归零
        optimizer_G.zero_grad()

        # 产生输入噪声,维度为latent_dim
        z = Variable(Tensor(np.random.normal(0, 1, (imgs.shape[0], opt.latent_dim))))

        # 把噪声输入生成器,生成fake图像
        gen_imgs = generator(z)

        # 计算生成器的损失
        g_loss = adversarial_loss(discriminator(gen_imgs), valid)

        g_loss.backward()   # 反向传播
        optimizer_G.step()  # 更新参数

        # ---------------------
        #  训练 Discriminator
        # ---------------------

        optimizer_D.zero_grad()

        # 估计判别器的判别能力
        real_loss = adversarial_loss(discriminator(real_imgs), valid)   # 判别真实数据时的损失
        fake_loss = adversarial_loss(discriminator(gen_imgs.detach()), fake)    # 判别假数据时的损失
        d_loss = (real_loss + fake_loss) / 2    # 做平均

        d_loss.backward()
        optimizer_D.step()

        # print(
        #     "[Epoch %d/%d] [Batch %d/%d] [D loss: %f] [G loss: %f]"
        #     % (epoch, opt.n_epochs, i, len(dataloader), d_loss.item(), g_loss.item())
        # )

        # 进度条参数
        loop.set_description(f"Epoch [{epoch}/{opt.n_epochs}]")
        loop.set_postfix(D_loss=d_loss.item(), G_loss=g_loss.item())

        batches_done = epoch * len(dataloader) + i
        if batches_done % opt.sample_interval == 0:
            save_image(gen_imgs.data[:25], "images/%d.png" % batches_done, nrow=5, normalize=True)
  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
DCGAN(Deep Convolutional Generative Adversarial Network)是一种生成对抗网络GAN)的变体,它包括了卷积神经网络(CNN),并且已经成功地应用于图像生成任务中。 DCGAN 的主要思想是使用 CNN 作为生成器和辨别器模型的基础。生成器通过反向卷积(也称为转置卷积)操作将随机噪声数据转换为具有特定特征的图像。辨别器则通过卷积操作来判断输入数据是否为真实图像。 在训练过程中,生成器和辨别器通过对抗的方式相互学习。生成器尝试生成越来越逼真的图像,而辨别器则尝试区分真实图像和生成图像之间的差异。这种对抗训练的结果是生成器的输出逐渐变得更加真实,同时辨别器变得更加准确。 以下是 DCGAN 的基本架构: 1. 生成器:在 DCGAN 中,生成器使用反向卷积操作将随机噪声转换为图像。它通常由几个反向卷积层和规范化层组成,每个层都带有 ReLU 激活函数。 2. 辨别器:辨别器是一个 CNN,它接收真实图像和生成图像,并尝试区分它们之间的差异。它通常由几个卷积层和池化层组成,每个层都带有 LeakyReLU 激活函数。 3. 对抗训练:在训练过程中,生成器和辨别器通过对抗的方式相互学习。生成器尝试生成越来越逼真的图像,而辨别器则尝试区分真实图像和生成图像之间的差异。 4. 损失函数:在 DCGAN 中,使用元交叉熵作为损失函数。对于生成器和辨别器,都需要计算元交叉熵损失,并且将其最小化。 DCGAN 已经被成功地应用于图像生成任务中,例如生成手写数字、人脸、自然景观等。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

travellerss

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值