代码老矣,尚能跑否?
之后他黑进了军方网络里,险些引发了一场全球性的热核战争。Rougier的需求就没这么刺激了。他只想从自己的Mac台式机往一台老古董电脑上传一个文本文件——1977年的出品的Apple II。这是苹果公司的第一部消费产品。
Rougier是法国国家信息与自动化研究所(INRIA)的计算神经科学家兼程序员。传这个文件是他自己提出的计算挑战的最后一步:十年代码复现大挑战(Ten Years Reproducibility Challenge, https://rescience.github.io/ten-years/)。2019年,他和法国国家科学研究中心(CNRS)的理论生物物理学家Konrad Hinsen共同发起了这个挑战,要求找一份老代码并重新执行,从而对至少已发表了十年的以计算为主的论文进行复现。原本的计划是参与者在波尔多6月举办的研讨会上讨论心得,但是因为COVID-19被迫延期(目前暂定延期到2021年6月)。
双胞胎计划的插图
虽然计算在科学中起到了越来越关键的作用,但科学文章很少会包含计算用的代码,Rougier说。即使有包含,也很难由其他人执行,甚至连原作者过一段时间后,都可能在执行时遇到问题。编程语言在发展,运行代码的计算机环境也是一样。今天可以顺利运行的代码明天可能就会出问题。
2015年,Rougier和Hinsen创办了《 ReScience C 》。这份期刊刊载的是研究人员如何基于原始论文和自行编写的开源代码来复现其他人的计算方法。评审人再研究代码以确认它是否能用。但即使在这种理想化的场景——作者有意愿让代码复现、评审人纯熟于计算领域、代码也是新写的——整个流程依然有很多难点。
十年代码大挑战的目标是“找出十年前哪些写作和发布代码的技术好到如今依然可用”,Hinsen说。挑战的时间设在了2020年1月1日这个Python 2“退场”的时间点。这个在科学领域非常流行的语言在出现20年之后决定终止支持。(2008年出现的Python 3仍然在继续开发,但是这两者之间的区别比较大,用其中一种写成的代码在另一个环境下可能无法运行。)
“在软件的世界里,十年是很长很长很长的时间。”Victoria Stodden说。她在伊利诺伊大学厄巴纳-香槟分校研究计算的可复现性。作出这一论断后,她说这个挑战本质上是鼓励研究者探索代码复现的极限,能不能在一个“对软件世界来说几乎无穷长的时间”里复现。
一共有35个挑战者。在他们提出要复现的43篇文章里,其中28篇提交了复现报告。《 ReScience C 》从今年初开始刊载他们的工作。使用的程序语言从C和R到Mathematica和Pascal;一位挑战者复现的不是代码,而是用系统生物学标记语言(SBML)编码的分子模型。
虽然是在数码世界,但挑战者的经历和现实世界的考古一样可以借古喻今,提出未来复现代码的最佳策略。其中一个共同点是,科学家想要复现代码就必须优化文档。“2002年,我觉得我所有东西都能记一辈子,”威斯康星大学麦迪逊分校的生物统计学家Karl Broman说,“之后我才意识到,过不了一个月就会忘了。”
重现科研
Rougier的参赛作品重现的是整个挑战里最老的代码[1],他在16岁时为Apple II写的图像放大器,文章发表于一份已经停刊的法国业余爱好者杂志《 Tremplin Micro 》(挑战里最老的科学代码是一份28年前的绘制水质数据的Pascal程序,之后会在《 ReScience C 》上发表)。32年之后,Rougier已经记不清代码是怎么运作的了,它用的还是长得像咒语一样的AppleSoft BASIC代码——“挺奇怪的,毕竟是我本人写的”。但他成功地在网上找到了这份代码,并用网页版本的Apple II模拟器成功运行。这一步比较简单,他说,在实际的Apple II上运行才是真正的困难。
硬件并不是问题——Rougier在办公室有一台Apple II,是他同事清理办公室的时候捡回来的。“年轻人会问‘这是个啥?’”他说,“然后你就得解释‘这是台电脑’。老人看到就会说‘哦,我对这机器有印象’。”但是因为Apple II比USB和互联网还要早——而现代的电脑也没法直接和老式硬盘连接——Rougier就需要一些自制的硬件,外加上一盒老式软盘,才能让电脑读取代码。他在亚马逊上找到了这些东西,1993年造的“全新”品。写入三次保证所有比特都稳定之后,他确认了这些软盘可用。
INRIA研究中心的计算机科学家Bruno Levy评审了Rougier写的内容。Levy也有一台Apple II,还在推特上发布了一小段视频。在老式键盘的一声“咔哒”之后,他调用了代码并成功执行,缓缓显示出一行纯绿色的“我们重现科研!”。
过时的硬件,已死的语言
当法国国家科学研究中心的生物物理化学家Charles Robert听说这一挑战的时候,他决定用这个机会回顾一个他很多年都没再思考的研究课题。“这个挑战给了我临门一脚,让我再向那个方向努力一把。”他说。
1995年,Robert用运行了商业软件Mathematica的计算笔记本为真核染色体的三维结构做了建模。Robert在MacBook上有Mathematica,但是为了好玩,他花了100欧元(约合800人民币)买了一台Raspberry Pi,这是个爱好者玩的单片机,上面安装了Linux系统,并预安装了Mathematica 12。
Robert运行代码时基本没什么问题,但是暴露出了计算笔记本可能会引发的难点[2],例如缺乏代码结构,而代码段也可能不按顺序执行。到了今天,Robert通过将代码拆成模块并写了代码测试。他还使用了版本控制来追踪代码的修改,并记录了哪个版本的软件产生了什么样的结果。“当我读到老代码的时候,偶尔会起鸡皮疙瘩,然后思考现在能如何做得更好,”他说,“不过,我还觉得整个过程让我复习了从那之后学的一些知识。”
成功完成挑战的Robert并非孤例:至今为止发表的13份重现论文中只有2篇失败了。其中之一是Hinsen写的,1990年代初他用来系统性储存代码的磁带让他栽了跟头[3]。“这就是做了备份却没能在十年后检查备份是否能读的下场。”他说,“之前你有这套很好的磁带,还有备份,但是现在没有读取设备了。”(Hinsen还发表了一篇成功完成的文章[4])其他没能成功完成挑战的参与者归因于时间不足,特别是在疫情之下。
挑战者遇到的另一个普遍问题是过时的计算环境。现在在意大利国家研究委员会的大气污染研究所任职的计算物理学家Sabino Maggi曾经使用程序语言Fortran为一种叫做Josephson结的超导设备进行了建模,并用微软的Visual Basic处理了结果。在那之后,Fortran的改变不多,因此Maggi只微调了一些就成功编译了代码。Visual Basic则造成了更大的麻烦。
“Visual Basic,”Maggi在文章[5]里写道,“是一门已死的语言,已经被Visual Basic.NET取代很久了,而两者之间只有名字一样。”为了运行代码,他不得不在Mac笔记本上重构了一个十年前的Windows虚拟机。他用网上找到的安装盘装了微软DOS6.22和Windows3.11(都是1994年前后的软件)以及Visual Basic。“即使是很久以前的软件,使用模拟器安装版权软件仍然可能有合法性的问题。”Maggi承认。不过,因为他当时做科研的时候有合法的证书,他说他觉得“至少道德上有资格”使用。
但是该用哪个版本的Visual Basic?微软在几年内发布了好几个版本的Visual Basic,并且不都是向前兼容的。Maggi已经记不起1996年他用的是哪个版本的了,而地下室的一次漏水摧毁了他早年记录这些细节的笔记本。“我得从头开始了。”他说。
在Mac上运行1994年Windows的模拟器以运行微软的Visual Basic。来源:Sabino Maggi
INRIA的研究工程师Ludovic Courtès重现了一份2006年的研究,内容是比较不同的数据压缩策略,代码是用C语言[6]写的。但是程序员所使用的应用程序接口(API)变了,因此他的程序没办法用现代的软件库编译。“所有的东西都在进化——当然了,只有论文里用到的那段软件除外。”他说。最后,他不得不将五六个程序库回退到了老版本——他称为“降级的连锁反应”。“这坑有点深。”他说。
今天,研究者可以使用Docker容器[7]和Conda虚拟环境[8]对计算环境进行包装以便于重用。但是几个挑战者选择了另一种方式。Courtes说这“很可能代表了重现科研论文的‘黄金标准’”:一个叫做Guix的Linux软件包管理系统。它保证环境可以连每个比特都完全重现,并且对于代码链接时的版本完全透明。“整个环境,事实上整篇论文都可以从源代码开始查看并链接。”他说。Hinsen将它称为“可能是目前为止重现科研最好用的东西。”
需要文档
在INRIA和巴黎大学的计算机科学家Roberto DiCosmo尝试重现[9]的论文中,他提出了另一个挑战者常见的难题:寻找自己到底把代码放哪了。DiCosmo挑战的是1998年的一篇论文,其中描述了一个叫做OcamlP3l的并行程序系统。他搜遍了硬盘和备份,还请1998年的合作者们也搜了一遍,但是什么都没找到。之后他搜了自己2015年建立的一个服务Software Heritage。“找到了,不可思议。”他说。
Software Heritage定期爬Github一类的代码分享站,和Internet Archive备份网页一样备份源代码。开发者也可以要求该服务备份自己的库,而挑战的规则也要求挑战者这样做:DiCosmo并不是一开始就去Software Heritage上搜索的,因为他开发OcamlP3l的时候Software Heritage还没出现。不过,不知道是谁把他的代码发到了一个叫Gitorious的库上。Gitorious现在已经消失了,但在那之前被Software Heritage备份,上面的OcamlP3l也就一起被收录进去。
当然了,找到代码不意味着就知道该怎么用。比如说,Broman的文章里就提到,他在重现2003年一篇论文[10]的时候因为缺乏文档和“古怪的”文件结构而花了很大力气才搞明白到底该运行哪个代码。“结果到头来我得花功夫去读当初的那篇论文。”他写道。
“(在结构良好的程序里)文档比代码长并不是罕见的事情。”在加州大学伯克利分校重点研究计算可重现性的Karthik Ram说,“有了足够详细的文档,再更广泛地描述分析方法,数据来源,数据和代码的元数据,这些都是很关键的。”
爱丁堡大学的神经科学家Melanie Stefan利用这次挑战评估了她用SBML写的计算模型的可重现性。虽然代码很好找,但是她找不到之前使用的参数了(例如分子浓度)。数据归一化时的关键细节也没有详细记录。结果,Stefan无法重现一部分研究。“你做科研时候差不多是显而易见的事不再那么显而易见了——对10-12年之后的你来说。谁能想到!”她自嘲。
可重现性的光谱
Stefan的经历驱使她给实验室订下了文档上的规章——例如,模型中必须附上这样的说明:“想重现图5的话,需要按以下步骤执行。”
但是写这些资源需要时间,Stodden说。清理代码并补充文档,撰写测试,整理数据集,重现计算环境——“这些工作量都不出成果”。研究者没什么动力去做这些事,她补充说,而科学界关于可重现的论文应当长什么样也没什么共识。让问题进一步复杂化的是计算系统还在继续进化,因此难以预测哪种策略能一直有效。
可重现性是一条光谱,曼彻斯特大学的计算机科学家、研究可重现性的Carole Goble说。从科学家复现自己的研究,到同行评审人试运行代码以证明其有效,再到研究者将发表的算法应用在新数据上。类似地,研究者为了保证可重现性所做的事情也能够成一条光谱(见下“可重现性检查表”),但是这张表可能会很长。Goble说,把源代码发布出去,这样至少未来其他人可以浏览并按需改写——Goble管它叫“读代码的重现手段”。“软件是有生命的,”她说,“而有生命的东西终将腐朽,因此需要不断修理,最终就得换掉。”
可重现性检查表
虽然以下手段不可能百分之百保证计算可重现性,但是可以增大成功率。
代码- 如果你的计算过程是在图形界面上点来点去,例如Excel,是不可重现的。将你的计算和数据操作写成代码。
文档- 使用注释、计算笔记本和README文件来解释程序的运作方式,并将预期的参数和所需的计算环境也定义好。
记录- 记录关键参数,例如随机数生成器的种子。这类记录可以用来重现代码,发现漏洞并追踪意料之外的结果。
测试- 写一套测试函数。使用正向和负向的控制组数据集来确保你能获得预期的结果,并在开发过程中不断运行这些测试以便在编程出错时立刻发现。
指南- 写一个主脚本(例如run.sh文件)来下载所需要的数据集和变量,执行计算流程并为你的代码提供一个显而易见的入口。
存档- GitHub是一个流行但是非永久性的在线代码库。使用Zenodo、Figshare和Software Heritage这样的存档服务来保证长期的稳定性。
追踪- 使用Git一类的版本控制工具记录项目历史。记录产生各种结果的分别是哪个版本。
打包- 使用容器化的工具(例如Docker和Singularity)、网上服务(Code Ocean、Gigantum、Binder)或是虚拟环境管理器(Conda)设置可以即时使用的计算环境。
自动化- 使用持续集成服务(例如Travis CI)来自动、定期、在各种计算环境下测试代码。
简化- 避免罕见或难以安装的第三方代码库,以简化重用代码的难度。
验证- 在不同的计算环境下运行你的代码,以确认其可移植性。
一个不怎么符合直觉的事实是,很多挑战者都发现使用更老的语言写成的代码反而更易于复用。新语言的应用程序接口会频繁更新,而它们所依赖的第三方库则导致代码更易损坏。从这个意义上讲,今年初Python 2.7的退役为科学家提供了一个机会,Rougier和Hinsen说。Python 2.7“让我们有了一个保证不会再变化的高级编程语言。”Rougier写道[1]。
无论研究者使用什么样的编程语言和可复现策略,实际验证一遍都是明智之举,谢菲尔德大学的研究软件工程师Anna Krystalli说。Krystalli负责举办一个叫ReproHacks的研讨会,让研究者提交已经发表的论文、代码和数据,然后要求其他参与者重现其结果。她说,大多数情况下是重现不出来的:作者没能提供一些他们看起来显而易见而其他人却不知道的关键细节。“无论我们在做什么,如果不实际用一用,摆弄摆弄的话就不可能知道是否真的可以重现。”Krystalli说,“实际上,这比人们所想象的要难得多。”
- Python实战:将头像转成动漫风!
- 编译代码时动态地链接库
- 聊聊 Ahooks 是怎么解决用户多次提交问题?
- 机器学习:使用 Python 进行预测
- 开始使用Dark构建Serverless后端
- Spring Boot 使用 Disruptor 做内部高性能消息队列
- 基于互联网架构演进, 构建秒杀系统
- Vue3 中 watch 与 watchEffect 有什么区别?
- 如何在 Kubernetes Pod 中注入环境变量及优先级问题
- 一文详解Python中__new__方法的作用
- 如何调用一个只支持batch_call的服务?
- Spring框架之基于Java的容器注解
- iOS 16 新功能总整理,十个你一定要知道的功能
- 两种方式!带你快速实现前端截图
- 云存储架构中企业级数据流转平台技术方案
- Ahooks 是怎么解决 React 的闭包问题的?
- Spring框架之Spring AOP Logging教程
- 使用 Locust 进行 Kubernetes 分布式性能测试
- 有些时候,你也许不需要使用UI框架
- Node.js 17 达到 EOL:这意味着什么?