說是行為克隆,聽著高大上,其實(shí)原理很簡單。掛載在車蓋上的攝像頭拍攝一張圖片,網(wǎng)絡(luò)對(duì)其進(jìn)行回歸預(yù)測,輸出車輪轉(zhuǎn)向角。最終目標(biāo)就是使模擬器中的小車可以自己開,不能超出車道線。
訓(xùn)練數(shù)據(jù)就是好多好多張車輛運(yùn)行中攝像頭拍攝的圖片,而label就是各個(gè)圖片對(duì)應(yīng)的車輪轉(zhuǎn)向角,為[-25,25]中的值。
原理都很簡單,主要是我第一次是用的keras搭建的網(wǎng)絡(luò),并且使用generator來生成訓(xùn)練數(shù)據(jù),主要記錄這些內(nèi)容。
網(wǎng)絡(luò)結(jié)構(gòu)
使用的是NVIDIA發(fā)表的一篇端到端自動(dòng)駕駛論文中的網(wǎng)絡(luò)結(jié)構(gòu),網(wǎng)絡(luò)功能很強(qiáng)大,對(duì)于這個(gè)小項(xiàng)目綽綽有余了。網(wǎng)絡(luò)為五層卷積后接四層全連接,因?yàn)橹活A(yù)測轉(zhuǎn)向角,所以輸出只有一個(gè)神經(jīng)元。另外,在第一層卷積之前有兩個(gè)預(yù)處理步驟:1.因?yàn)閿z像頭拍攝的范圍還是挺大的,對(duì)于圖像中天空這一部分是不需要的,所以給它裁掉。還有攝像頭還會(huì)拍到一點(diǎn)車蓋,這個(gè)給它裁掉。輸入圖像是(160,320,3),我看了下,上面裁60行,下面裁20行就可以。2.之后對(duì)輸入normalize一下,這里只是簡單的除以255后減去0.5而已。
from keras.models import Sequential
from keras.layers import Flatten, Dense, Lambda, Dropout
from keras.layers.convolutional import Conv2D, Cropping2D
from keras.layers.pooling import MaxPooling2D
conv_trainable = True
model = Sequential()
model.add(Cropping2D(cropping=((60,20),(0,0)), input_shape=(160,320,3)))
model.add(Lambda(lambda x: x / 255.0 - 0.5))
model.add(Conv2D(24,(5,5),strides=(2,2),activation='relu', trainable=conv_trainable))
model.add(Conv2D(36,(5,5),strides=(2,2),activation='relu', trainable=conv_trainable))
model.add(Conv2D(48,(5,5),strides=(2,2),activation='relu', trainable=conv_trainable))
model.add(Conv2D(64,(3,3),activation='relu', trainable=conv_trainable))
model.add(Conv2D(64,(3,3),activation='relu', trainable=conv_trainable))
model.add(Flatten())
# 我訓(xùn)練的epoch不多,還沒過擬合,所以沒用dropout
# model.add(Dropout(0.5))
model.add(Dense(100,activation='relu'))
# model.add(Dropout(0.6))
model.add(Dense(50,activation='relu'))
model.add(Dense(10,activation='relu'))
model.add(Dense(1))
model.compile(loss='mse', optimizer='adam')
model.summary()
對(duì)于結(jié)構(gòu)這么簡單的網(wǎng)絡(luò),使用keras可是太方便了!
訓(xùn)練數(shù)據(jù)
訓(xùn)練數(shù)據(jù)是好多好多圖片,shape為(160,320,3)。所有圖片的信息都存在一個(gè)csv文件中,csv文件的每一行包括車上三個(gè)攝像頭拍攝圖片的地址(中,左,右),和此時(shí)車輪的轉(zhuǎn)向角。
在訓(xùn)練之前先要做個(gè)data augment。我只是對(duì)每張圖片都翻轉(zhuǎn)一下。翻轉(zhuǎn)后要記錄新的圖片地址和取了負(fù)值的轉(zhuǎn)向角,然后更新原來的csv文件。下面代碼完成這個(gè)操作:
import csv
import cv2
import numpy as np
# 本代碼由于augment data,對(duì)于所有圖片左右翻轉(zhuǎn)下
# 方法是通過csv文件中的地址分別讀取圖片,翻轉(zhuǎn)圖片,存儲(chǔ)新圖片,如此循環(huán)
# 在該過程中記錄新圖片地址,最后更新csv文件。
new_csv = []
with open('./driving_log.csv', 'r') as csvfile:
reader = csv.reader(csvfile)
# 注意line中所有數(shù)據(jù)都是字符串形式
for line in reader:
for i in range(3):
# 防止地址前后有空格,沒有什么特殊的含義
path = line[i].strip()
img = cv2.imread(path)
img = cv2.flip(img, 1)
# 翻轉(zhuǎn)后圖片的新名字
new_path = path.split('.jpg')[0] + '_r.jpg'
cv2.imwrite(new_path, img)
line[i] = new_path
# 因?yàn)榉D(zhuǎn)了,所以左圖右圖互換
line[1],line[2] = line[2], line[1]
line[3] = str(-1.0 * float(line[3]))
# 把新的信息存到列表里,后面更新csv文件
new_csv.append(line)
# 用open以寫方式打開csv文件需要指定newline=''
# 不然,寫的行與行之間會(huì)多一個(gè)空行
with open('./driving_log.csv', 'a', newline='') as csvfile:
writer = csv.writer(csvfile)
writer.writerows(new_csv)
下面代碼讀入csv文件,提取出每一行,shuffle后,分類訓(xùn)練、驗(yàn)證集。
lines = []
with open('./driving_log.csv') as csvfile:
# reader為一生成器
reader = csv.reader(csvfile)
for line in reader:
lines.append(line)
# sklearn的工具還是很好用的
from sklearn.utils import shuffle
lines = shuffle(lines)
from sklearn.model_selection import train_test_split
# 注意這種分法沒有完全分開同一時(shí)刻左中右三個(gè)攝像頭拍攝的圖片
train_samples, validation_samples = train_test_split(lines, test_size=0.2)
這時(shí)訓(xùn)練數(shù)據(jù)我記得是好五六萬張吧。一次都讀進(jìn)內(nèi)存電腦課吃不消。所以每訓(xùn)練一個(gè)batch再從硬盤中讀一個(gè)batch的圖片。所以這里shuffle的只是圖片的地址,到時(shí)候根據(jù)地址讀圖片就好了。
使用生成器訓(xùn)練網(wǎng)絡(luò)
需要構(gòu)造一個(gè)生成器,在訓(xùn)練時(shí)傳入到model.fit_generator中,生成器每次返回一個(gè)batch的數(shù)據(jù)。
# samples接收的就是保存了地址的列表
# 返回的batch全部在samples中。
def generator(samples, batch_size=32):
num_samples = len(samples)
# 要不斷生成數(shù)據(jù),所以無限循環(huán)
while 1:
# 每次大循環(huán)完該samples,也就是一個(gè)epoch,都shuffle下整個(gè)數(shù)據(jù)集
shuffle(samples)
# 在該for中每次yeild一個(gè)batch數(shù)據(jù)
for offset in range(0, num_samples, batch_size):
batch_samples = samples[offset:offset+batch_size]
images = []
labels = []
for batch_sample in batch_samples:
# 這里使用了左中右三個(gè)攝像頭的圖片
# 所以yeild的真正的數(shù)據(jù)量為batch*3
for i in range(3):
# stip就是防止地址前后的空格
# 讀不到圖片imread可不會(huì)報(bào)錯(cuò)
image = cv2.imread(batch_sample[i].strip())
images.append(image)
label = float(batch_sample[3])
label_left = label + correction
label_right = label - correction
labels.extend([label, label_left, label_right])
X_train = np.array(images)
y_train = np.array(labels)
# 這個(gè)shuffle我感覺沒太大用
yield shuffle(X_train, y_train)
上面只是生成器的定義,下面,要建立訓(xùn)練集和驗(yàn)證集的生成器對(duì)象,分別傳入之前分割好的列表即可:
batch_size = 16
train_generator = generator(train_samples, batch_size=batch_size)
validation_generator = generator(validation_samples, batch_size=batch_size)
這兩個(gè)生成器就是用于model.fit_generator()中的了。
訓(xùn)練!
直接調(diào)用model.fit_generator就好:
# 每一個(gè)epoch都保存一個(gè)模型
# 所以設(shè)置model.fit_generator中的epochs參數(shù)為1
# 并在循環(huán)中調(diào)用
# 每次fit結(jié)束后就保持模型。
for i in range(5):
history_object = model.fit_generator(generator = train_generator,
steps_per_epoch = int(len(train_samples)/batch_size),
epochs = 1,
validation_data = validation_generator,
validation_steps = int(len(validation_samples)/batch_size))
model.save('./model_%d.h5'%i)
用keras可太方便了,這要是用tensorflow可費(fèi)勁了。
上面說這個(gè)網(wǎng)絡(luò)結(jié)構(gòu)強(qiáng)大是有原因的,一開始我搭建網(wǎng)絡(luò)的時(shí)候忘記指定全連接層的轉(zhuǎn)移函數(shù)了!??!所以這四層全連接就相當(dāng)于一層線性層,但是,就算是這樣,使用第4個(gè)epoch之后得到的模型,小車也能非常穩(wěn)定的開。。。