CNN+FashionMNIST+分类+代码详解

目录

一、引言

二、虚拟环境的配置

三、数据集

四、准备代码

1、包的导入

2、数据集的加载

3、创建模型

4、GPU

5、损失函数及优化器               

五、训练     

1、模型训练部分 (train() 函数)

2、模型加载和测试部分

3、训练结果

六、验证

1、代码

2、验证结果

七、提高正确率的训练策略

八、总结

九、后续


一、引言

        笔者的第一篇文章中提到在最原始的神经网络ANN全连接层中,每个神经元与上一层的每个神经元全连接,当输入层的特征维度变得很高时,这时全连接网络需要训练的参数就会增大很多,计算速度就会变得很慢。

        例如:在利用人工神经网络解决图像分类问题时,随着图像尺寸的增加,需要训练参数的数量急剧增加。对于一个非常深的神经网络(具有大量隐藏层的网络),梯度在向后传播时消失或爆炸,从而导致梯度消失和爆炸。

        而CNN可以解决这个问题,CNN采用卷积层来代替全连接层。卷积层中的每个神经元只与输入数据的局部区域(即感受野)进行连接。通过使用卷积核(滤波器)在输入数据上滑动来进行局部特征的提取。这样,卷积层的参数量大大减少。

        CNN现在网上的教程很多,在很多地方都可以看到关于CNN的详细介绍,笔者就不对CNN进行讲解,主要是分享笔者学习在代码层面应用CNN解决图像分类问题的过程。

二、虚拟环境的配置

        首先是笔者使用的IDE为PyCharm2022.1.3专业版。

        主要的虚拟环境的配置为:

  • python                       3.8        
  • torch                         1.13.1+cu117
  • torchaudio                 0.13.1+cu117
  • torchvision                0.14.1
  • tensorboard              2.14.0
  • numpy                       1.24.4
     

        虚拟环境最重要的是兼容性,电脑可以使用cuda的最好安装编译时与 CUDA  兼容的PyTorch,可以加快训练和推理的速度。

三、数据集

        Fashion-MNIST是Zalando是Zalando是Zalando文章图片的数据集,由 60,000 个示例的训练集和 10,000 个示例的测试集组成。每个示例都是一个 28x28 灰度图像,与来自 10 个类别的标签相关联。将其作为原始MNIST 数据集Fashion-MNIST的直接替代品,用于对机器学习算法进行基准测试。它具有相同的图像大小和训练和测试分割结构。笔者将使用该数据集对模型进行训练,实现图像分类问题。

        以下是数据示例(每个类占三行):

        

        每个训练和测试示例被分配以下标签之一:

标签描述
0T恤/上衣
1裤子
2套衫
3裙子
4外套
5檀香
6衬衫
7运动鞋
8
9踝靴

        获取数据集:GitHub - zalandoresearch/fashion-mnist: A MNIST-like fashion product database. BenchmarkA MNIST-like fashion product database. Benchmark :point_down: - GitHub - zalandoresearch/fashion-mnist: A MNIST-like fashion product database. Benchmarkicon-default.png?t=N7T8https://github.com/zalandoresearch/fashion-mnist

四、准备代码

1、包的导入

import matplotlib.pyplot as plt
import torch
import torchvision
from torch.nn import MaxPool2d, Flatten
from torchvision.datasets import FashionMNIST
from torchvision import transforms
from torch.utils.data import DataLoader
from torch import nn
import os
from PIL import Image
import numpy as np

2、数据集的加载

# 预处理:将两个步骤整合在一起
transform = transforms.Compose({
    transforms.ToTensor(), # 转为Tensor,范围改为0-1
    transforms.Normalize((0.1307,),(0.3081)) # 数据归一化,即均值为0,标准差为1
})


# 训练数据集
train_data = FashionMNIST(root='./data',train=True,download=False,transform=transforms.ToTensor())
train_loader = DataLoader(train_data, shuffle=True, batch_size=64)

# 测试数据集
test_data = FashionMNIST(root='./data',train=False,download=False,transform=transforms.ToTensor())
test_loader = DataLoader(test_data, shuffle=False, batch_size=64)

        这边首先使用transforms.Compose 将多个图像转换操作按顺序组合在一起,接受一个由变换操作构成的列表,并按照列表中的顺序依次应用这些变换。将图像数据转换为 PyTorch 张量,并将其归一化,以使其在训练深度学习模型时符合模型输入的期望格式

        然后加载 FashionMNIST 数据集

参数解释:

  • root='./data': 指定数据集的存储路径。在这个例子中,数据集会被下载或读取到 ./data 目录。
  • train=True: 指定要加载训练数据集。如果设置为 False,则加载测试数据集。
  • download=False: 如果 True,并且数据集尚未下载,它会从网上下载数据集。在这里设置为 False 表示数据集已经下载并存储在指定目录中。
  • transform=transforms.ToTensor(): 对数据集中的每张图像应用 transforms.ToTensor() 转换,将其从 PIL Image 或 NumPy ndarray 转换为 PyTorch 张量,并将像素值范围从 [0, 255] 缩放到 [0.0, 1.0]。这种转换确保了数据在输入模型之前是正确格式的。

       由于事先把数据集下载好了,所以就不用在此处下载数据集。

        使用 DataLoader 创建一个训练数据加载器,配置为每次从数据集中加载 64 张图像,并在每个 epoch 开始时对数据进行打乱。DataLoader 用于批量加载数据,并支持多线程数据加载。它从 train_data 中批量读取数据,以供训练模型使用。

参数解释:

  • train_data: 数据集对象,即上面创建的 FashionMNIST 实例。
  • shuffle=True: 指定是否在每个 epoch 开始时打乱数据。True 表示数据会被打乱,这有助于提高模型的训练效果和泛化能力。
  • batch_size=64: 指定每个批次的样本数量。这里设置为 64,表示每次从数据集中读取 64 张图像进行训练。

3、创建模型

# 模型
class Model(nn.Module):
    def __init__(self):
        super(Model,self).__init__()
        self.linear1 = nn.Linear(1600,1024)
        self.linear2 = nn.Linear(1024,528)
        self.linear3 = nn.Linear(528,10) # 10层对应的10个输出
        self.conv1 = nn.Conv2d(1,32,3,1)
        self.conv2 = nn.Conv2d(32,64,4,1)
        self.maxpool1 = MaxPool2d(2)
        self.maxpool2 = MaxPool2d(2)
        self.flatten = Flatten()
        self.softmax = nn.Softmax(dim=1)

    def forward(self, x):
        x = self.conv1(x)
        x = self.maxpool1(x)
        x = self.conv2(x)
        x = self.maxpool2(x)
        x = self.flatten(x)
        x = torch.relu(self.linear1(x))
        x = torch.relu(self.linear2(x))
        x = self.linear3(x)  # 不需要ReLU在这里
        return x

model = Model()

        创建一个简单的CNN网络:

  • x = self.conv1(x): 通过第一个卷积层处理输入数据。
  • x = self.maxpool1(x): 通过第一个最大池化层下采样。
  • x = self.conv2(x): 通过第二个卷积层处理数据。
  • x = self.maxpool2(x): 通过第二个最大池化层下采样。
  • x = self.flatten(x): 展平特征图。
  • x = torch.relu(self.linear1(x)): 通过第一个全连接层,并应用 ReLU 激活函数。
  • x = torch.relu(self.linear2(x)): 通过第二个全连接层,并应用 ReLU 激活函数。
  • x = self.linear3(x): 通过输出层,将特征映射到最终的类别分数(不使用激活函数,通常在计算交叉熵损失时会隐式地应用 softmax)。

       最后实例化模型

                model = Model()

4、GPU

#GPU设备
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

model = Model().to(device)#模型转到GPU

     自动适应不同的计算环境:如果有 GPU 可以使用,它会选择 GPU,否则选择 CPU   

如果训练时发现训练的速度太慢,或者在任务管理器中发现GPU占用率很小,说明没有使用        gpu,可以看一下剩下的部分,如果没有问题,可以跳过下半部分。

       这里首先查看自己自己的电脑是否支持使用GPU:

        1)首先在终端中查看环境中的cuda是什么版本

nvidia-smi

        2)然后安装与cuda版本兼容的pytorch

pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu117

        这里的117是指cuda为11.7,以此类推。

        3)安装完成之后查看是否能够使用GPU,在控制台或者python文件中查看

import torch

x = torch.rand(5, 5)
print(x)

if torch.cuda.is_available():
    print("CUDA is available")
    x = x.to('cuda')
    print(x)
else:
    print("CUDA is not available")

5、损失函数及优化器               

# CrossEntropyLoss
criterion = nn.CrossEntropyLoss() # 交叉熵损失,相当于Softmax+Log+NllLoss
criterion = nn.CrossEntropyLoss().to(device)#损失函数转到GPU运算

#Stochastic Gradient Descent
optimizer = torch.optim.SGD(model.parameters(),0.4) # 第一个参数是初始化参数值,第二个参数是学习率

        nn.CrossEntropyLoss: 这是 PyTorch 中用于分类任务的标准损失函数。它结合了 SoftmaxLogNegative Log Likelihood Loss (NLLLoss),用于多类分类问题。还要将将损失函数转移到GPU。

        Stochastic Gradient Descent(随机梯度下降),每次迭代中使用训练数据的一个小批量(mini-batch)来更新模型参数,进而优化损失函数。

        损失函数和优化器可以按照自己的理解选择或者自己重新设计。

五、训练     

# 模型训练
best_val_correct = 0  # 初始化为最小值
num_epochs = 10  # 定义训练的 epoch 数量(可以根据实际需求调整)
def train():
    global best_val_correct  # 确保可以访问外部的 `best_val_correct` 变量
    for epoch in range(num_epochs):  # 通常训练会在多个 epoch 中进行
        model.train() # 切换模型到训练模式
        for index,data in enumerate(train_loader):
            inputs,targets = data # input为输入数据,target为标签
            inputs = inputs.to(device) # input转到GPU
            targets = targets.to(device) # target转到GPU
            optimizer.zero_grad() # 梯度清零
            y_predict = model(inputs) # 模型预测
            loss = criterion(y_predict,targets) # 计算损失
            loss.backward() # 反向传播
            optimizer.step() # 更新参数
            if index % 100 == 0: # 每一百次保存一次模型,打印损失
                os.makedirs("./model", exist_ok=True)  # 确保保存路径存在
                torch.save(model.state_dict(),"./model/model.pkl") # 保存模型
                torch.save(optimizer.state_dict(),"./model/optimizer.pkl")
                print("[%d/%d] 损失值为:%.2f" % (index, epoch, loss.item()))
        # 在每个epoch结束时进行验证
        model.eval()  # 切换模型到评估模式
        val_correct = test()  # 确保 test() 函数正确返回验证指标
        if val_correct > best_val_correct:
            best_val_correct = val_correct
            torch.save(model.state_dict(), "./model/best_model.pkl")
            torch.save(optimizer.state_dict(), "./model/best_optimizer.pkl")
            print("[第%d个epoch结束,准确率为:%.2f" % (epoch, val_correct))
            print(f"保存了新的最佳模型best_model")
        else:
            print("[第%d个epoch结束,准确率为:%.2f" % (epoch, val_correct))
            print(f"没有达到最佳模型")

1、模型训练部分 (train() 函数)

1)全局变量和初始化:

  • best_val_correct = 0:初始化为一个较小的值,用于记录最佳验证集准确率。
  • num_epochs = 10:定义训练的 epoch 数量,即整个训练数据集将被遍历多少次。

2)训练函数 (train()):

  • global best_val_correct:声明为全局变量,以便在训练过程中更新最佳验证准确率。
  • for epoch in range(num_epochs)::循环遍历每个 epoch,整个训练过程会重复执行多次。

3)训练循环:

  • model.train():将模型设置为训练模式
  • for index, data in enumerate(train_loader)::使用数据加载器 (train_loader) 遍历训练集,每次迭代获取一个 batch 的数据。
  • 数据处理和模型训练:
    • inputs, targets = data:获取当前 batch 的输入数据和对应的标签。
    • inputs = inputs.to(device)targets = targets.to(device):将数据移动到 GPU(如果可用)。
    • optimizer.zero_grad():梯度清零,避免梯度累积。
    • y_predict = model(inputs):模型进行前向传播,生成预测结果。
    • loss = criterion(y_predict, targets):计算预测值与真实标签之间的损失。
    • loss.backward():反向传播,计算梯度。
    • optimizer.step():更新模型参数,执行一步优化器的优化操作。

4)模型保存和打印信息:

  • if index % 100 == 0::每隔一定的 batch 打印一次当前损失,并保存模型和优化器的状态。
    • torch.save(model.state_dict(), "./model/model.pkl"):保存当前模型的参数。
    • torch.save(optimizer.state_dict(), "./model/optimizer.pkl"):保存当前优化器的状态。
    • print("[%d/%d] 损失值为:%.2f" % (index, epoch, loss.item())):打印当前训练的批次数、epoch 数和损失值。

5)验证阶段:

  • model.eval():将模型设置为评估模式,这会关闭具有不同行为的模型层,例如 dropout。
  • val_correct = test():调用 test() 函数评估模型在验证集上的性能。

6)更新最佳模型:

  • 如果当前 val_correct(验证集准确率)大于 best_val_correct,则更新 best_val_correct 为当前 val_correct
  • 并保存当前模型和优化器的状态作为新的最佳模型。
  • 打印当前 epoch 结束时的验证准确率和保存信息。

# 加载模型
if os.path.exists("./model/best_optimizer.pkl"):
    model.load_state_dict(torch.load("./model/best_model.pkl", map_location=device)) # 加载保存模型的参数

# 模型测试
def test():
    correct = 0 # 正确预测的个数
    total = 0 # 总数
    with torch.no_grad(): # 测试不用计算梯度
        for data in test_loader:
            input,target = data
            input = input.to(device) # input转到GPU
            target = target.to(device) # target转到GPU
            output=model(input) # output输出10个预测取值,其中最大的即为预测的数
            probability,predict=torch.max(output.data,dim=1) # 返回一个元组,第一个为最大概率值,第二个为最大值的下标
            total += target.size(0) # target是形状为(batch_size,1)的矩阵,使用size(0)取出该批的大小
            correct += (predict == target).sum().item() # predict和target均为(batch_size,1)的矩阵,sum()求出相等的个数
        val_correct = correct / total
        return val_correct

2、模型加载和测试部分

1)模型加载:

  • 如果存在保存的最佳模型的优化器状态 ("./model/best_optimizer.pkl"),则加载对应的模型参数 ("./model/best_model.pkl")。
  • 这部分确保在之后的测试阶段使用的是性能最佳的模型。

2)模型测试 (test() 函数):

  • test() 函数用于评估模型在测试集上的性能,计算测试集上的准确率。
  • 使用 torch.no_grad() 确保在测试过程中不计算梯度,从而节省内存和加快计算速度。
  • 遍历测试集,对每个样本进行预测,并统计预测正确的数量。
  • 返回测试集上的准确率。

3、训练结果

[0/0] 损失值为:2.30
[100/0] 损失值为:0.70
[200/0] 损失值为:0.43
[300/0] 损失值为:0.60
[400/0] 损失值为:0.63
[500/0] 损失值为:0.25
[600/0] 损失值为:0.36
[700/0] 损失值为:0.41
[800/0] 损失值为:0.47
[900/0] 损失值为:0.30
[第0个epoch结束,准确率为:0.85
保存了新的最佳模型best_model
[0/1] 损失值为:0.32
[100/1] 损失值为:0.39
[200/1] 损失值为:0.34
[300/1] 损失值为:0.16
[400/1] 损失值为:0.36
[500/1] 损失值为:0.26
[600/1] 损失值为:0.35
[700/1] 损失值为:0.22
[800/1] 损失值为:0.21
[900/1] 损失值为:0.27
[第1个epoch结束,准确率为:0.87
保存了新的最佳模型best_model
[0/2] 损失值为:0.34
[100/2] 损失值为:0.10
[200/2] 损失值为:0.17
[300/2] 损失值为:0.14
[400/2] 损失值为:0.32
[500/2] 损失值为:0.20
[600/2] 损失值为:0.16
[700/2] 损失值为:0.29
[800/2] 损失值为:0.14
[900/2] 损失值为:0.14
[第2个epoch结束,准确率为:0.89
保存了新的最佳模型best_model
[0/3] 损失值为:0.16
[100/3] 损失值为:0.22
[200/3] 损失值为:0.18
[300/3] 损失值为:0.30
[400/3] 损失值为:0.13
[500/3] 损失值为:0.09
[600/3] 损失值为:0.12
[700/3] 损失值为:0.23
[800/3] 损失值为:0.12
[900/3] 损失值为:0.11
[第3个epoch结束,准确率为:0.90
保存了新的最佳模型best_model
[0/4] 损失值为:0.21
[100/4] 损失值为:0.27
[200/4] 损失值为:0.17
[300/4] 损失值为:0.15
[400/4] 损失值为:0.24
[500/4] 损失值为:0.13
[600/4] 损失值为:0.19
[700/4] 损失值为:0.13
[800/4] 损失值为:0.15
[900/4] 损失值为:0.12
[第4个epoch结束,准确率为:0.91
保存了新的最佳模型best_model
[0/5] 损失值为:0.11
[100/5] 损失值为:0.08
[200/5] 损失值为:0.10
[300/5] 损失值为:0.11
[400/5] 损失值为:0.17
[500/5] 损失值为:0.11
[600/5] 损失值为:0.20
[700/5] 损失值为:0.13
[800/5] 损失值为:0.24
[900/5] 损失值为:0.15
[第5个epoch结束,准确率为:0.91
保存了新的最佳模型best_model
[0/6] 损失值为:0.05
[100/6] 损失值为:0.13
[200/6] 损失值为:0.11
[300/6] 损失值为:0.15
[400/6] 损失值为:0.05
[500/6] 损失值为:0.11
[600/6] 损失值为:0.09
[700/6] 损失值为:0.25
[800/6] 损失值为:0.05
[900/6] 损失值为:0.13
[第6个epoch结束,准确率为:0.90
没有达到最佳模型
[0/7] 损失值为:0.06
[100/7] 损失值为:0.04
[200/7] 损失值为:0.13
[300/7] 损失值为:0.17
[400/7] 损失值为:0.18
[500/7] 损失值为:0.16
[600/7] 损失值为:0.06
[700/7] 损失值为:0.07
[800/7] 损失值为:0.06
[900/7] 损失值为:0.17
[第7个epoch结束,准确率为:0.90
没有达到最佳模型
[0/8] 损失值为:0.03
[100/8] 损失值为:0.08
[200/8] 损失值为:0.04
[300/8] 损失值为:0.04
[400/8] 损失值为:0.09
[500/8] 损失值为:0.08
[600/8] 损失值为:0.07
[700/8] 损失值为:0.10
[800/8] 损失值为:0.09
[900/8] 损失值为:0.06
[第8个epoch结束,准确率为:0.90
没有达到最佳模型
[0/9] 损失值为:0.14
[100/9] 损失值为:0.06
[200/9] 损失值为:0.09
[300/9] 损失值为:0.08
[400/9] 损失值为:0.08
[500/9] 损失值为:0.09
[600/9] 损失值为:0.08
[700/9] 损失值为:0.05
[800/9] 损失值为:0.04
[900/9] 损失值为:0.23
[第9个epoch结束,准确率为:0.91
没有达到最佳模型

进程已结束,退出代码0

六、验证

# 类别名称列表
class_names = ['T恤/上衣', '裤子', '	套衫', '裙子', '外套', '凉鞋', '衬衫', '运动鞋', '包', '踝靴']
def test_mydata():
    image = Image.open('./test/test_13.png') # 读取图片
    image = image.resize((28,28)) # 裁剪尺寸为28*28
    image = image.convert('L') # 转换为灰度图像
    transform = transforms.ToTensor()
    image = transform(image)
    image = image.resize(1,1,28,28)
    image = image.to(device) # 转到GPU
    output = model(image)
    probability,predict=torch.max(output.data,dim=1)
    probabilities = torch.softmax(output, dim=1)  # 应用 softmax 得到所有类别的概率分布
    print("此图片中的是:%s, 其最大概率为: %.2f" % (class_names[predict[0]], probabilities[0][predict[0]]))
    # 将图像张量从 GPU 移到 CPU 上
    image_cpu = image.cpu()
    # 将图像张量转换为 NumPy 数组
    image_numpy = image_cpu.squeeze().numpy()
    # print("此图片中的是:%s, 其最大概率为: %.2f" % (class_names[predict[0]], probability))
    plt.title('此图片中的是:{}'.format(class_names[predict[0]]), fontname="SimHei")
    plt.imshow(image_numpy, cmap='gray')  # 显示灰度图像
    plt.show()

# 主函数
if __name__ == '__main__':
    # 自定义测试
    test_mydata()
    # 训练与测试
    # train()

1、代码

1)类别名称列表定义

class_names 是一个包含了10个类别名称的列表,每个类别对应着一个物品或者衣物类型。

2) test_mydata() 函数

  • Image.open('./test/test_13.png'):使用 PIL 库打开指定路径的图像文件。
  • image.resize((28,28)):将图像尺寸调整为 28x28 像素。
  • image.convert('L'):将图像转换为灰度图像。
  • transforms.ToTensor():将 PIL 图像转换为 PyTorch 张量。
  • image = transform(image):应用转换,将图像转换为张量。
  • image.resize(1,1,28,28):调整张量的形状,添加一个 batch 维度。
  • image.to(device):将图像张量移动到 GPU 上(如果可用)。
  • output = model(image):输入模型进行预测,得到输出张量。
  • torch.softmax(output, dim=1):应用 softmax 函数得到所有类别的概率分布。
  • _, predict = torch.max(output.data, 1):获取预测结果的索引,即概率最高的类别。
  • print("此图片中的是:%s, 其最大概率为: %.2f" % (class_names[predict.item()], probabilities[0][predict.item()])):打印预测结果和对应的最大概率。
  • 将图像从 GPU 移到 CPU,并将其转换为 NumPy 数组以便于 matplotlib 显示。
  • 使用 matplotlib 显示图像和预测结果。

3)主函数 (__main__ 函数)

__main__ 函数作为程序的入口点。

控制测试和验证

2、验证结果

        验证阶段选取的图片是数据集中没有的,百度上随机选取的衣服类别的图片,部分验证结果如下。

        

        除了上面展示的正确结果,当然还有很多错误的结果,比如下面的裙子被错分类为上衣。

         分类错误的可能原因有很多,可能有优化器选择不当,特征选择不当,评估指标不合适,数据预处理不当等等。

        测试了很多,这里就不一一演示了。

七、提高正确率的训练策略

提高分类正确率通常涉及以下几个方面的优化和调整:

1、数据预处理

数据增强(Data Augmentation):在训练过程中通过旋转、缩放、平移、随机裁剪等方法增加数据多样性,使模型更鲁棒。

标准化(Normalization):确保输入数据的均值和方差归一化,有助于加速训练过程和提高模型收敛速度。

2、模型优化

选择合适的网络架构:根据问题复杂性选择合适的卷积神经网络(CNN)架构,如经典的 AlexNet、VGG、ResNet 或最新的 EfficientNet。

调整超参数:如学习率、批量大小、优化器(如 Adam、SGD 等)的参数,以及正则化方法(如 dropout)等。

3、损失函数

选择合适的损失函数:针对多分类问题,交叉熵损失函数通常是一个良好的选择。可以考虑加权损失函数来解决类别不平衡问题。

4、训练策略

学习率调度(Learning Rate Scheduling):随着训练的进行逐步减小学习率,以提高模型在局部最优解附近的精度。

早停策略(Early Stopping):监控验证集性能,在性能不再提升时停止训练,以防止过拟合。

5、模型评估与调试

交叉验证(Cross-Validation):使用交叉验证评估模型性能,确保模型对未见过的数据泛化能力强。

混淆矩阵(Confusion Matrix):分析模型在不同类别上的表现,发现模型容易混淆的类别,进一步优化模型。

6、模型集成与迁移学习

模型集成(Model Ensembling):结合多个模型的预测结果,以提高准确性。

迁移学习(Transfer Learning):使用预训练模型,通过微调或特征提取来加速训练和提高分类准确率。

7、调试与错误分析

错误分析(Error Analysis):分析模型在测试集上预测错误的样本,探索其原因并调整模型或数据处理策略。

八、总结

        对CNN这个大家已经耳熟能详的网络,没有采取介绍CNN知识框架的形式,而是用采取代码详解的方式来记录自己的学习过程。

九、后续

后续可能文章的计划安排:

        Mamba、loss function、Gan(Gan部分也会采取代码的形式);

        语音领域相关基础知识;

        还可能会分享一些电路方面的内容。

敬请期待!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值