快速入门

Pytorch官网:快速入门Pytorch地址:
https://pytorch.org/tutorials/beginner/basics/intro.html

步骤:
1.数据准备——2. 模型选择/开发——3. 模型训练调优——4. 模型评估测试

1. 数据准备:加载数据

https://pytorch.org/tutorials/beginner/basics/data_tutorial.html

import torch
from torch import nn
from torch.utils.data import DataLoader # 处理数据API:torch.utils.data.DataLoader and torch.utils.data.Dataset
                                        # Dataset存储示例及其对应的标签
                                        # DataLoader将可迭代对象包装在Dataset周围。
from torchvision import datasets    #PyTorch提供特定于域的库,如TorchText、TorchVision和TorchAudio,所有这些都包括数据集。
                                    #每个TorchVision数据集包含两个参数:Transform和Target_Transform,分别用于修改样本和标签。
from torchvision.transforms import ToTensor, Lambda, Compose
import matplotlib.pyplot as plt

# Download training data from open datasets.
training_data = datasets.FashionMNIST(
    root="data",
    train=True,
    download=True,
    transform=ToTensor(),
)
# Download test data from open datasets.
test_data = datasets.FashionMNIST(
    root="data",
    train=False,
    download=True,
    transform=ToTensor(),
)
# 加载数据:https://pytorch.org/tutorials/beginner/basics/data_tutorial.html
# 我们将Dataset作为参数传递给DataLoader。
# 这将在我们的数据集上封装一个可迭代的包,并支持自动批处理、采样、洗牌和多进程数据加载。
# 在这里,我们定义了一个64的批处理大小batch size,即dataloader迭代中的每个元素都将返回一个由64个特性和标签组成的批处理。
batch_size = 64

# Create data loaders.
train_dataloader = DataLoader(training_data, batch_size=batch_size)
test_dataloader = DataLoader(test_data, batch_size=batch_size)

for X, y in test_dataloader:
    print("Shape of X [N, C, H, W]: ", X.shape)
    print("Shape of y: ", y.shape, y.dtype)
    break

2. 模型选择/开发

# 创建模型
#构建神经网络:https://pytorch.org/tutorials/beginner/basics/buildmodel_tutorial.html
# PyTorch中定义一个神经网络:需要创建了一个继承自nn.Module的类。
# 我们在__init_函数中定义网络层,并指定数据在forward函数中通过网络。
# 为了加速神经网络中的操作,如果有可用的话,我们将其移动到GPU。
# Get cpu or gpu device for training.
device = "cuda" if torch.cuda.is_available() else "cpu"
print("Using {} device".format(device))

# Define model
class NeuralNetwork(nn.Module):
    def __init__(self):
        super(NeuralNetwork, self).__init__()
        self.flatten = nn.Flatten()
        self.linear_relu_stack = nn.Sequential(
            nn.Linear(28*28, 512),
            nn.ReLU(),
            nn.Linear(512, 512),
            nn.ReLU(),
            nn.Linear(512, 10),
            nn.ReLU()
        )

    def forward(self, x):
        x = self.flatten(x)
        logits = self.linear_relu_stack(x)
        return logits

model = NeuralNetwork().to(device)
print(model)

3. 模型训练调优

# 优化模型参数:https://pytorch.org/tutorials/beginner/basics/optimization_tutorial.html
# 要训练模型,我们需要一个损失函数和一个优化器。
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)
# 在单个训练循环中,模型对训练数据集进行预测(分批反馈),并反向传播预测误差以调整模型的参数。
def train(dataloader, model, loss_fn, optimizer):
    size = len(dataloader.dataset)
    for batch, (X, y) in enumerate(dataloader):
        X, y = X.to(device), y.to(device)

        # Compute prediction error
        pred = model(X)
        loss = loss_fn(pred, y)

        # Backpropagation
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if batch % 100 == 0:
            loss, current = loss.item(), batch * len(X)
            print(f"loss: {loss:>7f}  [{current:>5d}/{size:>5d}]")

4. 模型评估测试

# 根据测试数据集检查模型的性能
def test(dataloader, model):
    size = len(dataloader.dataset)
    model.eval()
    test_loss, correct = 0, 0
    with torch.no_grad():
        for X, y in dataloader:
            X, y = X.to(device), y.to(device)
            pred = model(X)
            test_loss += loss_fn(pred, y).item()
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()
    test_loss /= size
    correct /= size
    print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")


# 训练过程经过几次迭代。在每次迭代,模型都会学习参数,以做出更好的预测。
# 在每个epoch打印模型的准确性和损失;我们希望看到准确性随着每个时代的增长而增加,损失减少。
epochs = 5
for t in range(epochs):
    print(f"Epoch {t+1}\n-------------------------------")
    train(train_dataloader, model, loss_fn, optimizer)
    test(test_dataloader, model)
print("Done!")

保存模型、加载模型

# 保存模型
# 保存模型的常见方法是序列化内部状态字典(包含模型参数)。
torch.save(model.state_dict(), "model.pth")
print("Saved PyTorch Model State to model.pth")



# 加载模型
# 加载模型的过程包括重新创建模型结构并将状态字典加载到其中。
model = NeuralNetwork()
model.load_state_dict(torch.load("model.pth"))

classes = [
    "T-shirt/top",
    "Trouser",
    "Pullover",
    "Dress",
    "Coat",
    "Sandal",
    "Shirt",
    "Sneaker",
    "Bag",
    "Ankle boot",
]

model.eval()
x, y = test_data[0][0], test_data[0][1]
with torch.no_grad():
    pred = model(x)
    predicted, actual = classes[pred[0].argmax(0)], classes[y]
    print(f'Predicted: "{predicted}", Actual: "{actual}"')

Tensors张量

Tensors是一种与数组和矩阵非常相似的专门数据结构。在PyTorch中,我们使用张量对模型的输入和输出以及模型的参数进行编码。张量与NumPy的ndarray相似,只是张量可以在GPU或其他硬件加速器上运行。

import torch
import numpy as np


# 初始化张量Tensor
#法一: 使用数据,初始化Tensor
data = [[1, 2],[3, 4]]
x_data = torch.tensor(data)
print("数据初始化Tensor",x_data.shape)
#法二: NumPy数组,初始化Tensor
np_array = np.array(data)
x_np = torch.from_numpy(np_array)
#法三:另一个张量,初始化Tensor
x_ones = torch.ones_like(x_data) # retains the properties of x_data
print(f"Ones Tensor: \n {x_ones} \n")
x_rand = torch.rand_like(x_data, dtype=torch.float) # overrides the datatype of x_data
print(f"Random Tensor: \n {x_rand} \n")
#法四:使用随机值或常量值,初始化Tensor
shape = (2,3,) #shape决定了输出张量的尺寸。
rand_tensor = torch.rand(shape)
ones_tensor = torch.ones(shape)
zeros_tensor = torch.zeros(shape)

print(f"Random Tensor: \n {rand_tensor} \n")
print(f"Ones Tensor: \n {ones_tensor} \n")
print(f"Zeros Tensor: \n {zeros_tensor}")


# 张量属性——描述它们的形状、数据类型和存储它们的设备。
tensor = torch.rand(3,4)

print(f"Shape of tensor: {tensor.shape}")
print(f"Datatype of tensor: {tensor.dtype}")
print(f"Device tensor is stored on: {tensor.device}")


# 张量操作:https://pytorch.org/docs/stable/torch.html。100多个张量运算,包括算术、线性代数、矩阵操作(转置、索引、切片)、采样等。
# 默认情况下,张量是在CPU上创建的。我们需要使用.to方法显式地将张量移动到GPU上(在检查GPU可用性后)。
if torch.cuda.is_available():
  tensor = tensor.to('cuda')

# 标准数字索引和切片:
tensor = torch.ones(4, 4)
print('First row: ',tensor[0])
print('First column: ', tensor[:, 0])
print('Last column:', tensor[..., -1])
tensor[:,1] = 0
print(tensor)

# 连接多个tensor
t1 = torch.cat([tensor, tensor, tensor], dim=1)
print(t1)

# 算数运算
#通过将张量的所有值聚合为一个值,您可以使用item()将其转换为Python数值:
agg = tensor.sum()
agg_item = agg.item()
print(agg_item, type(agg_item))

# 代替操作——加,在之前基础上+5
print(tensor, "\n")
tensor.add_(5)
print(tensor)

# 和Numpy共享底层数据
# 张量到Numpy数组
t = torch.ones(5)
print(f"t: {t}")  # t: tensor([1., 1., 1., 1., 1.])
n = t.numpy()
print(f"n: {n}")  #  n: [1. 1. 1. 1. 1.]
#张量的变化反映在NumPy数组中。
t.add_(1)
print(f"t: {t}")
print(f"n: {n}")

# NumPy数组到张量
n = np.ones(5)
t = torch.from_numpy(n)
# NumPy数组的变化反映在张量中。
np.add(n, 1, out=n)
print(f"t: {t}")
print(f"n: {n}")

Dataset和DataLoader 数据集和数据加载器

我们希望数据集代码与模型训练代码分离,以获得更好的可读性和模块化。pytorch提供了torch.utils.data.DataLoader和torch.utils.data.Dataset,允许使用预加载的数据集以及您自己的数据。Dataset存储样本及其对应的标签,DataLoader将一个可迭代对象包装在数据集周围,以便方便地访问样本。
PyTorch域库提供了许多预加载的数据集(比如fashionmist),它们是torch.utils.data的子类。数据集和实现特定数据的函数。它们可以用来制作模型的原型和基准测试。例如: Image Datasets, Text Datasets, and Audio Datasets

从 TorchVision 加载 Fashion-MNIST 数据集的示例。Fashion-MNIST是Zalando文章图像的数据集,由60,000个培训示例和10,000个测试示例组成。每个示例包括28×28灰度图像和来自10个类别之一的相关标签。

import torch
from torch.utils.data import Dataset
from torchvision import datasets
from torchvision.transforms import ToTensor, Lambda
import matplotlib.pyplot as plt

# 加载数据集
# 加载带有以下参数的FashionMNIST数据集:
# root是存储训练/测试数据的路径,
# train指定训练或测试数据集,
# download=True downloads the data from the internet if it’s not available at root.
# transform和target_transform指定功能和标签转换

training_data = datasets.FashionMNIST(
    root="data",
    train=True,
    download=True,
    transform=ToTensor()
)

test_data = datasets.FashionMNIST(
    root="data",
    train=False,
    download=True,
    transform=ToTensor()
)

# 迭代和可视化数据集
labels_map = {
    0: "T-Shirt",
    1: "Trouser",
    2: "Pullover",
    3: "Dress",
    4: "Coat",
    5: "Sandal",
    6: "Shirt",
    7: "Sneaker",
    8: "Bag",
    9: "Ankle Boot",
}
figure = plt.figure(figsize=(8, 8))
cols, rows = 3, 3
for i in range(1, cols * rows + 1):
    sample_idx = torch.randint(len(training_data), size=(1,)).item()
    img, label = training_data[sample_idx]
    figure.add_subplot(rows, cols, i)
    plt.title(labels_map[label])
    plt.axis("off")
    plt.imshow(img.squeeze(), cmap="gray")
plt.show()

# 创建自定义数据集
# 必须包含__init__, __len__, and __getitem__三个方法。
import os
import pandas as pd
from torchvision.io import read_image

class CustomImageDataset(Dataset):
    def __init__(self, annotations_file, img_dir, transform=None, target_transform=None):#初始化包含图像、注释文件和两个转换的目录
        self.img_labels = pd.read_csv(annotations_file)# 照片名,label:tshirt1.jpg, 0
        self.img_dir = img_dir
        self.transform = transform
        self.target_transform = target_transform

    def __len__(self):#函数返回数据集中的样本数量
        return len(self.img_labels)

    def __getitem__(self, idx): #从给定索引idx的数据集加载并返回示例。
        img_path = os.path.join(self.img_dir, self.img_labels.iloc[idx, 0])
        image = read_image(img_path)#基于索引,标识图像在磁盘上的位置,并使用read_image将其转换为张量
        label = self.img_labels.iloc[idx, 1]#从self.img_labels中的csv数据中检索相应的标签
        if self.transform:
            image = self.transform(image)
        if self.target_transform:
            label = self.target_transform(label)
        sample = {"image": image, "label": label}
        return sample



# DataLoaders为训练准备数据,并可以根据需要迭代该数据集。
# 在训练模型时,我们通常希望以“小批量”的方式传递样本,在每个epoch重新洗刷数据以减少模型过拟合,并使用Python的多处理来加速数据检索。
# DataLoader是一个迭代器。
from torch.utils.data import DataLoader
#该数据集加载到Dataloader。小批量
train_dataloader = DataLoader(training_data, batch_size=64, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size=64, shuffle=True)

# 通过DataLoader迭代
#下面的每次迭代都返回一批train_features和train_labels ' '(分别包含' ' batch_size=64个特性和标签)。
# shuffle=True,在遍历所有批次之后,数据将被打乱(为了更细粒度地控制数据加载顺序)。
# Display image and label.
train_features, train_labels = next(iter(train_dataloader))
print(f"Feature batch shape: {train_features.size()}")
print(f"Labels batch shape: {train_labels.size()}")
img = train_features[0].squeeze()
label = train_labels[0]
plt.imshow(img, cmap="gray")
plt.show()
print(f"Label: {label}")

Transformers

我们使用Transformers来对数据进行一些操作,并使其适合训练。所有TorchVision数据集都有两个参数——transform以修改功能和target_transform以修改标签——它们接受包含转换逻辑的可调用参数。torchvision.transforms模块提供了几种常用的开箱即用转换。
例如:FashionMNIST功能采用PIL图像格式,标签为整数。对于训练,我们需要normalized标准化张量功能,以及one-hot编码的张量标签。为了进行这些转换,我们使用ToTensor和Lambda。
其中ToTensor(),将PIL图像或NumPy ndarray转换为FloatTensor。并将图像的像素强度值缩放到[0.,1]范围内。
其中Lambda,定义一个函数来将整数转换为一个one-hot编码张量。它首先创建一个大小为10的零张量(数据集中标签的数量),并调用scatter_,该函数在标签y给定的索引上赋值为1。

import torch
from torchvision import datasets
from torchvision.transforms import ToTensor, Lambda

ds = datasets.FashionMNIST(
    root="data",
    train=True,
    download=True,
    transform=ToTensor(),#normalized标准化,将PIL图像或NumPy ndarray转换为FloatTensor。并将图像的像素强度值缩放到[0.,1]范围内。
    target_transform=Lambda(lambda y: torch.zeros(10, dtype=torch.float).scatter_(0, torch.tensor(y), value=1))#定义一个函数来将整数转换为一个one-hot编码
                                        #它首先创建一个大小为10的零张量(数据集中标签的数量),并调用scatter_,该函数在标签y给定的索引上赋值为1。
)

BUILD THE NEURAL NETWORK建立神经网络

torch.nn命名空间提供了构建自己的神经网络所需的所有构建块。torch.nn.Module是所有神经网络模块的基类。神经网络是由其他模块(层)组成的模块本身。
例子:将建立一个神经网络,对FashionMNIST数据集中的图像进行分类。

import os
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

#希望能够在GPU等硬件加速器上训练我们的模型,让我们检查 torch.cuda 是否可用,否则我们将继续使用 CPU。
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print('Using {} device'.format(device))


# Define the Class
#我们通过神经网络子类 nn.Module 来定义神经网络。
# 在__init__中初始化神经网络层。每一个nn.Module子类实现了forward方法中对输入数据的操作。
class NeuralNetwork(nn.Module):#定义神经网络,继承自 nn.Module
    def __init__(self):#初始化
        super(NeuralNetwork, self).__init__()
        self.flatten = nn.Flatten()# 将每个2D 28x28图像转换为一个由784像素值组成的连续数组
        self.linear_relu_stack = nn.Sequential(#Sequential是一个有序的模块容器。数据按照定义的相同顺序通过所有模块。您可以使用顺序容器来组合一个像seq_modules这样的快速网络。
            nn.Linear(28*28, 512),#使用weights 和 biases.对输入应用线性变换的模块。
            nn.ReLU(),#非线性激活在模型的输入和输出之间创建了复杂的映射。它们在线性变换后应用于引入非线性,
            nn.Linear(512, 512),
            nn.ReLU(),
            nn.Linear(512, 10),
            nn.ReLU()
        )

    def forward(self, x):#自动调用model.forward()
        x = self.flatten(x)
        logits = self.linear_relu_stack(x)
        return logits

model = NeuralNetwork().to(device)#创建实例自定义的神经网络
print(model)

#调用model,将返回一个10维张量,其中包含每个类的原始预测值。我们通过nn.Softmax得到预测概率。
X = torch.rand(1, 28, 28, device=device)
logits = model(X)
pred_probab = nn.Softmax(dim=1)(logits)#logits被缩放到值[0,1]代表模型对每个类的预测概率。dim参数表示数值之和必须为1的维度。
y_pred = pred_probab.argmax(1)
print(f"Predicted class: {y_pred}")


#在训练过程中优化了相关的权重和偏差,nn.Module模块自动跟踪模型对象中定义的所有字段
# ,并使用模型的parameters()或named_parameters()方法访问所有参数。
print("Model structure: ", model, "\n\n")

for name, param in model.named_parameters():
    print(f"Layer: {name} | Size: {param.size()} | Values : {param[:2]} \n")

Autograd

在训练神经网络时,最常用的算法是反向传播算法。在该算法中,根据损失函数相对于给定参数的梯度来调整参数(模型权值)。为了计算这些梯度,PyTorch内置了一个名为torch.autograd的差异化引擎。它支持自动计算梯度为任何计算图形。

import torch

x = torch.ones(5)  # input tensor
y = torch.zeros(3)  # expected output
w = torch.randn(5, 3, requires_grad=True)# 默认情况下,所有require_grad =True的张量都在跟踪它们的计算历史,并支持梯度计算。
b = torch.randn(3, requires_grad=True)
z = torch.matmul(x, w)+b
loss = torch.nn.functional.binary_cross_entropy_with_logits(z, y)

print('Gradient function for z =',z.grad_fn)#grad_fn存储反向传播函数的引用;
print('Gradient function for loss =', loss.grad_fn)

loss.backward()#计算导数,loss.backward(),从w.grad和b.grad中检索值。
print(w.grad)
print(b.grad)

OPTIMIZING MODEL PARAMETERS 优化模型参数

#先决条件代码
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor, Lambda

training_data = datasets.FashionMNIST(
    root="data",
    train=True,
    download=True,
    transform=ToTensor()
)

test_data = datasets.FashionMNIST(
    root="data",
    train=False,
    download=True,
    transform=ToTensor()
)

train_dataloader = DataLoader(training_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)

class NeuralNetwork(nn.Module):
    def __init__(self):
        super(NeuralNetwork, self).__init__()
        self.flatten = nn.Flatten()
        self.linear_relu_stack = nn.Sequential(
            nn.Linear(28*28, 512),
            nn.ReLU(),
            nn.Linear(512, 512),
            nn.ReLU(),
            nn.Linear(512, 10),
            nn.ReLU()
        )

    def forward(self, x):
        x = self.flatten(x)
        logits = self.linear_relu_stack(x)
        return logits

model = NeuralNetwork()


# Hyperparameters 超参数是可调参数,让您控制模型优化过程。不同的超参数值可以影响模型训练和收敛速度。
#为训练定义了以下超参数:
# epoch的数量——在数据集上迭代的次数
# Batch Size-模型在每个epoch 中看到的数据样本的数量
# Learning Rate-在每个batch/epoch更新模型参数的次数。较小的值产生缓慢的学习速度,而较大的值可能导致在训练过程中不可预测的行为
learning_rate = 1e-3
batch_size = 64
epochs = 5

# 一旦我们设置了超参数,我们就可以用一个优化循环来训练和优化我们的模型。优化循环的每次迭代称为epoch。
#每个epoch包括两个主要部分:
# 训练循环-迭代训练数据集,并尝试收敛到最优参数。
# 验证/测试循环——遍历测试数据集以检查模型性能是否得到改善。

# 损失函数Loss Function
#当使用一些训练数据时,未经训练的网络很可能给出不正确的答案。
# 损失函数用来度量得到的结果与目标值的不相似程度,它是我们在训练过程中希望最小化的损失函数。
# 为了计算损失,我们使用给定数据样本的输入进行预测,并将其与真实数据标签值进行比较。
# 常见的损失函数:nn.MSELoss (Mean Square Error)用于回归任务;nn.NLLLoss (Negative Log Likelihood)用于分类任务。
# nn.CrossEntropyLoss 结合了 nn.LogSoftmax、nn.NLLLoss.

# Initialize the loss function
loss_fn = nn.CrossEntropyLoss()# 模型的输出logit传递给nn.CrossEntropyLoss,它将标准化logit并计算预测误差。


# 优化器Optimizer
#优化是在每个训练步骤中调整模型参数以减少模型误差的过程。
# 优化算法定义了如何执行这个过程(在本例中,我们使用了随机梯度下降)。
# 所有优化逻辑都封装在优化器对象中。这里,我们使用SGD优化器;此外,PyTorch中还有许多不同的优化器,如ADAM和RMSProp,它们更适合于不同类型的模型和数据。
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)# 初始化需要训练的模型参数来初始化优化器,并通过学习速率超参数。
# 在训练循环中,优化分为三个步骤:
# 调用optimizer.zero_grad()重置模型参数的梯度。渐变默认为累加;为了防止重复计算,我们在每次迭代时显式地将它们置零。
# 通过调用loss.backward()来反向传播预测损失。
# 有了梯度之后,我们调用optimizer.step()通过向后传递收集的梯度来调整参数。
def train_loop(dataloader, model, loss_fn, optimizer):# 优化代码
    size = len(dataloader.dataset)
    for batch, (X, y) in enumerate(dataloader):
        # Compute prediction and loss
        pred = model(X)
        loss = loss_fn(pred, y)

        # Backpropagation
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if batch % 100 == 0:
            loss, current = loss.item(), batch * len(X)
            print(f"loss: {loss:>7f}  [{current:>5d}/{size:>5d}]")


def test_loop(dataloader, model, loss_fn):# 根据测试数据评估模型性能
    size = len(dataloader.dataset)
    test_loss, correct = 0, 0

    with torch.no_grad():
        for X, y in dataloader:
            pred = model(X)
            test_loss += loss_fn(pred, y).item()
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()

    test_loss /= size
    correct /= size
    print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")

loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)

epochs = 10
for t in range(epochs):
    print(f"Epoch {t+1}\n-------------------------------")
    train_loop(train_dataloader, model, loss_fn, optimizer)
    test_loop(test_dataloader, model, loss_fn)
print("Done!")

SAVE AND LOAD THE MODEL 保存加载模型

import torch
import torch.onnx as onnx
import torchvision.models as models

# PyTorch模型将学习到的参数存储在名为state_dict的内部状态字典中。这些可以通过torch.save 保存方法:
model = models.vgg16(pretrained=True)
torch.save(model.state_dict(), 'model_weights.pth')

#要加载模型权重,您需要首先创建同一模型的实例,然后使用load_state_dict()方法加载参数。
model = models.vgg16() # we do not specify pretrained=True, i.e. do not load default weights
model.load_state_dict(torch.load('model_weights.pth'))
model.eval()

#将类的结构与模型一起保存,在这种情况下,我们可以将model(而不是model.state_dict())传递给保存函数:
torch.save(model, 'model.pth')

model = torch.load('model.pth')#加载模型:
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐