0 前言
最近学习了李牧的动手学深度学习V2,他在kaggle平台上发起了一个目标检测的比赛,本着动手学习的态度,先使用他人的代码模型跑通试试看,一方面是熟悉学校服务器的一些配置,一方面是学习代码。
检测主要包括:牛仔夹克、墨镜、靴子、牛仔帽、腰带
使用的是yolov5l,参考别人的模型代码,代码获取:【windows下使用Yolov5l用于牛仔行头检测】
他的一些代码配置有点小小的问题,在这里记录一下。
本次牛仔行头检测我使用的是ultralytics/yolov5 ,该仓库提供了s、m、l、x四个规模的预训练模型,我使用的预训练模型是Yolov5l,最后可以在公榜(也就是valid.csv)上达到60的MAP值。
1 环境配置
配置之前请确保已经安装好了英伟达显卡的驱动,在命令行中输入nvidia-smi会输出相应的显卡信息。由于我们使用的是anconda来控制虚拟环境,使用conda命令安装Pytorch的时候会自动检查依赖并进行安装,所以不需要额外在本地安装cuda。(这些我的没有问题,毕竟用的学校服务器,之前装好了anconda)
1.1 把代码下载到本地
执行下列命令。(下载完后一般是在C盘的具体用户文件夹下会有一个yolov5的文件夹,这个需要上传到服务器,如果是自己电脑放工程文件夹下就行)
cmd
git clone https://github.com/ultralytics/yolov5.git
1.2 创建并激活虚拟环境
在服务器上创建一个专门yolo使用的虚拟环境,然后激活转到yolo环境下操作;
conda create -n yolo python==3.8.5
conda activate yolo
1.3 安装相关模块包、库文件
1、安装GPU版本的Pytorch,注意30系列显卡是安培架构,只支持11版本的cuda。(我不知道学校的服务器什么情况,反正我装的10.2可以)
conda install pytorch==1.7.0 torchvision torchaudio cudatoolkit=10.2
这个我的报错了,说找不到pytorch==1.7.0,所以我去网站https://pytorch.org/找下载命令,CUDA可以选10.2或者11.1,这里我和他一致选10.2。
conda install pytorch torchvision torchaudio cudatoolkit=10.2 -c pytorch
2、安装程序所需的其他的第三方库
把下载的模型文件夹上传到服务器后,cd到文件目录下,然后进行相关环境依赖安装;安装要求都在requirements.txt文件中。
它这里会把所有满足要求比如大于等于某个版本的全部安装,时间有点久。
cd yolov5
pip install -r requirements.txt
我的一开始没成功,因为这是个requirements.txt文件,他原本没有加后缀txt;
后面又报错scipy安装不上:
ERROR: Could not find a version that satisfies the requirement scipy>=1.4.1 (from versions: none)
ERROR: No matching distribution found for scipy>=1.4.1
如果遇到安装不上的,就单独使用pip install scipy这种命令安装,然后再用下面的命令安装即可。
pip install -r requirements.txt
2 数据预处理
数据部分需要处理成yolo的数据格式,如果觉得比较麻烦的小伙伴可以使用我处理好的,链接如下:
链接:https://pan.baidu.com/s/1SGdjCTAq6Sa4LLgAG9Xx5w 提取码:v9re
为了方便,我直接使用的这个处理好的,想自己处理的,前言中给了链接,自行获取代码处理。
原始的数据包含四部分的内容:
images # 存放图片文件 test.csv # 私榜需要测试的图片 train.json # 训练的数据以及标注 vaild.csv # 公榜需要测试的图片
下载他的文件夹包括:
cow_yolo_dataset:
score: # 存放训练的图片文件
images:
test
train # 后面配置用到
val # 后面配置用到
lables:
test
train
val
test_val:# 测试的照片
test
val # 后面转换格式的时候用到
3 修改配置文件
所有的修改都是在yolov5之前下载的文件夹下进行操作。
3.1 在data目录下设置数据集的配置文件
我们在data目录下新建一个cow_data.yaml的文件,在文件中指定训练集和验证集的地址(需要更改为自己的存放路径)、数据集的目标数目和目标类名;(这个暂时还没找到在哪用,后续发现在运行的时候指定了这个文件)
# Custom data for safety helmet
# train and val data as 1) directory: path/images/, 2) file: path/images.txt, or 3) list: [path1/images/, path2/images/]
train: /data1/scm/2021/com/cow/cow_yolo_dataset/score/images/train # 这个就是上面train的文件路径,改成自己的
val: /data1/scm/2021/com/cow/cow_yolo_dataset/score/images/val # 这个就是训练的val的文件路径,改成自己的
# number of classes
nc: 5
# class names
names: ["belt", "sunglasses", "boot", "cowboy_hat", "jacket"]
3.2 在models目录下设置模型的配置文件
在models目录下新建一个cow_yolov5l.yaml的模型配置文件,这个文件应该是模型读取那一块要用的,在yolo.py的79行(盲猜的)。(实际也是在运行的时候指定的,这里改不改应该没什么问题)
具体内容如下:(无更改,直接使用)
# parameters
nc: 5 # number of classes # 修改为我们模型的类别数目
depth_multiple: 1.0 # model depth multiple
width_multiple: 1.0 # layer channel multiple
# anchors
anchors:
- [10,13, 16,30, 33,23] # P3/8
- [30,61, 62,45, 59,119] # P4/16
- [116,90, 156,198, 373,326] # P5/32
# YOLOv5 backbone
backbone:
# [from, number, module, args]
[[-1, 1, Focus, [64, 3]], # 0-P1/2
[-1, 1, Conv, [128, 3, 2]], # 1-P2/4
[-1, 3, C3, [128]],
[-1, 1, Conv, [256, 3, 2]], # 3-P3/8
[-1, 9, C3, [256]],
[-1, 1, Conv, [512, 3, 2]], # 5-P4/16
[-1, 9, C3, [512]],
[-1, 1, Conv, [1024, 3, 2]], # 7-P5/32
[-1, 1, SPP, [1024, [5, 9, 13]]],
[-1, 3, C3, [1024, False]], # 9
]
# YOLOv5 head
head:
[[-1, 1, Conv, [512, 1, 1]],
[-1, 1, nn.Upsample, [None, 2, \'nearest\']],
[[-1, 6], 1, Concat, [1]], # cat backbone P4
[-1, 3, C3, [512, False]], # 13
[-1, 1, Conv, [256, 1, 1]],
[-1, 1, nn.Upsample, [None, 2, \'nearest\']],
[[-1, 4], 1, Concat, [1]], # cat backbone P3
[-1, 3, C3, [256, False]], # 17 (P3/8-small)
[-1, 1, Conv, [256, 3, 2]],
[[-1, 14], 1, Concat, [1]], # cat head P4
[-1, 3, C3, [512, False]], # 20 (P4/16-medium)
[-1, 1, Conv, [512, 3, 2]],
[[-1, 10], 1, Concat, [1]], # cat head P5
[-1, 3, C3, [1024, False]], # 23 (P5/32-large)
[[17, 20, 23], 1, Detect, [nc, anchors]], # Detect(P3, P4, P5)
]
4 训练模型
首先从点这里下载预训练的模型yolov5l(yolov5l.pt)到本地,存放在yolov5的weights目录下(没有就创建weights文件夹)。
上面的过程中我们已经设置好了数据集和模型的配置文件,我们只需要在项目的根目录下执行下列代码即可开始训练,由于我的显卡只有8G,所以这里的batchsize设置为4,训练的轮数设置为200;
(由于我用的服务器,我申请了两块CPU,所以batchsize我改大了也没什么问题,轮数200会有点久,差不多3小时吧,batchsize设置为4,轮数设置为50的时候是0.5小时,我的batch大,所以久点)
具体的命令如下:(记得cd到train.py的文件目录下,这个文件yolov5有,我是直接用的,没有复制他给出的代码)
python train.py --data data/cow_data.yaml --cfg models/cow_yolov5l.yaml --weights weights/yolov5l.pt --batch-size 8 --epochs 200
训练的结果会保存在runs/train/exp目录下,其中有各种可视化的过程图,weights目录下是训练好的权重文件,也就是我们最后需要的模型。
!!!注意:在这里可以看到,上面的配置文件在这里运行的时候指定了,所以我们可以不要在源码中进行更改。
到目前没什么问题,正在跑模型中,跑的有点久。
4.1 训练运行结果
可视化结果
训练结果
训练锚框标记结果
验证标记结果
验证预测结果
5 测试模型
Yolov5中自带的代码可以直接调用模型对结果进行推理,我对detect的代码进行了修改,直接运行cow_detect_kaggle.py就能将图片和文本文件的检测结果保存在runs/detect目录下,代码如下:
(这里的文件夹没有这个文件,我们新建一个python文件在train同一级目录下即可,这里要注意的是158行的目录要改成你自己的目录!!!改前面的就可以了)
1 # 用于生成txt文件 2 import argparse 3 import time 4 from pathlib import Path 5 6 import cv2 7 import torch 8 import torch.backends.cudnn as cudnn 9 10 from models.experimental import attempt_load 11 from utils.datasets import LoadStreams, LoadImages 12 from utils.general import check_img_size, check_requirements, check_imshow, non_max_suppression, apply_classifier, \ 13 scale_coords, xyxy2xywh, strip_optimizer, set_logging, increment_path, save_one_box 14 from utils.plots import colors, plot_one_box 15 from utils.torch_utils import select_device, load_classifier, time_sync # time_synchronized 16 # import utils.torch_utils 17 18 @torch.no_grad() 19 def detect(opt): 20 source, weights, view_img, save_txt, imgsz = opt.source, opt.weights, opt.view_img, opt.save_txt, opt.img_size 21 save_img = not opt.nosave and not source.endswith(\'.txt\') # save inference images 22 webcam = source.isnumeric() or source.endswith(\'.txt\') or source.lower().startswith( 23 (\'rtsp://\', \'rtmp://\', \'http://\', \'https://\')) 24 save_txt = True 25 print(save_txt) 26 27 # Directories 28 save_dir = increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok) # increment run 29 (save_dir / \'labels\' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir 30 31 # Initialize 32 set_logging() 33 device = select_device(opt.device) 34 half = opt.half and device.type != \'cpu\' # half precision only supported on CUDA 35 36 # Load model 37 model = attempt_load(weights, map_location=device) # load FP32 model 38 stride = int(model.stride.max()) # model stride 39 imgsz = check_img_size(imgsz, s=stride) # check img_size 40 names = model.module.names if hasattr(model, \'module\') else model.names # get class names 41 if half: 42 model.half() # to FP16 43 44 # Second-stage classifier 45 classify = False 46 if classify: 47 modelc = load_classifier(name=\'resnet101\', n=2) # initialize 48 modelc.load_state_dict(torch.load(\'weights/resnet101.pt\', map_location=device)[\'model\']).to(device).eval() 49 50 # Set Dataloader 51 vid_path, vid_writer = None, None 52 if webcam: 53 view_img = check_imshow() 54 cudnn.benchmark = True # set True to speed up constant image size inference 55 dataset = LoadStreams(source, img_size=imgsz, stride=stride) 56 else: 57 dataset = LoadImages(source, img_size=imgsz, stride=stride) 58 59 # Run inference 60 if device.type != \'cpu\': 61 model(torch.zeros(1, 3, imgsz, imgsz).to(device).type_as(next(model.parameters()))) # run once 62 t0 = time.time() 63 for path, img, im0s, vid_cap in dataset: 64 img = torch.from_numpy(img).to(device) 65 img = img.half() if half else img.float() # uint8 to fp16/32 66 img /= 255.0 # 0 - 255 to 0.0 - 1.0 67 if img.ndimension() == 3: 68 img = img.unsqueeze(0) 69 70 # Inference 71 t1 = time_sync() # time_synchronized() 72 pred = model(img, augment=opt.augment)[0] 73 74 # Apply NMS 75 pred = non_max_suppression(pred, opt.conf_thres, opt.iou_thres, opt.classes, opt.agnostic_nms, 76 max_det=opt.max_det) 77 t2 = time_sync() # time_synchronized() 78 79 # Apply Classifier 80 if classify: 81 pred = apply_classifier(pred, modelc, img, im0s) 82 83 # Process detections 84 for i, det in enumerate(pred): # detections per image 85 if webcam: # batch_size >= 1 86 p, s, im0, frame = path[i], f\'{i}: \', im0s[i].copy(), dataset.count 87 else: 88 p, s, im0, frame = path, \'\', im0s.copy(), getattr(dataset, \'frame\', 0) 89 90 p = Path(p) # to Path 91 save_path = str(save_dir / p.name) # img.jpg 92 txt_path = str(save_dir / \'labels\' / p.stem) + (\'\' if dataset.mode == \'image\' else f\'_{frame}\') # img.txt 93 s += \'%gx%g \' % img.shape[2:] # print string 94 gn = torch.tensor(im0.shape)[[1, 0, 1, 0]] # normalization gain whwh 95 imc = im0.copy() if opt.save_crop else im0 # for opt.save_crop 96 if len(det): 97 # Rescale boxes from img_size to im0 size 98 det[:, :4] = scale_coords(img.shape[2:], det[:, :4], im0.shape).round() 99 100 # Print results 101 for c in det[:, -1].unique(): 102 n = (det[:, -1] == c).sum() # detections per class 103 s += f"{n} {names[int(c)]}{\'s\' * (n > 1)}, " # add to string 104 105 # Write results 106 for *xyxy, conf, cls in reversed(det): 107 if save_txt: # Write to file 108 xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh 109 line = (cls, *xywh, conf) if opt.save_conf else (cls, *xywh) # label format 110 with open(txt_path + \'.txt\', \'a\') as f: 111 f.write((\'%g \' * len(line)).rstrip() % line + \'\n\') 112 113 if save_img or opt.save_crop or view_img: # Add bbox to image 114 c = int(cls) # integer class 115 label = None if opt.hide_labels else (names[c] if opt.hide_conf else f\'{names[c]} {conf:.2f}\') 116 plot_one_box(xyxy, im0, label=label, color=colors(c, True), line_thickness=opt.line_thickness) 117 if opt.save_crop: 118 save_one_box(xyxy, imc, file=save_dir / \'crops\' / names[c] / f\'{p.stem}.jpg\', BGR=True) 119 120 # Print time (inference + NMS) 121 print(f\'{s}Done. ({t2 - t1:.3f}s)\') 122 123 # Stream results 124 if view_img: 125 cv2.imshow(str(p), im0) 126 cv2.waitKey(1) # 1 millisecond 127 128 # Save results (image with detections) 129 if save_img: 130 if dataset.mode == \'image\': 131 cv2.imwrite(save_path, im0) 132 else: # \'video\' or \'stream\' 133 if vid_path != save_path: # new video 134 vid_path = save_path 135 if isinstance(vid_writer, cv2.VideoWriter): 136 vid_writer.release() # release previous video writer 137 if vid_cap: # video 138 fps = vid_cap.get(cv2.CAP_PROP_FPS) 139 w = int(vid_cap.get(cv2.CAP_PROP_FRAME_WIDTH)) 140 h = int(vid_cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) 141 else: # stream 142 fps, w, h = 30, im0.shape[1], im0.shape[0] 143 save_path += \'.mp4\' 144 vid_writer = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*\'mp4v\'), fps, (w, h)) 145 vid_writer.write(im0) 146 147 if save_txt or save_img: 148 s = f"\n{len(list(save_dir.glob(\'labels/*.txt\')))} labels saved to {save_dir / \'labels\'}" if save_txt else \'\' 149 print(f"Results saved to {save_dir}{s}") 150 151 print(f\'Done. ({time.time() - t0:.3f}s)\') 152 153 154 if __name__ == \'__main__\': 155 parser = argparse.ArgumentParser() 156 parser.add_argument(\'--weights\', nargs=\'+\', type=str, default=\'runs/train/exp/weights/best.pt\', # 设置模型位置 157 help=\'model.pt path(s)\') 158 parser.add_argument(\'--source\', type=str, default=\'../kaggleCode/cowboyOutfits_object_detection/cow_yolo_dataset/cow_yolo_dataset/test_val/val\', # todo 设置验证集图片位置 159 help=\'source\') # file/folder, 0 for webcam 160 parser.add_argument(\'--img-size\', type=int, default=640, help=\'inference size (pixels)\') 161 parser.add_argument(\'--conf-thres\', type=float, default=0.25, help=\'object confidence threshold\') 162 parser.add_argument(\'--iou-thres\', type=float, default=0.45, help=\'IOU threshold for NMS\') 163 parser.add_argument(\'--max-det\', type=int, default=1000, help=\'maximum number of detections per image\') 164 parser.add_argument(\'--device\', default=\'\', help=\'cuda device, i.e. 0 or 0,1,2,3 or cpu\') 165 parser.add_argument(\'--view-img\', action=\'store_true\', help=\'display results\') 166 parser.add_argument(\'--save-txt\', default="True", action=\'store_true\', 167 help=\'save results to *.txt\') # todo 设置是否保存txt检测结果 168 parser.add_argument(\'--save-conf\', default="True", action=\'store_true\', 169 help=\'save confidences in --save-txt labels\') # todo 设置是否保存置信度 170 parser.add_argument(\'--save-crop\', action=\'store_true\', help=\'save cropped prediction boxes\') 171 parser.add_argument(\'--nosave\', action=\'store_true\', help=\'do not save images/videos\') 172 parser.add_argument(\'--classes\', nargs=\'+\', type=int, help=\'filter by class: --class 0, or --class 0 2 3\') 173 parser.add_argument(\'--agnostic-nms\', action=\'store_true\', help=\'class-agnostic NMS\') 174 parser.add_argument(\'--augment\', action=\'store_true\', help=\'augmented inference\') 175 parser.add_argument(\'--update\', action=\'store_true\', help=\'update all models\') 176 parser.add_argument(\'--project\', default=\'runs/detect\', help=\'save results to project/name\') 177 parser.add_argument(\'--name\', default=\'exp\', help=\'save results to project/name\') 178 parser.add_argument(\'--exist-ok\', action=\'store_true\', help=\'existing project/name ok, do not increment\') 179 parser.add_argument(\'--line-thickness\', default=3, type=int, help=\'bounding box thickness (pixels)\') 180 parser.add_argument(\'--hide-labels\', default=False, action=\'store_true\', help=\'hide labels\') 181 parser.add_argument(\'--hide-conf\', default=False, action=\'store_true\', help=\'hide confidences\') 182 parser.add_argument(\'--half\', action=\'store_true\', help=\'use FP16 half-precision inference\') 183 opt = parser.parse_args() 184 print(opt) 185 check_requirements(exclude=(\'tensorboard\', \'pycocotools\', \'thop\')) 186 187 if opt.update: # update all models (to fix SourceChangeWarning) 188 for opt.weights in [\'yolov5s.pt\', \'yolov5m.pt\', \'yolov5l.pt\', \'yolov5x.pt\']: 189 detect(opt=opt) 190 strip_optimizer(opt.weights) 191 else: 192 detect(opt=opt)
5.1 测试结果
会输出一连串这样的数字,主要是你测试生成锚框的图片的数字
6 生成可提交的测试文件
为了能够在竞赛的网站上进行提交,我们还需要将得到的txt格式的yolo文件转化一下,转化为竞赛网站所需的json格式,转化的代码如下:
!!!注意:需要修改的地方已标记出来
1 import os 2 import pandas as pd 3 # import tqdm 4 from PIL import Image 5 import zipfile 6 7 8 9 10 # convert yolo to coco annotation format 11 def yolo2cc_bbox(img_width, img_height, bbox): 12 x = (bbox[0] - bbox[2] * 0.5) * img_width 13 y = (bbox[1] - bbox[3] * 0.5) * img_height 14 w = bbox[2] * img_width 15 h = bbox[3] * img_height 16 return (x, y, w, h) 17 18 19 20 21 22 def make_submission(df, PRED_PATH, IMAGE_PATH): 23 output = [] 24 # for i in tqdm(range(len(df))): 25 for i in range(len(df)): 26 # print(i) 27 row = df.loc[i] 28 image_id = row[\'id\'] 29 file_name = row[\'file_name\'].split(\'.\')[0] 30 if f\'{file_name}.txt\' in prediction_files:# 这里文件名不在里面,如果val的文件在预测的文件里,就打印;问题找到了,出在cow_detect_kaggle.py的val的图片路径搞错了 31 print(file_name) 32 img = Image.open(f\'{IMAGE_PATH}/{file_name}.jpg\') 33 width, height = img.size 34 with open(f\'{PRED_PATH}/{file_name}.txt\', \'r\') as file: 35 for line in file: 36 preds = line.strip(\'\n\').split(\' \') 37 preds = list(map(float, preds)) # conver string to float 38 print(preds) 39 print(preds[1:-1]) 40 cc_bbox = yolo2cc_bbox(width, height, preds[1:-1]) 41 result = { 42 \'image_id\': image_id, 43 \'category_id\': re_cate_id_map[preds[0]], 44 \'bbox\': cc_bbox, 45 \'score\': preds[-1] 46 } 47 48 output.append(result) 49 50 return output 51 52 53 54 55 if __name__ == \'__main__\': 56 57 valid_df = pd.read_csv(\'../kaggleCode/cowboyOutfits_object_detection/valid.csv\') # 这是你kaggle下载的原始的数据文件下的valid.csv
58 # test_df = pd.read_csv(\'../kaggleCode/cowboyOutfits_object_detection/test.csv\')
59 # valid_df.head() 60 61 cate_id_map = {87: 0, 1034: 1, 131: 2, 318: 3, 588: 4} 62 PRED_PATH = "../kaggleCode/cowboyOutfits_object_detection/yolov5/runs/detect/exp/labels/" # 这是你运行之后生成的文件目录
63 IMAGE_PATH = "../kaggleCode/cowboyOutfits_object_detection/images/" # 这是你kaggle下载的原始的数据文件下的图片目录
64 65 #with open(\'../kaggleCode/cowboyOutfits_object_detection/yolov5/runs/detect/exp/labels/0030eb25a4d98400.txt\', \'r\') as file:
66 # for line in file: 67 # print(line) 68 69 # Image.open(\'../kaggleCode/cowboyOutfits_object_detection/yolov5/runs/detect/exp/0030eb25a4d98400.jpg\')
70 71 # list our prediction files path 72 prediction_files = os.listdir(PRED_PATH) 73 print(\'Number of test images with detections: \n\', len(prediction_files)) 74 75 # reverse the categories numer to the origin id 76 re_cate_id_map = dict(zip(cate_id_map.values(), cate_id_map.keys())) 77 78 print(\'origin id: \n\', re_cate_id_map) 79 80 81 sub_data = make_submission(valid_df, PRED_PATH, IMAGE_PATH) 82 83 op_pd = pd.DataFrame(sub_data) 84 85 op_pd.sample(10) 86 87 # 这个文件夹的目录都没有,需要自己创建,创建在工程文件下就行,改成自己的,answer.json复制train.json改下名字 88 op_pd.to_json(\'../kaggleCode/cowboyOutfits_object_detection/cow_yolo_dataset/cow_answer/answer.json\', orient=\'records\') 89 zf = zipfile.ZipFile(\'../kaggleCode/cowboyOutfits_object_detection/cow_yolo_dataset/cow_answer/sample_answer.zip\', \'w\')
90 zf.write(\'../kaggleCode/cowboyOutfits_object_detection/cow_yolo_dataset/cow_answer/answer.json\', \'answer.json\')
91 zf.close()
6.1 提交结果
上面会生成answer.json和sample_answer.zip两个文件,我们在网站上提交sample_answer.zip即可。