基于LeNet5实现手写数字识别,可视化卷积层。

LeNet5

CNN卷积网络的发展史

1. LetNet5(1998)
2. AlexNet(2012)
3. ZFNet(2013)
4. VGGNet(2014)
5. GoogLeNet(2014)
6. ResNet(2015)
7. DenseNet(2017)
8. EfficientNet(2019)
9. Vision Transformers(2020)
10. 自适应卷积网络(2021)

上面列出了发展到现在CNN的一些经典的网络模型,我将持续不断更新学习上述神经网络的笔记。共勉!
当我开始学现有的神经网络的时候我总是在想,卷积层到底是越多越好呢,还是越少越好?他们是怎么确保自己构建的神经网络模型能够达到最优的?非常好奇每一次卷积之后的图像变成了什么样子。这是本人自己学习LetNet5时的一些笔记,希望对大家有所帮助。并且也希望可以在后续学习中,逐步找到答案。也希望大佬们可以给我一些建议。在笔记最后会有一句话,希望可以帮助到大家。

这里是原论文地址:Gradient-Based Learning Applied to Document Recognition
接下里和你一起详解论文!

这里我就不去详解论文了,有感兴趣的可以点击这个来理解该论文Gradient-Based Learning Applied to Document Recognition

该论文的主要内容是以字符识别为例,证明使用基于梯度的反向传播训练的多层神经网络优于手动提取特征的识别算法

目录:

这里我会讲解一些个人觉得比较重要的部分,并且会给出一些代码,方便大家理解。

  • 梯度是如何计算的?
  • 以及他在更新参数中是起到什么作用?

1. 梯度下降

假如你有一个线性模型 f w , b ( x ( i ) ) f_{w,b}(x^{(i)}) fw,b(x(i)):
f w , b ( x ( i ) ) = w x ( i ) + b (1) f_{w,b}(x^{(i)}) = wx^{(i)} + b \tag{1} fw,b(x(i))=wx(i)+b(1)
在线性回归中,通过最小化我们的预测 f w , b ( x ( i ) ) f_{w,b}(x^{(i)}) fw,b(x(i))和实际数据 y ( i ) y^{(i)} y(i)之间的误差来利用输入训练数据来拟合参数 w w w b b b。这个度量被称为代价函数$ J(w,b) 。在训练中,你测量所有训练样本的代价函数 。在训练中,你测量所有训练样本的代价函数 。在训练中,你测量所有训练样本的代价函数x{(i)},y{(i)}$
J ( w , b ) = 1 2 m ∑ i = 0 m − 1 ( f w , b ( x ( i ) ) − y ( i ) ) 2 (2) J(w,b) = \frac{1}{2m} \sum\limits_{i = 0}^{m-1} (f_{w,b}(x^{(i)}) - y^{(i)})^2\tag{2} J(w,b)=2m1i=0m1(fw,b(x(i))y(i))2(2)

当所有预测值和实际值之间的误差 J ( w , b ) J(w,b) J(w,b)最小时,模型拟合层度越好,参数 w w w b b b就达到了最优值。

梯度下降如下所示:

repeat  until convergence:    {    w = w − α ∂ J ( w , b ) ∂ w    b = b − α ∂ J ( w , b ) ∂ b } \begin{align*} \text{repeat}&\text{ until convergence:} \; \lbrace \newline \; w &= w - \alpha \frac{\partial J(w,b)}{\partial w} \tag{3} \; \newline b &= b - \alpha \frac{\partial J(w,b)}{\partial b} \newline \rbrace \end{align*} repeatwb} until convergence:{=wαwJ(w,b)=bαbJ(w,b)(3)
其中,参数 w w w b b b同时更新。
梯度定义为:
∂ J ( w , b ) ∂ w = 1 m ∑ i = 0 m − 1 ( f w , b ( x ( i ) ) − y ( i ) ) x ( i ) ∂ J ( w , b ) ∂ b = 1 m ∑ i = 0 m − 1 ( f w , b ( x ( i ) ) − y ( i ) ) \begin{align} \frac{\partial J(w,b)}{\partial w} &= \frac{1}{m} \sum\limits_{i = 0}^{m-1} (f_{w,b}(x^{(i)}) - y^{(i)})x^{(i)} \tag{4}\\ \frac{\partial J(w,b)}{\partial b} &= \frac{1}{m} \sum\limits_{i = 0}^{m-1} (f_{w,b}(x^{(i)}) - y^{(i)}) \tag{5}\\ \end{align} wJ(w,b)bJ(w,b)=m1i=0m1(fw,b(x(i))y(i))x(i)=m1i=0m1(fw,b(x(i))y(i))(4)(5)

在学习过程中我们总会好奇电脑是怎么找到最优解的,上述计算的过程就可以帮助我们实现,在高中我们我们就学过斜率是函数变化的方向放到模型传播中如何理解呢?

模型前向传播得到输出值通过反向传播计算梯度,通过不断重复,让曲线或者点不断的按照我们预期的方向移动。直到梯度为0,就达到了最优解。
这里大家可以想象自己就是一个点,你想要实现一个目标,神经网路喂入了一个值,该值是你要达到的方向,然后该值经过前向传播,反向传播后得到梯度(斜率)就是给你的指引。

graph LR
A[你] -->|1. 神经网路喂入一个值,前向传播| B[目标(输出值)]
B -->|2. 反向传播得到梯度(指引,思考方式)| A
A -->|3. 重复1,2步骤(又喂入一个值,前向传播,反向传播)直到梯度为0(学习到实现目标最优的方法)| B

2. CNN

我们都知道卷积的主要思想是:通过卷积核在图片上滑动,通过小区域的多次卷积,最后组合,这样子不论图片经过旋转,拉伸等操作后,都可以识别出两张图片是否一样,并且可以通过下采样(池化)缩小图片尺寸,减小计算量,最后通过全连接层进行分类。但是我很好奇:

  1. 卷积之后提取的图片是什么样的?
  2. 为什么LeNet5是7层网络,如果少一层会差多少,多一层又会怎样?

接下来我们通过代码来进行可视化分析!

3. LeNet5网络结构

在这里插入图片描述

这里我只给出LetNet5的网络结构图并不做详解,在此篇博客中有详细的讲解大家可以点击这里

在这里插入图片描述

接下来我会用代码实现LetNet5神经网络,并且可视化每一层卷积之后得到的图像。希望可视化之后可以让大家对CNN这个抽象的概念有更清晰的理解。

这里推荐一个可视化卷积的网站感兴趣的可以点击此处进行访问卷积可视化.该网站可供我们查看每一层卷积的图像,以及卷积过程和参数。

4. 手写数字识别代码实现

(1) 代码实现

import torch
import torchvision
from torch.utils.data import DataLoader
import torch.nn as nn
from torchsummary import summary
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
1.1 数据预处理
# 数据预处理
# 1. 将图片转换为Tensor
transform = torchvision.transforms.Compose([torchvision.transforms.ToTensor()])
# 2. 加载数据集
train_data = torchvision.datasets.MNIST('./data',train=True,transform=transform,download=True)
test_data = torchvision.datasets.MNIST('./data',train=False,transform=transform,download=True)
# 3. 创建数据加载器
train_dataloder = DataLoader(train_data,batch_size=64)
test_dataloder = DataLoader(test_data,batch_size=64)
# 4. 计算数据集大小
len_train_data = len(train_data)
len_test_data = len(test_data)

print(len_train_data,len_test_data)
60000 10000
1.2 构建网络模型

下面我们编写LetNet5的网络结构,两个卷积层,两个池化层,三个全连接层。

(1) LeNet5网络结构
class MY_LeNet5(nn.Module):
    def __init__(self):
        super(MY_LeNet5,self).__init__()
        self.net = nn.Sequential(

            nn.Conv2d(in_channels=1,out_channels=6,kernel_size=5,padding=2),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2,stride=2),

            nn.Conv2d(6,16,5),
            nn.ReLU(),
            nn.MaxPool2d(2,2),
            nn.Flatten(),
            
            nn.Linear(16*5*5,120),
            nn.ReLU(),
            
            nn.Linear(120,84),
            nn.ReLU(),
            
            nn.Linear(84,10)
            
            
            )
        
        
    def forward(self,x):
        return self.net(x)
(2)自建网络模型结构

我们往Lenet5中加入一层卷积核为5*5的卷积层层看看变化

class My_self_Lenet(nn.Module):
    def __init__(self):
        super(My_self_Lenet,self).__init__()
        self.net = nn.Sequential(
            # Reshape(),
            nn.Conv2d(in_channels=1,out_channels=6,kernel_size=5,padding=2),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2,stride=2),

            nn.Conv2d(6,16,5),
            nn.ReLU(),
            nn.MaxPool2d(2,2),

            nn.Conv2d(16,32,5),
            nn.ReLU(),
            nn.Flatten(),

            nn.Linear(32,120),
            nn.ReLU(),

            nn.Linear(120,84),
            nn.ReLU(),

            nn.Linear(84,10)

        )

    def forward(self,x):
        return self.net(x)
1.2.1 查看网络模型结构

我们可以从以下两个方面来评估模型,分别是:

  • 模型复杂度:
  1. 计算量
  2. 参数
  3. 显存占用
  • 模型性能:
  1. 准确率
  2. 精确度
  3. 召回率
  4. F1分数
  5. MSE
  6. R 2 R^2 R2分数

本次实验值采用了准确率来衡量模型性能,在后续实验中我们将会使用上述方法中的多个来衡量模型性能。下面我们将采用summary来查看网络模型的参数,模型大小

loss = nn.CrossEntropyLoss()
loss = loss.to(device)

model_LeNet = MY_LeNet5().to(device)
summary(model_LeNet,(1,28,28))
optimizer = torch.optim.Adam(model_LeNet.parameters(),lr=0.001)

----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Conv2d-1            [-1, 6, 28, 28]             156
              ReLU-2            [-1, 6, 28, 28]               0
         MaxPool2d-3            [-1, 6, 14, 14]               0
            Conv2d-4           [-1, 16, 10, 10]           2,416
              ReLU-5           [-1, 16, 10, 10]               0
         MaxPool2d-6             [-1, 16, 5, 5]               0
           Flatten-7                  [-1, 400]               0
            Linear-8                  [-1, 120]          48,120
              ReLU-9                  [-1, 120]               0
           Linear-10                   [-1, 84]          10,164
             ReLU-11                   [-1, 84]               0
           Linear-12                   [-1, 10]             850
================================================================
Total params: 61,706
Trainable params: 61,706
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.00
Forward/backward pass size (MB): 0.11
Params size (MB): 0.24
Estimated Total Size (MB): 0.35
----------------------------------------------------------------
1.3 训练模型

epoch = 10 
total_train_step = 0
total_train_acc =0
for i in range(epoch):
    print("----------LeNet5第{}轮训练开始----------".format(i+1))
    for data in train_dataloder:
        img,target = data
        img = img.to(device)
        target = target.to(device)
        output = model_LeNet(img)
        loss_value = loss(output,target)

        optimizer.zero_grad()
        loss_value.backward()
        optimizer.step()
        
        total_train_step += 1

        acc = (output.argmax(dim=1) == target).sum()
        total_train_acc += acc
        if total_train_step % 100 == 0:
            print("训练次数:{:d},Loss:{:f}".format(total_train_step,loss_value.item()))
            # 保存模型
            torch.save(model_LeNet.state_dict(),"./model/LeNet5.pth")

print("准确率:",total_train_acc/len_train_data)
----------LeNet5第1轮训练开始----------
训练次数:100,Loss:0.098323
训练次数:200,Loss:0.001862
训练次数:300,Loss:0.002084
训练次数:400,Loss:0.102222
训练次数:500,Loss:0.032754
训练次数:600,Loss:0.053640
...
----------LeNet5第10轮训练开始----------
训练次数:8500,Loss:0.038482
训练次数:8600,Loss:0.131152
训练次数:8700,Loss:0.002011
训练次数:8800,Loss:0.000486
训练次数:8900,Loss:0.000392
训练次数:9000,Loss:0.040055
训练次数:9100,Loss:0.000649
训练次数:9200,Loss:0.010035
训练次数:9300,Loss:0.007745
准确率: tensor(9.9402, device='cuda:0')
1.4 测试模型
model_LeNet.eval()

total_test_acc = 0
with torch.no_grad():
    for data in test_dataloder:
        img,target = data
        img = img.to(device)
        target = target.to(device)
        output = model_LeNet(img)
        loss_value = loss(output,target)
        
        acc = (output.argmax(dim=1) == target).sum()
        total_test_acc += acc
        
print("准确率:",total_test_acc/len_test_data)
准确率: tensor(0.9888, device='cuda:0')
1.2.2 查看自建网络模型的结构
loss = nn.CrossEntropyLoss()
loss = loss.to(device)

model_myself = My_self_Lenet().to(device)
summary(model_myself,(1,28,28))

optimizer = torch.optim.Adam(model_myself.parameters(),lr=0.001)

----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Conv2d-1            [-1, 6, 28, 28]             156
              ReLU-2            [-1, 6, 28, 28]               0
         MaxPool2d-3            [-1, 6, 14, 14]               0
            Conv2d-4           [-1, 16, 10, 10]           2,416
              ReLU-5           [-1, 16, 10, 10]               0
         MaxPool2d-6             [-1, 16, 5, 5]               0
            Conv2d-7             [-1, 32, 1, 1]          12,832
              ReLU-8             [-1, 32, 1, 1]               0
           Flatten-9                   [-1, 32]               0
           Linear-10                  [-1, 120]           3,960
             ReLU-11                  [-1, 120]               0
           Linear-12                   [-1, 84]          10,164
             ReLU-13                   [-1, 84]               0
           Linear-14                   [-1, 10]             850
================================================================
Total params: 30,378
Trainable params: 30,378
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.00
Forward/backward pass size (MB): 0.11
Params size (MB): 0.12
Estimated Total Size (MB): 0.23
----------------------------------------------------------------

通过summary我们可以发现,增加一层卷积层,我们可以减少参数数量,减少模型的大小。虽说有14层网络但是模型大小却比LeNet5小。

1.3 训练模型(自建模型)

epoch = 10 
total_train_step = 0
total_train_acc =0
for i in range(epoch):
    print("----------第{}轮训练----------".format(i+1))
    for data in train_dataloder:
        img,target = data
        img = img.to(device)
        target = target.to(device)
        output = model_myself(img)
        loss_value = loss(output,target)

        optimizer.zero_grad()
        loss_value.backward()
        optimizer.step()
        
        total_train_step += 1

        acc = (output.argmax(dim=1) == target).sum()
        total_train_acc += acc
        if total_train_step % 100 == 0:
            print("训练次数:{:d},Loss:{:f}".format(total_train_step,loss_value.item()))
            # 保存模型
            torch.save(model_myself.state_dict(),"./model/self_LeNet.pth")

print("准确率:",total_train_acc/len_train_data)
----------第1轮训练----------
训练次数:100,Loss:0.456620
训练次数:200,Loss:0.506838
训练次数:300,Loss:0.346417
训练次数:400,Loss:0.266719
训练次数:500,Loss:0.208805
训练次数:600,Loss:0.300649
训练次数:700,Loss:0.199465
...
----------第10轮训练----------
训练次数:8500,Loss:0.022954
训练次数:8600,Loss:0.044531
训练次数:8700,Loss:0.000516
训练次数:8800,Loss:0.003007
训练次数:8900,Loss:0.012397
训练次数:9000,Loss:0.007868
训练次数:9100,Loss:0.000951
训练次数:9200,Loss:0.044759
训练次数:9300,Loss:0.060102
准确率: tensor(9.7456, device='cuda:0')
1.4 测试模型(自建模型)
#测试模型
model_myself.eval()

total_test_acc = 0
with torch.no_grad():
    for data in test_dataloder:
        img,target = data
        img = img.to(device)
        target = target.to(device)
        output = model_myself(img)
        loss_value = loss(output,target)

        acc = (output.argmax(dim=1) == target).sum()
        total_test_acc += acc

print("准确率:",total_test_acc/len_test_data)
准确率: tensor(0.9866, device='cuda:0')

通过准确率我们可以发现,LeNet5的准确率为0.9888,而自建网络模型的准确率为0.9866。差了一点点并不多

1.5 预测模型
import torch
import torchvision.models as models
import matplotlib.pyplot as plt
from PIL import Image
from torchvision import transforms
import torch.nn.functional as F
input_image = Image.open('./image/5.jpg')
plt.imshow(input_image)

在这里插入图片描述


preprocess = transforms.Compose([
    transforms.Grayscale(),
    transforms.Resize((28,28)),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.48], std=[0.229]),
])

input_tensor = preprocess(input_image)
image = input_tensor.unsqueeze(0) # create a mini-batch as expected by the model
image = image.to(device)

model_myself_pr = My_self_Lenet()

#将model_myself_pr加载到GPU上
model_myself_pr.to(device)
model_myself_pr.load_state_dict(torch.load("./model/self_LeNet.pth"))

#预测图片是否跟结果一致
output = model_myself_pr(image)
print(output.data)
_, predicted = torch.max(output.data, 1)

print(predicted)
tensor([[  3.4511, -13.5221, -12.3029,  -3.7890, -28.5683,  14.7039,  18.2915,
         -36.7622,  -5.3393, -11.0205]], device='cuda:0')
tensor([6], device='cuda:0')
input_image = Image.open('./image/5.jpg')

#数据预处理(1. 将图片转换为灰度图;2. 将图片大小调整为28*28;3. 将图片转换为Tensor;4. 对Tensor进行归一化)
preprocess = transforms.Compose([
    transforms.Grayscale(),
    transforms.Resize((28,28)),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.48], std=[0.229]),
])



input_tensor = preprocess(input_image)
image = input_tensor.unsqueeze(0) # create a mini-batch as expected by the model
image = image.to(device)
print(image.shape)

model_LeNet_pr = MY_LeNet5()
#将model2加载到GPU上
model_LeNet_pr.to(device)
model_LeNet_pr.load_state_dict(torch.load("./model/LeNet5.pth"))

#预测图片是否跟结果一致
output2 = model_LeNet_pr(image)
print(output2.data)
_, predicted2 = torch.max(output2.data, 1)

print(predicted2)
torch.Size([1, 1, 28, 28])
tensor([[  0.4604, -40.1675, -32.2241, -22.7404, -30.2796,  47.7085,  14.1403,
         -44.4054,   4.5876, -10.1606]], device='cuda:0')
tensor([5], device='cuda:0')

使用两个神经网络模型预测同一张图片数字"5",自建模型错误的预测为了6,而LeNet5模型正确的预测为了5。哈哈哈哈看来增加一层卷积层过度的提取特征并不能帮助模型更好的预测。

这里大家可以自行在网络上下载图片用来预测,重复多次看看哪个模型预测更为准确!

(2) 卷积层可视化

注意这里只可视化了自建网络模型,因为它是3层卷积层。下面让我们一起用代码来实现!

import torch
import torchvision.models as models
import matplotlib.pyplot as plt
from PIL import Image
from torchvision import transforms
import torch.nn.functional as F
input_image = Image.open('./image/2.jpg')
# 查看图片大小
print(input_image.size)

# 数据预处理(1. 将图片转换为灰度图;2. 将图片大小调整为28*28;3. 将图片转换为Tensor;4. 对Tensor进行归一化)
preprocess = transforms.Compose([

    transforms.Grayscale(),
    transforms.Resize((28,28)),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.48], std=[0.229]),
])
input_tensor = preprocess(input_image)
# 展示图片
plt.imshow(input_tensor.squeeze())
print(input_tensor.shape)

# 为了喂入模型,需要增加一个维度
image = input_tensor.unsqueeze(0) # create a mini-batch as expected by the model

image = image.to(device)
print(image.shape)
(700, 730)
torch.Size([1, 28, 28])
torch.Size([1, 1, 28, 28])

在这里插入图片描述

model_weights = []
conv_layers = []
counter = 0

model_children = list(model_myself.children())

print(model_children)

for i in range(len(model_children[0])):
    if type(model_children[0][i]) == nn.Conv2d:
        counter+=1
        model_weights.append(model_children[0][i].weight)
        conv_layers.append(model_children[0][i])
        


print(f"Total convolution layers: {counter}")
print(conv_layers)

print(model_weights[0].shape)
[Sequential(
  (0): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
  (1): ReLU()
  (2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (3): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
  (4): ReLU()
  (5): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (6): Conv2d(16, 32, kernel_size=(5, 5), stride=(1, 1))
  (7): ReLU()
  (8): Flatten(start_dim=1, end_dim=-1)
  (9): Linear(in_features=32, out_features=120, bias=True)
  (10): ReLU()
  (11): Linear(in_features=120, out_features=84, bias=True)
  (12): ReLU()
  (13): Linear(in_features=84, out_features=10, bias=True)
)]
Total convolution layers: 3
[Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2)), Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1)), Conv2d(16, 32, kernel_size=(5, 5), stride=(1, 1))]
torch.Size([6, 1, 5, 5])
outputs = []
names = []

for layer in conv_layers:    # conv_layers即是存储了所有卷积层的列表
    
    image = F.conv2d(image,weight=layer.weight,bias=None,stride=1,padding=0)  # 每个卷积层对image做计算,得到以矩阵形式存储的图片,需要通过matplotlib画出
    outputs.append(image)
    names.append(str(layer))
print(len(outputs))

for feature_map in outputs:
    print(feature_map.shape)
3
torch.Size([1, 6, 24, 24])
torch.Size([1, 16, 20, 20])
torch.Size([1, 32, 16, 16])
print(outputs[0].shape)
print(outputs[0].squeeze().shape)
print(torch.sum(outputs[0].squeeze(0),0).shape)
torch.Size([1, 6, 24, 24])
torch.Size([6, 24, 24])
torch.Size([24, 24])

processed = []

for feature_map in outputs:
    feature_map = feature_map.squeeze(0)  # torch.Size([1, 64, 112, 112]) —> torch.Size([64, 112, 112])  去掉第0维 即batch_size维
    gray_scale = torch.sum(feature_map,0)
    gray_scale = gray_scale / feature_map.shape[0]  # torch.Size([64, 112, 112]) —> torch.Size([112, 112])   从彩色图片变为黑白图片  压缩64个颜色通道维度,否则feature map太多张
    processed.append(gray_scale.data.cpu().numpy())  # .data是读取Variable中的tensor  .cpu是把数据转移到cpu上  .numpy是把tensor转为numpy

for fm in processed:
    print(fm.shape)

(24, 24)
(20, 20)
(16, 16)

import matplotlib.pyplot as plt

fig = plt.figure(figsize=(30, 50))

for i in range(len(processed)):   # len(processed) = 17
    a = fig.add_subplot(1,len(processed), i+1)
    img_plot = plt.imshow(processed[i])
    a.axis("off")
    a.set_title(names[i].split('(')[0], fontsize=30)   # names[i].split('(')[0] 结果为Conv2d

plt.savefig('resnet18_feature_maps.jpg', bbox_inches='tight')

在这里插入图片描述

通过上面的结果我们我们发现,随着卷积的增加,提取的特征不断放大,第一层卷积我们可以很好的识别出是数字”2“,第二次卷积已经分不清是什么了,提取的好像是数字”2“的边缘特征,第3层已经越来越抽象了。

最后希望本次的可视化可以使大家更好的理解卷积神经网络的工作原理。理解CNN这个抽象的概念!

须知少时凌云志,曾许人间第一流!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值