生命游戏中的卷积
今天编程练习时做到了生命游戏,一道经典的二维数组模拟题。规则很简单:每个细胞的下一个状态取决于它周围八个邻居的存活数量。但实现起来,那八个方向的边界判断却写得很是臃肿:
// ...
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
int count = 0;
if (i > 0 && j > 0) count += board[i-1][j-1];
if (i > 0 && j < n-1) count += board[i-1][j+1];
if (i < m-1 && j > 0) count += board[i+1][j-1];
if (i < m-1 && j < n-1) count += board[i+1][j+1];
if (i > 0) count += board[i-1][j];
if (j > 0) count += board[i][j-1];
if (i < m-1) count += board[i+1][j];
if (j < n-1) count += board[i][j+1];
// 然后根据 count 和当前状态更新细胞...
}
}
看着这些重复的边界判断,我不禁想到:这种「用一个固定模式对每个位置求和」的操作,不就是卷积吗?
卷积视角下的生命游戏
卷积操作简单来说,就是用一个卷积核在特征图上滑动,每次将核与对应区域的值逐点相乘再求和。对于生命游戏,我们只需要统计八个邻居的存活数,而不包括中心细胞本身。因此,构造一个 3×3 的卷积核,中心为 0,其余为 1:
$$ \mathbf{K} = \begin{bmatrix} 1 & 1 & 1 \cr 1 & 0 & 1 \cr 1 & 1 & 1 \end{bmatrix} $$
那么,对棋盘做一次卷积,输出矩阵的每个位置就是该细胞周围的存活邻居数。设棋盘为矩阵 $\mathbf{A}$,位置 x, y 的细胞状态为 $a_{x,y}$,则存活邻居数 $N(x,y)$ 可以表示为:
$$ N(x, y) = \sum_{(i, j) \in \mathcal{N}} a_{x+i, y+j}, \mathcal{N} = \lbrace-1, 0, 1\rbrace^2 \setminus \lbrace(0, 0)\rbrace $$
应用到整个棋盘即
$$ \mathbf{N} = \mathbf{A} * \mathbf{K} $$
其中 $*$ 表示二维卷积。这样便完美避开了所有手动边界判断——卷积层自带的 padding 机制已经处理好了边缘情况。
用 PyTorch 玩一把
既然想到了卷积,那就干脆用深度学习框架实现一下,虽然对这道题来说完全是杀鸡用牛刀,但 just for fun 嘛!
import torch
import torch.nn as nn
# 1. 初始化棋盘(使用 float32 适配 GPU 加速)
board = torch.tensor([[0, 1, 0],
[0, 0, 1],
[1, 1, 1],
[0, 0, 0]], dtype=torch.float32).unsqueeze(0).unsqueeze(0)
# 2. 设置卷积层(无 bias,固定核)
conv = nn.Conv2d(1, 1, kernel_size=3, padding=1, bias=False)
with torch.no_grad():
kernel = torch.tensor([[1, 1, 1],
[1, 0, 1],
[1, 1, 1]], dtype=torch.float32)
conv.weight.copy_(kernel.view(1, 1, 3, 3))
# 3. 计算邻居数量
with torch.no_grad():
neighbors = conv(board)
# 4. 应用康威规则
# 规则:活细胞邻居=2或3则存活,死细胞邻居=3则诞生
next_gen = (neighbors == 3) | ((neighbors == 2) & (board == 1))
result = next_gen.int()
print(result.squeeze())
感慨
很多知识都是从just for fun学来的。虽然并不会有人用深度学习工具研究生命游戏(应该是以约束求解器为主),但这次思考是一个很好的拓宽知识面的机会。