第六周周报:动手深度学习(一)

目录

摘要

Abstract

一、预备知识

1.1 数据操作

1.2 自动求导

二、线性回归

三、softmax回归

四、多层感知机

五、应对过拟合问题

5.1 权重衰减

5.2 丢弃法

六、 Kaggle实战:房价预测

总结


摘要

本周跟着李沐老师的动手深度学习课程,主要学习了使用PyTorch工具对深度学习进行集成性操作,本周的内容主要涉及对tensor数据的操作、自动求导、线性回归模型的实现、softmax回归模型的实现,以及如何解决过拟合问题,最后还有kaggle实战房价预测。本周的学习内容主要围绕代码展开,本博客也会附上完整代码和数据。

Abstract

This week, I followed Professor Li Mu's hands-on deep learning course and mainly learned how to use PyTorch tool to integrate deep learning operations. The content of this week mainly involves the operation of Tensor data, automatic differentiation, implementation of linear regression model, implementation of softmax regression model, and how to solve overfitting problems. Finally, there is also practical housing price prediction on Kaggle. This week's learning content mainly revolves around code, and this blog will also include complete code and data.

一、预备知识

1.1 数据操作

  • 创建tensor
import torch
import numpy as np

#-----------------------------------------------------创建Tensor--------------------------------------------------------
print("创建一个5x3的未初始化的Tensor")
x = torch.empty(5, 3)  #创建一个5x3的未初始化的Tensor
print(x)

print("创建一个5x3的随机初始化均匀分布的Tensor")
x = torch.rand(5, 3)  #创建一个5x3的随机初始化均匀分布的Tensor
print(x)

print("创建一个5x3的long型全0的Tensor,torch.long长整型数值")
x = torch.zeros(5, 3, dtype=torch.long)  #创建一个5x3的long型全0的Tensor,torch.long长整型数值
print(x)

print("创建一个5x3的float型全1的Tensor")
x = x.new_ones(5, 3, dtype=torch.float64)  #创建一个5x3的float型全1的Tensor
print(x)

print("直接根据数据创建")
x = torch.tensor([5.5, 3])  #直接根据数据创建
print(x)

print("创建一个每个元素均为标准正态分布的4行3列随机张量")
x = torch.randn(4, 3)  #创建一个每个元素均为标准正态分布的4行3列随机张量
print(x)

print("生成一个与输入张量大小相同的张量,其中填充了均值为 0 方差为 1 的正态分布的随机值")
x=torch.randn_like(x)  #生成一个与输入张量大小相同的张量,其中填充了均值为 0 方差为 1 的正态分布的随机值
print(x)

print("通过shape或者size()来获取Tensor的形状")
#通过shape或者size()来获取Tensor的形状
print(x.size())
print(x.shape)

print("创建一个5x3的对角线为1,其他为0的Tensor")
x = torch.eye(5, 5, dtype=torch.long)  #创建一个5x3的对角线为1,其他为0的Tensor
print(x)

print("从s到e,均匀切分成steps份的张量")
x = torch.linspace(0, 15, 6)  #从s到e,均匀切分成steps份的张量
print(x)

print("从s到e,步长为step的张量")
x = torch.arange(0,15,3)  #从s到e,步长为step的张量
print(x)

print("随机生成一个长度为m的Tensor,其中包含从0到m-1的整数")
x = torch.randperm(5)   #随机生成一个长度为m的Tensor,其中包含从0到m-1的整数
print(x,"\n\n")

运行结果如下图所示:

  • 操作tensor
#-----------------------------------------------------------操作Tensor------------------------------------------------------
#加法,以下输出结果都相同
print("加法,以下输出结果都相同")
x = x.new_ones(5, 3, dtype=torch.float64)
y = torch.rand(5, 3)
print(x + y)  #1
print(torch.add(x, y))  #2
result = torch.empty(5, 3)
torch.add(x, y, out=result)  #3
print(result)
y.add_(x)  #4
print(y)

#索引,索引出来的结果与原数据共享内存,也即修改一个,另一个会跟着修改
print("索引")
y = x[0, :]  #第一行
y += 1
print(y)
print(x[0, :])  #源tensor也被改了

#改变形状
print("改变形状")
y = x.view(15)
z = x.view(-1, 5)  # -1所指的维度可以根据其他维度的值推出来
print(x.size(), y.size(), z.size())
#注意view()返回的新Tensor与源Tensor虽然可能有不同的size,但是是共享data的,也即更改其中的一个,另外一个也会跟着改变。(顾名思义,view仅仅是改变了对这个张量的观察角度,内部数据并未改变)
x += 1
print(x)
print(y) # 也加了1
print("clone()真正新的副本(即不共享data内存)")
x_cp = x.clone().view(15)  #真正新的副本(即不共享data内存)
x -= 1
print(x)
print(x_cp)
print("将张量改为数字")
x = torch.randn(1)
print(x)
print(x.item(),"\n\n")

运行结果如下图所示:

  • 广播机制

当对两个形状不同的Tensor按元素运算时,可能会触发广播机制:先适当复制元素使这两个Tensor形状相同后再按元素运算。

#-----------------------------------------------------------广播机制------------------------------------------------------
#当对两个形状不同的Tensor按元素运算时,可能会触发广播(broadcasting)机制:先适当复制元素使这两个Tensor形状相同后再按元素运算
print("广播机制")
x = torch.arange(1, 3).view(1, 2)
print(x)
y = torch.arange(1, 4).view(3, 1)
print(y)
print(x + y,"\n\n")  #由于x和y分别是1行2列和3行1列的矩阵,如果要计算x + y,那么x中第一行的2个元素被广播(复制)到了第二行和第三行,而y中第一列的3个元素被广播(复制)到了第二列。如此,就可以对2个3行2列的矩阵按元素相加。

运行结果如下图所示:

  • tensor与NumPy之间的转换
#-----------------------------------------------------------Tensor和NumPy相互转换------------------------------------------------------
print("Tensor转NumPy")
a = torch.ones(5)
b = a.numpy()
print(a, b)
a += 1
print(a, b)
b += 1
print(a, b)

print("NumPy数组转Tensor")
a = np.ones(5)
b = torch.from_numpy(a)
print(a, b)
a += 1
print(a, b)
b += 1
print(a, b)

c = torch.tensor(a)  #该方法总是会进行数据拷贝,返回的Tensor和原来的数据不再共享内存
a += 1
print(a, c,"\n")

运行结果如下图所示:

1.2 自动求导

属性.requires_grad设置为True,它将开始追踪在其上的所有操作(这样就可以利用链式法则进行梯度传播了)。完成计算后,可以调用.backward()来完成所有梯度计算。此Tensor的梯度将累积到.grad属性中。例如:在y.backward()时,如果y是标量,则不需要为backward()传入任何参数;否则,需要传入一个与y同形的Tensor。

#-----------------------------------------------------------自动求梯度------------------------------------------------------
x = torch.ones(2, 2, requires_grad=True)  #属性.requires_grad设置为True,它将开始追踪(track)在其上的所有操作(这样就可以利用链式法则进行梯度传播了)
print(x)
print(x.grad_fn)  #该属性即创建该Tensor的Function, 就是说该Tensor是不是通过某些运算得到的,若是,则grad_fn返回一个与这些运算相关的对象,否则是None。

y = x + 2
print(y)
print(y.grad_fn)

z = y * y * 3
out = z.mean()  #.mean()计算张量中的平均值
print(z, out)

#调用.backward()来完成所有梯度计算,此Tensor的梯度将累积到.grad属性中
#在y.backward()时,如果y是标量,则不需要为backward()传入任何参数;否则,需要传入一个与y同形的Tensor
# 再来反向传播一次,注意grad是累加的
print("反向传播,算梯度")
out.backward() # 等价于 out.backward(torch.tensor(1.))
print(x.grad)  #out关于x的梯度

#grad在反向传播过程中是累加的(accumulated),这意味着每一次运行反向传播,梯度都会累加之前的梯度,所以一般在反向传播之前需把梯度清零。
out2 = x.sum()
out2.backward()
print(x.grad)
#不允许张量对张量求导,只允许标量对张量求导,求导结果是和自变量同形的张量。所以必要时我们要把张量通过将所有张量的元素加权求和的方式转换为标量
out3 = x.sum()  #x就为标量了
x.grad.data.zero_()  #梯度清0
out3.backward()
print(x.grad)

with torch.no_grad():  #中断梯度追踪
    y2 = x ** 3

#如果我们想要修改tensor的数值,但是又不希望被autograd记录(即不会影响反向传播),那么我么可以对tensor.data进行操作
x = torch.ones(1,requires_grad=True)
print(x.data) # 还是一个tensor
print(x.data.requires_grad) # 但是已经是独立于计算图之外
y = 2 * x
x.data *= 100 # 只改变了值,不会记录在计算图,所以不会影响梯度传播
y.backward()
print(x) # 更改data的值也会影响tensor的值
print(x.grad)

运行结果如下图所示:

二、线性回归

线性回归是一个单层神经网络,公式如下:

y=w_{i}x_{i}+b

例如上图所示,输入分别为x_{1}x_{2}​,因此输入层的输入个数为2。输入个数也叫特征数或特征向量维度。网络的输出为o,输出层的输出个数为1。若不太了解线性回归模型,请移步这篇博客学习线性回归模型相关知识。

import numpy as np
import torch
import torch.utils.data as Data
from torch import nn, optim
from torch.nn import init

num_inputs = 2
num_examples = 1000
true_w = [2, -3.4]
true_b = 4.2
features = torch.tensor(np.random.normal(0, 1, (num_examples, num_inputs)), dtype=torch.float)
labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b
labels += torch.tensor(np.random.normal(0, 0.01, size=labels.size()), dtype=torch.float)

batch_size = 10
# 将训练数据的特征和标签组合
dataset = Data.TensorDataset(features, labels)  # 对应匹配
# 随机读取小批量
data_iter = Data.DataLoader(dataset, batch_size, shuffle=True)

print(net)  # 使用print可以打印出网络的结构

init.normal_(net[0].weight, mean=0, std=0.01)  # 将权重参数每个元素初始化为随机采样于均值为0、标准差为0.01的正态分布
init.constant_(net[0].bias, val=0)  # 也可以直接修改bias的data: net[0].bias.data.fill_(0)

loss = nn.MSELoss()  #均方误差损失作为模型的损失函数

optimizer = optim.SGD(net.parameters(), lr=0.03)

num_epochs = 3
for epoch in range(1, num_epochs + 1):
    for X, y in data_iter:
        output = net(X)
        l = loss(output, y.view(-1, 1))
        optimizer.zero_grad() # 梯度清零,等价于net.zero_grad()
        l.backward()
        optimizer.step()  # 梯度更新
    print('epoch %d, loss: %f' % (epoch, l.item()))

dense = net[0]
print(true_w, dense.weight)
print(true_b, dense.bias)

运行结果如下图所示:

三、softmax回归

第二节介绍的线性回归模型适用于输出为连续值的情景。在另一类情景中,模型输出可以是一个像图像类别这样的离散值。对于这样的离散值预测问题,可以使用诸如softmax回归在内的分类模型。和线性回归不同,softmax回归的输出单元从一个变成了多个,且引入了softmax运算使输出更适合离散值的预测和训练。

softmax回归同线性回归一样,也是一个单层神经网络。

softmax回归如下图所示:

softmax的输出即输出分类中属于每一类的概率,然后将最有可能的类别作为模型的预测。 

import torch
import sys
from torch import nn
from torch.nn import init
from d2lzh_pytorch import FlattenLayer
sys.path.append("..")  # 为了导入上层目录的d2lzh_pytorch
import d2lzh_pytorch as d2l
from collections import OrderedDict

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

# 定义和初始化模型
num_inputs = 784  # 1x28x28的图片,需要先将图片.view()一下
num_outputs = 10

net = nn.Sequential(
    OrderedDict([  # OrderedDict指定每个模块的键(名称)和值(模块本身)
        ('flatten', FlattenLayer()),  # 格式转换
        ('linear', nn.Linear(num_inputs, num_outputs))
    ])
)

# 随机初始化参数
init.normal_(net.linear.weight, mean=0, std=0.01)
init.constant_(net.linear.bias, val=0)

# 交叉熵损失函数
loss = nn.CrossEntropyLoss()

# 优化算法
optimizer = torch.optim.SGD(net.parameters(), lr=0.1)

# 训练模型
num_epochs = 5
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)

运行结果如下图所示:

四、多层感知机

线性回归和softmax回归是单层神经网络,然而深度学习主要关注多层模型。多层感知机在单层神经网络的基础上引入了一到多个隐藏层(hidden layer),隐藏层位于输入层和输出层之间。如下图所示:

import torch
from torch import nn
from torch.nn import init
import numpy as np
import sys
sys.path.append("..")
import d2lzh_pytorch as d2l

num_inputs, num_outputs, num_hiddens = 784, 10, 256

# 和softmax回归唯一的不同在于,多加了一个全连接层作为隐藏层。它的隐藏单元个数为256,并使用ReLU函数作为激活函数。
net = nn.Sequential(
        d2l.FlattenLayer(),
        nn.Linear(num_inputs, num_hiddens),
        nn.ReLU(),  # 如果网络中没有激活函数,那么无论网络有多少层,其输出都是输入的线性组合,这限制了模型的能力
        nn.Linear(num_hiddens, num_outputs),
)

for params in net.parameters():
    init.normal_(params, mean=0, std=0.01)

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

loss = torch.nn.CrossEntropyLoss()

optimizer = torch.optim.SGD(net.parameters(), lr=0.1)

num_epochs = 5
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)

运行结果如下图所示:

五、应对过拟合问题

过拟合,即模型的训练误差远小于它在测试集上的误差。虽然增大训练数据集可能会减轻过拟合,但是获取额外的训练数据往往代价高昂。所以引入权重衰减和丢弃发两种方法。过拟合情况如下图所示:

5.1 权重衰减

权重衰减等价于L2范数正则化,正则化通过为模型损失函数添加惩罚项使学出的模型参数值较小,是应对过拟合的常用手段。L2范数正则化在模型原损失函数基础上添加L2范数惩罚项,从而得到训练所需要最小化的函数。L2范数惩罚项指的是模型权重参数每个元素的平方和与一个正的常数的乘积。损失函数如下:

import torch
import numpy as np
import sys
sys.path.append("..")
import d2lzh_pytorch as d2l

n_train, n_test, num_inputs = 20, 100, 200  # 训练数据很少
true_w, true_b = torch.ones(num_inputs, 1) * 0.01, 0.05

# 创建数据
features = torch.randn((n_train + n_test, num_inputs))
labels = torch.matmul(features, true_w) + true_b  # matmul()矩阵点乘
labels += torch.tensor(np.random.normal(0, 0.01, size=labels.size()), dtype=torch.float)  # 加入噪声
train_features, test_features = features[:n_train, :], features[n_train:, :]
train_labels, test_labels = labels[:n_train], labels[n_train:]

# 定义随机初始化模型参数的函数,并为每个参数都附上梯度
def init_params():
    w = torch.randn((num_inputs, 1), requires_grad=True)
    b = torch.zeros(1, requires_grad=True)
    return [w, b]

# 定义L2范数惩罚项,只惩罚模型的权重参数
def l2_penalty(w):
    return (w**2).sum() / 2

# 训练网络
batch_size, num_epochs, lr = 1, 100, 0.003
net, loss = d2l.linreg, d2l.squared_loss

dataset = torch.utils.data.TensorDataset(train_features, train_labels)
train_iter = torch.utils.data.DataLoader(dataset, batch_size, shuffle=True)

def fit_and_plot(lambd):
    w, b = init_params()
    train_ls, test_ls = [], []
    for _ in range(num_epochs):
        for X, y in train_iter:
            # 添加了L2范数惩罚项
            l = loss(net(X, w, b), y) + lambd * l2_penalty(w)  # 损失函数加上权重
            l = l.sum()

            if w.grad is not None:
                w.grad.data.zero_()
                b.grad.data.zero_()
            l.backward()
            d2l.sgd([w, b], lr, batch_size)

        # .item()方法用于将这个标量Tensor转换为一个Python标量(通常是float类型)
        # train_ls.append()将计算得到的平均损失值添加到名为train_ls的列表中
        train_ls.append(loss(net(train_features, w, b), train_labels).mean().item())
        test_ls.append(loss(net(test_features, w, b), test_labels).mean().item())
    print("平均损失值:", sum(test_ls)/len(test_ls))
    # 画图显示
    d2l.semilogy(range(1, num_epochs + 1), train_ls, 'epochs', 'loss', range(1, num_epochs + 1), test_ls, ['train', 'test'])
    print('L2 norm of w:', w.norm().item())

# 当lambd设为0时,没有使用权重衰减
fit_and_plot(lambd=0)

fit_and_plot(lambd=3)

运行结果如下图所示:

5.2 丢弃法

对某隐藏层使用丢弃法时,该层的隐藏单元将有一定概率被丢弃掉。设丢弃概率为p,那么有p的概率h_{i}会被清零,有1-p的概率h_{i}会除以1−p拉伸。丢弃法不改变其输入的期望值,在反向传播时,被丢弃的隐藏单元相关的权重的梯度均为0。由于在训练中隐藏层神经元的丢弃是随机的,即所有都有可能被清零,输出层的计算无法过度依赖隐藏层中的任一个单元,从而在训练模型时起到正则化的作用,并可以用来应对过拟合。丢弃概率是丢弃法的超参数

随机丢弃了h_{2}h_{5},如下图所示:

# 倒置丢弃法,丢弃法不改变其输入的期望值,对该隐藏层使用丢弃法时,该层的隐藏单元将有一定概率被丢弃掉。
# 设丢弃概率为p,那么有p的概率参数hi会被清零。丢弃概率是丢弃法的超参数。

import torch
import torch.nn as nn
import sys
sys.path.append("..")
import d2lzh_pytorch as d2l

num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256

drop_prob1, drop_prob2 = 0.2, 0.5

net = nn.Sequential(
        d2l.FlattenLayer(),
        nn.Linear(num_inputs, num_hiddens1),
        nn.ReLU(),
        nn.Dropout(drop_prob1),
        nn.Linear(num_hiddens1, num_hiddens2),
        nn.ReLU(),
        nn.Dropout(drop_prob2),
        nn.Linear(num_hiddens2, 10)
        )

for param in net.parameters():
    nn.init.normal_(param, mean=0, std=0.01)

num_epochs, batch_size = 5, 256
loss = torch.nn.CrossEntropyLoss()
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
optimizer = torch.optim.SGD(net.parameters(), lr=0.5)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)

运行结果如下图所示:

六、 Kaggle实战:房价预测

import torch
import torch.nn as nn
import pandas as pd
import sys
sys.path.append("..")
import d2lzh_pytorch as d2l

torch.set_default_tensor_type(torch.FloatTensor)

train_data = pd.read_csv('./data/kaggle/train.csv')
test_data = pd.read_csv('./data/kaggle/test.csv')

# 将所有的训练数据和测试数据的79个特征按样本连结
all_features = pd.concat((train_data.iloc[:, 1:-1], test_data.iloc[:, 1:]))

# 数据预处理,对连续数值的特征做标准化
numeric_features = all_features.dtypes[all_features.dtypes != 'object'].index
all_features[numeric_features] = all_features[numeric_features].apply(lambda x: (x - x.mean()) / (x.std()))
# 标准化后,每个数值特征的均值变为0,所以可以直接用0来替换缺失值
all_features[numeric_features] = all_features[numeric_features].fillna(0)

# dummy_na=True将缺失值也当作合法的特征值并为其创建指示特征
all_features = pd.get_dummies(all_features, dummy_na=True)

n_train = train_data.shape[0]  # 训练集数据长度
print("n_train:", n_train)
# train_features = torch.tensor(all_features[:n_train].values, dtype=torch.float)
train_features = torch.tensor(all_features[:n_train].to_numpy(dtype=float), dtype=torch.float)
# test_features = torch.tensor(all_features[n_train:].values, dtype=torch.float)
test_features = torch.tensor(all_features[n_train:].to_numpy(dtype=float), dtype=torch.float)
# train_labels = torch.tensor(train_data.SalePrice.values, dtype=torch.float).view(-1, 1)
train_labels = torch.tensor(train_data.SalePrice.to_numpy(dtype=float), dtype=torch.float).view(-1,1)

loss = torch.nn.MSELoss()

def get_net(feature_num):
    net = nn.Linear(feature_num, 1)
    for param in net.parameters():
        nn.init.normal_(param, mean=0, std=0.01)
    return net

# 对数均方根误差
def log_rmse(net, features, labels):
    with torch.no_grad():
        # 将小于1的值设成1,使得取对数时数值更稳定
        clipped_preds = torch.max(net(features), torch.tensor(1.0))
        rmse = torch.sqrt(loss(clipped_preds.log(), labels.log()))
    return rmse.item()

def train(net, train_features, train_labels, test_features, test_labels, num_epochs, learning_rate, weight_decay, batch_size):
    train_ls, test_ls = [], []
    dataset = torch.utils.data.TensorDataset(train_features, train_labels)
    train_iter = torch.utils.data.DataLoader(dataset, batch_size, shuffle=True)
    # 这里使用了Adam优化算法
    optimizer = torch.optim.Adam(params=net.parameters(), lr=learning_rate, weight_decay=weight_decay)
    net = net.float()
    for epoch in range(num_epochs):
        for X, y in train_iter:
            l = loss(net(X.float()), y.float())
            optimizer.zero_grad()
            l.backward()
            optimizer.step()
        train_ls.append(log_rmse(net, train_features, train_labels))
        if test_labels is not None:
            test_ls.append(log_rmse(net, test_features, test_labels))
    return train_ls, test_ls

# K折交叉验证,返回第i折交叉验证时所需要的训练和验证数据
def get_k_fold_data(k, i, X, y):
    # 返回第i折交叉验证时所需要的训练和验证数据
    assert k > 1
    fold_size = X.shape[0] // k
    X_train, y_train = None, None
    for j in range(k):
        idx = slice(j * fold_size, (j + 1) * fold_size)
        X_part, y_part = X[idx, :], y[idx]
        if j == i:
            X_valid, y_valid = X_part, y_part
        elif X_train is None:
            X_train, y_train = X_part, y_part
        else:
            X_train = torch.cat((X_train, X_part), dim=0)
            y_train = torch.cat((y_train, y_part), dim=0)
    return X_train, y_train, X_valid, y_valid

# K折交叉验证中返回训练和验证的平均误差
def k_fold(k, X_train, y_train, num_epochs, learning_rate, weight_decay, batch_size):
    train_l_sum, valid_l_sum = 0, 0
    for i in range(k):
        data = get_k_fold_data(k, i, X_train, y_train)
        net = get_net(X_train.shape[1])
        train_ls, valid_ls = train(net, *data, num_epochs, learning_rate, weight_decay, batch_size)
        train_l_sum += train_ls[-1]
        valid_l_sum += valid_ls[-1]
        if i == 0:
            d2l.semilogy(range(1, num_epochs + 1), train_ls, 'epochs', 'rmse',
                         range(1, num_epochs + 1), valid_ls, ['train', 'valid'])
        # 训练集均方根误差和测试集均方根误差
        print('fold %d, train rmse %f, valid rmse %f' % (i, train_ls[-1], valid_ls[-1]))
    return train_l_sum / k, valid_l_sum / k

k, num_epochs, lr, weight_decay, batch_size = 5, 100, 5, 0, 64
train_l, valid_l = k_fold(k, train_features, train_labels, num_epochs, lr, weight_decay, batch_size)
print('%d-fold validation: avg train rmse %f, avg valid rmse %f' % (k, train_l, valid_l))

运行结果如下图所示:

总结

本周的学习到此结束,初步形成了用PyTorch编写深度学习算法的模型,还需要通过长时间的练习来熟练掌握编写流程。下周将继续学习用PyTorch进行深度学习的计算,以及CNN的编写。

如有错误,请大佬们指出,谢谢! 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值