Quickstart in PyTorch

概述

​ Pytorch官网中的Quickstart部分简要说明了使用pytorch中的FashionMNIST数据集,构建并训练一个简单的分类网络的过程。下文是对这一过程的具体实践。具体的实战部分主要包括库的引入、加载数据、构建神经网络、设置损失函数和优化器、调整训练和测试的参数、训练、模型的存储、加载和预测。

实战部分

1 导入库

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


​pytorch​​中包含最基本的函数库​​torch​​。在这个库中有许多包。

​torch.nn​​中提供了我们在构建神经网络中所需要的一系列层。

​torch.utils.data.Datasets​​ 和 ​​torch.utils.data.DataLoader​​是两个最基础的功能包。前者包括torch中储存着的一些数据样本和对应的标签,后者在对这些数据样本和标签进行包装处理后传递给train和test函数进行交叉验证和测试。本文中只使用后者来对我们的数据集进行处理。

与此同时,​​Pytorch​​也提供一些具有针对视频,音频,文本的数据集,如​​torchvision​​、​​torchtext​​、​​torchaudio​​,它们包含了图片数据、文本信息、音频信息,本文中所采用的​​FashionMNIST​​数据集就是存储在​​torchvision​​下的​​datasets​​中。

​torchvision.transforms.ToTensor​​让我们能够将​​PIL(Python Image Library)​​图像转化为​​Tensor​​张量的图像。

​matplotlib.pyplot​​用于让我们做数据的可视化功能。

2 加载数据

​FashionMNIST​​包含有60,000个训练数据和10,000个测试数据,每一个数据都包含一个 \(28 pixel\times28pixel\times28pixel\)的灰度图像和一个代表样本类别的标签。我们需要将​​torchvision.datasets​​中的​​FashionMNIST​​导入进来,并使用DataLoader将其包裹成一个可迭代的对象。

# download training data from datasets
training_data = datasets.FashionMNIST(root='data',
train=True,
download=True,
transform=ToTensor())

# download test data from datasets
test_data = datasets.FashionMNIST(root='data',
train=False,
download=True,
transform=ToTensor())


​root​​表示数据集的存储路径,若不存在指定的文件夹,将自行创建。

​train​​表明该数据是数据集中的训练数据还是测试数据。

​download​​表示是否下载,如果其值为​​True​​,那么将检测​​root​​下的文件夹是否含有数据集,如果没有,将下载数据集,如果有,不再重复下载;如果其值为​​False​​,不下载数据集。

​transform​​指定了样本的转换格式。

​target_transform​​指定了标签的转换格式(示例代码中并没有特殊指定)。

​ToTensor()​​函数官方文档如下:

class ToTensor(object):
"""Convert a ``PIL Image`` or ``numpy.ndarray`` to tensor.

Converts a PIL Image or numpy.ndarray (H x W x C) in the range
[0, 255] to a torch.FloatTensor of shape (C x H x W) in the range [0.0, 1.0].
"""


​ToTensor()​​可以将PIL图像或Numpy数组转换成​​FloatTensor()​​,并将图像的像素密度值压缩到[0.,1.]区间。

# define batch size
batch_size = 16
# create data loaders
train_dataloader = DataLoader(training_data, batch_size=batch_size, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size=batch_size, shuffle=True)


​batch_size​​表示在训练或者测试过程中,一次喂入的数据量,对本例中的​​FashionMNIST​​来说,训练集中有60,000个样本数据,一次喂入16个样本,那么总共需要 \(60,000 \div 16 = 3750\) 个​​batch​​才能将训练数据全部喂进网络中。使用batch的一部分原因是,通过设置合理的​​batch_size​​值,我们可以提高内存的利用率,提高训练速度,并使梯度下降更为准确。

​shuffle​​表示打乱数据集中的数据,通常针对一个数据集,我们会遍历多次在下文所述的优化器中进行迭代,每一次称为一个epoch,为了提高网络的泛用性,降低过拟合程度,我们需要将每一个epoch中的样本打乱。

3 构建网络

在导入数据后,我们就可以开始着手构建我们的网络了,本文中的神经网络结构较为简单,主要就是全连接层和激活函数ReLU的堆叠。

# chose CPU or GPU for training
if torch.cuda.is_available():
device = 'cuda'
else:
device = 'cpu'
print(f'You are currently using {device}.')


​torch.cuda.is_available()​​判断cuda是否可用,进而选择使用GPU或CPU进行训练。

# define model
class NeuralNetwork(nn.Module):
def __init__(self):
super().__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)


我们继承​​nn.Module​​并在​​__init__​​下定义自己的网络结构,如上代码所示,我们在__init__下定义了一个​​self.flatten​​和一个​​self.linear_relu_stack​​。

​self.flatten​​的作用为将 \(m \times n\) 的图像变为 \(1 \times (m*n)\) 的图像,即将图像“摊平”,以送入全连接层。在本例中,即将 \(28 \times 28\) 的图像变为 \(1 \times 784\)的图像。

​self.linear_relu_stack​​中的结构非常简单:

\(784 \times 512 - Linear \Longrightarrow RuLU \Longrightarrow 512 \times 512 - Linear \Longrightarrow ReLU \Longrightarrow 512 \times 10 - Linear \Longrightarrow ReLU\)

因为最后​​FashionMNIST​​中有10个类,因此最后的输出也为10个数。

最后,在​​forward​​中,我们应用上述结构,构造一个前向通路。并根据先前选择的​​device​​部署模型,打印模型结构。

3.1 全连接层

在 CNN 中,全连接常出现在最后几层,用于对前面设计的特征做加权和。比如 ​​mnist​​,前面的卷积和池化相当于做特征工程,后面的全连接相当于做特征加权。全连接层​​(fully connected layers,FC)​​在整个卷积神经网络中起到“分类器”的作用。如果说卷积层、池化层和激活函数层等操作是将原始数据映射到隐层特征空间的话,全连接层则起到将学到的“分布式特征表示”映射到样本标记空间的作用。

3.2 激活函数ReLU


\[ReLU(x) = max(0, x)\]


ReLU激活函数比起其它的激活函数有以下几个优点

  1. 克服梯度消失的问题
  2. 加快训练速度

缺点

  1. 输入负数,则完全不激活,ReLU函数死掉。
  2. ReLU函数输出要么是0,要么是正数,也就是ReLU函数不是以0为中心的函数

4 损失函数和优化器

Pytorch提供了丰富的损失函数和优化器,在这里我们使用​​nn.CrossEntropyLoss()​​作为损失函数,​​torch.optim.SGD()​​作为优化器。

# define loss function
loss_function = nn.CrossEntropyLoss()

# define optimizer
optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)


4.1 损失函数

​nn.CrossEntropyLoss​​是​​pytorch​​中一个常用的损失函数,它结合了​​nn.LogSoftmax()​​和​​nn.NLLLoss()​​两个函数。它在做分类训练的时候是非常有用的。在训练过程中,对于每个类分配权值,可选的参数权值应该是一个1D张量。

该函数的交叉熵并不是采用


\[H(p, q) = -\sum_x (p(x)log(q(x)) + (1-p(x))log(1-q(x)))\]


而是采用


\[H(p, q) = -\sum_x (p(x)logq(x))\]


它是交叉熵的另外一种方式。

4.2 优化器

一言以蔽之,优化器就是在深度学习反向传播过程中,指引损失函数(目标函数)的各个参数往正确的方向更新合适的大小,使得更新后的各个参数让损失函数(目标函数)值不断逼近全局最小。这里我们使用SGD(随机最速下降法)优化器,这里有几篇优化器很好的文章:

​https://www.leiphone.com/category/yanxishe/c7nM342MTsWgau9f.html​

​https://zhuanlan.zhihu.com/p/32230623​

​torch.optim.SGD()​​是​​Pytorch​​中一个常用的优化器,

5 训练和测试

# define train loop
def train(dataloader, model, loss_function, optimizer):
size = len(dataloader.dataset)
for batch, (X, y) in enumerate(dataloader): # batch represents batch-th
X, y = X.to(device), y.to(device) # X represents img, while y represents label

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

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

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


​size​​获取数据集中所有样本的个数。

在for循环中,我们遍历​​dataloader​​中的所有数据,获取其索引及对应数据(注意dataloader中含有两项元素——样本及其对应的标签)。由于我们先前在​​train_dataloader​​中定义了​​batch_size​​,因此此处的​​batch​​代表的​​enumerate​​的索引值恰好表示了第几个​​batch​​。

接着我们调用​​loss_function​​计算​​loss​​,再利用优化器进行反向传播更新网络参数。

为了监督进程,对每100个​​batch​​,我们获取它当前的​​loss​​和当前进行到第几个数据,并打印出来。

# define test loop
def test(dataloader, model, loss_function):
size = len(dataloader.dataset) # numbers of the whole test images(10,000)
num_batches = len(dataloader) # 10,000 / batch size(16) = 625
model.eval()
sum_test_loss, sum_correct = 0, 0
with torch.no_grad():
for X, y in dataloader:
X, y = X.to(device), y.to(device)
pred = model(X)
sum_test_loss += loss_function(pred, y).item()
sum_correct += (pred.argmax(1) == y).type(torch.float).sum().item()
test_loss = sum_test_loss / num_batches
correct_ratio = 100 * (sum_correct / size)
print(f'Test Error: \n Accuracy: {(correct_ratio):>0.1f}%, Avg loss: {test_loss:>8f} \n')


为了观察经过训练后的模型的表现,我们还需要测试集来评估模型的好坏。

​size​​获取数据集中所有样本个数。

​num_batches​​获取​​batch​​个数,由于​​FashionMNIST​​中的测试集有10,000个样本,且我们此前设置的​​batch_size​​为16,因此​​batch​​为 \(10,000 \div 16 = 625\)。

​model.eval()​​的作用像是一种开关,由于一些层(比如​​Dropouts Layers​​、​​BatchNorm Layers​​)在训练和测试中的行为是不一样的,因此在测试时,我们需要​​model.eval()​​将它们置为测试(非训练)状态,与之相对的则是​​model.train()​​——置为训练状态。由于作者水平有限,其在本例中的作用尚不明确,欢迎有知道的大佬补充。

注意到在测试中,梯度信息是不需要的,因此​​with torch.no_grad()​​可以帮助我们停止计算梯度以节省算力。

为了方便后续理解,在这里给出打印后pred的大致结构

tensor([[0.1421, 0.0000, 0.0000, 0.0000, 0.0413, 0.4401, 0.0000, 0.7510, 0.8822, 0.0000],
[0.9166, 0.6223, 0.0000, 0.9044, 1.2210, 0.0000, 0.0000, 0.0000, 0.3413, 0.0000],
[0.2356, 0.0000, 0.0000, 0.0826, 0.5078, 0.0776, 0.0000, 0.0544, 0.4084, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000, 0.2197, 0.2384, 0.0000, 0.4583, 0.9327, 0.0000],
[1.0815, 1.9916, 0.0000, 1.9508, 0.3624, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.0574, 0.0000, 0.0000, 0.0000, 0.1917, 0.2527, 0.0000, 0.2819, 0.7821, 0.0000],
[1.8598, 2.3490, 0.0000, 2.5405, 0.7339, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.7836, 1.3618, 0.0000, 0.9962, 0.2862, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.5627, 0.2218, 0.0000, 0.2929, 0.1805, 0.0201, 0.0000, 0.0000, 0.0482, 0.0000],
[0.8809, 2.1661, 0.0000, 1.5107, 0.2718, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000, 0.3268, 0.5301, 0.0000, 1.5194, 1.8777, 0.0000],
[0.1255, 0.0000, 0.0000, 0.0000, 1.2370, 0.0451, 0.0000, 0.0322, 0.8576, 0.0000],
[0.7843, 0.2338, 0.0000, 0.6287, 0.6042, 0.0785, 0.0000, 0.0000, 0.2600, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.2876, 0.0000, 0.6852, 0.5046, 0.0000],
[0.4747, 0.0000, 0.0000, 0.0000, 0.6698, 0.1957, 0.0000, 0.2946, 0.9717, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.3904, 0.0000, 1.1054, 0.5773, 0.0000]])


上图展示的是一个​​batch​​中的数据,总共有625个​​batch​​。

​sum_test_loss​​表示所有的​​batch​​的​​loss​​的加和,一个​​batch​​的​​loss​​为该​​batch​​下16个​​loss​​的均值。

​(pred.argmax(1) == y).type(torch.float)​​表示返回​​pred​​的每一行最大值的索引,再与标签进行比较,相等返回1,反之返回0。

​sum_correct​​则是对上述所有的1的加和,表征了在​​test​​中模型预测正确的样本个数。

​test_loss​​表示平均损失,​​correct_ratio​​表示精确度。

6 开始训练

# start train
epochs = 5
for t in range(epochs):
print(f'Epoch {t + 1}\n----------------------------------')
train(train_dataloader, model, loss_function, optimizer)
test(test_dataloader, model, loss_function)


为了节省时间,这里我们只训练5个​​epoch​​,实际情况下根据个人需求自行设置。根据我的个人测试,在训练了50个epoch左右时,精度能达到85%。

7 模型的存储、加载和预测

# save model parameters
torch.save(model.state_dict(), 'model.pth')
print('Save Pytorch Model State to model.pth')


模型训练结束后,我们可以通过上述代码将其保存为​​.pth​​文件,注意这里仅仅保存了网络参数,不包括网络结构,若想既包含网络参数又包含网络结构可以去官方文档查找(我懒得找了)。

# load model
model = NeuralNetwork()
model.load_state_dict(torch.load('model.pth'))


加载保存在​​model.pth​​中的网络参数。

# make predictions
classes = ['T-shirts/top', 'Trousers', '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 = classes[pred.argmax(1)]
actual = classes[y]
print(f'Predicted: "{predicted}", Actual: "{actual}"')


上述代码展示了​​FashionMNIST​​中包含的10类物体名称,并根据我们构建的网络进行预测,代码比较简单,这里不再赘述,这里我们简单挑选了​​FashionMNIST​​测试集中的第一组数据作为预测对象。

本地运行

训练情况

You are currently using cpu.

NeuralNetwork(

(flatten): Flatten(start_dim=1, end_dim=-1)

(linear_relu_stack): Sequential(

(0): Linear(in_features=784, out_features=512, bias=True)

(1): ReLU()

(2): Linear(in_features=512, out_features=512, bias=True)

(3): ReLU()

(4): Linear(in_features=512, out_features=10, bias=True)

(5): ReLU()

)

)

本地运行结果告诉我们:

我们正在使用cpu(关于cpu,gpu,cudnn,cuda的简介)​

展现了我们搭建的神经网络的结构:

​ 第一层为张量范围为(1, -1)

​ 第二层为线性ReLU激活函数的堆栈,结构如上表。

训练数据

Epoch 1

loss:2.302129 [ 0/60000]

loss:2.287403 [ 1600/60000]

loss:2.279875 [ 3200/60000]

loss:2.282575 [ 4800/60000]

loss:2.277425 [ 6400/60000]

loss:2.269187 [ 8000/60000]

loss:2.273017 [ 9600/60000]

loss:2.246931 [11200/60000]

loss:2.234808 [12800/60000]

loss:2.251579 [14400/60000]

loss:2.262180 [16000/60000]

loss:2.241689 [17600/60000]

loss:2.252061 [19200/60000]

loss:2.211784 [20800/60000]

loss:2.193383 [22400/60000]

loss:2.095272 [24000/60000]

loss:2.235866 [25600/60000]

loss:2.253394 [27200/60000]

loss:2.113588 [28800/60000]

loss:2.182225 [30400/60000]

loss:2.172397 [32000/60000]

loss:2.028497 [33600/60000]

loss:2.012153 [35200/60000]

loss:2.139345 [36800/60000]

loss:2.203317 [38400/60000]

loss:1.971179 [40000/60000]

loss:2.013982 [41600/60000]

loss:2.149199 [43200/60000]

loss:1.889025 [44800/60000]

loss:2.163815 [46400/60000]

loss:1.980354 [48000/60000]

loss:1.919316 [49600/60000]

loss:2.101635 [51200/60000]

loss:2.031067 [52800/60000]

loss:1.847806 [54400/60000]

loss:2.007991 [56000/60000]

loss:1.780223 [57600/60000]

loss:1.941932 [59200/60000]

Test Error:

Accuracy: 45.6%, Avg loss: 1.932846

Epoch 2

loss:1.997460 [ 0/60000]

loss:1.804145 [ 1600/60000]

loss:1.938063 [ 3200/60000]

loss:1.777107 [ 4800/60000]

loss:1.871894 [ 6400/60000]

loss:1.761343 [ 8000/60000]

loss:1.780803 [ 9600/60000]

loss:2.035735 [11200/60000]

loss:1.813080 [12800/60000]

loss:1.919040 [14400/60000]

loss:1.630369 [16000/60000]

loss:2.077603 [17600/60000]

loss:1.786863 [19200/60000]

loss:1.789215 [20800/60000]

loss:1.609293 [22400/60000]

loss:1.703949 [24000/60000]

loss:1.516612 [25600/60000]

loss:1.980411 [27200/60000]

loss:1.728755 [28800/60000]

loss:1.664719 [30400/60000]

loss:1.723999 [32000/60000]

loss:1.910558 [33600/60000]

loss:1.653847 [35200/60000]

loss:1.270724 [36800/60000]

loss:1.931647 [38400/60000]

loss:1.722482 [40000/60000]

loss:1.392919 [41600/60000]

loss:1.526159 [43200/60000]

loss:1.446384 [44800/60000]

loss:1.414868 [46400/60000]

loss:1.995398 [48000/60000]

loss:1.909297 [49600/60000]

loss:1.918296 [51200/60000]

loss:1.320389 [52800/60000]

loss:1.661063 [54400/60000]

loss:1.390407 [56000/60000]

loss:1.147652 [57600/60000]

loss:1.845389 [59200/60000]

Test Error:

Accuracy: 52.0%, Avg loss: 1.570699

Epoch 3

loss:1.092074 [ 0/60000]

loss:1.741982 [ 1600/60000]

loss:1.369524 [ 3200/60000]

loss:1.323857 [ 4800/60000]

loss:1.266949 [ 6400/60000]

loss:1.562171 [ 8000/60000]

loss:1.563104 [ 9600/60000]

loss:1.229966 [11200/60000]

loss:1.535066 [12800/60000]

loss:1.250649 [14400/60000]

loss:1.820632 [16000/60000]

loss:1.524231 [17600/60000]

loss:1.468110 [19200/60000]

loss:1.420336 [20800/60000]

loss:1.714278 [22400/60000]

loss:1.513260 [24000/60000]

loss:2.120431 [25600/60000]

loss:1.365088 [27200/60000]

loss:1.666167 [28800/60000]

loss:1.297498 [30400/60000]

loss:1.615333 [32000/60000]

loss:1.401120 [33600/60000]

loss:1.402781 [35200/60000]

loss:1.373979 [36800/60000]

loss:1.190076 [38400/60000]

loss:1.719997 [40000/60000]

loss:1.338256 [41600/60000]

loss:1.791476 [43200/60000]

loss:1.531789 [44800/60000]

loss:1.537996 [46400/60000]

loss:1.278385 [48000/60000]

loss:1.564274 [49600/60000]

loss:1.360564 [51200/60000]

loss:1.240304 [52800/60000]

loss:1.212397 [54400/60000]

loss:1.112574 [56000/60000]

loss:1.184957 [57600/60000]

loss:1.850699 [59200/60000]

Test Error:

Accuracy: 55.1%, Avg loss: 1.415252

Epoch 4

loss:1.147449 [ 0/60000]

loss:1.560359 [ 1600/60000]

loss:1.543317 [ 3200/60000]

loss:1.730983 [ 4800/60000]

loss:1.174177 [ 6400/60000]

loss:1.445379 [ 8000/60000]

loss:1.562520 [ 9600/60000]

loss:1.897955 [11200/60000]

loss:1.960352 [12800/60000]

loss:1.199088 [14400/60000]

loss:0.863414 [16000/60000]

loss:1.398360 [17600/60000]

loss:1.527734 [19200/60000]

loss:1.379669 [20800/60000]

loss:1.472406 [22400/60000]

loss:1.820169 [24000/60000]

loss:1.146074 [25600/60000]

loss:1.472795 [27200/60000]

loss:1.709622 [28800/60000]

loss:1.476590 [30400/60000]

loss:1.154761 [32000/60000]

loss:1.410364 [33600/60000]

loss:2.256364 [35200/60000]

loss:1.148926 [36800/60000]

loss:1.419194 [38400/60000]

loss:1.519497 [40000/60000]

loss:0.849636 [41600/60000]

loss:1.398620 [43200/60000]

loss:1.133907 [44800/60000]

loss:0.922331 [46400/60000]

loss:1.535455 [48000/60000]

loss:1.321102 [49600/60000]

loss:1.795982 [51200/60000]

loss:0.979792 [52800/60000]

loss:0.818761 [54400/60000]

loss:1.346267 [56000/60000]

loss:0.953565 [57600/60000]

loss:1.072881 [59200/60000]

Test Error:

Accuracy: 57.2%, Avg loss: 1.336358

Epoch 5

loss:0.634092 [ 0/60000]

loss:1.802971 [ 1600/60000]

loss:1.657984 [ 3200/60000]

loss:1.600910 [ 4800/60000]

loss:1.257860 [ 6400/60000]

loss:1.261429 [ 8000/60000]

loss:1.332216 [ 9600/60000]

loss:1.221099 [11200/60000]

loss:0.950734 [12800/60000]

loss:1.075305 [14400/60000]

loss:1.640876 [16000/60000]

loss:1.257362 [17600/60000]

loss:1.077084 [19200/60000]

loss:1.382569 [20800/60000]

loss:1.685493 [22400/60000]

loss:1.042982 [24000/60000]

loss:1.566217 [25600/60000]

loss:1.312096 [27200/60000]

loss:0.845461 [28800/60000]

loss:1.557899 [30400/60000]

loss:1.157883 [32000/60000]

loss:1.782711 [33600/60000]

loss:1.142777 [35200/60000]

loss:1.118635 [36800/60000]

loss:1.291814 [38400/60000]

loss:1.557206 [40000/60000]

loss:1.018585 [41600/60000]

loss:0.958620 [43200/60000]

loss:1.060240 [44800/60000]

loss:0.582739 [46400/60000]

loss:1.147862 [48000/60000]

loss:1.558747 [49600/60000]

loss:1.661504 [51200/60000]

loss:1.073796 [52800/60000]

loss:1.314006 [54400/60000]

loss:0.881447 [56000/60000]

loss:1.528572 [57600/60000]

loss:1.275195 [59200/60000]

Test Error:

Accuracy: 58.5%, Avg loss: 1.282056

Save Pytorch Model State to model.pth

Predicted: "Sandal", Actual: "Ankle boot"

总结

可以看到,我们训练的loss并不是单调递减而是有一个总体的趋势下降,这主要跟我们运用的SGD算法特性有关。最后预测的结果是失败的,因为5个epoch对于整个神经网络来说还是不够,一般来说,数据量越大准确性越高,我们的神经网络初体验也只能算是浅尝辄止,期待下一步的学习。