1 基础实现

paddle原理跟pytorch是一模一样的,只是有些细节操作差别。
复现第6节中的MNIST图像分类实现,核心代码如下:

  1. from tqdm import tqdm
  2. import numpy as np
  3. import pandas as pd
  4. import paddle
  5. from paddle.io import DataLoader
  6. from paddle.vision import datasets
  7. # 零 配置表
  8. NUM_CLASSES = 10 # 几分类任务
  9. paddle.set_device('gpu') # 在哪个设备运行
  10. BASE_LR = 0.01 # 学习率
  11. IMS_PER_BATCH = 200 # BATCH_SIZE
  12. STATE_FILE = 'MobileNetV2_model.pth' # 计划存储权重文件的路径
  13. # 一 数据集
  14. def mnist_transform(x):
  15. # 这里获得的是PIL格式的图片
  16. y = x.resize([32, 32]).convert('RGB')
  17. img = np.array(y, dtype='float32') / 255.
  18. img = img.transpose([2, 0, 1])
  19. return paddle.to_tensor(img)
  20. # 训练集数量 60000, 28*28 -> 32*32
  21. train_dataset = datasets.MNIST(mode='train', transform=mnist_transform)
  22. train_loader = DataLoader(train_dataset, batch_size=IMS_PER_BATCH)
  23. # 验证集数量 10000, 28*28 -> 32*32
  24. val_dataset = datasets.MNIST(mode='test', transform=mnist_transform)
  25. val_loader = DataLoader(val_dataset, batch_size=IMS_PER_BATCH)
  26. # 二 模型
  27. # 这里特地挑了轻量的MobileNetV2来测试,换成LeNet、resnet18等一样可以运行的。
  28. from paddle.vision.models import MobileNetV2
  29. # 三 训练、推断
  30. def train():
  31. # 1 准备工作
  32. model = MobileNetV2(num_classes=NUM_CLASSES)
  33. optimizer = paddle.optimizer.Adam(learning_rate=BASE_LR, parameters=model.parameters()) # 学习器
  34. # 2 开始训练
  35. loader = DataLoader(train_dataset, batch_size=IMS_PER_BATCH, shuffle=True)
  36. for batch_inputs in tqdm(loader, total=len(train_dataset) // IMS_PER_BATCH):
  37. x, y = batch_inputs
  38. y_hat = model(x)
  39. loss = paddle.nn.functional.cross_entropy(y_hat, y)
  40. optimizer.clear_grad()
  41. loss.backward()
  42. optimizer.step()
  43. paddle.save(model.state_dict(), STATE_FILE)
  44. def eval():
  45. # 1 加载模型
  46. model = MobileNetV2(num_classes=NUM_CLASSES) # 初始化模型结构
  47. model.set_state_dict(paddle.load(STATE_FILE)) # 加载模型权重
  48. model.eval() # 进入推断模式
  49. # 2 验证集
  50. with paddle.no_grad():
  51. gt, pred = [], []
  52. for batched_inputs in val_loader:
  53. x, y = batched_inputs
  54. y_hat = model(x).argmax(axis=1)
  55. gt += y.reshape([-1]).tolist()
  56. pred += y_hat.tolist()
  57. df = pd.DataFrame.from_dict({'gt': gt, 'pred': pred})
  58. print('验证集各类别出现次数(行ground truth,列pred)')
  59. print(pd.crosstab(df['gt'], df['pred']))
  60. correct = sum(df['gt'] == df['pred'])
  61. total = len(df)
  62. print(f'正确率: {correct} / {total} ≈ {correct / total:.2%}')
  63. if __name__ == '__main__':
  64. train()
  65. eval()
  66. # 验证集各类别出现次数(行ground truth,列pred)
  67. # pred 0 1 2 3 4 5 6 7 8 9
  68. # gt
  69. # 0 966 0 1 0 1 3 4 1 3 1
  70. # 1 0 1080 4 8 1 0 0 25 17 0
  71. # 2 7 0 1016 2 1 0 0 2 3 1
  72. # 3 1 0 6 986 0 3 0 5 1 8
  73. # 4 0 0 1 0 963 0 3 1 0 14
  74. # 5 1 0 0 11 0 866 2 1 0 11
  75. # 6 5 5 0 0 20 14 905 0 9 0
  76. # 7 0 0 12 3 0 0 0 1007 1 5
  77. # 8 1 0 7 9 5 4 2 0 931 15
  78. # 9 2 0 1 2 4 2 0 3 1 994
  79. # 正确率: 9714 / 10000 ≈ 97.14%

paddle官方文档有对这个MNIST任务底层原理,更加详细的一步步解释和原理实现,
详见 第二章:一个案例吃透深度学习,建议初学者多花时间详细学习,能掌握、巩固深度学习的基础原理。
之后的章节,很多底层实现都会被工程化的框架封装,成为黑箱了,如果不熟悉相关基础,很容易云里雾里。

2 高层API实现

基本训练结构

  1. import paddle
  2. from paddle.vision.transforms import Transpose
  3. from paddle.vision.datasets import Cifar10
  4. from paddle.vision.models import resnet18
  5. from paddle.optimizer import Adam
  6. from paddle.nn import CrossEntropyLoss
  7. from paddle.metric import Accuracy
  8. # 0 设备
  9. paddle.set_device('gpu:0')
  10. # 1 数据
  11. # datasets相关处理默认是pil模式,这里使用cv2模式,确保获得的是np.ndarray类型
  12. paddle.vision.set_image_backend('cv2')
  13. # 使用Cifar10数据集,训练集5万张,测试集1万张
  14. # 这里的 Transpose 是预设好的基础transform,做了通道调整:HWC->CHW
  15. train_dataset = Cifar10(mode='train', transform=Transpose())
  16. val_dataset = Cifar10(mode='test', transform=Transpose())
  17. # cifar10是类似MNIST的十分类数据,但难度比MNIST更大。
  18. # 其图片内容可以随便百度到,这里节约篇幅不做过多介绍。
  19. # 2 调用resnet18模型,将其传给paddle.Model高层API接口类
  20. model = paddle.Model(resnet18(num_classes=10))
  21. # 进行训练前准备
  22. model.prepare(Adam(0.01, parameters=model.parameters()), # 优化器
  23. CrossEntropyLoss(), # 损失函数
  24. Accuracy() # 测评函数
  25. )
  26. # 3 启动训练,运行需要850M显存
  27. model.fit(train_dataset, val_dataset, # 训练集、验证集
  28. epochs=5,
  29. batch_size=64,
  30. save_dir='./output'
  31. )
  1. Epoch 1/5
  2. step 10/782 - loss: 2.4510 - acc: 0.1234 - 93ms/step
  3. step 20/782 - loss: 2.4635 - acc: 0.1258 - 58ms/step
  4. step 30/782 - loss: 2.7744 - acc: 0.1375 - 45ms/step
  5. save checkpoint at /home/chenkunze/slns/doai/section07/output/0
  6. Eval begin...
  7. step 10/157 - loss: 1.8727 - acc: 0.3766 - 12ms/step
  8. ...
  9. step 157/157 - loss: 2.0318 - acc: 0.3696 - 11ms/step
  10. Eval samples: 10000
  11. Epoch 2/5
  12. ...
  13. Epoch 5/5
  14. ...
  15. step 157/157 - loss: 0.5258 - acc: 0.6902 - 10ms/step
  16. Eval samples: 10000
  17. save checkpoint at /home/chenkunze/slns/doai/section07/output/final

在output目录下,会保存每轮epoch的模型结果:
image.png
pdparams是pd+params的意思,pd又是paddle的缩写,表示模型权重文件。
pdopt是pd+opt,优化器状态等参数值,可以用来回复训练状态。

0是epoch=0训练后的模型,4是epoch=4训练后的模型,训练完,还会存储一份final模型。
这里4.pdparams和final.pdparams是一样的。


fit函数有很多参数细节可以调整,比如保存模型的间隔等,详见官方API文档:Model-API文档-PaddlePaddle深度学习平台

官方示例代码:换组件、调参

使用飞桨高层API直接调用图像分类网络 (对官方代码做了细微调整)

  1. import paddle
  2. from paddle.vision.models import resnet50
  3. from paddle.vision.datasets import Cifar10
  4. from paddle.optimizer import Momentum
  5. from paddle.regularizer import L2Decay
  6. from paddle.nn import CrossEntropyLoss
  7. from paddle.metric import Accuracy
  8. from paddle.vision.transforms import Transpose
  9. # 0 设备
  10. paddle.set_device('gpu:0')
  11. # 1 数据
  12. paddle.vision.set_image_backend('cv2')
  13. train_dataset = Cifar10(mode='train', transform=Transpose())
  14. val_dataset = Cifar10(mode='test', transform=Transpose())
  15. # 2 模型
  16. model = paddle.Model(resnet50(pretrained=False, num_classes=10))
  17. # 进行训练前准备
  18. optimizer = Momentum(learning_rate=0.01,
  19. momentum=0.9,
  20. weight_decay=L2Decay(1e-4),
  21. parameters=model.parameters())
  22. model.prepare(optimizer, CrossEntropyLoss(), Accuracy(topk=(1, 5)))
  23. # 3 启动训练
  24. model.fit(train_dataset,
  25. val_dataset,
  26. epochs=50,
  27. batch_size=64,
  28. save_dir="./output",
  29. num_workers=8)

官方代码在优化器、测评函数上进行了更精细的定制。并且训练阶段epochs加到50,设了num_workers。
运行后,可以看到平均速度 48ms/step,训练782+验证157=939。
即一轮epoch需要 939step * 48ms ≈ 45秒。在笔者服务器跑完实验约需要 38 分钟。

最后得到在验证集和训练集的效果:
step 782/782 - loss: 0.0143 - acc_top1: 0.9803 - acc_top5: 0.9999 - 48ms/step
step 157/157 - loss: 2.5486 - acc_top1: 0.6897 - acc_top5: 0.9663 - 24ms/step

即结论,在优化器上做精细的配置和加大epoch是能提高模型效果的。
但这里resnet50在训练集上明显过拟合了,导致在验证集效果不佳,我们可以把模型换成resnet18试试。

resnet18大概只需原来一半的时间,最后效果会稍好些:
step 782/782 - loss: 0.1704 - acc_top1: 0.9927 - acc_top5: 1.0000 - 21ms/step
step 157/157 - loss: 3.4462 - acc_top1: 0.7417 - acc_top5: 0.9739 - 11ms/step

如何再提升验证集上的效果,有兴趣的读者可以自己做做实验再想想办法。

本处主要是解释实际应用中,epochs要调大一些,以及很多细节参数可以调整,提高性能上限。
比如nn.CrossEntropyLoss还可以传入一个权重,提高样本量较少的类的权重,处理类别不平衡问题:

  1. nn.CrossEntropyLoss(paddle.to_tensor([1.3, 1, 2], dtype='float32')

本节后文主要讲一些工程代码组织方式,重点不是刷精度,为了快速测试效果,所以只用epochs=5做实验。

自定义组件:个性化配置

除了官方支持的多种组件和参数,我们也可以写自定义组件。
详见官方文档:飞桨高层API使用指南-API文档-PaddlePaddle深度学习平台
这里我在pyxlpr扩展了一个ClasAccuracy组件,支持每轮epoch显示acc之外,还能显示f1分数、crosstab。

还有个比较特别的是callbacks,fit接口的callback参数支持传一个Callback类实例,用来在每轮训练和每个batch训练前后进行调用,可以通过callback收集到训练过程中的一些数据和参数,或者实现一些自定义操作。这里我在pyxlpr也实现了一个VisualAcc,可以可视化查看精度变化图:

  1. class VisualAcc(paddle.callbacks.Callback):
  2. def __init__(self, logdir, experimental_name):
  3. """
  4. :param logdir: log所在根目录
  5. :param experimental_name: 实验名子目录
  6. """
  7. from pyxllib.prog.pupil import check_install_package
  8. check_install_package('visualdl')
  9. from visualdl import LogWriter
  10. super().__init__()
  11. # 这样奇怪地加后缀,是为了字典序后,每个实验的train显示在eval之前
  12. d = XlPath(logdir) / (experimental_name + '_train')
  13. # if d.exists(): shutil.rmtree(d)
  14. self.write = LogWriter(logdir=str(d))
  15. d = XlPath(logdir) / (experimental_name + '_val')
  16. # if d.exists(): shutil.rmtree(d)
  17. self.eval_writer = LogWriter(logdir=str(d))
  18. self.eval_times = 0
  19. def on_epoch_end(self, epoch, logs=None):
  20. self.write.add_scalar('acc', step=epoch, value=logs['acc'])
  21. self.write.flush()
  22. def on_eval_end(self, logs=None):
  23. self.eval_writer.add_scalar('acc', step=self.eval_times, value=logs['acc'])
  24. self.eval_writer.flush()
  25. self.eval_times += 1
  1. import paddle
  2. from paddle.vision.transforms import Transpose
  3. from paddle.vision.datasets import Cifar10
  4. from paddle.vision.models import resnet18
  5. from paddle.optimizer import Adam
  6. from paddle.nn import CrossEntropyLoss
  7. from pyxlpr.ai.paddle import ClasAccuracy, VisualAcc # pip install pyxllib>=0.2.48
  8. # 0 设备
  9. paddle.set_device('gpu:0')
  10. # 1 数据
  11. # datasets相关处理默认是pil模式,这里使用cv2模式,确保获得的是np.ndarray类型
  12. paddle.vision.set_image_backend('cv2')
  13. # 使用Cifar10数据集,训练集5万张,测试集1万张
  14. # 这里的 Transpose 是预设好的基础transform,做了通道调整:HWC->CHW
  15. train_dataset = Cifar10(mode='train', transform=Transpose())
  16. val_dataset = Cifar10(mode='test', transform=Transpose())
  17. # 2 调用MobileNetV2模型,将其传给paddle.Model高层API接口类
  18. model = paddle.Model(resnet18(num_classes=10))
  19. # 进行训练前准备
  20. model.prepare(Adam(0.01, parameters=model.parameters()), # 优化器
  21. CrossEntropyLoss(), # 损失函数
  22. ClasAccuracy(print_mode=2) # 测评函数,改成了自定义测评类
  23. )
  24. # 3 启动训练,运行需要850M显存
  25. model.fit(train_dataset, val_dataset, # 训练集、验证集
  26. epochs=5,
  27. batch_size=64,
  28. save_dir='./output',
  29. callbacks=[VisualAcc('./output', 'exp001')]
  30. )

自定义Metric效果

  1. Epoch 5/5
  2. ...
  3. step 780/782 - loss: 0.5276 - acc: 0.7375 - 32ms/step
  4. ...
  5. Eval begin...
  6. ...
  7. step 157/157 - loss: 0.4542 - acc: 0.6772 - 16ms/step
  8. {'f1_weighted': 0.6692, 'f1_macro': 0.6692, 'f1_micro': 0.6772}
  9. pred 0 1 2 3 4 5 6 7 8 9
  10. gt
  11. 0 784 13 29 10 5 0 4 3 120 32
  12. 1 30 804 10 4 1 0 9 3 52 87
  13. 2 115 9 683 22 43 10 57 9 39 13
  14. 3 69 14 160 431 67 29 109 24 62 35
  15. 4 75 6 119 37 611 3 58 46 33 12
  16. 5 40 10 217 189 52 315 66 45 39 27
  17. 6 30 9 85 26 20 6 779 4 25 16
  18. 7 48 7 91 39 59 14 13 694 7 28
  19. 8 54 19 11 3 0 0 4 1 891 17
  20. 9 53 82 19 4 2 2 6 5 47 780

自定义callbacks实现visualdl可视化

注意我的VisualAcc需要设置根目录和子目录:VisualAcc(‘./output’, ‘exp001’),最后得到的是这样的数据:
image.png

在项目目录下执行: visualdl —logdir .
image.png
image.png

如果还有不同参数实验,可以子目录命名exp002、exp003…
然后在visualdl里可以看到不同实验的acc变化效果图。

visualdl页面中的,我一般都会把“最值”打开,能清晰看到最佳模型效果是哪个,以及关闭“平滑度”。
关于“平滑度”的配置看需求,我一般不调,就是想看每个batch的峰值、谷值,像过山车一样~
如果只想看整体趋势的变化,则可以设一下smoothing会平滑一点。

源码部署

静态部署

3 实战——电子元件模板分类