当我们让 GPT-4 帮助我们解决编程问题时,它可能会生成一段看起来完美的代码,但实际上,这段代码可能并不能解决我们的问题,或者它的解决方案可能并不是最优的。这是因为GPT-4并不理解代码的含义,它只是模仿它在训练数据中看到的代码模式。这种现象被成为幻觉,用 GPT-4 久了的话,基本都会体会到。

识破这种幻觉的关键在于理解GPT-4的工作原理,以及它的局限性。我们需要意识到,GPT-4并不理解我们的问题,也不理解它生成的答案。它只是一个非常复杂的模式匹配器,它的目标是生成看起来合理的文本,而不是解决我们的问题。当我们使用GPT-4时,我们需要批判性地思考它的回答,验证它的答案,甚至需要寻求其他专家或资源的帮助。只有这样,我们才能充分利用GPT-4的能力,同时避免被它的表面表现所迷惑。

在让 GPT 解决 Leetcode 题目 2352. Equal Row and Column Pairs 的过程中,又一次见识到了 GPT-4 的幻觉。刚读完题目,很快就给出了一个不错的实现,接着让 GPT-4 优化代码实现的时候,它就开始坑蒙拐骗了,还好我及时识破幻觉,通过验证,证实这里确实有问题。和 ChatGPT 完整的对话见这里

GPT4 对自己出错的解释

题目以及实现代码

题目很简单,给定一个 N*N 的整数矩阵 grid,返回所有满足条件的对(ri, cj)的数量,其中行ri和列cj是相等的。如果他们包含相同的元素并且顺序也相同(即,是相等的数组),则认为行和列的配对是相等的。

约束条件:

  • n == grid.length == grid[i].length
  • 1 <= n <= 200
  • 1 <= grid[i][j] <= 105

GPT-4 很快就给出了一个直观的解决方法,首先将grid的每一行和每一列转换为元组,并将它们存储在字典中,字典的键是它们各自的索引。然后,通过遍历行和列并进行比较,计算行等于列的次数。代码实现也很简单:

1
2
3
4
5
6
class Solution:
def equalPairs(self, grid: List[List[int]]) -> int:
n = len(grid)
rows = {i: tuple(grid[i]) for i in range(n)}
cols = {j: tuple(grid[i][j] for i in range(n)) for j in range(n)}
return sum(row == cols[j] for i, row in rows.items() for j in range(n))

这个方法的时间复杂度和空间复杂度都是O(n^2)。

  • 时间复杂度:我们需要遍历整个矩阵两次,一次是为了创建行和列的元组,另一次是为了比较行和列的元组。每次遍历都需要O(n^2)的时间,所以总的时间复杂度是O(n^2)。
  • 空间复杂度:我们需要存储所有行和列的元组,这需要O(n^2)的空间。因此,空间复杂度也是O(n^2)。

这里 GPT-4 认为时间复杂度已经是最优的,没法继续优化,但是空间复杂度还可以继续优化,可以使用哈希表来存储每一行和每一列的哈希值,然后比较这些哈希值来确定行和列是否相等。优化后,空间复杂度降低到了O(n),因为我们只需要存储n个哈希值,而不是n个元组。

1
2
3
4
5
6
class Solution:
def equalPairs(self, grid: List[List[int]]) -> int:
n = len(grid)
rows = {i: hash(tuple(grid[i])) for i in range(n)}
cols = {j: hash(tuple(grid[i][j] for i in range(n))) for j in range(n)}
return sum(row == cols[j] for i, row in rows.items() for j in range(n))

优化执行速度

接着问了 GPT-4 一个问题:

有没有一些技巧,可以再某些数据类型下降低平均执行时间,虽然时间复杂度没变化。

然后 GPT-4 给了一个看起来很合理的优化,这里使用一种称为”早期退出(提前退出)“的技巧来在某些情况下降低平均执行时间。这种技巧的基本思想是,如果我们在比较行和列的过程中发现它们不可能相等,那么我们就可以立即停止比较,而不需要检查所有的元素。

具体来说,在计算行和列的哈希值时,同时计算它们的元素总和。然后,在比较行和列的哈希值之前,先比较它们的元素总和。如果元素总和不相等,那么我们就可以确定这一行和这一列不可能相等,因此可以立即停止比较 hash 值。

1
2
3
4
5
6
class Solution:
def equalPairs(self, grid: List[List[int]]) -> int:
n = len(grid)
rows = {i: (hash(tuple(grid[i])), sum(grid[i])) for i in range(n)}
cols = {j: (hash(tuple(grid[i][j] for i in range(n))), sum(grid[i][j] for i in range(n))) for j in range(n)}
return sum(row_hash == cols[j][0] and row_sum == cols[j][1] for i, (row_hash, row_sum) in rows.items() for j in range(n))

这种方法并不会改变算法的最坏情况时间复杂度,它仍然是O(n^2)。但是,在某些情况下,它可以显著降低算法的平均执行时间,特别是当矩阵中的行和列大部分都不相等时。

GPT-4 认为这个优化的关键在于:哈希值的比较是一个相对耗时的操作,特别是当行和列的长度很长时。因此,即使我们只能在一部分情况下避免进行哈希值的比较,我们也可能获得显著的性能提升。

识破 GPT-4 幻觉

上面优化的关键前提是对的吗?为了让 GPT-4 自己确认一遍,我重新提问:

你提到了“哈希值的比较是一个相对耗时的操作,特别是当行和列的长度很长时”,这里我不太理解,可以详细解释下吗?

结果它就认识到自己的错误了,直接说“在Python中,哈希值本身是一个整数,比较哈希值的操作实际上是非常快的”。它前面其实有点混淆比较 hash 和计算 hash 的时间成本了,不过到目前为止,他依然认为自己的优化方案有效,因为能够通过比较和(这是一个更快的操作)来避免一些哈希值的比较

GPT-4 认识到错误,然而坚持优化方案有效

我们当然能看出来这是不对的,原本的计算方案只用计算 hash 并比较hash 整数。现在优化方法多计算了和,比较的时候最好情况也得比较和,差的情况还需要比较 hash。计算和比较操作都多了,性能不会有提升。直接把我们的看法扔给 GPT-4,然后 GPT-4 终于意识到自己的错误,承认了在这个问题中,最有效的方法仍然是直接计算并比较哈希值。这是因为哈希值是一个很好的方式来快速比较两个序列是否相等,而且在Python中,计算哈希值的操作是非常快的。

GPT-4 帮忙验证

为了进一步确认下在 Python 中,比较和(一般数字)和比较 hash 整数(比较大)的性能到底有没有差异,我们可以做一个性能测评。当然是直接让 GPT-4 来写测评代码了,不过这里也是反复跟 GPT-4 沟通了几轮,才写了符合预期的代码。

第一次问“我要怎么才能验证一下,比较和和比较 hash 的性能差不多。”,然后它给了一段代码,创建一个包含100000个随机整数的列表,然后分别测量计算这个列表的和和哈希值的时间。明明要他计算比较操作的耗时,结果去计算求 sum 和 hash 的耗时。

没办法,换一个更易懂的问法,“我想要的是,测试下一般整数的比较,和 hash 整数的比较,看下速度有没有差异。”。GPT-4 还挺自信,直接告诉我 Python 中,这两种操作的速度应该是相同的,因为在底层,它们都是进行整数比较(这么自信,为啥开始还认为比较和更快。)。不过还是给出了一份代码,创建两个大的整数和它们的哈希值,然后分别测量比较这两个整数和比较这两个哈希值的时间。我不得不说,这个性能测试太不专业了,只执行一次就想来评测速度。

只能再次提示它,让它给一个更专业的测试方案,至少也得测试很多轮,然后看平均时间。这次 GPT-4 终于 Get 到了要点,考虑了统计显著性。知道在代码中多次运行测试,然后取平均值来得到更准确的结果。

不过我发现又被它带坑里了,上面测试中,num 是整数,hash(num) 在 python 中返回的是整数本身,所以这里的比较没有任何意义。于是只好让它改下上面的测试,生成 2 个 list,然后计算他们的sum, hash,然后测试比较和与比较hash哪个更耗时。最后给了一个还算可以的测评代码:

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
import timeit
import statistics
import random

# 创建两个大的列表
list1 = [random.randint(1, 100) for _ in range(100000)]
list2 = [random.randint(1, 100) for _ in range(100000)]

# 预先计算和和哈希值
sum1 = sum(list1)
sum2 = sum(list2)
hash1 = hash(tuple(list1))
hash2 = hash(tuple(list2))

# 定义比较和的函数
def compare_sums():
return sum1 == sum2

# 定义比较哈希值的函数
def compare_hashes():
return hash1 == hash2

# 使用timeit模块多次运行测试,并取平均值
num_tests = 1000000
num_repeats = 5

sum_times = timeit.repeat(compare_sums, number=num_tests, repeat=num_repeats)
hash_times = timeit.repeat(compare_hashes, number=num_tests, repeat=num_repeats)

print(f"Sum comparison: min={min(sum_times)}, max={max(sum_times)}, mean={statistics.mean(sum_times)}, stdev={statistics.stdev(sum_times)}")
print(f"Hash comparison: min={min(hash_times)}, max={max(hash_times)}, mean={statistics.mean(hash_times)}, stdev={statistics.stdev(hash_times)}")

这里运行结果来看,比较操作的最小,最长,以及平均耗时基本都一样。标准偏差,sum 比较的是0.00016289003975172148,而哈希的比较的标准偏差是9.324125240116931e-05。虽然有差异,但是这两个标准偏差都非常小,这意味着在大多数情况下,这两种操作的执行时间都非常稳定。

GPT-4 解释自己的幻觉

整个实现没什么疑问了,不过为啥 GPT-4 开始会给出 “计算和的操作相比于哈希值的比较,计算成本更低。” 这种错误结论呢,它是这样回答的。

这是因为我没有充分理解问题的上下文和需求。我错误地假设了计算和的操作会比计算哈希值更快,但这并不正确。在Python中,计算哈希值的操作通常非常快,而且哈希值提供了一种高效的方式来比较两个序列是否可能相等。

我意识到这一点是不对的,是因为我重新审视了问题的需求和上下文,以及哈希值在Python中的实现和用途。我意识到哈希值提供了一种更高效的比较方式,而计算和的操作并不能提供任何性能优化。

那么我们后续使用 GPT-4 的过程中,如何验证它的回答是不是符合事实呢,GPT-4 同样给出了自己的看法:

  1. 实践验证:对于编程或技术相关的问题,你可以尝试运行我提供的代码或按照我提供的步骤操作,看看结果是否符合预期。
  2. 查阅文档或其他资源:对于我提供的信息,你可以查阅相关的官方文档、教程或其他可靠的资源来进行验证。
  3. 询问其他专家或社区:你也可以将我的回答与其他专家或社区的意见进行对比,看看他们是否有相同或不同的观点。
  4. 提问和反馈:如果你对我的回答有疑问,或者发现我的回答有误,你可以随时向我提问或提供反馈。我会尽力提供更准确和有用的信息。

随着人工智能的不断发展和进步,我们期待这种”幻觉”现象会逐渐减少。然而,无论AI的能力如何增强,我们都必须保持清醒的头脑,不盲目迷信AI。无论何时,我们都需要批判性地思考,验证AI的答案。只有这样,我们才能充分利用AI的能力,同时避免被它的幻觉所迷惑。