1 基础实现
paddle原理跟pytorch是一模一样的,只是有些细节操作差别。
复现第6节中的MNIST图像分类实现,核心代码如下:
from tqdm import tqdmimport numpy as npimport pandas as pdimport paddlefrom paddle.io import DataLoaderfrom paddle.vision import datasets# 零 配置表NUM_CLASSES = 10 # 几分类任务paddle.set_device('gpu') # 在哪个设备运行BASE_LR = 0.01 # 学习率IMS_PER_BATCH = 200 # BATCH_SIZESTATE_FILE = 'MobileNetV2_model.pth' # 计划存储权重文件的路径# 一 数据集def mnist_transform(x):# 这里获得的是PIL格式的图片y = x.resize([32, 32]).convert('RGB')img = np.array(y, dtype='float32') / 255.img = img.transpose([2, 0, 1])return paddle.to_tensor(img)# 训练集数量 60000, 28*28 -> 32*32train_dataset = datasets.MNIST(mode='train', transform=mnist_transform)train_loader = DataLoader(train_dataset, batch_size=IMS_PER_BATCH)# 验证集数量 10000, 28*28 -> 32*32val_dataset = datasets.MNIST(mode='test', transform=mnist_transform)val_loader = DataLoader(val_dataset, batch_size=IMS_PER_BATCH)# 二 模型# 这里特地挑了轻量的MobileNetV2来测试,换成LeNet、resnet18等一样可以运行的。from paddle.vision.models import MobileNetV2# 三 训练、推断def train():# 1 准备工作model = MobileNetV2(num_classes=NUM_CLASSES)optimizer = paddle.optimizer.Adam(learning_rate=BASE_LR, parameters=model.parameters()) # 学习器# 2 开始训练loader = DataLoader(train_dataset, batch_size=IMS_PER_BATCH, shuffle=True)for batch_inputs in tqdm(loader, total=len(train_dataset) // IMS_PER_BATCH):x, y = batch_inputsy_hat = model(x)loss = paddle.nn.functional.cross_entropy(y_hat, y)optimizer.clear_grad()loss.backward()optimizer.step()paddle.save(model.state_dict(), STATE_FILE)def eval():# 1 加载模型model = MobileNetV2(num_classes=NUM_CLASSES) # 初始化模型结构model.set_state_dict(paddle.load(STATE_FILE)) # 加载模型权重model.eval() # 进入推断模式# 2 验证集with paddle.no_grad():gt, pred = [], []for batched_inputs in val_loader:x, y = batched_inputsy_hat = model(x).argmax(axis=1)gt += y.reshape([-1]).tolist()pred += y_hat.tolist()df = pd.DataFrame.from_dict({'gt': gt, 'pred': pred})print('验证集各类别出现次数(行ground truth,列pred)')print(pd.crosstab(df['gt'], df['pred']))correct = sum(df['gt'] == df['pred'])total = len(df)print(f'正确率: {correct} / {total} ≈ {correct / total:.2%}')if __name__ == '__main__':train()eval()# 验证集各类别出现次数(行ground truth,列pred)# pred 0 1 2 3 4 5 6 7 8 9# gt# 0 966 0 1 0 1 3 4 1 3 1# 1 0 1080 4 8 1 0 0 25 17 0# 2 7 0 1016 2 1 0 0 2 3 1# 3 1 0 6 986 0 3 0 5 1 8# 4 0 0 1 0 963 0 3 1 0 14# 5 1 0 0 11 0 866 2 1 0 11# 6 5 5 0 0 20 14 905 0 9 0# 7 0 0 12 3 0 0 0 1007 1 5# 8 1 0 7 9 5 4 2 0 931 15# 9 2 0 1 2 4 2 0 3 1 994# 正确率: 9714 / 10000 ≈ 97.14%
paddle官方文档有对这个MNIST任务底层原理,更加详细的一步步解释和原理实现,
详见 第二章:一个案例吃透深度学习,建议初学者多花时间详细学习,能掌握、巩固深度学习的基础原理。
之后的章节,很多底层实现都会被工程化的框架封装,成为黑箱了,如果不熟悉相关基础,很容易云里雾里。
2 高层API实现
基本训练结构
import paddlefrom paddle.vision.transforms import Transposefrom paddle.vision.datasets import Cifar10from paddle.vision.models import resnet18from paddle.optimizer import Adamfrom paddle.nn import CrossEntropyLossfrom paddle.metric import Accuracy# 0 设备paddle.set_device('gpu:0')# 1 数据# datasets相关处理默认是pil模式,这里使用cv2模式,确保获得的是np.ndarray类型paddle.vision.set_image_backend('cv2')# 使用Cifar10数据集,训练集5万张,测试集1万张# 这里的 Transpose 是预设好的基础transform,做了通道调整:HWC->CHWtrain_dataset = Cifar10(mode='train', transform=Transpose())val_dataset = Cifar10(mode='test', transform=Transpose())# cifar10是类似MNIST的十分类数据,但难度比MNIST更大。# 其图片内容可以随便百度到,这里节约篇幅不做过多介绍。# 2 调用resnet18模型,将其传给paddle.Model高层API接口类model = paddle.Model(resnet18(num_classes=10))# 进行训练前准备model.prepare(Adam(0.01, parameters=model.parameters()), # 优化器CrossEntropyLoss(), # 损失函数Accuracy() # 测评函数)# 3 启动训练,运行需要850M显存model.fit(train_dataset, val_dataset, # 训练集、验证集epochs=5,batch_size=64,save_dir='./output')
Epoch 1/5step 10/782 - loss: 2.4510 - acc: 0.1234 - 93ms/stepstep 20/782 - loss: 2.4635 - acc: 0.1258 - 58ms/stepstep 30/782 - loss: 2.7744 - acc: 0.1375 - 45ms/stepsave checkpoint at /home/chenkunze/slns/doai/section07/output/0Eval begin...step 10/157 - loss: 1.8727 - acc: 0.3766 - 12ms/step...step 157/157 - loss: 2.0318 - acc: 0.3696 - 11ms/stepEval samples: 10000Epoch 2/5...Epoch 5/5...step 157/157 - loss: 0.5258 - acc: 0.6902 - 10ms/stepEval samples: 10000save checkpoint at /home/chenkunze/slns/doai/section07/output/final
在output目录下,会保存每轮epoch的模型结果:
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直接调用图像分类网络 (对官方代码做了细微调整)
import paddlefrom paddle.vision.models import resnet50from paddle.vision.datasets import Cifar10from paddle.optimizer import Momentumfrom paddle.regularizer import L2Decayfrom paddle.nn import CrossEntropyLossfrom paddle.metric import Accuracyfrom paddle.vision.transforms import Transpose# 0 设备paddle.set_device('gpu:0')# 1 数据paddle.vision.set_image_backend('cv2')train_dataset = Cifar10(mode='train', transform=Transpose())val_dataset = Cifar10(mode='test', transform=Transpose())# 2 模型model = paddle.Model(resnet50(pretrained=False, num_classes=10))# 进行训练前准备optimizer = Momentum(learning_rate=0.01,momentum=0.9,weight_decay=L2Decay(1e-4),parameters=model.parameters())model.prepare(optimizer, CrossEntropyLoss(), Accuracy(topk=(1, 5)))# 3 启动训练model.fit(train_dataset,val_dataset,epochs=50,batch_size=64,save_dir="./output",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还可以传入一个权重,提高样本量较少的类的权重,处理类别不平衡问题:
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,可以可视化查看精度变化图:
class VisualAcc(paddle.callbacks.Callback):def __init__(self, logdir, experimental_name):""":param logdir: log所在根目录:param experimental_name: 实验名子目录"""from pyxllib.prog.pupil import check_install_packagecheck_install_package('visualdl')from visualdl import LogWritersuper().__init__()# 这样奇怪地加后缀,是为了字典序后,每个实验的train显示在eval之前d = XlPath(logdir) / (experimental_name + '_train')# if d.exists(): shutil.rmtree(d)self.write = LogWriter(logdir=str(d))d = XlPath(logdir) / (experimental_name + '_val')# if d.exists(): shutil.rmtree(d)self.eval_writer = LogWriter(logdir=str(d))self.eval_times = 0def on_epoch_end(self, epoch, logs=None):self.write.add_scalar('acc', step=epoch, value=logs['acc'])self.write.flush()def on_eval_end(self, logs=None):self.eval_writer.add_scalar('acc', step=self.eval_times, value=logs['acc'])self.eval_writer.flush()self.eval_times += 1
import paddlefrom paddle.vision.transforms import Transposefrom paddle.vision.datasets import Cifar10from paddle.vision.models import resnet18from paddle.optimizer import Adamfrom paddle.nn import CrossEntropyLossfrom pyxlpr.ai.paddle import ClasAccuracy, VisualAcc # pip install pyxllib>=0.2.48# 0 设备paddle.set_device('gpu:0')# 1 数据# datasets相关处理默认是pil模式,这里使用cv2模式,确保获得的是np.ndarray类型paddle.vision.set_image_backend('cv2')# 使用Cifar10数据集,训练集5万张,测试集1万张# 这里的 Transpose 是预设好的基础transform,做了通道调整:HWC->CHWtrain_dataset = Cifar10(mode='train', transform=Transpose())val_dataset = Cifar10(mode='test', transform=Transpose())# 2 调用MobileNetV2模型,将其传给paddle.Model高层API接口类model = paddle.Model(resnet18(num_classes=10))# 进行训练前准备model.prepare(Adam(0.01, parameters=model.parameters()), # 优化器CrossEntropyLoss(), # 损失函数ClasAccuracy(print_mode=2) # 测评函数,改成了自定义测评类)# 3 启动训练,运行需要850M显存model.fit(train_dataset, val_dataset, # 训练集、验证集epochs=5,batch_size=64,save_dir='./output',callbacks=[VisualAcc('./output', 'exp001')])
自定义Metric效果
Epoch 5/5...step 780/782 - loss: 0.5276 - acc: 0.7375 - 32ms/step...Eval begin......step 157/157 - loss: 0.4542 - acc: 0.6772 - 16ms/step{'f1_weighted': 0.6692, 'f1_macro': 0.6692, 'f1_micro': 0.6772}pred 0 1 2 3 4 5 6 7 8 9gt0 784 13 29 10 5 0 4 3 120 321 30 804 10 4 1 0 9 3 52 872 115 9 683 22 43 10 57 9 39 133 69 14 160 431 67 29 109 24 62 354 75 6 119 37 611 3 58 46 33 125 40 10 217 189 52 315 66 45 39 276 30 9 85 26 20 6 779 4 25 167 48 7 91 39 59 14 13 694 7 288 54 19 11 3 0 0 4 1 891 179 53 82 19 4 2 2 6 5 47 780
自定义callbacks实现visualdl可视化
注意我的VisualAcc需要设置根目录和子目录:VisualAcc(‘./output’, ‘exp001’),最后得到的是这样的数据:
在项目目录下执行: visualdl —logdir .

如果还有不同参数实验,可以子目录命名exp002、exp003…
然后在visualdl里可以看到不同实验的acc变化效果图。
visualdl页面中的,我一般都会把“最值”打开,能清晰看到最佳模型效果是哪个,以及关闭“平滑度”。
关于“平滑度”的配置看需求,我一般不调,就是想看每个batch的峰值、谷值,像过山车一样~
如果只想看整体趋势的变化,则可以设一下smoothing会平滑一点。
