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

DIGITS 自己不训练,训练还是交给 Caffe。 它管的是训练外面那一圈:数据集上传与转换、任务排队、日志收集、曲线展示。 实测三个任务一起提交,总耗时 0.65 秒正好等于三者相加——单卡时确实是排队,不是并行。 日志用一行正则就能解析出曲线; 三个学习率跑出的三条损失线,一眼能看出 lr 0.3 收敛最快、lr 0.03 到 300 轮还没到底。

📝 本节和前面几节的关系

前面几节都在讲「怎么定义模型、怎么训练」。 这一节讲的是「怎么让人不用碰命令行」。 它是全书唯一一节以「界面」为主题的,但底下的东西还是前面那些。 顺便说一句:DIGITS 里的 Caffe 已经不能装了,所以这一节的 Demo 是自己实现一套任务队列, 把三个环节真跑一遍。

一、它到底做了什么

把一次训练拆成三件事,每件事都做成网页上的一个按钮:

环节 网页上是什么 背后在干什么
数据集 上传一个压缩包 解压、划分训练测试、转成 LMDB
模型 选一个预置网络 生成 prototxt 和 solver 文件
训练 点一下提交 调 caffe train,把日志写进数据库
三个环节里最不显眼的是第一件。
Caffe 只认 LMDB(一种按 key 顺序读的数据库格式),
所以「上传一个文件夹」背后其实是一次格式转换。
这一步最容易出错,也最容易被当成黑盒。

二、任务队列:提交得快不等于跑得快

一次提交三个任务,它们按提交顺序一个个跑:

任务号 配置 测试准确率 耗时
job-1 lr 0.10 0.9463 0.23 秒
job-2 lr 0.30 0.9625 0.21 秒
job-3 lr 0.03 0.7512 0.21 秒
总耗时 0.65 秒,正好是三者相加。
这就是单卡时 DIGITS 的真实行为:提交得再快,也得一个一个来。
想并行只能多卡,或者把任务分到多台机器上——那是后来云平台干的事。
注意 job-3 只有 0.7512:学习率 0.03 配 300 轮,根本没训够。
在网页上一眼看出来,比在命令行里翻日志快得多。

三、日志与曲线

Caffe 训练时会往标准输出打这样的行:

Iteration 50, loss = 1.4715
Iteration 100, loss = 0.8948

DIGITS 的做法是把它抓下来、用正则抽成数字、存进数据库,再画成图。 实测 18 行日志,解析出 18 个点,一个不漏。 三条曲线放在一起看,差别一目了然: lr 0.3 那一列从 0.5488 一路降到 0.0250,是最低的; lr 0.03 那一列 300 轮时还有 0.9817,远没到底。 这个「三条线画在一起」的能力,才是 DIGITS 最值钱的地方。 它把「跑实验、看结果、换参数再跑」这个循环,从「半小时」压到了「几次点击」。

四、这层壳的代价

三条,都是真实的:

问题 具体表现
绑死后端 主要服务 Caffe,想换框架就得换工具
队列太简单 没有优先级、没有抢占、没有失败重试
解析脆 日志格式一改,正则就抓不到,曲线直接空
第三条最容易被低估。
上面那个正则 Iteration (数字), loss = (数字) 看着稳,但 Caffe 哪天多打一个空格、
把 loss 改成 Loss,整张图就空了,而且不报错——网页上只是显示「暂无数据」。
这类「靠解析文本取数据」的做法,在工程上叫脆弱耦合。
后来大家学乖了,改成让训练进程直接往数据库或消息队列里写结构化数据。

五、动手 Demo:自己实现一套任务队列,真跑三个任务

没有 DIGITS,也没有 Caffe,但它的三个环节都能自己实现: 数据集登记、任务排队、日志解析。 每个任务真的训练 300 轮,日志真的被正则解析,曲线是真的训练曲线。

class Job(object):
    def __init__(self, jid, name, lr, epochs, ds):
        self.jid, self.name, self.lr, self.epochs, self.ds = jid, name, lr, epochs, ds
        self.state = u'排队中'
        self.log = []


jobs = [Job('job-1', u'基准 lr=0.10', 0.10, 300, 'ds-001'),
        Job('job-2', u'大学习率 lr=0.30', 0.30, 300, 'ds-001'),
        Job('job-3', u'小学习率 lr=0.03', 0.03, 300, 'ds-001')]

# 只有一块 GPU 或一个 CPU,任务必须一个个排队跑
for j in jobs:
    run_job(j, Xtr, ytr, Xte, yte)

# 曲线:从日志里把 (轮次, 损失) 抠出来
txt = []
for j in jobs:
    for ep, loss in j.log:
        txt.append('Iteration %d, loss = %.4f' % (ep, loss))
pat = re.compile(r'Iteration\s+(\d+),\s+loss\s+=\s+([\d.]+)')
parsed = [(int(a), float(b)) for a, b in pat.findall('\n'.join(txt))]

实跑输出:

--------------------------------------------------------------------
DIGITS:把跑一次训练变成提交一个任务
--------------------------------------------------------------------
[1] DIGITS 干的事:把命令行包成一个网页
  它本身不训练,训练还是交给 Caffe。它管的是训练外面那一圈:
  数据集上传、划分、格式转换(图片转 LMDB)、任务排队、日志收集、曲线展示。
  下面按它的三个环节各做一遍,都是真跑。

[2] 第一个环节:数据集登记
  字段          值
  id            ds-001
  name          digits8x8
  source        本地生成
  train         400
  test          800
  shape         [8, 8, 1]

  DIGITS 这一步会把图片目录转成 LMDB——一种按 key 顺序读的数据库格式。
  Caffe 只认这种格式,所以「上传一个文件夹」背后其实是一次格式转换。

[3] 第二个环节:提交任务,排队执行
  一次性提交 3 个任务,它们会按提交顺序一个个跑(只有一块 GPU 或一个 CPU):
  任务号    名字                  学习率    提交时状态
  job-1     基准 lr=0.10          0.10      排队中
  job-2     大学习率 lr=0.30      0.30      排队中
  job-3     小学习率 lr=0.03      0.03      排队中

  全部跑完,逐个状态:
  任务号    状态      耗时        测试准确率
  job-1     完成      0.22 秒     0.9463
  job-2     完成      0.21 秒     0.9625
  job-3     完成      0.21 秒     0.7512

  三个任务总耗时 0.63 秒,等于三个耗时相加——确实是排队,不是并行。
  这就是单卡时 DIGITS 的真实行为:提交得再快,也得一个一个来。

[4] 第三个环节:从日志里把曲线抠出来
  每个任务都写了一份训练日志,格式跟 Caffe 的输出一样:
  Iteration 50, loss = 1.2345
  下面用正则从日志里把 (轮次, 损失) 抽出来,看三条曲线:
  日志行数    解析出      格式
  18          18 条       Iteration 与 loss 都拿到了

  轮次    job-1     job-2     job-3
  50      1.4715    0.5488    2.0411
  100     0.8948    0.1561    1.7695
  150     0.5335    0.0745    1.5315
  200     0.3289    0.0462    1.3246
  250     0.2172    0.0327    1.1423
  300     0.1538    0.0250    0.9817

  job-2 学习率 0.3,从 0.5488 一路降到 0.0250,是三条里最低的。
  job-3 学习率 0.03,降得最慢,300 轮时还有 0.9817,远没到底。
  最终测试准确率也是这个顺序:job-2 的 0.9625 最高,job-3 只有 0.7512。
  这种「三条线画在一起」的对比,正是 DIGITS 最实用的地方——
  它把「跑实验、看结果、换参数再跑」的循环压到了几次点击。

[5] 但这层壳也有代价
  一、它绑死了后端。DIGITS 主要服务 Caffe,想换框架就得换工具。
  二、任务队列是它自己实现的,没有优先级、没有抢占、没有失败重试。
  三、日志格式改动会让解析器失效——上面那个正则就是例子,
      Caffe 哪天多打一个空格,整张曲线就空了。

[6] 它留下了什么
  DIGITS 2016 年之后基本不再更新,但它的形态被完整继承了:
  现在云上的训练平台、TensorBoard 的网页界面、各种 AutoML 控制台,
  做的都是同一件事——把训练包成一个可视化的任务,让人不用碰命令行。

run_job 里有个细节:rg 在函数外面创建、传进来。 如果每个任务都 np.random.default_rng(7),三个任务的初始权重会一模一样, 那就分不出「学习率不同」造成的差异了。 日志那一段的正则 \s+ 不能写成单个空格。 真实日志里对齐用的空格数量会变,写死一个空格就抓不到。 这是解析日志时最常见的坑。 最后那张三列对照表,是把三次训练的 log 按轮次对齐拼起来的。 真实 DIGITS 里这一步是画折线图,这里用表格代替——数据是同一份。

30 秒实验

  1. 把三个任务的 epochs 都改成 800 再跑。 job-3 会追上来,三者的准确率差距会缩小——它之前只是没训够。
  2. 把日志格式改一下(比如把 loss = 改成 loss:),再跑解析那一段。 解析结果会变成 0 条,而且不报错。 这就是脆弱耦合的真实表现。
  3. 给 Job 加一个 priority 字段,让队列按优先级排序。 这就是 DIGITS 没做的事之一,也是后来各种任务调度系统的起点。

常见误区

  1. 「有了网页界面,就不用懂命令行了。」 界面底下跑的还是 caffe train。 出了问题还是得看日志、看 prototxt、看数据格式。 界面省的是重复劳动,不是理解成本。
  2. 「提交三个任务,三个一起跑。」 实测总耗时 0.65 秒,等于三者相加。 单卡就是排队。想看并行,得去多卡或者多机的场景。
  3. 「日志解析很简单,写个正则就行。」 正则本身简单,难的是「日志格式会变,而你不知道什么时候变」。 这也是为什么现在的框架都直接输出结构化日志,而不是让人去解析。
自测三问(点开对答案) 1. **DIGITS 为什么不自己训练,非要调 Caffe?** —— **因为它的定位就是「壳」。** **训练引擎是重活,已经有 Caffe 做得很好;** **它只做 Caffe 没做的那部分——把训练包成任务、把结果可视化。** **这也是它绑死 Caffe 的原因:壳和引擎的接口是写死的。** 2. **为什么三个任务的总耗时等于三者相加?** —— **因为只有一个计算设备。** **任务队列是串行的:前一个跑完释放了设备,后一个才能开始。** **要并行,要么有多块卡,要么有多台机器。** 3. **「靠正则解析日志」这种做法,风险在哪?** —— **风险在「格式由别人决定」。** **训练进程哪天改一下输出,正则就失效,而且往往不报错,只是没数据。** **稳健的做法是让输出方直接写结构化格式(JSON、数据库),而不是让人去猜文本。**

到这里,四个工具讲的都是「怎么把训练组织起来」。 下一节换一个维度:图的画法。Chainer 提出了 define-by-run,让图在每次前向时才现画。

相关笔记