你是如何从纸上变成代码的?

机器算法验证 r 软件
2022-03-18 02:59:51

我的背景是计算生物学,尽管我是一名受过训练的生物学家。我最近刚开始攻读博士学位,虽然我对计算生物统计学/流行病学和方法开发有浓厚的兴趣/好奇,但我相当确定我不必开发和实施新方法。但正如我所说,我对此很好奇。

更准确地说,我仍然不明白有人如何从研究论文变成一个R包。我觉得这很困难,不仅因为我可能对它没有正确的理论理解,还因为我发现编程统计方法特别困难。我知道如何编写代码,我编写了相当多的代码,但我从来不需要从介绍它的论文开始实现一个方法。

举个例子,我最近看到了这篇论文,显然有人在R 这里实现了它(文件名为focus_1.0.0.tar.gz)。我真的不知道从哪里开始。你需要什么样的心智模型才能将这样的框架翻译成代码?

3个回答

对我来说,我一直在学习数学直到我理解它(并且可以确定使线性代数高效和稳定的方法)。这通常涉及以尽可能小的步骤(作为 LaTeX 文档)再次写出数学。这通常用作维护文档 - 如果您在一年后回到该软件,您可能不会确切记得它是如何工作的,并且一步一步的数学推导(带有解释您的问题的注释)是快速记住它的方法。通常,编程的主要困难不是编码(例如,弄清楚如何告诉计算机做某事),而是解决问题/理解问题(准确地弄清楚你想让计算机做什么)。

下一步是确定您可以单独测试的算法/方法的组件。不要试图一口气实现整个事情。我非常喜欢海因莱因的名言“当遇到一个你不理解的问题时,做你理解的任何部分,然后再看一遍”。

阅读其他人的代码并找出他们以自己的方式实现它的原因也是一个好主意,并在其他人使用的包的情况下查看代码的结构。文档也很重要,如果你想让人们使用你的代码,你需要让它尽可能简单,所以尽量减少依赖(例如对第三方包)使用户界面或 API 简单且符合预期,并提供良好的文档。这需要我们大多数人(包括我)似乎无法节省的大量时间/精力;o)

更准确地说,我仍然不明白有人如何从研究论文转到例如 R 包。

就像生活中的很多事情一样,有时最困难的部分才刚刚开始。很多人为失败做好了准备,只是,好吧,认为他们没有做好足够的准备并且会失败,所以他们甚至在尝试之前就已经放弃了。

话虽如此,你必须至少知道一点才能开始。在基于论文编写 R 包的上下文中,您不一定需要熟悉主题背后的数学或理论才能开始。您可能会发现编写包的过程实际上有助于加深您最终想要拥有的理解,并且随着时间的推移,您将对反映这一点的包进行更改。实际上,我有一个非常健壮、用户友好的 R 包,我就是在这种情况下开始的。我有很多编程经验,我认识的一个人有他一直在开发的这个不平凡的理论,但没有能力在一个包中正确实现,所以他让我帮忙。数学和理论的细节对我来说大多是陌生的,因为我的专长在其他地方。我没有' 那时甚至不知道创建 R 包的细节。但是现在,在完成了创建包的过程之后,我什至能够对理论及其应用做出自己的贡献和进步。

首先,您需要做的是考虑用户可能想要的包中的内容。这篇论文提出了哪些潜在用户会发现有用的信息?这确实需要至少对论文完成的内容有一个高层次的理解;大概,如果您不知道论文的内容,您就不会尝试为论文编写包。因此,列出用户想要的东西。现在将这些项目分配给函数名称,而不用担心这些函数内部如何工作的细节。例如,您可能有一个执行核心统计分析的函数,一个计算置信区间的函数,一个计算方差的函数,还有一个执行基本可视化的函数。

接下来,这些功能需要哪些最基本的输入?可能有很多小东西对功能的便利性、调整或灵活性有用,但这些还不重要。重要的是让基本功能运行起来;您可以稍后返回并处理更精细的细节,以免它们现在让您不知所措。

现在,这些函数返回的值是什么?换句话说,用户期望或发现在结果方面最有用的是什么?其中一部分可能最终会被前面关于输入的观点塑造或塑造。如果你有一个函数建立在另一个函数的结果之上,那么一个函数的输入将成为另一个函数的输出。因此,根据具体情况,您可能有一个函数返回简单的东西,如数字或向量,而您可能有另一个函数返回复杂的东西,如列表或类对象。

一旦你有了一个你想要的函数列表,它们需要什么输入,以及它们产生什么,你现在就拥有了一个基本骨架包所需的东西。因此,通过创建一堆空函数开始编码。或者也许只是让他们打印一些东西。如果您以前从未编写过软件包,那么现在是尝试构建和安装它(在您的计算机上本地)以确保它工作的好时机。在您工作时,定期构建、安装、加载和测试您的包,以确保一切都按照您的想法进行。

最后,通过骨架包设置,您现在可以开始深入研究数学/理论细节。你的函数可以作为指导来帮助你分解这个任务,这样你就不会试图通过一次专注于所有事情来压倒自己。只专注于一件事,并努力让函数的基础知识运行起来。此时您绝对不应该担心性能。其他事情也一样,比如带有输入数据的奇怪的极端情况。您应该专注于以尽可能简单的方式实现事物。一旦一切都在运行并且您对一切都有更好的感觉,那么您就可以重新访问。

最终,您将更深入地了解您正在尝试使用您的包完成什么。您可能会发现自己意识到您需要一些以前没有想到的功能,或者可能需要组合您的几个功能。你甚至可能回过头来意识到有一种完全不同的做事方式更好。对于我的包,v0.1.0(本质上是一个测试版)和 v1.0.0(第一个生产版本)是不同的。我从头开始完全重写了 v1.0.0 的包。v0.1.0 非常适合学习,不仅是数学和理论,还有我的框架决策作为用户和包维护者的感受。事实证明,这对双方来说都是笨重且令人不快的,并且值得投资进行全面检修。所以不要担心马上让它变得完美;花点时间在预发布阶段进行尝试,最好让一些潜在用户有机会尝试并提供反馈。我无法告诉你其他人的真实世界数据多久会暴露你从未考虑过的奇怪的极端情况......

一些额外的提示:

  • 技术债务:有时会有多种方式来完成一项任务。在发布您的包之前,您需要提前考虑这些选择的含义,因为如果您只是采取简单的方法,您可能会发现以后要花费大量时间来解决问题。最好在发布之前处理好这一点,因为您希望避免在用户开始使用包后破坏他们的东西。有大量关于“技术债务”的文章。花一些时间阅读其中的一些。
  • 维护负担:如果你打算创建一个 R 包,请致力于维护它。我在科学软件中看到的一个大问题是,许多人将软件/软件包作为出版物的一次性任务发布,然后让它腐烂,当软件出现问题时,这最终会花费其他人大量的时间和精力为他们。希望你不是其中之一。为此,您必须了解您在设计包装时的选择将如何影响您以后的时间承诺。您添加的每一件小事都是一件可能会破坏的事情,需要测试,需要对用户的支持等,从而导致花费更多时间来维护您的包,而不是您想做的其他事情。因此,请谨慎对待添加的内容和从功能列表中删除的内容;如果您尝试添加人们想要的每一个方便的小东西,那么您以后会后悔的。考虑你的包需要哪些外部包也是如此;您添加的每个依赖项都是另一个人在他们的包中进行的更改以破坏您的包的机会。
  • 要发布 R 包,请将其放在 CRAN 上。我无法告诉你有多少次我查看了非 CRAN 包的 GitHub 存储库(带有经过同行评审的出版物),只是为了发现 CRAN 的“烦人”政策可以防止的基本缺陷。说真的,我查看的一个包改变了许多其他常见包隐式依赖的多个默认 R 行为。只需加载包,它就可以完全改变其他包的行为,这种方式完全静默,结果变化不明显,并且只能通过重新启动 R 轻松修复(并且永远不会再次加载包)。CRAN 包中明确禁止此包用于执行此操作的命令。如果作者试图发布到 CRAN,他们会抓住它(或者他们确实尝试过并决定它不是
  • 使用自动化测试(对于 R,请参阅“testthat”包)。这确实应该是任何软件包的要求。不幸的是,仅仅因为一个包有单元测试并不意味着它做得很好。弄清楚如何做好它可能是一种艺术形式。
  • 良好的文档是必要的。而且我不仅仅指典型的 R 函数引用。我希望大多数人都能从各种解释概念和举例的教程/小插曲中受益。尝试写这些实际上将有助于您自己对数学和理论的理解,或者至少让您对所学内容充满信心。对于 R,我建议为此查看“pkgdown”包;它可以帮助您为您的包构建一个网站,该网站作为包的 GitHub 存储库的一部分托管。

一些告别的话:如果您要为科学家编写软件,请记住责任的重要性。如果你真的全身心投入并且做对了,你可以对许多人更有效地进行研究的能力产生巨大的影响。但是,如果你半途而废或懒于维护它,你可能会搞砸很多人,并花费他们大量的时间、精力和潜在的金钱。无论哪种方式,你都有可能对科学产生巨大的影响,而你通常只呆在你自己的小研究泡沫中就可能不会产生这种影响。

为了补充Dikran Marsupial 的回答,以下是我个人使用的过程的阐述。这是从编码机器学习算法进行研究的角度来看,而不是从成熟的生产级软件工程的角度来看。

首先,我经常发现将机器学习算法从头开始编码的过程看作是对数学思想的实例化很有用:

数学,预算法阶段。

通常在机器学习中,您使用某种数学或形式原理来指导算法的开发。它可以是统计的,例如最大似然估计,并且很可能涉及一定程度的优化。通常,你需要做大量的数学推导工作,直到你得到你需要的东西。

就个人而言,我发现一个人既需要对这里运行的数学原理有扎实的底层掌握,也需要绝对安全地保证推导的正确性。在这个阶段的任何模糊或不确定性都会在实施过程中加剧,此时人们还必须担心调试。在进行下一个阶段之前,我会经常使用符号计算包或计算机代数系统,例如Mathematica来检查我所有的推导,因为在这个阶段即使是一个错误也会导致不得不搜索的极端不良情况推导和/或调试错误。

算法。

在我检查了我的数学推导是无懈可击的,没有错误,并且我处于可以在纸上指定和勾勒出我的算法的水平之后,我开始起草正式的伪代码。与上面的海报类似,我发现强迫自己algorithm2e在 LaTeX 中使用它会有所帮助,因为它会让你以仅使用笔和纸的方式来工作。

在这个伪代码起草阶段,我开始考虑更多的实现问题,例如使我的推导尽可能矢量化,即使用尽可能少的循环,或者尽可能使用线性代数技术。然而,实现上的关注点仅扩展到遵守算法设计的基本建议和标准原则,而不是优化代码。

执行。

在代码中实现伪代码。你的伪代码越清晰,你的符号越一致,这就越容易。正如上面的海报所说,您可能会为自己选择的是它带来了巨大的红利,以确保您正在编写模块化代码将编写的代码分成小块,您可以单独对其进行测试。

在大多数情况下,您几乎总是可以进行非常基本的健全性检查。例如,如果您的代码使用梯度,那么您可以使用有限差分来评估这是否已正确计算。如果您正在实施 EM 算法,您的对数似然将单调增加,因此任何减少都很可能是编码错误,前提是您的推导是正确的。

优化。

关于这个我不想多说,只是它可以很快深入。在这个阶段,你可以为你的代码计时,或者使用大量的数值线性代数、矩阵计算和凸优化技巧来尽可能多地提高性能。


关于调试工具的使用,我对 R 中的调试工具了解不多,因为这是我刚刚掌握的东西。但是,看起来 RStudio 调试器类似于pdbPython 中的调试器。

就 Python 而言,PyCharm IDE 是一个示例,我发现这些工具可以极大地改进调试过程。您可以在代码运行时遍历代码的每一行,并逐步进入、退出、并排评估,以及一次跟踪所有变量状态。我个人发现一次访问所有变量状态的可视化方法远远优于使用pdb. 虽然我希望我早点知道这一点,但我强调这只是为了考虑,我与 JetBrains 没有任何关系。

一些最后的事情。就像生活中的大多数事情一样,你会通过练习变得更好。在paperwithcode.com上有很多论文附有代码还有一个机器学习论文的可重复性挑战

最后,如果你发现调试已经开始磨砺你的灵魂,我发现想想人们过去使用打孔卡来实现算法的日子会有所帮助......