训练数据生成
配置文件的修改
本文以人手检测为例配置(只有一个label:hand)
- 添加或修改data/hand.names文件,此文件记录label,每行一个label(注意:对应于训练数据的labels)
- 添加或修改cfg/hand.data文件,其内容如下:
classes= 1 # 自己数据集的类别数(不包含背景类)
train = /home/xxx/darknet/train.txt # train文件的路径
valid = /home/xxx/darknet/test.txt # test文件的路径
names = /home/xxx/darknet/data/hand.names
backup = /home/xxx/darknet/backup # 生成权重存放文件夹,如果不存在,需提前创建
- 修改cfg/yolov3.cfg文件
如图所示修改3处,直接根据“yolo”关键字查询定位,然后根据注释修改:
anchors的计算采用kmeans算法进行聚类而获得,计算代码如下:
#coding=utf-8
import numpy as np
import os
from operator import attrgetter
#bbox类
class Box():
def __init__(self,x,y,w,h):
self.x = x
self.y = y
self.w = w
self.h = h
def __repr__(self):
return repr((self.x,self.y,self.w,self.h))
#计算两个box交集面积
#box1和box2是Box类的两个对象
#返回两个box交集的面积
def cal_box_intersection(box1,box2):
overlap_width = min(box1.w,box2.w)
overlap_height = min(box1.h,box2.h)
if overlap_width < 0 or overlap_height < 0:
return 0
return overlap_height * overlap_width
#计算两个box并集面积
#box1和box2是Box类的两个对象
#返回两个box并集的面积
def cal_box_union(box1,box2):
box_intersection = cal_box_intersection(box1,box2)
area_union = box1.w * box1.h + box2.w * box2.h - box_intersection
return area_union
#计算两个box的iou
#box1和box2是Box类的两个对象
#返回两个box的iou值
def cal_iou(box1,box2):
iou = cal_box_intersection(box1,box2) / cal_box_union(box1,box2)
return iou
#k-mean++算法中用于初始化种子点
#boxes为装载Box类实例的列表
#num_anchor为需要种子点数量,即需要聚类的数量
def init_centroids(boxes,num_anchor):
centroids = []
num_box = len(boxes)
centroid_index = np.random.choice(num_box,1)
centroids.append(boxes[centroid_index[0]])
for centroid_index in range(0,num_anchor -1):
sum_dis = 0
dis_threshold = 0
dis_list = []
cur_sum = 0
for box in boxes:
min_dis = 1
for centroid_id, centroid_box in enumerate(centroids):
dis = (1 - cal_iou(box,centroid_box))
if dis < min_dis:
min_dis = dis
sum_dis += min_dis
dis_list.append(min_dis)
dis_threshold = sum_dis * np.random.random()
for i in range(0,num_box):
cur_sum += dis_list[i]
if(cur_sum > dis_threshold):
centroids.append(boxes[i])
break
return centroids
#迭代一次kmeans算法
#num_anchor是种子点数目
#boxes是Box类实例的列表
#centroids是种子点列表
#返回新的种子点列表,box的分组和误差值
def do_kmeans(num_anchor,boxes,centroids):
loss = 0
groups = []
new_centroids = []
for i in range(num_anchor):
groups.append([])
new_centroids.append(Box(0,0,0,0))
for box in boxes:
min_dis = 1
group_index = 0
for centroid_id,centroid_box in enumerate(centroids):
dis = 1 - cal_iou(box,centroid_box)
if dis < min_dis:
min_dis = dis
group_index = centroid_id
groups[group_index].append(box)
loss += min_dis
new_centroids[group_index].w += box.w
new_centroids[group_index].h += box.h
for i in range(num_anchor):
len_group = len(groups[i])
if len_group == 0:
len_group += 1
new_centroids[i].w /= len_group
new_centroids[i].h /= len_group
return new_centroids,groups,loss
#对得到的anchors按面积从小到大进行排序
def sort_centroids(centroids):
for centroid in centroids:
centroid.x = centroid.w * centroid.h
sorted_centroids = sorted(centroids,key=attrgetter('x'))
for centroid in sorted_centroids:
centroid.x = 0
return sorted_centroids
#计算得到样本的anchor值
#label_path为darknet的txt标注文件件路径
#num_anchor为需要生成的anchor的数量
#img_size为darknet的cfg中图像的尺寸
#num_iterations为最大的迭代次数,超过则停止迭代并给出anchors
#loss_thresh为前后两次计算的loss的差值的一个阈值,两次loss的差值小于这个数,则停止迭代并给出anchors
#use_kmeans_plus_plus为种子点初始化时,是否使用kmean++方法来初始化种子点,1为使用,0为不使用
def cal_anchors(label_path,num_anchor,img_size_w, img_size_h,num_iterations,loss_thresh = 1e-3,use_kmeans_plusplus = 1):
files = os.listdir(label_path)
boxes = []
for file in files:
if not file == 'train.txt':
txt_file = open(label_path + '/%s'%file,'r')
list_txt_content = txt_file.read().split(' ')
list_index = 0
while(list_index < (len(list_txt_content) - 1)):
list_index += 1
x = float(list_txt_content[list_index].replace('\n',''))
list_index += 1
y = float(list_txt_content[list_index].replace('\n',''))
list_index += 1
w = float(list_txt_content[list_index].replace('\n',''))
list_index += 1
h = float(list_txt_content[list_index].replace('\n',''))
boxes.append(Box(x,y,w,h))
txt_file.close()
if use_kmeans_plusplus:
centroids = init_centroids(boxes,num_anchor)
else:
centroid_indices = np.random.choice(len(boxes),num_anchor)
centroids = []
for centroid_index in centroid_indices:
centroids.append(boxes[centroid_index])
centroids, groups, old_loss = do_kmeans(num_anchor,boxes,centroids)
iteration = 1
while (True):
centroids, groups, loss = do_kmeans(num_anchor,boxes,centroids)
iteration += 1
#print('loss = %f' %loss)
if abs(old_loss - loss) < loss_thresh or iteration > num_iterations:
break
old_loss = loss
sum_iou = 0
for i in range(num_anchor):
for box in groups[i]:
iou = cal_iou(box,centroids[i])
sum_iou += iou
avg_iou = sum_iou / len(boxes)
centroids = sort_centroids(centroids)
anchor_txt = open('./anchor.txt','w')
for centroid in centroids:
anchor = ' %(width)3.0f,%(height)3.0f,'%{'width':(centroid.w * img_size_w), 'height':(centroid.h * img_size_h)}
print(anchor,end='')
anchor_txt.write(anchor)
print()
anchor_txt.close()
print('The Accuracy(average IOU) is ',avg_iou)
if __name__ == '__main__':
# 由标注的文件生成的txt文件所在的路径
label_path = '/home/xxxx/work/xj/datasets/water/dataparse_water/WaterMeterData/labels'
# #生成的anchors的数量
num_anchor = 6
# #对应网络的size
img_size_w = 256
img_size_h = 256
# #计算anchor时的最大迭代次数
num_iterations = 2000
# #计算anchors,并在脚本同目录下生一个anchor.txt文件来记录anchors的数值
cal_anchors(label_path,num_anchor,img_size_w, img_size_h,num_iterations)
使用darknet训练模型
注意事项:
- .data文件。该文件包含一些配置信息,具体为训练的总类别数,训练数据和验证数据的路径,类别名称,模型存放路径等。
- 在读入训练数据时,只给程序输入了图片所在路径,而标签数据的路径并没有直接给,是通过对图片路径进行修改得到的。(替换的前提是,标签数据文件夹labels与图片数据文件夹images具有相同的父目录。另外,直接把txt标签文件放在与图片同一路径下也没问题。)
- .cfg文件。主要包含训练的一些配置信息,如输入图像大小、学习率、数据增强等。还包括训练的网络结结构。
详见:注意事项详细说明
模型训练命令
./darknet detector test <data_cfg> <models_cfg> <weights> <test_file> [-thresh] [-out]
./darknet detector train <data_cfg> <models_cfg> <weights> [-thresh] [-gpu] [-gpus] [-clear]
./darknet detector valid <data_cfg> <models_cfg> <weights> [-out] [-thresh]
./darknet detector recall <data_cfg> <models_cfg> <weights> [-thresh]
'< >'必选项,’[ ]‘可选项
- data_cfg:数据配置文件,eg:cfg/voc.data
- models_cfg:模型配置文件,eg:cfg/yolov3-voc.cfg
- weights:权重配置文件,eg:weights/yolov3.weights
- test_file:测试文件,eg://*/test.txt
- -thresh:显示被检测物体中confidence大于等于 [-thresh] 的bounding-box,默认0.005
- -out:输出文件名称,默认路径为results文件夹下,eg:-out “” //输出class_num个文件,文件名为class_name.txt;若不选择此选项,则默认输出文件名为comp4_det_test_“class_name”.txt
- -i/-gpu:指定单个gpu,默认为0,eg:-gpu 2
- -gpus:指定多个gpu,默认为0,eg:-gpus 0,1,2
训练模型并保存日志(保存至当前文件下的log.txt文件中):
./darknet detector train cfg/voc.data cfg/yolov3-tiny.cfg backup202106032111/yolov3-tiny.backup > results/log.txt
训练模型可视化
yolov3训练日志可视化主要为loss和iou曲线的可视化,这些是我们查看训练效果的重要依据。
其中,darknet训练时控制台打印的log信息截图:
log信息解析:
- Aug IOU:当前迭代中,预测的box与标注的box的平均交并比,越大越好,期望数值为1;
- Class:标注物体的分类准确率,越大越好,期望数值为1;
- obj: 越大越好,期望数值为1;
- No obj: 越小越好;
- .5R: 以IOU=0.5为阈值时候的recall; recall = 检出的正样本/实际的正样本
- 0.75R: 以IOU=0.75为阈值时候的recall;
- count:正样本数目。
注:存在nan值说明该子批次没有预测到正样本,在训练开始时候有出现是正常现象。
每批次在以后一行输出意义:
- 第几批次:总损失,平均损失,当前学习率,当前批次训练时间,目前为止参与训练的图片总数
模型训练可视化脚本:
#coding=utf-8
import os
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s %(levelname)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger(__name__)
class Yolov3LogVisualization:
def __init__(self,log_path,result_dir):
self.log_path = log_path
self.result_dir = result_dir
def extract_log(self, save_log_path, key_word):
with open(self.log_path, 'r') as f:
with open(save_log_path, 'w') as train_log:
next_skip = False
for line in f:
if next_skip:
next_skip = False
continue
# 去除多gpu的同步log
if 'Syncing' in line:
continue
# 去除除零错误的log
if 'nan' in line:
continue
if 'Saving weights to' in line:
next_skip = True
continue
if key_word in line:
train_log.write(line)
f.close()
train_log.close()
def parse_loss_log(self,log_path, line_num=2000):
result = pd.read_csv(log_path, skiprows=[x for x in range(line_num) if ((x % 10 != 9) | (x < 1000))],error_bad_lines=False, names=['loss', 'avg', 'rate', 'seconds', 'images'])
result['loss'] = result['loss'].str.split(' ').str.get(1)
result['avg'] = result['avg'].str.split(' ').str.get(1)
result['rate'] = result['rate'].str.split(' ').str.get(1)
result['seconds'] = result['seconds'].str.split(' ').str.get(1)
result['images'] = result['images'].str.split(' ').str.get(1)
result['loss'] = pd.to_numeric(result['loss'])
result['avg'] = pd.to_numeric(result['avg'])
result['rate'] = pd.to_numeric(result['rate'])
result['seconds'] = pd.to_numeric(result['seconds'])
result['images'] = pd.to_numeric(result['images'])
return result
def gene_loss_pic(self, pd_loss):
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot(pd_loss['avg'].values, label='avg_loss')
ax.legend(loc='best')
ax.set_title('The loss curves')
ax.set_xlabel('batches')
fig.savefig(self.result_dir + '/avg_loss')
logger.info('save iou loss done')
def loss_pic(self):
train_log_loss_path = os.path.join(self.result_dir, 'train_log_loss.txt')
self.extract_log(train_log_loss_path, 'images')
pd_loss = self.parse_loss_log(train_log_loss_path)
self.gene_loss_pic(pd_loss)
def parse_iou_log(self,log_path, line_num=2000):
result = pd.read_csv(log_path, skiprows=[x for x in range(line_num) if (x % 10 == 0 or x % 10 == 9)],error_bad_lines=False,names=['Region Avg IOU', 'Class', 'Obj', 'No Obj', 'Avg Recall', 'count'])
result['Region Avg IOU'] = result['Region Avg IOU'].str.split(': ').str.get(1)
result['Class'] = result['Class'].str.split(': ').str.get(1)
result['Obj'] = result['Obj'].str.split(': ').str.get(1)
result['No Obj'] = result['No Obj'].str.split(': ').str.get(1)
result['Avg Recall'] = result['Avg Recall'].str.split(': ').str.get(1)
result['count'] = result['count'].str.split(': ').str.get(1)
result['Region Avg IOU'] = pd.to_numeric(result['Region Avg IOU'])
result['Class'] = pd.to_numeric(result['Class'])
result['Obj'] = pd.to_numeric(result['Obj'])
result['No Obj'] = pd.to_numeric(result['No Obj'])
result['Avg Recall'] = pd.to_numeric(result['Avg Recall'])
result['count'] = pd.to_numeric(result['count'])
return result
def gene_iou_pic(self, pd_loss):
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot(pd_loss['Region Avg IOU'].values, label='Region Avg IOU')
# ax.plot(result['Class'].values,label='Class')
# ax.plot(result['Obj'].values,label='Obj')
# ax.plot(result['No Obj'].values,label='No Obj')
# ax.plot(result['Avg Recall'].values,label='Avg Recall')
# ax.plot(result['count'].values,label='count')
ax.legend(loc='best')
ax.set_title('The Region Avg IOU curves')
ax.set_xlabel('batches')
fig.savefig(self.result_dir + '/region_avg_iou')
logger.info('save iou pic done')
def iou_pic(self):
train_log_loss_path = os.path.join(self.result_dir, 'train_log_iou.txt')
self.extract_log(train_log_loss_path, 'IOU')
pd_loss = self.parse_iou_log(train_log_loss_path)
self.gene_iou_pic(pd_loss)
if __name__ == '__main__':
log_path = '/path/to/log/file' # 模型训练输出日志 可在训练命令后重定向获得
result_dir = '/path/to/save/result' # 分析的结果文件输出所在文件夹
logVis = Yolov3LogVisualization(log_path,result_dir)
logVis.loss_pic()
logVis.iou_pic()