☰
用Python从零手写神经网络玩赛车游戏:前馈网络与行为克隆实战
2026/10/10 14:38:10 网站建设 项目流程

简介:这份资源面向神经网络入门者与游戏AI爱好者,用Python从零实现神经网络来操纵赛车游戏,解决“看懂原理却不知如何落地”的问题。包内提供两条技术路线:一条基于TensorFlow框架搭建训练模型,另一条完全手写DNN与激活函数,便于对比理解底层计算与工程封装差异。资源共54个文件,以16个py脚本、10个pyc缓存、9个txt说明、7个xml配置及5张png素材为主,另含2段wmv人机对战与人工操作录屏、模型checkpoint与data分片文件,压缩包约8.9MB,目录按游戏逻辑、AI逻辑、素材与模型数据分层组织。已有2062人学习下载。读者可拿到完整可运行的赛车游戏环境、数据生成与训练脚本、模型测试入口以及录屏对照,既能跑通训练流程,也能拆解网络结构与调参思路,适合作为神经网络课程设计或自学练手项目。

1. 用 Python 从零搭一个能开赛车游戏的神经网络:先搞清楚它到底在学什么

很多人第一次听到「用 Python 自己实现神经网络操纵赛车游戏」,脑子里浮现的是自动驾驶那种复杂系统,觉得门槛高得离谱。其实拆开看,这件事的核心链路非常短:游戏画面或车辆状态作为输入,神经网络输出方向、油门、刹车这几个控制量,游戏给出得分反馈,网络根据反馈调整权重。它本质上是一个「感知 → 决策 → 反馈 → 更新」的闭环,和你训练一个手写数字分类器没有本质区别,只是把标签换成了游戏里的奖励信号。

这条链路能解决的问题很具体:你想验证一个前馈神经网络到底能不能学会「看到弯道就减速打方向」这种连续控制,而不是停留在调库跑 MNIST。适合两类人:一类是刚学完 BP 神经网络反向传播推导、想找个能跑起来的项目练手的;另一类是做过游戏 AI,想用最朴素的 numpy 手写网络、不依赖 PyTorch 这类框架,把每一层梯度都攥在自己手里的。下面这套方案,我用 numpy 从零实现,不调任何深度学习框架,全程可复现。

2. 选型与原理:为什么用前馈网络而不是 CNN 或 LSTM

2.1 输入到底喂什么:像素还是状态向量

这是第一个必须拍板的决策,直接决定后面网络结构和训练难度。两条路:

第一条是喂原始像素。把游戏画面缩放到比如 84×84 灰度图,展平成 7056 维向量丢进网络。好处是通用,理论上不需要你理解游戏内部状态;坏处是维度高、样本效率低,一个纯前馈网络在像素上学会开车,往往要几十万帧,而且对光照、颜色变化极其敏感,很容易翻车。

第二条是喂游戏暴露的状态向量。多数 2D 赛车游戏(比如 Gym 里的 CarRacing 简化版、或者你自己用 pygame 写的)能直接拿到车辆的位置、速度、朝向角、到赛道边界的距离等。把这些拼成一个十几维的向量,网络小、收敛快,几百到几千帧就能看到明显效果。

我一般会选第二条路起步。原因很直接:你要验证的是「神经网络能不能学会控制」,不是「卷积特征提取强不强」。用状态向量能把变量控制到最少,等闭环跑通了,再换成像素输入去挑战 CNN,那才是下一步的事。常见做法是先用状态向量把整条训练管线打通,再逐步替换输入。

2.2 网络结构:一个隐藏层就够,别一上来堆深

标题里说的是「自己实现神经网络」,那结构就别搞太复杂。对于状态向量输入、连续控制输出这个任务,一个「输入层 → 隐藏层(tanh 或 ReLU)→ 输出层」的前馈网络足够。隐藏层神经元数量从 16 到 64 之间试,输入维度十几维的话,32 个隐藏单元是个稳妥起点。

输出层要分两路理解:如果控制量是连续的(方向盘转角 -1 到 1,油门 0 到 1),输出层用 tanh 或 sigmoid 把值压到范围内;如果离散成「左/右/直行/加速/刹车」几个动作,输出层用 softmax 做分类。赛车游戏里连续控制更自然,我倾向连续输出。

为什么不用 LSTM 或一维卷积?因为单帧状态向量已经包含了决策所需的全部信息(马尔可夫性基本成立),加循环结构只会让反向传播变复杂、训练变慢。等你发现「只看当前帧会犹豫、需要历史速度趋势」时,再考虑 LSTM 也不迟。这是典型的「先能跑,再优化」。

2.3 学习信号从哪来:监督学习还是强化学习

这是最容易让人卡住的地方。神经网络要更新权重,得有「正确答案」或者「奖励」。赛车游戏里没有现成标签,所以主流有两条落地路径:

一是行为克隆。你先用键盘手动开几十圈,记录下每一帧的「状态 → 你的操作」,把它当成监督学习数据集,网络学的是「模仿人类」。优点是实现简单,就是一个回归/分类任务,反向传播你刚学的那套直接用;缺点是人的操作有噪声,且网络没见过的情况(比如冲出赛道后)不知道怎么救。

二是策略梯度这类强化学习。网络自己开,开得好给正奖励,撞墙给负奖励,用奖励加权更新动作概率。优点是能超越人类,缺点是方差大、训练慢、调参玄学。

对「自己实现神经网络」这个目标,我强烈建议从行为克隆起步。它把「神经网络实现」和「强化学习算法」两件事解耦了,你能专注把前向传播、损失函数、反向传播、梯度下降这四步用 numpy 写对。等这套跑通,再上策略梯度,你才有能力判断问题出在网络还是出在奖励设计。

3. 动手实现:numpy 手写前馈网络 + 反向传播

3.1 环境准备与依赖

只需要 numpy,画图可选 matplotlib。不装深度学习框架是刻意的,这样每一行梯度你都能对上公式。

pip install numpy matplotlib

如果你还没配好 Python 环境,用 python 3.8 及以上都行,numpy 装最新稳定版即可。这一步没有坑,装完import numpy不报错就继续。

3.2 定义网络结构与前向传播

先写一个两层的网络类,权重用 He 初始化(配合 ReLU)或 Xavier 初始化(配合 tanh)。这里用 tanh,因为输出层也要 tanh,风格统一。

import numpy as np class NeuralNet: def __init__(self, n_in, n_hidden, n_out): # Xavier 初始化,适配 tanh,避免一开始就饱和 self.W1 = np.random.randn(n_in, n_hidden) * np.sqrt(1.0 / n_in) self.b1 = np.zeros((1, n_hidden)) self.W2 = np.random.randn(n_hidden, n_out) * np.sqrt(1.0 / n_hidden) self.b2 = np.zeros((1, n_out)) def forward(self, x): # x: (batch, n_in) self.x = x self.z1 = x @ self.W1 + self.b1 # 隐藏层线性输出 self.a1 = np.tanh(self.z1) # 隐藏层激活 self.z2 = self.a1 @ self.W2 + self.b2 self.out = np.tanh(self.z2) # 输出压到 [-1, 1] return self.out

逻辑说明:forward里把中间变量z1/a1/z2都存下来,因为反向传播要用。输出用 tanh 是因为方向盘和油门都能映射到 [-1, 1],油门再线性变换到 [0, 1] 即可。参数上,n_in由你的状态向量维度决定,n_hidden从 32 起调,n_out等于控制量个数(比如转向 + 油门就是 2)。

3.3 反向传播与参数更新

行为克隆用均方误差当损失,反向传播就是链式法则一层层往回推。这里手写梯度,不调 autograd。

def backward(self, y_true, lr=0.01): batch = self.x.shape[0] # 输出层梯度:MSE 对 z2 的导数,tanh 导数 = 1 - tanh^2 dout = (self.out - y_true) / batch dz2 = dout * (1 - self.out ** 2) dW2 = self.a1.T @ dz2 db2 = np.sum(dz2, axis=0, keepdims=True) # 隐藏层梯度 da1 = dz2 @ self.W2.T dz1 = da1 * (1 - self.a1 ** 2) dW1 = self.x.T @ dz1 db1 = np.sum(dz1, axis=0, keepdims=True) # 梯度下降更新 self.W2 -= lr * dW2 self.b2 -= lr * db2 self.W1 -= lr * dW1 self.b1 -= lr * db1

逻辑说明:dout除以batch是取平均损失,避免梯度随批量大小放大。1 - out**2是 tanh 的导数,这一步写错是新手最常见的翻车点——很多人直接拿dout当dz2,网络就学不动。lr学习率从 0.01 起,太大震荡,太小收敛慢。参数更新用最朴素的 SGD,够用了。

3.4 采集人类操作数据

行为克隆的关键是数据。写一个循环,每帧读游戏状态,同时记录你的键盘操作。

import pygame # 假设游戏用 pygame def collect_data(env, episodes=20): X, Y = [], [] for ep in range(episodes): state = env.reset() done = False while not done: keys = pygame.key.get_pressed() steer = 0.0 if keys[pygame.K_LEFT]: steer = -1.0 if keys[pygame.K_RIGHT]: steer = 1.0 throttle = 1.0 if keys[pygame.K_UP] else 0.0 X.append(state) Y.append([steer, throttle]) state, done = env.step(steer, throttle) return np.array(X), np.array(Y)

逻辑说明:state是游戏给你的状态向量,Y是这一帧你的操作。注意steer和throttle的取值范围要和网络输出对齐,都归一化到 [-1, 1] 或 [0, 1]。参数上,episodes越多数据越好,但手动开 20 圈通常够起步。这里有个血泪经验:别一边开一边看手机,数据里混入大量「直行」样本,网络会学成只会往前冲。

3.5 训练循环与损失观察

把上面拼起来,跑训练。

net = NeuralNet(n_in=X.shape[1], n_hidden=32, n_out=2) for epoch in range(2000): idx = np.random.permutation(len(X)) for i in range(0, len(X), 64): batch_idx = idx[i:i+64] net.forward(X[batch_idx]) net.backward(Y[batch_idx], lr=0.01) if epoch % 100 == 0: pred = net.forward(X) loss = np.mean((pred - Y) ** 2) print(f"epoch {epoch}, loss {loss:.4f}")

逻辑说明:每个 epoch 打乱数据,按 64 一批做 mini-batch 梯度下降。loss用全量数据算,方便观察趋势。正常情况下 loss 会从零点几降到 0.01 以下。如果 loss 卡住不动,先查 tanh 导数那行有没有写错,再查学习率是不是太大导致震荡。

4. 避坑与排查:训练不收敛时先看这五条

4.1 现象:loss 一直不降,输出全是 0

原因:最常见是反向传播里 tanh 导数漏乘,或者输出层和标签的取值范围没对齐(比如标签是 0/1,网络输出是 -1/1)。解决:打印一批out和y_true对比,确认量纲一致;再手算一个样本的梯度,和代码输出对一遍。

4.2 现象:网络学会直行,一遇到弯就冲出去

原因:训练数据里直行样本远多于转弯样本,网络退化成「永远输出直行」的偷懒解。解决:采集数据时故意多跑弯道,或者对转弯样本做加权,让损失函数对转弯误差更敏感。

4.3 现象:训练 loss 很低,但实际开车一塌糊涂

原因:过拟合。你采集的数据覆盖的场景太窄,网络只是记住了那几帧。解决:增加数据多样性(不同速度、不同入弯角度),或者加一点权重衰减。行为克隆的分布偏移问题在这里就暴露了——网络没见过自己开出来的状态。

4.4 现象:梯度爆炸,权重变成 nan

原因:学习率太大,或者输入特征没归一化,某一维数值范围是几百,梯度直接飞了。解决:把所有输入特征归一化到 [-1, 1] 或标准化到均值 0 方差 1;学习率降到 0.001 再试。

4.5 现象:训练特别慢,一个 epoch 要几分钟

原因:用了全量梯度下降而不是 mini-batch,或者 numpy 没走 BLAS 加速。解决:确认批量大小在 32 到 128 之间;检查 numpy 是不是装的最优版本,矩阵乘法在底层会调用多线程。

5. 进阶技巧:用「专家纠正」把行为克隆的分布偏移压下去

行为克隆最大的软肋是分布偏移:训练时网络只见过人类开出来的状态,一旦它自己开偏了,进入没见过的状态,就不知道怎么救,越偏越远。有个成本很低但效果明显的技巧,叫 DAgger 式的专家纠正。

具体做法:让训练好的网络自己开,你在旁边盯着,一旦它要冲出赛道,你立刻接管并纠正,把「网络开偏的那一帧状态 + 你的纠正操作」也加进训练集。重复几轮,网络见过的状态分布就被逐步扩展到它自己会犯错的区域。

# 伪代码:一轮 DAgger net.forward(state) # 网络先给出动作 action = net.out[0] if about_to_crash(state): # 你判断要撞了 action = human_correction() # 你接管 X_extra.append(state) # 把这一帧和纠正动作存下来 Y_extra.append(action) # 把 X_extra/Y_extra 并入原数据集,重新训练

这个循环跑三五轮,你会发现网络在弯道的表现明显变稳。代价是你得再花时间盯着它开,但比重新采集几十圈数据划算得多。

验证方法上,别只看训练 loss。准备一个固定种子的测试赛道,让网络跑十次,统计平均完成圈数和撞墙次数。这两个指标比 loss 更能说明问题。我自己的习惯是:每次改完网络结构或学习率,都跑同一个测试赛道,记录完成率,只有完成率涨了才算真的改进,否则 loss 再低都是自欺欺人。

最后说个我踩过的坑:一开始我图省事,把方向盘和油门塞进一个输出层用同一个 tanh,结果油门永远到不了满值,因为 tanh 在接近 1 的地方梯度太小。后来把油门单独用 sigmoid 输出,问题就没了。网络结构上这种小细节,往往比调学习率更影响最终效果。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询