项目介绍
场景足够丰富,操作足够简单,有大量的交通工具和驾驶视角可供选择,游戏《Grand Theft Auto 5》是一个相对廉价且适合初级人工智能探索的自动驾驶试验场。
本AI(暂且称之为ScooterV2)借鉴了美国死宅Harrison Kinsley的Charles方案,只使用截图捕捉的画面以模拟摄像头数据作为AI的输入,并没有真实的智能驾驶所涉及的传感器与雷达数据。AI的决策过程目前只停留在CNN(AlexNet)对单张图片进行分类选择操作的阶段,尚未引入记忆,无法处理时间序列数据,因而相比于引入循环神经网络,目前的ScooterV2任然需要大量的数据进行fit训练(目前已完成的ScooterV3采用了强化学习,不需要任何训练数据集,但是由于驾驶场景过于复杂,尚未设计出完美的奖励机制,虽然降低了训练成本但是效果不如目前的V2版本)。
但由于机能限制和存储能力限制(其实是因为不想花太多训练时间,以及经常改方案、丢数据、丢模型),ScooterV2相对于Charles做了一些简化:
Charles的驾驶载体为民用车(GTA5抢劫神车Kuroma装甲轿车),视角为引擎盖视角(为了模拟真实的摄像头),设计目标为保证在当前道路上保持车道行驶的同时尽量避开障碍物(由于Kuroma装甲车速度很快,避开障碍物主要以变道的形式完成),且仿制出了许多真实的智能驾驶模块(前碰撞预警、障碍物探测、行人检测)。
我做的ScooterV2驶载体为摩托车(DoubleT),视角为第三人称视角(为了看到更大的场景区域,为了捕捉到的车道线斜率范围更大,期望以更小的样本量在更少的时间训练出足够好的效果),设计目标为在当前道路上保持车道行驶(使用Mod屏蔽了所有交通和行人),且没有设计其他模块(由于机能限制,串联其它CNN模块会成倍增加单帧处理时间,使得模型的测试效果不美观)。
模型的训练分为三个部分:
- 数据集制作:监督式学习,数据集分为input data和label,其中输入数据为经过灰度处理、区域屏蔽和大小缩放的图像数据,截取自1280720分辨率的GTA5游戏画面,缩放为16090的大小;标签数据为每一个图像样本对应的1*3规格的操作向量,分别代表向左、向右、前进(Press A/W/D)。在游戏中人为驾驶5小时,将每一帧图片以及其所对应的操作向量记录在数据集张量中。数据集分5批录制完成,对不同操作所对应的图片进行数量平衡(W:A:D = 8:1:1)打乱数据集后取1000张图片作为测试集,其他的为训练集。
- 训练模型:创建AlexNet初始网络,对保存下来的数据集进行拟合。总共进行了约240000次权值更新,学习耗时3天左右。
- 测试模型,用getkey函数与keycheck函数定义操作向量与按键联系,用训练好的AlexNet对捕捉到的图片进行分类预测,执行当前类别对应的按键操作以进行驾驶。
制作数据集
导入依赖库:
import numpy as np
import cv2
import time
from grabscreen import grab_screen
from getkeys import key_check
import os
cv2进行图像处理;time用来记录单帧的处理时间;grab_screen从现有的py文件中导入,作用是截取屏幕上的一定区域;key_check用来处理当前帧的操作按键,将其转化为向量。
定义屏蔽函数:
vertices = np.array([[1,60],[1,89],[159,89],[159,60],[80,35],], np.int32)
def roi(img, vertices):
mask = np.zeros_like(img)
cv2.fillPoly(mask, vertices, 255)
masked = cv2.bitwise_and(img, mask)
return masked
为了减少图像无用区域对训练过程的影响,需要将图像上方天空区域以及两侧街景进行涂黑屏蔽。
vertices定义了一个区域,roi函数将此区域外的像素用255灰度涂黑。
将操作转化为标签向量:
def keys_to_output(keys):
output = [0,0,0]
if 'A' in keys:
output[0] = 1
elif 'D' in keys:
output[2] = 1
else:
output[1] = 1
return output
若按键A,则标签向量为[1,0,0];
若按键d,则标签向量为[0,0,1];
否则,则标签向量为[0,1,0];
数据集录制:
file_name = 'training_data_X.npy'
if os.path.isfile(file_name):
print('File exists, loading previous data!')
training_data = list(np.load(file_name))
else:
print('File does not exist, starting fresh!')
training_data = []
初始化空数据集,其中'training_data_X.npy'中的X应用数字表示当前录制批次。
def collect():
for i in list(range(10))[::-1]:
print(i+1)
time.sleep(1)
last_time = time.time()
paused = False
while True:
if not paused:
screenshot = grab_screen(region=(0,32,1280,752))
print('Frame took {} seconds'.format(time.time()-last_time))
last_time = time.time()
screen = cv2.cvtColor(screenshot, cv2.COLOR_BGR2GRAY)
screen = cv2.resize(screen, (160,90))
screen = roi(screen, [vertices])
keys = key_check()
output = keys_to_output(keys)
training_data.append([screen,output])
cv2.imshow('window2',screen)
if cv2.waitKey(25) & 0xFF == ord('q'):
cv2.destroyAllWindows()
break
if len(training_data) % 5000 == 0:
print(len(training_data))
np.save(file_name,training_data)
keys = key_check()
if 'T' in keys:
if paused:
paused = False
print('unpaused!')
time.sleep(1)
else:
print('Pausing!')
paused = True
time.sleep(1)
- 加入倒计时,在执行程序与开始录制数据集之间留下10秒空余,用以调整姿态与视角。
- 截取大小为1280*720的游戏区域,并对其进行缩小、灰化和屏蔽操作。
- 用key_check提取当前操作按键,并用函数转化为标签向量,与处理过的图片一起append到数据集中。
- 每5000帧保存一次数据集;设置T为暂停键。
平衡数据:
import numpy as np
import pandas as pd
from collections import Counter
from numpy.random import shuffle
train_1 = np.load('training_data_1.npy')
print('done1')
train_2 = np.load('training_data_2.npy')
print('done2')
train_3 = np.load('training_data_3.npy')
print('done3')
train_4 = np.load('training_data_4.npy')
print('done4')
train_5 = np.load('training_data_5.npy')
print('done5')
train = np.concatenate([train_1,train_2,train_3,train_4,train_5])
lefts = []
rights = []
forwards = []
shuffle(train)
for data in train:
img = data[0]
choice = data[1]
if choice == [1,0,0]:
lefts.append([img,choice])
elif choice == [0,1,0]:
forwards.append([img,choice])
elif choice == [0,0,1]:
rights.append([img,choice])
else:
print('no matches')
forwards = forwards[:8*len(lefts)][:8*len(rights)]
lefts = lefts[:len(forwards)]
rights = rights[:len(forwards)]
final_data = forwards + lefts + rights
shuffle(final_data)
np.save('training_data_after_shuffle.npy', final_data)
将5批数据集合并在一起后进行随机排序,并依据不同的标签划分为3个数组,按一定的比例进行截取后合并再进行随机排序,最后保存为training_data_after_shuffle.npy文件