Python - 深度学习系列1-目标识别 yolo

1 目的

实现基于yolo网络的目标识别。
使用github上开源的代码。那么需要做的事只有几样:

  • 1 原理。多少还是知道一下yolo的原理以及应用特点。
  • 2 环境。对应的安装包,特别是cpu、gpu的配置。
  • 3 数据。yolo的标签格式还是稍有不同的。
  • 4 模型。模型没啥可说的,但是预训练的参数怎么选要看看。
  • 5 服务封装。模型训练好了就可以封装为服务调用了。

这篇文章的内容很有帮助,不过里面也有不少坑,我下面边踩边说。

2 环境准备

2.1 MAC

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文件。

2.2 Ubuntu

cpu版本的应该和mac差不多。gpu版本的得等我的小伙伴帮我一起弄。
值得一提的是ubuntu作为生产机,一般不会是云主机(太浪费了),所以可以想象成一台放在办公室或者家里的台式机。
那么就要考虑如何进行ssh进行操作。
【这块 后补】

3 实验

目前我们已经有了可执行的模型代码,如果我们要自己用起来,那么还缺什么呢?
假设我们遇到了一个新的目标识别问题,我们应该会拿到图片,然后我们对这批图片进行标注(参考LabelImg工具),这时我们就有了数据和标签,理论上就可以开始训练了。
LabelImg操作:

Ctrl + u  加载目录中的所有图像,鼠标点击Open dir同功能
Ctrl + r  更改默认注释目标目录(xml文件保存的地址) 
Ctrl + s  保存
Ctrl + d  复制当前标签和矩形框
space     将当前图像标记为已验证
w         创建一个矩形框
d         下一张图片
a         上一张图片
del       删除选定的矩形框
Ctrl++    放大
Ctrl--    缩小
↑→↓←        键盘箭头移动选定的矩形框

通常的建模需要把数据分为训练、验证和测试。另外就是要把我们自己做的(VOC规范)的标签文件啥的转成模型认可的格式。
再之后则是根据我们训练的任务,调整模型参数。具体点就是我们预测的类别不同,选择的模型结构不同,对应的参数要调整。
最后就是下载模型的预训练权重,基于这个权重去计算就可以了。(这里我还没完全搞明白,预训练的权重是哪些,为什么可以给任何新任务使用)

实验分两步,一步是使用小数据集在MAC上进行初步验证。其意义在于证明过程可行,并且如果基于小数据出来结果还可以,那么这个模型的再利用就会很方便。
第二步则是利用稍大一点的数据集(VOC2007)来观察其表现,这时候可能会在意数据的大小,运行时间和效果等。

3.1 数据准备

使用一批医学影像图片,具体来说大约是400张左右的图片(640*480 , 大约25k一张),目标是识别其中的红细胞(RBC)。
数据项目地址,我们可以称为BloodImage。

红细胞也称红血球,在常规化验中英文常缩写成RBC,是血液中数量最多的一种血细胞,同时也是脊椎动物体内通过血液运送氧气的最主要的媒介,同时还具有免疫功能。哺乳动物成熟的红细胞是无核的,这意味着它们失去了DNA。红细胞也没有线粒体,它们通过分解葡萄糖释放能量。运输氧气,也运输一部分二氧化碳。运输二氧化碳时呈暗紫色,运输氧气时呈鲜红色。
红细胞会生成于骨髓之内。红细胞老化后,易导致血管堵塞,所以会自动返回骨髓深处,由白细胞负责销毁;或是在经过肝脏时,被巨噬细胞分解成为胆汁。

在这里插入图片描述
以下是原始文件的处理过程:

基于BloodImage:

  • 1 原始文件:图片和标注文件是一一对应的

    • Annotation 标注文件 xml
    • JPEGImages 图片 jpeg
  • 2 根据原始文件进行训练、测试和验证的数据集分割(test1_maketxt.py)

    • Annotation 标注文件 xml
    • JPEGImages 图片 jpeg
    • ImageSets 图片数据集 txt【新增】
  • 3 读取并解析xml里的数据,形成标签(test2_voc_label.py)

    • Annotation 标注文件 xml
    • JPEGImages 图片 jpeg
    • ImageSets 图片数据集 txt
    • labels 标签 txt 【新增】
    • train.txt 训练图片绝对地址文件【新增】
    • test.txt 测试图片绝对地址文件【新增】
    • val.txt 验证图片绝对地址文件【新增】

某个label文件的内容如下(因为识别红细胞是单目标任务,所以索引为0,否则可能为3,5 ,8 …):

标签索引箱体坐标(x,y,w,h)
00.48333333333333334 0.3585164835164835 0.19583333333333333 0.3763736263736264
00.5135416666666667 0.5686813186813188 0.6520833333333333 0.7087912087912088
  • 4 复制并更改图片文件夹
    • Annotation 标注文件 xml
    • JPEGImages 图片 jpeg
    • ImageSets 图片数据集 txt
    • labels 标签 txt
    • train.txt 训练图片绝对地址文件
    • test.txt 测试图片绝对地址文件
    • val.txt 验证图片绝对地址文件
    • images 图片(复制JPEGImages)【新增】

手动将对应文件搬过去(注意在生成图片绝对地址的时候要写目标位置的 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论的训练,模型已经可以产生一些有用的输出。虽然效果可能还不太好,但至少在应用层面是通了的。

3.2 基于voc 2007 数据的实验

待续

4 服务封装

内容有点多,换一个帖子

5 原理回顾与探索

待续

6 那些踩过的坑

  • 1 将voc的xml转为label
    open + w 还是 open + a

w 是只写方式
a 是追加方式
单步调试时,写的数据总是不出来,原因大概是文件被打开但是还没有关掉。这样有点不文明(虽然可能在函数中有自动关闭的机制,或者是打开未关闭的文件太多系统的自动操作)。
要立即显示结果要么就直接指定文件的close动作,或者使用 with open.

  • 2 运行几轮

在做红细胞识别的实验时,如果只用MAC跑10个epochs那么啥也识别不出,至少要50个epochs。我的MAC跑了3个小时。

  • 3 配置文件
    最好不要做行内注释,否则可能会出错。例如
[convolutional]
size=1
stride=1
pad=1
filters=18   #3*(class + 4 + 1) 不要加行内注释
activation=linear
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值