赞
踩
实现基于yolo网络的目标识别。
使用github上开源的代码。那么需要做的事只有几样:
这篇文章的内容很有帮助,不过里面也有不少坑,我下面边踩边说。
mac真的不太适合深度学习,就跑跑实验吧。
因为mac不会是生产机,也不支持cuda,所以安装起来直接装就好了。总体来说,就是安装一个虚拟环境(virtualenv),从github上拉下代码,然后按照代码里的requirements.txt安装包就好了(之后的操作全部在虚拟环境下)。
官方给的requirements比较少,剩下的自己看情况加一下。(官方说 Python 3.8 or later,我用Python3.6.5也ok,我还是不太喜欢升级Python,或许下个本子升吧 )。
另外,这里用的是pytorch框架实现的yolo,可以理解为yolo是一个模型,有n种框架可以支持。目前大致有:
1.darknet + yolo(作者使用的框架)
2.tensorflow + yolo(应该有没试过)
3.torch + yolo
4.keras + yolo(应该有没试过)
关于pytorch和tensorflow/keras可以借鉴一下这篇文章, 看来pytorch是必须要考虑的了。
Cython
matplotlib>=3.2.2
numpy>=1.18.5
opencv-python>=4.1.2
pillow
# pycocotools>=2.0
PyYAML>=5.3
scipy>=1.4.1
tensorboard>=2.2
torch>=1.6.0
torchvision>=0.7.0
tqdm>=4.41.0
下载成功后,可以测试一下。(项目data/samples下面已经有了两张测试图片)
# 在项目根文件夹下执行 python3 detect.py --source data/samples/bus.jpg --cfg cfg/yolov3.cfg --weights yolov3.pt Namespace(agnostic_nms=False, augment=False, cfg='cfg/yolov3.cfg', classes=None, conf_thres=0.3, device='', fourcc='mp4v', half=False, img_size=512, iou_thres=0.6, names='data/coco.names', output='output', save_txt=False, source='data/samples/bus.jpg', view_img=False, weights='yolov3.pt') Using CPU Model Summary: 222 layers, 6.19491e+07 parameters, 6.19491e+07 gradients % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 408 0 408 0 0 446 0 --:--:-- --:--:-- --:--:-- 446 0 0 0 0 0 0 0 0 --:--:-- 0:00:01 --:--:-- 0 0 0 0 0 0 0 0 0 --:--:-- 0:00:02 --:--:-- 0 100 236M 0 236M 0 0 3771k 0 --:--:-- 0:01:04 --:--:-- 4658k Downloading https://drive.google.com/uc?export=download&id=1SHNFyoe5Ni8DajDNEqgB2oVKBb_NoEad as yolov3.pt... Done (65.3s) image 1/1 data/samples/bus.jpg: 512x384 4 persons, 1 buss, Done. (1.263s) Results saved to /绝对路径/yolov3/output Done. (1.709s)
结果如下
更多测试,从相机里随便选相片,放到samples文件夹下面:
然后执行
python3 detect.py --cfg cfg/yolov3.cfg --weights yolov3.pt
Namespace(agnostic_nms=False, augment=False, cfg='cfg/yolov3.cfg', classes=None, conf_thres=0.3, device='', fourcc='mp4v', half=False, img_size=512, iou_thres=0.6, names='data/coco.names', output='output', save_txt=False, source='data/samples', view_img=False, weights='yolov3.pt')
然后就可以看到结果了。参数上可以观察一下,预训练的模型已经很厉害了。关键依赖的参数有两个,一个是网络的.cfg文件,还有一个就是.pt文件。
cpu版本的应该和mac差不多。gpu版本的得等我的小伙伴帮我一起弄。
值得一提的是ubuntu作为生产机,一般不会是云主机(太浪费了),所以可以想象成一台放在办公室或者家里的台式机。
那么就要考虑如何进行ssh进行操作。
【这块 后补】
目前我们已经有了可执行的模型代码,如果我们要自己用起来,那么还缺什么呢?
假设我们遇到了一个新的目标识别问题,我们应该会拿到图片,然后我们对这批图片进行标注(参考LabelImg工具),这时我们就有了数据和标签,理论上就可以开始训练了。
LabelImg操作:
Ctrl + u 加载目录中的所有图像,鼠标点击Open dir同功能
Ctrl + r 更改默认注释目标目录(xml文件保存的地址)
Ctrl + s 保存
Ctrl + d 复制当前标签和矩形框
space 将当前图像标记为已验证
w 创建一个矩形框
d 下一张图片
a 上一张图片
del 删除选定的矩形框
Ctrl++ 放大
Ctrl-- 缩小
↑→↓← 键盘箭头移动选定的矩形框
通常的建模需要把数据分为训练、验证和测试。另外就是要把我们自己做的(VOC规范)的标签文件啥的转成模型认可的格式。
再之后则是根据我们训练的任务,调整模型参数。具体点就是我们预测的类别不同,选择的模型结构不同,对应的参数要调整。
最后就是下载模型的预训练权重,基于这个权重去计算就可以了。(这里我还没完全搞明白,预训练的权重是哪些,为什么可以给任何新任务使用)
实验分两步,一步是使用小数据集在MAC上进行初步验证。其意义在于证明过程可行,并且如果基于小数据出来结果还可以,那么这个模型的再利用就会很方便。
第二步则是利用稍大一点的数据集(VOC2007)来观察其表现,这时候可能会在意数据的大小,运行时间和效果等。
使用一批医学影像图片,具体来说大约是400张左右的图片(640*480 , 大约25k一张),目标是识别其中的红细胞(RBC)。
数据项目地址,我们可以称为BloodImage。
红细胞也称红血球,在常规化验中英文常缩写成RBC,是血液中数量最多的一种血细胞,同时也是脊椎动物体内通过血液运送氧气的最主要的媒介,同时还具有免疫功能。哺乳动物成熟的红细胞是无核的,这意味着它们失去了DNA。红细胞也没有线粒体,它们通过分解葡萄糖释放能量。运输氧气,也运输一部分二氧化碳。运输二氧化碳时呈暗紫色,运输氧气时呈鲜红色。
红细胞会生成于骨髓之内。红细胞老化后,易导致血管堵塞,所以会自动返回骨髓深处,由白细胞负责销毁;或是在经过肝脏时,被巨噬细胞分解成为胆汁。
以下是原始文件的处理过程:
基于BloodImage:
1 原始文件:图片和标注文件是一一对应的
2 根据原始文件进行训练、测试和验证的数据集分割(test1_maketxt.py)
3 读取并解析xml里的数据,形成标签(test2_voc_label.py)
某个label文件的内容如下(因为识别红细胞是单目标任务,所以索引为0,否则可能为3,5 ,8 …):
标签索引 | 箱体坐标(x,y,w,h) |
---|---|
0 | 0.48333333333333334 0.3585164835164835 0.19583333333333333 0.3763736263736264 |
0 | 0.5135416666666667 0.5686813186813188 0.6520833333333333 0.7087912087912088 |
手动将对应文件搬过去(注意在生成图片绝对地址的时候要写目标位置的 yolo文件夹下)。
对应的两个处理数据的python文件如下:
test1_maketxt.py
import os import random ''' 源文件的命名是混乱的,大体上原作者希望: 1. 划出一定比例进行测试和验证。对应于 train 和 train_val,所以trainval_percent = 0.1, train_percent=0.9的意思是这部分有90%是测试集,用于训练 !- - 2. 剩余的部分都是用于训练的。train 3. 这个以写入txt文件的为准。(因为后面yolo就是读这个训练的) ''' # 训练集+验证集占总图片比例 trainval_percent = 0.1 # 其中训练集的占比 train_percent = 0.9 yolo_path = '你的路径/yolov3/data/' # 文件的路径地址 xmlpath = '你的路径/bloodimage_data/Annotations/' imglist_path = '你的路径/bloodimage_data/ImageSets/' if not os.path.exists(imglist_path): os.makedirs(imglist_path) # xml文件列表 xml_list = [x for x in os.listdir(xmlpath) if x.endswith('.xml')] # 文件个数 num = len(xml_list) # xml下标索引 ind_list = range(num) # 训练和验证的个数 train_validate_numbers = int(num*trainval_percent) train_numbers = int(train_validate_numbers*train_percent) # train_val 是随机选中的文件名下标列表。其实直接筛选文件名列表也可以 train_val = random.sample(ind_list , train_validate_numbers) train = random.sample(train_val, train_numbers) # 既然已经读入了所有的文件列表,那么内存应该也是足够的,直接先缓存再一次性写入 trainval_content = '' test_content = '' train_content = '' val_content = '' # 文件分配 for i in ind_list: # 将.xml后缀去掉 name = xml_list[i][:-4] + '\n' if i in train_val: trainval_content += name if i in train: test_content+= name else: val_content += name else: train_content += name with open(imglist_path + 'trainval.txt', 'w') as f: f.write(trainval_content) with open(imglist_path + 'test.txt', 'w') as f: f.write(test_content) with open(imglist_path + 'train.txt', 'w') as f: f.write(train_content) with open(imglist_path + 'val.txt', 'w') as f: f.write(val_content)
test2_voc_label.py
import xml.etree.ElementTree as ET import os from os import listdir, getcwd from os.path import join # 坐标转换 xmin, xmax, ymin, ymax def convert(size, box): # 宽度单位 dw = 1. / size[0] # 高度单位 dh = 1. / size[1] # 宽的中线 x = (box[0] + box[1]) / 2.0 # 高的中线 y = (box[2] + box[3]) / 2.0 # 箱体宽 w = box[1] - box[0] # 箱体高 h = box[3] - box[2] # 根据宽度和高度单位标准化 x = x * dw w = w * dw y = y * dh h = h * dh return (x, y, w, h) # 为xml的Annotation生成一一对应的label(一个txt文件中可能有几个目标) # 函数一次处理一个文件 def convert_annotation(image_id, infile_folder, outfile_folder, classes): infile_name = str(image_id) + '.xml' outfile_name = str(image_id) + '.txt' # 一次读入xml的ElementTree with open(infile_folder + infile_name) as f: tree = ET.parse(f) root = tree.getroot() size = root.find('size') w = int(size.find('width').text) h = int(size.find('height').text) # 循环的将标记目标存入输出文件 with open(outfile_folder + outfile_name, 'w') as f: for obj in root.iter('object'): difficult = obj.find('difficult').text clsname = obj.find('name').text if clsname not in classes or int(difficult) == 1: continue cls_id = classes.index(clsname) xmlbox = obj.find('bndbox') b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text),float(xmlbox.find('ymax').text)) bb = convert((w, h), b) f.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n') if __name__ =='__main__': # 数据集 sets = ['train', 'test', 'val'] # 类别:本例只识别这种细胞 classes = ['RBC'] # 当前目录 wd = getcwd() # 数据目录 xmlpath = '你的路径/bloodimage_data/Annotations/' data_path = '你的路径/bloodimage_data/' imageset_path = '你的路径/bloodimage_data/ImageSets/' label_path = data_path + 'label/' # yolo的路径 to_data_path = '你的路径/yolov3/data/' # labels文件夹 if not os.path.exists(label_path): os.makedirs(label_path) # 根据sets指定的数据集(同时也是img的目录文件关键字)来生成对应的标签 for image_set in sets: # 读取每个集合的文件列表 set_filename = image_set + '.txt' image_ids = open(imageset_path+set_filename).read().strip().split() # 根据image_ids 结合绝对路径生成文件列表 with open(data_path + set_filename, 'w') as f: for image_id in image_ids: image_content = to_data_path + 'images/' + str(image_id) + '.jpg\n' f.write(image_content) convert_annotation(image_id, xmlpath,label_path, classes)
新增配置文件:
1 data/rbc.data : 这个主要的目的是告诉程序去哪里读取训练集、测试以及对应的目标标签。
classes=1
train=data/train.txt
valid=data/test.txt
names=data/rbc.names
backup=backup/
eval=coco
新建一个backup文件夹
2 修改配置文件
yolov3-tiny.cfg(修改之前先做了拷贝一个备份,结尾命名.bak)
[yolo]
mask = 3,4,5
anchors = 10,14, 23,27, 37,58, 81,82, 135,169, 344,319
classes= 1 # 从80改为 1
num=6
[convolutional]
size=1
stride=1
pad=1
filters=18 #3*(class + 4 + 1)
activation=linear
注意:把参数改好之后那些 # 之后的是不要的,如果行内有这些注释性的字符会出错的
3 获取weights
按照官网链接下载权重文件,和配置文件配套yolov3-tiny.weights。顺带说下,.weights文件是Darknet框架的模型权重,我们用pytorch生成的会是.pt文件,两者是可以互转的。
4 执行训练步骤
首先了解下训练能够传入的参数,原作者把一些程序参数放在了命令行里。在linux里,参数选项通常可以分为:
1)短选项(short option):由一个连字符和一个字母构成,例如:-a, -s等;
2)长选项(long options):由两个连字符和一些大小写字母组合的单词构成,例如:–size,–help等。
通常,一个程序会提供short option和long options两种形式,例如:ls -a,–all。另外,短选项(short option)是可以合并的,例如:-sh表示-s和-h的组合,如果要表示为一个选项需要用长选项–sh。
在我看来,短选项有点像args, 长选项有点像 kwargs。
python通常可以使用getopt或者sys.argv 来获取命令行传入的参数。
我看了下代码,作者用了argparse这个包,那么也就看看吧。train.py关于参数的设置和读取主要在这里。
import argparse if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--epochs', type=int, default=300) # 500200 batches at bs 16, 117263 COCO images = 273 epochs parser.add_argument('--batch-size', type=int, default=16) # effective bs = batch_size * accumulate = 16 * 4 = 64 parser.add_argument('--cfg', type=str, default='cfg/yolov3-spp.cfg', help='*.cfg path') parser.add_argument('--data', type=str, default='data/coco2017.data', help='*.data path') parser.add_argument('--multi-scale', action='store_true', help='adjust (67%% - 150%%) img_size every 10 batches') parser.add_argument('--img-size', nargs='+', type=int, default=[320, 640], help='[min_train, max-train, test]') parser.add_argument('--rect', action='store_true', help='rectangular training') parser.add_argument('--resume', action='store_true', help='resume training from last.pt') parser.add_argument('--nosave', action='store_true', help='only save final checkpoint') parser.add_argument('--notest', action='store_true', help='only test final epoch') parser.add_argument('--evolve', action='store_true', help='evolve hyperparameters') parser.add_argument('--bucket', type=str, default='', help='gsutil bucket') parser.add_argument('--cache-images', action='store_true', help='cache images for faster training') parser.add_argument('--weights', type=str, default='weights/yolov3-spp-ultralytics.pt', help='initial weights path') parser.add_argument('--name', default='', help='renames results.txt to results_name.txt if supplied') parser.add_argument('--device', default='', help='device id (i.e. 0 or 0,1 or cpu)') parser.add_argument('--adam', action='store_true', help='use adam optimizer') parser.add_argument('--single-cls', action='store_true', help='train as single-class dataset') parser.add_argument('--freeze-layers', action='store_true', help='Freeze non-output layers') opt = parser.parse_args() opt.weights = last if opt.resume and not opt.weights else opt.weights check_git_status() opt.cfg = check_file(opt.cfg) # check file opt.data = check_file(opt.data) # check file print(opt) opt.img_size.extend([opt.img_size[-1]] * (3 - len(opt.img_size))) # extend to 3 sizes (min, max, test) device = torch_utils.select_device(opt.device, apex=mixed_precision, batch_size=opt.batch_size)
我们根据实际情况修改参数,调用train.py进行计算。
# 需要声明的参数
--data data/rbc.data 数据
--cfg cfg/yolov3-tiny.cfg 模型配置文件
--weights weights/yolov3-tiny.weights 模型权重
--name data/rbs.name 标签名
--epochs 50 训练的轮数
# 因此(切到项目文件夹目录下),训练的命令为
python3 train.py --data data/rbc.data --cfg cfg/yolov3-tiny.cfg --weights weights/yolov3-tiny.weights --name data/rbs.name --epochs 50
训练后会在weights文件下产生新的pt权重数据,一个叫best.pt ,一个叫last.pt, 看名字就知道啥意思了。
因为这玩意比较耗时,所以可能分次训练。按定义的参数理解,有个resume参数。
python3 train.py --data data/rbc.data --cfg cfg/yolov3-tiny.cfg --weights weights/last.pt --name data/rbs.name --epochs 5 --resume
# 可以看到,这个是接着前面训练的
Model Summary: 37 layers, 8.66988e+06 parameters, 8.66988e+06 gradients
Optimizer groups: 13 .bias, 13 Conv2d.weight, 11 other
weights/last.pt has been trained for 49 epochs. Fine-tuning for 5 additional epochs.
Caching labels data/train.txt (309 found, 0 missing, 0 empty, 0 duplicate, for 309 images): 100%|█| 309/309 [00:00<00:
Caching labels data/test.txt (30 found, 0 missing, 0 empty, 0 duplicate, for 30 images): 100%|█| 30/30 [00:00<00:00, 2
Image sizes 320 - 640 train, 640 test
Using 4 dataloader workers
Starting training for 54 epochs...
Epoch gpu_mem GIoU obj cls total targets img_size
50/53 0G 1.87 5.2 0 7.07 275 512: 20%|▏| 4/20 [00:44<03:02, 11.44s/it
5 本来应该执行test.py来看测试结果的,我比较懒,就不看了
6 使用detect.py来直接运行。
执行语句后会读取samples下面的图片(事先把要批量预测的放在这个文件夹下),然后输出到output文件夹
python3 detect.py --cfg yolov3-tiny.cfg --weights weights/best.pt --names data/rbc.names
可以看到,经过50论的训练,模型已经可以产生一些有用的输出。虽然效果可能还不太好,但至少在应用层面是通了的。
待续
内容有点多,换一个帖子
待续
w 是只写方式
a 是追加方式
单步调试时,写的数据总是不出来,原因大概是文件被打开但是还没有关掉。这样有点不文明(虽然可能在函数中有自动关闭的机制,或者是打开未关闭的文件太多系统的自动操作)。
要立即显示结果要么就直接指定文件的close动作,或者使用 with open.
在做红细胞识别的实验时,如果只用MAC跑10个epochs那么啥也识别不出,至少要50个epochs。我的MAC跑了3个小时。
[convolutional]
size=1
stride=1
pad=1
filters=18 #3*(class + 4 + 1) 不要加行内注释
activation=linear
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。