本章我们主要学习以下内容:

  1. 阅读自动驾驶论文
  2. 采集数据
  3. 根据论文搭建自动驾驶神经网络
  4. 训练模型
  5. 在仿真环境中进行自动驾驶

image.png

论文介绍

本文参考自2016年英伟达发表的论文《End to End Learning for Self-Driving Cars》
end2end.pdf
image.png
论文的核心思想是以图像为特征,以方向盘的转向角度为标签,通过深度学习来学习画面对应的方向盘角度.image.png
正如上图所示, 我们首先从中间摄像头中读取当前画面, 将读到的画面传输给卷积神经网络, 卷积神经网络提取到图片的特征,计算出方向盘转动的角度, 我们再根据角度控制汽车的方向盘.
在2016年自动驾驶研究火热的时候, 这是一篇相当有影响力的论文, 它现在已经成为入门自动驾驶必读的论文. 下面我们来看看它的网络结构
image.png

假设现在我们正在开车处于如下图所示的情况 作为一名经验丰富的老司机, 我们需要控制方向盘和油门. 很显然,在当前车道内,我们需要前方路口左转。 我们会控制方向盘往左转动, 同时我们需要降低油门,防止转弯速度过快跑出车道。
image.png
总结一下刚才老司机的步骤:

  1. 当前画面映入眼帘,
  2. 大脑飞速运转,得出需要左转的结论
  3. 手开始慢慢打方向盘
  4. 脚开始控制油门

这个过程如果用计算机来处理的话:

  1. 摄像头捕捉画面
  2. 画面经过神经网络处理,得出转弯角度
  3. 机械控制转弯角度
  4. 机械控制汽车速度

要想成为一名老司机,以应对各种可能出现的情况,我们要多开车,多见世面。 还是以开车为例,本地司机永远是最牛的司机,例如

  1. 武汉的公交司机,敢在水里开公交,因为他们频繁遭遇暴雨,已经掌握了开潜艇的技巧
  2. 重庆的出租车司机,完全不需要高德,百度的误导,因为这个地方他们比导航软件还熟
  3. 广州的出租车,在错综复杂的高架桥中高速穿行,因为他们已经积累到了丰富的经验,而新进广州的司机,可能分不清现在应该走在桥上还是桥下

我们要想让电脑成为一名老司机,我们需要按照如下步骤进行:

  1. 搜集数据: 画面 —— 转弯角度
  2. 训练模型:神经网络
  3. 验证模型:车里坐个人,时刻关注车辆运行的状态

数据采集

  1. import torch
  2. import csv
  3. import cv2 as cv
  4. from utils.image_utils import preprocess_bgr
  5. # 自定义数据集
  6. class AutoDriveDataset(torch.utils.data.Dataset):
  7. def __init__(self,csv_path,image_dir,transform=None):
  8. super(AutoDriveDataset,self).__init__()
  9. # 读取csv文件里面的数据
  10. self.samples = []
  11. with open(csv_path) as csvfile:
  12. reader = csv.reader(csvfile)
  13. for line in reader:
  14. self.samples.append(line)
  15. # 图片文件夹路径
  16. self.image_dir = image_dir
  17. # 图像预处理
  18. self.transform = transform
  19. def __getitem__(self, index):
  20. # 获取当前行的文本信息
  21. example = self.samples[index]
  22. # 图片路径
  23. center = example[0]
  24. # 方向盘的角度
  25. angle = example[3]
  26. # 获取图片信息
  27. image = cv.imread(center)
  28. image = preprocess_bgr(image)
  29. if self.transform:
  30. image = self.transform(image)
  31. return image,float(angle)
  32. def __len__(self):
  33. return len(self.samples)

模型定义

  1. import torch
  2. import torch.nn as nn
  3. # 定义模型
  4. class AutoDriveNet(torch.nn.Module):
  5. def __init__(self): # 原始图片320*120
  6. super(AutoDriveNet, self).__init__()
  7. # 神经网络 外部输入的数据为 3*66*200
  8. # 卷积核5x5 半径2: 66 - 2*2--> 62 -步长->31
  9. self.conv_layers = nn.Sequential(
  10. nn.Conv2d(3,24,kernel_size=5,stride=2), # 66-4/2=31 200-4/2= 98 158
  11. nn.ReLU(),
  12. nn.Conv2d(24, 36, kernel_size=5, stride=2), # 31 - 4 13.5 26 98-4 /2=47 77
  13. nn.ReLU(),
  14. nn.Conv2d(36, 48, kernel_size=5, stride=2), # 13.5-4 /2 4.5 47-4 /2=21.5 37
  15. nn.ReLU(),
  16. nn.Conv2d(48, 64, kernel_size=3, stride=1),# 4.5-2=2.5 21.5-2=19.5 37-2 = 35
  17. nn.ReLU(),
  18. nn.Conv2d(64, 64, kernel_size=3, stride=1),# 2.5-2=0.5 19.5-2 = 17.5 35-2 =33
  19. nn.Dropout(p=0.5), # 防止过拟合
  20. )
  21. # 线性层
  22. self.linear_layers = nn.Sequential(
  23. nn.Linear(64*1*18,100),
  24. nn.ReLU(),
  25. nn.Linear(100, 50),
  26. nn.ReLU(),
  27. nn.Linear(50, 10),
  28. nn.Linear(10, 1)
  29. )
  30. def forward(self,x):
  31. # 正向传播/前向传播 n 3*66*200
  32. out = x.view(x.size(0),3,66,200)
  33. out = self.conv_layers(out)
  34. # 将数据打平
  35. out = out.view(out.size(0),-1)
  36. out = self.linear_layers(out)
  37. return out

过拟合问题

过拟合是指模型只过分地匹配特定训练数据集,以至于对训练集外数据无良好地拟合及预测。其本质原因是模型从训练数据中学习到了一些统计噪声,即这部分信息仅是局部数据的统计规律,该信息没有代表性,在训练集上虽然效果很好,但未知的数据集(测试集)并不适用。

一句话讲就是过拟合在训练集中表现良好,但是在测试集中表现较差的一种现象
image.png

Dropout是正则化技术简单有趣且有效的方法,在神经网络很常用。其方法是:在每个迭代过程中,以一定概率p随机选择输入层或者隐藏层的(通常隐藏层)某些节点,并且删除其前向和后向连接(让这些节点暂时失效)。权重的更新不再依赖于有“逻辑关系”的隐藏层的神经元的共同作用,一定程度上避免了一些特征只有在特定特征下才有效果的情况,迫使网络学习更加鲁棒(指系统的健壮性)的特征,达到减小过拟合的效果。这也可以近似为机器学习中的集成bagging方法,通过bagging多样的的网络结构模型,达到更好的泛化效果。

torch.nn.Dropout(p=0.5, inplace=False)
训练过程中按照概率p随机地将输入张量中的元素置为0

evere channel will be zeroed out independently on every forward call.

Parameters:
p(float):每个元素置为0的概率,默认是0.5
inplace(bool):是否对原始张量进行替换

模型训练

  1. import torch
  2. import numpy as np
  3. import cv2 as cv
  4. import torch.nn as nn
  5. import csv
  6. from torch.utils.data.sampler import SubsetRandomSampler
  7. class AverageLoss(object):
  8. '''
  9. 计算每轮训练的平均loss
  10. '''
  11. def __init__(self):
  12. self.reset()
  13. def reset(self):
  14. self.val = 0
  15. self.avg = 0
  16. self.sum = 0
  17. self.count = 0
  18. def update(self, val, n=1):
  19. self.val = val
  20. self.sum += val * n
  21. self.count += n
  22. self.avg = self.sum / self.count
  23. import os
  24. import torch.backends.cudnn as cudnn
  25. import time
  26. from AutoDriveDataset import AutoDriveDataset
  27. from AutoDriveNet import AutoDriveNet
  28. from torchvision.transforms import transforms
  29. from torch.utils.tensorboard import SummaryWriter
  30. if __name__ == '__main__':
  31. # 模型训练:
  32. CSV_PATH = "d:/DATA/ml/driving/3/driving_log.csv"
  33. IMAGE_PATH = "d:/DATA/ml/driving/3/IMG"
  34. # CSV_PATH = "d:/DATA/ml/driving/merge/driving_log.csv"
  35. # IMAGE_PATH = "d:/DATA/ml/driving/merge/IMG"
  36. transform = transforms.Compose([transforms.Lambda(lambda x:(x/127.5) - 1.0)])
  37. # 数据集
  38. dataset = AutoDriveDataset(CSV_PATH,IMAGE_PATH,transform)
  39. total_size = len(dataset)
  40. print("总共的数据量:",total_size)
  41. # 将数据进行打散
  42. # 获取所有的数据的索引
  43. indexes = list(range(total_size))
  44. print(indexes)
  45. np.random.seed(1234)
  46. np.random.shuffle(indexes)
  47. print(indexes)
  48. # 按照一定比例划分: 训练集0.8, 验证集:0.2
  49. splitRatio = 0.8
  50. trainSize = int(total_size*splitRatio)
  51. trainIndexes = indexes[0:trainSize]
  52. valIndexes = indexes[trainSize:]
  53. print("训练集的索引:",trainIndexes)
  54. print("验证集的索引:",valIndexes)
  55. # 随机采样
  56. trainSampler = SubsetRandomSampler(trainIndexes)
  57. valSampler = SubsetRandomSampler(valIndexes)
  58. # 数据加载器
  59. trainDataLoder = torch.utils.data.DataLoader(dataset,batch_size=400,sampler=trainSampler)
  60. valDataLoder = torch.utils.data.DataLoader(dataset,batch_size=400,sampler=valSampler)
  61. writer = SummaryWriter()
  62. # 获取当前支持的设备
  63. if torch.cuda.is_available():
  64. device = torch.device("cuda:0")
  65. cudnn.benchmark = True
  66. else:
  67. device = torch.device("cpu")
  68. # 为了接着上一次的训练结果继续训练
  69. if os.path.exists("models/best-itheima_{}.pt"):
  70. model = torch.load("models/best-itheima.pt")
  71. else:
  72. model = AutoDriveNet()
  73. model.to(device)
  74. lossFunction = nn.MSELoss().to(device)
  75. optimzer = torch.optim.Adam(model.parameters(),lr=1e-4)
  76. best_loss = 10000.0
  77. for epoch in range(300):
  78. model.train()
  79. loss_epoch = AverageLoss()
  80. loss = None;
  81. for inputs,labels in trainDataLoder:
  82. # 为了提升运算效率,将数据的运算移动到GPU中
  83. inputs = inputs.float().to(device)
  84. labels = labels.float().to(device)
  85. # 1. 正向传播
  86. pred = model(inputs)
  87. # 2. 计算损失
  88. loss = lossFunction(pred,labels.unsqueeze(1))
  89. # 3. 反向传播
  90. loss.backward()
  91. # 4. 更新参数
  92. optimzer.step()
  93. # 5. 梯度清空
  94. optimzer.zero_grad()
  95. # 记录损失值
  96. loss_epoch.update(loss.item(), inputs.size(0))
  97. # 保存最有的loss
  98. if loss.item() < best_loss:
  99. best_loss = loss.item()
  100. value = int(time.time())
  101. torch.save(model,f"models/best-itheima{value}.pt")
  102. # 监控损失值变化
  103. writer.add_scalar('MSE_Loss', loss_epoch.avg, epoch)
  104. print('epoch:' + str(epoch) + ' MSE_Loss:' + str(loss_epoch.avg))
  105. print("训练已经完成...")
  106. torch.save(model,"last-itheima.pt")
  107. writer.close()

查看训练过程中的结果

  1. pip install tensorboard

在命令行中启动tensorboard

  1. tensorboard --logdir=runs

验证模型

  1. """
  2. 验证当前模型是否能够完成自动驾驶
  3. """
  4. import argparse
  5. import base64
  6. from datetime import datetime
  7. import os
  8. import shutil
  9. from AutoDriveNet import AutoDriveNet
  10. import numpy as np
  11. import socketio
  12. import eventlet
  13. import eventlet.wsgi
  14. from PIL import Image
  15. from flask import Flask
  16. from io import BytesIO
  17. import torch
  18. import torchvision.transforms as transforms
  19. import cv2
  20. import cv2 as cv
  21. import matplotlib.pyplot as plt
  22. import traceback
  23. eventlet.monkey_patch(socket=True, select=True)
  24. sio = socketio.Server(async_mode='eventlet')
  25. app = socketio.WSGIApp(sio)
  26. model = None
  27. prev_image_array = None
  28. transformations = transforms.Compose([transforms.Lambda(lambda x: (x / 127.5) - 1.0)])
  29. IMAGE_HEIGHT, IMAGE_WIDTH, IMAGE_CHANNELS = 66, 200, 3
  30. def preprocess(image):
  31. """
  32. 对图像进行预处理
  33. """
  34. image = image[60:-25, :, :]
  35. image = cv2.resize(image, (IMAGE_WIDTH, IMAGE_HEIGHT))
  36. image = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)
  37. return image
  38. # set min/max speed for our autonomous car
  39. MAX_SPEED = 13
  40. MIN_SPEED = 3
  41. # and a speed limit
  42. speed_limit = MAX_SPEED
  43. device = torch.device("cuda")
  44. @sio.on('telemetry')
  45. def telemetry1(sid, data):
  46. if data:
  47. # 当前的方向角度
  48. steering_angle = float(data["steering_angle"])
  49. # 当前的油门
  50. throttle = float(data["throttle"])
  51. # 当前车速
  52. speed = float(data["speed"])
  53. # The current image from the center camera of the car
  54. image = Image.open(BytesIO(base64.b64decode(data["image"])))
  55. try:
  56. with torch.no_grad():
  57. image = np.asarray(image) # from PIL image to numpy array
  58. image = preprocess(image) # apply the preprocessing
  59. image = np.array([image]) # the model expects 4D array
  60. #image = image.astype(np.float32)/255.0
  61. image = transformations(image)
  62. image = torch.Tensor(image).to(device)
  63. # 预测方向盘的角度
  64. steering_angle = model(image).view(-1).cpu().data.numpy()[0]
  65. global speed_limit
  66. if speed > speed_limit:
  67. speed_limit = MIN_SPEED # slow down
  68. else:
  69. speed_limit = MAX_SPEED
  70. # throttle = controller.update(float(speed))
  71. throttle = 1.0 - steering_angle ** 2 - (speed / speed_limit) ** 2
  72. # steering_angle = steering_angle*180/3.1415926
  73. print('{} {} {}'.format(steering_angle, throttle, speed))
  74. send_control(steering_angle, throttle)
  75. except Exception as e:
  76. print(traceback.format_exc())
  77. else:
  78. sio.emit('manual', data={}, skip_sid=True)
  79. @sio.on('connect')
  80. def connect(sid, environ):
  81. print("connect ", sid)
  82. send_control(0, 0)
  83. def send_control(steering_angle, throttle):
  84. sio.emit(
  85. "steer",
  86. data={
  87. 'steering_angle': steering_angle.__str__(),
  88. 'throttle': throttle.__str__()
  89. },
  90. skip_sid=True)
  91. """
  92. pip install eventlet==0.29.1 --force-reinstall
  93. pip install python-socketio==4.5.1 --force-reinstall
  94. pip install python-engineio==3.11.2 --force-reinstall
  95. """
  96. if __name__ == '__main__':
  97. model_path = "best-itheima.pt"
  98. model_path = "best-itheima222.pt"
  99. model_path = "models/best-itheima1697381912.pt"
  100. model = torch.load(model_path)
  101. model.eval()
  102. print(model)
  103. # 启动服务
  104. eventlet.wsgi.server(eventlet.listen(('', 4567)), app)