Pytorch:卷积神经网络-VGG

Pytorch: 走向深度-Visual Geometry Group Network(VGGNet)

Copyright: Jingmin Wei, Pattern Recognition and Intelligent System, School of Artificial and Intelligence, Huazhong University of Science and Technology

Pytorch教程专栏链接



本教程不商用,仅供学习和参考交流使用,如需转载,请联系本人。

Reference

VGG 论文原文链接

本章节需要有一定的爬虫基础知识,在下载数据集时使用了 requests 库。

import numpy as np 
import pandas as pd 
import matplotlib.pyplot as plt 
import requests
import cv2
import torch
import torch.nn as nn
import torch.nn.functional as F 
from torchvision import models
from torchvision import transforms
from PIL import Image
网络基本结构

VGGNet 探索了网络深度和性能的关系,用更小的卷积核和更深的网络结构,取得了较好的效果,是一个非常重要的网络。

主要贡献

使用具有非常小( 3 × 3 3×3 3×3​ )卷积滤波器的架构对深度不断增加的网络进行了全面评估,这表明通过将深度提升到 16 – 19 16–19 16–19​​ 个权重层,可以实现对现有技术配置的显著改进。

为什么使用 3 × 3 3\times3 3×3​​​ 卷积核?

两个 3 × 3 3\times3 3×3 的卷积层串联相当于 1 1 1 5 × 5 5\times5 5×5 的卷积层,即一个像素会跟周围 5 × 5 5\times5 5×5 的像素产生关联,可以说感受野大小为 5 × 5 5\times5 5×5 .

以此类推, 3 3 3 3 × 3 3\times3 3×3 的卷积层串联的效果则相当于 1 1 1 7 × 7 7\times7 7×7 的卷积层。那为什么选择使用 3 3 3 3 × 3 3\times3 3×3 的卷积层而不是使用 1 1 1 7 × 7 7\times7 7×7 的卷积层呢?

V G G VGG VGG​​​​ 中,使用了 3 3 3​​​​ 个 3 × 3 3\times3 3×3​​​​ 卷积核来代替 7 × 7 7\times7 7×7​​​​ 卷积核,使用了 2 2 2​​​​ 个 3 × 3 3\times3 3×3​​​​ 卷积核来代替 5 × 5 5\times5 5×5​​​​​​​ 卷积核。

3 3 3​ 个串联的 3 × 3 3\times3 3×3​ 的卷积层,拥有比 1 1 1​ 个 7 × 7 7\times7 7×7​ 的卷积层更少的参数,参数量是后者的 3 × 3 × 3 7 × 7 = 55 % \frac{3\times3\times3}{7\times7} = 55\% 7×73×3×3=55%

3 3 3​ 个 3 × 3 3\times3 3×3​ 的卷积层比 1 1 1​ 个 7 × 7 7\times7 7×7​ 的卷积层拥有更多的非线性变换,前者可以使用 3 3 3​ 次 R e L U ReLU ReLU​ 激活函数,而后者只能使一次,这样使得 C N N CNN CNN​ 对特征的学习能力更强。

对于给定的感受野(与输出有关的输入图片的局部大小),采用堆积的小卷积核优于采用大的卷积核,因为可以增加网络深度来保证学习更复杂的模式,而且代价还比较小(参数更少)。

这样做的主要目的是在保证具有相同感知野的条件下,提升了网络的深度,在一定程度上提升了神经网络的效果。

网络结构

随着更多层的添加(添加的层以粗体显示),配置的深度从左( A A A​ )到右( E E E​ )增加。卷积层参数表示为"滤波器大小-通道数量(卷积核个数)"。

在这里插入图片描述

从图中看出,VGG 采用了五组卷积层和三个全连接层,最后使用 Softmax 进行多分类。它有一个显著的特点,每次经过池化层后特征图的尺寸会减小一倍,而通道数则增加一倍。

nn.Sequential(*layers) 的*号,用于向函数传递参数,将变量中可迭代对象的元素拆解出来,作为独立的参数传给函数。

class myVGG(nn.Module):
    def __init__(self, num_classes=1000):
        super(myVGG, self).__init__()
        layers = []
        input_dim = 3
        output_dim = 64
        # 循环构造卷积层
        for i in range(13):
            layers += [ nn.Conv2d(input_dim, output_dim, 
                                kernel_size=3, stride=1, padding=1),
                        nn.ReLU(inplace=True)]
            # 其它层的卷积核个数保持和上一层一致,卷积大小永远保持为3*3
            input_dim = output_dim
            # 在第 2,4,7,10,13 后增加最大值池化层,并让卷积核个数翻倍
            if i == 1 or i == 3 or i == 6 or i == 9 or i == 12:
                layers += [nn.MaxPool2d(2, 2)]
                # 第10个卷积后的通道和前面一致,都为512,其他的2,4,7层卷积核个数翻倍
                if i != 9:
                    output_dim *= 2
        self.features = nn.Sequential(*layers) # 拆解元素,挨个传给函数
        # 构造全连接层
        self.classifier = nn.Sequential(
            nn.Linear(512 * 7 * 7, 4096),
            nn.ReLU(True),
            nn.Dropout(0.5),
            nn.Linear(4096, 4096),
            nn.ReLU(True),
            nn.Dropout(0.5),
            nn.Linear(4096, num_classes),
            nn.Softmax(dim=1)
        )

    def forward(self, x):
        x = self.features(x)
        # 将特征图的维度从[1, 512, 7, 7]变为[1, 512*7*7]
        x = x.view(x.size(0), -1)
        output = self.classifier(x)
        return output
myvgg = myVGG(21).cuda()
from torchsummary import summary
summary(myvgg, input_size=(3, 224, 224)) 
----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Conv2d-1         [-1, 64, 224, 224]           1,792
              ReLU-2         [-1, 64, 224, 224]               0
            Conv2d-3         [-1, 64, 224, 224]          36,928
              ReLU-4         [-1, 64, 224, 224]               0
         MaxPool2d-5         [-1, 64, 112, 112]               0
            Conv2d-6        [-1, 128, 112, 112]          73,856
              ReLU-7        [-1, 128, 112, 112]               0
            Conv2d-8        [-1, 128, 112, 112]         147,584
              ReLU-9        [-1, 128, 112, 112]               0
        MaxPool2d-10          [-1, 128, 56, 56]               0
           Conv2d-11          [-1, 256, 56, 56]         295,168
             ReLU-12          [-1, 256, 56, 56]               0
           Conv2d-13          [-1, 256, 56, 56]         590,080
             ReLU-14          [-1, 256, 56, 56]               0
           Conv2d-15          [-1, 256, 56, 56]         590,080
             ReLU-16          [-1, 256, 56, 56]               0
        MaxPool2d-17          [-1, 256, 28, 28]               0
           Conv2d-18          [-1, 512, 28, 28]       1,180,160
             ReLU-19          [-1, 512, 28, 28]               0
           Conv2d-20          [-1, 512, 28, 28]       2,359,808
             ReLU-21          [-1, 512, 28, 28]               0
           Conv2d-22          [-1, 512, 28, 28]       2,359,808
             ReLU-23          [-1, 512, 28, 28]               0
        MaxPool2d-24          [-1, 512, 14, 14]               0
           Conv2d-25          [-1, 512, 14, 14]       2,359,808
             ReLU-26          [-1, 512, 14, 14]               0
           Conv2d-27          [-1, 512, 14, 14]       2,359,808
             ReLU-28          [-1, 512, 14, 14]               0
           Conv2d-29          [-1, 512, 14, 14]       2,359,808
             ReLU-30          [-1, 512, 14, 14]               0
        MaxPool2d-31            [-1, 512, 7, 7]               0
           Linear-32                 [-1, 4096]     102,764,544
             ReLU-33                 [-1, 4096]               0
          Dropout-34                 [-1, 4096]               0
           Linear-35                 [-1, 4096]      16,781,312
             ReLU-36                 [-1, 4096]               0
          Dropout-37                 [-1, 4096]               0
           Linear-38                   [-1, 21]          86,037
          Softmax-39                   [-1, 21]               0
================================================================
Total params: 134,346,581
Trainable params: 134,346,581
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.57
Forward/backward pass size (MB): 218.58
Params size (MB): 512.49
Estimated Total Size (MB): 731.65
----------------------------------------------------------------
input = torch.randn(1, 3, 224, 224).cuda()
input.shape
torch.Size([1, 3, 224, 224])
pred = myvgg(input)
pred.shape
torch.Size([1, 21])
# 也可以单独调用其中的卷积模块
conv_feature = myvgg.features(input)
conv_feature.shape
torch.Size([1, 512, 7, 7])

VGGNet 简单灵活,拓展性强,并且迁移到其他数据集的泛化能力也很好,因此该 backbone 也适用于现在的很多检测和分割算法。

使用预训练好的VGGNet

下面使用已经训练好的卷积神经网络模型完成三个任务:

  1. 获取VGG16针对一张图像的中间特征输出,并将其可视化。

  2. 输出预训练好的VGG16网络对图像进行预测结果,已经训练好的深度学习网络可以直接用来预测输入的图像类别。

  3. 可视化图像的类激活热力图。它是与特定了捏相关的二维分数网络可视化的图像,对任何输入图像的每个位置都要计算该位置对预测类别的重要程度,然后将这种重要程度使用热力图进行可视化。计算方法是给订一张输入图像,对一个卷积层的输出特征,通过计算类别对应于每个通道的梯度,使用梯度将输出特征的每个通道进行加权。

获取中间特征进行可视化
# 导入预训练好的VGG16网络
vgg16 = models.vgg16(pretrained = True)
Downloading: "https://download.pytorch.org/models/vgg16-397923af.pth" to C:\Users\魏靖旻/.cache\torch\hub\checkpoints\vgg16-397923af.pth
100%|██████████| 528M/528M [01:45<00:00, 5.23MB/s]  

以下代码用于查看一些模型特征。

print(len(vgg16.features)) # 特征层长度
print(len(vgg16.classifier)) # 分类层长度
print(vgg16.classifier[-1]) # 可以索引任何一层
print(vgg16.features[24: ]) # 也可以选取某个特征组,如左是网络的最后一个卷积模组
31
7
Linear(in_features=4096, out_features=1000, bias=True)
Sequential(
  (24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (25): ReLU(inplace=True)
  (26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (27): ReLU(inplace=True)
  (28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (29): ReLU(inplace=True)
  (30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
# 读取一张图片,并对其可视化
im = Image.open('./data/大象.jpg')
imarray = np.asarray(im) / 255.0
plt.figure()
plt.imshow(imarray)
plt.axis('off')
plt.show()

在这里插入图片描述

对图像进行预处理:

data_transforms = transforms.Compose([
    transforms.Resize((224, 224)), # 重置图像分辨率
    transforms.ToTensor(), # 转为张量并归一化至[0-1]
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
input_im = data_transforms(im).unsqueeze(0)
print(input_im.shape)
torch.Size([1, 3, 224, 224])

定义一个辅助函数更方便的获取、保存所需要的中间特征输出。

activation = {} # 保存不同层的输出
def get_activation(name):
    def hook(model, input, output):
        activation[name] = output.detach()
    return hook

获取网络中第四层,即经过第一次最大值池化后的特征映射。

# 获取中间的卷积后的图像特征
vgg16.eval()
# 获取第四层,即经过第一次最大值池化后的特征映射
vgg16.features[4].register_forward_hook(get_activation('maxpool1'))
_ = vgg16(input_im)
maxpool1 = activation['maxpool1']
print('获取特征尺寸为:', maxpool1.shape)
获取特征尺寸为: torch.Size([1, 64, 112, 112])
# 可视化中间层64个特征映射
plt.figure(figsize = (11, 6))
for ii in range(maxpool1.shape[1]):
    # 可视化每张手写体
    plt.subplot(6, 11, ii + 1)
    plt.imshow(maxpool1.data.numpy()[0, ii, :, :], cmap = 'gray')
    plt.axis('off')
plt.subplots_adjust(wspace = 0.1, hspace = 0.1)
plt.show()

在这里插入图片描述

我们不难发现,很多特征映射都能分辨出原始图形所包含的内容,反映了网络中的较浅层能够获取图像的较大粒度的特征。

接下来获取更深层次的映射

# 获取中间的卷积后的图像特征
vgg16.eval()
# 获取第四层,即经过第一次最大值池化后的特征映射
vgg16.features[21].register_forward_hook(get_activation('layer21_conv'))
_ = vgg16(input_im)
layer21_conv = activation['layer21_conv']
print('获取特征尺寸为:', layer21_conv.shape)
获取特征尺寸为: torch.Size([1, 512, 28, 28])
# 只可视化前72个特征映射
plt.figure(figsize = (12, 6))
for ii in range(72):
    # 可视化每张手写体
    plt.subplot(6, 12, ii + 1)
    plt.imshow(layer21_conv.data.numpy()[0, ii, :, :], cmap = 'gray')
    plt.axis('off')
plt.subplots_adjust(wspace = 0.1, hspace = 0.1)
plt.show()

在这里插入图片描述

我们不难发现,更深层次的非线性映射已经不能分辨出图像的具体内容,说明他能从图像中提取更细粒度的特征。

预训练的VGG16预测图像

针对预训练好的网络的数据集网址如下:https://s3.amazonaws.com/outcome-blog/imagenet/labels.json

# 获取VGG16模型训练时对应的1000个类别标签

label_url = 'https://s3.amazonaws.com/outcome-blog/imagenet/labels.json'

# 从网页链接中获取类别标签
try:
    response = requests.get(label_url)
    response.raise_for_status()
    # response.encoding = response.apparent_encoding
except:
    print("爬取失败")

label = {int(key): value for key, value in response.json().items()}
print(len(label))
label
1000





{0: 'tench, Tinca tinca',
 1: 'goldfish, Carassius auratus',
 2: 'great white shark, white shark, man-eater, man-eating shark, Carcharodon carcharias',
 3: 'tiger shark, Galeocerdo cuvieri',
 4: 'hammerhead, hammerhead shark',
 5: 'electric ray, crampfish, numbfish, torpedo',
 6: 'stingray',
 7: 'cock',
 8: 'hen',
 9: 'ostrich, Struthio camelus',
 10: 'brambling, Fringilla montifringilla',
 
 ...
 
 990: 'buckeye, horse chestnut, conker',
 991: 'coral fungus',
 992: 'agaric',
 993: 'gyromitra',
 994: 'stinkhorn, carrion fungus',
 995: 'earthstar',
 996: 'hen-of-the-woods, hen of the woods, Polyporus frondosus, Grifola frondosa',
 997: 'bolete',
 998: 'ear, spike, capitulum',
 999: 'toilet tissue, toilet paper, bathroom tissue'}

上面的程序从网页中读取了对应数据的类别标签,转为字典格式。

接下来导入VGG16模型并对图像预测。

# 使用VGG16网络预测图像的种类
vgg16.eval()
im_pre = vgg16(input_im)
# 计算预测top-5的可能性
softmax = nn.Softmax(dim = 1)
im_pre_prob = softmax(im_pre)
prob, prelab = torch.topk(im_pre_prob, 5)
prob = prob.data.numpy().flatten()
prelab = prelab.numpy().flatten()
for ii, lab in enumerate(prelab):
    print('index: ', lab, '  label: ', label[lab], ' ||', prob[ii])
index:  385   label:  Indian elephant, Elephas maximus  || 0.58669925
index:  101   label:  tusker  || 0.38530138
index:  386   label:  African elephant, Loxodonta africana  || 0.027998904
index:  246   label:  Great Dane  || 2.5944323e-07
index:  243   label:  bull mastiff  || 8.592071e-08

可以看到,预测概率最大的是 Indian elephant,可能性为 58.67% ,第二类则是 tusker,可能性为 35.23% 。

可视化激活函数的类激活热力图

为了便于观察图像中哪些位置的内容对分类结果影响较大,可以输出图像的类季火热力图。计算图像类激活热力图数据,可以使用卷积神经网络中最后一层网络输出和其对应的梯度,但需要先定义一个性的网络,并且输出网络的卷积核梯度。

# 定义一个能够获得最后卷积层输出和梯度的新网络
class MyVgg16(nn.Module):
    def __init__(self):
        super(MyVgg16, self).__init__()
        self.vgg = models.vgg16(pretrained = True)
        # 切分VGG16模型,便于获取卷积层输出
        self.features_conv = self.vgg.features[: 30]
        # 使用原始的最大值池化层
        self.max_pool = self.vgg.features[30]
        self.avgpool = self.vgg.avgpool
        # 使用VGG16的分类层
        self.classifier = self.vgg.classifier
        # 生成梯度占位符
        self.gradients = None

    # 获取梯度的钩子
    def activations_hook(self, grad):
        self.gradients = grad

    def forward(self, x):
        x = self.features_conv(x)
        # 注册钩子,保存最后一层特征映射的梯度信息
        h = x.register_hook(self.activations_hook)
        # 对卷积后的输出使用最大值池化
        x = self.max_pool(x)
        x = self.avgpool(x)
        x = x.view((1, -1))
        x = self.classifier(x)
        return x

    # 获取梯度的方法
    def get_activations_gradient(self):
        return self.gradients

    # 获取卷积层输出的方法
    def get_activations(self, x):
        return self.features_conv(x)

用新的卷积神经网络对前面图像进行预测:

# 初始化网络
vggcam = MyVgg16()

vggcam.eval()

# 计算网络对图像的预测值
im_pre = vggcam(input_im)
# 计算预测top-5的可能性
softmax = nn.Softmax(dim = 1)
im_pre_prob = softmax(im_pre)
prob, prelab = torch.topk(im_pre_prob, 5)
prob = prob.data.numpy().flatten()
prelab = prelab.numpy().flatten()
for ii, lab in enumerate(prelab):
    print('index: ', lab, '  label: ', label[lab], ' ||', prob[ii])
index:  385   label:  Indian elephant, Elephas maximus  || 0.58669925
index:  101   label:  tusker  || 0.38530138
index:  386   label:  African elephant, Loxodonta africana  || 0.027998904
index:  246   label:  Great Dane  || 2.5944323e-07
index:  243   label:  bull mastiff  || 8.592071e-08

接下来计算需要的特征映射与梯度信息:

# 获取相对于模型参数的输出梯度
im_pre[:, prelab[0]].backward()
# 获取模型的梯度
gradients = vggcam.get_activations_gradient()
# 计算梯度相应通道的均值
mean_gradients = torch.mean(gradients, dim = [0, 2, 3])
# 获取图像在相应卷积层输出的卷积特征
activations = vggcam.get_activations(input_im).detach()
# 每个通道乘以相应的梯度均值
for i in range(len(mean_gradients)):
    activations[:, i, :, :] *= mean_gradients[i]
# 计算所有通道的均值输出得到热力图
heatmap = torch.mean(activations, dim = 1).squeeze()
# 使用ReLU函数作用于热力图
heatmap = F.relu(heatmap)
# 标准化热力图
heatmap /= torch.max(heatmap)
heatmap = heatmap.numpy()
# 可视化热力图
plt.matshow(heatmap)
<matplotlib.image.AxesImage at 0x2e3998b7640>

在这里插入图片描述

程序使用get_activations_gradient()方法获得梯度信息后,将每个通道的梯度信息计算均值,针对512张特征映射得到了512个值,然后将特征映射的每个通道乘以相应的梯度均值,在经过ReLU运算后即可得到heatmap,标准化处理后即可可视化。

将热力图与原始图像融合,能更直观观察图像中对分类结果影响更大的内容:

# 将CAM热力图融合到原始图像上
img = Image.open("./data/大象.jpg")
img = cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR) # 转为cv格式
heatmap = cv2.resize(heatmap, (img.shape[1], img.shape[0]))
heatmap = np.uint8(255 * heatmap)
heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
Grad_cam_img = heatmap * 0.4 + img
Grad_cam_img = Grad_cam_img / Grad_cam_img.max()
# 可视化图像
b, g, r = cv2.split(Grad_cam_img)
Grad_cam_img = cv2.merge([r, g, b])
plt.figure()
plt.imshow(Grad_cam_img)
plt.axis('off')
plt.show()

在这里插入图片描述

该图显示了预测结果相应的主要位置,头部和牙齿的内容对预测的结果影响更大。

我们同样尝试老虎的数据如下:

# 读取一张图片,并对其可视化
im1 = Image.open('./data/老虎.jpg')
imarray = np.asarray(im1) / 255.0
plt.figure()
plt.imshow(imarray)
plt.axis('off')
plt.show()

data_transforms = transforms.Compose([
    transforms.Resize((224, 224)), # 重置图像分辨率
    transforms.ToTensor(), # 转为张量并归一化至[0-1]
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
input_im1 = data_transforms(im1).unsqueeze(0)
print(input_im1.shape)

在这里插入图片描述

torch.Size([1, 3, 224, 224])
# 初始化网络
vggcam = MyVgg16()

vggcam.eval()

# 计算网络对图像的预测值
im_pre = vggcam(input_im1)
# 计算预测top-5的可能性
softmax = nn.Softmax(dim = 1)
im_pre_prob = softmax(im_pre)
prob, prelab = torch.topk(im_pre_prob, 5)
prob = prob.data.numpy().flatten()
prelab = prelab.numpy().flatten()
for ii, lab in enumerate(prelab):
    print('index: ', lab, '  label: ', label[lab], ' ||', prob[ii])
index:  292   label:  tiger, Panthera tigris  || 0.7226738
index:  282   label:  tiger cat  || 0.2767185
index:  290   label:  jaguar, panther, Panthera onca, Felis onca  || 0.00032113976
index:  287   label:  lynx, catamount  || 8.872946e-05
index:  340   label:  zebra  || 6.449003e-05
# 获取相对于模型参数的输出梯度
im_pre[:, prelab[0]].backward()
# 获取模型的梯度
gradients = vggcam.get_activations_gradient()
# 计算梯度相应通道的均值
mean_gradients = torch.mean(gradients, dim = [0, 2, 3])
# 获取图像在相应卷积层输出的卷积特征
activations = vggcam.get_activations(input_im1).detach()
# 每个通道乘以相应的梯度均值
for i in range(len(mean_gradients)):
    activations[:, i, :, :] *= mean_gradients[i]
# 计算所有通道的均值输出得到热力图
heatmap = torch.mean(activations, dim = 1).squeeze()
# 使用ReLU函数作用于热力图
heatmap = F.relu(heatmap)
# 标准化热力图
heatmap /= torch.max(heatmap)
heatmap = heatmap.numpy()
# 可视化热力图
plt.matshow(heatmap)
<matplotlib.image.AxesImage at 0x2e39f61b250>

在这里插入图片描述

# 将CAM热力图融合到原始图像上
img = Image.open("./data/老虎.jpg")
img = cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR) # 转为cv格式
heatmap = cv2.resize(heatmap, (img.shape[1], img.shape[0]))
heatmap = np.uint8(255 * heatmap)
heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
Grad_cam_img = heatmap * 0.4 + img
Grad_cam_img = Grad_cam_img / Grad_cam_img.max()
# 可视化图像
b, g, r = cv2.split(Grad_cam_img)
Grad_cam_img = cv2.merge([r, g, b])
plt.figure()
plt.imshow(Grad_cam_img)
plt.axis('off')
plt.show()

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值