1 图像分类/imgcls1_basic.py

相比上节只是讲基本框架的demo,这次新增了很多新知识点,对常用功能封装组件进行使用:

  1. 【超参数】集中管理配置
  2. 【图片数据】的处理,制作dataloader
    1. 使用torchvision内置的MNIST数据
    2. 为了简化代码,使用xl库的File、Dir等文件工具。
  3. 【模型】使用LeNet5
  4. 【训练阶段】使用ITER,而不是epoch的机制迭代训练
    1. 这里需要一个无限抽样的TrainingSampler类
    2. 以及结合ITER,进行周期性保存model权重文件等扩展操作
  5. 【评价阶段】
    1. 对预测器XlPredictor进行封装
    2. 对分类任务评价方法的功能ClasEvaluater进行封装,使用更严谨的f1_score分类指标
  1. import os
  2. from tqdm import tqdm
  3. import torch
  4. from torch.utils.data import DataLoader
  5. from torchvision import datasets, transforms
  6. from pyxllib.xl import TicToc
  7. from pyxlpr.ai.torch import TrainingSampler, LeNet5, XlPredictor, ClasEvaluater # 封装了一些通用组件,简化开发
  8. # 零 配置表
  9. DATA_DIR = 'datasets' # 数据集所在根目录
  10. NUM_CLASSES = 10 # 几分类任务
  11. DEVICE = 'cuda' # 在哪个设备运行
  12. BASE_LR = 0.01 # 学习率
  13. IMS_PER_BATCH = 200 # BATCH_SIZE
  14. MAX_ITER = 600 # 训练集迭代次数
  15. CHECKPOINT_PERIOD = 100 # 每迭代多少次保存模型
  16. STATE_FILE = 'mnist/lenet5_model.pth' # 计划存储权重文件的路径
  17. # 一 数据集
  18. mnist_transform = transforms.Compose([
  19. transforms.ToTensor(),
  20. transforms.Resize((32, 32)),
  21. ])
  22. # 训练集数量 60000, 28*28 -> 32*32
  23. train_dataset = datasets.MNIST(DATA_DIR, train=True, download=True, transform=mnist_transform)
  24. train_loader = DataLoader(train_dataset, batch_size=IMS_PER_BATCH)
  25. # 验证集数量 10000, 28*28 -> 32*32
  26. val_dataset = datasets.MNIST(DATA_DIR, train=False, transform=mnist_transform)
  27. val_loader = DataLoader(val_dataset, batch_size=IMS_PER_BATCH)
  28. # 二 训练、推断
  29. def train():
  30. # 1 准备工作
  31. model = LeNet5(NUM_CLASSES)
  32. model.to(DEVICE) # 使用cpu,还是哪个gpu训练
  33. optimizer = torch.optim.Adam(model.parameters(), lr=BASE_LR) # 学习器
  34. # 2 开始训练
  35. # 这里不用前面预设的train_loader,而是新建一个可以无限迭代的loader
  36. loader = DataLoader(train_dataset, batch_size=IMS_PER_BATCH,
  37. sampler=TrainingSampler(len(train_dataset)))
  38. for i, batch_inputs in tqdm(enumerate(loader, 1), 'train', MAX_ITER):
  39. # 2.1 训练终止标记/正常训练过程
  40. if i > MAX_ITER:
  41. break
  42. loss = model(batch_inputs)
  43. optimizer.zero_grad()
  44. loss.backward()
  45. optimizer.step()
  46. # 2.2 扩展功能,可以根据i进行某些周期性操作,也可以写一些每次迭代都处理的操作
  47. if CHECKPOINT_PERIOD and i % CHECKPOINT_PERIOD == 0:
  48. # 确保父级目录存在,也可以用 os.makedirs(str(STATE_FILE.parent), exist_ok=True) 实现
  49. os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
  50. torch.save(model.state_dict(), STATE_FILE)
  51. def eval():
  52. predictor = XlPredictor(LeNet5(NUM_CLASSES), STATE_FILE, DEVICE, batch_size=IMS_PER_BATCH)
  53. print('【训练集】') # 训练集一般不做eval。但为了分析过拟合欠拟合问题,是可以对比验证集看一下的。
  54. # 因为要返回gt给下游的eval计算分值,所以要打开return_gt,这跟部署阶段使用模式是不一样的
  55. preds = predictor.forward(train_loader, print_mode=True) # 得到所有数据的预测结果
  56. evaluater = ClasEvaluater.from_pairs(preds) # 测评器
  57. print(evaluater.f1_score('all')) # 训练集看下总精度就行了
  58. print('【验证集】')
  59. preds = predictor.forward(val_loader, print_mode=True)
  60. evaluater = ClasEvaluater.from_pairs(preds)
  61. print(evaluater.crosstab()) # 验证集可以详细看下交叉表
  62. print(evaluater.f1_score('all'))
  63. if __name__ == '__main__':
  64. with TicToc(__name__):
  65. train()
  66. eval()
  67. # 不设种子seed的话,每次结果都会不太一样,但这个模型效果基本稳定在0.97
  68. # 2021-07-23 10:51:47 time.process_time(): 2.58 seconds.
  69. # train: 100%|██████████| 600/600 [00:27<00:00, 21.85it/s]
  70. # 【训练集】
  71. # eval batch: 100%|██████████| 300/300 [00:12<00:00, 24.59it/s]
  72. # {'f1_weighted': 0.9743, 'f1_macro': 0.9743, 'f1_micro': 0.9744}
  73. # 【验证集】
  74. # eval batch: 100%|██████████| 50/50 [00:02<00:00, 21.51it/s]
  75. # pred 0 1 2 3 4 5 6 7 8 9
  76. # gt
  77. # 0 957 0 3 0 1 2 4 8 2 3
  78. # 1 0 1126 0 2 0 2 2 1 2 0
  79. # 2 6 5 967 27 4 0 1 13 9 0
  80. # 3 0 0 1 992 0 2 0 8 3 4
  81. # 4 2 0 1 0 970 0 2 1 0 6
  82. # 5 4 0 0 9 0 869 3 1 3 3
  83. # 6 8 2 2 0 6 3 935 0 2 0
  84. # 7 0 4 1 6 1 0 0 1016 0 0
  85. # 8 6 4 1 11 4 3 1 7 935 2
  86. # 9 1 6 0 3 21 3 0 14 4 957
  87. # {'f1_weighted': 0.9724, 'f1_macro': 0.9723, 'f1_micro': 0.9724}
  88. # 2021-07-23 10:52:33 __main__ finished in 45.52 seconds.

注意这里CHECKPOINT_PERIOD只是一个简单的示例,实际运行中,可能要按照model100.pth、model200.pth来保存特定迭代次数后的权重,不要相互覆盖。
在for结束后,还要再存一个modelfinal.pth。eval阶段调用的是modelfinal.pth进行测评。

Macro-F1 Score与Micro-F1 Score - 知乎
weighted:每一类都算出f1,然后按样本量加权平均
macro:每一类都算出f1,然后求平均值(样本少的类依然有同等权重)
micro:按二分类形式直接计算全样本的f1 (效果同常见的acc)

2 MNIST数据可视化

存成图片文件

  1. import os
  2. import os.path as osp
  3. import tempfile
  4. from tqdm import tqdm
  5. from torchvision import datasets
  6. DATA_DIR = osp.join(tempfile.gettempdir(), 'datasets')
  7. # 验证集数量 10000
  8. train_dataset = datasets.MNIST(DATA_DIR, train=False, download=True)
  9. for i, (img, label) in tqdm(enumerate(train_dataset, start=1)):
  10. file = osp.join(DATA_DIR, f'MNIST/test/{label}/{i:06}.jpg')
  11. os.makedirs(osp.dirname(file), exist_ok=True)
  12. # 可以调试,查出img是PIL.Image类型,可以直接用save保存成文件
  13. # 如果是tensor,可以transforms.ToPILImage转PIL
  14. # np.ndarray同理,可以做格式转换,或者使用cv2.imwrite
  15. img.save(file)

image.pngimage.png

(常见)matplotlib

matplotlib是最常见的可视化工具库,其展示图片的方法大概如下:

  1. import matplotlib.pyplot as plt
  2. for i, (img, label) in tqdm(enumerate(train_dataset, start=1)):
  3. plt.imshow(img)
  4. plt.show()
  5. break

还有很多复杂的用法,详见XLPR_Classification,或官方文档
在需要精细化展示标注,比如标出每个检测框,和对应文本信息,特别是结果可视化的时候,用的会比较多。

(高级)tensorboard

也很常见的可视化工具,原本是tensorflow框架体系的一个工具,但也可以直接pip install tensorboard来使用。

在pytorch中,要用from torch.utils.tensorboard import SummaryWriter,
将模型、各种曲线图数据存储到一个目录中。

然后执行命令行:tensorboard —logdir=”logger_dir”,开启服务,默认在http://localhost:6006/

详细用法见 XLPR_Classification,不仅可以看曲线图,还能查看模型结构。

(轻便)visdom

参考:visdom · 语雀,是一个专为pytorch设计的可视化工具,数据结构互通。
先做好准备工作

  1. pip install visdom # 安装
  2. python -m visdom.server # 开服务, http://localhost:8097

visdom是专为pytorch设计的,所以直接兼容标准的dataset格式[batch_size, chanels, height, width]

  1. from visdom import Visdom
  2. vis = Visdom()
  3. for x, y in train_loader:
  4. vis.images(x * 255, win='训练集数据') # ToTensor会变成0~1,但是visdom是按[0,255]来显示图片的
  5. break # 只展示一轮随便看下

image.png


可以输出y,或者使用vis.text对比查看标签;

3 visdom在研发中的应用/imgcls2_visdom.py

一般直接看模型最后的指标,就能对着做消融实验改进了。

但有时候遇到bug,或者需要精调模型的时候,需要分析一些训练过程中的细节问题,这时候可以使用一些可视化等辅助工具手段。

训练过程的loss可视化

之前的各种分类实验,很难把握应该跑几次iter收敛。
此时可以把训练中的loss画出来,能更清楚模型训练中的效果。

只需在train增加几行代码(见红色代码部分,和蓝色解释):
image.png

可以看到损失的变化过程,前面100轮在快速下降,后面500轮则没有显著的下降与收敛。
image.png


如果嫌刚开始的数值尺度太大,不方便查看后面的具体loss效果,有两种办法:

  1. 图表右上角有工具栏,可以调整比例查看
  2. 加判断条件if i > 100,vis.line只在第100次迭代后,才开始展示loss
    1. if i > 100:
    2. if vis.win_exists('loss'): # 添加数据
    3. vis.line([float(loss)], [i], 'loss', update='append')
    4. else: # 第一次展示窗口
    5. vis.line([float(loss)], [i], 'loss', opts={'title': 'loss', 'xlabel': 'epoch'})
    image.png

精度可视化/train和val对比

可以每隔几个大ITER,就对train、val的精度做一轮计算,查看中间结果,这在需要训练好几天的模型中时用的比较多,
也更便于掌握是否过拟合、欠拟合,需要早停等问题。

加个EVAL_PERIOD=200的配置参数,然后完整的train实现如下:

  1. def train():
  2. # 1 准备工作
  3. from visdom import Visdom # 如果在开头写,这里就不用import了
  4. model = LeNet5(NUM_CLASSES)
  5. model.to(DEVICE) # 使用cpu,还是哪个gpu训练
  6. optimizer = torch.optim.Adam(model.parameters(), lr=BASE_LR) # 学习器
  7. vis = Visdom()
  8. vis.close('loss')
  9. vis.close('eval')
  10. # 2 开始训练
  11. # 这里不用前面预设的train_loader,而是新建一个可以无限迭代的loader
  12. loader = DataLoader(train_dataset, batch_size=IMS_PER_BATCH,
  13. sampler=TrainingSampler(len(train_dataset)))
  14. for i, batch_inputs in tqdm(enumerate(loader, 1), 'train', MAX_ITER):
  15. # 2.1 训练终止标记/正常训练过程
  16. if i > MAX_ITER:
  17. break
  18. loss = model(batch_inputs)
  19. optimizer.zero_grad()
  20. loss.backward()
  21. optimizer.step()
  22. # 2.2 扩展功能,可以根据i进行某些周期性操作,也可以写一些每次迭代都处理的操作
  23. # 2.2.1 按周期保存模型,防止突然断网、报错,需要重新开始训练
  24. if CHECKPOINT_PERIOD and i % CHECKPOINT_PERIOD == 0:
  25. Dir(STATE_FILE.parent).ensure_dir()
  26. torch.save(model.state_dict(), str(STATE_FILE))
  27. # 2.2.2 loss可视化
  28. if vis.win_exists('loss'): # 添加数据
  29. vis.line([float(loss)], [i], 'loss', update='append')
  30. else: # 第一次展示窗口
  31. vis.line([float(loss)], [i], 'loss', opts={'title': 'loss', 'xlabel': 'epoch'})
  32. # 2.2.3 模型精度中间结果可视化(含train和test数据效果对比)
  33. if EVAL_PERIOD and i % EVAL_PERIOD == 0:
  34. predictor = XlPredictor(model)
  35. train_f1 = ClsEvaluater(predictor.forward(train_loader)).f1_score()
  36. test_f1 = ClsEvaluater(predictor.forward(val_loader)).f1_score()
  37. model.train() # 计算完要主动转回train模式
  38. if vis.win_exists('eval'):
  39. vis.line([[train_f1, test_f1]], [i], 'eval', update='append')
  40. else:
  41. vis.line([[train_f1, test_f1]], [i], 'eval',
  42. opts={'title': '模型精度', 'legend': ['train', 'test']})

从精度(f1_score)曲线可以看出,虽然后续loss好像变化不大,但模型实际效果确实是有提升的。
image.png

看着图(没图的时候其实直接看结果精度分析也行,只是图表能直观展现更多不易发觉的细节问题),
有问题可以对照下述策略改进(吴恩达课程的学习笔记

  1. 训练集的精度太低,属于高偏差,欠拟合
    1. 可以尝试更大的网络模型
    2. 看loss图,检查是不是没收敛,尝试训练更久一点
    3. 超参数搜索,尝试修改学习率等参数
  2. 训练集还不错,但验证集太低,属于高方差,过拟合
    1. 尝试给模型加正则项
    2. 寻找更大的训练集

4 改resnet,tensorboard示例/imgcls3_resnet.py

tensorboard · 语雀

5 选读:部署阶段

  1. from torchvision import transforms
  2. from pyxllib.xl import XlPath, TicToc, dprint # pip install pyxllib
  3. from pyxllib.xlcv import xlcv # pip install pyxllib[xlcv]
  4. from pyxlpr.ai.torch import LeNet5, XlPredictor # 封装了一些通用组件,简化开发
  5. NUM_CLASSES = 10 # 几分类任务
  6. IMS_PER_BATCH = 200 # BATCH_SIZE
  7. STATE_FILE = XlPath('mnist/lenet5_model.pth') # 计划存储权重文件的路径
  8. def deploy():
  9. """ 部署阶段开发演示
  10. """
  11. # 1 初始化预测器:因为是预测真实数据,没有y标签,可以y_placeholder=-1作为占位符制作dataset数据集
  12. # 权重文件支持给url,会自动下载到本地,在部署一些小模型、可公开功能的时候很方便。也方便在云端替换最新版最好的权重文件。
  13. # 如果读者写的model.forward前传机制不同,本来batch_inputs就只输入x没有y,则这里不用设置y_placeholder参数
  14. numcls = XlPredictor(LeNet5(NUM_CLASSES), STATE_FILE, 'cuda', batch_size=IMS_PER_BATCH, y_placeholder=-1)
  15. numcls.transform = transforms.Compose([
  16. lambda x: xlcv.read(x, 0), # 我自己的一个类,能自动读取文件、或者转换格式成numpy数据,类似cv2.imread,但比其强大的多
  17. transforms.ToTensor(),
  18. transforms.Resize((32, 32)),
  19. ])
  20. # 2 使用函数接口功能,执行下游任务
  21. # 2.1 这是验证集里的图片,训练集和验证集的数据精度都很高,预测基本没有问题
  22. v1 = numcls('test/000031.jpg')
  23. dprint(v1)
  24. # v1<int>=3,准确返回3
  25. # 2.2 这里我们自己手写几个数字试试,存成文件abcde,这些图片接近部署真实场景,尺寸都是随意的,没有固定到32*32
  26. v2 = numcls('test/a.jpg')
  27. dprint(v2)
  28. # v<int>=1,准确识别为1
  29. # 2.3 支持批量识别,forward的时候会使用前面设置的batch_size批量前传
  30. vals1 = numcls(['test/b.jpg', 'test/c.jpg', 'test/d.jpg'])
  31. dprint(vals1)
  32. # vals1<list>=[2, 3, 4],b、d、e都正确识别
  33. # 2.4 传入numpy数据也行
  34. import cv2
  35. imgs = [cv2.imread('test/d.jpg'), cv2.imread('test/e.jpg', 0)]
  36. vals2 = numcls(imgs)
  37. dprint(vals2)
  38. # vals2<list>=[4, 5]
  39. # 2.5 也支持PIL数据格式
  40. from PIL import Image
  41. img = Image.open('test/e.jpg')
  42. vals3 = numcls([img])
  43. dprint(vals3)
  44. # vals3<int>=[5]
  45. if __name__ == '__main__':
  46. with TicToc(__name__):
  47. deploy()

部署所用测试数据包: test.zip.docx (下载后去掉docx后缀)
image.png