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 秒实验
- 把三个任务的
epochs都改成 800 再跑。 job-3 会追上来,三者的准确率差距会缩小——它之前只是没训够。 - 把日志格式改一下(比如把
loss =改成loss:),再跑解析那一段。 解析结果会变成 0 条,而且不报错。 这就是脆弱耦合的真实表现。 - 给 Job 加一个
priority字段,让队列按优先级排序。 这就是 DIGITS 没做的事之一,也是后来各种任务调度系统的起点。
常见误区
- 「有了网页界面,就不用懂命令行了。」
界面底下跑的还是
caffe train。 出了问题还是得看日志、看 prototxt、看数据格式。 界面省的是重复劳动,不是理解成本。 - 「提交三个任务,三个一起跑。」 实测总耗时 0.65 秒,等于三者相加。 单卡就是排队。想看并行,得去多卡或者多机的场景。
- 「日志解析很简单,写个正则就行。」 正则本身简单,难的是「日志格式会变,而你不知道什么时候变」。 这也是为什么现在的框架都直接输出结构化日志,而不是让人去解析。
自测三问(点开对答案)
1. **DIGITS 为什么不自己训练,非要调 Caffe?** —— **因为它的定位就是「壳」。** **训练引擎是重活,已经有 Caffe 做得很好;** **它只做 Caffe 没做的那部分——把训练包成任务、把结果可视化。** **这也是它绑死 Caffe 的原因:壳和引擎的接口是写死的。** 2. **为什么三个任务的总耗时等于三者相加?** —— **因为只有一个计算设备。** **任务队列是串行的:前一个跑完释放了设备,后一个才能开始。** **要并行,要么有多块卡,要么有多台机器。** 3. **「靠正则解析日志」这种做法,风险在哪?** —— **风险在「格式由别人决定」。** **训练进程哪天改一下输出,正则就失效,而且往往不报错,只是没数据。** **稳健的做法是让输出方直接写结构化格式(JSON、数据库),而不是让人去猜文本。**到这里,四个工具讲的都是「怎么把训练组织起来」。 下一节换一个维度:图的画法。Chainer 提出了 define-by-run,让图在每次前向时才现画。
- ☐ (本节完,下一节 7.6 Chainer)