本文汇总使用 PyTorch 搭建项目时的一些边缘性的工程性知识,让代码真正地成为一个完整的深度学习项目。这部分内容包括如何可视化数据、读写训练进度等。本文参考 Dive into Deep Learning (PyTorch 版) 中的以下内容:
- 5.5 节:读写文件;
我也写过一个持续学习项目 HAT 的代码笔记,有助于理解一个完整的深度学习项目是如何写出来的,请参考:<>。
读写训练进度
深度学习程序的一个特点是运行时间长,一个任务经常需要跑几天、几个月。可以把深度学习程序比作 RPG 游戏,打通关时间长的 RPG 时我们需要定期存档,不仅为了下一次打开游戏时接续进度,还能预防电脑未响应、死机等突发情况导致游戏白打,甚至有时需要换台电脑玩这个进度、应当存档拷贝到新电脑;而且有时候会存多份档,为了预防游戏中某一次策略错误(如,买错了道具;打 boss 打不过去或者游戏有 bug 导致的陷入死循环,俗称坏档)导致的严重后果,起到后悔药的作用。
大型的深度学习程序需要定期存档且存多份档,和上面是一个道理,不必多解释了。它与游戏的不同在于用户无法在运行过程中手动控制,只有停止程序这一个选择;定期存档的操作需要预先写进代码里。
深度学习程序也是 Python 程序,当然可以用 Python 自带的文件读写功能,将变量保存于本地文件。但 PyTorch 为深度学习设计了专门更高级的 API,更加方便,最好使用这套 API。PyTorch 可以读写 Tensor 对象,nn.Parameter
对象,还可以是 {字符串:Tensor或Parameter}
的字典:
torch.save(obj, path)
:将对象 obj 存到路径为 path 的文件中;obj = torch.load(filename)
:将 filename 文件存储的变量赋值到 obj。 此类文件属文本文件,PyTorch 推荐使用扩展名.pt
,.pth
(书中使用了.params
)。存档文件最好存储在项目单独的一个子目录下。
深度学习最需要存档的东西是模型参数,它是训练的目标。网络结构无需保存,因为它就写在代码里,只需保存其参数即可。保存模型参数的推荐方法是存它的 .state_dict()
(前面说过它是存所有模型参数的字典),因为 nn.Module
有一个方便的 API:net.load_state_dict(state_dict)
,能将 state_dict 一步读取所有参数到模型 net 中。
除了模型参数,还有一些必须存档的信息:当前 epoch 轮数,优化器里还有一些状态量(optimizer.state_dict()
),如果用了调度器它也有状态量 scheduler.state_dict()
,等等。可以将其统统打包成一个字典,类似下面的做法:
1
2
3
4
5
checkpoint = {
'epoch': epoch,
'net': net.state_dict(),
'optimizer': optimizer.state_dict()
}
除此之外,为了方便,也可以打包进去其他需要记住的东西,如超参数、配置变量、当前 loss 等统计信息,等等。写到字典里是为了方便程序内使用,如果只是给人看一下,一些小的信息也可以传给 path
,写到文件名内。
下面讨论存档的频率。首先要说一点,为了实现多份存档,文件名最好不一样,防止覆盖。存档太频会浪费硬盘空间,例如一个 batch 或 epoch 一存;太不频则有更大的重新训练风险。而且并不是所有的档都需要存,和游戏一个道理,一般是在比较关键的进度存一下档。常见的做法是在训练循环体中设置条件判断语句写的检查点(checkpoint),判断是不是关键的存档。
以下是一套完整的流程(引自知乎,作者“”人类之奴):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
start_epoch = -1
# 如果接续训练(RESUME=1),则加载 checkpoint
if RESUME:
path_checkpoint = checkpoint_path
checkpoint = torch.load(path_checkpoint)
start_epoch = checkpoint['epoch']
net.load_state_dict(checkpoint['net'])
optimizer.load_state_dict(checkpoint['optimizer'])
scheduler.load_state_dict(checkpoint['lr_schedule'])
for epoch in range(start_epoch+1, num_epochs):
train(net, train_loader)
test_loss = test(net, test_loader)
# 检查点:测试集 loss 小于一定阈值。epoch 小于一半总训练轮数时认为训练不够,不设检查点
min_loss_val = 1
if epoch > int(num_epochs/2) and test_loss <= min_loss_val:
min_loss_val = test_loss
checkpoint = {
'loss':test_loss,
'epoch':epoch,
'net':net.state_dict(),
'optimizer':optimizer.state_dict(),
'lr_schedule':scheduler.state_dict()}
if not os.path.isdir(r'tf-logs/'+save_model):
os.mkdir(r'tf-logs/'+save_model)
torch.save(checkpoint,r'tf-logs/'+save_model+'/ckpt_best_%s.pth'%(str(epoch+1)))
读写功能除了上述存档接续训练进度,还有其他常见的应用场景:例如保存训练好的参数给别人使用。常见的大型网络可以使用别人预训练的权重,再在自己的任务上微调,这些预训练权重通常保存在
.pth
文件中,从网上下载。
可视化
深度学习里很多内容需要可视化,辅助研究并呈现结果,例如:
- 数据集中的数据;
- 网络结构;
- 学习曲线、指标的变化曲线等。
为实现此目的,除了可以手动调用例如 Matplotlib 等天然的可视化工具,深度学习框架也开发了自己的可视化工具。本章介绍 TensorBoard 的使用,它是 TensorFlow 的可视化工具,目前也支持了 PyTorch。
TensorBoard
TensorBoard 逻辑
TensorBoard 的逻辑可以看成一个画家,以及一个画布,给画家各种作画指示,它就会按要求在画布上作出各种图。
工具的两个部分:
- 画家和各种作画指示:在
torch
库中,存放在torch.utils.TensorBoard
。画家是TensorBoard.SummaryWriter
类,作画指令就是类方法add_xx(...)
(xx 表示各种支持的内容,例如 scalar、graph 等),每调用一次就会在画布上画方法参数中对应的内容; - 画布:是一个本地软件,在本地端口运行(浏览器打开,类似于 Jupyter Notebook),需要额外安装。必须启动画布,才能看到画家作的画。
问题来了,画家和画布是两个程序,画布怎么知道画家的作画内容呢?这是通过日志实现的。画家作画其实是输出了一些画布能读懂的日志,画布通过输入日志来呈现画家的作画。这些日志存放在文本文件(称为日志文件),并通常放于专门的日志目录下(在代码中,画家和画布都是从指定目录下输出、输入日志),使用时应当为画家和画布指定相同的日志目录。
命令总结:
- 安装画布:
conda install tensorboard
; - 启动画布:
tensorboard --logdir=log
(runs 为日志目录,必须指定),并按提示打开浏览器端口; - 召唤画家:
1 2
from torch.utils.tensorboard import SummaryWriter summaryWriter = SummaryWriter(log_dir='log') # 实例化画家,log_dir 为日志目录
日志文件的组织方式:每运行一次(一个 “run”,即每实例化一个画家 SummaryWriter)都会产生一个新的日志文件。日志文件中记录了时间、设备等元信息与该画家的作画内容信息。画布会呈现所有日志文件所画内容的并集(可以在界面左下角选择部分的 “run” 显示),因此画家之间唯一的区别方式就是日志目录。
TensorBoard 能画什么
官方文档:https://pytorch.org/docs/stable/TensorBoard.html
TensorBoard 通过 SummaryWriter 类的 add_xx
方法来画不同的内容,呈现在画布的各个版块上(上方选项卡),每个版块都有包含若干子版块(右方);画布是交互式的而非静态,可以在画布上进一步调整可视化的效果,甚至导出(左方)。
add_xx
方法有共同的参数:
tag
参数:这部分内容的名字(字符串),必须指定。walltime
参数:默认为系统时间time.time()
。可以在 TensorBoard 界面红可视化这一信息。在画布 TIME SERIES 版块可以查看所有作画历史记录,会将调用的add_xx
作出的内容按照该时间顺序排列。
以下是 TensorBoard 能画的东西(详细用法见文档,我只总结核心的东西):
- 画曲线
add_scalar
- 呈现在画布 SCALARS 版块;
- 在曲线
tag
上添加一个坐标为 (global_step, scalar_value) 的点(注意 global_step 必须为整数); - 不同的曲线画在不同的图上,一个子版块对应一个图;
add_scalars
可以把多个曲线画在同一个图上; - 同理可以画直方图:
add_histogram
、二维图add_mesh
等。
- 画图像:
add_image
- 呈现在画布 IMAGES 版块;
- 在 IMAGES 板块的子版块
tag
上打印格式为 Tensor 类型的图像 img_tensor; add_images
可以在一个子版块打印多个图像;- 同理可以画其他数据:音频
add_audio
,文本add_text
,视频add_video
,Matplotlib 的 figureadd_figure
;
- 画表格:
add_hparams
- 呈现在画布 HPARAMS 版块;
- 一次调用就添加一条表格记录(表格的一行);
- 传入字典,字典的键对应属性,值对应属性值;
- 应传入两个字典,一个是自变量,一个是因变量;区分它们的意义是画布有对因变量做数据分析的交互功能;
- 画计算图:
add_graph
- 画 tensor 中存储的计算图;
- 传入
torch.nn.Module
模型即可;
- 画 PR 曲线:
add_pr_curve
- 传入预测标签和真实标签,会自动计算准确率和召回率;
- 一次调用就在 PR 坐标上添加一个点。
- 在低维空间(不超过 3 维)上展示高维数据:
add_embedding
- 采用的降维方法是 t-SNE,是数据可视化常用的降维方法;
- 传入数据矩阵 Tensor;
- 可以传入类别标签,则会以不同颜色显示类别;也可以传入其他类型的标签如字符串乃至图像,则图中的数据点会显示字符串或图像。
TensorBoard 在深度学习中的用处
从上面看,TensorBoard 和通用的可视化工具的功能与逻辑差别不大。但它一开始就是为深度学习可视化设计的,主要兼容深度学习框架 Tensor 类型的数据,设计的可画内容都是深度学习需要可视化的。深度学习需要可视化:
- 数据:
add_images
,add_video
等; - 学习曲线、loss 曲线等指标(随时间变化):
add_scalar
; - 网络结构图:用
add_graph
; - 不同超参数的效果比较:
add_hparams
(顾名思义,add_hparams
画表格就专门为超参数这事的); - 在低维空间可视化模型中间层 Embedding:
add_embedding
; - PR 曲线:
add_pr_curve
。
深度学习的程序往往耗时很长,需要有存档机制,在代码中设置一些检查点(checkpoint),保存、加载训练到一半的模型参数等在这篇笔记中有详细的讨论。TensorBoard 也需要有存档机制,根据日志文件的组织方式——每次运行都会保存一个日志文件,画布会加载日志目录下的所有日志文件,所以我们无需手动保存、加载 TensorBoard 画图的进度。
##
本文介绍 Python logging
库的使用方法,该库是 Python 中更复杂的调试工具,可以将程序的调试信息输出为格式更丰富的日志或日志文件,适合大型项目(例如深度学习)的调试与监控。
logging
tqdm
超参数优化
(待更新)
对于很大的实验,在真正开始训练前,最好对代码做一个完整的检查。
学习曲线
过拟合、欠拟合的判断。
随机数种子
深度学习有很多地方涉及随机数:
- 数据增强;
- 持续学习里构造数据集的 Permuted 操作;
为了避免每次实验因为随机数的原因都不一样,也为了别人能够复现,一个完整的深度学习项目通常要设定随机数种子(seed)。计算机里的随机数生成算法都是伪随机数,算法接受一个随机数种子作为输入,通过固定的计算过程模拟某个分布得到这种随机数的。因此,一但随机数种子被人为设定,生成的随机数也就固定了,设定随机数种子起到固定随机数的作用。
Python 中凡是随机算法的程序都有设定种子的接口,包括两种:
- 局部变量设定:随机数函数一般有指定种子的参数如
random_state
; - 全局设定:对随机数模块调用
seed
函数;
深度学习程序中,最省心的做法是在程序开头对所有随机数模块(包括 Python 内置 random 模块、numpy.random、torch 等),也一般打包成函数:
1
2
3
4
5
6
7
8
def setup_seed(seed):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.deterministic = True
setup_seed(0) # 调用
使用随机数种子应当注意:
- 设定随机数种子会拖慢程序,对运算量大的深度学习程序有影响;
- 随机数的随机性一般对深度学习结果的影响不会太大;
- 随机数种子是依赖于机器的,设定同样的种子,在不同的机器上结果会不同。 应当根据需要,决定需不需要设定种子。