飞桨(paddlepaddle )入门图像分类最强指南

简介: 飞桨(paddlepaddle )入门图像分类最强指南

项目传送门


零基础白话图像分类



课程传送门『领航团图像分类课程』

全流程白话解析传送门


什么是图像分类


图像分类是基于深度学习的cv分类任务。

核心是从给定的分类集合中给图像分配一个标签的任务。

实际上我们的任务是分析一个输入图像并返回一个图像分类的标签。

标签总是来自预定义的可能类别集。


说白了就是经过学习以后的模型可以划分图像的类别

就像刚出生的婴儿识别猫狗一样,也是后天大量数据学习得到的。


import paddle
print("paddle当前版本 " + paddle.__version__)
paddle当前版本 2.0.0


什么是数据如何处理数据


  • 什么是数据(以图为例)
    数据可以分为训练集、验证集和测试集 一般比例为6:2:2
    训练集就是给既定的模型喂数据(给孩子看大量的猫狗图片)
    验证集就是问大街上单独的一个猫或狗的数据然后对错误的进行纠正以便以下次更好的训练
    测试集就是当孩子熟悉了有关的特征以后自然的报出猫或狗


个人理解:训练集类似于平时学习的知识;验证集是周考月考,便于调整学习状态等;而测试集就是期末考试检验模型真实的水平



数据如何处理(以图为例)

根据不同的图我们要对模型进行设计,这里的数据就需要做归一化处理,图像是多大的,几通道等等

在paddle中可以通过继承paddle.io.Dataset修改里面的__getitem____init____len__进行调整

也可以直接导入官方已经处理好的数据集可以直接进行使用。


# 框架处理好的数据集查看
print('视觉相关数据集:', paddle.vision.datasets.__all__)
print('自然语言相关数据集:', paddle.text.datasets.__all__)
视觉相关数据集: ['DatasetFolder', 'ImageFolder', 'MNIST', 'FashionMNIST', 'Flowers', 'Cifar10', 'Cifar100', 'VOC2012']
自然语言相关数据集: ['Conll05st', 'Imdb', 'Imikolov', 'Movielens', 'UCIHousing', 'WMT14', 'WMT16']
from paddle.vision.transforms import ToTensor  # 导入ToTensor 用ToTensor将数据格式转为paddle Tensor
train_dataset = paddle.vision.datasets.MNIST(mode='train', transform=ToTensor())
print(train_dataset[0])  # 查看数据(paddle Tensor 大小为1,28,28)
(Tensor(shape=[1, 28, 28], dtype=float32, place=CPUPlace, stop_gradient=True,
       [[[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.01176471, 0.07058824, 0.07058824, 0.07058824, 0.49411768, 0.53333336, 0.68627453, 0.10196079, 0.65098041, 1., 0.96862751, 0.49803925, 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0., 0., 0.11764707, 0.14117648, 0.36862746, 0.60392159, 0.66666669, 0.99215692, 0.99215692, 0.99215692, 0.99215692, 0.99215692, 0.88235301, 0.67450982, 0.99215692, 0.94901967, 0.76470596, 0.25098041, 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0., 0.19215688, 0.93333340, 0.99215692, 0.99215692, 0.99215692, 0.99215692, 0.99215692, 0.99215692, 0.99215692, 0.99215692, 0.98431379, 0.36470589, 0.32156864, 0.32156864, 0.21960786, 0.15294118, 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0., 0.07058824, 0.85882360, 0.99215692, 0.99215692, 0.99215692, 0.99215692, 0.99215692, 0.77647066, 0.71372551, 0.96862751, 0.94509810, 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0., 0., 0.31372550, 0.61176473, 0.41960788, 0.99215692, 0.99215692, 0.80392164, 0.04313726, 0., 0.16862746, 0.60392159, 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.05490196, 0.00392157, 0.60392159, 0.99215692, 0.35294119, 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.54509807, 0.99215692, 0.74509805, 0.00784314, 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.04313726, 0.74509805, 0.99215692, 0.27450982, 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.13725491, 0.94509810, 0.88235301, 0.62745100, 0.42352945, 0.00392157, 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.31764707, 0.94117653, 0.99215692, 0.99215692, 0.46666670, 0.09803922, 0., 0., 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.17647059, 0.72941178, 0.99215692, 0.99215692, 0.58823532, 0.10588236, 0., 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.06274510, 0.36470589, 0.98823535, 0.99215692, 0.73333335, 0., 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.97647065, 0.99215692, 0.97647065, 0.25098041, 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.18039216, 0.50980395, 0.71764708, 0.99215692, 0.99215692, 0.81176478, 0.00784314, 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.15294118, 0.58039218, 0.89803928, 0.99215692, 0.99215692, 0.99215692, 0.98039222, 0.71372551, 0., 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.09411766, 0.44705886, 0.86666673, 0.99215692, 0.99215692, 0.99215692, 0.99215692, 0.78823537, 0.30588236, 0., 0., 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0., 0., 0.09019608, 0.25882354, 0.83529419, 0.99215692, 0.99215692, 0.99215692, 0.99215692, 0.77647066, 0.31764707, 0.00784314, 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0.07058824, 0.67058825, 0.85882360, 0.99215692, 0.99215692, 0.99215692, 0.99215692, 0.76470596, 0.31372550, 0.03529412, 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0.21568629, 0.67450982, 0.88627458, 0.99215692, 0.99215692, 0.99215692, 0.99215692, 0.95686281, 0.52156866, 0.04313726, 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0.53333336, 0.99215692, 0.99215692, 0.99215692, 0.83137262, 0.52941179, 0.51764709, 0.06274510, 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]]]), array([5]))
# 此处没有导入图就做一个解析
from paddle.io import Dataset  # 导入Datasrt库
class MyDataset(Dataset):
    """
    步骤一:继承paddle.io.Dataset类
    """
    def __init__(self, mode='train'):
        """
        步骤二:实现构造函数,定义数据读取方式,划分训练和测试数据集
        """
        self.data = []
        with open(f'{mode}.txt') as f:  # 读取数据文档
            for line in f.readlines():  # 循环读取的数据
                info = line.strip().split('\t')  # 对信息进行切块
                if len(info) > 0:  # 对有效信息进行判断
                    self.data.append([info[0].strip(), info[1].strip()])   # 处理完的数据分别放入列表
    def __getitem__(self, index):
        """
        步骤三:实现__getitem__方法,定义指定index时如何获取数据,并返回单条数据(训练数据,对应的标签)
        """
        image_file, label = self.data[index]  # 解压list
        img = Image.open(image_file)  #  读取图片
        img = img.resize((100, 100), Image.ANTIALIAS)  # 大小归一化
        img = np.array(img).astype('float32')  # 数据处理
        img = img.transpose((2, 0, 1))     #读出来的图像是rgb,rgb,rbg..., 转置为 rrr...,ggg...,bbb...
        img = img/255.0  # 灰度处理
        return img, np.array(label, dtype='int64')  # 导出
    def __len__(self):
        """
        步骤四:实现__len__方法,返回数据集总数目
        """
        return len(self.data)
train_dataset = FoodDataset(mode='train_data')  # 实例化训练的数据提供器
print('train大小:', train_dataset.__len__())  # 查看数据大小


上面的数据处理做了一个解析,没有用实际的图大家理解一下,基本上的注释都有。

__init__、:对数据列表文件进行读取并划分训练和测试数据集

__getitem__:对划分好的文件数据进行处理并读取图片,对图片做归一化处理

__len__:查看图片数量


以上数据就不具体举例了


什么是模型什么是神经网络

  • 模型
    模型简单说可以理解为函数,里面大量的数据和参数组成了认知模式.


  • 神经网络


  • 线性神经网络 (感知机、DNN)

类似于一元函数,模拟人脑神经元的反应。


31719bb7d61d39bb464a188b1b6c4092.jpg


如上图单层的网络构成就是简单的感知机。


8bcad3148665be8acb725b5e7b453cee.jpg


如上图由多层线性网络构成就是DNN。

实际上线性神经网络只有3层,输入层、隐藏层 、输出层 ,也就是中间的统称为隐藏层

『领航团图像分类课程』书写数字识别花式解析使用3个案例对线性进行了解析(主要是使用了3种办法解决了手写数字识别)


# 单层神经网络构建
model=paddle.nn.Linear(in_features=2, out_features=1)  # 单层线性网络(2个输入一个输出对应上方感知机)
paddle.summary(model, (2,))
---------------------------------------------------------------------------
 Layer (type)       Input Shape          Output Shape         Param #    
===========================================================================
   Linear-1            [[2]]                 [1]                 3       
===========================================================================
Total params: 3
Trainable params: 3
Non-trainable params: 0
---------------------------------------------------------------------------
Input size (MB): 0.00
Forward/backward pass size (MB): 0.00
Params size (MB): 0.00
Estimated Total Size (MB): 0.00
---------------------------------------------------------------------------
{'total_params': 3, 'trainable_params': 3}
# DNN神经网络构建(继承法)
import paddle.nn.functional as F
class MLPModel(paddle.nn.Layer):  # 继承paddle.nn.Layer类
    def __init__(self):
        super(MLPModel, self).__init__()
        self.flatten=paddle.nn.Flatten()  # 数据拉直
        self.hidden=paddle.nn.Linear(in_features=784,out_features=128)  # 线性输入784输出128
        self.output=paddle.nn.Linear(in_features=128,out_features=10)  # 线性输入128输出10
    def forward(self, x):
        x=self.flatten(x)  #  拉直
        x=self.hidden(x)  # 经过隐藏层(线性层1)
        x=F.relu(x) # 经过激活层
        x=self.output(x)  # 经过输出层
        return x
model=paddle.Model(MLPModel())  # 实例化模型
model.summary((1, 28, 28))
---------------------------------------------------------------------------
 Layer (type)       Input Shape          Output Shape         Param #    
===========================================================================
   Flatten-4       [[1, 28, 28]]           [1, 784]              0       
   Linear-6          [[1, 784]]            [1, 128]           100,480    
   Linear-7          [[1, 128]]            [1, 10]             1,290     
===========================================================================
Total params: 101,770
Trainable params: 101,770
Non-trainable params: 0
---------------------------------------------------------------------------
Input size (MB): 0.00
Forward/backward pass size (MB): 0.01
Params size (MB): 0.39
Estimated Total Size (MB): 0.40
---------------------------------------------------------------------------
{'total_params': 101770, 'trainable_params': 101770}
# DNN神经网络构建(高阶函数法)
linear=paddle.nn.Sequential(
        paddle.nn.Flatten(),#将[1,28,28]形状的图片数据改变形状为[1,784]
        paddle.nn.Linear(784,128),
        paddle.nn.Linear(128,10)
        )
model=paddle.Model(linear)
model.summary((1, 28, 28))


---------------------------------------------------------------------------
 Layer (type)       Input Shape          Output Shape         Param #    
===========================================================================
   Flatten-6       [[1, 28, 28]]           [1, 784]              0       
   Linear-8          [[1, 784]]            [1, 128]           100,480    
   Linear-9          [[1, 128]]            [1, 10]             1,290     
===========================================================================
Total params: 101,770
Trainable params: 101,770
Non-trainable params: 0
---------------------------------------------------------------------------
Input size (MB): 0.00
Forward/backward pass size (MB): 0.01
Params size (MB): 0.39
Estimated Total Size (MB): 0.40
---------------------------------------------------------------------------
{'total_params': 101770, 'trainable_params': 101770}


上一层的输出等于本层的输入

  • 卷积神经网络


卷积网络通过一系列方法,成功将数据量庞大的图像识别问题不断降维,最终使其能够被训练


1933c55f1aac081d9bd018bb1ee01f53.png


最经典的卷积神经网络就是CNN神经网络

『领航团图像分类课程』卷积神经网络解析项目近几年经典卷积网络解析(内含卷积网络计算图解)


20210205112823341.png


# 卷积神经网络(高级api版)
import paddle.nn as nn
network = nn.Sequential(
    nn.Conv2D(in_channels=1, out_channels=6, kernel_size=3, stride=1, padding=1),  # 卷积
    nn.ReLU(),  # 激活函数
    nn.MaxPool2D(kernel_size=2, stride=2),  # 最大池化
    nn.Conv2D(in_channels=6, out_channels=16, kernel_size=5, stride=1, padding=0),
    nn.ReLU(),
    nn.MaxPool2D(kernel_size=2, stride=2),
    nn.Flatten(),
    nn.Linear(in_features=400, out_features=120),  # 400 = 5x5x16,输入形状为32x32, 输入形状为28x28时调整为256
    nn.Linear(in_features=120, out_features=84),
    nn.Linear(in_features=84, out_features=10)
)
paddle.summary(network, (1, 1, 28, 28))
---------------------------------------------------------------------------
 Layer (type)       Input Shape          Output Shape         Param #    
===========================================================================
   Conv2D-3       [[1, 1, 28, 28]]      [1, 6, 28, 28]          60       
    ReLU-3        [[1, 6, 28, 28]]      [1, 6, 28, 28]           0       
  MaxPool2D-3     [[1, 6, 28, 28]]      [1, 6, 14, 14]           0       
   Conv2D-4       [[1, 6, 14, 14]]     [1, 16, 10, 10]         2,416     
    ReLU-4       [[1, 16, 10, 10]]     [1, 16, 10, 10]           0       
  MaxPool2D-4    [[1, 16, 10, 10]]      [1, 16, 5, 5]            0       
   Flatten-3      [[1, 16, 5, 5]]          [1, 400]              0       
   Linear-4          [[1, 400]]            [1, 120]           48,120     
   Linear-5          [[1, 120]]            [1, 84]            10,164     
   Linear-6          [[1, 84]]             [1, 10]              850      
===========================================================================
Total params: 61,610
Trainable params: 61,610
Non-trainable params: 0
---------------------------------------------------------------------------
Input size (MB): 0.00
Forward/backward pass size (MB): 0.11
Params size (MB): 0.24
Estimated Total Size (MB): 0.35
---------------------------------------------------------------------------
{'total_params': 61610, 'trainable_params': 61610}
# 卷积神经网络(CNN)(继承法)
class AlexNetModel(paddle.nn.Layer):
    def __init__(self):
        super(AlexNetModel, self).__init__()
        self.conv_pool1 = paddle.nn.Sequential(  
            paddle.nn.Conv2D(in_channels=1, out_channels=6, kernel_size=3, stride=1, padding=1),     
            paddle.nn.ReLU(),       
            paddle.nn.MaxPool2D(kernel_size=2, stride=2)) 
        self.conv_pool2 = paddle.nn.Sequential(
            paddle.nn.Conv2D(in_channels=6, out_channels=16, kernel_size=5, stride=1, padding=0), 
            paddle.nn.ReLU(),       
            paddle.nn.MaxPool2D(kernel_size=2, stride=2))    
        self.linear1 = paddle.nn.Linear(400, 120)
        self.linear2 = paddle.nn.Linear(120, 84)
        self.linear3 = paddle.nn.Linear(84, 10)
        self.flatten=paddle.nn.Flatten()
    def forward(self, x):  # 前向传播
        x = self.conv_pool1(x)
        x = self.conv_pool2(x)
        x = self.flatten(x)
        x = self.linear1(x)
        x = self.linear2(x)
        x = self.linear3(x)
        return x
model = AlexNetModel()
paddle.summary(model,(1, 1, 28, 28))
---------------------------------------------------------------------------
 Layer (type)       Input Shape          Output Shape         Param #    
===========================================================================
   Conv2D-1       [[1, 1, 28, 28]]      [1, 6, 28, 28]          60       
    ReLU-1        [[1, 6, 28, 28]]      [1, 6, 28, 28]           0       
  MaxPool2D-1     [[1, 6, 28, 28]]      [1, 6, 14, 14]           0       
   Conv2D-2       [[1, 6, 14, 14]]     [1, 16, 10, 10]         2,416     
    ReLU-2       [[1, 16, 10, 10]]     [1, 16, 10, 10]           0       
  MaxPool2D-2    [[1, 16, 10, 10]]      [1, 16, 5, 5]            0       
   Flatten-1      [[1, 16, 5, 5]]          [1, 400]              0       
   Linear-1          [[1, 400]]            [1, 120]           48,120     
   Linear-2          [[1, 120]]            [1, 84]            10,164     
   Linear-3          [[1, 84]]             [1, 10]              850      
===========================================================================
Total params: 61,610
Trainable params: 61,610
Non-trainable params: 0
---------------------------------------------------------------------------
Input size (MB): 0.00
Forward/backward pass size (MB): 0.11
Params size (MB): 0.24
Estimated Total Size (MB): 0.35
---------------------------------------------------------------------------
{'total_params': 61610, 'trainable_params': 61610}


训练模型

以上就把数据和模型 进行了处理,那么怎么样进行反馈训练结果呢?

这里就出现了一个值叫Loss,通过其进行反馈就可以获得好坏的内容

为了判断学习的好坏,已经效率等,引入了梯度 的概念


20210305022848647.png


# 随机梯度下降算法的优化器(优化器可以自己调整)
sgd_optimizer=paddle.optimizer.SGD(learning_rate=0.001, parameters=model.parameters())
# loss计算
mse_loss=paddle.nn.MSELoss()


训练轮数:

我们问什么要有训练轮数呢?

不同Epoch的训练,其实用的是同一个训练集的数据。假设第1个Epoch和第10个Epoch虽然用的都是训练集的60000图片,但是对模型的权重更新值却是完全不同的。因为不同Epoch的模型处于代价函数空间上的不同位置,模型的训练代越靠后,越接近谷底,其代价越小。


相对应很多数据我们学习的时候不是很理解,多学习几轮就会 发现一些新的特征,通过特征可以提高成绩~


model.prepare(paddle.optimizer.Adam(learning_rate=0.001, parameters=model.parameters()),
              paddle.nn.CrossEntropyLoss(),   # 交叉熵损失函数。线性模型+该损失函数,即softmax分类器。
              paddle.metric.Accuracy(topk=(1,2)))
model.fit(train_dataset, # 训练数据集
          val_dataset,  # 测试数据集
          epochs=2, # 训练的总轮次
          batch_size=64, # 训练使用的批大小
          verbose=1)  # 日志展示形式
model.evaluate(test_dataset,batch_size=64,verbose=1)  # 评估


总结

我们从数据处理到模型构建然后训练都做了一站式解析,具体的案例可以查看有关链接。

深度学习远不如此,让我们一起携手继续学习

以后我们会涉及,鲁棒性、拟合、过拟合、数据增广等一些列专业的内容,让我们一起加油!


传说中的飞桨社区最差代码人,让我们一起努力!

记住:三岁出品必是精品 (不要脸系列


目录
相关文章
|
11月前
|
机器学习/深度学习 数据采集 存储
基于PaddlePaddle的词向量实战 | 深度学习基础任务教程系列
基于PaddlePaddle的词向量实战 | 深度学习基础任务教程系列
|
12月前
|
机器学习/深度学习 自然语言处理 算法
瞎聊深度学习——PaddlePaddle的使用(一)
瞎聊深度学习——PaddlePaddle的使用(一)
|
机器学习/深度学习 编解码 算法
Paddle目标检测学习笔记
Paddle目标检测学习笔记
166 0
Paddle目标检测学习笔记
|
机器学习/深度学习 算法 计算机视觉
Paddle目标检测学习笔记(一)
Paddle目标检测学习笔记(一)
109 0
Paddle目标检测学习笔记(一)
|
编解码 算法 计算机视觉
Paddle目标检测学习笔记(二)
Paddle目标检测学习笔记(二)
145 0
Paddle目标检测学习笔记(二)
|
机器学习/深度学习 算法 Linux
飞桨文心大模型中Paddle库编译
飞桨文心大模型中Paddle库编译
222 0
飞桨文心大模型中Paddle库编译
|
API 异构计算
使用OpenVINO 和 PaddlePaddle 进行图像分类预测
使用OpenVINO 和 PaddlePaddle 进行图像分类预测
232 0
使用OpenVINO 和 PaddlePaddle 进行图像分类预测
|
机器学习/深度学习 移动开发 人工智能
花书线性回归-PaddlePaddle版本
花书线性回归-PaddlePaddle版本
82 0
花书线性回归-PaddlePaddle版本
|
机器学习/深度学习 数据处理
Paddle实现迁移学习
Paddle实现迁移学习
140 0
Paddle实现迁移学习
|
机器学习/深度学习
机器学习PaddlePaddle项目训练代码模板
机器学习PaddlePaddle项目训练代码模板
213 0