AI博士高效实验的关键技术与实践

本文探讨人工智能博士研究中高效实验的重要性,涵盖实验设计、版本控制、可复现性、日志监控和自动化等关键技术,提供具体代码示例和实践经验,帮助研究者优化实验流程。

有效实验在AI博士研究中的重要性

工程化和运行实验是大多数人工智能博士研究的核心组成部分。尽管存在许多更理论化的课题通常仅限于小规模实验,但扩大模型、数据集和实验规模已成为明确趋势。因此,成为高效的工程师并以高效方式设置实验可能是博士期间取得成功的关键因素。

在人工智能领域工作,尤其是进行实证博士研究时,有效实验所需的工程化极其重要。当大部分研究转向使用大型基础模型时,这一点日益明显。即使在约7年前开始博士研究时,我也很快意识到有效运行实验至关重要。这是因为有效实验意味着可以快速检验研究假设,并为后续假设提供洞察。例如,我在TPAMI上关于比特错误鲁棒性的论文包含了数千个训练和评估的模型,以检验各种正交假设。

不幸的是,用于管理实验的基础设施和工具很少与研究代码一起开源。这可能是因为这些方面通常受到较少关注,或者极度依赖于硬件、操作系统等具体环境。即使是我开源的代码通常也只包含基本内容,丢弃了许多日常用于运行实验的工具。不过,你仍然可以零星找到一些个别成果,例如我的置信校准对抗训练工作中的日志工具或设置测试,或者我在3D形状补全工作中使用的JSON配置文件。

在本文中,我想分享一些在博士期间运行机器学习模型大规模实验时学到的更通用的经验。具体来说,我确定了以下重要方面:

  • 运行正确的实验
  • 确保实验可复现
  • 通过日志和监控进行分析
  • 自动化一切

运行正确的实验

这是我们每天在研究中解决的关键问题,我觉得这在很大程度上依赖于直觉。然而,我学到了一些技巧使其更容易。对我而言,实验想法通常来自阅读论文或与同事和导师讨论问题。一旦想法更具体,我会尝试写下假设和高级实验设计。有时,实验设置需要在如何精确实施、测量什么以及如何总结结果方面进行迭代。然后,一旦设置最终确定并实施,我会运行实验并确保保存评估结果/分析——这可能是一些数字或图表,或者只是一个包含一些分析的Jupyter笔记本。

理想情况下,每个实验都应回答一个相当具体的问题。尤其是在博士初期,基于先前已发表的工作从增量式研究问题开始可能很有用。当然,这有些理想化。会有许多实验主要用于测试代码或因各种无关原因失败。有时一个问题还涉及一系列实验,例如超参数调优。然而,我发现反复进行明确写下特定假设然后用一系列实验评估的练习,有助于培养直觉和为后续实验提出良好研究问题的能力。这也确保每个实验都有其目的。

可复现的实验

在开始实验之前,确保它们可复现很重要。可复现性是学术研究的关键要素,因为它允许复制实验并最终就所处理的研究假设达成共识。在人工智能中,可复现性更为重要,因为我们所做的许多研究本质上是建设性的——提出新算法或训练新模型。根据我的经验,实现可复现性的三个最重要方面是版本控制、控制随机性和管理编码环境。针对机器学习实验,我还学到拥有明确的配置文件很重要。尽管机器学习模型难以测试,但我相信至少有一些测试可以显著提高可复现性。

版本控制听起来直接,应该是默认做法。然而,这主要涉及代码的版本控制,并不一定指导我们如何处理实验。我学到将每个实验与一次提交关联是使用版本控制的理想结果。在实践中,我通过将代码检出两次来实现:一次用于主动开发,一次用于运行实验。后者甚至不在IDE中打开,因此无法进行更改;它仅用于运行实验。在许多版本控制设置中,这也可以通过使用单独的分支来完成:一个“开发”分支和一个“实验”分支。然后,我有一个启动脚本,确保“开发”分支中的所有更改作为启动实验的一部分提交。这些更改然后在“实验”分支中检出以运行实际实验。通过一些额外的日志记录,这看起来如下:

 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def yes_or_no():
    answer = input('Commit? (y/n): ').lower().strip()
    while not (answer == 'y' or answer == 'yes' or answer == 'n' or answer == 'no'):
        answer = input('Commit? (y/n): ').lower().strip()
        if answer[0] == 'y':
            return True
        else:
            return False

# This is run from the development checkout of the code, and WDIR
# holds the directory from which experiments are run:
WDIR = '...'
if os.path.normpath(WDIR) != os.path.normpath(os.getcwd()):
    if yes_or_no():
        os.system('git commit ..')
        os.system('git push origin master')
    else:
        exit()

# Name of the experiment.
name = '...'
# All experiments were started on a contact server as tmux sessions so I can
# check on their progress and debug if I want to.
response = os.system('tmux has-session -t=%s' % vname)
exists = (response != 256)
if exists:
    log('Name taken!', LogLevel.WARNING)
    log('Continue to kill session before starting the experiment:')
    input('press any key')

    files = self.args.file.split(',')
    if len(files) > 1:
        log('[Error] multiple commands not supported on Slurm')
        exit();

# The actual server to run the experiment on, script with arguments to run.
server = '...'  # This could be a Slurm submit server.
script = '...'
arguments = '...'
script_cmd = 'python3 %s %s' % (script, arguments)
# There could be an optional step to set up some Slurm launch file or any
# other file needed for a specific cluster.

commands = [
    'tmux kill-session -t %s' % name,
    'tmux new-session -d -s %s' % name,
    # ssh into the server where the experiment is actually run.
    'tmux send-keys -t %s "ssh %s" ENTER' % (name, server),
    'tmux send-keys -t %s "cd %s" ENTER' % (name, WDIR),
    # Some optional bash profile to set the right CUDA env.
    'tmux send-keys -t %s "source ~/.bashrc_cuda10" ENTER' % name,
    # Git pull the commit we did above.
    'tmux send-keys -t %s "git pull" ENTER' % name,
    # Run the experiment; this will also log progress, experiment configuration etc.
    'tmux send-keys -t %s "%s" ENTER' % (name, script_cmd),
]

for command in commands:
    common.experiments.Monitor.get_instance().log(command)
    os.system(command)
    log(command)

版本控制的另一个重要方面是什么包含在版本控制中,什么不包含。在经典软件开发中,配置文件和数据通常单独处理,但我认为包含所有带有超参数的配置文件很重要。这也包括所有随机种子、数据文件路径,甚至可能包括一些数据文件,例如已调整的数据归一化值。

随机性是所有机器学习的关键要素。它决定模型初始化、数据分割、我们看到训练示例的顺序、许多组件如dropout或优化过程中的任何随机噪声等。因此,随机性通常来自各种不同的库和函数。例如,处理TensorFlow、PyTorch和NumPy随机种子相当常见。个人认为,Jax目前在使函数调用中的随机性明确方面做得最好。为了可复现性,关键是要意识到所有随机性来源并无情地控制它们;这不仅包括训练,还包括评估和测试。我通常为所有随机性来源设置明确的种子,可以在配置文件中控制。

环境如今相当容易控制,但在必须为许多机器学习项目使用各种类型GPU的情况下,仍然具有挑战性。有各种工具如conda可以完全控制所有使用的软件版本,我建议 heavily 依赖此类工具。这很重要,因为环境可能因实验而异,尤其是在使用开源基线时。对于conda,我发现每当更新包时更新/导出/提交environment.yml很有用。

实验配置包括所有超参数、种子、模型配置、评估和数据集配置。如上所述,它应是版本控制的一部分。然而,也值得提一下,在明确配置文件中收集所有这些参数和任何额外的魔数实际上很有用。本质上,每个实验应完全由实验配置定义。然后,给定存储库中的提交和实验配置(包括随机种子),每个实验应完全可复现。多年来,将这些超参数存储在JSON文件中或作为Python字典已成为常见做法。另一种选择是某机构的ml_collections。我还学到,实际上将这些配置文件与实验输出一起转储可能对调试有用。

最后,关于测试的简短说明:这是研究,大多数博士学生不愿添加精细的测试。实验失败是非常常见的现象,也是研究的一部分。然而,每个人都知道周五晚上开始实验并在周一发现它失败是多么烦人,因为它找不到数据、包更新搞乱了导入、无法在具有正确CUDA版本的GPU上运行,或者某些位置无法按预期写入/读取等。因此,我开始有一个测试套件,检查(a)所有必要导入,(b)所有数据源,(c)可读/可写位置,(d)GPU上的基本操作等。这个setup.py是早期版本。这应该足够轻量,可以在每个实验之前运行。

通过日志和监控进行分析

在正确启动实验后,日志和监控对于调试和分析很重要。日志记录的目标只是输出有用信息以帮助追踪错误。相反,监控侧重于分析。例如,这包括在整个训练或评估过程中跟踪关键量,这些量后来用于实际评估实验和相应的研究假设。日志记录和监控在设置上都有些繁琐,因为它们通常需要在模型、训练周期、数据集等之间传递奇怪的信息。

在博士初期,我自己写了一个自定义日志工具,但我确信现在有更好的替代方案。本质上,我学到总是将关键实验信息记录到(非临时)文件很有用。记录器通常包括时间戳和记录的文件。具体来说,我发现以下内容对日志记录非常有用:

  • 实验环境(主机/机器、GPU信息、任何调度/取消调度事件、Python/包版本)
  • 实验配置(网络架构、所有超参数等)
  • 数据加载(示例数量、读取的文件、图像或输入大小、初始化后的网络大小等)
  • 损失,包括所有组件如正则化项
  • 检查点、写入的模型文件、写入的数据文件、生成的图表等

除了基本日志记录,监控实际上保存与要处理的研究假设相关的中间状态或实验结果。通常这包括训练的模型或评估结果如指标或原始预测。但也可能包括中间模型激活、整个训练过程中对测试集的预测、一些训练或测试输入以检查数据增强方案。许多这些可以与TensorBoard等工具结合使用,使监控具有交互性。本质上,我希望访问所有信息以运行计划的分析。

自动化一切

上述许多内容隐含地涉及自动化——自动化版本控制、调度、日志记录、监控等。从根本上说,自动化需要所有这些组件,否则你的时间成为瓶颈。然后,你可以运行的实验数量受限于你的带宽,并且不允许并行化实验和其他工作项,如论文写作、讨论、编程等。然而,自动化可以更进一步。我通常还尝试在很大程度上自动化绘图和分析。例如,评估可以在训练后自动运行,分析可以是一个Jupyter笔记本,自动运行并随后保存为PDF或HTML。这允许运行更大的实验集,特别是在为顶级论文提交准备消融研究或附录时。

结论

总体而言,有效运行实验对于人工智能的实证博士研究很重要。对我而言,运行正确的实验、使实验可复现、适当的日志记录和监控以及自动化是实现有效实验的关键方面。所有这些将使检验研究假设更容易,允许运行更多实验,并将你的时间从“照看”实验中解放出来用于其他工作项。现在在某机构的DeepMind,我也学到这些方面是使许多项目 incredibly 成功的原因。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计