系列文章目录
前言
小车收到后需要按照图示连接好,需要固定好相机,电源线,数据线等(如图所示)。
微雪的中文文档并没有很详细,因此以下作翻译。
注意:
- 我使用了官方提供的预训练模型,此模型泛化能力较好,可以避免频繁采集数据训练。
- 机器人连接好WiFi后实际运行之前要关闭图像界面
一、基本运动
在本笔记本中,我们将介绍控制 JetBot 的基础知识。
1.1 导入 Robot 类
要开始对 JetBot 编程,我们需要导入 Robot 类。通过该类,我们可以轻松控制机器人的电机!该类包含在 jetbot 软件包中。
如果你是 Python 的新手,那么软件包本质上就是一个包含代码文件的文件夹。这些代码文件被称为模块。
要导入 Robot 类,请选中下面的单元格并按下 ctrl + enter 或上面的 play 图标。这将执行单元格中包含的代码
from jetbot import Robot
现在,我们已经导入了 Robot 类,可以按如下方式初始化该类实例。
robot = Robot()
1.2 控制机器人
既然我们已经创建了名为 “robot” 的 Robot 实例,就可以用这个实例来控制机器人了。要让机器人以最大速度的 30% 逆时针旋转,我们可以调用以下命令
警告:下一条命令将使机器人移动!请确保机器人有空间。
robot.left(speed=0.3)
酷,你应该能看到机器人逆时针旋转!
如果机器人没有向左转,说明其中一个电机接反了!请尝试关闭机器人电源,然后调换接错电机的红色和黑色电缆的接线端子。
提醒: 请务必小心检查接线,不要在运行系统上更改接线!
现在,您可以调用 stop 方法来停止机器人。
robot.stop()
也许我们只想让机器人运行一段时间。为此,我们可以使用 Python time 包。
import time
该软件包定义了 sleep 函数,它可以在运行下一条命令前,使代码执行阻塞指定的秒数。试试下面的方法,让机器人只向左转半秒。
robot.left(0.3)
time.sleep(0.5)
robot.stop()
好极了 你应该能看到机器人向左转了一会儿,然后停了下来。
想知道 left 方法中的 speed= 发生了什么吗?Python 允许我们通过函数名或函数定义的顺序(不指定名称)来设置函数参数。
BasicJetbot 类还有 right、forward 和 backward 方法。请尝试创建自己的单元格,让机器人以 50%的速度向前移动一秒钟。
选中一个现有单元格,按 b 或上方的 + 图标,创建一个新单元格。完成后,输入你认为能让机器人以 50%的速度前进一秒的代码。
1.3 单独控制电机
上面我们介绍了如何使用左 left、右 right 等命令来控制机器人。但如果我们想单独设置每个电机的速度,该怎么办呢?有两种方法可以做到这一点
第一种方法是调用 set_motors 方法。例如,要沿左拱形方向转动一秒钟,我们可以将左侧电机的转速设置为 30%,右侧电机的转速设置为 60%,如下所示。
robot.set_motors(0.3, 0.6)
time.sleep(1.0)
robot.stop()
好极了!你应该能看到机器人沿着左移动。但实际上,我们还有另一种方法可以实现同样的效果。
Robot 类有两个名为 left_motor 和 right_motor 的属性,分别代表每个电机。这些属性是 Motor 类实例,每个实例都包含一个值属性。这个 value 属性是一个 traitlet,当被赋予一个新值时会产生events 。在电机类中,我们附加了一个函数,每当值发生变化时,该函数就会更新电机指令。
因此,为了完成与上述完全相同的操作,我们可以执行以下操作。
robot.left_motor.value = 0.3
robot.right_motor.value = 0.6
time.sleep(1.0)
robot.left_motor.value = 0.0
robot.right_motor.value = 0.0
你会看到机器人以同样的方式移动!
1.4 将电机链接到 Traitlets
这些 traitlets 的一个很酷的功能是,我们还可以将它们链接到其他 traitlets!这非常方便,因为 Jupyter Notebook 允许我们制作在引擎盖下使用 traitlets 的图形小部件 widgets。这意味着我们可以将电机附加到小工具 widgets上,以便从浏览器中控制它们,或者直接可视化它们的值。
为了展示如何做到这一点,让我们创建并显示两个滑块,用来控制电机。
import ipywidgets.widgets as widgets
from IPython.display import display
# create two sliders with range [-1.0, 1.0]
left_slider = widgets.FloatSlider(description='left', min=-1.0, max=1.0, step=0.01, orientation='vertical')
right_slider = widgets.FloatSlider(description='right', min=-1.0, max=1.0, step=0.01, orientation='vertical')
# create a horizontal box container to place the sliders next to each other
slider_container = widgets.HBox([left_slider, right_slider])
# display the container in this cell's output
display(slider_container)
你会看到上面显示了两个垂直滑块。
帮助提示:在 Jupyter Lab 中,你实际上可以将单元格的输出 “弹出 ”到完全独立的窗口中!它仍与笔记本相连,但会单独显示。如果我们想将执行的代码输出固定在其他地方,这将很有帮助。为此,右键单击单元格的输出,然后选择为输出创建新视图。然后,你可以将新窗口拖动到你喜欢的位置。
试着点击并上下拖动滑块。请注意,当前移动滑块时不会发生任何情况。这是因为我们还没有将它们连接到电机上!我们将使用 traitlets 软件包中的 link 功能来实现这一点。
import traitlets
left_link = traitlets.link((left_slider, 'value'), (robot.left_motor, 'value'))
right_link = traitlets.link((right_slider, 'value'), (robot.right_motor, 'value'))
现在试着拖动滑块(一开始要慢)。你应该会看到相应的电机转动!
我们上面创建的 link 函数实际上创建了一个双向链接!也就是说,如果我们在其他地方设置了电机值,滑块也会随之更新!试着执行下面的代码块
robot.forward(0.3)
time.sleep(1.0)
robot.stop()
您应该能看到滑块对电机指令做出响应!如果我们想移除此连接,可以调用每个链接的 unlink 方法。
left_link.unlink()
right_link.unlink()
但如果我们不需要双向链接呢?比方说,我们只想使用滑块显示电机值,但不想控制它们。为此,我们可以使用 dlink 功能。左侧输入为源 source,右侧输入为目标 target
left_link = traitlets.dlink((robot.left_motor, 'value'), (left_slider, 'value'))
right_link = traitlets.dlink((robot.right_motor, 'value'), (right_slider, 'value'))
现在试着移动滑块。你会发现机器人没有任何反应。但当使用其他方法设置电机时,滑块就会更新并显示数值!
1.5 为事件附加函数
另一种使用 traitlets 的方法是为事件附加函数(如 forward)。每当对象发生变化时,这些函数就会被调用,并传递一些有关变化的信息,如 old 旧值和 new 新值。
让我们创建并显示一些用于控制机器人的按钮。
# create buttons
button_layout = widgets.Layout(width='100px', height='80px', align_self='center')
stop_button = widgets.Button(description='stop', button_style='danger', layout=button_layout)
forward_button = widgets.Button(description='forward', layout=button_layout)
backward_button = widgets.Button(description='backward', layout=button_layout)
left_button = widgets.Button(description='left', layout=button_layout)
right_button = widgets.Button(description='right', layout=button_layout)
# display buttons
middle_box = widgets.HBox([left_button, stop_button, right_button], layout=widgets.Layout(align_self='center'))
controls_box = widgets.VBox([forward_button, middle_box, backward_button])
display(controls_box)
您应该会看到上面显示的一组机器人控件!但现在它们什么也做不了。为此,我们需要创建一些函数,并将其附加到按钮的 on_click 事件中。
def stop(change):
robot.stop()
def step_forward(change):
robot.forward(0.4)
time.sleep(0.5)
robot.stop()
def step_backward(change):
robot.backward(0.4)
time.sleep(0.5)
robot.stop()
def step_left(change):
robot.left(0.3)
time.sleep(0.5)
robot.stop()
def step_right(change):
robot.right(0.3)
time.sleep(0.5)
robot.stop()
现在我们已经定义了函数,让我们将它们附加到每个按钮的单击事件上
# link buttons to actions
stop_button.on_click(stop)
forward_button.on_click(step_forward)
backward_button.on_click(step_backward)
left_button.on_click(step_left)
right_button.on_click(step_right)
现在,当你点击每个按钮时,你应该能看到机器人在移动!
1.6 心跳开关
这里我们展示了如何连接 “心跳 ”来阻止机器人移动。这是一种检测机器人连接是否有效的简单方法。您可以调低下面的滑块,缩短心跳周期(以秒为单位)。如果浏览器之间无法在两个心跳周期内完成往返通信,心跳的 “status ”属性将被设置为 “dead”。一旦连接恢复,状态属性将恢复为 “alive”。
from jetbot import Heartbeat
heartbeat = Heartbeat()
# this function will be called when heartbeat 'alive' status changes
def handle_heartbeat_status(change):
if change['new'] == Heartbeat.Status.dead:
robot.stop()
heartbeat.observe(handle_heartbeat_status, names='status')
period_slider = widgets.FloatSlider(description='period', min=0.001, max=0.5, step=0.01, value=0.5)
traitlets.dlink((period_slider, 'value'), (heartbeat, 'period'))
display(period_slider, heartbeat.pulseout)
试着执行下面的代码来启动电机,然后降低滑块,看看会发生什么。您也可以尝试断开机器人或电脑的连接。
robot.left(0.2)
# now lower the `period` slider above until the network heartbeat can't be satisfied
1.7 结论
本示例笔记本就介绍到这里!希望您现在对自己的机器人移动编程有信心了:)
二、视觉避障
在本笔记本中,我们将使用训练好的模型来检测机器人是处于自由状态还是受阻状态,从而在机器人上启用避撞行为。
2.1 总体流程
在本例中,我们将收集一个图像分类数据集,用于帮助确保 JetBot 的安全!我们将教 JetBot 检测空闲和受阻两种情况。我们将使用这个人工智能分类器来防止 JetBot 进入危险区域。
2.1.1 步骤 1 - 收集 JetBot 的数据
- 通过导航至 http://<jetbot_ip_address>:8888 连接到您的机器人
- 使用默认密码 jetbot 登录
- 选择 “内核”->“关闭所有内核”... 关闭所有其他正在运行的笔记本电脑
- 导航至 ~/Notebooks/collision_avoidance/
- 打开并跟踪 data_collection.ipynb 笔记本
提示
我们提供了一个预训练模型,如果需要,您可以跳到步骤 3。该模型是使用带广角附件的 Raspberry Pi V2 摄像头在有限的数据集上训练出来的。
2.1.2 步骤 2 - 训练神经网络
方案 1 - 在 Jetson Nano 上训练
通过导航至 http://<jetbot_ip_address>:8888 连接到您的机器人
使用默认密码 jetbot 登录
在 “Jupyter Lab ”选项卡中,导航至 ~/collision_avoidance
打开并跟踪 train_model_resnet18.ipynb 笔记本
方案 2 - 在其他 GPU 机器上训练
连接到已安装 PyTorch 并运行 Jupyter Lab 服务器的 GPU 机器
将避撞训练笔记本上传到这台机器上
打开并跟踪 train_model_resnet18.ipynb 笔记本
2.1.3 步骤 3 - 在 Jetson Nano 上优化模型
通过导航连接到您的机器人 https://<jetbot_ip_address>:8888
使用默认密码 jetbot 登录
选择 “内核”->“关闭所有内核...”,关闭所有其他正在运行的笔记本电脑
导航至 ~/Notebooks/road_following
打开并跟踪 live_demo_resnet18_build_trt.ipynb 笔记本,使用 TensorRT 优化模型
2.1.4 步骤 4 - 在 JetBot 上运行实时演示
通过导航至 http://<jetbot_ip_address>:8888 连接到您的机器人
使用默认密码 jetbot 登录
选择 “内核”->“关闭所有内核”... 关闭所有其他正在运行的笔记本电脑
导航至 ~/Notebooks/collision_avoidance
打开并跟随 live_demo_resnet18_trt.ipynb 笔记本运行优化模型
注意事项
JetBot 会在本笔记本中移动,请确保它有足够的活动空间。
2.2 实时演示(运行此示例即可)
2.2.1 加载训练好的模型
我们假设你已经按照训练笔记本中的说明将 best_model.pth 下载到了工作站。现在,你应该使用 Jupyter Lab 上传工具将模型上传到这个笔记本的目录中。上传完成后,本笔记本目录中就会出现一个名为 best_model.pth 的文件。
在调用下一单元之前,请确保文件已完全上传
(模型文件已经下载好了,在 notebook 中可以看到)
执行下面的代码初始化 PyTorch 模型。这看起来应该与训练笔记本非常相似。
import torch
import torchvision
model = torchvision.models.alexnet(pretrained=False)
model.classifier[6] = torch.nn.Linear(model.classifier[6].in_features, 2)
接下来,从上传的 best_model.pth 文件中加载训练好的权重
model.load_state_dict(torch.load('best_model.pth'))
目前,模型权重位于 CPU 内存中,执行下面的代码将其传输到 GPU 设备中。
device = torch.device('cuda')
model = model.to(device)
2.2.2 创建预处理函数
现在我们已经加载了模型,但有一个小问题。我们训练模型的格式与摄像机的格式并不完全一致。为此,我们需要进行一些预处理。这包括以下步骤
- 从 BGR 转换为 RGB
- 从 HWC 布局转换为 CHW 布局
- 使用与训练时相同的参数进行归一化处理(我们的摄像头提供的数值范围为 [0, 255],而训练时加载的图像范围为 [0, 1],因此我们需要缩放 255.0
- 将数据从 CPU 内存传输到 GPU 内存
- 添加批次维度
import cv2
import numpy as np
mean = 255.0 * np.array([0.485, 0.456, 0.406])
stdev = 255.0 * np.array([0.229, 0.224, 0.225])
normalize = torchvision.transforms.Normalize(mean, stdev)
def preprocess(camera_value):
global device, normalize
x = camera_value
x = cv2.cvtColor(x, cv2.COLOR_BGR2RGB)
x = x.transpose((2, 0, 1))
x = torch.from_numpy(x).float()
x = normalize(x)
x = x.to(device)
x = x[None, ...]
return x
好极了!我们现在已经定义了预处理函数,它可以将图像从摄像机格式转换为神经网络输入格式。
现在,让我们启动并显示摄像头。你现在应该对此非常熟悉了。我们还将创建一个滑块,显示机器人被阻挡的概率。我们还将显示一个滑块,用于控制机器人的基本速度。
import traitlets
from IPython.display import display
import ipywidgets.widgets as widgets
from jetbot import Camera, bgr8_to_jpeg
camera = Camera.instance(width=224, height=224)
image = widgets.Image(format='jpeg', width=224, height=224)
blocked_slider = widgets.FloatSlider(description='blocked', min=0.0, max=1.0, orientation='vertical')
speed_slider = widgets.FloatSlider(description='speed', min=0.0, max=0.5, value=0.0, step=0.01, orientation='horizontal')
camera_link = traitlets.dlink((camera, 'value'), (image, 'value'), transform=bgr8_to_jpeg)
display(widgets.VBox([widgets.HBox([image, blocked_slider]), speed_slider]))
我们还将创建机器人实例,我们需要用它来驱动电机。
from jetbot import Robot
robot = Robot()
接下来,我们将创建一个函数,每当摄像头的值发生变化时就会调用该函数。该函数将执行以下步骤
- 预处理摄像头图像
- 执行神经网络
- 当神经网络的输出显示我们被挡住时,我们就向左转,否则就向前走。
import torch.nn.functional as F
import time
def update(change):
global blocked_slider, robot
x = change['new']
x = preprocess(x)
y = model(x)
# we apply the `softmax` function to normalize the output vector so it sums to 1 (which makes it a probability distribution)
y = F.softmax(y, dim=1)
prob_blocked = float(y.flatten()[0])
blocked_slider.value = prob_blocked
if prob_blocked < 0.5:
robot.forward(speed_slider.value)
else:
robot.left(speed_slider.value)
time.sleep(0.001)
update({'new': camera.value}) # we call the function once to initialize
酷 我们已经创建了神经网络执行函数,但现在需要将其连接到摄像头上进行处理。
我们使用观察函数来实现这一目标。
警告:此代码可能会移动机器人 调整我们之前定义的速度滑块,控制机器人的基本速度。有些套件会移动得很快,因此开始时速度要慢,然后逐渐增加数值。
camera.observe(update, names='value') # this attaches the 'update' function to the 'value' traitlet of our camera
真棒!如果您的机器人已经插上电源,那么它现在应该会随着每一帧新的坐标系生成新的指令。也许您可以先将机器人放在地面上,看看它在遇到障碍物时会做什么。
如果您想停止这种行为,可以执行下面的代码来取消回调。
import time
camera.unobserve(update, names='value')
time.sleep(0.1) # add a small sleep to make sure frames have finished processing
robot.stop()
也许您想让机器人在不向浏览器传输视频流的情况下运行。您可以像下面这样取消链接摄像头。
camera_link.unlink() # don't stream to browser (will still run camera)
要继续流媒体播放,请调用
camera_link.link() # stream to browser (wont run camera)
同样,让我们正确关闭摄像头连接,以便在其他笔记本上使用摄像头。
camera.stop()
2.2.3 结束语
本次现场演示到此结束!希望您玩得开心,您的机器人能聪明地避免碰撞!
如果您的机器人没有很好地避免碰撞,请试着找出它的失误之处。这样做的好处是,我们可以收集更多关于这些故障场景的数据,这样机器人就会变得更好了:)
三、采集数据使用 ResNet-18 训练模型的步骤
3.1 数据收集 - data_collection.ipynb
如果您已经完成了基本运动笔记本的学习,希望您已经喜欢上了让机器人移动起来是多么简单的一件事!这很酷!但更酷的是让 JetBot 自己动起来!
这是一项超难的任务,有许多不同的方法,但整个问题通常会被分解成更容易的子问题。可以说,要解决的最重要的子问题之一,就是防止机器人进入危险环境!我们称之为避免碰撞。
在这组笔记本中,我们将尝试使用深度学习和一个非常通用的传感器:摄像头来解决这个问题。你将看到,通过神经网络、摄像头和英伟达 Jetson Nano,我们可以教会机器人一种非常有用的行为!
我们避免碰撞的方法是在机器人周围创建一个虚拟的 “安全气泡(safety bubble)”。在这个安全气泡内,机器人可以绕圈旋转,而不会撞到任何物体(或其他危险情况,如从窗台跌落)。
当然,机器人受到其视野范围的限制,我们无法阻止机器人身后放置物体等。但我们可以防止机器人自己进入这些场景。
我们的方法非常简单:
首先,我们会手动将机器人放置在其 “安全气泡 ”受到侵犯的场景中,并标注这些场景已被阻挡(blocked)。我们会将机器人看到的场景快照和标签一起保存下来。
其次,我们将手动把机器人放置在可以安全前进的场景中,并标注这些场景为自由(free)。同样,我们也会保存一张快照和这个标签。
这就是我们在本笔记本中要做的全部工作:收集数据。一旦我们有了大量的图像和标签,我们就会将这些数据上传到支持 GPU 的机器上,在那里我们将训练一个神经网络,根据它所看到的图像来预测机器人的安全气泡是否受到侵犯。最后,我们将利用这一点来实现简单的防碰撞行为:)
重要提示:当 JetBot 原地旋转时,它实际上是围绕两个轮子之间的中心旋转,而不是围绕机器人底盘本身的中心旋转。当您试图估算机器人的安全气泡是否被破坏时,这是一个需要记住的重要细节。不过不用担心,您不一定要精确。如果有疑问,最好偏向谨慎的一方(一个大的安全气泡)。我们要确保 JetBot 不会进入原地打转也无法摆脱的场景。
3.1.1 显示实时摄像头画面
让我们开始吧。首先,让我们像在远程操作笔记本中那样初始化并显示摄像头。
我们的神经网络将 224x224 像素的图像作为输入。我们将把摄像头设置为这一大小,以尽量减小数据集的文件大小(我们已经测试过这一方法适用于本任务)。在某些情况下,以更大的图像尺寸收集数据,然后再缩小到所需的尺寸可能会更好。
import traitlets
import ipywidgets.widgets as widgets
from IPython.display import display
from jetbot import Camera, bgr8_to_jpeg
camera = Camera.instance(width=224, height=224)
image = widgets.Image(format='jpeg', width=224, height=224) # this width and height doesn't necessarily have to match the camera
camera_link = traitlets.dlink((camera, 'value'), (image, 'value'), transform=bgr8_to_jpeg)
display(image)
太棒了,接下来让我们创建几个目录来存储所有数据。我们将创建一个文件夹 dataset,其中包含两个子文件夹 free 和 blocked,我们将在其中放置每个场景的图片。
import os
blocked_dir = 'dataset/blocked'
free_dir = 'dataset/free'
# we have this "try/except" statement because these next functions can throw an error if the directories exist already
try:
os.makedirs(free_dir)
os.makedirs(blocked_dir)
except FileExistsError:
print('Directories not created because they already exist')
如果刷新左侧的 Jupyter 文件浏览器,就会看到这些目录。接下来,让我们创建并显示一些按钮,用来保存每个类别标签的快照。我们还将添加一些文本框,用于显示我们目前已经收集到的每个类别的图片数量。这很有用,因为我们要确保收集到的 free 图片数量与 blocked 图片数量相当。这也有助于了解我们总共收集了多少张图片。
button_layout = widgets.Layout(width='128px', height='64px')
free_button = widgets.Button(description='add free', button_style='success', layout=button_layout)
blocked_button = widgets.Button(description='add blocked', button_style='danger', layout=button_layout)
free_count = widgets.IntText(layout=button_layout, value=len(os.listdir(free_dir)))
blocked_count = widgets.IntText(layout=button_layout, value=len(os.listdir(blocked_dir)))
display(widgets.HBox([free_count, free_button]))
display(widgets.HBox([blocked_count, blocked_button]))
现在,这些按钮不会做任何事情。我们必须在按钮的 on_click 事件中附加为每个类别保存图片的函数。我们将保存 Image 图像部件的值(而不是相机),因为它已经是压缩的 JPEG 格式!
为了确保不重复任何文件名(即使是在不同的机器上!),我们将使用 python 中的 uuid 软件包,它定义了 uuid1 方法来生成唯一标识符。这个唯一标识符由当前时间和机器地址等信息生成。
from uuid import uuid1
def save_snapshot(directory):
image_path = os.path.join(directory, str(uuid1()) + '.jpg')
with open(image_path, 'wb') as f:
f.write(image.value)
def save_free():
global free_dir, free_count
save_snapshot(free_dir)
free_count.value = len(os.listdir(free_dir))
def save_blocked():
global blocked_dir, blocked_count
save_snapshot(blocked_dir)
blocked_count.value = len(os.listdir(blocked_dir))
# attach the callbacks, we use a 'lambda' function to ignore the
# parameter that the on_click event would provide to our function
# because we don't need it.
free_button.on_click(lambda x: save_free())
blocked_button.on_click(lambda x: save_blocked())
太好了!现在,上面的按钮应该会将图片保存到 free 和 blocked 的目录中。您可以使用 Jupyter Lab 文件浏览器查看这些文件!
现在继续收集数据
- 将机器人置于受阻的场景中,然后按添加受阻
- 将机器人放置在空闲场景中,按添加空闲
- 重复 1、2
提醒:右键单击单元格,然后单击 “为输出创建新视图”,即可将部件移动到新窗口。或者,你也可以像下面一样将它们重新显示在一起。
以下是一些标注数据的提示
- 尝试不同的方向
- 尝试不同的光照
- 尝试不同的物体/碰撞类型;墙壁、壁架、物体
- 尝试不同纹理的地板/物体;花纹、光滑、玻璃等。
归根结底,我们掌握的机器人在真实世界中会遇到的场景数据越多,我们的避撞行为就会越好。重要的是要获取各种数据(如上述提示所述),而不仅仅是大量数据,但您可能需要每类至少 100 张图片(这并不科学,只是一个有用的提示)。不过不用担心,一旦你开始做,很快就能完成:)
display(image)
display(widgets.HBox([free_count, free_button]))
display(widgets.HBox([blocked_count, blocked_button]))
让我们再次正确关闭摄像头连接,以便在后面的笔记本中使用摄像头。
camera.stop()
下一步
收集到足够的数据后,我们需要将数据复制到 GPU 桌面或云计算机上进行训练。首先,我们可以调用以下终端命令,将数据集文件夹压缩成一个 zip 文件。
前缀 ! 表示我们要以 shell(或终端)命令的形式运行该单元。
下面 zip 命令中的 -r 标志表示递归,这样我们就包含了所有嵌套文件,而 -q 标志表示静音,这样 zip 命令就不会打印任何输出了
!zip -r -q dataset.zip dataset
你应该在 Jupyter Lab 文件浏览器中看到一个名为 dataset.zip 的文件。您应使用 Jupyter Lab 文件浏览器右键单击并选择 “下载(Download) ”来下载该压缩文件。
接下来,我们需要将这些数据上传到 GPU 桌面或云计算机(我们称之为主机),以训练防碰撞神经网络。我们假设您已经按照 JetBot WiKi 中的说明设置了训练机器。如果已经设置,则可以导航至 http://<host_ip_address>:8888 以打开主机上运行的 Jupyter Lab 环境。您需要打开的笔记本名为 collision_avoidance/train_model.ipynb。
因此,请前往你的训练机器,并按照那里的说明进行操作!模型训练完成后,我们将返回机器人 Jupyter Lab 环境,使用模型进行现场演示!
3.2 训练模型 (ResNet18) -
train_model_resnet18.ipynb
欢迎使用主机端 Jupyter 笔记本!如果你看过在机器人上运行的笔记本,应该会觉得很熟悉。在本笔记本中,我们将对图像分类器进行训练,以检测 free 和 blocked 两个类别,从而避免碰撞。为此,我们将使用流行的深度学习库 PyTorch
import torch
import torch.optim as optim
import torch.nn.functional as F
import torchvision
import torchvision.datasets as datasets
import torchvision.models as models
import torchvision.transforms as transforms
3.2.1 上传并提取数据集
开始之前,您应上传在机器人上的 data_collection.ipynb 笔记本中创建的 dataset.zip 文件。
然后,您应调用以下命令提取该数据集
!unzip -q dataset.zip
你应该会看到文件浏览器中出现一个名为数据集的文件夹。
3.2.2 创建数据集实例
现在,我们使用 torchvision.datasets 包中的 ImageFolder 数据集类。我们从 torchvision.transforms 包中附加转换,为训练准备数据。
dataset = datasets.ImageFolder(
'dataset',
transforms.Compose([
transforms.ColorJitter(0.1, 0.1, 0.1, 0.1),
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
)
3.2.3 将数据集分成训练集和测试集
接下来,我们将数据集分成训练集和测试集。测试集将用于验证我们训练的模型的准确性。
train_dataset, test_dataset = torch.utils.data.random_split(dataset, [len(dataset) - 50, 50])
3.2.4 创建数据加载器以批量加载数据
我们将创建两个 DataLoader 实例,它们提供了用于洗牌数据、生成批量图像以及与多个工作者并行加载样本的实用程序。
train_loader = torch.utils.data.DataLoader(
train_dataset,
batch_size=8,
shuffle=True,
num_workers=0,
)
test_loader = torch.utils.data.DataLoader(
test_dataset,
batch_size=8,
shuffle=True,
num_workers=0,
)
3.2.5 定义神经网络
现在,我们定义要训练的神经网络。torchvision 软件包提供了一系列预训练模型供我们使用。
在一个称为迁移学习的过程中,我们可以将预先训练好的模型(在数百万张图像上训练好的)重新用于数据量可能少得多的新任务。
在最初训练预训练模型时学习到的重要特征可以重新用于新任务。我们将使用 resnet18 模型。
model = models.resnet18(pretrained=True)
resnet18 模型最初是针对有 1000 个类别标签的数据集训练的,但我们的数据集只有两个类别标签!我们将用一个新的、未经训练的、只有两个输出的层替换最后一层。
model.fc = torch.nn.Linear(512, 2)
最后,我们将模型转移到 GPU 上执行
device = torch.device('cuda')
model = model.to(device)
3.2.6 训练神经网络
使用下面的代码,我们将对神经网络进行 30 个历元的训练,在每个历元后保存性能最好的模型。
一个 epoch 是对数据的一次完整运行。
NUM_EPOCHS = 30
BEST_MODEL_PATH = 'best_model_resnet18.pth'
best_accuracy = 0.0
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
for epoch in range(NUM_EPOCHS):
for images, labels in iter(train_loader):
images = images.to(device)
labels = labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = F.cross_entropy(outputs, labels)
loss.backward()
optimizer.step()
test_error_count = 0.0
for images, labels in iter(test_loader):
images = images.to(device)
labels = labels.to(device)
outputs = model(images)
test_error_count += float(torch.sum(torch.abs(labels - outputs.argmax(1))))
test_accuracy = 1.0 - float(test_error_count) / float(len(test_dataset))
print('%d: %f' % (epoch, test_accuracy))
if test_accuracy > best_accuracy:
torch.save(model.state_dict(), BEST_MODEL_PATH)
best_accuracy = test_accuracy
完成后,你应该会在 Jupyter Lab 文件浏览器中看到 best_model_resnet18.pth 文件。选择右击(Right click) -> 下载(Download),将模型下载到工作站中
3.3 为实时演示建立 TensorRT 模型 -
live_demo_resnet18_build_trt.ipynb
在本笔记本中,我们将使用训练好的模型来检测机器人是处于自由状态还是受阻状态,从而在机器人上启用避撞行为。
3.3.1 加载训练好的模型
我们假设您已经按照训练笔记本中的说明将 best_model.pth 下载到工作站。现在,您应该使用 Jupyter Lab 上传工具将模型上传到本笔记本的目录中。上传完成后,本笔记本目录中就会出现一个名为 best_model.pth 的文件。
在调用下一单元之前,请确保文件已完全上传
执行下面的代码初始化 PyTorch 模型。这看起来应该与训练笔记本非常相似。
import torch
import torchvision
model = torchvision.models.resnet18(pretrained=False)
model.fc = torch.nn.Linear(512, 2)
model = model.cuda().eval().half()
接下来,从上传的 best_model_resnet18.pth 文件中加载训练好的权重
model.load_state_dict(torch.load('best_model_resnet18.pth'))
目前,模型权重位于 CPU 内存中,执行下面的代码将其传输到 GPU 设备中。
device = torch.device('cuda')
3.3.2 TensorRT
如果您的系统没有安装 torch2trt,则需要首先安装 torch2trt,方法是在控制台中执行以下命令。
cd $HOME git clone https://github.com/NVIDIA-AI-IOT/torch2trt cd torch2trt sudo python3 setup.py install
使用 torch2trt 转换和优化模型,以便更快地使用 TensorRT 进行推理。更多详情,请参阅 torch2trt readme。
这个优化过程可能需要几分钟才能完成。
from torch2trt import torch2trt
data = torch.zeros((1, 3, 224, 224)).cuda().half()
model_trt = torch2trt(model, [data], fp16_mode=True)
使用下面的单元格保存优化后的模型
torch.save(model_trt.state_dict(), 'best_model_trt.pth')
下一步
打开 live_demo_resnet18_build_trt.ipynb,使用 TensorRT 优化模型移动 JetBot。
3.4 实时演示(TensorRT)
在本笔记本中,我们将使用训练好的模型来检测机器人是处于自由状态(free)还是受阻状态(blocked),从而在机器人上启用防碰撞行为。
3.4.1 TensorRT
import torch
device = torch.device('cuda')
执行以下单元格,加载优化后的模型
import torchvision
from torch2trt import TRTModule
model_trt = TRTModule()
model_trt.load_state_dict(torch.load('best_model_trt.pth'))
3.4.2 创建预处理函数
现在我们已经加载了模型,但有一个小问题。我们训练模型的格式与摄像机的格式并不完全一致。为此,我们需要进行一些预处理。这包括以下步骤
- 将 HWC 布局转换为 CHW 布局
- 使用与训练时相同的参数进行归一化处理(我们的摄像头提供的数值范围为 [0, 255],而训练时加载的图像范围为 [0, 1],因此我们需要按 255.0 缩放)。
- 将数据从 CPU 内存传输到 GPU 内存
- 添加批次维度
import torchvision.transforms as transforms
import torch.nn.functional as F
import cv2
import PIL.Image
import numpy as np
mean = torch.Tensor([0.485, 0.456, 0.406]).cuda().half()
std = torch.Tensor([0.229, 0.224, 0.225]).cuda().half()
normalize = torchvision.transforms.Normalize(mean, std)
def preprocess(image):
image = PIL.Image.fromarray(image)
image = transforms.functional.to_tensor(image).to(device).half()
image.sub_(mean[:, None, None]).div_(std[:, None, None])
return image[None, ...]
好极了!我们现在已经定义了预处理函数,它可以将图像从摄像机格式转换为神经网络输入格式。
现在,让我们启动并显示摄像头。你现在应该对此非常熟悉了。我们还将创建一个滑块,显示机器人被阻挡的概率。我们还将显示一个滑块,用于控制机器人的基本速度。
import traitlets
from IPython.display import display
import ipywidgets.widgets as widgets
from jetbot import Camera, bgr8_to_jpeg
camera = Camera.instance(width=224, height=224)
image = widgets.Image(format='jpeg', width=224, height=224)
blocked_slider = widgets.FloatSlider(description='blocked', min=0.0, max=1.0, orientation='vertical')
speed_slider = widgets.FloatSlider(description='speed', min=0.0, max=0.5, value=0.0, step=0.01, orientation='horizontal')
camera_link = traitlets.dlink((camera, 'value'), (image, 'value'), transform=bgr8_to_jpeg)
display(widgets.VBox([widgets.HBox([image, blocked_slider]), speed_slider]))
我们还将创建机器人实例,我们需要用它来驱动电机。
from jetbot import Robot
robot = Robot()
接下来,我们将创建一个函数,每当摄像头的值发生变化时就会调用该函数。该函数将执行以下步骤
- 预处理摄像头图像
- 执行神经网络
- 当神经网络的输出显示我们被挡住时,我们就向左转,否则就向前走。
import torch.nn.functional as F
import time
def update(change):
global blocked_slider, robot
x = change['new']
x = preprocess(x)
y = model_trt(x)
#print(y)
# we apply the `softmax` function to normalize the output vector so it sums to 1 (which makes it a probability distribution)
y = F.softmax(y, dim=1)
#print(y)
prob_blocked = float(y.flatten()[0])
#print(prob_blocked)
blocked_slider.value = prob_blocked
if prob_blocked < 0.5:
robot.forward(speed_slider.value)
else:
robot.left(speed_slider.value)
time.sleep(0.001)
update({'new': camera.value}) # we call the function once to initialize
酷 我们已经创建了神经网络执行函数,但现在需要将其连接到摄像头上进行处理。
我们使用观察函数来实现这一目标。
警告:此代码可能会移动机器人 调整我们之前定义的速度滑块,控制机器人的基本速度。有些套件会移动得很快,因此开始时速度要慢,然后逐渐增加数值。
camera.observe(update, names='value') # this attaches the 'update' function to the 'value' traitlet of our camera
真棒!如果您的机器人已经插上电源,那么它现在应该会随着每一帧新的坐标系生成新的指令。也许您可以先将机器人放在地面上,看看它在遇到障碍物时会做什么。
如果您想停止这种行为,可以执行下面的代码来取消回调。
import time
camera.unobserve(update, names='value')
time.sleep(0.1) # add a small sleep to make sure frames have finished processing
robot.stop()
也许您想让机器人在不向浏览器传输视频流的情况下运行。您可以像下面这样取消链接摄像头。
camera_link.unlink() # don't stream to browser (will still run camera)
要继续流媒体播放,请调用。
camera_link.link() # stream to browser (wont run camera)
同样,让我们正确关闭摄像头连接,以便在其他笔记本上使用摄像头。
camera.stop()
结束语
本次现场演示到此结束!希望您玩得开心,您的机器人能聪明地避免碰撞!
如果您的机器人没有很好地避免碰撞,请试着找出它的失误之处。这样做的好处是,我们可以收集更多关于这些故障场景的数据,这样机器人就会变得更好了:)