【CSDN 编者按】新年伊始,在各位大佬进行年终总结之时,近日 Redis 之父 antirez 也发布了他今年的第一篇博文:“2024 年初,聊聊 LLM 和编程”。不同于预想之中的盘点和回顾,这篇文章 antirez 以一位独立开发者的角度,分享了他这一年来对于 AI 的使用感受,并表示:“就我个人而言,我将继续广泛使用 LLM。”
作者 | antirez,Redis 作者
翻译 | ChatGPT 责编 | 郑丽媛
出品 | CSDN(ID:CSDNnews)
2023 年对 AI 而言是特殊的一年,这毫无疑问。首先我声明一下,本文并非是对 LLM(大型语言模型)过去一年的回顾,而是以一位独立程序员的身份聊聊我对 AI 的见证。自 ChatGPT 问世以来,以及后来使用在本地运行的 LLM,我已将这项新技术广泛应用。从个人角度来说,我使用大模型一方面是为了提升编程速度,另一方面也是希望不再浪费精力在不值得努力的编程方面。
例如,我曾花了无数个小时搜索那些奇特却无趣的技术文档、曾被迫学习过于复杂的 API、曾编写过一些几个小时后就被丢弃的程序……这些都是我不想做的事情,尤其现在谷歌搜索引擎也已沦为垃圾信息的海洋,我得费心筛选才能找到一些有用的东西。
与此同时,我在编程方面当然不是新手。我可以在没有任何辅助工具的情况下编写代码,并且也经常这么做。但随着时间的推移,我开始越来越多地用 LLM 来编写高级代码,尤其是在 Python 中,在 C 语言中则较少。在对 LLM 频繁使用的过程中,我逐渐知道了什么情况该使用它们、什么情况使用它们只会拖慢速度。
除此之外,我还了解到,LLM 其实有点像维基百科和 YouTube 上的各种视频课程:对于本身就有意愿、有能力和有纪律的人,有了 LLM 的帮助几乎如虎添翼;而对于那些本就不太上进、拖后腿的人,强大如 LLM 也帮不了他们。因此我担心,至少就现阶段而言,LLM 只会让本就优秀的人变得更优秀。
但是,让我们一步一步来。
全知全能还是鹦鹉学舌?
在这波机器学习的新潮中,最令人担忧的现象之一是 AI 专家对于 LLM 的认知也较为有限。伟人发明了神经网络,甚至还发明了自动优化神经网络参数的算法,而硬件能训练越来越大的模型,利用对要处理数据的统计知识(先验)以及大量试错来逼近最佳结果,人们发现了比其他方法更有效的架构。但总体而言,神经网络仍然相当不透明。
由于无法解释 LLM 为何会出现某些新功能,许多人推测科学家们会更加谨慎。但另一方面,还有部分人严重低估 LLM,认为它们充其量只是略为先进的马尔可夫链,最多只能重复在训练集中看到的极其有限的变化。不过后来在面对证据时,这种“鹦鹉学舌”的说法几乎被推翻。
与此同时,还有许多热心群众将现实中并不存在的超自然力量也归因于 LLM——而实际情况是,LLM 最多只能对自己在训练期间接触过的数据表示空间中进行插值,这早已不是什么新鲜事。另外值得一提的是,LLM 的插值能力也很有限,如果某个 LLM 能够在其接触过的所有代码所限定的空间内连续插值,即使它做不到真正的创新,也能够取代 99% 的程序员了。
好在实际情况并非如此。LLM 确实能编写出自己从未见过的程序,并以一定频率将训练集中出现的不同想法巧妙融合,但这种能力的局限性也很大:每当需要微妙的推理时,LLM 就会惨遭失败。不过话说回来,LLM 仍代表了 AI 诞生至今的最大成就,这没什么好否认的。
愚蠢,但无所不知
有个事实我们需要明确:LLM 最多只能进行最基本的推理,且往往不准确,还经常夹杂着一些不存在事实的幻觉,但它们确实知识渊博。在编程领域以及其他有高质量数据的领域,LLM 就像愚蠢的天才,知道很多事情。
与这样的搭档进行结对编程非常麻烦(对我而言,结对编程这件事本身就很麻烦):它们会有一些荒谬的想法,我们则必须不断地将自己的想法强加于它。当然,如果这个博学的“傻瓜”可以为我们所用,回答我们向它提出的所有问题,那情况就大不相同了。现有的 LLM 可能还无法跨越知识的鸿沟,但如果我们想处理一个不太了解的主题,那 LLM 可以让我们从绝对无知的状态中解脱出来,让我们了解到足够的知识从而独立前行。
在编程领域,也许是二三十年前,人们对 LLM 的能力兴趣并不大。那时,你必须掌握几种编程语言、经典算法和十个基本库。剩下的就得靠自己了,靠自己的智慧、专业知识和设计技能。如果你具备了这些要素,你就是一个熟练的程序员,几乎能做所有的事情。随着时间的推移,我们目睹了框架、编程语言和各类库的爆炸式增长,虽然这种复杂性的爆炸增长往往是完全不必要和不合理的,但事实就是事实。在这种情况下,一个博学的“傻瓜”就是一位宝贵盟友。
我举个例子:我对机器学习的实验至少进行了一年,一直在使用 Keras,后来出于各种原因,我转到了 PyTorch。我已经知道嵌入或残差网络是什么,但我不想一步一步地学习 PyTorch 文档(就像我学习 Keras 时那样,当时还没有 ChatGPT)。而有了 LLM 后,编写使用 Torch 的 Python 代码就变得非常容易了,我只需对我想要组合的模型有清晰的想法,并提出正确的问题即可。
举例说明
我不是在谈论像“嘿,X 类中执行 Y 的方法是什么?”这样的简单问题,这样我可能会同意那些对 LLM 持怀疑态度的人的观点。事实证明,更复杂的模型所能做的事情要精细得多。我可以告诉 GPT4:看,这是我在 PyTorch 中实现的神经网络模型,这是我的批处理数据,我想调整张量的大小,使输出批次的函数与神经网络的输入相匹配,我想用这种特殊的方式来表示事物。你能给我展示进行重塑所需的代码吗?然后,GPT4 编写了代码,而我只需要在 Python CLI 中测试张量是否真的具有我需要的维度,以及数据布局是否正确。
还有一个例子。前段时间,我不得不为某些基于 ESP32 的设备实现一个 BLE 客户端。经过研究后,我发现多平台蓝牙编程绑定或多或少都无法使用。解决方案很简单,使用 MacOS 的本地 API 用 Objective C 编写代码。于是,我不得不同时处理两个问题:学习 Objective C 繁琐的 BLE API,同时还要记起如何在 Objective C 中编程——我上一次用 Objective C 写程序是十年前,根本不记得事件循环、内存管理等许多细节。
然而在 LLM 的帮助下,我用极短的时间就写完了代码。最终的代码是这样的,虽然不算美观,但至少能完成任务:
https://github.com/antirez/freakwan/blob/main/osx-bte-cli/SerialBTE.m
代码主要是通过在 ChatGPT 上剪切粘贴我想要做的事情来编写的,由于刚开始我不太了解如何做,最初生成的代码没法正常运行,但我可以让 LLM 向我解释问题所在以及如何解决它。如果没有 ChatGPT,我能做得到吗?当然可以,但这不仅浪费了我的时间,我可能根本也不会去尝试,因为这不值得:编写这样一个对我的项目来说次要的程序,其付出和收益之间的比例并不可观。
最后还有一个例子,与代码编写无关,而是与数据解释有关。当时,我想建立一个用我在网上找到的卷积神经网络的 Python 脚本,但文档相当缺乏。这个网络的优势在于它采用 ONNX 格式,因此我可以轻松提取输入和输出列表以及它们分配名称的列表。我只知道这个卷积神经网络能检测图像中的某些特征,但输入图像的格式和大小及输出的复杂度我都不太了解。
我首先将 ONNX 网络元数据的输出复制粘贴到 ChatGPT 中,并同步了我对该网络的一点了解。然后,ChatGPT 假设输入的组织方式,输出可能是表示图像中与潜在缺陷相对应部分的归一化方框等。经过几分钟的来回讨论后,我得到了一个能进行网络推理的 Python 脚本以及将起始图像转换为适合输入的张量所需的代码等等。
一次性程序
上述类似的例子还有很多,我在这里就不一一赘述了,基本上都是同样的情况和结果。除此之外,我还经常遇到另一类情况:想迅速了解某些可以快速验证的东西。在这种情况下,我就会用 LLM 来加快我对知识的需求。
不过,在不同的情况下,我也会让 LLM 编写所有代码。例如,当我需要编写一个一次性的程序时,比如这个:
https://github.com/antirez/simple-language-model/blob/main/plot.py
我需要可视化一个小型神经网络学习过程中的损失曲线。我向 GPT4 展示了 PyTorch 程序在学习过程中生成的 CSV 文件格式,然后我要求,如果我在命令行中指定了多个 CSV 文件,我就不再需要相同实验的训练和验证损失曲线,而是要比较不同实验的验证损失曲线。以上结果就是 GPT4 生成的结果,总共耗时 30 秒。
同样,我需要一个程序来读取 AirBnB 的 CSV 报告,并按月份和年份进行分组。然后,考虑清洁费用和每次预订的住宿天数,它将统计出一年中不同月份的平均租金价格。这个程序对我来说非常有用,但编写它也非常无聊:没有任何有趣的东西。因此,我从 CSV 文件中截取了一小部分,并在 GPT4 上进行了剪切粘贴,然后给 LLM 写了要解决的问题,其生成的程序一次就成功了,以下,我将展示完整代码:
python
import pandas as pd
pd.set_option('display.max_rows', None)
df = pd.read_csv('listings.csv')
reservations = df[df['Type'] == 'Reservation']
reservations['Start Date'] = pd.to_datetime(reservations['Start Date'])
reservations['Year'] = reservations['Start Date'].dt.year
reservations['Month'] = reservations['Start Date'].dt.month
reservations['Nightly Rate'] = (reservations['Amount'] - reservations['Cleaning Fee']) / reservations['Nights']
all_listings = reservations['Listing'].unique()
all_years = reservations['Year'].unique()
all_months = range(1, 13)
index = pd.MultiIndex.from_product([all_listings, all_years, all_months], names=['Listing', 'Year', 'Month'])
all_data = pd.DataFrame(index=index).reset_index()
merged_data = pd.merge(all_data, reservations, on=['Listing', 'Year', 'Month'], how='left')
average_nightly_rates = merged_data.groupby(['Listing', 'Year', 'Month'])['Nightly Rate'].mean().fillna(0)
print(average_nightly_rates)
要准确理解如何对数据进行分组,且这些数据本来就相当分散和无序,需要一些简单的推理:这不是一个 LLM 简单地重复一些它所见过的语言可以解决的问题。GPT4 在训练过程中可能接触过类似程序,但与我所要求的提示条件必定有所不同,但它依旧能写出符合的程序代码。在我看来,这表明 LLM 能在一定程度上对训练集中各种程序描绘的空间内进行插值。
在我看来,编写这种程序只会浪费我的时间,而 LLM 可以为我代劳,这无疑直接加速了我编写代码的能力:我可以专注于真正重要的事情。
当 LLM 不尽如人意时:系统编程
尽管我在使用 LLM 和编程方面取得了成功,但当我编写 C 语言程序时,我几乎总是把 LLM 仅用作一种更方便的文档形式。我是系统编程专家,在这种情况下,只有当 LLM 能够进行复杂推理时才会对我有用:然而在这种场景中,它们几乎总是失败的。
让我们从这个实验的提示开始:
概述一个漂亮、小巧但可行的 C 语言实现的 Bloom 过滤器的要求。重点考虑哈希函数的重要方面,然后用高质量的 C 语言编写出来。同时考虑到你的示例实现应能存储 100,000 个元素,误报率不超过 5%,添加的元素是以空字符结尾的字符串。
以下是输出:
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#define N 100000 // Number of elements
#define P 0.05 // False positive probability
#define M (int)(-(N * log(P)) / (log(2) * log(2))) // Size of bit array
#define K (int)(M / N * log(2)) // Number of hash functions
unsigned char bitArray[M / 8 + 1];
void setBit(int n) {
bitArray[n / 8] |= 1 << (n % 8);
}
int getBit(int n) {
return bitArray[n / 8] & (1 << (n % 8));
}
// Basic hash functions (for demonstration purposes)
int hash1(char *str) {
int hash = 5381;
int c;
while ((c = *str++))
hash = ((hash << 5) + hash) + c;
return abs(hash % M);
}
int hash2(char *str) {
unsigned long hash = 0;
int c;
while ((c = *str++))
hash = c + (hash << 6) + (hash << 16) - hash;
return abs(hash % M);
}
void add(char *str) {
setBit(hash1(str));
setBit(hash2(str));
// Add more hash functions if needed
}
int is_in(char *str) {
return getBit(hash1(str)) && getBit(hash2(str));
// Check more hash functions if used
}
int main() {
char *testStr = "testString";
add(testStr);
if (is_in(testStr)) {
printf("%s is probably in the set.\n", testStr);
} else {
printf("%s is definitely not in the set.\n", testStr);
}
return 0;
}
从生成代码来看,GPT4 的表现不太出色。Bloom 过滤器的应用相当广泛,不是一种太过奇异的数据结构。不过要编写一个像样的 Bloom 过滤器,显然需要更强的抽象能力,例如找到一种有效方法,对同一字符串进行 N 次哈希处理,并确保各哈希值充分不相关。
然而,如果你明确要求 GPT4 修改哈希函数,使其产生 N 个不相关的输出,它提出的解决方案要好得多:
c
unsigned int hash(const char* str, unsigned int hash_id) {
unsigned int hash = 5381;
int c;
while ((c = *str++)) {
hash = ((hash << 5) + hash) + c + hash_id;
}
return hash % BF_SIZE;
}
如果是 GPT4 自己提出了这个想法,它可能会以不同的方式编写 Bloom 过滤器,使用单个哈希函数一次设置 K 个 bits。
也就是说,GPT4 能独立完成编写一个合适且更通用的哈希函数的任务,但在编写 Bloom 过滤器这一更大的项目中,它却未能表现出足够的推理能力,而是输出了两个不同但相似的哈希函数——这与网络上一篇关于 Bloom 过滤器实现不佳的示例文章相符。
目前,LLM 的推理能力较弱,如果关于提问的信息来源稀缺,加上还掺杂着许多质量低劣的信息来源,输出结果就很容易不尽如人意。然而,这并非个案,我曾多次尝试在算法或系统编程领域使用 LLM,结果都非常糟糕
与此同时,GPT4 能够反汇编它生成的函数(在单独的会话中),并准确理解其目的:
https://chat.openai.com/share/f54b64d7-d19a-4be4-8f10-08553aa812f1
因此,断言 LLM 在系统编程上没有用途肯定是不准确的。
一个有趣且基本在意料之中的现象是,在这些情况下,较小模型和较大模型之间的差异确实很大。下面让我们看看两个本地模型的表现。
例如,Mistral 的 MoE 模型 Mixtral 对同样的问题的回答如下:
c
unsigned int hash(const char* str, unsigned int hash_id) {
unsigned int hash = 5381;
int c;
while ((c = *str++)) {
hash = ((hash << 5) + hash) + c;
}
return hash + hash_id;
}
显然,仅在最后添加 hash_id 是一个极其糟糕的解决方案。Mixtral 在很多方面都是一个很好的模型,但对于推理能力本就薄弱的 LLM 而言,目前的规则无疑是:越大越好。
这是我与本地 LLM deepseek 编码器的聊天,由于我没有足够的 RAM 以更高的精度使用该模型,所以量化到了 4 位。然而,由于它有 340 亿个参数,因此在这个问题上的推理能力似乎相当先进:直说将 hash_id 添加到末尾可能会导致分布不均,尤其是当 hash_id 过大或分布不均时,同时建议用位运算将 hash_id 混合进去。
c
unsigned int hash(const char* str, unsigned int hash_id) {
unsigned int hash = 5381;
int c;
while ((c = *str++)) {
hash = ((hash << 5) + hash) ^ c + hash_id; // Using XOR to mix in the hash_id
}
return (hash ^ hash_id); // Mixing the hash_id one more time at the end
}
这个结果,至少在我的 MacBook M1 Max 上运行得还不错,它还使用了异或来混合求和结果。在这种情况下,我提供了解决问题的线索肯定对模型有所帮助,但是模型确定了问题的真正源头,并提出有效的解决方案——上述情况,是任何书籍、文档或谷歌搜索都无法实现的。
不论这是一种插值的原始结果,还是从其他角度来看,不可否认模型确实进行了某种形式的推理,我们找到问题起源和解决方案也正得益于此。所以,无论人们如何看待 LLM,断言它们对程序员没有帮助是一种极为草率的行为。
但与此同时,凭我在过去几个月的经验表明,对于系统编程而言,如果你已经是一名经验丰富的程序员,LLM 几乎永远也提供不了有效的解决方案。举个例子,我目前的项目是 ggufflib,涉及编写一个读写 GGUF 格式文件的库,这是 llama.cpp 加载量化模型的格式。最初,为了了解量化编码是如何工作的,我尝试用 ChatGPT,但后来我决定对 llama.cpp 的代码进行逆向工程:这样更快。
如果 LLM 能为系统程序员提供适当的帮助,那么看到数据编码“struct”声明和解码函数时,就应该能重建数据格式文档。llama.cpp的函数很小,完全符合 GPT4 的要求,但输出结果却完全没用。在这种情况下,我们就只能像过去一样:掏出纸和笔,阅读代码,看看解码器提取的 bits 在哪里注册。
透过外在看本质
我这么说可能很直接,但事实确实如此:当今的大多数编程工作,都是以略有不同的形式重复同样的内容——而这,并不需要高水平的推理能力。尽管 LLM 会受到上下文的严重限制,但它们在做这方面确实相当擅长。
这应该引起程序员的思考:编写这类程序是否值得?当然,你会得到报酬,还可能是相当丰厚的报酬,但如果用一个 LLM 就可以完成其中一部分,那么也许五年或十年后,这份工作并不是你的最好归宿。
其次,LLM 到底是真的具备某种推理能力,还只是“鹦鹉学舌”?也许有时候它们看起来会推理,符合符号学家所说的“能指”概念,但实际上这是一种并不存在的意义。那些长期与 LLM 打交道、并深知其限制的人们,对此应该深有感触:它们对以往接触过的内容的融合能力,远远超出了其随机输出单词的能力。尽管 LLM 的大部分训练主要是在预训练期间进行的,但在预测下一个 token 时,大模型还是会根据目标创建某种形式的抽象模型。这个模型可能很脆弱、零散且不完美,但通过实际观察,我们会发现这种能力一定存在。如果我们的数学定理令人怀疑,而最伟大的专家们经常持相反意见,那么对我们来说,“眼见为实”似乎是一种明智的做法。
最后,我想说:事已至此,不使用 LLM 进行编程还有什么意义呢?向 LLM 提出正确的问题已是一项基本技能,这种技能练得越少,AI 对工作的帮助就越小。此外,培养对问题的描述能力在与其他人交谈时也很有用,有时并非只有 LLM 不理解我们想说什么。沟通不畅是一个很大的局限,很多程序员尽管在自己的特定领域能力很强,但沟通能力却很差。
目前,谷歌搜索已经乱得不能用了:使用 LLM,哪怕只是把它作为一种压缩的文档形式,也是一个不错的选择。就我个人而言,我将继续广泛使用 LLM,我从来都不喜欢学习晦涩难懂的通信协议细节,也很讨厌那些想展示自己有多么优秀的人编写的库的复杂方法——对我来说,这些似乎都是“知识垃圾”,感谢 LLM 每天都在把我从这一切中解救出来。
原文链接:http://antirez.com/news/140