基于TensorFlow的端到端模仿学习:马里奥赛车自动驾驶入门实战
1. 项目缘起与整体设计思路
1.1 为什么选马里奥赛车做自动驾驶入门
很多人一听到“自动驾驶”四个字,第一反应是激光雷达、多传感器融合、高精地图那一套重资产方案,觉得离自己太远。但如果你只是想入门人工智能在自动驾驶方向的应用,完全没必要一上来就啃那些工业级系统。马里奥赛车这类竞速游戏,其实是一个被严重低估的练手场景。
原因很简单:它把自动驾驶的核心问题压缩到了一个极简的闭环里。你需要的输入只有屏幕画面,输出只有方向、油门、刹车这几个连续或离散的控制量,中间不需要考虑行人、红绿灯、复杂路口博弈。换句话说,它是一个端到端模仿学习的绝佳沙盒——你喂给它“人怎么开”的数据,它学着“自己怎么开”。
我当初选这个项目,就是因为它能让我在一台普通带独显的笔记本上,把“数据采集→模型训练→实时推理→闭环控制”这条链路完整跑通一遍。这条链路跑通了,你再去看那些真实的自动驾驶数据集和论文,会发现底层逻辑是相通的,只是规模和复杂度不同而已。
这个项目适合谁?适合已经会一点 Python、想找个具体项目把 TensorFlow 用起来的人;适合上完人工智能导论课但不知道知识能干嘛的学生;也适合想给自己的孩子演示“AI 到底怎么学会开车的”的家长。你不需要有深度学习调参经验,但需要有一点耐心,因为数据采集这一步会占用你不少时间。
1.2 整体技术方案选型与考量
整个项目的架构可以拆成四块:游戏环境、数据采集、模型训练、实时推理控制。我逐一说说选型背后的逻辑。
游戏环境这块,我用的是一个开源的马里奥赛车模拟环境。选它的理由很实际:它提供了 Python 接口,能直接读取屏幕帧、能程序化地注入按键,省去了我自己去截屏和模拟键盘的麻烦。如果你用的是其他竞速游戏,理论上也能做,但你需要自己解决“怎么拿到画面”和“怎么发按键”这两个工程问题,入门阶段没必要给自己加难度。
数据采集环节,核心思路是人类示范。我用手柄(或者键盘)自己开若干圈,程序在后台以固定频率同时记录“当前画面”和“我此刻的按键”。这里有个关键决策:记录频率选多少?我实测下来,每秒 10 到 15 帧比较合适。太低了,画面变化跨度大,模型学不准;太高了,相邻帧几乎一样,数据冗余严重,训练还慢。我最后定的是每 100 毫秒采一帧,也就是 10Hz。
模型这块,我用的是卷积神经网络。为什么不用循环网络或者强化学习?因为模仿学习的本质是“看到什么画面,就输出什么动作”,这是一个典型的图像到向量的映射问题,CNN 天生适合干这个。循环网络更适合处理时序依赖强的任务,而在这个场景里,单帧画面其实已经包含了足够的信息来判断该往哪打方向。强化学习当然也能做,但它需要设计奖励函数、需要大量试错,入门门槛高得多,不适合作为第一个项目。
推理控制环节,我用了一个滑动窗口平滑的技巧。模型每一帧都会输出一个预测动作,但如果你直接把每一帧的预测原样发给游戏,车子会抖得厉害,因为相邻帧的预测可能有细微跳变。我的做法是取最近 5 帧预测的众数(对于离散动作)或均值(对于连续动作),这样控制信号就稳多了。这个技巧在后面会详细讲。
1.3 环境搭建:TensorFlow 安装的坑与解法
说到 TensorFlow 安装,这可能是劝退新手的第一道坎。我见过太多人卡在版本不匹配、CUDA 装不上、import 报错这些问题上。这里我把我的安装路径完整说一遍。
首先明确一点:如果你没有 NVIDIA 显卡,直接装 CPU 版就行,别折腾 GPU。CPU 版训练这个项目虽然慢一点,但完全能跑通。安装命令就一行:
pip install tensorflow但如果你有 NVIDIA 显卡,想用 GPU 加速,那就需要关注版本匹配。截至我写这篇东西的时候,TensorFlow 2.5.0 是一个比较稳定的选择,它对应的 CUDA 是 11.2,cuDNN 是 8.1。你需要先确认自己的显卡驱动版本足够新,然后按顺序装 CUDA Toolkit 和 cuDNN,最后再装 TensorFlow。
注意:千万不要先装 TensorFlow 再装 CUDA,因为 TensorFlow 安装时会检查 CUDA 是否存在,顺序反了容易出问题。
装完之后,用这段代码验证 GPU 是否可用:
import tensorflow as tf print(tf.config.list_physical_devices('GPU'))如果输出是一个非空列表,说明 GPU 认到了。如果是空列表,说明要么没装好,要么驱动版本不对。这时候别急着重新装,先检查nvidia-smi命令能不能正常输出,如果能,说明驱动没问题,那就是 CUDA 或 cuDNN 的版本对不上。
关于 PyTorch 和 TensorFlow 的区别,顺便说一句我的看法。PyTorch 在学术界更流行,动态图写起来更顺手;TensorFlow 在工业部署上生态更成熟,尤其是 TensorFlow Lite 和 TensorFlow Serving 这套工具链。这个项目用 TensorFlow,主要是因为它对新手更友好一些,Keras 高层 API 几行代码就能搭一个 CNN。你要是已经习惯 PyTorch,用 PyTorch 复现这个项目也完全没问题,逻辑是一样的。
2. 核心细节解析与实操要点
2.1 数据采集:怎么采、采多少、采什么样
数据采集这个环节,看似简单,其实决定了模型效果的上限。我踩过的坑基本都集中在这里。
采多少?我一开始只开了 5 圈,大概采了 3000 多帧,训练出来的模型在直道上还行,一到弯道就冲出去。后来加到 20 圈,接近 15000 帧,效果明显好转。我的经验是:至少 15 圈,最好 30 圈,而且要有意识地多跑弯道,因为直道的数据模型很容易学会,弯道才是难点。
采什么样?这里有个关键原则:数据分布要覆盖你希望模型学会的所有场景。如果你采集的时候总是贴着内道走,模型就学不会外道过弯;如果你从来不刹车,模型就不知道刹车是什么。所以采集的时候要刻意多样化:有时候走内线,有时候走外线,有时候提前刹车,有时候晚刹车。这样模型才能学到更鲁棒的控制策略。
怎么采?我写了一个采集脚本,核心逻辑是一个循环:读当前帧→存下来→读当前按键→存下来→等待 100 毫秒→重复。按键的存储格式要注意,我用的是 one-hot 编码,比如“左+油门”是一个向量,“右+油门”是另一个向量。这样模型输出层用 softmax 激活,就能直接对应到各个动作组合的概率。
实操心得:采集的时候最好把画面缩小再存。原始画面如果是 1280x720,存下来又占空间又拖慢训练。我统一缩到 200x150 左右,既保留了足够的视觉信息,又大大降低了计算量。这个尺寸是我试出来的,再小就看不清弯道了,再大就浪费算力。
还有一个细节:画面要转灰度。彩色画面有三个通道,灰度只有一个通道,计算量直接降到三分之一,而且对于判断赛道位置来说,颜色信息其实没那么重要。我实测灰度图和彩色图的训练效果差别很小,但训练速度快了很多。
2.2 模型结构设计:为什么这么搭
模型结构这块,我用的是一个比较经典的 CNN 架构,参考了 NVIDIA 那篇著名的端到端自动驾驶论文的思路,但做了简化。整体结构是这样的:
第一层是卷积层,32 个 5x5 的卷积核,步长 2,激活函数用 ReLU。第二层也是卷积层,64 个 5x5 的卷积核,步长 2,ReLU。第三层还是卷积层,64 个 3x3 的卷积核,步长 1,ReLU。然后接一个 Flatten 层,把特征图拉平,再接一个 128 维的全连接层,最后输出层的大小等于动作组合的数量。
为什么这么设计?卷积层的作用是提取画面里的空间特征,比如赛道边缘、弯道方向。第一层用大步长是为了快速降维,减少计算量;后面的层用小步长是为了提取更精细的特征。全连接层的作用是把提取到的特征映射到具体的控制动作上。
注意:输出层不要用 softmax 如果做的是回归任务。我这里把动作离散化了,所以用 softmax 输出各个动作的概率。如果你想做连续控制(比如方向盘角度),那输出层就不用激活函数,损失函数也要换成均方误差。
损失函数我用的是交叉熵,优化器用 Adam,学习率设 0.001。这些参数都是比较常规的选择,没有特别调。训练的时候我留了 20% 的数据做验证集,用来监控过拟合。如果验证集损失开始上升而训练集损失还在下降,那就是过拟合了,需要加 Dropout 或者减少模型复杂度。
2.3 训练过程中的参数计算与调整
训练轮数(epoch)设多少?这个没有标准答案,要看你的数据量和模型复杂度。我的做法是设一个比较大的轮数(比如 50),然后开 EarlyStopping,当验证集损失连续 5 轮不下降就自动停。这样既不会欠拟合,也不会浪费时间。
Batch size 设多少?我用的 32。这个值太小了训练不稳定,太大了内存吃不消。32 是一个比较折中的选择。如果你显卡内存大,可以试试 64 或 128,训练会快一些。
学习率要不要调?我一开始用 0.001,后来发现训练后期损失下降很慢,就加了一个学习率衰减策略:每 10 轮把学习率乘以 0.5。这样前期学得快,后期学得细。这个技巧在大多数训练任务里都适用。
实操心得:训练的时候一定要把训练集和验证集的损失曲线画出来看。我见过太多人只看最终准确率,不看曲线,结果模型过拟合了都不知道。曲线图能告诉你很多信息:如果两条曲线都还在下降,说明可以继续训练;如果训练集下降但验证集上升,说明过拟合了;如果两条曲线都平了,说明训练到头了。
3. 实操过程与核心环节实现
3.1 完整实操流程概览
整个项目的实操流程可以分成六步:环境搭建→游戏环境接入→数据采集→模型训练→推理控制→效果调优。我按顺序把每一步的关键操作说清楚。
第一步环境搭建,前面已经讲过了,核心就是装好 TensorFlow 和必要的依赖库。除了 TensorFlow,你还需要装 numpy、opencv-python、pillow 这几个库,分别用来做数值计算、图像处理和图像读取。
第二步游戏环境接入,你需要找到你所用模拟环境的 Python 接口文档,搞清楚怎么读画面、怎么发按键。这一步因环境而异,但通常都有现成的示例代码可以参考。
第三步数据采集,这是最耗时间的一步。我写了一个采集脚本,运行之后你就开始开车,脚本在后台默默记录。采集完成后,你会得到一个包含画面和按键的数据集,通常存成 numpy 的 .npy 文件或者 hdf5 文件。
第四步模型训练,把采集到的数据喂给模型,训练若干轮,保存效果最好的模型权重。
第五步推理控制,加载训练好的模型,实时读取游戏画面,预测动作,发给游戏。
第六步效果调优,根据实际表现调整模型结构、训练参数、控制策略。
3.2 数据采集脚本的关键实现
数据采集脚本的核心是一个循环,我把它简化成伪代码给你看:
import cv2 import numpy as np import time frames = [] actions = [] while True: frame = get_screen() # 读取当前画面 frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 转灰度 frame = cv2.resize(frame, (200, 150)) # 缩放 action = get_current_key() # 读取当前按键 frames.append(frame) actions.append(action) time.sleep(0.1) # 等待100毫秒这里有几个细节要注意。get_screen()和get_current_key()这两个函数需要根据你的具体环境来实现。time.sleep(0.1)保证了采集频率是 10Hz,这个值可以根据实际情况调整。
采集完成后,把 frames 和 actions 存成文件:
np.save('frames.npy', np.array(frames)) np.save('actions.npy', np.array(actions))注意:采集的时候要确保画面和按键是同步的。如果你的读取画面和读取按键之间有延迟,会导致标签错位,模型学出来的东西就是错的。我的做法是先读画面,再读按键,两者之间的时间差控制在几毫秒以内。
3.3 模型训练脚本的关键实现
训练脚本的核心是搭模型、编译、fit。我把关键代码列出来:
import tensorflow as tf from tensorflow.keras import layers, models model = models.Sequential([ layers.Conv2D(32, (5, 5), strides=2, activation='relu', input_shape=(150, 200, 1)), layers.Conv2D(64, (5, 5), strides=2, activation='relu'), layers.Conv2D(64, (3, 3), strides=1, activation='relu'), layers.Flatten(), layers.Dense(128, activation='relu'), layers.Dense(num_actions, activation='softmax') ]) model.compile(optimizer=tf.keras.optimizers.Adam(0.001), loss='categorical_crossentropy', metrics=['accuracy']) history = model.fit(X_train, y_train, validation_split=0.2, epochs=50, batch_size=32, callbacks=[tf.keras.callbacks.EarlyStopping(patience=5)])这段代码里,num_actions是你的动作组合数量。input_shape要和你的画面尺寸匹配。validation_split=0.2表示留 20% 做验证。
训练完成后,保存模型:
model.save('mario_kart_model.h5')3.4 推理控制脚本的关键实现
推理控制脚本的核心是加载模型、读画面、预测、发按键:
model = tf.keras.models.load_model('mario_kart_model.h5') action_history = [] while True: frame = get_screen() frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) frame = cv2.resize(frame, (200, 150)) frame = frame.reshape(1, 150, 200, 1) / 255.0 prediction = model.predict(frame, verbose=0) action = np.argmax(prediction) action_history.append(action) if len(action_history) > 5: action_history.pop(0) smoothed_action = max(set(action_history), key=action_history.count) send_key(smoothed_action) time.sleep(0.05)这里我用了滑动窗口平滑,取最近 5 帧预测的众数作为最终动作。这个技巧能有效减少抖动。time.sleep(0.05)表示推理频率是 20Hz,比采集频率高一些,这样控制更及时。
实操心得:推理的时候,画面预处理要和训练时完全一致。我见过有人训练时用了灰度+缩放,推理时忘了转灰度,结果模型完全跑偏。这种低级错误一定要避免。
4. 常见问题与排查技巧实录
4.1 训练不收敛怎么办
训练不收敛是最常见的问题,表现是损失一直很高,或者损失震荡不下降。排查思路是这样的:
先检查数据。把几张训练画面和对应的标签打印出来看看,确认画面和按键是对应的。如果标签错了,模型当然学不会。再检查数据量,如果只有几百帧,那肯定不够,至少要几千帧。
再检查模型。如果模型太复杂而数据太少,会过拟合;如果模型太简单,会欠拟合。可以试试减少或增加层数。学习率也是一个常见问题,太大了会震荡,太小了会下降很慢。可以试试 0.0001 到 0.01 之间的几个值。
最后检查预处理。画面有没有归一化?标签有没有做 one-hot 编码?这些细节都会影响训练。
4.2 模型在直道上表现好但弯道冲出去
这是最典型的问题,根本原因是弯道数据不足。模型在直道上看到的画面多,学得好;弯道数据少,学得差。解决办法很简单:多采集弯道数据。你可以专门找一条弯道多的赛道,反复跑,把弯道数据补上来。
另一个可能的原因是模型容量不够。如果模型太小,它可能学不会弯道这种复杂的视觉模式。可以试试增加卷积层的通道数,或者加一层全连接层。
还有一个原因是控制延迟。如果你的推理频率太低,模型看到弯道的时候已经来不及打方向了。可以试试提高推理频率,或者用预测未来几帧的方式做提前量。
4.3 车子抖动厉害怎么办
抖动的原因是相邻帧的预测不一致。解决办法就是我前面说的滑动窗口平滑。窗口大小可以调,我用的 5,你可以试试 3 到 10 之间的值。窗口越大越平滑,但响应也越慢。这是一个权衡。
另一个减少抖动的方法是在训练数据里加入平滑。比如采集的时候,如果连续几帧的按键是一样的,就只保留一帧。这样模型学到的动作会更稳定。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决办法 |
|---|---|---|---|
| 损失不下降 | 学习率不当 | 打印损失曲线 | 调整学习率 |
| 损失震荡 | 学习率太大 | 观察损失波动 | 减小学习率 |
| 过拟合 | 数据太少或模型太复杂 | 对比训练/验证损失 | 加数据或加Dropout |
| 欠拟合 | 模型太简单 | 训练损失也很高 | 增加模型复杂度 |
| 弯道冲出去 | 弯道数据不足 | 统计弯道数据量 | 补采弯道数据 |
| 抖动厉害 | 预测不稳定 | 观察连续预测 | 滑动窗口平滑 |
| 推理跑偏 | 预处理不一致 | 对比训练/推理预处理 | 统一预处理流程 |
| GPU 用不了 | CUDA版本不匹配 | 检查nvidia-smi | 重装匹配版本 |
4.5 独家避坑技巧
第一个技巧:采集数据时开一个计时器。我一开始采集的时候没计时,开了多少圈完全凭感觉,结果数据量忽多忽少。后来我加了一个计时器,每采集 5 分钟就提醒自己休息一下,顺便检查数据量。这样采集过程更可控。
第二个技巧:训练时保存每个 epoch 的模型。不要只保存最终模型,因为最终模型可能过拟合了。保存每个 epoch 的模型,然后拿几个不同的模型去实测,选效果最好的那个。我实测下来,验证集损失最低的那个模型不一定实际表现最好,有时候中间某个 epoch 的模型反而更稳。
第三个技巧:推理时加一个“紧急刹车”逻辑。如果模型输出的动作置信度很低(比如最高概率不到 0.5),说明模型不确定该怎么做,这时候可以强制刹车或者保持上一帧的动作。这个逻辑能避免模型在陌生场景下做出离谱的操作。
第四个技巧:用多个赛道训练。如果你只用一条赛道训练,模型会记住这条赛道的特征,换一条赛道就废了。用多条赛道训练,模型才能学到更通用的驾驶策略。这个道理和真实自动驾驶一样,训练数据要多样化。
5. 项目扩展与进阶方向
5.1 从模仿学习到强化学习
模仿学习的上限是你的驾驶水平,你开得好,模型就好;你开得烂,模型就烂。如果你想突破这个上限,可以试试强化学习。强化学习的思路是让模型自己试错,通过奖励函数来引导它学会更好的驾驶策略。
具体到这个项目,你可以设计这样的奖励:每前进一米加一分,撞墙扣十分,跑完全程额外加一百分。然后让模型自己跑,跑得多了,它就能学会比人类更好的路线。当然,强化学习的训练时间比模仿学习长得多,可能需要跑几万圈才能收敛。
5.2 从单帧到时序模型
现在的模型只看单帧画面,没有利用时序信息。如果你把连续几帧画面叠在一起作为输入,模型就能感知到速度、加速度这些动态信息,控制会更精准。具体做法是把输入从(150, 200, 1)改成(150, 200, 4),四个通道分别是最近四帧的灰度图。
更进一步,你可以用 LSTM 或 GRU 来处理时序。把 CNN 提取的特征按时间顺序喂给 LSTM,LSTM 的输出再接全连接层。这样模型就能记住之前几帧的信息,做出更合理的决策。
5.3 从游戏到真实场景的迁移思考
这个项目虽然是在游戏里做的,但它的技术栈和真实自动驾驶是相通的。真实自动驾驶也是“感知→决策→控制”这个链路,只是感知用的传感器更多(摄像头、激光雷达、毫米波雷达),决策用的模型更复杂(多任务学习、规划算法),控制更精细(油门、刹车、转向的连续控制)。
如果你想往真实自动驾驶方向发展,这个项目是一个很好的起点。你可以把它当作一个“最小可行系统”,理解了它的每一个环节,再去学真实的自动驾驶系统,会容易很多。真实系统里那些复杂的部分,本质上都是在解决这个最小系统里没有覆盖到的边缘情况。
实操心得:我在做完这个项目之后,最大的收获不是学会了 TensorFlow,而是理解了“数据质量决定模型上限”这个道理。你喂给模型什么样的数据,模型就学成什么样。这个认知在我后来做其他 AI 项目的时候,反复被验证。
最后再分享一个小技巧:如果你想让模型学得更快,可以在训练前对数据做增强。比如把画面稍微左右平移、调整亮度、加一点噪声。这样相当于变相增加了数据量,模型会更鲁棒。我实测下来,加了数据增强之后,模型在陌生赛道上的表现明显更好。这个技巧在数据量不足的时候特别有用,你可以试试。