嘿!您似乎在 United States,您想使用我们的 English 网站吗?
Switch to English site
Skip to main content

NVIDIA Jetson Nano应用- Google Colab云端训练客制化 YOLOv4对象辨识-下篇

下篇分为几个重点:

1.找寻欲训练的数据集

2.使用特定数据集进行Transfer Learning

3.于Colab上进行测试

4.下载至Jetson Nano并进行测试

5.补充-如何让训练更好?

找寻欲训练的数据集

去年的文章有带大家自己标记数据并且使用YOLOv5进行迁移式学习 ( 文章连结 : https://www.rs-online.com/designspark/pytorchx-nvidia-jetsonnano-yolov5-transfer-learning-cn ),这次教大家运用另一个开源的资源 – Open Image,它是一个开源的大型数据集,其中有超过6000个类别,有对象辨识也有语意分割的数据,这次我们使用的是Open Image V6的版本(如下图)。

image00112_e10d09ea54b2294cc0e14f328122884583a107f3.png

我们可以先点击 Explore进行探索,点进来后每一次都会random一样物品给你,你也可以在上方导览列点击「Random category」来随机产生图片:

image00210_5279ee86721fdb5fd92a901e3b5f8919b85f569b.png

接着你可以在「Type」的位置选择要哪一种类别的数据,这边有「Detection」、「Segmentation」、「Relationships」三种可以选择,预设都是segmentation,点下去后图片就会改变。

image00312_dcd9eefd52cb7857990eab22ae3b338461c23e0d.png

下图为同样是Cucumber但是选择成Detection的结果:

image00413_a9c089bbfe707095ea1c8619c793f36490372ba4.png

在Type中有一个大家可能比较不熟悉的就是「Relationships」这是表达对象跟对象之间的交互关系,像是图片中有「人」跟「人」可能就会有「hug」这个动作。

image00511_5782c6f7bf7718dd9f677f90b692c0b787b2a792.png

到这边稍微简单的介绍了一下Open Image V6接下来我们就可以进行下载了!

这边我们使用的资源是网友改良过的「OIDv4 Toolkits」,他是一个可以帮助下载Open Image V4的工具,经过实测V6也可以进行下载,我会选择改良版本的原因是因为他有一个程序可以帮助我们自动将OpenImage的格式进行转换,这部分后续会再介绍,现在就让我们直接开启一个全新的Colab来下载数据集吧。

首先,由于暂存的空间会在关闭的时候清除数据,所以我们需要绑定云端硬盘,将数据集下载到自己的云端云端硬盘:

from google.colab import drive
drive.mount('/content/gdrive')

执行画面如下,需要点击连结取得认证码:

image00617_916b6aca6d2f2d8a0e8fb3ddc23a7f58e345ca3d.png

输入完按下Enter就会跳出「Mounted at /content/gdrive」的字样,接着我们可以移动到工作目录去,这边简单写了一下防呆,如果没有工作目录就直接创建。

import os
working_dir = '/content/gdrive/MyDrive/yolov4'
try:
 os.makedirs(working_dir)
 print('Create directory')
except FileExistsError:
 print('Directory is exists!')
os.chdir(working_dir)
print(f'Move to working space ({os.getcwd()})')

执行完上一步,Colab的位置就会移动到yolov4这个文件夹里了,接着我们需要将OIDv4 Toolkits下载下来并安装相关套件:

!git clone https://github.com/theAIGuysCode/OIDv4_ToolKit.git
!pip3 install -r ./OIDv4_ToolKit/requirements.txt

接着就可以使用该Toolkits进行下载了,由于在Colab当中我都会谨慎地在每一个block多做一个路径的确认,除此之外如果有残留的旧数据也会先进行删除,以免混杂在一起,最后我们直接执行main.py这个程序来进行下载,--class 后面可以输入多个类别,type_csv则是宣告是train还是test的文件夹,limit则是限制下载的数量,接着multiclasses如果设定成True就会把所有的类别放在同一个文件夹当中,最后thread就是线程的数量了。

import os
import shutil
# 确保移动到对的目录
working_dir = '/content/gdrive/MyDrive/yolov4/OIDv4_ToolKit'
os.chdir(working_dir)
# 如果有旧有的数据集要先删除
target_dir = 'OID/Dataset/train'
if os.path.exists(target_dir) is True:
 shutil.rmtree(target_dir, ignore_errors=True)
# 于 classes 中找 特定 类别
!python ./main.py downloader --classes 'Apple' --type_csv train --limit 300 --multiclasses 1 --y --n_threads 4

执行画面如下,下载完之后会在OIDv4_Toolkits/OID/Dataset/train/Apple当中出现图片以及Label的文件夹存放所有图片的卷标档案:

image00713_45463fa037ff81560b24970e8ca391ecae394220.png

此外,这边顺便给大家参考多类别的指令:

!python ./main.py downloader --classes 'Human eye' 'Human mouth' 'Human nose' --type_csv train --limit 500

下载完之后,我们可以用下列的程序稍微检查一下数据:

import cv2
import matplotlib.pyplot as plt
%matplotlib inline
task = 'Apple'
file_path = f'OID/Dataset/train/{task}'
label_dir = f'OID/Dataset/train/{task}/Label'
# 随机选一张图片
import random
file_list = os.listdir(file_path)
trg_id = random.randint(0, len(file_list))
# 取得 图文件 以及 标签文件
title, ext = os.path.splitext( file_list[trg_id] )
img_path = os.path.join(file_path, title+'.jpg')
label_path = os.path.join(label_dir, title+'.txt')
# 读取图片
img = cv2.imread(img_path)
# 开启标签文件 并绘制 bonding box
with open(label_path) as f:
 for line in f.readlines():
  # 取得坐标
  x1,y1,x2,y2 = [ round(float(c)) for c in line.rstrip('\n').split(' ')[1:]]  
  # 绘制bbox
  cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 1)
# 在 Jupyter Notebook 上需要转换成 Matplot 显示才行
fig = plt.gcf()
fig.set_size_inches(18, 10)
plt.axis('off')
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.show()

执行结果如下:

image0089_51da354a4f2d2136d5448edf044546cebb8f1d95.png

稍微确认过后,我们需要将原本Open Image的标签形式转换成YOLO可用的形式,原本Open Image的形式是 「<class_name> <left_x> <left_y> <right_x> <right_y>」,如下图所示:

image00910_fc6b0efb595fc4b32c3b6b969c2df2fc64869701.png

但是YOLO的格式是「<class_id> <center_x> <center_y> <width> <height>」,并且都会进行正规化例如<center_x>= <absolute_x> / <image_width>又或者<height> = <absolute_height> / <image_height>,所以上面那个标签文件转换完就会如下图所示:

image0106_90df29dcd9d192252280b469a1856905137d0c50.png

由于经过正规化的关系所以数值会介于0~1之间,刚刚上述的动作可以自己撰写程序进行换算,不过我们下载的改良版Toolkits已经有写好程序,我们可以直接使用下列程序去转换,不过在转换之前我们需要先修改OIDv4_Toolkits的classes.txt档案,在classes.txt当中输入我们的所有标签。

image0117_877da320ae97f5a0f0746bb5e259833fa7a57c2b.png

接着就可以进行转换,执行完这个程序后:

%cd '/content/gdrive/MyDrive/yolov4/OIDv4_ToolKit'
!python ./convert_annotations.py

接着我们需要把整理好的数据复制到对应的文件夹当中:

import shutil
# task = 'custom'
task = 'Apple'
shutil.copytree(f'/content/gdrive/MyDrive/yolov4/OIDv4_ToolKit/OID/Dataset/train/{task}', f'../darknet/data/{task}')
print('Finish')

最后需要删除旧有的标签档案( Label文件夹):

import os
task = 'Apple'
os.chdir('/content/gdrive/MyDrive/yolov4/darknet/')
shutil.rmtree(f'./data/{task}/Label')

到目前我们已经完成数据击的下载与整理,接着就要进行Trainning了!

使用特定数据集进行Transfer Learning

我的作法会在这边直接新增一个Colab档案把下载数据集与训练分开来,所以需要重新挂接云端硬盘以及移动到工作目录:

from google.colab import drive
drive.mount('/content/gdrive')

挂接完就可以进行移动,用跟之前一样的程序代码即可:

import os
working_dir = '/content/gdrive/MyDrive/yolov4'
try:
 os.makedirs(working_dir)
 print('Create directory')
except FileExistsError:
 print('Directory is exists!')
os.chdir(working_dir)
print(f'Move to working space ({os.getcwd()})')

接着需要下载并且进入该文件夹:

import os
darknet_path = os.path.join(working_dir,'darknet')
if os.path.exists( darknet_path ):
 print('Darknet is exists!')
else:
 !git clone https://github.com/AlexeyAB/darknet.git

紧接着需要建构函式库,大约3分钟:

import time
%cd /content/gdrive/MyDrive/yolov4/darknet
print('\n\nBuild Darknet...')
t_start = time.time()
# 修改 Make File
!sed -i 's/OPENCV=0/OPENCV=1/' Makefile
!sed -i 's/GPU=0/GPU=1/' Makefile
!sed -i 's/CUDNN=0/CUDNN=1/' Makefile
!sed -i 's/CUDNN_HALF=0/CUDNN_HALF=1/' Makefile
!sed -i 's/LIBSO=0/LIBSO=1/' Makefile
# 建构环境
!make
t_end = time.time()
print(f'\n\nFinish ({int(t_end-t_start)}s)')

接着需要把握YOLOv4在darknet当中训练的几个大原则:

  1. 新增数据集的名称与类别的名称的档案 ( train.txt , test.txt)
  2. 修改模型的配置文件 (config)
  3. 建立训练用的档案 (obj.data, obj.names)
  4. 下载预训练模型开始训练

1. 新增数据集的名称与类别的名称的档案 ( train.txt , test.txt)

在这一步我们会将所有的文件名汇整到train.txt以集test.txt,可以透过percentage来宣告有多少笔数据要被归类到test.txt来验证使用,最后将这两个档案存放到data文件夹当中。

import glob, os
# 目标文件夹的名称 ( 放到 ./data 当中 )
trg_dir = 'Apple'
# 宣告多少笔数据要变成测试数据
percentage_test = 10;
# 确保数据集存在
datasets_path = f'data/{trg_dir}'
if os.path.exists(datasets_path):
 # 取得 train.txt , test_txt 的完整路径
 train_txt_path = f'data/{trg_dir}_train.txt'
 test_txt_path = f'data/{trg_dir}_test.txt'
 # 确认是否移动到当前目录
 os.chdir('/content/gdrive/MyDrive/yolov4/darknet')
 print('当前路径为:', os.getcwd())
 # 宣告数据集路径
 dataset_dir = f'data/{trg_dir}'
 print('目标文件夹:', dataset_dir)
 # 建立以及开启 train.txt, test.txt
 file_train = open(train_txt_path, 'w')
 file_test = open(test_txt_path, 'w')
 # 开始输入训练数据
 counter = 1
 index_test = round(100 / percentage_test)
 # glob.iglob 可以将该目录下所有的 .jpg 储存成一个 List
 for file_path in glob.iglob(os.path.join(dataset_dir, "*.jpg")):
   if counter == index_test:
     counter = 1
     file_test.write(file_path + "\n")
   else:
     file_train.write(file_path + "\n")
     counter = counter + 1
 file_train.close()
 file_test.close()
 print('Finish')
else:
 print(f'Check the target datasets is exists ({datasets_path})')

2. 修改模型的配置文件 (config)

接着我需要修改模型用的配置档案,我这边使用的是tiny版本的所以要去修改的是cfg/yolov4-tiny-custom.cfg,这边有几个重点要修改:

1.类别的数量 (classes_num),其中每一个YOLO层都要修改类别数量;

2.滤波器的数量 (target_filter) 这个会与类别数量有关,修改每一个YOLO层的前一个Convolution层,其中的filter=255都要都要修改成对应的数字,必须要符合 ( classes + 5 * 3 )这个公式,是YOLO输出维度的关系,5代表的是<classes_id><center_x><center_y><width><height>,这部分推荐大家去看吴恩达的YOLO介绍,将会很清楚明白原因为何!

3.训练的总次数 (max_batches),这边YOLO是建议类别的数量乘上2000,但不能低于6000回,所以这边我有写了一个简单的判断;

4. 接着就是learning rate的调整,这边YOLO也是建议使用训练到八成以及九成的时候在进行调整。

# 刚刚宣告的 target directory
# trg_dir = 'Apple'
target_cfg = f"cfg/yolov4-tiny-{trg_dir}.cfg"
classes_nums = 1
target_filter = int((classes_nums+5)*3)
max_batches = classes_nums*2000 if classes_nums>=3 else 6000
steps_1, steps_2 = int(max_batches*0.8), int(max_batches*0.9)
info = '\
classes_num : {}\n\
target_filter : {}\n\
max_batches : {}\n\
steps : {},{}\n\
'.format(classes_nums, target_filter, max_batches, steps_1, steps_2)
print(info)
!cp cfg/yolov4-tiny-custom.cfg {target_cfg}
!sed -i '7c subdivisions=16' {target_cfg}
!sed -i '8c width=416' {target_cfg}
!sed -i '9c height=416' {target_cfg}
# classes*2000 = max_batches
!sed -i '20c max_batches={max_batches}' {target_cfg}
# step = max_batches*80% and 90%
!sed -i '22c steps={steps_1},{steps_2}' {target_cfg}
# last layer's filter = ( classes + 5 ) * 3
# classes代表有几个类别 而这边5代表的是 class,x1,x2,y1,y2 3代表rgb通道数
!sed -i '212c filters={target_filter}' {target_cfg}
!sed -i '220c classes={classes_nums}' {target_cfg}
!sed -i '263c filters={target_filter}' {target_cfg}
!sed -i '269c classes={classes_nums}' {target_cfg}
print(f'Created {target_cfg}.')

3. 建立训练用的档案 (obj.data, obj.names)

注意obj是代名词,这边会替换成trg_dir的名称,其中obj.data存放训练所需要的信息,像是类别有几类、训练的文件名、验证的文件名、类别的名称、权重储存的位置,obj.names会存放所有类别的名称,直接看程序可能会比较清楚,最后这两个档案会被存放在data文件夹里:

import os
# 刚刚宣告的 target directory
# trg_dir = 'Apple'
obj_names_cnt=[
 'Apple'   
]
classes_nums = len(obj_names_cnt)
obj_data = f'data/{trg_dir}.data'
obj_names = f'data/{trg_dir}.names'
obj_data_cnt = [
f'classes = {classes_nums}',
f'train = data/{trg_dir}_train.txt',
f'valid = data/{trg_dir}_test.txt',
f'names = data/{trg_dir}.names',
'backup = ../backup-tiny/'
]
# 写入data档案
with open(obj_data, 'w') as f:
 [ f.write(i+'\n') for i in obj_data_cnt]
# 写入name档案
with open(obj_names, 'w') as f:
 [ f.write(i+'\n') for i in obj_names_cnt]
# 确保储存权重的文件夹要存在
try:
 import os
 os.mkdir('../backup-tiny/')
except FileExistsError:
 print("Direction is exist !!!")

4. 下载预训练模型开始训练

上述档案都准备完成之后我们就可以下载预训练模型来进行transfer learning了,这边yolov4有帮我们准备好可供预训练的模型 (yolov4-tiny.conv.29):

!wget https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v4_pre/yolov4-tiny.conv.29

接着就可以运行下列指令进行训练,这边的trg_dir是之前所宣告的:

%cd '/content/gdrive/MyDrive/yolov4/darknet'
!./darknet detector train data/{trg_dir}.data cfg/yolov4-tiny-{trg_dir}.cfg ./yolov4-tiny.conv.29 -dont_show -map

开始训练之后,每1000次训练都会备份一次权重,所以会在backup-tiny的文件夹当中就可以看到如同我画面中的内容,每一个档案都有纪录多少批次的时候储存的,除此之外还有best、last等权重。

image0127_77601e97e88407c7517f6caa61ca183900942ebb.png

补充1. 当训练中断了

我们可以使用backup-tiny当中的last.weights来接续训练:

!./darknet detector train data/{trg_dir}.data cfg/yolov4-tiny-{trg_dir}.cfg ../backup-tiny/yolov4-tiny-{trg_dir}_last.weights -dont_show -map

补充2. 模拟使用者还在在线

这部分是我在网络上找到的方式,我们可以透过下列程序不断开启关闭特定窗口来仿真用户还在使用中,首先我们必须先按Ctrl+Shift+i,点击Console于最底部贴上程序代码并且按下Enter执行:

function ClickConnect(){
console.log("Working"); 
document
.querySelector('#top-toolbar > colab-connect-button')
.shadowRoot.querySelector('#connect')
.click() 
}
setInterval(ClickConnect,60000)

于Colab上进行测试

这个部分与上篇的程序相同,有些不同的地方是在宣告完darknet_helper之后就直接导入图片 (image) 并且进行辨识,取得辨识完的结果用for循环将所有辨识到的对象画上边界窗以及卷标信息,最后用matplot显示出来。

import cv2
import matplotlib.pyplot as plt
%matplotlib inline
# import darknet functions to perform object detections
from darknet import *
# load in our YOLOv4 architecture network
# 执行刚刚训练好的模型
#trg_dir = 'Apple'
network, class_names, class_colors = load_network(f"cfg/yolov4-tiny-{trg_dir}.cfg", f"data/{trg_dir}.data", f"../backup-tiny/yolov4-tiny-{trg_dir}_best.weights")
width = network_width(network)
height = network_height(network)
# darknet helper function to run detection on image
def darknet_helper(img, width, height):
 darknet_image = make_image(width, height, 3)
 img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
 img_resized = cv2.resize(img_rgb, (width, height),
               interpolation=cv2.INTER_LINEAR)
 # get image ratios to convert bounding boxes to proper size
 img_height, img_width, _ = img.shape
 width_ratio = img_width/width
 height_ratio = img_height/height
 # run model on darknet style image to get detections
 copy_image_from_bytes(darknet_image, img_resized.tobytes())
 detections = detect_image(network, class_names, darknet_image)
 free_image(darknet_image)
 return detections, width_ratio, height_ratio
# run test on person.jpg image that comes with repository
image = cv2.imread('./data/apple_test.jpg')
detections, width_ratio, height_ratio = darknet_helper(image, width, height)
for label, confidence, bbox in detections:
 left, top, right, bottom = bbox2points(bbox)
 left, top, right, bottom = int(left * width_ratio), int(top * height_ratio), int(right * width_ratio), int(bottom * height_ratio)
 cv2.rectangle(image, (left, top), (right, bottom), class_colors[label], 2)
 cv2.putText(image, "{} [{:.2f}]".format(label, float(confidence)),
          (left, top - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5,
          class_colors[label], 2)
fig = plt.gcf()
fig.set_size_inches(18, 10)
plt.axis('off')
plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
plt.show()

结果如下:

image01310_fbf84449e6ece90601eb67f826da7b63a557c8ef.png

image0145_2ff90b150b60a6926b1e9f8f1497ab01bbee68a9.png

下载至Jetson Nano并进行测试

由于程序细节与Colab上相同只差在darknet建构,所以这边我提供流程:

  1. 参考此篇文章在Jetson Nano上建构darknet (https://www.rs-online.com/designspark/nvidia-jetson-nanotensor-rtyolov4-cn )。
  2. 从Google Drive中下载训练好的 {obj}.data、{obj}.names、yolov4-tiny-{}.cfg、yolov4-tiny-{}.weights,并且放在Jetson Nano的darkent中。
  3. 执行程序或参考此篇文章自行撰写程序 (https://www.rs-online.com/designspark/nvidai-jetson-nano-opencvyolov4-cn ) 。

结语

这篇的信息量算是蛮大的,大家可以多多善用Colab进行训练,训练完之后在放置到Jetson Nano中去做Inference等应用,最后鼓励大家多去翻看darknet的github它不仅仅有更详细的细节也在issue当中可以找到很多常遇到的问题,除此之外还有提供帮助训练的技巧。

CAVEDU Education is devoted into robotics education and maker movement since 2008, and is intensively active in teaching fundamental knowledge and skills. We had published many books for readers in all ages, topics including Deep Learning, edge computing, App Inventor, IoT and robotics. Please check CAVEDU's website for more information: http://www.cavedu.com, http://www.appinventor.tw

评论