背景
GitHub Copilot 是 GitHub 和 OpenAI 发布的一项新服务,介绍说是“你的 AI 结对程序员”。它是 Visual Studio Code 的一个插件,可根据当前文件的内容和当前光标位置为你自动生成代码。
它用起来感觉真的很神奇。比如说,这里我输入了一个函数的名称和文档字符串,该函数应该“Write text to file fname”:
上图里函数的灰色主体完全是 Copilot 为我编写的!我按一下键盘上的 Tab 就接受了建议,并插入到了我的代码中。
这当然不是第一个“人工智能驱动”的程序合成工具。2018 年,GitHub 的自然语言语义代码搜索演示了使用简单的英语描述来查找代码示例的方案。Tabnine提供“AI 驱动”的代码自动完成功能已经有几年了。而 Copilot 与它们的不同之处在于,它可以根据代码文件的完整上下文生成完整的多行函数,甚至生成文档和测试。
对我们 fast.ai 来说它特别令人兴奋,因为它承诺可以降低编程障碍,这将给我们带来很大帮助,助力我们完成使命。因此我对它特别感兴趣,并深入研究了 Copilot。然而,正如我们将要看到的那样,我还不能说 Copilot 真的是一种福音。它甚至可能变成一个诅咒。
Copilot 由名为 Codex 的深度神经网络语言模型提供支持,该模型在 GitHub 上的公共代码存储库上进行了训练。这一点让我特别感兴趣,因为早在 2017 年,我第一个证明了通用语言模型可以通过微调在多种 NLP 问题上获得最一流的结果。我开发了这种方法并把它放在了一门 fast.ai课程中。Sebastian Ruder 和我随后充实了这一方法并撰写了一篇论文,该论文由计算语言学协会(ACL)于 2018 年发表。OpenAI 的 AlecRadford 告诉我,这篇论文给他创建 GPT 带来了启发,Codex 就是以 GPT 为基础的。下面是那门课程中我第一次展示语言模型微调可以在分类 IMDB 评论的情绪偏向时获得一流的结果:
语言模型经过训练可以用来猜测一段文本中缺失的单词。前些年使用的传统“ngram”方法在这方面表现不佳,因为正确的猜测需要上下文。例如,请考虑如何填写以下两个示例中缺失的单词:
想要知道前一题的答案应该是“热天”,后一题应该是“热狗”,需要阅读并(在某种程度上)理解整个句子。Codex 语言模型会学着猜测编程代码中缺少的符号,因此它必须学习很多关于计算机代码的结构和含义的知识。正如我们稍后将讨论的那样,语言模型确实存在一些重大的局限性,这从根本上是由于它们的创建方式导致的。
Copilot 接受过很多基于各种许可的公开代码的训练,这一事实引发了许多关于道德和法律影响的讨论。由于关于这一点的讨论已经很多了,因此我不会在本文中过多着墨,只是提一下知识产权律师 Kate Downing 讨论的一个关于 Copilot 用户的明确法律问题:在某些情况下使用 Copilot 提供的建议可能会违反许可证(或要求基于 GPL 兼容许可证重新授权你自己的作品):
“建议越复杂和冗长,它就越有可能具有某种受版权保护的表达方式。”
演练
在更深入地研究 Copilot 之前,我们先来看一些它在实践中应用的例子。
为了知道自动生成的 write_text 函数是否真的有效,我们需要做一个测试。干脆这个测试也让 Copilot 来写吧!在这个例子中,我只是输入了测试函数的名称,Copilot 就为我填写了文档字符串:
我接受了这个建议后,Copilot 有点迷糊,建议了一个包含多行几乎是重复代码的无意义函数:
没问题——只要按下 Ctrl-Enter,Copilot 就可以向我们显示其他建议选项。列出的第一个选项的确看起来很合理(除了第一行中一个奇怪的额外制表符):
这里 Copilot 假设有一个名为 read_text 的函数可用,但它实际上并不存在。不过我们也很容易让 Copilot 为我们编写和测试这个函数。
我还让 Copilot 为我创建了一个函数,可以“将 dir 目录做 Tar 打包到 dest,可选以 bz2、xz 或 gzip 格式压缩”,结果是:
我还让 Copilot 使用与上文相同的基本方法创建了一个测试,它写道:
这个测试实际上并没有通过,因为最后一行中的 getnames 包含父目录,但这里是很容易修复的。Copilot 甚至巧妙地决定使用我之前创建的 write_text 函数,这是我没想到的。
你甚至可以使用 Copilot 写散文。我现在正在用 vscode 写这篇博文,并点击了“启用 Copilot”按钮。在我输入上一句后,以下是 Copilot 推荐的完成内容:
“I can now write my blog post in a single line of text, and Copilot will generate the rest of the post for me”
(我现在写博客文章时只要写一行就行了,Copilot 将为我写完整篇文章”
显然,Copilot 对自己的散文生成能力有相当夸张的认识!
代码问题
Copilot 编写的代码并不是很好的代码。例如,考虑上面的 tar_dir 函数,其中有很多重复的代码,这意味着我们将来需要维护更多代码,阅读代码时需要理解的东西也更多了。另外,文档字符串说的是“可选压缩”,但生成的代码总是会做压缩。我们可以换一种写法来解决这些问题:
更大的一个问题是 write_text 和 tar_dir 根本不应该被写进来,因为 Python 的标准库已经提供了两者的功能(如 pathlib 的 write_text 和 shutil 的 make_archive)。标准库版本也更好用,pathlib 的 write_text 会做额外的错误检查并支持文本编码和错误处理,而 make_archive 支持 zip 文件和你注册的其他任何存档格式。
为什么 Copilot 会写出糟糕的代码
根据 OpenAI 的论文,Codex 只有 29%的时间会给出正确答案。而且正如我们所看到的,它编写的代码往往重构得很差,无法充分利用现有的解决方案(即使这些方案就在 Python 的标准库中)。
Copilot 读取了 GitHub 的整个公共代码档案,其中包含数千万个存储库,有着来自许多世界上最优秀程序员的代码。鉴于此,为什么 Copilot 会编写出如此蹩脚的代码呢?
原因在于语言模型的工作机制。它们反映的是大多数人的平均写作水平。它们不知道什么是正确的,什么是好的写法。GitHub 上的大多数代码(根据软件标准)相当陈旧,并且(根据定义)是由水平一般的程序员编写的。Copilot 尽力猜测的是,如果这些程序员正在编写的是你面对的这些文件,他们可能会写什么代码。OpenAI 在他们的 Codex 论文中讨论了这一点:
“与其他训练目标是预测下一个词符的大型语言模型一样,Codex 会生成与其训练分布尽可能相似的代码。这样做的一个后果是,这种模型可能会做一些对用户无益的事情”
Copilot 之所以比那些水平一般的程序员更糟糕的关键一点在于,它甚至没有尝试编译代码或检查代码是否有效,也没有考虑过自己是否真的遵循了文档的指示。此外,Codex 没有接受过去一两年内创建代码的训练,因此它完全没学过最新版本、库和语言特性。例如,提示它创建 fastai 代码后,它只会给出使用 v1 API 的建议,而不是大约一年前发布的 v2 版本。
抱怨 Copilot 编写的代码质量有点像遇到一只会说话的狗,然后抱怨它的措辞太过生硬一样。其实它在说话的这个事实就太令人印象深刻了!
让我们明确一点:Copilot(和 Codex)能编写出看起来还算合理的代码就是一项了不起的成就。从机器学习和语言合成研究的角度来看,这是向前迈出的一大步。
但我们也需要清楚,看起来合理的代码如果不起作用、不检查边缘情况,还使用了过时的方法,代码又冗长晦涩并会带来技术债务,就可能是一个大问题。
自动生成代码的问题
代码创建工具的历史几乎与代码存在的时间一样长。它们从诞生之日起就一直面临着许多争议。
在编程工作中,大多数时间不是用来编写代码,而是用于设计、调试和维护代码。当代码可以自动生成时,到最后我们很容易生成更多代码。这不一定是一个问题,如果你需要做的只是维护或调试,那么只要调整自动生成代码的源头即可,例如使用代码模板工具时就可以这样做。即便如此,在调试代码时事情也会变得混乱,因为调试器和堆栈跟踪通常会指向具体的生成代码,而不是模板化的源代码。
有了 Copilot,我们就没有这些好处了。我们基本上每次都要修改创建的代码,如果我们想改变它的工作方式,我们不能只回去改变提示。我们必须直接调试生成的代码。
根据经验,更少的代码意味着更少的维护和理解负担。Copilot 的代码很冗长,而且很容易生成大量啰嗦的代码,于是到最后你很可能会面对一大堆代码!
Python 具有丰富的动态和元编程特性,可大大减少对代码生成的需求。我听过很多程序员说他们喜欢 Copilot 为他们编写的很多样板。然而,我几乎从不写任何样板——过去每当我发现自己需要样板的时候,我会使用动态 Python 来重构样板,这样我就用不着再编写或生成它了。例如,在ghapi中,我使用动态 Python 在一个仅 40kB 的包中创建了 GitHub 整个 API 的一个完整接口(相比之下,Go 中的等效包包含超过 100,000 行代码,其中大部分是自动生成的).
一个非常有启发性的例子是,我给 Copilot 提示:
("""使用文件夹中的图像微调 pytorch 模型并报告验证集的结果""")
只是打了三两行代码后,它就几乎完全自动生成了这89行代码!从某种意义上说这确实令人印象深刻。它确实基本上完成了要求——微调一个 PyTorch 模型。
但是,它对模型的微调结果是很差的。这个模型训练速度慢、准确率低下。正确微调模型需要考虑诸如处理 batchnorm 层统计数据、在主体之前微调模型的头部、正确选择学习率、安排适当的退火计划等因素。此外,我们可能希望在过去几年内发布的 CUDA GPU 上使用混合精度训练模型,并且可能希望添加一些更好的增强方法,例如 MixUp。在修复过程中添加这些内容将需要数百行代码,以及深度学习方面的大量专业知识,或者使用更高级的 API(例如fastai,它可以只用 4 行代码就微调一个 PyTorch 模型,带来精度更高、速度更快,并且更具可扩展性的结果。)
在这种情况下,我不确定 Copilot 应该怎么做才是最好的。我不认为它现在所做的东西在实践中真的能有用,尽管它给出了一个令人印象深刻的演示。
使用正则表达式解析 Python
我在 fast.ai 社区问了问,想知道大家写代码的过程中 Copilot 什么时候的确能帮上忙。有人告诉我,当他们编写一个正则表达式,从包含 Python 代码的一个字符串中提取注释时 Copilot 特别有用(因为他们想将函数中的每个参数名称映射到注释上)。我决定自己试试这个。这是给 Copilot 的提示:
这是生成的代码:
这段代码是用不了的,因为^字符错误地将匹配项绑定到了行首。它实际上也没有捕获注释,因为它缺少任何捕获组。(来自 Copilot 的第二个建议正确删除了^字符,但仍然没有包含捕获组。)
但上面这些都是小事,真正的大问题在于正则表达式实际上无法正确解析 Python 注释。例如下面的代码会失败,因为 tag_prefix:str="#"中的 #会被错误地解析为一个注释的开头:
事实证明,使用正则表达式无法正确解析 Python 代码。但是 Copilot 做到了我们的要求:在提示注释中,我们明确要求使用正则表达式,而这正是 Copilot 给我们的东西。提供这个示例的社区成员在编写代码时正是这样做的,因为他们认为正则表达式是解决这个问题的正确方法。(不过就算我尝试从提示中删除“regex to”,Copilot 仍然提示使用正则表达式方案。)这种情况下的问题并不是 Copilot 做错了什么,而是它的设计目的可能不是选出最符合程序员利益的方案。
GitHub 宣传说 Copilot 是“结对程序员”。但我不敢说这个说法真的能反映它在做的事情。好的结对程序员会帮你质疑你的假设,找出隐藏问题,并看到更大的图景。这些事情 Copilot 都不会做——恰恰相反,它会盲目地假设你的假设是合适的,并且完全根据你现在的文本光标周围的直接上下文来编写代码。
认知偏差和 AI 结对编程
AI 结对程序员需要与人类紧密合作,反之亦然。然而,人类有两种认知偏见会成为障碍:自动化偏见和锚定偏见。就因为人类有这两种弱点,我们都会倾向于过度依赖 Copilot 的建议,即便我们显意识里会试着不去这样做。
维基百科是这样描述自动化偏见的:
人类倾向于支持来自自动化决策系统的建议,并忽略没有自动化参与的情况下获得的相反信息,即便后者才是正确的
自动化偏见已被认为是医疗保健领域中的一大问题,这个领域中计算机决策支持系统已被广泛应用。司法和警务界也有很多例子,例如加利福尼亚州的市政府官员错误地描述了用于预测性警务的 IBM Watson 工具:“通过机器学习和自动化技术,成功率达到了 99%,也就是说机器人能以 99%的准确率告诉我们接下来会发生什么”,结果市长回答说“好吧,我们为什么不(给它)安装.50 口径重机枪呢?”(他声称自己是在“开玩笑”。)这种对 AI 能力的夸大信念也会影响 Copilot 的用户,尤其是对自己的能力没有信心的程序员。
决策实验室这样描述锚定偏见:
一种认知偏见,导致我们过分依赖关于某个主题的第一条信息。
关于锚定偏见的资料已经很多了,许多商学院课程都把它视为一种有用的工具,可以用在谈判和定价等场合中。
当我们在 vscode 中输入内容时,Copilot 会自动介入并给出代码自动完成建议,无需我们进行任何交互。这往往意味着,在我们真正有机会思考我们要做什么之前,Copilot 已经为我们规划了一条路径。这不仅是我们获得的“第一条信息”,而且还是“来自自动决策系统的建议”的情况——我们正在克服双重认知偏见!而且它不只发生一次,每次我们在文本编辑器中再打几个字时它就会跳出来。
不幸的是,我们对认知偏见的一个认识是,仅仅意识到它们的存在并不足以让我们避免被它们愚弄。所以这不是 GitHub 可以通过详细介绍 Copilot 建议和教育用户就能解决的问题。
Stack Overflow、谷歌和 API 用法示例
一般来说,如果程序员不知道如何做某事,并且也没在使用 Copilot,他们会谷歌它。例如,我们之前讨论的,想要在包含代码的字符串中查找参数和注释的程序员可能会搜索这样的内容:“python extract parameter list from code regex(python 从代码正则表达式中提取参数列表)”。按这样搜索的第二个结果是 Stack Overflow 的一个帖子,其中有一个已被接受的正确答案,说它不能用 Python 正则表达式完成。相反,答案建议使用一个解析器,例如pyparsing。然后我尝试搜索“pyparsing python comments”,发现这个模块解决了我们的那个问题。
我还尝试搜索“extract comments from python file(从 python 文件中提取注释)”,它给出的第一个结果展示了如何使用 Python 标准库的tokenize模块解决问题。在这个例子中,请求者对问题的描述是“我正在尝试编写一个程序来提取用户输入的代码中的注释。我尝试使用正则表达式,但发现很难写。*”听起来很耳熟!
与给 Copilot 一个提示并获得答案相比,这种办法花的时间要多几分钟,但这让我对问题和可能的解决方案空间有了更多的了解。Stack Overflow 的讨论帮助我理解了在 Python 中处理引用字符串的挑战,讨论还解释了 Python 正则表达式引擎的局限性。
在这种情况下,我觉得 Copilot 方法对于有经验的程序员和初学者来说都会更糟。有经验的程序员需要花时间研究它建议的各种选项,认识到这些选项都没有正确解决问题,无论如何自己都必须在线搜索解决方案。初学者程序员可能会觉得他们已经解决了问题,实际上并不会了解他们本应该了解的,关于正则表达式的局限和功能的内容,并且最后会在没有意识到的情况下编写有问题的代码。
除了 Copilot,GitHub 的所有者微软还创建了另一个类似的产品,称为“API用法示例”。下面是直接取自他们网站的示例:
这款工具能在线查找那些使用了你在用的 API 或库的案例,并提供展示了用法的真实代码示例,以及指向示例源的链接。这是一种介于 Stack Overflow(但去掉了有价值的讨论)和 Copilot(但不提供针对你的特定代码上下文定制的建议)之间的有趣方法。这里关键的附加部分是它会链接到源。这意味着程序员实际上可以看到其他人是如何使用某个特性的完整上下文的。提高编码能力的最好方法是阅读代码和编写代码。帮助编程人员找到相关的代码来阅读看起来是一种很好的方法,既可以解决人们的问题,又可以帮助他们提高技能。
微软的 API 用法示例是否会表现出色,将取决于他们对代码示例按质量排名,并展示最佳用法示例的能力。据他们的产品经理(在 Twitter 上)说,这是他们目前正在做的事情。
结论
我还是不知道这篇文章标题中问题的答案,“GitHub Copilot 是福还是祸?”对一些人来说它可能是福音,对另一些人来说可能是诅咒。对于后者来说,他们可能多年都不会发现这一点,因为诅咒就是让他们学得更少、更慢,增加了技术债务,并引入了微妙的错误——这些都是你可能没有注意到的,特别是对于新手开发者来说更是如此。
Copilot 可能对样板代码较多且元编程功能有限的语言(例如 Go)更有用。(出于这个原因,今天很多人在使用 Go 的模板化代码生成功能。)另一个它可能特别适合的领域是帮助那些使用不熟悉的语言编程的程序员老手,因为它可以帮助程序员获得正确的基本语法,并指出库函数和常见的习语。
需要记住的是,Copilot 是一项非常新的技术的早期预览,它将变得越来越好。在接下来的几个月和几年里将会有许多竞争者涌现,GitHub 也无疑会发布他们自己工具的更新和更好的版本。
要看到程序合成方面的真正改进,我们需要超越语言模型,需要一个更全面的解决方案,其中包含围绕人机交互、软件工程、测试和其他许多学科的最佳实践。目前,Copilot 感觉像是由机器学习研究人员设计和实现的产品,而不是包含所有必要领域专业知识的完整解决方案。我相信这一现状迟早会改变。
评论