AlexNet模型搭建(三部曲_2)

猫狗二分类,模型简单,训练精度并不高。数据集下载:<https://aistudio.baidu.com/datasetdetail/26884> 百度飞浆上找的大小只有60多M

1模型介绍

AlexNet是一个卷积神经网络的名字,最初是与CUDA一起使用GPU支持运行的,AlexNet是2012年ImageNet竞赛冠军获得者Alex Krizhevsky设计的。该网络的错误率与前一届冠军相比减小了10%以上,比亚军高出10.8个百分点。AlexNet是由多伦多大学SuperVision组设计的,由Alex Krizhevsky, Geoffrey Hinton和Ilya Sutskever组成。

AlexNet模型共有5个卷积层,3个全连接层,前两个卷积层和第五个卷积层有pool池化层,其他两个卷积层没有池化。
AlexNet介绍:https://www.jiqizhixin.com/graph/technologies/a0955638-affc-472d-80b0-f645a4fd4604
在这里插入图片描述
计算过程举例:
卷积操作后的输出尺寸可以通过以下公式计算
torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, padding_mode=‘zeros’, device=None, dtype=None)
官方介绍 https://pytorch.org/docs/stable/generated/torch.nn.Conv2d.html#torch.nn.Conv2d
在这里插入图片描述
一般默认dilation=1,简化之后:
在这里插入图片描述
在 PyTorch 中,卷积操作会按照上述公式计算输出特征图的尺寸,并且会使用 floor 函数来向下取整。padding 和 stride 参数在卷积计算中会影响最终的输出维度。

例如
输入的一批张量的维度为 (64, 3, 224, 224),这个张量代表一个批量大小为 64 的图片数据集合,每张图片有 3 个通道(RGB ),每个通道的分辨率【图片大H和W】为 224x224。经过该nn.Conv2d(3, 96, kernel_size=11, stride=4, padding=2)卷积操作,输出张量是什么样的情况?
在这里插入图片描述
输出张量的形状将是 (64, 96, 55, 55)。其中:
批量大小仍然是 64;
输出通道数变为 96;【上图是借助两个GPU运算,分两个out_c=48,合起来就是out_c=96】
每个通道的高和宽均为 55x55。

最大池化操作后的输出尺寸可以通过以下公式计算
官方文档:https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html#torch.nn.MaxPool2d
torch.nn.MaxPool2d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)
在这里插入图片描述
带入默认值简化之后如下:【注意:向下取整符号】
在这里插入图片描述
将上一步卷积后的输出张量 (64, 96, 55, 55),传递给最大池化层 nn.MaxPool2d(kernel_size=3, stride=2) 池化操作后输出结果是什么?
在这里插入图片描述
输出张量的形状将是 (64, 96, 27, 27)。其中:
批量大小仍然是 64;
输出通道数仍然是 96;
每个通道的高和宽现在是 27x27。

2 模型搭建

import torch
from torch import nn


class AlexNet(nn.Module):
    def __init__(self, num_classes=2):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 96, kernel_size=11, stride=4, padding=2),  # [None, 3, 224, 224] --> [None, 96, 55, 55]
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=3, stride=2),  # [None, 96, 55, 55] --> [None, 96, 27, 27]
            nn.Conv2d(96, 256, kernel_size=5, padding=2),  # [None, 96, 27, 27] --> [None, 256, 27, 27]
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=3, stride=2),  # [None, 256, 27, 27] --> [None, 256, 13, 13]
            nn.Conv2d(256, 384, kernel_size=3, padding=1),  # [None, 256, 27, 27] --> [None, 384, 13, 13]
            nn.ReLU(),
            nn.Conv2d(384, 384, kernel_size=3, padding=1),  # [None, 384, 13, 13] --> [None, 384, 13, 13]
            nn.ReLU(),
            nn.Conv2d(384, 256, kernel_size=3, padding=1),  # [None, 384, 13, 13] --> [None, 256, 13, 13]
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=3, stride=2)  # [None, 256, 13, 13] --> [None, 256, 6, 6]
        )

        self.classifier = nn.Sequential(
            nn.Dropout(p=0.5),
            # 256 * 6 * 6 是经过池化后的展平特征图的大小
            nn.Linear(256 * 6 * 6, 4096),
            nn.ReLU(),
            nn.Dropout(p=0.5),
            nn.Linear(4096, 4096),
            nn.ReLU(),
            nn.Linear(4096, num_classes)
        )

    def forward(self, inputs):
        x = self.features(inputs)
        x = torch.flatten(x, start_dim=1)
        outputs = self.classifier(x)
        return outputs

3 模型训练

train_dataset = ImageFolder(ROOT_TRAIN, transform=train_transform)的作用是使用 PyTorch 提供的 ImageFolder 类来加载指定目录 【例如目录:ROOT_TRAIN】中的图像数据,并应用数据预处理或增强变换 (train_transform)。【专门用于加载文件夹格式的数据集。】
它要求数据集的组织形式为:【每个子文件夹的名称 (class1, class2, 等等) 代表一个类别,文件夹中的所有图片都属于该类别。】
ROOT_TRAIN/
├── class1/
│ ├── img1.jpg
│ ├── img2.jpg
│ └── …
├── class2/
│ ├── img1.jpg
│ ├── img2.jpg
│ └── …
└── …
ImageFolder 会自动为每个类别生成一个标签(从0开始的整数),并将其与相应的图像关联。例如,如果目录下有两个子文件夹 class1 和 class2,则 class1 中的所有图像的标签为 0,class2 中的图像标签为 1。

为什么说这个呢,因为我遇到问题了,还困扰了我好一会儿

我是在colab上跑的,第一次用这个ImageFolder函数,也不太知道详情,我第一的数据集是这样的,就是猫狗未分文件夹,统一放在train或者val中的。
data/
├── train/
│ ├── cat.jpg
│ ├── dog.jpg
│ └── …
├── val/
│ ├── cat.jpg
│ ├── dog.jpg
│ └── …
└── …
在这里插入图片描述
后边了解之后,就将train和val下的猫狗分类放入对应的文件夹
data/
├── train/
│ ├── cat/
│ │ ├── cat.jpg
│ │ └── …
│ ├── dog/
│ │ ├── dog.jpg
│ │ └── …
│ └── …
├── val/
│ ├── cat/
│ │ ├── cat.jpg
│ │ └── …
│ ├── dog/
│ │ ├── dog.jpg
│ │ └── …
│ └── …
在这里插入图片描述

这里介绍两个自定义工具类:
SortUtil.py
将数据集从样式
在这里插入图片描述
随机分割组装成第二种格式
在这里插入图片描述

import os
from shutil import copy
import random

def make_dir(file):
    if not os.path.exists(file):
        os.makedirs(file)

# 获取data_class文件夹下所有文件夹名(即需要分类的类名)
file_path = '../data_class'  # ../data_class 是数据文件目录
flower_class = [cla for cla in os.listdir(file_path)]
# 创建 训练集train 文件夹,并由类名在其目录下创建5个子目录
make_dir('../data/train')
for cla in flower_class:
    make_dir('../data/train/' + cla)

# 创建 验证集val 文件夹,并由类名在其目录下创建子目录
make_dir('../data/val')
for cla in flower_class:
    make_dir('../data/val/' + cla)

# 划分比例,训练集 : 验证集 = 7 : 3
split_rate = 0.3

# 遍历所有类别的全部图像并按比例分成训练集和验证集
for cla in flower_class:
    cla_path = file_path + '/' + cla + '/'  # 某一类别的子目录
    images = os.listdir(cla_path)  # iamges 列表存储了该目录下所有图像的名称
    num = len(images)
    eval_index = random.sample(images, k=int(num * split_rate))  # 从images列表中随机抽取 k 个图像名称
    for index, image in enumerate(images):
        # eval_index 中保存验证集val的图像名称
        if image in eval_index:
            image_path = cla_path + image
            new_path = '../data/val/' + cla
            copy(image_path, new_path)  # 将选中的图像复制到新路径

        # 其余的图像保存在训练集train中
        else:
            image_path = cla_path + image
            new_path = '../data/train/' + cla
            copy(image_path, new_path)
        print("\r[{}] processing [{}/{}]".format(cla, index + 1, num), end="")  # processing bar
    print()

print("processing done!")

ZipExtract.py 【zip文件解压缩】
这里是因为我用了colab 去跑模型,然后遇到了一些问题。https://colab.google/

目前使用的感受:
弊端1:GPU有时候没有资源,抢不到,只能用cpu
弊端2:上传的文件他不能保存,当你使用的文本和服务器断联后,再进去,文件就没了
弊端3:上传数据很慢,下载也是

原由:我需要将自己的数据集上传到colab上面,但是直接上传文件中的图片,大几万张,比较乱还比较慢。我就想到能不能上传个压缩包,上传之后,发现他没有解压选项,所以就有了这个工具类,去解压zip数据。

import zipfile
import os

def unzip_file(zip_file_path, extract_to_dir):
    """
    解压 ZIP 文件到指定目录。

    :param zip_file_path: ZIP 文件的路径
    :param extract_to_dir: 目标解压目录
    """
    # 检查目标目录是否存在,如果不存在则创建
    if not os.path.exists(extract_to_dir):
        os.makedirs(extract_to_dir)

    # 打开 ZIP 文件
    with zipfile.ZipFile(zip_file_path, 'r') as zip_ref:
        # 解压所有内容到目标目录
        zip_ref.extractall(extract_to_dir)
        print(f"解压缩完成,文件解压到: {extract_to_dir}")


# 你的 ZIP 文件路径
zip_file_path = './example'
# 你希望解压到的目录
extract_to_dir = './output'
unzip_file(zip_file_path, extract_to_dir)

模型训练搭建

import torch
from torch import nn
from AlexNet import AlexNet
from torch.optim import lr_scheduler
import os
from datetime import datetime
from torchvision import transforms
from torchvision.datasets import ImageFolder
from torch.utils.data import DataLoader

import matplotlib.pyplot as plt

# 解决中文显示问题
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

ROOT_TRAIN = r'../data/train'
ROOT_TEST = r'../data/val'

# 将图像RGB三个通道的像素值分别减去0.5,再除以0.5.从而将所有的像素值固定在[-1,1]范围内
normalize = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
train_transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.RandomVerticalFlip(),  # 随机垂直旋转
    transforms.ToTensor(),  # 将0-255范围内的像素转为0-1范围内的tensor
    normalize])

val_transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    normalize])

train_dataset = ImageFolder(ROOT_TRAIN, transform=train_transform)
val_dataset = ImageFolder(ROOT_TEST, transform=val_transform)


train_dataloader = DataLoader(train_dataset, batch_size=64, shuffle=True)
val_dataloader = DataLoader(val_dataset, batch_size=64, shuffle=True)

# 如果显卡可用,则用显卡进行训练
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("当前设备{}".format(device))

# 调用net里面的定义的网络模型, 如果GPU可用则将模型转到GPU
my_nn = AlexNet(num_classes=2).to(device)

# 定义损失函数(交叉熵损失)
loss_fn = nn.CrossEntropyLoss().to(device)

# 定义优化器(SGD)
optimizer = torch.optim.SGD(my_nn.parameters(), lr=0.01, momentum=0.9)

# 学习率衰减 ,每十轮衰减一次 ,衰减为原来的0.1
lr_scheduler = lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)


# 定义训练函数
def train_net(dataloader, model, loss_fn, optim, lr_scheduler):
    model.train()
    # 一轮次的损失值、一轮次的正确率、一轮次被分为几个批次
    epoch_loss, epoch_acc, batch_count = 0.0, 0.0, 0
    # 学习率衰减的几轮
    cur_epoch = lr_scheduler.last_epoch + 1
    print("当前为第{}轮次,此时的学习率为{}".format(cur_epoch, optim.param_groups[0]['lr']))
    for data in dataloader:
        img, tag = data
        # 将数据移动到设备上【cpu或者gpu,看支持情况】
        img, tag = img.to(device), tag.to(device)
        # 记录该批次模型训练输出结果
        output = model(img)
        # 记录该批次的损失值
        batch_loss = loss_fn(output, tag)
        # 对 output 张量的每一行取最大值和对应的索引。_ 保存最大值,pred 保存最大值的索引,也就是预测的类别。
        pred_num, pred = torch.max(output, axis=1)
        # 将正确的预测数量除以总的样本数量,得到当前批次的准确率
        cur_acc = torch.sum(tag == pred) / output.shape[0]
        # 梯度清零,pytorch框架需要手动调用
        optim.zero_grad()
        # 反向传播计算梯度
        batch_loss.backward()
        # 更新模型参数
        optim.step()
        # 累加批次损失值
        epoch_loss += batch_loss.item()
        # 累加批次正确率
        epoch_acc += cur_acc.item()
        # 记录总批次数
        batch_count += 1
    """
    lr_scheduler.step() 有内部机制来管理学习率的调整,有一个内部计数器来跟踪训练进度。
    每次调用 step() 方法时,StepLR 会检查当前的 epoch 数量是否达到了 step_size 的倍数。
    如果是,它会按照设定的 gamma 参数更新学习率;如果不是,它不会调整学习率。
    """
    # 每个epoch结束后,更新学习率
    lr_scheduler.step()
    print("Train_Avg_Batch_Loss:{} ".format(epoch_loss / batch_count))
    print("Train_Avg_Batch_Acc: {}".format(epoch_acc / batch_count))
    return epoch_loss / batch_count, epoch_acc / batch_count


# 定义模型测试方法
def test_net(dataloader, model, loss_fn):
    model.eval()
    # 一轮次的损失值、一轮次的正确率、一轮次被分为几个批次
    epoch_loss, epoch_acc, batch_count = 0.0, 0.0, 0
    # 禁用梯度计算,节省内存和加速计算【在test阶段,是不需要梯度计算的】
    with torch.no_grad():
        for data in dataloader:
            img, tag = data
            # 将数据移动到设备上【cpu或者gpu,看支持情况】
            img, tag = img.to(device), tag.to(device)
            # 记录该批次模型训练输出结果
            output = model(img)
            # 记录该批次的损失值
            batch_loss = loss_fn(output, tag)
            # 对 output 张量的每一行取最大值和对应的索引。pred_num存最大值,pred 保存最大值的索引,也就是预测的类别。
            pred_num, pred = torch.max(output, axis=1)
            # 将正确的预测数量除以总的样本数量,得到当前批次的准确率
            cur_acc = torch.sum(tag == pred) / output.shape[0]
            # 累加批次损失值
            epoch_loss += batch_loss.item()
            # 累加批次正确率
            epoch_acc += cur_acc.item()
            # 记录总批次数
            batch_count += 1
        print("Test_Avg_Batch_Loss:{} ".format(epoch_loss / batch_count))
        print("Test_Avg_Batch_Acc: {}".format(epoch_acc / batch_count))
    # 返回批次正确率
    return epoch_loss / batch_count, epoch_acc / batch_count


# 画图函数
def matplot_loss(train_loss, val_loss):
    plt.plot(train_loss, label='train_loss')
    plt.plot(val_loss, label='val_loss')
    plt.legend(loc='best')
    plt.ylabel('loss', fontsize=12)
    plt.xlabel('epoch', fontsize=12)
    plt.title("训练集和验证集loss值对比图")
    plt.show()


def matplot_acc(train_acc, val_acc):
    plt.plot(train_acc, label='train_acc')
    plt.plot(val_acc, label='val_acc')
    plt.legend(loc='best')
    plt.ylabel('acc', fontsize=12)
    plt.xlabel('epoch', fontsize=12)
    plt.title("训练集和验证集精确度值对比图")
    plt.show()


# 开始训练
loss_train = []
acc_train = []
loss_val = []
acc_val = []

"""
只保存两个模型:
    1、测试结果最好的那一个模型
    2、最后的那一个模型
"""
# 开始训练模型
epoch = 25
max_acc = 0.0
# 计时
start_time = datetime.now()
print("当前训练模型是AlexNet,猫狗二分类,预定训练轮次-{}".format(epoch))
for t in range(epoch):
    print("-----第{}轮训练开始-----".format(t + 1))
    train_loss, train_acc = train_net(train_dataloader, my_nn, loss_fn, optimizer, lr_scheduler)
    val_loss, val_acc = test_net(val_dataloader, my_nn, loss_fn)

    loss_train.append(train_loss)
    acc_train.append(train_acc)
    loss_val.append(val_loss)
    acc_val.append(val_acc)

    # 保存最好的模型权重文件
    if val_acc > max_acc:
        folder = '../save_models'
        if not os.path.exists(folder):
            os.mkdir('../save_models')
        max_acc = val_acc
        print(f'save best model,第{t + 1}轮')
        torch.save(my_nn.state_dict(), '../save_models/best_model.pth')
    # 保存最后的权重模型文件
    if t == epoch - 1:
        torch.save(my_nn.state_dict(), '../save_models/last_model.pth')
        print("save last ok!")
print('Done!')
end_time = datetime.now()
print("start_time:{}".format(start_time))
print("end_time:{}".format(end_time))
print("{}训练总用时:{}".format(device, end_time - start_time))
print("Done!")
matplot_loss(loss_train, loss_val)
matplot_acc(acc_train, acc_val)

问题1:处理方式对比

transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])

Resize(256): 将图像的较长边调整为256像素。
CenterCrop(224): 从调整后的图像中中心裁剪224x224的区域。
ToTensor(): 将图像从[0, 255]范围内的像素值转换为[0, 1]范围内的浮点数,并将图像转换为PyTorch的Tensor格式。
Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]): 使用ImageNet数据集的均值和标准差对图像进行归一化。

train_transform = transforms.Compose([
transforms.Resize((224, 224)), # 裁剪为224x224
transforms.RandomVerticalFlip(), # 随机垂直翻转
transforms.ToTensor(), # 将0-255范围内的像素转为0-1范围内的tensor
normalize
])

Resize((224, 224)): 将图像调整为224x224的大小(这与标准处理中的CenterCrop不同,可能会影响图像的内容)。
RandomVerticalFlip(): 在训练时随机进行垂直翻转,这是一种数据增强方法,有助于提高模型的泛化能力。
ToTensor(): 将图像从[0, 255]范围内的像素值转换为[0, 1]范围内的浮点数,并将图像转换为PyTorch的Tensor格式。
Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]): 将图像像素值范围从[0, 1]调整到[-1, 1]。【更适合与自定义数据集配合使用】

问题2:归一化计算方式

transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
ransforms.Normalize(mean=[0.5, 0.5, 0.5], std= [0.5, 0.5, 0.5])

首先,需要知道ToTensor()的处理结果。
在这里插入图片描述
然后,是Normalize(mean, std)是如何处理计算的。
在这里插入图片描述
以 transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])为例。
在这里插入图片描述
计算下线【pixel=0】,计算上线【pixel=1】。
在这里插入图片描述
在这里插入图片描述在这里插入图片描述

问题3:在深度学习中,normalize函数的作用是什么,有他没他会对训练造成什么影响?

在深度学习中,normalize函数通常用于将数据的数值范围调整到一个标准范围(例如0到1,或者均值为0,标准差为1)。
其主要作用包括:

  1. 加速收敛:将输入数据归一化到相同的尺度可以帮助加速训练过程。梯度下降法在训练时会更稳定,减少了训练过程中出现的梯度消失或梯度爆炸的问题。
  2. 提高模型稳定性:通过归一化,模型在训练时可以更稳定地更新权重,因为不同特征的数值范围被标准化到类似的尺度,这样模型可以更容易地学习到有用的模式。
  3. 避免特征间的不平衡:如果不同特征的尺度差异很大,某些特征可能会对模型的训练产生不成比例的影响。归一化可以确保所有特征在训练过程中对模型的贡献更加均衡。

如果不进行归一化,训练可能会遇到以下问题:

  • 训练速度慢:由于特征的尺度不同,模型可能需要更长的时间来收敛,因为每个特征的更新幅度不同。
  • 收敛不稳定:特征值范围差异大可能导致梯度在训练过程中变化剧烈,增加了梯度消失或爆炸的风险。
  • 模型性能下降:特征不均衡可能导致模型无法有效地学习所有特征的有用信息,从而影响最终的预测性能。

问题4:如果我的数据训练集上都加上了normalize,那么我在模型训练结束之后的测试集,是不是也应该加上相同的normalize处理,不加会怎么样,或者加的normalize参数和训练集的不一样会怎么样?

在进行模型评估或测试时,测试集也应该进行相同的归一化处理。归一化是数据预处理的一部分,保持训练集和测试集的一致性非常重要。
测试集也要进行与训练集相同的归一化
如果测试数据的归一化方式与训练数据不一致或没有归一化,模型的输出将无法反映真实性能,测试结果将不可比。这样会导致测试结果失真,因为模型已经适应了训练数据的特定尺度。模型在训练时是基于归一化后的数据进行学习的,因此在测试时也必须使用相同的归一化标准。

4 模型预测

import torch
from AlexNet import AlexNet
from torch.autograd import Variable
from torchvision import datasets, transforms
from torchvision.transforms import ToPILImage
from torchvision.datasets import ImageFolder
import matplotlib.pyplot as plt

ROOT_TEST = r'../data2/train'


val_transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

val_dataset = ImageFolder(ROOT_TEST, transform=val_transform)
print(len(val_dataset))

# 如果显卡可用,则用显卡进行训练
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("当前设备{}".format(device))

# 调用net里面的定义的网络模型, 如果GPU可用则将模型转到GPU
model = AlexNet().to(device)

# 加载模型train.py里面训练的模型
model.load_state_dict(torch.load('../save_models/best_model.pth', weights_only=True))

# 获取预测结果
classes = ['cat', 'dog']

# 把tensor转成Image,方便可视化
show = ToPILImage()

# 进入验证阶段
model.eval()
arr = [1500, 1123, 45, 36, 78, 50, 1999, 1998]
# 对val_dataset里面的照片进行推理验证
for i in arr:
    x, y = val_dataset[i][0], val_dataset[i][1]
    # show(x).show()
    img = show(x)
    # 使用 matplotlib 显示图像并设置标题
    plt.imshow(img)
    plt.title(i)
    plt.axis('off')  # 如果不想显示坐标轴,可以关闭
    plt.show()
    x = Variable(torch.unsqueeze(x, dim=0).float(), requires_grad=False).to(device)
    with torch.no_grad():
        pred = model(x)
        # print(pred)
        predicted, actual = classes[torch.argmax(pred[0])], classes[y]
        print(f'Predicted: "{predicted}", Actual: "{actual}"')

这是训练25轮次,模型在测试集上正确率达到0.64左右时的,预测效果。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值