📗 第7章 深度学习工具
📄 一句话概括

Pylearn2 的规矩:训练不写在代码里,写在配置文件里。 它只提供一个 train.py 入口,参数全从一份 YAML 读。 实测真拿 yaml.safe_load 解析:epochs: 1500 认成整数、lr: 0.1 认成浮点、 layers: [64, 32, 16, 10] 认成列表。 按这份配置训练 400 张图、2778 个参数,测试 0.9287。 只改 lr 从 0.02 到 2.0,测试从 0.8775 到 0.9487。

📝 本节和前面几节的关系

上一节的 Theano 解决「怎么算」,这一节解决「怎么描述一次实验」。 两者当年经常配着用:Theano 当引擎,Pylearn2 当外壳。 这一节的重点不是 Pylearn2 本身(它 2017 年就停了), 而是「配置驱动」这个思路为什么能活到今天。

一、训练写在配置里

先看一份真实的配置长什么样:

dataset:
  name: digits8x8
  n_per_class: 40
  jitter: 1
  noise: 0
model:
  layers: [64, 32, 16, 10]
  act: tanh
train:
  epochs: 1500
  lr: 0.1
  seed: 0

三块内容:数据从哪来、网络长什么样、怎么训练。 这三块恰好就是任何一次实验都绕不开的三件事。 配置驱动的好处不在「代码更短」,在「实验管得住」: 十几个人的实验室共用一个 YAML 目录,谁跑了什么、用了什么超参数,看文件名和内容就知道, 不用去翻 git 历史。

二、解析出来是什么

YAML 不是字符串拼接,它自己认类型:

配置路径 解析出的值 类型
dataset.name digits8x8 str
dataset.n_per_class 40 int
model.layers [64, 32, 16, 10] list
model.act tanh str
train.epochs 1500 int
train.lr 0.1 float
800 是整数不是字符串,0.1 是浮点不是字符串——这一点很重要。
因为它意味着写配置的人不用关心类型转换,写 lr: 0.1 就够了。
但也正是这一点埋下了坑,下一段说。

三、改参数的成本

配置驱动真正的价值,是把「改一个超参数」的成本压到了最低。 下面同一份数据、同一个网络,只改 train.lr:

学习率 测试准确率 训练准确率
0.02 0.8775 0.9975
0.1 0.9287 1.0000
0.5 0.9275 1.0000
2.0 0.9487 1.0000
四行代码一个字没改,只是换了配置文件里的一个数。
幅度不算惊人(七个点),但要注意最好的不是中间那一档,是学习率最大的 2.0。
这跟「自己写代码」有什么不同?不同在扫参的成本。
换代码要改文件、要记得自己改过什么;换配置只改一个数,还能把配置存档。
所以配置驱动不是为了效果好,是为了实验管得住。

四、配置的通病:报错往后拖

YAML 只认语法,不认你的意图。写错了,往往要等跑到那一步才炸。三种典型:

写错的方式 报什么 什么时候炸
缺 model.act KeyError: 'act' 取这个字段时
lr 写成 "abc" TypeError 拿它去乘一个数时
layers 写成 "64-32-16" ValueError 逐字符转整数时
第一条还算好查,KeyError 直接把键名甩给你。
第二条最磨人:配置解析得好好的,一直到 lr 乘梯度 那一行才报错,报错信息指着梯度那一行,
跟真正的原因隔了十万八千里。
第三条最阴:字符串 "64-32-16" 是能迭代的,于是代码把它一个字符一个字符地转成整数,
转到 - 时抛 ValueError。
报错信息会指着一个跟真实原因毫不相干的字符。
对策是加一层校验:读进来之后立刻检查类型和取值范围,让错误在最早的地方暴露。
现在流行的 hydra 就是在 YAML 外面套了一层结构定义,专门治这个。

五、动手 Demo:真解析 YAML、按配置训练、以及三种写错的下场

这一段用的是真的 yaml 库,不是自己写的解析器。 配置里的每个字段都会真的被用到训练里,不是摆样子。 最后一段故意写三份错配置,看它们各自怎么炸——这三条都是真实报错。

import yaml

CFG = u"""
dataset:
  name: digits8x8
  n_per_class: 40
  jitter: 1
  noise: 0
model:
  layers: [64, 32, 16, 10]
  act: tanh
train:
  epochs: 1500
  lr: 0.1
  seed: 0
"""

cfg = yaml.safe_load(CFG)
print(type(cfg['train']['epochs']), type(cfg['train']['lr']))
# <class 'int'> <class 'float'>   YAML 自己认类型

# 按配置建模型、训练,一行代码都不改
Xtr, ytr = make_data(cfg['dataset']['n_per_class'],
                     cfg['dataset']['jitter'],
                     cfg['dataset']['noise'], seed=0)
m = fit(Xtr, ytr, cfg['model']['layers'][1:-1],
        act=cfg['model']['act'],
        steps=cfg['train']['epochs'],
        lr=cfg['train']['lr'])

# 只改一个数,重跑
for lr in ('0.02', '0.1', '0.5', '2.0'):
    c = yaml.safe_load(BASE % lr)
    mm = fit(Xtr, ytr, c['model']['layers'][1:-1], lr=c['train']['lr'])
    print(lr, acc(Xte, yte, mm))

实跑输出:

--------------------------------------------------------------------
Pylearn2:配置驱动训练,改配置不改代码
--------------------------------------------------------------------
[1] Pylearn2 的规矩:训练不写在代码里,写在配置里
  它提供的是 train.py 这一个入口,参数全从一份 YAML 里读。
  下面这份配置是真的 YAML,等一下要拿 yaml.safe_load 解析它:

    # 数据从哪来
    dataset:
      name: digits8x8
      n_per_class: 40
      jitter: 1
      noise: 0

    # 网络长什么样
    model:
      layers: [64, 32, 16, 10]
      act: tanh

    # 怎么训练
    train:
      epochs: 1500
      lr: 0.1
      seed: 0

[2] 解析结果就是普通的字典,没有任何魔法
  路径                      值                类型
  dataset.name              digits8x8         str
  dataset.n_per_class       40                int
  model.layers              [64, 32, 16, 10]  list
  model.act                 tanh              str
  train.epochs              1500              int
  train.lr                  0.1               float

  注意 train.epochs 解析出来是整数 800,不是字符串。
  YAML 会自动认类型:`800` 是数,`0.1` 是浮点,`tanh` 是字符串。

[3] 按配置建模型、训练
  配置项                  实际用到的值
  数据量                  400 张
  层结构                  [64, 32, 16, 10]
  参数量                  2778
  训练轮数                1500
  测试准确率              0.9287

[4] 配置驱动的价值:改参数不改代码
  学习率    测试准确率    训练准确率
  0.02      0.8775        0.9975
  0.1       0.9287        1.0000
  0.5       0.9275        1.0000
  2.0       0.9487        1.0000

  四行代码一个字没改,只是换了配置文件里的一个数,测试就从 0.8775 到 0.9487。
  幅度不算惊人(七个点),但要注意最好的不是中间那一档,是学习率最大的 2.0。
  这跟自己写代码有什么不同?不同在「扫参的成本」:
  换代码要改文件、要记得自己改过什么;换配置只改一个数,还能把配置存档。
  这就是配置驱动真正的价值——不是为了效果好,是为了实验管得住。

[5] 代价:配置写错了,报错要往后拖很久
  缺 model.act      KeyError: 'act'
  lr 写成字符串     TypeError: can't multiply sequence by non-int of type 'fl
  层写成字符串      ValueError: invalid literal for int() with base 10: '-'

  三种错法,三种报错,而且都要等跑到那一步才炸:
  第一个是缺字段,KeyError 直接把键名甩给你,还算好查。
  第二个 lr 是字符串,一直不报错,直到拿它去乘一个数才 TypeError。
  第三个最阴:字符串 "64-32-16" 是能迭代的,
  于是代码把它一个字符一个字符地转成整数,转到 "-" 时 ValueError。
  报错信息会指着一个跟真实原因毫不相干的字符。
  YAML 只认语法,不认你的意图——这是配置驱动的通病。

[6] 落盘验证:它真的能当文件用
  写到 C:\Users\admin\AppData\Local\Temp\workbuddy-update-x64\_pl2_cfg.yaml
  读回来 equal 原字典:True
  文件大小 231 字节——比等价的 Python 代码短得多。

  Pylearn2 当年就是靠这个:实验室里十几个人共享一个 YAML 目录,
  谁跑了什么实验、用了什么超参数,看文件名和内容就知道。
  2017 年它停止维护,但它这套「配置即实验记录」的做法被后来的
  各种实验管理工具(比如 hydra、wandb 的 sweep)继承了下来。

yaml.safe_load 里的 safe 不是装饰。 yaml.load 能构造任意 Python 对象,等于让配置文件执行代码; safe_load 只认基本类型,是读取别人给的配置时该用的那个。 训练那一段有个细节:cfg['model']['layers'][1:-1]。 配置里写的是完整结构 [64, 32, 16, 10](含输入输出维), 而 fit 要的是中间的隐层 [32, 16],所以要切掉头尾。 这种「配置的写法和代码的用法不一致」的地方,是配置驱动最容易出错的地方。 最后那张表里,lr 那一行最好玩:看起来四行差别不大,但训练准确率全是 1.0000—— 说明这个网络容量足够,学习率影响的只是「收敛到多好」,不是「能不能学会」。

30 秒实验

  1. 把 model.layers 改成 [64, 8, 10] 再跑。 瓶颈太窄,测试准确率会掉到 0.8 以下——配置改结构也很方便。
  2. 把 dataset.jitter 从 1 改成 0。 训练集没有平移,测试集有,准确率会掉十几个点:这就是数据增强的价值。
  3. 给配置加一个 train.momentum 字段,然后在 fit 里用上它。 感受一下配置驱动的代价:加一个参数要改两个地方,配置和代码必须同步。

常见误区

  1. 「配置文件只是把命令行参数挪了个地方。」 不一样。配置文件能存档、能进版本库、能对比两次实验的差异。 命令行参数敲完就没了,三个月后没人记得当时用了什么。
  2. 「YAML 比 JSON 好写,所以用它。」 好写是真的,但 YAML 的坑也多:缩进错一格就是另一个结构,yes 会被解析成布尔值, 1.0 和 1 类型不同。 选它是因为可读性,不是因为它安全。
  3. 「配置驱动能提升效果。」 它一点都不提升效果。它提升的是「试错效率」——同样的时间能试更多组参数,仅此而已。
自测三问(点开对答案) 1. **为什么 `yaml.safe_load` 比 `yaml.load` 安全?** —— **因为 `yaml.load` 支持标签构造,配置里写一行特殊标记就能构造出任意对象,** **等于让配置文件执行代码。** **`safe_load` 只认字典、列表、字符串、数字、布尔这几个基本类型,** **读别人给的配置时必须用它。** 2. **配置写错了,为什么要尽早校验?** —— **因为错误信息和真实原因隔得越远,排查成本越高。** **`lr` 是字符串这件事,拖到乘法那一行才报错,你会去查梯度计算,** **而真正的问题在配置文件第一行。** 3. **Pylearn2 停了,为什么这套做法还在?** —— **因为「实验要能复现」这个需求没变。** **今天的 hydra、wandb sweep、各种实验管理平台,做的都是同一件事:** **把一次实验的全部参数固化下来,可查、可比、可复现。**

配置驱动解决了「参数怎么记」。 下一节的 Caffe 走得更远:连网络结构本身都写成文件,用一份 prototxt 描述每一层怎么连。

相关笔记