├── .gitignore ├── LICENSE ├── README.md ├── VOCdevkit └── VOC2007 │ ├── Annotations │ └── README.md │ ├── ImageSets │ └── Main │ │ └── README.md │ └── JPEGImages │ └── README.md ├── get_map.py ├── img └── street.jpg ├── kmeans_for_anchors.py ├── logs └── README.md ├── model_data ├── coco_classes.txt ├── simhei.ttf ├── voc_classes.txt └── yolo_anchors.txt ├── nets ├── CSPdarknet53_tiny.py ├── __init__.py ├── attention.py ├── yolo.py └── yolo_training.py ├── predict.py ├── requirements.txt ├── summary.py ├── train.py ├── utils ├── __init__.py ├── callbacks.py ├── dataloader.py ├── utils.py ├── utils_bbox.py ├── utils_fit.py └── utils_map.py ├── utils_coco ├── coco_annotation.py └── get_map_coco.py ├── voc_annotation.py ├── yolo.py └── 常见问题汇总.md /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore map, miou, datasets 2 | map_out/ 3 | miou_out/ 4 | VOCdevkit/ 5 | datasets/ 6 | Medical_Datasets/ 7 | lfw/ 8 | logs/ 9 | model_data/ 10 | .temp_map_out/ 11 | 12 | # Byte-compiled / optimized / DLL files 13 | __pycache__/ 14 | *.py[cod] 15 | *$py.class 16 | 17 | # C extensions 18 | *.so 19 | 20 | # Distribution / packaging 21 | .Python 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | pip-wheel-metadata/ 35 | share/python-wheels/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | MANIFEST 40 | 41 | # PyInstaller 42 | # Usually these files are written by a python script from a template 43 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 44 | *.manifest 45 | *.spec 46 | 47 | # Installer logs 48 | pip-log.txt 49 | pip-delete-this-directory.txt 50 | 51 | # Unit test / coverage reports 52 | htmlcov/ 53 | .tox/ 54 | .nox/ 55 | .coverage 56 | .coverage.* 57 | .cache 58 | nosetests.xml 59 | coverage.xml 60 | *.cover 61 | *.py,cover 62 | .hypothesis/ 63 | .pytest_cache/ 64 | 65 | # Translations 66 | *.mo 67 | *.pot 68 | 69 | # Django stuff: 70 | *.log 71 | local_settings.py 72 | db.sqlite3 73 | db.sqlite3-journal 74 | 75 | # Flask stuff: 76 | instance/ 77 | .webassets-cache 78 | 79 | # Scrapy stuff: 80 | .scrapy 81 | 82 | # Sphinx documentation 83 | docs/_build/ 84 | 85 | # PyBuilder 86 | target/ 87 | 88 | # Jupyter Notebook 89 | .ipynb_checkpoints 90 | 91 | # IPython 92 | profile_default/ 93 | ipython_config.py 94 | 95 | # pyenv 96 | .python-version 97 | 98 | # pipenv 99 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 100 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 101 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 102 | # install all needed dependencies. 103 | #Pipfile.lock 104 | 105 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 106 | __pypackages__/ 107 | 108 | # Celery stuff 109 | celerybeat-schedule 110 | celerybeat.pid 111 | 112 | # SageMath parsed files 113 | *.sage.py 114 | 115 | # Environments 116 | .env 117 | .venv 118 | env/ 119 | venv/ 120 | ENV/ 121 | env.bak/ 122 | venv.bak/ 123 | 124 | # Spyder project settings 125 | .spyderproject 126 | .spyproject 127 | 128 | # Rope project settings 129 | .ropeproject 130 | 131 | # mkdocs documentation 132 | /site 133 | 134 | # mypy 135 | .mypy_cache/ 136 | .dmypy.json 137 | dmypy.json 138 | 139 | # Pyre type checker 140 | .pyre/ 141 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 JiaQi Xu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## YOLOV4-Tiny:You Only Look Once-Tiny目标检测模型在Pytorch当中的实现 2 | --- 3 | 4 | ## 目录 5 | 1. [仓库更新 Top News](#仓库更新) 6 | 2. [相关仓库 Related code](#相关仓库) 7 | 3. [性能情况 Performance](#性能情况) 8 | 4. [所需环境 Environment](#所需环境) 9 | 5. [文件下载 Download](#文件下载) 10 | 6. [训练步骤 How2train](#训练步骤) 11 | 7. [预测步骤 How2predict](#预测步骤) 12 | 8. [评估步骤 How2eval](#评估步骤) 13 | 9. [参考资料 Reference](#Reference) 14 | 15 | ## Top News 16 | **`2022-04`**:**支持多GPU训练,新增各个种类目标数量计算,新增heatmap。** 17 | 18 | **`2022-03`**:**进行了大幅度的更新,修改了loss组成,使得分类、目标、回归loss的比例合适、支持step、cos学习率下降法、支持adam、sgd优化器选择、支持学习率根据batch_size自适应调整、新增图片裁剪。** 19 | BiliBili视频中的原仓库地址为:https://github.com/bubbliiiing/yolov4-tiny-pytorch/tree/bilibili 20 | 21 | **`2021-10`**:**进行了大幅度的更新,增加了大量注释、增加了大量可调整参数、对代码的组成模块进行修改、增加fps、视频预测、批量预测等功能。** 22 | 23 | ## 相关仓库 24 | | 模型 | 路径 | 25 | | :----- | :----- | 26 | YoloV3 | https://github.com/bubbliiiing/yolo3-pytorch 27 | Efficientnet-Yolo3 | https://github.com/bubbliiiing/efficientnet-yolo3-pytorch 28 | YoloV4 | https://github.com/bubbliiiing/yolov4-pytorch 29 | YoloV4-tiny | https://github.com/bubbliiiing/yolov4-tiny-pytorch 30 | Mobilenet-Yolov4 | https://github.com/bubbliiiing/mobilenet-yolov4-pytorch 31 | YoloV5-V5.0 | https://github.com/bubbliiiing/yolov5-pytorch 32 | YoloV5-V6.1 | https://github.com/bubbliiiing/yolov5-v6.1-pytorch 33 | YoloX | https://github.com/bubbliiiing/yolox-pytorch 34 | YoloV7 | https://github.com/bubbliiiing/yolov7-pytorch 35 | YoloV7-tiny | https://github.com/bubbliiiing/yolov7-tiny-pytorch 36 | 37 | ## 性能情况 38 | | 训练数据集 | 权值文件名称 | 测试数据集 | 输入图片大小 | mAP 0.5:0.95 | mAP 0.5 | 39 | | :-----: | :-----: | :------: | :------: | :------: | :-----: | 40 | | VOC07+12+COCO | [yolov4_tiny_weights_voc.pth](https://github.com/bubbliiiing/yolov4-tiny-pytorch/releases/download/v1.0/yolov4_tiny_weights_voc.pth) | VOC-Test07 | 416x416 | - | 77.8 41 | | VOC07+12+COCO | [yolov4_tiny_weights_voc_SE.pth](https://github.com/bubbliiiing/yolov4-tiny-pytorch/releases/download/v1.0/yolov4_tiny_weights_voc_SE.pth) | VOC-Test07 | 416x416 | - | 78.4 42 | | VOC07+12+COCO | [yolov4_tiny_weights_voc_CBAM.pth](https://github.com/bubbliiiing/yolov4-tiny-pytorch/releases/download/v1.0/yolov4_tiny_weights_voc_CBAM.pth) | VOC-Test07 | 416x416 | - | 78.6 43 | | VOC07+12+COCO | [yolov4_tiny_weights_voc_ECA.pth](https://github.com/bubbliiiing/yolov4-tiny-pytorch/releases/download/v1.0/yolov4_tiny_weights_voc_ECA.pth) | VOC-Test07 | 416x416 | - | 77.6 44 | | COCO-Train2017 | [yolov4_tiny_weights_coco.pth](https://github.com/bubbliiiing/yolov4-tiny-pytorch/releases/download/v1.0/yolov4_tiny_weights_coco.pth) | COCO-Val2017 | 416x416 | 21.5 | 41.0 45 | 46 | ## 所需环境 47 | torch==1.2.0 48 | 49 | ## 文件下载 50 | 训练所需的各类权值均可在百度网盘中下载。 51 | 链接: https://pan.baidu.com/s/1ABR6lOd0_cs5_2DORrMSRw 52 | 提取码: iauv 53 | 54 | VOC数据集下载地址如下,里面已经包括了训练集、测试集、验证集(与测试集一样),无需再次划分: 55 | 链接: https://pan.baidu.com/s/19Mw2u_df_nBzsC2lg20fQA 56 | 提取码: j5ge 57 | 58 | ## 训练步骤 59 | ### a、训练VOC07+12数据集 60 | 1. 数据集的准备 61 | **本文使用VOC格式进行训练,训练前需要下载好VOC07+12的数据集,解压后放在根目录** 62 | 63 | 2. 数据集的处理 64 | 修改voc_annotation.py里面的annotation_mode=2,运行voc_annotation.py生成根目录下的2007_train.txt和2007_val.txt。 65 | 66 | 3. 开始网络训练 67 | train.py的默认参数用于训练VOC数据集,直接运行train.py即可开始训练。 68 | 69 | 4. 训练结果预测 70 | 训练结果预测需要用到两个文件,分别是yolo.py和predict.py。我们首先需要去yolo.py里面修改model_path以及classes_path,这两个参数必须要修改。 71 | **model_path指向训练好的权值文件,在logs文件夹里。 72 | classes_path指向检测类别所对应的txt。** 73 | 完成修改后就可以运行predict.py进行检测了。运行后输入图片路径即可检测。 74 | 75 | ### b、训练自己的数据集 76 | 1. 数据集的准备 77 | **本文使用VOC格式进行训练,训练前需要自己制作好数据集,** 78 | 训练前将标签文件放在VOCdevkit文件夹下的VOC2007文件夹下的Annotation中。 79 | 训练前将图片文件放在VOCdevkit文件夹下的VOC2007文件夹下的JPEGImages中。 80 | 81 | 2. 数据集的处理 82 | 在完成数据集的摆放之后,我们需要利用voc_annotation.py获得训练用的2007_train.txt和2007_val.txt。 83 | 修改voc_annotation.py里面的参数。第一次训练可以仅修改classes_path,classes_path用于指向检测类别所对应的txt。 84 | 训练自己的数据集时,可以自己建立一个cls_classes.txt,里面写自己所需要区分的类别。 85 | model_data/cls_classes.txt文件内容为: 86 | ```python 87 | cat 88 | dog 89 | ... 90 | ``` 91 | 修改voc_annotation.py中的classes_path,使其对应cls_classes.txt,并运行voc_annotation.py。 92 | 93 | 3. 开始网络训练 94 | **训练的参数较多,均在train.py中,大家可以在下载库后仔细看注释,其中最重要的部分依然是train.py里的classes_path。** 95 | **classes_path用于指向检测类别所对应的txt,这个txt和voc_annotation.py里面的txt一样!训练自己的数据集必须要修改!** 96 | 修改完classes_path后就可以运行train.py开始训练了,在训练多个epoch后,权值会生成在logs文件夹中。 97 | 98 | 4. 训练结果预测 99 | 训练结果预测需要用到两个文件,分别是yolo.py和predict.py。在yolo.py里面修改model_path以及classes_path。 100 | **model_path指向训练好的权值文件,在logs文件夹里。 101 | classes_path指向检测类别所对应的txt。** 102 | 完成修改后就可以运行predict.py进行检测了。运行后输入图片路径即可检测。 103 | 104 | ## 预测步骤 105 | ### a、使用预训练权重 106 | 1. 下载完库后解压,在百度网盘下载yolo_weights.pth,放入model_data,运行predict.py,输入 107 | ```python 108 | img/street.jpg 109 | ``` 110 | 2. 在predict.py里面进行设置可以进行fps测试和video视频检测。 111 | ### b、使用自己训练的权重 112 | 1. 按照训练步骤训练。 113 | 2. 在yolo.py文件里面,在如下部分修改model_path和classes_path使其对应训练好的文件;**model_path对应logs文件夹下面的权值文件,classes_path是model_path对应分的类**。 114 | ```python 115 | _defaults = { 116 | #--------------------------------------------------------------------------# 117 | # 使用自己训练好的模型进行预测一定要修改model_path和classes_path! 118 | # model_path指向logs文件夹下的权值文件,classes_path指向model_data下的txt 119 | # 如果出现shape不匹配,同时要注意训练时的model_path和classes_path参数的修改 120 | #--------------------------------------------------------------------------# 121 | "model_path" : 'model_data/yolov4_tiny_weights_coco.pth', 122 | "classes_path" : 'model_data/coco_classes.txt', 123 | #---------------------------------------------------------------------# 124 | # anchors_path代表先验框对应的txt文件,一般不修改。 125 | # anchors_mask用于帮助代码找到对应的先验框,一般不修改。 126 | #---------------------------------------------------------------------# 127 | "anchors_path" : 'model_data/yolo_anchors.txt', 128 | "anchors_mask" : [[3,4,5], [1,2,3]], 129 | #-------------------------------# 130 | # 所使用的注意力机制的类型 131 | # phi = 0为不使用注意力机制 132 | # phi = 1为SE 133 | # phi = 2为CBAM 134 | # phi = 3为ECA 135 | #-------------------------------# 136 | "phi" : 0, 137 | #---------------------------------------------------------------------# 138 | # 输入图片的大小,必须为32的倍数。 139 | #---------------------------------------------------------------------# 140 | "input_shape" : [416, 416], 141 | #---------------------------------------------------------------------# 142 | # 只有得分大于置信度的预测框会被保留下来 143 | #---------------------------------------------------------------------# 144 | "confidence" : 0.5, 145 | #---------------------------------------------------------------------# 146 | # 非极大抑制所用到的nms_iou大小 147 | #---------------------------------------------------------------------# 148 | "nms_iou" : 0.3, 149 | #---------------------------------------------------------------------# 150 | # 该变量用于控制是否使用letterbox_image对输入图像进行不失真的resize, 151 | # 在多次测试后,发现关闭letterbox_image直接resize的效果更好 152 | #---------------------------------------------------------------------# 153 | "letterbox_image" : False, 154 | #-------------------------------# 155 | # 是否使用Cuda 156 | # 没有GPU可以设置成False 157 | #-------------------------------# 158 | "cuda" : True, 159 | } 160 | ``` 161 | 3. 运行predict.py,输入 162 | ```python 163 | img/street.jpg 164 | ``` 165 | 4. 在predict.py里面进行设置可以进行fps测试和video视频检测。 166 | 167 | ## 评估步骤 168 | ### a、评估VOC07+12的测试集 169 | 1. 本文使用VOC格式进行评估。VOC07+12已经划分好了测试集,无需利用voc_annotation.py生成ImageSets文件夹下的txt。 170 | 2. 在yolo.py里面修改model_path以及classes_path。**model_path指向训练好的权值文件,在logs文件夹里。classes_path指向检测类别所对应的txt。** 171 | 3. 运行get_map.py即可获得评估结果,评估结果会保存在map_out文件夹中。 172 | 173 | ### b、评估自己的数据集 174 | 1. 本文使用VOC格式进行评估。 175 | 2. 如果在训练前已经运行过voc_annotation.py文件,代码会自动将数据集划分成训练集、验证集和测试集。如果想要修改测试集的比例,可以修改voc_annotation.py文件下的trainval_percent。trainval_percent用于指定(训练集+验证集)与测试集的比例,默认情况下 (训练集+验证集):测试集 = 9:1。train_percent用于指定(训练集+验证集)中训练集与验证集的比例,默认情况下 训练集:验证集 = 9:1。 176 | 3. 利用voc_annotation.py划分测试集后,前往get_map.py文件修改classes_path,classes_path用于指向检测类别所对应的txt,这个txt和训练时的txt一样。评估自己的数据集必须要修改。 177 | 4. 在yolo.py里面修改model_path以及classes_path。**model_path指向训练好的权值文件,在logs文件夹里。classes_path指向检测类别所对应的txt。** 178 | 5. 运行get_map.py即可获得评估结果,评估结果会保存在map_out文件夹中。 179 | 180 | ## Reference 181 | https://github.com/qqwweee/keras-yolo3/ 182 | https://github.com/Cartucho/mAP 183 | https://github.com/Ma-Dan/keras-yolo4 184 | -------------------------------------------------------------------------------- /VOCdevkit/VOC2007/Annotations/README.md: -------------------------------------------------------------------------------- 1 | 存放标签文件 -------------------------------------------------------------------------------- /VOCdevkit/VOC2007/ImageSets/Main/README.md: -------------------------------------------------------------------------------- 1 | 存放训练索引文件 -------------------------------------------------------------------------------- /VOCdevkit/VOC2007/JPEGImages/README.md: -------------------------------------------------------------------------------- 1 | 存放图片文件 -------------------------------------------------------------------------------- /get_map.py: -------------------------------------------------------------------------------- 1 | import os 2 | import xml.etree.ElementTree as ET 3 | 4 | from PIL import Image 5 | from tqdm import tqdm 6 | 7 | from utils.utils import get_classes 8 | from utils.utils_map import get_coco_map, get_map 9 | from yolo import YOLO 10 | 11 | if __name__ == "__main__": 12 | ''' 13 | Recall和Precision不像AP是一个面积的概念,因此在门限值(Confidence)不同时,网络的Recall和Precision值是不同的。 14 | 默认情况下,本代码计算的Recall和Precision代表的是当门限值(Confidence)为0.5时,所对应的Recall和Precision值。 15 | 16 | 受到mAP计算原理的限制,网络在计算mAP时需要获得近乎所有的预测框,这样才可以计算不同门限条件下的Recall和Precision值 17 | 因此,本代码获得的map_out/detection-results/里面的txt的框的数量一般会比直接predict多一些,目的是列出所有可能的预测框, 18 | ''' 19 | #------------------------------------------------------------------------------------------------------------------# 20 | # map_mode用于指定该文件运行时计算的内容 21 | # map_mode为0代表整个map计算流程,包括获得预测结果、获得真实框、计算VOC_map。 22 | # map_mode为1代表仅仅获得预测结果。 23 | # map_mode为2代表仅仅获得真实框。 24 | # map_mode为3代表仅仅计算VOC_map。 25 | # map_mode为4代表利用COCO工具箱计算当前数据集的0.50:0.95map。需要获得预测结果、获得真实框后并安装pycocotools才行 26 | #-------------------------------------------------------------------------------------------------------------------# 27 | map_mode = 0 28 | #--------------------------------------------------------------------------------------# 29 | # 此处的classes_path用于指定需要测量VOC_map的类别 30 | # 一般情况下与训练和预测所用的classes_path一致即可 31 | #--------------------------------------------------------------------------------------# 32 | classes_path = 'model_data/voc_classes.txt' 33 | #--------------------------------------------------------------------------------------# 34 | # MINOVERLAP用于指定想要获得的mAP0.x,mAP0.x的意义是什么请同学们百度一下。 35 | # 比如计算mAP0.75,可以设定MINOVERLAP = 0.75。 36 | # 37 | # 当某一预测框与真实框重合度大于MINOVERLAP时,该预测框被认为是正样本,否则为负样本。 38 | # 因此MINOVERLAP的值越大,预测框要预测的越准确才能被认为是正样本,此时算出来的mAP值越低, 39 | #--------------------------------------------------------------------------------------# 40 | MINOVERLAP = 0.5 41 | #--------------------------------------------------------------------------------------# 42 | # 受到mAP计算原理的限制,网络在计算mAP时需要获得近乎所有的预测框,这样才可以计算mAP 43 | # 因此,confidence的值应当设置的尽量小进而获得全部可能的预测框。 44 | # 45 | # 该值一般不调整。因为计算mAP需要获得近乎所有的预测框,此处的confidence不能随便更改。 46 | # 想要获得不同门限值下的Recall和Precision值,请修改下方的score_threhold。 47 | #--------------------------------------------------------------------------------------# 48 | confidence = 0.001 49 | #--------------------------------------------------------------------------------------# 50 | # 预测时使用到的非极大抑制值的大小,越大表示非极大抑制越不严格。 51 | # 52 | # 该值一般不调整。 53 | #--------------------------------------------------------------------------------------# 54 | nms_iou = 0.5 55 | #---------------------------------------------------------------------------------------------------------------# 56 | # Recall和Precision不像AP是一个面积的概念,因此在门限值不同时,网络的Recall和Precision值是不同的。 57 | # 58 | # 默认情况下,本代码计算的Recall和Precision代表的是当门限值为0.5(此处定义为score_threhold)时所对应的Recall和Precision值。 59 | # 因为计算mAP需要获得近乎所有的预测框,上面定义的confidence不能随便更改。 60 | # 这里专门定义一个score_threhold用于代表门限值,进而在计算mAP时找到门限值对应的Recall和Precision值。 61 | #---------------------------------------------------------------------------------------------------------------# 62 | score_threhold = 0.5 63 | #-------------------------------------------------------# 64 | # map_vis用于指定是否开启VOC_map计算的可视化 65 | #-------------------------------------------------------# 66 | map_vis = False 67 | #-------------------------------------------------------# 68 | # 指向VOC数据集所在的文件夹 69 | # 默认指向根目录下的VOC数据集 70 | #-------------------------------------------------------# 71 | VOCdevkit_path = 'VOCdevkit' 72 | #-------------------------------------------------------# 73 | # 结果输出的文件夹,默认为map_out 74 | #-------------------------------------------------------# 75 | map_out_path = 'map_out' 76 | 77 | image_ids = open(os.path.join(VOCdevkit_path, "VOC2007/ImageSets/Main/test.txt")).read().strip().split() 78 | 79 | if not os.path.exists(map_out_path): 80 | os.makedirs(map_out_path) 81 | if not os.path.exists(os.path.join(map_out_path, 'ground-truth')): 82 | os.makedirs(os.path.join(map_out_path, 'ground-truth')) 83 | if not os.path.exists(os.path.join(map_out_path, 'detection-results')): 84 | os.makedirs(os.path.join(map_out_path, 'detection-results')) 85 | if not os.path.exists(os.path.join(map_out_path, 'images-optional')): 86 | os.makedirs(os.path.join(map_out_path, 'images-optional')) 87 | 88 | class_names, _ = get_classes(classes_path) 89 | 90 | if map_mode == 0 or map_mode == 1: 91 | print("Load model.") 92 | yolo = YOLO(confidence = confidence, nms_iou = nms_iou) 93 | print("Load model done.") 94 | 95 | print("Get predict result.") 96 | for image_id in tqdm(image_ids): 97 | image_path = os.path.join(VOCdevkit_path, "VOC2007/JPEGImages/"+image_id+".jpg") 98 | image = Image.open(image_path) 99 | if map_vis: 100 | image.save(os.path.join(map_out_path, "images-optional/" + image_id + ".jpg")) 101 | yolo.get_map_txt(image_id, image, class_names, map_out_path) 102 | print("Get predict result done.") 103 | 104 | if map_mode == 0 or map_mode == 2: 105 | print("Get ground truth result.") 106 | for image_id in tqdm(image_ids): 107 | with open(os.path.join(map_out_path, "ground-truth/"+image_id+".txt"), "w") as new_f: 108 | root = ET.parse(os.path.join(VOCdevkit_path, "VOC2007/Annotations/"+image_id+".xml")).getroot() 109 | for obj in root.findall('object'): 110 | difficult_flag = False 111 | if obj.find('difficult')!=None: 112 | difficult = obj.find('difficult').text 113 | if int(difficult)==1: 114 | difficult_flag = True 115 | obj_name = obj.find('name').text 116 | if obj_name not in class_names: 117 | continue 118 | bndbox = obj.find('bndbox') 119 | left = bndbox.find('xmin').text 120 | top = bndbox.find('ymin').text 121 | right = bndbox.find('xmax').text 122 | bottom = bndbox.find('ymax').text 123 | 124 | if difficult_flag: 125 | new_f.write("%s %s %s %s %s difficult\n" % (obj_name, left, top, right, bottom)) 126 | else: 127 | new_f.write("%s %s %s %s %s\n" % (obj_name, left, top, right, bottom)) 128 | print("Get ground truth result done.") 129 | 130 | if map_mode == 0 or map_mode == 3: 131 | print("Get map.") 132 | get_map(MINOVERLAP, True, score_threhold = score_threhold, path = map_out_path) 133 | print("Get map done.") 134 | 135 | if map_mode == 4: 136 | print("Get map.") 137 | get_coco_map(class_names = class_names, path = map_out_path) 138 | print("Get map done.") 139 | -------------------------------------------------------------------------------- /img/street.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bubbliiiing/yolov4-tiny-pytorch/1bbb2f281b09aaf7522ab871a9abf417e8e19587/img/street.jpg -------------------------------------------------------------------------------- /kmeans_for_anchors.py: -------------------------------------------------------------------------------- 1 | #-------------------------------------------------------------------------------------------------------# 2 | # kmeans虽然会对数据集中的框进行聚类,但是很多数据集由于框的大小相近,聚类出来的9个框相差不大, 3 | # 这样的框反而不利于模型的训练。因为不同的特征层适合不同大小的先验框,shape越小的特征层适合越大的先验框 4 | # 原始网络的先验框已经按大中小比例分配好了,不进行聚类也会有非常好的效果。 5 | #-------------------------------------------------------------------------------------------------------# 6 | import glob 7 | import xml.etree.ElementTree as ET 8 | 9 | import matplotlib.pyplot as plt 10 | import numpy as np 11 | from tqdm import tqdm 12 | 13 | 14 | def cas_iou(box, cluster): 15 | x = np.minimum(cluster[:, 0], box[0]) 16 | y = np.minimum(cluster[:, 1], box[1]) 17 | 18 | intersection = x * y 19 | area1 = box[0] * box[1] 20 | 21 | area2 = cluster[:,0] * cluster[:,1] 22 | iou = intersection / (area1 + area2 - intersection) 23 | 24 | return iou 25 | 26 | def avg_iou(box, cluster): 27 | return np.mean([np.max(cas_iou(box[i], cluster)) for i in range(box.shape[0])]) 28 | 29 | def kmeans(box, k): 30 | #-------------------------------------------------------------# 31 | # 取出一共有多少框 32 | #-------------------------------------------------------------# 33 | row = box.shape[0] 34 | 35 | #-------------------------------------------------------------# 36 | # 每个框各个点的位置 37 | #-------------------------------------------------------------# 38 | distance = np.empty((row, k)) 39 | 40 | #-------------------------------------------------------------# 41 | # 最后的聚类位置 42 | #-------------------------------------------------------------# 43 | last_clu = np.zeros((row, )) 44 | 45 | np.random.seed() 46 | 47 | #-------------------------------------------------------------# 48 | # 随机选5个当聚类中心 49 | #-------------------------------------------------------------# 50 | cluster = box[np.random.choice(row, k, replace = False)] 51 | 52 | iter = 0 53 | while True: 54 | #-------------------------------------------------------------# 55 | # 计算当前框和先验框的宽高比例 56 | #-------------------------------------------------------------# 57 | for i in range(row): 58 | distance[i] = 1 - cas_iou(box[i], cluster) 59 | 60 | #-------------------------------------------------------------# 61 | # 取出最小点 62 | #-------------------------------------------------------------# 63 | near = np.argmin(distance, axis=1) 64 | 65 | if (last_clu == near).all(): 66 | break 67 | 68 | #-------------------------------------------------------------# 69 | # 求每一个类的中位点 70 | #-------------------------------------------------------------# 71 | for j in range(k): 72 | cluster[j] = np.median( 73 | box[near == j],axis=0) 74 | 75 | last_clu = near 76 | if iter % 5 == 0: 77 | print('iter: {:d}. avg_iou:{:.2f}'.format(iter, avg_iou(box, cluster))) 78 | iter += 1 79 | 80 | return cluster, near 81 | 82 | def load_data(path): 83 | data = [] 84 | #-------------------------------------------------------------# 85 | # 对于每一个xml都寻找box 86 | #-------------------------------------------------------------# 87 | for xml_file in tqdm(glob.glob('{}/*xml'.format(path))): 88 | tree = ET.parse(xml_file) 89 | height = int(tree.findtext('./size/height')) 90 | width = int(tree.findtext('./size/width')) 91 | if height<=0 or width<=0: 92 | continue 93 | 94 | #-------------------------------------------------------------# 95 | # 对于每一个目标都获得它的宽高 96 | #-------------------------------------------------------------# 97 | for obj in tree.iter('object'): 98 | xmin = int(float(obj.findtext('bndbox/xmin'))) / width 99 | ymin = int(float(obj.findtext('bndbox/ymin'))) / height 100 | xmax = int(float(obj.findtext('bndbox/xmax'))) / width 101 | ymax = int(float(obj.findtext('bndbox/ymax'))) / height 102 | 103 | xmin = np.float64(xmin) 104 | ymin = np.float64(ymin) 105 | xmax = np.float64(xmax) 106 | ymax = np.float64(ymax) 107 | # 得到宽高 108 | data.append([xmax - xmin, ymax - ymin]) 109 | return np.array(data) 110 | 111 | if __name__ == '__main__': 112 | np.random.seed(0) 113 | #-------------------------------------------------------------# 114 | # 运行该程序会计算'./VOCdevkit/VOC2007/Annotations'的xml 115 | # 会生成yolo_anchors.txt 116 | #-------------------------------------------------------------# 117 | input_shape = [416, 416] 118 | anchors_num = 6 119 | #-------------------------------------------------------------# 120 | # 载入数据集,可以使用VOC的xml 121 | #-------------------------------------------------------------# 122 | path = 'VOCdevkit/VOC2007/Annotations' 123 | 124 | #-------------------------------------------------------------# 125 | # 载入所有的xml 126 | # 存储格式为转化为比例后的width,height 127 | #-------------------------------------------------------------# 128 | print('Load xmls.') 129 | data = load_data(path) 130 | print('Load xmls done.') 131 | 132 | #-------------------------------------------------------------# 133 | # 使用k聚类算法 134 | #-------------------------------------------------------------# 135 | print('K-means boxes.') 136 | cluster, near = kmeans(data, anchors_num) 137 | print('K-means boxes done.') 138 | data = data * np.array([input_shape[1], input_shape[0]]) 139 | cluster = cluster * np.array([input_shape[1], input_shape[0]]) 140 | 141 | #-------------------------------------------------------------# 142 | # 绘图 143 | #-------------------------------------------------------------# 144 | for j in range(anchors_num): 145 | plt.scatter(data[near == j][:,0], data[near == j][:,1]) 146 | plt.scatter(cluster[j][0], cluster[j][1], marker='x', c='black') 147 | plt.savefig("kmeans_for_anchors.jpg") 148 | plt.show() 149 | print('Save kmeans_for_anchors.jpg in root dir.') 150 | 151 | cluster = cluster[np.argsort(cluster[:, 0] * cluster[:, 1])] 152 | print('avg_ratio:{:.2f}'.format(avg_iou(data, cluster))) 153 | print(cluster) 154 | 155 | f = open("yolo_anchors.txt", 'w') 156 | row = np.shape(cluster)[0] 157 | for i in range(row): 158 | if i == 0: 159 | x_y = "%d,%d" % (cluster[i][0], cluster[i][1]) 160 | else: 161 | x_y = ", %d,%d" % (cluster[i][0], cluster[i][1]) 162 | f.write(x_y) 163 | f.close() 164 | -------------------------------------------------------------------------------- /logs/README.md: -------------------------------------------------------------------------------- 1 | 用于存放训练好的文件 -------------------------------------------------------------------------------- /model_data/coco_classes.txt: -------------------------------------------------------------------------------- 1 | person 2 | bicycle 3 | car 4 | motorbike 5 | aeroplane 6 | bus 7 | train 8 | truck 9 | boat 10 | traffic light 11 | fire hydrant 12 | stop sign 13 | parking meter 14 | bench 15 | bird 16 | cat 17 | dog 18 | horse 19 | sheep 20 | cow 21 | elephant 22 | bear 23 | zebra 24 | giraffe 25 | backpack 26 | umbrella 27 | handbag 28 | tie 29 | suitcase 30 | frisbee 31 | skis 32 | snowboard 33 | sports ball 34 | kite 35 | baseball bat 36 | baseball glove 37 | skateboard 38 | surfboard 39 | tennis racket 40 | bottle 41 | wine glass 42 | cup 43 | fork 44 | knife 45 | spoon 46 | bowl 47 | banana 48 | apple 49 | sandwich 50 | orange 51 | broccoli 52 | carrot 53 | hot dog 54 | pizza 55 | donut 56 | cake 57 | chair 58 | sofa 59 | pottedplant 60 | bed 61 | diningtable 62 | toilet 63 | tvmonitor 64 | laptop 65 | mouse 66 | remote 67 | keyboard 68 | cell phone 69 | microwave 70 | oven 71 | toaster 72 | sink 73 | refrigerator 74 | book 75 | clock 76 | vase 77 | scissors 78 | teddy bear 79 | hair drier 80 | toothbrush 81 | -------------------------------------------------------------------------------- /model_data/simhei.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bubbliiiing/yolov4-tiny-pytorch/1bbb2f281b09aaf7522ab871a9abf417e8e19587/model_data/simhei.ttf -------------------------------------------------------------------------------- /model_data/voc_classes.txt: -------------------------------------------------------------------------------- 1 | aeroplane 2 | bicycle 3 | bird 4 | boat 5 | bottle 6 | bus 7 | car 8 | cat 9 | chair 10 | cow 11 | diningtable 12 | dog 13 | horse 14 | motorbike 15 | person 16 | pottedplant 17 | sheep 18 | sofa 19 | train 20 | tvmonitor -------------------------------------------------------------------------------- /model_data/yolo_anchors.txt: -------------------------------------------------------------------------------- 1 | 10,14, 23,27, 37,58, 81,82, 135,169, 344,319 -------------------------------------------------------------------------------- /nets/CSPdarknet53_tiny.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import torch 4 | import torch.nn as nn 5 | 6 | 7 | #-------------------------------------------------# 8 | # 卷积块 9 | # Conv2d + BatchNorm2d + LeakyReLU 10 | #-------------------------------------------------# 11 | class BasicConv(nn.Module): 12 | def __init__(self, in_channels, out_channels, kernel_size, stride=1): 13 | super(BasicConv, self).__init__() 14 | 15 | self.conv = nn.Conv2d(in_channels, out_channels, kernel_size, stride, kernel_size//2, bias=False) 16 | self.bn = nn.BatchNorm2d(out_channels) 17 | self.activation = nn.LeakyReLU(0.1) 18 | 19 | def forward(self, x): 20 | x = self.conv(x) 21 | x = self.bn(x) 22 | x = self.activation(x) 23 | return x 24 | 25 | 26 | ''' 27 | input 28 | | 29 | BasicConv 30 | ----------------------- 31 | | | 32 | route_group route 33 | | | 34 | BasicConv | 35 | | | 36 | ------------------- | 37 | | | | 38 | route_1 BasicConv | 39 | | | | 40 | -----------------cat | 41 | | | 42 | ---- BasicConv | 43 | | | | 44 | feat cat--------------------- 45 | | 46 | MaxPooling2D 47 | ''' 48 | #---------------------------------------------------# 49 | # CSPdarknet53-tiny的结构块 50 | # 存在一个大残差边 51 | # 这个大残差边绕过了很多的残差结构 52 | #---------------------------------------------------# 53 | class Resblock_body(nn.Module): 54 | def __init__(self, in_channels, out_channels): 55 | super(Resblock_body, self).__init__() 56 | self.out_channels = out_channels 57 | 58 | self.conv1 = BasicConv(in_channels, out_channels, 3) 59 | 60 | self.conv2 = BasicConv(out_channels//2, out_channels//2, 3) 61 | self.conv3 = BasicConv(out_channels//2, out_channels//2, 3) 62 | 63 | self.conv4 = BasicConv(out_channels, out_channels, 1) 64 | self.maxpool = nn.MaxPool2d([2,2],[2,2]) 65 | 66 | def forward(self, x): 67 | # 利用一个3x3卷积进行特征整合 68 | x = self.conv1(x) 69 | # 引出一个大的残差边route 70 | route = x 71 | 72 | c = self.out_channels 73 | # 对特征层的通道进行分割,取第二部分作为主干部分。 74 | x = torch.split(x, c//2, dim = 1)[1] 75 | # 对主干部分进行3x3卷积 76 | x = self.conv2(x) 77 | # 引出一个小的残差边route_1 78 | route1 = x 79 | # 对第主干部分进行3x3卷积 80 | x = self.conv3(x) 81 | # 主干部分与残差部分进行相接 82 | x = torch.cat([x,route1], dim = 1) 83 | 84 | # 对相接后的结果进行1x1卷积 85 | x = self.conv4(x) 86 | feat = x 87 | x = torch.cat([route, x], dim = 1) 88 | 89 | # 利用最大池化进行高和宽的压缩 90 | x = self.maxpool(x) 91 | return x,feat 92 | 93 | class CSPDarkNet(nn.Module): 94 | def __init__(self): 95 | super(CSPDarkNet, self).__init__() 96 | # 首先利用两次步长为2x2的3x3卷积进行高和宽的压缩 97 | # 416,416,3 -> 208,208,32 -> 104,104,64 98 | self.conv1 = BasicConv(3, 32, kernel_size=3, stride=2) 99 | self.conv2 = BasicConv(32, 64, kernel_size=3, stride=2) 100 | 101 | # 104,104,64 -> 52,52,128 102 | self.resblock_body1 = Resblock_body(64, 64) 103 | # 52,52,128 -> 26,26,256 104 | self.resblock_body2 = Resblock_body(128, 128) 105 | # 26,26,256 -> 13,13,512 106 | self.resblock_body3 = Resblock_body(256, 256) 107 | # 13,13,512 -> 13,13,512 108 | self.conv3 = BasicConv(512, 512, kernel_size=3) 109 | 110 | self.num_features = 1 111 | # 进行权值初始化 112 | for m in self.modules(): 113 | if isinstance(m, nn.Conv2d): 114 | n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels 115 | m.weight.data.normal_(0, math.sqrt(2. / n)) 116 | elif isinstance(m, nn.BatchNorm2d): 117 | m.weight.data.fill_(1) 118 | m.bias.data.zero_() 119 | 120 | 121 | def forward(self, x): 122 | # 416,416,3 -> 208,208,32 -> 104,104,64 123 | x = self.conv1(x) 124 | x = self.conv2(x) 125 | 126 | # 104,104,64 -> 52,52,128 127 | x, _ = self.resblock_body1(x) 128 | # 52,52,128 -> 26,26,256 129 | x, _ = self.resblock_body2(x) 130 | # 26,26,256 -> x为13,13,512 131 | # -> feat1为26,26,256 132 | x, feat1 = self.resblock_body3(x) 133 | 134 | # 13,13,512 -> 13,13,512 135 | x = self.conv3(x) 136 | feat2 = x 137 | return feat1,feat2 138 | 139 | def darknet53_tiny(pretrained, **kwargs): 140 | model = CSPDarkNet() 141 | if pretrained: 142 | model.load_state_dict(torch.load("model_data/CSPdarknet53_tiny_backbone_weights.pth")) 143 | return model 144 | -------------------------------------------------------------------------------- /nets/__init__.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------- /nets/attention.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | import math 4 | 5 | class se_block(nn.Module): 6 | def __init__(self, channel, ratio=16): 7 | super(se_block, self).__init__() 8 | self.avg_pool = nn.AdaptiveAvgPool2d(1) 9 | self.fc = nn.Sequential( 10 | nn.Linear(channel, channel // ratio, bias=False), 11 | nn.ReLU(inplace=True), 12 | nn.Linear(channel // ratio, channel, bias=False), 13 | nn.Sigmoid() 14 | ) 15 | 16 | def forward(self, x): 17 | b, c, _, _ = x.size() 18 | y = self.avg_pool(x).view(b, c) 19 | y = self.fc(y).view(b, c, 1, 1) 20 | return x * y 21 | 22 | class ChannelAttention(nn.Module): 23 | def __init__(self, in_planes, ratio=8): 24 | super(ChannelAttention, self).__init__() 25 | self.avg_pool = nn.AdaptiveAvgPool2d(1) 26 | self.max_pool = nn.AdaptiveMaxPool2d(1) 27 | 28 | # 利用1x1卷积代替全连接 29 | self.fc1 = nn.Conv2d(in_planes, in_planes // ratio, 1, bias=False) 30 | self.relu1 = nn.ReLU() 31 | self.fc2 = nn.Conv2d(in_planes // ratio, in_planes, 1, bias=False) 32 | 33 | self.sigmoid = nn.Sigmoid() 34 | 35 | def forward(self, x): 36 | avg_out = self.fc2(self.relu1(self.fc1(self.avg_pool(x)))) 37 | max_out = self.fc2(self.relu1(self.fc1(self.max_pool(x)))) 38 | out = avg_out + max_out 39 | return self.sigmoid(out) 40 | 41 | class SpatialAttention(nn.Module): 42 | def __init__(self, kernel_size=7): 43 | super(SpatialAttention, self).__init__() 44 | 45 | assert kernel_size in (3, 7), 'kernel size must be 3 or 7' 46 | padding = 3 if kernel_size == 7 else 1 47 | self.conv1 = nn.Conv2d(2, 1, kernel_size, padding=padding, bias=False) 48 | self.sigmoid = nn.Sigmoid() 49 | 50 | def forward(self, x): 51 | avg_out = torch.mean(x, dim=1, keepdim=True) 52 | max_out, _ = torch.max(x, dim=1, keepdim=True) 53 | x = torch.cat([avg_out, max_out], dim=1) 54 | x = self.conv1(x) 55 | return self.sigmoid(x) 56 | 57 | class cbam_block(nn.Module): 58 | def __init__(self, channel, ratio=8, kernel_size=7): 59 | super(cbam_block, self).__init__() 60 | self.channelattention = ChannelAttention(channel, ratio=ratio) 61 | self.spatialattention = SpatialAttention(kernel_size=kernel_size) 62 | 63 | def forward(self, x): 64 | x = x*self.channelattention(x) 65 | x = x*self.spatialattention(x) 66 | return x 67 | 68 | class eca_block(nn.Module): 69 | def __init__(self, channel, b=1, gamma=2): 70 | super(eca_block, self).__init__() 71 | kernel_size = int(abs((math.log(channel, 2) + b) / gamma)) 72 | kernel_size = kernel_size if kernel_size % 2 else kernel_size + 1 73 | 74 | self.avg_pool = nn.AdaptiveAvgPool2d(1) 75 | self.conv = nn.Conv1d(1, 1, kernel_size=kernel_size, padding=(kernel_size - 1) // 2, bias=False) 76 | self.sigmoid = nn.Sigmoid() 77 | 78 | def forward(self, x): 79 | y = self.avg_pool(x) 80 | y = self.conv(y.squeeze(-1).transpose(-1, -2)).transpose(-1, -2).unsqueeze(-1) 81 | y = self.sigmoid(y) 82 | return x * y.expand_as(x) 83 | 84 | class CA_Block(nn.Module): 85 | def __init__(self, channel, reduction=16): 86 | super(CA_Block, self).__init__() 87 | 88 | self.conv_1x1 = nn.Conv2d(in_channels=channel, out_channels=channel//reduction, kernel_size=1, stride=1, bias=False) 89 | 90 | self.relu = nn.ReLU() 91 | self.bn = nn.BatchNorm2d(channel//reduction) 92 | 93 | self.F_h = nn.Conv2d(in_channels=channel//reduction, out_channels=channel, kernel_size=1, stride=1, bias=False) 94 | self.F_w = nn.Conv2d(in_channels=channel//reduction, out_channels=channel, kernel_size=1, stride=1, bias=False) 95 | 96 | self.sigmoid_h = nn.Sigmoid() 97 | self.sigmoid_w = nn.Sigmoid() 98 | 99 | def forward(self, x): 100 | # batch_size, c, h, w 101 | _, _, h, w = x.size() 102 | 103 | # batch_size, c, h, w => batch_size, c, h, 1 => batch_size, c, 1, h 104 | x_h = torch.mean(x, dim = 3, keepdim = True).permute(0, 1, 3, 2) 105 | # batch_size, c, h, w => batch_size, c, 1, w 106 | x_w = torch.mean(x, dim = 2, keepdim = True) 107 | 108 | # batch_size, c, 1, w cat batch_size, c, 1, h => batch_size, c, 1, w + h 109 | # batch_size, c, 1, w + h => batch_size, c / r, 1, w + h 110 | x_cat_conv_relu = self.relu(self.bn(self.conv_1x1(torch.cat((x_h, x_w), 3)))) 111 | 112 | # batch_size, c / r, 1, w + h => batch_size, c / r, 1, h and batch_size, c / r, 1, w 113 | x_cat_conv_split_h, x_cat_conv_split_w = x_cat_conv_relu.split([h, w], 3) 114 | 115 | # batch_size, c / r, 1, h => batch_size, c / r, h, 1 => batch_size, c, h, 1 116 | s_h = self.sigmoid_h(self.F_h(x_cat_conv_split_h.permute(0, 1, 3, 2))) 117 | # batch_size, c / r, 1, w => batch_size, c, 1, w 118 | s_w = self.sigmoid_w(self.F_w(x_cat_conv_split_w)) 119 | 120 | out = x * s_h.expand_as(x) * s_w.expand_as(x) 121 | return out 122 | -------------------------------------------------------------------------------- /nets/yolo.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | 4 | from nets.CSPdarknet53_tiny import darknet53_tiny 5 | from nets.attention import cbam_block, eca_block, se_block, CA_Block 6 | 7 | attention_block = [se_block, cbam_block, eca_block, CA_Block] 8 | 9 | #-------------------------------------------------# 10 | # 卷积块 -> 卷积 + 标准化 + 激活函数 11 | # Conv2d + BatchNormalization + LeakyReLU 12 | #-------------------------------------------------# 13 | class BasicConv(nn.Module): 14 | def __init__(self, in_channels, out_channels, kernel_size, stride=1): 15 | super(BasicConv, self).__init__() 16 | 17 | self.conv = nn.Conv2d(in_channels, out_channels, kernel_size, stride, kernel_size//2, bias=False) 18 | self.bn = nn.BatchNorm2d(out_channels) 19 | self.activation = nn.LeakyReLU(0.1) 20 | 21 | def forward(self, x): 22 | x = self.conv(x) 23 | x = self.bn(x) 24 | x = self.activation(x) 25 | return x 26 | 27 | #---------------------------------------------------# 28 | # 卷积 + 上采样 29 | #---------------------------------------------------# 30 | class Upsample(nn.Module): 31 | def __init__(self, in_channels, out_channels): 32 | super(Upsample, self).__init__() 33 | 34 | self.upsample = nn.Sequential( 35 | BasicConv(in_channels, out_channels, 1), 36 | nn.Upsample(scale_factor=2, mode='nearest') 37 | ) 38 | 39 | def forward(self, x,): 40 | x = self.upsample(x) 41 | return x 42 | 43 | #---------------------------------------------------# 44 | # 最后获得yolov4的输出 45 | #---------------------------------------------------# 46 | def yolo_head(filters_list, in_filters): 47 | m = nn.Sequential( 48 | BasicConv(in_filters, filters_list[0], 3), 49 | nn.Conv2d(filters_list[0], filters_list[1], 1), 50 | ) 51 | return m 52 | #---------------------------------------------------# 53 | # yolo_body 54 | #---------------------------------------------------# 55 | class YoloBody(nn.Module): 56 | def __init__(self, anchors_mask, num_classes, phi=0, pretrained=False): 57 | super(YoloBody, self).__init__() 58 | self.phi = phi 59 | self.backbone = darknet53_tiny(pretrained) 60 | 61 | self.conv_for_P5 = BasicConv(512,256,1) 62 | self.yolo_headP5 = yolo_head([512, len(anchors_mask[0]) * (5 + num_classes)],256) 63 | 64 | self.upsample = Upsample(256,128) 65 | self.yolo_headP4 = yolo_head([256, len(anchors_mask[1]) * (5 + num_classes)],384) 66 | 67 | if 1 <= self.phi and self.phi <= 4: 68 | self.feat1_att = attention_block[self.phi - 1](256) 69 | self.feat2_att = attention_block[self.phi - 1](512) 70 | self.upsample_att = attention_block[self.phi - 1](128) 71 | 72 | def forward(self, x): 73 | #---------------------------------------------------# 74 | # 生成CSPdarknet53_tiny的主干模型 75 | # feat1的shape为26,26,256 76 | # feat2的shape为13,13,512 77 | #---------------------------------------------------# 78 | feat1, feat2 = self.backbone(x) 79 | if 1 <= self.phi and self.phi <= 4: 80 | feat1 = self.feat1_att(feat1) 81 | feat2 = self.feat2_att(feat2) 82 | 83 | # 13,13,512 -> 13,13,256 84 | P5 = self.conv_for_P5(feat2) 85 | # 13,13,256 -> 13,13,512 -> 13,13,255 86 | out0 = self.yolo_headP5(P5) 87 | 88 | # 13,13,256 -> 13,13,128 -> 26,26,128 89 | P5_Upsample = self.upsample(P5) 90 | # 26,26,256 + 26,26,128 -> 26,26,384 91 | if 1 <= self.phi and self.phi <= 4: 92 | P5_Upsample = self.upsample_att(P5_Upsample) 93 | P4 = torch.cat([P5_Upsample,feat1],axis=1) 94 | 95 | # 26,26,384 -> 26,26,256 -> 26,26,255 96 | out1 = self.yolo_headP4(P4) 97 | 98 | return out0, out1 99 | 100 | -------------------------------------------------------------------------------- /nets/yolo_training.py: -------------------------------------------------------------------------------- 1 | import math 2 | from functools import partial 3 | 4 | import numpy as np 5 | import torch 6 | import torch.nn as nn 7 | 8 | class YOLOLoss(nn.Module): 9 | def __init__(self, anchors, num_classes, input_shape, cuda, anchors_mask = [[6,7,8], [3,4,5], [0,1,2]], label_smoothing = 0): 10 | super(YOLOLoss, self).__init__() 11 | #-----------------------------------------------------------# 12 | # 13x13的特征层对应的anchor是[81,82],[135,169],[344,319] 13 | # 26x26的特征层对应的anchor是[10,14],[23,27],[37,58] 14 | #-----------------------------------------------------------# 15 | self.anchors = anchors 16 | self.num_classes = num_classes 17 | self.bbox_attrs = 5 + num_classes 18 | self.input_shape = input_shape 19 | self.anchors_mask = anchors_mask 20 | self.label_smoothing = label_smoothing 21 | 22 | self.balance = [0.4, 1.0, 4] 23 | self.box_ratio = 0.05 24 | self.obj_ratio = 5 * (input_shape[0] * input_shape[1]) / (416 ** 2) 25 | self.cls_ratio = 1 * (num_classes / 80) 26 | 27 | self.ignore_threshold = 0.5 28 | self.cuda = cuda 29 | 30 | def clip_by_tensor(self, t, t_min, t_max): 31 | t = t.float() 32 | result = (t >= t_min).float() * t + (t < t_min).float() * t_min 33 | result = (result <= t_max).float() * result + (result > t_max).float() * t_max 34 | return result 35 | 36 | def MSELoss(self, pred, target): 37 | return torch.pow(pred - target, 2) 38 | 39 | def BCELoss(self, pred, target): 40 | epsilon = 1e-7 41 | pred = self.clip_by_tensor(pred, epsilon, 1.0 - epsilon) 42 | output = - target * torch.log(pred) - (1.0 - target) * torch.log(1.0 - pred) 43 | return output 44 | 45 | def box_ciou(self, b1, b2): 46 | """ 47 | 输入为: 48 | ---------- 49 | b1: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh 50 | b2: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh 51 | 52 | 返回为: 53 | ------- 54 | ciou: tensor, shape=(batch, feat_w, feat_h, anchor_num, 1) 55 | """ 56 | #----------------------------------------------------# 57 | # 求出预测框左上角右下角 58 | #----------------------------------------------------# 59 | b1_xy = b1[..., :2] 60 | b1_wh = b1[..., 2:4] 61 | b1_wh_half = b1_wh/2. 62 | b1_mins = b1_xy - b1_wh_half 63 | b1_maxes = b1_xy + b1_wh_half 64 | #----------------------------------------------------# 65 | # 求出真实框左上角右下角 66 | #----------------------------------------------------# 67 | b2_xy = b2[..., :2] 68 | b2_wh = b2[..., 2:4] 69 | b2_wh_half = b2_wh/2. 70 | b2_mins = b2_xy - b2_wh_half 71 | b2_maxes = b2_xy + b2_wh_half 72 | 73 | #----------------------------------------------------# 74 | # 求真实框和预测框所有的iou 75 | #----------------------------------------------------# 76 | intersect_mins = torch.max(b1_mins, b2_mins) 77 | intersect_maxes = torch.min(b1_maxes, b2_maxes) 78 | intersect_wh = torch.max(intersect_maxes - intersect_mins, torch.zeros_like(intersect_maxes)) 79 | intersect_area = intersect_wh[..., 0] * intersect_wh[..., 1] 80 | b1_area = b1_wh[..., 0] * b1_wh[..., 1] 81 | b2_area = b2_wh[..., 0] * b2_wh[..., 1] 82 | union_area = b1_area + b2_area - intersect_area 83 | iou = intersect_area / torch.clamp(union_area,min = 1e-6) 84 | 85 | #----------------------------------------------------# 86 | # 计算中心的差距 87 | #----------------------------------------------------# 88 | center_distance = torch.sum(torch.pow((b1_xy - b2_xy), 2), axis=-1) 89 | 90 | #----------------------------------------------------# 91 | # 找到包裹两个框的最小框的左上角和右下角 92 | #----------------------------------------------------# 93 | enclose_mins = torch.min(b1_mins, b2_mins) 94 | enclose_maxes = torch.max(b1_maxes, b2_maxes) 95 | enclose_wh = torch.max(enclose_maxes - enclose_mins, torch.zeros_like(intersect_maxes)) 96 | #----------------------------------------------------# 97 | # 计算对角线距离 98 | #----------------------------------------------------# 99 | enclose_diagonal = torch.sum(torch.pow(enclose_wh,2), axis=-1) 100 | ciou = iou - 1.0 * (center_distance) / torch.clamp(enclose_diagonal,min = 1e-6) 101 | 102 | v = (4 / (math.pi ** 2)) * torch.pow((torch.atan(b1_wh[..., 0] / torch.clamp(b1_wh[..., 1],min = 1e-6)) - torch.atan(b2_wh[..., 0] / torch.clamp(b2_wh[..., 1], min = 1e-6))), 2) 103 | alpha = v / torch.clamp((1.0 - iou + v), min=1e-6) 104 | ciou = ciou - alpha * v 105 | return ciou 106 | 107 | #---------------------------------------------------# 108 | # 平滑标签 109 | #---------------------------------------------------# 110 | def smooth_labels(self, y_true, label_smoothing, num_classes): 111 | return y_true * (1.0 - label_smoothing) + label_smoothing / num_classes 112 | 113 | def forward(self, l, input, targets=None): 114 | #----------------------------------------------------# 115 | # l 代表使用的是第几个有效特征层 116 | # input的shape为 bs, 3*(5+num_classes), 13, 13 117 | # bs, 3*(5+num_classes), 26, 26 118 | # targets 真实框的标签情况 [batch_size, num_gt, 5] 119 | #----------------------------------------------------# 120 | #--------------------------------# 121 | # 获得图片数量,特征层的高和宽 122 | #--------------------------------# 123 | bs = input.size(0) 124 | in_h = input.size(2) 125 | in_w = input.size(3) 126 | #-----------------------------------------------------------------------# 127 | # 计算步长 128 | # 每一个特征点对应原来的图片上多少个像素点 129 | # 130 | # 如果特征层为13x13的话,一个特征点就对应原来的图片上的32个像素点 131 | # 如果特征层为26x26的话,一个特征点就对应原来的图片上的16个像素点 132 | # stride_h = stride_w = 32、16 133 | #-----------------------------------------------------------------------# 134 | stride_h = self.input_shape[0] / in_h 135 | stride_w = self.input_shape[1] / in_w 136 | #-------------------------------------------------# 137 | # 此时获得的scaled_anchors大小是相对于特征层的 138 | #-------------------------------------------------# 139 | scaled_anchors = [(a_w / stride_w, a_h / stride_h) for a_w, a_h in self.anchors] 140 | #-----------------------------------------------# 141 | # 输入的input一共有三个,他们的shape分别是 142 | # bs, 3 * (5+num_classes), 13, 13 => bs, 3, 5 + num_classes, 13, 13 => batch_size, 3, 13, 13, 5 + num_classes 143 | 144 | # batch_size, 3, 13, 13, 5 + num_classes 145 | # batch_size, 3, 26, 26, 5 + num_classes 146 | #-----------------------------------------------# 147 | prediction = input.view(bs, len(self.anchors_mask[l]), self.bbox_attrs, in_h, in_w).permute(0, 1, 3, 4, 2).contiguous() 148 | 149 | #-----------------------------------------------# 150 | # 先验框的中心位置的调整参数 151 | #-----------------------------------------------# 152 | x = torch.sigmoid(prediction[..., 0]) 153 | y = torch.sigmoid(prediction[..., 1]) 154 | #-----------------------------------------------# 155 | # 先验框的宽高调整参数 156 | #-----------------------------------------------# 157 | w = prediction[..., 2] 158 | h = prediction[..., 3] 159 | #-----------------------------------------------# 160 | # 获得置信度,是否有物体 161 | #-----------------------------------------------# 162 | conf = torch.sigmoid(prediction[..., 4]) 163 | #-----------------------------------------------# 164 | # 种类置信度 165 | #-----------------------------------------------# 166 | pred_cls = torch.sigmoid(prediction[..., 5:]) 167 | 168 | #-----------------------------------------------# 169 | # 获得网络应该有的预测结果 170 | #-----------------------------------------------# 171 | y_true, noobj_mask, box_loss_scale = self.get_target(l, targets, scaled_anchors, in_h, in_w) 172 | 173 | #---------------------------------------------------------------# 174 | # 将预测结果进行解码,判断预测结果和真实值的重合程度 175 | # 如果重合程度过大则忽略,因为这些特征点属于预测比较准确的特征点 176 | # 作为负样本不合适 177 | #----------------------------------------------------------------# 178 | noobj_mask, pred_boxes = self.get_ignore(l, x, y, h, w, targets, scaled_anchors, in_h, in_w, noobj_mask) 179 | 180 | if self.cuda: 181 | y_true = y_true.type_as(x) 182 | noobj_mask = noobj_mask.type_as(x) 183 | box_loss_scale = box_loss_scale.type_as(x) 184 | #--------------------------------------------------------------------------# 185 | # box_loss_scale是真实框宽高的乘积,宽高均在0-1之间,因此乘积也在0-1之间。 186 | # 2-宽高的乘积代表真实框越大,比重越小,小框的比重更大。 187 | # 使用iou损失时,大中小目标的回归损失不存在比例失衡问题,故弃用 188 | #--------------------------------------------------------------------------# 189 | box_loss_scale = 2 - box_loss_scale 190 | 191 | loss = 0 192 | obj_mask = y_true[..., 4] == 1 193 | n = torch.sum(obj_mask) 194 | if n != 0: 195 | #---------------------------------------------------------------# 196 | # 计算预测结果和真实结果的差距 197 | # loss_loc ciou回归损失 198 | # loss_cls 分类损失 199 | #---------------------------------------------------------------# 200 | ciou = self.box_ciou(pred_boxes, y_true[..., :4]).type_as(x) 201 | # loss_loc = torch.mean((1 - ciou)[obj_mask] * box_loss_scale[obj_mask]) 202 | loss_loc = torch.mean((1 - ciou)[obj_mask]) 203 | 204 | loss_cls = torch.mean(self.BCELoss(pred_cls[obj_mask], y_true[..., 5:][obj_mask])) 205 | loss += loss_loc * self.box_ratio + loss_cls * self.cls_ratio 206 | 207 | loss_conf = torch.mean(self.BCELoss(conf, obj_mask.type_as(conf))[noobj_mask.bool() | obj_mask]) 208 | loss += loss_conf * self.balance[l] * self.obj_ratio 209 | # if n != 0: 210 | # print(loss_loc * self.box_ratio, loss_cls * self.cls_ratio, loss_conf * self.balance[l] * self.obj_ratio) 211 | return loss 212 | 213 | def calculate_iou(self, _box_a, _box_b): 214 | #-----------------------------------------------------------# 215 | # 计算真实框的左上角和右下角 216 | #-----------------------------------------------------------# 217 | b1_x1, b1_x2 = _box_a[:, 0] - _box_a[:, 2] / 2, _box_a[:, 0] + _box_a[:, 2] / 2 218 | b1_y1, b1_y2 = _box_a[:, 1] - _box_a[:, 3] / 2, _box_a[:, 1] + _box_a[:, 3] / 2 219 | #-----------------------------------------------------------# 220 | # 计算先验框获得的预测框的左上角和右下角 221 | #-----------------------------------------------------------# 222 | b2_x1, b2_x2 = _box_b[:, 0] - _box_b[:, 2] / 2, _box_b[:, 0] + _box_b[:, 2] / 2 223 | b2_y1, b2_y2 = _box_b[:, 1] - _box_b[:, 3] / 2, _box_b[:, 1] + _box_b[:, 3] / 2 224 | 225 | #-----------------------------------------------------------# 226 | # 将真实框和预测框都转化成左上角右下角的形式 227 | #-----------------------------------------------------------# 228 | box_a = torch.zeros_like(_box_a) 229 | box_b = torch.zeros_like(_box_b) 230 | box_a[:, 0], box_a[:, 1], box_a[:, 2], box_a[:, 3] = b1_x1, b1_y1, b1_x2, b1_y2 231 | box_b[:, 0], box_b[:, 1], box_b[:, 2], box_b[:, 3] = b2_x1, b2_y1, b2_x2, b2_y2 232 | 233 | #-----------------------------------------------------------# 234 | # A为真实框的数量,B为先验框的数量 235 | #-----------------------------------------------------------# 236 | A = box_a.size(0) 237 | B = box_b.size(0) 238 | 239 | #-----------------------------------------------------------# 240 | # 计算交的面积 241 | #-----------------------------------------------------------# 242 | max_xy = torch.min(box_a[:, 2:].unsqueeze(1).expand(A, B, 2), box_b[:, 2:].unsqueeze(0).expand(A, B, 2)) 243 | min_xy = torch.max(box_a[:, :2].unsqueeze(1).expand(A, B, 2), box_b[:, :2].unsqueeze(0).expand(A, B, 2)) 244 | inter = torch.clamp((max_xy - min_xy), min=0) 245 | inter = inter[:, :, 0] * inter[:, :, 1] 246 | #-----------------------------------------------------------# 247 | # 计算预测框和真实框各自的面积 248 | #-----------------------------------------------------------# 249 | area_a = ((box_a[:, 2]-box_a[:, 0]) * (box_a[:, 3]-box_a[:, 1])).unsqueeze(1).expand_as(inter) # [A,B] 250 | area_b = ((box_b[:, 2]-box_b[:, 0]) * (box_b[:, 3]-box_b[:, 1])).unsqueeze(0).expand_as(inter) # [A,B] 251 | #-----------------------------------------------------------# 252 | # 求IOU 253 | #-----------------------------------------------------------# 254 | union = area_a + area_b - inter 255 | return inter / union # [A,B] 256 | 257 | def get_target(self, l, targets, anchors, in_h, in_w): 258 | #-----------------------------------------------------# 259 | # 计算一共有多少张图片 260 | #-----------------------------------------------------# 261 | bs = len(targets) 262 | #-----------------------------------------------------# 263 | # 用于选取哪些先验框不包含物体 264 | #-----------------------------------------------------# 265 | noobj_mask = torch.ones(bs, len(self.anchors_mask[l]), in_h, in_w, requires_grad = False) 266 | #-----------------------------------------------------# 267 | # 让网络更加去关注小目标 268 | #-----------------------------------------------------# 269 | box_loss_scale = torch.zeros(bs, len(self.anchors_mask[l]), in_h, in_w, requires_grad = False) 270 | #-----------------------------------------------------# 271 | # batch_size, 3, 13, 13, 5 + num_classes 272 | #-----------------------------------------------------# 273 | y_true = torch.zeros(bs, len(self.anchors_mask[l]), in_h, in_w, self.bbox_attrs, requires_grad = False) 274 | for b in range(bs): 275 | if len(targets[b])==0: 276 | continue 277 | batch_target = torch.zeros_like(targets[b]) 278 | #-------------------------------------------------------# 279 | # 计算出正样本在特征层上的中心点 280 | #-------------------------------------------------------# 281 | batch_target[:, [0,2]] = targets[b][:, [0,2]] * in_w 282 | batch_target[:, [1,3]] = targets[b][:, [1,3]] * in_h 283 | batch_target[:, 4] = targets[b][:, 4] 284 | batch_target = batch_target.cpu() 285 | 286 | #-------------------------------------------------------# 287 | # 将真实框转换一个形式 288 | # num_true_box, 4 289 | #-------------------------------------------------------# 290 | gt_box = torch.FloatTensor(torch.cat((torch.zeros((batch_target.size(0), 2)), batch_target[:, 2:4]), 1)) 291 | #-------------------------------------------------------# 292 | # 将先验框转换一个形式 293 | # 9, 4 294 | #-------------------------------------------------------# 295 | anchor_shapes = torch.FloatTensor(torch.cat((torch.zeros((len(anchors), 2)), torch.FloatTensor(anchors)), 1)) 296 | #-------------------------------------------------------# 297 | # 计算交并比 298 | # self.calculate_iou(gt_box, anchor_shapes) = [num_true_box, 9]每一个真实框和9个先验框的重合情况 299 | # best_ns: 300 | # [每个真实框最大的重合度max_iou, 每一个真实框最重合的先验框的序号] 301 | #-------------------------------------------------------# 302 | iou = self.calculate_iou(gt_box, anchor_shapes) 303 | best_ns = torch.argmax(iou, dim=-1) 304 | sort_ns = torch.argsort(iou, dim=-1, descending=True) 305 | 306 | def check_in_anchors_mask(index, anchors_mask): 307 | for sub_anchors_mask in anchors_mask: 308 | if index in sub_anchors_mask: 309 | return True 310 | return False 311 | 312 | for t, best_n in enumerate(best_ns): 313 | #----------------------------------------# 314 | # 防止匹配到的先验框不在anchors_mask中 315 | #----------------------------------------# 316 | if not check_in_anchors_mask(best_n, self.anchors_mask): 317 | for index in sort_ns[t]: 318 | if check_in_anchors_mask(index, self.anchors_mask): 319 | best_n = index 320 | break 321 | 322 | if best_n not in self.anchors_mask[l]: 323 | continue 324 | #----------------------------------------# 325 | # 判断这个先验框是当前特征点的哪一个先验框 326 | #----------------------------------------# 327 | k = self.anchors_mask[l].index(best_n) 328 | #----------------------------------------# 329 | # 获得真实框属于哪个网格点 330 | #----------------------------------------# 331 | i = torch.floor(batch_target[t, 0]).long() 332 | j = torch.floor(batch_target[t, 1]).long() 333 | #----------------------------------------# 334 | # 取出真实框的种类 335 | #----------------------------------------# 336 | c = batch_target[t, 4].long() 337 | 338 | #----------------------------------------# 339 | # noobj_mask代表无目标的特征点 340 | #----------------------------------------# 341 | noobj_mask[b, k, j, i] = 0 342 | #----------------------------------------# 343 | # tx、ty代表中心调整参数的真实值 344 | #----------------------------------------# 345 | y_true[b, k, j, i, 0] = batch_target[t, 0] 346 | y_true[b, k, j, i, 1] = batch_target[t, 1] 347 | y_true[b, k, j, i, 2] = batch_target[t, 2] 348 | y_true[b, k, j, i, 3] = batch_target[t, 3] 349 | y_true[b, k, j, i, 4] = 1 350 | y_true[b, k, j, i, c + 5] = 1 351 | #----------------------------------------# 352 | # 用于获得xywh的比例 353 | # 大目标loss权重小,小目标loss权重大 354 | #----------------------------------------# 355 | box_loss_scale[b, k, j, i] = batch_target[t, 2] * batch_target[t, 3] / in_w / in_h 356 | return y_true, noobj_mask, box_loss_scale 357 | 358 | def get_ignore(self, l, x, y, h, w, targets, scaled_anchors, in_h, in_w, noobj_mask): 359 | #-----------------------------------------------------# 360 | # 计算一共有多少张图片 361 | #-----------------------------------------------------# 362 | bs = len(targets) 363 | 364 | #-----------------------------------------------------# 365 | # 生成网格,先验框中心,网格左上角 366 | #-----------------------------------------------------# 367 | grid_x = torch.linspace(0, in_w - 1, in_w).repeat(in_h, 1).repeat( 368 | int(bs * len(self.anchors_mask[l])), 1, 1).view(x.shape).type_as(x) 369 | grid_y = torch.linspace(0, in_h - 1, in_h).repeat(in_w, 1).t().repeat( 370 | int(bs * len(self.anchors_mask[l])), 1, 1).view(y.shape).type_as(x) 371 | 372 | # 生成先验框的宽高 373 | scaled_anchors_l = np.array(scaled_anchors)[self.anchors_mask[l]] 374 | anchor_w = torch.Tensor(scaled_anchors_l).index_select(1, torch.LongTensor([0])).type_as(x) 375 | anchor_h = torch.Tensor(scaled_anchors_l).index_select(1, torch.LongTensor([1])).type_as(x) 376 | 377 | anchor_w = anchor_w.repeat(bs, 1).repeat(1, 1, in_h * in_w).view(w.shape) 378 | anchor_h = anchor_h.repeat(bs, 1).repeat(1, 1, in_h * in_w).view(h.shape) 379 | #-------------------------------------------------------# 380 | # 计算调整后的先验框中心与宽高 381 | #-------------------------------------------------------# 382 | pred_boxes_x = torch.unsqueeze(x + grid_x, -1) 383 | pred_boxes_y = torch.unsqueeze(y + grid_y, -1) 384 | pred_boxes_w = torch.unsqueeze(torch.exp(w) * anchor_w, -1) 385 | pred_boxes_h = torch.unsqueeze(torch.exp(h) * anchor_h, -1) 386 | pred_boxes = torch.cat([pred_boxes_x, pred_boxes_y, pred_boxes_w, pred_boxes_h], dim = -1) 387 | for b in range(bs): 388 | #-------------------------------------------------------# 389 | # 将预测结果转换一个形式 390 | # pred_boxes_for_ignore num_anchors, 4 391 | #-------------------------------------------------------# 392 | pred_boxes_for_ignore = pred_boxes[b].view(-1, 4) 393 | #-------------------------------------------------------# 394 | # 计算真实框,并把真实框转换成相对于特征层的大小 395 | # gt_box num_true_box, 4 396 | #-------------------------------------------------------# 397 | if len(targets[b]) > 0: 398 | batch_target = torch.zeros_like(targets[b]) 399 | #-------------------------------------------------------# 400 | # 计算出正样本在特征层上的中心点 401 | #-------------------------------------------------------# 402 | batch_target[:, [0,2]] = targets[b][:, [0,2]] * in_w 403 | batch_target[:, [1,3]] = targets[b][:, [1,3]] * in_h 404 | batch_target = batch_target[:, :4].type_as(x) 405 | #-------------------------------------------------------# 406 | # 计算交并比 407 | # anch_ious num_true_box, num_anchors 408 | #-------------------------------------------------------# 409 | anch_ious = self.calculate_iou(batch_target, pred_boxes_for_ignore) 410 | #-------------------------------------------------------# 411 | # 每个先验框对应真实框的最大重合度 412 | # anch_ious_max num_anchors 413 | #-------------------------------------------------------# 414 | anch_ious_max, _ = torch.max(anch_ious, dim = 0) 415 | anch_ious_max = anch_ious_max.view(pred_boxes[b].size()[:3]) 416 | noobj_mask[b][anch_ious_max > self.ignore_threshold] = 0 417 | return noobj_mask, pred_boxes 418 | 419 | def weights_init(net, init_type='normal', init_gain = 0.02): 420 | def init_func(m): 421 | classname = m.__class__.__name__ 422 | if hasattr(m, 'weight') and classname.find('Conv') != -1: 423 | if init_type == 'normal': 424 | torch.nn.init.normal_(m.weight.data, 0.0, init_gain) 425 | elif init_type == 'xavier': 426 | torch.nn.init.xavier_normal_(m.weight.data, gain=init_gain) 427 | elif init_type == 'kaiming': 428 | torch.nn.init.kaiming_normal_(m.weight.data, a=0, mode='fan_in') 429 | elif init_type == 'orthogonal': 430 | torch.nn.init.orthogonal_(m.weight.data, gain=init_gain) 431 | else: 432 | raise NotImplementedError('initialization method [%s] is not implemented' % init_type) 433 | elif classname.find('BatchNorm2d') != -1: 434 | torch.nn.init.normal_(m.weight.data, 1.0, 0.02) 435 | torch.nn.init.constant_(m.bias.data, 0.0) 436 | print('initialize network with %s type' % init_type) 437 | net.apply(init_func) 438 | 439 | def get_lr_scheduler(lr_decay_type, lr, min_lr, total_iters, warmup_iters_ratio = 0.05, warmup_lr_ratio = 0.1, no_aug_iter_ratio = 0.05, step_num = 10): 440 | def yolox_warm_cos_lr(lr, min_lr, total_iters, warmup_total_iters, warmup_lr_start, no_aug_iter, iters): 441 | if iters <= warmup_total_iters: 442 | # lr = (lr - warmup_lr_start) * iters / float(warmup_total_iters) + warmup_lr_start 443 | lr = (lr - warmup_lr_start) * pow(iters / float(warmup_total_iters), 2) + warmup_lr_start 444 | elif iters >= total_iters - no_aug_iter: 445 | lr = min_lr 446 | else: 447 | lr = min_lr + 0.5 * (lr - min_lr) * ( 448 | 1.0 + math.cos(math.pi* (iters - warmup_total_iters) / (total_iters - warmup_total_iters - no_aug_iter)) 449 | ) 450 | return lr 451 | 452 | def step_lr(lr, decay_rate, step_size, iters): 453 | if step_size < 1: 454 | raise ValueError("step_size must above 1.") 455 | n = iters // step_size 456 | out_lr = lr * decay_rate ** n 457 | return out_lr 458 | 459 | if lr_decay_type == "cos": 460 | warmup_total_iters = min(max(warmup_iters_ratio * total_iters, 1), 3) 461 | warmup_lr_start = max(warmup_lr_ratio * lr, 1e-6) 462 | no_aug_iter = min(max(no_aug_iter_ratio * total_iters, 1), 15) 463 | func = partial(yolox_warm_cos_lr ,lr, min_lr, total_iters, warmup_total_iters, warmup_lr_start, no_aug_iter) 464 | else: 465 | decay_rate = (min_lr / lr) ** (1 / (step_num - 1)) 466 | step_size = total_iters / step_num 467 | func = partial(step_lr, lr, decay_rate, step_size) 468 | 469 | return func 470 | 471 | def set_optimizer_lr(optimizer, lr_scheduler_func, epoch): 472 | lr = lr_scheduler_func(epoch) 473 | for param_group in optimizer.param_groups: 474 | param_group['lr'] = lr 475 | -------------------------------------------------------------------------------- /predict.py: -------------------------------------------------------------------------------- 1 | #-----------------------------------------------------------------------# 2 | # predict.py将单张图片预测、摄像头检测、FPS测试和目录遍历检测等功能 3 | # 整合到了一个py文件中,通过指定mode进行模式的修改。 4 | #-----------------------------------------------------------------------# 5 | import time 6 | 7 | import cv2 8 | import numpy as np 9 | from PIL import Image 10 | 11 | from yolo import YOLO, YOLO_ONNX 12 | 13 | if __name__ == "__main__": 14 | #----------------------------------------------------------------------------------------------------------# 15 | # mode用于指定测试的模式: 16 | # 'predict' 表示单张图片预测,如果想对预测过程进行修改,如保存图片,截取对象等,可以先看下方详细的注释 17 | # 'video' 表示视频检测,可调用摄像头或者视频进行检测,详情查看下方注释。 18 | # 'fps' 表示测试fps,使用的图片是img里面的street.jpg,详情查看下方注释。 19 | # 'dir_predict' 表示遍历文件夹进行检测并保存。默认遍历img文件夹,保存img_out文件夹,详情查看下方注释。 20 | # 'heatmap' 表示进行预测结果的热力图可视化,详情查看下方注释。 21 | # 'export_onnx' 表示将模型导出为onnx,需要pytorch1.7.1以上。 22 | # 'predict_onnx' 表示利用导出的onnx模型进行预测,相关参数的修改在yolo.py_416行左右处的YOLO_ONNX 23 | #----------------------------------------------------------------------------------------------------------# 24 | mode = "predict" 25 | #-------------------------------------------------------------------------# 26 | # crop 指定了是否在单张图片预测后对目标进行截取 27 | # count 指定了是否进行目标的计数 28 | # crop、count仅在mode='predict'时有效 29 | #-------------------------------------------------------------------------# 30 | crop = False 31 | count = False 32 | #----------------------------------------------------------------------------------------------------------# 33 | # video_path 用于指定视频的路径,当video_path=0时表示检测摄像头 34 | # 想要检测视频,则设置如video_path = "xxx.mp4"即可,代表读取出根目录下的xxx.mp4文件。 35 | # video_save_path 表示视频保存的路径,当video_save_path=""时表示不保存 36 | # 想要保存视频,则设置如video_save_path = "yyy.mp4"即可,代表保存为根目录下的yyy.mp4文件。 37 | # video_fps 用于保存的视频的fps 38 | # 39 | # video_path、video_save_path和video_fps仅在mode='video'时有效 40 | # 保存视频时需要ctrl+c退出或者运行到最后一帧才会完成完整的保存步骤。 41 | #----------------------------------------------------------------------------------------------------------# 42 | video_path = 0 43 | video_save_path = "" 44 | video_fps = 25.0 45 | #----------------------------------------------------------------------------------------------------------# 46 | # test_interval 用于指定测量fps的时候,图片检测的次数。理论上test_interval越大,fps越准确。 47 | # fps_image_path 用于指定测试的fps图片 48 | # 49 | # test_interval和fps_image_path仅在mode='fps'有效 50 | #----------------------------------------------------------------------------------------------------------# 51 | test_interval = 100 52 | fps_image_path = "img/street.jpg" 53 | #-------------------------------------------------------------------------# 54 | # dir_origin_path 指定了用于检测的图片的文件夹路径 55 | # dir_save_path 指定了检测完图片的保存路径 56 | # 57 | # dir_origin_path和dir_save_path仅在mode='dir_predict'时有效 58 | #-------------------------------------------------------------------------# 59 | dir_origin_path = "img/" 60 | dir_save_path = "img_out/" 61 | #-------------------------------------------------------------------------# 62 | # heatmap_save_path 热力图的保存路径,默认保存在model_data下 63 | # 64 | # heatmap_save_path仅在mode='heatmap'有效 65 | #-------------------------------------------------------------------------# 66 | heatmap_save_path = "model_data/heatmap_vision.png" 67 | #-------------------------------------------------------------------------# 68 | # simplify 使用Simplify onnx 69 | # onnx_save_path 指定了onnx的保存路径 70 | #-------------------------------------------------------------------------# 71 | simplify = True 72 | onnx_save_path = "model_data/models.onnx" 73 | 74 | if mode != "predict_onnx": 75 | yolo = YOLO() 76 | else: 77 | yolo = YOLO_ONNX() 78 | 79 | if mode == "predict": 80 | ''' 81 | 1、如果想要进行检测完的图片的保存,利用r_image.save("img.jpg")即可保存,直接在predict.py里进行修改即可。 82 | 2、如果想要获得预测框的坐标,可以进入yolo.detect_image函数,在绘图部分读取top,left,bottom,right这四个值。 83 | 3、如果想要利用预测框截取下目标,可以进入yolo.detect_image函数,在绘图部分利用获取到的top,left,bottom,right这四个值 84 | 在原图上利用矩阵的方式进行截取。 85 | 4、如果想要在预测图上写额外的字,比如检测到的特定目标的数量,可以进入yolo.detect_image函数,在绘图部分对predicted_class进行判断, 86 | 比如判断if predicted_class == 'car': 即可判断当前目标是否为车,然后记录数量即可。利用draw.text即可写字。 87 | ''' 88 | while True: 89 | img = input('Input image filename:') 90 | try: 91 | image = Image.open(img) 92 | except: 93 | print('Open Error! Try again!') 94 | continue 95 | else: 96 | r_image = yolo.detect_image(image, crop = crop, count=count) 97 | r_image.show() 98 | 99 | elif mode == "video": 100 | capture = cv2.VideoCapture(video_path) 101 | if video_save_path!="": 102 | fourcc = cv2.VideoWriter_fourcc(*'XVID') 103 | size = (int(capture.get(cv2.CAP_PROP_FRAME_WIDTH)), int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))) 104 | out = cv2.VideoWriter(video_save_path, fourcc, video_fps, size) 105 | 106 | ref, frame = capture.read() 107 | if not ref: 108 | raise ValueError("未能正确读取摄像头(视频),请注意是否正确安装摄像头(是否正确填写视频路径)。") 109 | 110 | fps = 0.0 111 | while(True): 112 | t1 = time.time() 113 | # 读取某一帧 114 | ref, frame = capture.read() 115 | if not ref: 116 | break 117 | # 格式转变,BGRtoRGB 118 | frame = cv2.cvtColor(frame,cv2.COLOR_BGR2RGB) 119 | # 转变成Image 120 | frame = Image.fromarray(np.uint8(frame)) 121 | # 进行检测 122 | frame = np.array(yolo.detect_image(frame)) 123 | # RGBtoBGR满足opencv显示格式 124 | frame = cv2.cvtColor(frame,cv2.COLOR_RGB2BGR) 125 | 126 | fps = ( fps + (1./(time.time()-t1)) ) / 2 127 | print("fps= %.2f"%(fps)) 128 | frame = cv2.putText(frame, "fps= %.2f"%(fps), (0, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) 129 | 130 | cv2.imshow("video",frame) 131 | c= cv2.waitKey(1) & 0xff 132 | if video_save_path!="": 133 | out.write(frame) 134 | 135 | if c==27: 136 | capture.release() 137 | break 138 | 139 | print("Video Detection Done!") 140 | capture.release() 141 | if video_save_path!="": 142 | print("Save processed video to the path :" + video_save_path) 143 | out.release() 144 | cv2.destroyAllWindows() 145 | 146 | elif mode == "fps": 147 | img = Image.open(fps_image_path) 148 | tact_time = yolo.get_FPS(img, test_interval) 149 | print(str(tact_time) + ' seconds, ' + str(1/tact_time) + 'FPS, @batch_size 1') 150 | 151 | elif mode == "dir_predict": 152 | import os 153 | 154 | from tqdm import tqdm 155 | 156 | img_names = os.listdir(dir_origin_path) 157 | for img_name in tqdm(img_names): 158 | if img_name.lower().endswith(('.bmp', '.dib', '.png', '.jpg', '.jpeg', '.pbm', '.pgm', '.ppm', '.tif', '.tiff')): 159 | image_path = os.path.join(dir_origin_path, img_name) 160 | image = Image.open(image_path) 161 | r_image = yolo.detect_image(image) 162 | if not os.path.exists(dir_save_path): 163 | os.makedirs(dir_save_path) 164 | r_image.save(os.path.join(dir_save_path, img_name.replace(".jpg", ".png")), quality=95, subsampling=0) 165 | 166 | elif mode == "heatmap": 167 | while True: 168 | img = input('Input image filename:') 169 | try: 170 | image = Image.open(img) 171 | except: 172 | print('Open Error! Try again!') 173 | continue 174 | else: 175 | yolo.detect_heatmap(image, heatmap_save_path) 176 | 177 | elif mode == "export_onnx": 178 | yolo.convert_to_onnx(simplify, onnx_save_path) 179 | 180 | elif mode == "predict_onnx": 181 | while True: 182 | img = input('Input image filename:') 183 | try: 184 | image = Image.open(img) 185 | except: 186 | print('Open Error! Try again!') 187 | continue 188 | else: 189 | r_image = yolo.detect_image(image) 190 | r_image.show() 191 | else: 192 | raise AssertionError("Please specify the correct mode: 'predict', 'video', 'fps', 'heatmap', 'export_onnx', 'dir_predict'.") 193 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | torch 2 | torchvision 3 | tensorboard 4 | scipy==1.2.1 5 | numpy==1.17.0 6 | matplotlib==3.1.2 7 | opencv_python==4.1.2.30 8 | tqdm==4.60.0 9 | Pillow==8.2.0 10 | h5py==2.10.0 -------------------------------------------------------------------------------- /summary.py: -------------------------------------------------------------------------------- 1 | #--------------------------------------------# 2 | # 该部分代码用于看网络结构 3 | #--------------------------------------------# 4 | import torch 5 | from thop import clever_format, profile 6 | from torchsummary import summary 7 | 8 | from nets.yolo import YoloBody 9 | 10 | if __name__ == "__main__": 11 | input_shape = [416, 416] 12 | anchors_mask = [[3, 4, 5], [1, 2, 3]] 13 | num_classes = 80 14 | phi = 0 15 | 16 | device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 17 | m = YoloBody(anchors_mask, num_classes, phi=phi).to(device) 18 | summary(m, (3, input_shape[0], input_shape[1])) 19 | 20 | dummy_input = torch.randn(1, 3, input_shape[0], input_shape[1]).to(device) 21 | flops, params = profile(m.to(device), (dummy_input, ), verbose=False) 22 | #--------------------------------------------------------# 23 | # flops * 2是因为profile没有将卷积作为两个operations 24 | # 有些论文将卷积算乘法、加法两个operations。此时乘2 25 | # 有些论文只考虑乘法的运算次数,忽略加法。此时不乘2 26 | # 本代码选择乘2,参考YOLOX。 27 | #--------------------------------------------------------# 28 | flops = flops * 2 29 | flops, params = clever_format([flops, params], "%.3f") 30 | print('Total GFLOPS: %s' % (flops)) 31 | print('Total params: %s' % (params)) 32 | 33 | -------------------------------------------------------------------------------- /train.py: -------------------------------------------------------------------------------- 1 | #-------------------------------------# 2 | # 对数据集进行训练 3 | #-------------------------------------# 4 | import datetime 5 | import os 6 | from functools import partial 7 | 8 | import numpy as np 9 | import torch 10 | import torch.backends.cudnn as cudnn 11 | import torch.distributed as dist 12 | import torch.nn as nn 13 | import torch.optim as optim 14 | from torch import nn 15 | from torch.utils.data import DataLoader 16 | 17 | from nets.yolo import YoloBody 18 | from nets.yolo_training import (YOLOLoss, get_lr_scheduler, set_optimizer_lr, 19 | weights_init) 20 | from utils.callbacks import EvalCallback, LossHistory 21 | from utils.dataloader import YoloDataset, yolo_dataset_collate 22 | from utils.utils import (get_anchors, get_classes, seed_everything, 23 | show_config, worker_init_fn) 24 | from utils.utils_fit import fit_one_epoch 25 | 26 | ''' 27 | 训练自己的目标检测模型一定需要注意以下几点: 28 | 1、训练前仔细检查自己的格式是否满足要求,该库要求数据集格式为VOC格式,需要准备好的内容有输入图片和标签 29 | 输入图片为.jpg图片,无需固定大小,传入训练前会自动进行resize。 30 | 灰度图会自动转成RGB图片进行训练,无需自己修改。 31 | 输入图片如果后缀非jpg,需要自己批量转成jpg后再开始训练。 32 | 33 | 标签为.xml格式,文件中会有需要检测的目标信息,标签文件和输入图片文件相对应。 34 | 35 | 2、损失值的大小用于判断是否收敛,比较重要的是有收敛的趋势,即验证集损失不断下降,如果验证集损失基本上不改变的话,模型基本上就收敛了。 36 | 损失值的具体大小并没有什么意义,大和小只在于损失的计算方式,并不是接近于0才好。如果想要让损失好看点,可以直接到对应的损失函数里面除上10000。 37 | 训练过程中的损失值会保存在logs文件夹下的loss_%Y_%m_%d_%H_%M_%S文件夹中 38 | 39 | 3、训练好的权值文件保存在logs文件夹中,每个训练世代(Epoch)包含若干训练步长(Step),每个训练步长(Step)进行一次梯度下降。 40 | 如果只是训练了几个Step是不会保存的,Epoch和Step的概念要捋清楚一下。 41 | ''' 42 | if __name__ == "__main__": 43 | #---------------------------------# 44 | # Cuda 是否使用Cuda 45 | # 没有GPU可以设置成False 46 | #---------------------------------# 47 | Cuda = True 48 | #----------------------------------------------# 49 | # Seed 用于固定随机种子 50 | # 使得每次独立训练都可以获得一样的结果 51 | #----------------------------------------------# 52 | seed = 11 53 | #---------------------------------------------------------------------# 54 | # distributed 用于指定是否使用单机多卡分布式运行 55 | # 终端指令仅支持Ubuntu。CUDA_VISIBLE_DEVICES用于在Ubuntu下指定显卡。 56 | # Windows系统下默认使用DP模式调用所有显卡,不支持DDP。 57 | # DP模式: 58 | # 设置 distributed = False 59 | # 在终端中输入 CUDA_VISIBLE_DEVICES=0,1 python train.py 60 | # DDP模式: 61 | # 设置 distributed = True 62 | # 在终端中输入 CUDA_VISIBLE_DEVICES=0,1 python -m torch.distributed.launch --nproc_per_node=2 train.py 63 | #---------------------------------------------------------------------# 64 | distributed = False 65 | #---------------------------------------------------------------------# 66 | # sync_bn 是否使用sync_bn,DDP模式多卡可用 67 | #---------------------------------------------------------------------# 68 | sync_bn = False 69 | #---------------------------------------------------------------------# 70 | # fp16 是否使用混合精度训练 71 | # 可减少约一半的显存、需要pytorch1.7.1以上 72 | #---------------------------------------------------------------------# 73 | fp16 = False 74 | #---------------------------------------------------------------------# 75 | # classes_path 指向model_data下的txt,与自己训练的数据集相关 76 | # 训练前一定要修改classes_path,使其对应自己的数据集 77 | #---------------------------------------------------------------------# 78 | classes_path = 'model_data/voc_classes.txt' 79 | #---------------------------------------------------------------------# 80 | # anchors_path 代表先验框对应的txt文件,一般不修改。 81 | # anchors_mask 用于帮助代码找到对应的先验框,一般不修改。 82 | # YoloV4-Tiny中,鉴于tiny模型对小目标的识别效果一般, 83 | # 官方使用的就是[[3, 4, 5], [1, 2, 3]], 84 | # 序号为0先验框未被使用到,无需过分纠结。 85 | #---------------------------------------------------------------------# 86 | anchors_path = 'model_data/yolo_anchors.txt' 87 | anchors_mask = [[3, 4, 5], [1, 2, 3]] 88 | #----------------------------------------------------------------------------------------------------------------------------# 89 | # 权值文件的下载请看README,可以通过网盘下载。模型的 预训练权重 对不同数据集是通用的,因为特征是通用的。 90 | # 模型的 预训练权重 比较重要的部分是 主干特征提取网络的权值部分,用于进行特征提取。 91 | # 预训练权重对于99%的情况都必须要用,不用的话主干部分的权值太过随机,特征提取效果不明显,网络训练的结果也不会好 92 | # 93 | # 如果训练过程中存在中断训练的操作,可以将model_path设置成logs文件夹下的权值文件,将已经训练了一部分的权值再次载入。 94 | # 同时修改下方的 冻结阶段 或者 解冻阶段 的参数,来保证模型epoch的连续性。 95 | # 96 | # 当model_path = ''的时候不加载整个模型的权值。 97 | # 98 | # 此处使用的是整个模型的权重,因此是在train.py进行加载的,下面的pretrain不影响此处的权值加载。 99 | # 如果想要让模型从主干的预训练权值开始训练,则设置model_path = '',下面的pretrain = True,此时仅加载主干。 100 | # 如果想要让模型从0开始训练,则设置model_path = '',下面的pretrain = Fasle,Freeze_Train = Fasle,此时从0开始训练,且没有冻结主干的过程。 101 | # 102 | # 一般来讲,网络从0开始的训练效果会很差,因为权值太过随机,特征提取效果不明显,因此非常、非常、非常不建议大家从0开始训练! 103 | # 从0开始训练有两个方案: 104 | # 1、得益于Mosaic数据增强方法强大的数据增强能力,将UnFreeze_Epoch设置的较大(300及以上)、batch较大(16及以上)、数据较多(万以上)的情况下, 105 | # 可以设置mosaic=True,直接随机初始化参数开始训练,但得到的效果仍然不如有预训练的情况。(像COCO这样的大数据集可以这样做) 106 | # 2、了解imagenet数据集,首先训练分类模型,获得网络的主干部分权值,分类模型的 主干部分 和该模型通用,基于此进行训练。 107 | #----------------------------------------------------------------------------------------------------------------------------# 108 | model_path = 'model_data/yolov4_tiny_weights_coco.pth' 109 | #------------------------------------------------------# 110 | # input_shape 输入的shape大小,一定要是32的倍数 111 | #------------------------------------------------------# 112 | input_shape = [416, 416] 113 | #-------------------------------# 114 | # 所使用的注意力机制的类型 115 | # phi = 0为不使用注意力机制 116 | # phi = 1为SE 117 | # phi = 2为CBAM 118 | # phi = 3为ECA 119 | # phi = 4为CA 120 | #-------------------------------# 121 | phi = 0 122 | #----------------------------------------------------------------------------------------------------------------------------# 123 | # pretrained 是否使用主干网络的预训练权重,此处使用的是主干的权重,因此是在模型构建的时候进行加载的。 124 | # 如果设置了model_path,则主干的权值无需加载,pretrained的值无意义。 125 | # 如果不设置model_path,pretrained = True,此时仅加载主干开始训练。 126 | # 如果不设置model_path,pretrained = False,Freeze_Train = Fasle,此时从0开始训练,且没有冻结主干的过程。 127 | #----------------------------------------------------------------------------------------------------------------------------# 128 | pretrained = False 129 | #------------------------------------------------------------------# 130 | # mosaic 马赛克数据增强。 131 | # mosaic_prob 每个step有多少概率使用mosaic数据增强,默认50%。 132 | # 133 | # mixup 是否使用mixup数据增强,仅在mosaic=True时有效。 134 | # 只会对mosaic增强后的图片进行mixup的处理。 135 | # mixup_prob 有多少概率在mosaic后使用mixup数据增强,默认50%。 136 | # 总的mixup概率为mosaic_prob * mixup_prob。 137 | # 138 | # special_aug_ratio 参考YoloX,由于Mosaic生成的训练图片,远远脱离自然图片的真实分布。 139 | # 当mosaic=True时,本代码会在special_aug_ratio范围内开启mosaic。 140 | # 默认为前70%个epoch,100个世代会开启70个世代。 141 | # 142 | # 余弦退火算法的参数放到下面的lr_decay_type中设置 143 | #------------------------------------------------------------------# 144 | mosaic = False 145 | mosaic_prob = 0.5 146 | mixup = False 147 | mixup_prob = 0.5 148 | special_aug_ratio = 0.7 149 | #------------------------------------------------------------------# 150 | # label_smoothing 标签平滑。一般0.01以下。如0.01、0.005。 151 | #------------------------------------------------------------------# 152 | label_smoothing = 0 153 | 154 | #----------------------------------------------------------------------------------------------------------------------------# 155 | # 训练分为两个阶段,分别是冻结阶段和解冻阶段。设置冻结阶段是为了满足机器性能不足的同学的训练需求。 156 | # 冻结训练需要的显存较小,显卡非常差的情况下,可设置Freeze_Epoch等于UnFreeze_Epoch,此时仅仅进行冻结训练。 157 | # 158 | # 在此提供若干参数设置建议,各位训练者根据自己的需求进行灵活调整: 159 | # (一)从整个模型的预训练权重开始训练: 160 | # Adam: 161 | # Init_Epoch = 0,Freeze_Epoch = 50,UnFreeze_Epoch = 100,Freeze_Train = True,optimizer_type = 'adam',Init_lr = 1e-3,weight_decay = 0。(冻结) 162 | # Init_Epoch = 0,UnFreeze_Epoch = 100,Freeze_Train = False,optimizer_type = 'adam',Init_lr = 1e-3,weight_decay = 0。(不冻结) 163 | # SGD: 164 | # Init_Epoch = 0,Freeze_Epoch = 50,UnFreeze_Epoch = 300,Freeze_Train = True,optimizer_type = 'sgd',Init_lr = 1e-2,weight_decay = 5e-4。(冻结) 165 | # Init_Epoch = 0,UnFreeze_Epoch = 300,Freeze_Train = False,optimizer_type = 'sgd',Init_lr = 1e-2,weight_decay = 5e-4。(不冻结) 166 | # 其中:UnFreeze_Epoch可以在100-300之间调整。 167 | # (二)从主干网络的预训练权重开始训练: 168 | # Adam: 169 | # Init_Epoch = 0,Freeze_Epoch = 50,UnFreeze_Epoch = 100,Freeze_Train = True,optimizer_type = 'adam',Init_lr = 1e-3,weight_decay = 0。(冻结) 170 | # Init_Epoch = 0,UnFreeze_Epoch = 100,Freeze_Train = False,optimizer_type = 'adam',Init_lr = 1e-3,weight_decay = 0。(不冻结) 171 | # SGD: 172 | # Init_Epoch = 0,Freeze_Epoch = 50,UnFreeze_Epoch = 300,Freeze_Train = True,optimizer_type = 'sgd',Init_lr = 1e-2,weight_decay = 5e-4。(冻结) 173 | # Init_Epoch = 0,UnFreeze_Epoch = 300,Freeze_Train = False,optimizer_type = 'sgd',Init_lr = 1e-2,weight_decay = 5e-4。(不冻结) 174 | # 其中:由于从主干网络的预训练权重开始训练,主干的权值不一定适合目标检测,需要更多的训练跳出局部最优解。 175 | # UnFreeze_Epoch可以在150-300之间调整,YOLOV5和YOLOX均推荐使用300。 176 | # Adam相较于SGD收敛的快一些。因此UnFreeze_Epoch理论上可以小一点,但依然推荐更多的Epoch。 177 | # (三)从0开始训练: 178 | # Init_Epoch = 0,UnFreeze_Epoch >= 300,Unfreeze_batch_size >= 16,Freeze_Train = False(不冻结训练) 179 | # 其中:UnFreeze_Epoch尽量不小于300。optimizer_type = 'sgd',Init_lr = 1e-2,mosaic = True。 180 | # (四)batch_size的设置: 181 | # 在显卡能够接受的范围内,以大为好。显存不足与数据集大小无关,提示显存不足(OOM或者CUDA out of memory)请调小batch_size。 182 | # 受到BatchNorm层影响,batch_size最小为2,不能为1。 183 | # 正常情况下Freeze_batch_size建议为Unfreeze_batch_size的1-2倍。不建议设置的差距过大,因为关系到学习率的自动调整。 184 | #----------------------------------------------------------------------------------------------------------------------------# 185 | #------------------------------------------------------------------# 186 | # 冻结阶段训练参数 187 | # 此时模型的主干被冻结了,特征提取网络不发生改变 188 | # 占用的显存较小,仅对网络进行微调 189 | # Init_Epoch 模型当前开始的训练世代,其值可以大于Freeze_Epoch,如设置: 190 | # Init_Epoch = 60、Freeze_Epoch = 50、UnFreeze_Epoch = 100 191 | # 会跳过冻结阶段,直接从60代开始,并调整对应的学习率。 192 | # (断点续练时使用) 193 | # Freeze_Epoch 模型冻结训练的Freeze_Epoch 194 | # (当Freeze_Train=False时失效) 195 | # Freeze_batch_size 模型冻结训练的batch_size 196 | # (当Freeze_Train=False时失效) 197 | #------------------------------------------------------------------# 198 | Init_Epoch = 0 199 | Freeze_Epoch = 50 200 | Freeze_batch_size = 32 201 | #------------------------------------------------------------------# 202 | # 解冻阶段训练参数 203 | # 此时模型的主干不被冻结了,特征提取网络会发生改变 204 | # 占用的显存较大,网络所有的参数都会发生改变 205 | # UnFreeze_Epoch 模型总共训练的epoch 206 | # SGD需要更长的时间收敛,因此设置较大的UnFreeze_Epoch 207 | # Adam可以使用相对较小的UnFreeze_Epoch 208 | # Unfreeze_batch_size 模型在解冻后的batch_size 209 | #------------------------------------------------------------------# 210 | UnFreeze_Epoch = 300 211 | Unfreeze_batch_size = 16 212 | #------------------------------------------------------------------# 213 | # Freeze_Train 是否进行冻结训练 214 | # 默认先冻结主干训练后解冻训练。 215 | #------------------------------------------------------------------# 216 | Freeze_Train = False 217 | 218 | #------------------------------------------------------------------# 219 | # 其它训练参数:学习率、优化器、学习率下降有关 220 | #------------------------------------------------------------------# 221 | #------------------------------------------------------------------# 222 | # Init_lr 模型的最大学习率 223 | # Min_lr 模型的最小学习率,默认为最大学习率的0.01 224 | #------------------------------------------------------------------# 225 | Init_lr = 1e-2 226 | Min_lr = Init_lr * 0.01 227 | #------------------------------------------------------------------# 228 | # optimizer_type 使用到的优化器种类,可选的有adam、sgd 229 | # 当使用Adam优化器时建议设置 Init_lr=1e-3 230 | # 当使用SGD优化器时建议设置 Init_lr=1e-2 231 | # momentum 优化器内部使用到的momentum参数 232 | # weight_decay 权值衰减,可防止过拟合 233 | # adam会导致weight_decay错误,使用adam时建议设置为0。 234 | #------------------------------------------------------------------# 235 | optimizer_type = "sgd" 236 | momentum = 0.937 237 | weight_decay = 5e-4 238 | #------------------------------------------------------------------# 239 | # lr_decay_type 使用到的学习率下降方式,可选的有step、cos 240 | #------------------------------------------------------------------# 241 | lr_decay_type = "cos" 242 | #------------------------------------------------------------------# 243 | # save_period 多少个epoch保存一次权值 244 | #------------------------------------------------------------------# 245 | save_period = 10 246 | #------------------------------------------------------------------# 247 | # save_dir 权值与日志文件保存的文件夹 248 | #------------------------------------------------------------------# 249 | save_dir = 'logs' 250 | #------------------------------------------------------------------# 251 | # eval_flag 是否在训练时进行评估,评估对象为验证集 252 | # 安装pycocotools库后,评估体验更佳。 253 | # eval_period 代表多少个epoch评估一次,不建议频繁的评估 254 | # 评估需要消耗较多的时间,频繁评估会导致训练非常慢 255 | # 此处获得的mAP会与get_map.py获得的会有所不同,原因有二: 256 | # (一)此处获得的mAP为验证集的mAP。 257 | # (二)此处设置评估参数较为保守,目的是加快评估速度。 258 | #------------------------------------------------------------------# 259 | eval_flag = True 260 | eval_period = 10 261 | #------------------------------------------------------------------# 262 | # num_workers 用于设置是否使用多线程读取数据 263 | # 开启后会加快数据读取速度,但是会占用更多内存 264 | # 内存较小的电脑可以设置为2或者0 265 | #------------------------------------------------------------------# 266 | num_workers = 4 267 | 268 | #------------------------------------------------------# 269 | # train_annotation_path 训练图片路径和标签 270 | # val_annotation_path 验证图片路径和标签 271 | #------------------------------------------------------# 272 | train_annotation_path = '2007_train.txt' 273 | val_annotation_path = '2007_val.txt' 274 | 275 | seed_everything(seed) 276 | #------------------------------------------------------# 277 | # 设置用到的显卡 278 | #------------------------------------------------------# 279 | ngpus_per_node = torch.cuda.device_count() 280 | if distributed: 281 | dist.init_process_group(backend="nccl") 282 | local_rank = int(os.environ["LOCAL_RANK"]) 283 | rank = int(os.environ["RANK"]) 284 | device = torch.device("cuda", local_rank) 285 | if local_rank == 0: 286 | print(f"[{os.getpid()}] (rank = {rank}, local_rank = {local_rank}) training...") 287 | print("Gpu Device Count : ", ngpus_per_node) 288 | else: 289 | device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') 290 | local_rank = 0 291 | rank = 0 292 | 293 | #----------------------------------------------------# 294 | # 获取classes和anchor 295 | #----------------------------------------------------# 296 | class_names, num_classes = get_classes(classes_path) 297 | anchors, num_anchors = get_anchors(anchors_path) 298 | 299 | #------------------------------------------------------# 300 | # 创建yolo模型 301 | #------------------------------------------------------# 302 | model = YoloBody(anchors_mask, num_classes, pretrained = pretrained, phi = phi) 303 | if not pretrained: 304 | weights_init(model) 305 | if model_path != '': 306 | #------------------------------------------------------# 307 | # 权值文件请看README,百度网盘下载 308 | #------------------------------------------------------# 309 | if local_rank == 0: 310 | print('Load weights {}.'.format(model_path)) 311 | 312 | #------------------------------------------------------# 313 | # 根据预训练权重的Key和模型的Key进行加载 314 | #------------------------------------------------------# 315 | model_dict = model.state_dict() 316 | pretrained_dict = torch.load(model_path, map_location = device) 317 | load_key, no_load_key, temp_dict = [], [], {} 318 | for k, v in pretrained_dict.items(): 319 | if k in model_dict.keys() and np.shape(model_dict[k]) == np.shape(v): 320 | temp_dict[k] = v 321 | load_key.append(k) 322 | else: 323 | no_load_key.append(k) 324 | model_dict.update(temp_dict) 325 | model.load_state_dict(model_dict) 326 | #------------------------------------------------------# 327 | # 显示没有匹配上的Key 328 | #------------------------------------------------------# 329 | if local_rank == 0: 330 | print("\nSuccessful Load Key:", str(load_key)[:500], "……\nSuccessful Load Key Num:", len(load_key)) 331 | print("\nFail To Load Key:", str(no_load_key)[:500], "……\nFail To Load Key num:", len(no_load_key)) 332 | print("\n\033[1;33;44m温馨提示,head部分没有载入是正常现象,Backbone部分没有载入是错误的。\033[0m") 333 | 334 | #----------------------# 335 | # 获得损失函数 336 | #----------------------# 337 | yolo_loss = YOLOLoss(anchors, num_classes, input_shape, Cuda, anchors_mask, label_smoothing) 338 | #----------------------# 339 | # 记录Loss 340 | #----------------------# 341 | if local_rank == 0: 342 | time_str = datetime.datetime.strftime(datetime.datetime.now(),'%Y_%m_%d_%H_%M_%S') 343 | log_dir = os.path.join(save_dir, "loss_" + str(time_str)) 344 | loss_history = LossHistory(log_dir, model, input_shape=input_shape) 345 | else: 346 | loss_history = None 347 | 348 | #------------------------------------------------------------------# 349 | # torch 1.2不支持amp,建议使用torch 1.7.1及以上正确使用fp16 350 | # 因此torch1.2这里显示"could not be resolve" 351 | #------------------------------------------------------------------# 352 | if fp16: 353 | from torch.cuda.amp import GradScaler as GradScaler 354 | scaler = GradScaler() 355 | else: 356 | scaler = None 357 | 358 | model_train = model.train() 359 | #----------------------------# 360 | # 多卡同步Bn 361 | #----------------------------# 362 | if sync_bn and ngpus_per_node > 1 and distributed: 363 | model_train = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model_train) 364 | elif sync_bn: 365 | print("Sync_bn is not support in one gpu or not distributed.") 366 | 367 | if Cuda: 368 | if distributed: 369 | #----------------------------# 370 | # 多卡平行运行 371 | #----------------------------# 372 | model_train = model_train.cuda(local_rank) 373 | model_train = torch.nn.parallel.DistributedDataParallel(model_train, device_ids=[local_rank], find_unused_parameters=True) 374 | else: 375 | model_train = torch.nn.DataParallel(model) 376 | cudnn.benchmark = True 377 | model_train = model_train.cuda() 378 | 379 | #---------------------------# 380 | # 读取数据集对应的txt 381 | #---------------------------# 382 | with open(train_annotation_path, encoding='utf-8') as f: 383 | train_lines = f.readlines() 384 | with open(val_annotation_path, encoding='utf-8') as f: 385 | val_lines = f.readlines() 386 | num_train = len(train_lines) 387 | num_val = len(val_lines) 388 | 389 | if local_rank == 0: 390 | show_config( 391 | classes_path = classes_path, anchors_path = anchors_path, anchors_mask = anchors_mask, model_path = model_path, input_shape = input_shape, \ 392 | Init_Epoch = Init_Epoch, Freeze_Epoch = Freeze_Epoch, UnFreeze_Epoch = UnFreeze_Epoch, Freeze_batch_size = Freeze_batch_size, Unfreeze_batch_size = Unfreeze_batch_size, Freeze_Train = Freeze_Train, \ 393 | Init_lr = Init_lr, Min_lr = Min_lr, optimizer_type = optimizer_type, momentum = momentum, lr_decay_type = lr_decay_type, \ 394 | save_period = save_period, save_dir = save_dir, num_workers = num_workers, num_train = num_train, num_val = num_val 395 | ) 396 | #---------------------------------------------------------# 397 | # 总训练世代指的是遍历全部数据的总次数 398 | # 总训练步长指的是梯度下降的总次数 399 | # 每个训练世代包含若干训练步长,每个训练步长进行一次梯度下降。 400 | # 此处仅建议最低训练世代,上不封顶,计算时只考虑了解冻部分 401 | #----------------------------------------------------------# 402 | wanted_step = 5e4 if optimizer_type == "sgd" else 1.5e4 403 | total_step = num_train // Unfreeze_batch_size * UnFreeze_Epoch 404 | if total_step <= wanted_step: 405 | if num_train // Unfreeze_batch_size == 0: 406 | raise ValueError('数据集过小,无法进行训练,请扩充数据集。') 407 | wanted_epoch = wanted_step // (num_train // Unfreeze_batch_size) + 1 408 | print("\n\033[1;33;44m[Warning] 使用%s优化器时,建议将训练总步长设置到%d以上。\033[0m"%(optimizer_type, wanted_step)) 409 | print("\033[1;33;44m[Warning] 本次运行的总训练数据量为%d,Unfreeze_batch_size为%d,共训练%d个Epoch,计算出总训练步长为%d。\033[0m"%(num_train, Unfreeze_batch_size, UnFreeze_Epoch, total_step)) 410 | print("\033[1;33;44m[Warning] 由于总训练步长为%d,小于建议总步长%d,建议设置总世代为%d。\033[0m"%(total_step, wanted_step, wanted_epoch)) 411 | 412 | #------------------------------------------------------# 413 | # 主干特征提取网络特征通用,冻结训练可以加快训练速度 414 | # 也可以在训练初期防止权值被破坏。 415 | # Init_Epoch为起始世代 416 | # Freeze_Epoch为冻结训练的世代 417 | # UnFreeze_Epoch总训练世代 418 | # 提示OOM或者显存不足请调小Batch_size 419 | #------------------------------------------------------# 420 | if True: 421 | UnFreeze_flag = False 422 | #------------------------------------# 423 | # 冻结一定部分训练 424 | #------------------------------------# 425 | if Freeze_Train: 426 | for param in model.backbone.parameters(): 427 | param.requires_grad = False 428 | 429 | #-------------------------------------------------------------------# 430 | # 如果不冻结训练的话,直接设置batch_size为Unfreeze_batch_size 431 | #-------------------------------------------------------------------# 432 | batch_size = Freeze_batch_size if Freeze_Train else Unfreeze_batch_size 433 | 434 | #-------------------------------------------------------------------# 435 | # 判断当前batch_size,自适应调整学习率 436 | #-------------------------------------------------------------------# 437 | nbs = 64 438 | lr_limit_max = 1e-3 if optimizer_type == 'adam' else 5e-2 439 | lr_limit_min = 3e-4 if optimizer_type == 'adam' else 5e-4 440 | Init_lr_fit = min(max(batch_size / nbs * Init_lr, lr_limit_min), lr_limit_max) 441 | Min_lr_fit = min(max(batch_size / nbs * Min_lr, lr_limit_min * 1e-2), lr_limit_max * 1e-2) 442 | 443 | #---------------------------------------# 444 | # 根据optimizer_type选择优化器 445 | #---------------------------------------# 446 | pg0, pg1, pg2 = [], [], [] 447 | for k, v in model.named_modules(): 448 | if hasattr(v, "bias") and isinstance(v.bias, nn.Parameter): 449 | pg2.append(v.bias) 450 | if isinstance(v, nn.BatchNorm2d) or "bn" in k: 451 | pg0.append(v.weight) 452 | elif hasattr(v, "weight") and isinstance(v.weight, nn.Parameter): 453 | pg1.append(v.weight) 454 | optimizer = { 455 | 'adam' : optim.Adam(pg0, Init_lr_fit, betas = (momentum, 0.999)), 456 | 'sgd' : optim.SGD(pg0, Init_lr_fit, momentum = momentum, nesterov=True) 457 | }[optimizer_type] 458 | optimizer.add_param_group({"params": pg1, "weight_decay": weight_decay}) 459 | optimizer.add_param_group({"params": pg2}) 460 | 461 | #---------------------------------------# 462 | # 获得学习率下降的公式 463 | #---------------------------------------# 464 | lr_scheduler_func = get_lr_scheduler(lr_decay_type, Init_lr_fit, Min_lr_fit, UnFreeze_Epoch) 465 | 466 | #---------------------------------------# 467 | # 判断每一个世代的长度 468 | #---------------------------------------# 469 | epoch_step = num_train // batch_size 470 | epoch_step_val = num_val // batch_size 471 | 472 | if epoch_step == 0 or epoch_step_val == 0: 473 | raise ValueError("数据集过小,无法继续进行训练,请扩充数据集。") 474 | 475 | #---------------------------------------# 476 | # 构建数据集加载器。 477 | #---------------------------------------# 478 | train_dataset = YoloDataset(train_lines, input_shape, num_classes, epoch_length = UnFreeze_Epoch, \ 479 | mosaic=mosaic, mixup=mixup, mosaic_prob=mosaic_prob, mixup_prob=mixup_prob, train=True, special_aug_ratio=special_aug_ratio) 480 | val_dataset = YoloDataset(val_lines, input_shape, num_classes, epoch_length = UnFreeze_Epoch, \ 481 | mosaic=False, mixup=False, mosaic_prob=0, mixup_prob=0, train=False, special_aug_ratio=0) 482 | 483 | if distributed: 484 | train_sampler = torch.utils.data.distributed.DistributedSampler(train_dataset, shuffle=True,) 485 | val_sampler = torch.utils.data.distributed.DistributedSampler(val_dataset, shuffle=False,) 486 | batch_size = batch_size // ngpus_per_node 487 | shuffle = False 488 | else: 489 | train_sampler = None 490 | val_sampler = None 491 | shuffle = True 492 | 493 | gen = DataLoader(train_dataset, shuffle = shuffle, batch_size = batch_size, num_workers = num_workers, pin_memory=True, 494 | drop_last=True, collate_fn=yolo_dataset_collate, sampler=train_sampler, 495 | worker_init_fn=partial(worker_init_fn, rank=rank, seed=seed)) 496 | gen_val = DataLoader(val_dataset , shuffle = shuffle, batch_size = batch_size, num_workers = num_workers, pin_memory=True, 497 | drop_last=True, collate_fn=yolo_dataset_collate, sampler=val_sampler, 498 | worker_init_fn=partial(worker_init_fn, rank=rank, seed=seed)) 499 | 500 | #----------------------# 501 | # 记录eval的map曲线 502 | #----------------------# 503 | if local_rank == 0: 504 | eval_callback = EvalCallback(model, input_shape, anchors, anchors_mask, class_names, num_classes, val_lines, log_dir, Cuda, \ 505 | eval_flag=eval_flag, period=eval_period) 506 | else: 507 | eval_callback = None 508 | 509 | #---------------------------------------# 510 | # 开始模型训练 511 | #---------------------------------------# 512 | for epoch in range(Init_Epoch, UnFreeze_Epoch): 513 | #---------------------------------------# 514 | # 如果模型有冻结学习部分 515 | # 则解冻,并设置参数 516 | #---------------------------------------# 517 | if epoch >= Freeze_Epoch and not UnFreeze_flag and Freeze_Train: 518 | batch_size = Unfreeze_batch_size 519 | 520 | #-------------------------------------------------------------------# 521 | # 判断当前batch_size,自适应调整学习率 522 | #-------------------------------------------------------------------# 523 | nbs = 64 524 | lr_limit_max = 1e-3 if optimizer_type == 'adam' else 5e-2 525 | lr_limit_min = 3e-4 if optimizer_type == 'adam' else 5e-4 526 | Init_lr_fit = min(max(batch_size / nbs * Init_lr, lr_limit_min), lr_limit_max) 527 | Min_lr_fit = min(max(batch_size / nbs * Min_lr, lr_limit_min * 1e-2), lr_limit_max * 1e-2) 528 | 529 | #---------------------------------------# 530 | # 获得学习率下降的公式 531 | #---------------------------------------# 532 | lr_scheduler_func = get_lr_scheduler(lr_decay_type, Init_lr_fit, Min_lr_fit, UnFreeze_Epoch) 533 | 534 | for param in model.backbone.parameters(): 535 | param.requires_grad = True 536 | 537 | epoch_step = num_train // batch_size 538 | epoch_step_val = num_val // batch_size 539 | 540 | if epoch_step == 0 or epoch_step_val == 0: 541 | raise ValueError("数据集过小,无法继续进行训练,请扩充数据集。") 542 | 543 | if distributed: 544 | batch_size = batch_size // ngpus_per_node 545 | 546 | gen = DataLoader(train_dataset, shuffle = shuffle, batch_size = batch_size, num_workers = num_workers, pin_memory=True, 547 | drop_last=True, collate_fn=yolo_dataset_collate, sampler=train_sampler, 548 | worker_init_fn=partial(worker_init_fn, rank=rank, seed=seed)) 549 | gen_val = DataLoader(val_dataset , shuffle = shuffle, batch_size = batch_size, num_workers = num_workers, pin_memory=True, 550 | drop_last=True, collate_fn=yolo_dataset_collate, sampler=val_sampler, 551 | worker_init_fn=partial(worker_init_fn, rank=rank, seed=seed)) 552 | 553 | UnFreeze_flag = True 554 | 555 | gen.dataset.epoch_now = epoch 556 | gen_val.dataset.epoch_now = epoch 557 | 558 | if distributed: 559 | train_sampler.set_epoch(epoch) 560 | 561 | set_optimizer_lr(optimizer, lr_scheduler_func, epoch) 562 | 563 | fit_one_epoch(model_train, model, yolo_loss, loss_history, eval_callback, optimizer, epoch, epoch_step, epoch_step_val, gen, gen_val, UnFreeze_Epoch, Cuda, fp16, scaler, save_period, save_dir, local_rank) 564 | 565 | if distributed: 566 | dist.barrier() 567 | 568 | if local_rank == 0: 569 | loss_history.writer.close() 570 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------- /utils/callbacks.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | 4 | import torch 5 | import matplotlib 6 | matplotlib.use('Agg') 7 | import scipy.signal 8 | from matplotlib import pyplot as plt 9 | from torch.utils.tensorboard import SummaryWriter 10 | 11 | import shutil 12 | import numpy as np 13 | 14 | from PIL import Image 15 | from tqdm import tqdm 16 | from .utils import cvtColor, preprocess_input, resize_image 17 | from .utils_bbox import DecodeBox 18 | from .utils_map import get_coco_map, get_map 19 | 20 | 21 | class LossHistory(): 22 | def __init__(self, log_dir, model, input_shape): 23 | self.log_dir = log_dir 24 | self.losses = [] 25 | self.val_loss = [] 26 | 27 | os.makedirs(self.log_dir) 28 | self.writer = SummaryWriter(self.log_dir) 29 | try: 30 | dummy_input = torch.randn(2, 3, input_shape[0], input_shape[1]) 31 | self.writer.add_graph(model, dummy_input) 32 | except: 33 | pass 34 | 35 | def append_loss(self, epoch, loss, val_loss): 36 | if not os.path.exists(self.log_dir): 37 | os.makedirs(self.log_dir) 38 | 39 | self.losses.append(loss) 40 | self.val_loss.append(val_loss) 41 | 42 | with open(os.path.join(self.log_dir, "epoch_loss.txt"), 'a') as f: 43 | f.write(str(loss)) 44 | f.write("\n") 45 | with open(os.path.join(self.log_dir, "epoch_val_loss.txt"), 'a') as f: 46 | f.write(str(val_loss)) 47 | f.write("\n") 48 | 49 | self.writer.add_scalar('loss', loss, epoch) 50 | self.writer.add_scalar('val_loss', val_loss, epoch) 51 | self.loss_plot() 52 | 53 | def loss_plot(self): 54 | iters = range(len(self.losses)) 55 | 56 | plt.figure() 57 | plt.plot(iters, self.losses, 'red', linewidth = 2, label='train loss') 58 | plt.plot(iters, self.val_loss, 'coral', linewidth = 2, label='val loss') 59 | try: 60 | if len(self.losses) < 25: 61 | num = 5 62 | else: 63 | num = 15 64 | 65 | plt.plot(iters, scipy.signal.savgol_filter(self.losses, num, 3), 'green', linestyle = '--', linewidth = 2, label='smooth train loss') 66 | plt.plot(iters, scipy.signal.savgol_filter(self.val_loss, num, 3), '#8B4513', linestyle = '--', linewidth = 2, label='smooth val loss') 67 | except: 68 | pass 69 | 70 | plt.grid(True) 71 | plt.xlabel('Epoch') 72 | plt.ylabel('Loss') 73 | plt.legend(loc="upper right") 74 | 75 | plt.savefig(os.path.join(self.log_dir, "epoch_loss.png")) 76 | 77 | plt.cla() 78 | plt.close("all") 79 | 80 | class EvalCallback(): 81 | def __init__(self, net, input_shape, anchors, anchors_mask, class_names, num_classes, val_lines, log_dir, cuda, \ 82 | map_out_path=".temp_map_out", max_boxes=100, confidence=0.05, nms_iou=0.5, letterbox_image=True, MINOVERLAP=0.5, eval_flag=True, period=1): 83 | super(EvalCallback, self).__init__() 84 | 85 | self.net = net 86 | self.input_shape = input_shape 87 | self.anchors = anchors 88 | self.anchors_mask = anchors_mask 89 | self.class_names = class_names 90 | self.num_classes = num_classes 91 | self.val_lines = val_lines 92 | self.log_dir = log_dir 93 | self.cuda = cuda 94 | self.map_out_path = map_out_path 95 | self.max_boxes = max_boxes 96 | self.confidence = confidence 97 | self.nms_iou = nms_iou 98 | self.letterbox_image = letterbox_image 99 | self.MINOVERLAP = MINOVERLAP 100 | self.eval_flag = eval_flag 101 | self.period = period 102 | 103 | self.bbox_util = DecodeBox(self.anchors, self.num_classes, (self.input_shape[0], self.input_shape[1]), self.anchors_mask) 104 | 105 | self.maps = [0] 106 | self.epoches = [0] 107 | if self.eval_flag: 108 | with open(os.path.join(self.log_dir, "epoch_map.txt"), 'a') as f: 109 | f.write(str(0)) 110 | f.write("\n") 111 | 112 | def get_map_txt(self, image_id, image, class_names, map_out_path): 113 | f = open(os.path.join(map_out_path, "detection-results/"+image_id+".txt"), "w", encoding='utf-8') 114 | image_shape = np.array(np.shape(image)[0:2]) 115 | #---------------------------------------------------------# 116 | # 在这里将图像转换成RGB图像,防止灰度图在预测时报错。 117 | # 代码仅仅支持RGB图像的预测,所有其它类型的图像都会转化成RGB 118 | #---------------------------------------------------------# 119 | image = cvtColor(image) 120 | #---------------------------------------------------------# 121 | # 给图像增加灰条,实现不失真的resize 122 | # 也可以直接resize进行识别 123 | #---------------------------------------------------------# 124 | image_data = resize_image(image, (self.input_shape[1], self.input_shape[0]), self.letterbox_image) 125 | #---------------------------------------------------------# 126 | # 添加上batch_size维度 127 | #---------------------------------------------------------# 128 | image_data = np.expand_dims(np.transpose(preprocess_input(np.array(image_data, dtype='float32')), (2, 0, 1)), 0) 129 | 130 | with torch.no_grad(): 131 | images = torch.from_numpy(image_data) 132 | if self.cuda: 133 | images = images.cuda() 134 | #---------------------------------------------------------# 135 | # 将图像输入网络当中进行预测! 136 | #---------------------------------------------------------# 137 | outputs = self.net(images) 138 | outputs = self.bbox_util.decode_box(outputs) 139 | #---------------------------------------------------------# 140 | # 将预测框进行堆叠,然后进行非极大抑制 141 | #---------------------------------------------------------# 142 | results = self.bbox_util.non_max_suppression(torch.cat(outputs, 1), self.num_classes, self.input_shape, 143 | image_shape, self.letterbox_image, conf_thres = self.confidence, nms_thres = self.nms_iou) 144 | 145 | if results[0] is None: 146 | return 147 | 148 | top_label = np.array(results[0][:, 6], dtype = 'int32') 149 | top_conf = results[0][:, 4] * results[0][:, 5] 150 | top_boxes = results[0][:, :4] 151 | 152 | top_100 = np.argsort(top_conf)[::-1][:self.max_boxes] 153 | top_boxes = top_boxes[top_100] 154 | top_conf = top_conf[top_100] 155 | top_label = top_label[top_100] 156 | 157 | for i, c in list(enumerate(top_label)): 158 | predicted_class = self.class_names[int(c)] 159 | box = top_boxes[i] 160 | score = str(top_conf[i]) 161 | 162 | top, left, bottom, right = box 163 | if predicted_class not in class_names: 164 | continue 165 | 166 | f.write("%s %s %s %s %s %s\n" % (predicted_class, score[:6], str(int(left)), str(int(top)), str(int(right)),str(int(bottom)))) 167 | 168 | f.close() 169 | return 170 | 171 | def on_epoch_end(self, epoch, model_eval): 172 | if epoch % self.period == 0 and self.eval_flag: 173 | self.net = model_eval 174 | if not os.path.exists(self.map_out_path): 175 | os.makedirs(self.map_out_path) 176 | if not os.path.exists(os.path.join(self.map_out_path, "ground-truth")): 177 | os.makedirs(os.path.join(self.map_out_path, "ground-truth")) 178 | if not os.path.exists(os.path.join(self.map_out_path, "detection-results")): 179 | os.makedirs(os.path.join(self.map_out_path, "detection-results")) 180 | print("Get map.") 181 | for annotation_line in tqdm(self.val_lines): 182 | line = annotation_line.split() 183 | image_id = os.path.basename(line[0]).split('.')[0] 184 | #------------------------------# 185 | # 读取图像并转换成RGB图像 186 | #------------------------------# 187 | image = Image.open(line[0]) 188 | #------------------------------# 189 | # 获得预测框 190 | #------------------------------# 191 | gt_boxes = np.array([np.array(list(map(int,box.split(',')))) for box in line[1:]]) 192 | #------------------------------# 193 | # 获得预测txt 194 | #------------------------------# 195 | self.get_map_txt(image_id, image, self.class_names, self.map_out_path) 196 | 197 | #------------------------------# 198 | # 获得真实框txt 199 | #------------------------------# 200 | with open(os.path.join(self.map_out_path, "ground-truth/"+image_id+".txt"), "w") as new_f: 201 | for box in gt_boxes: 202 | left, top, right, bottom, obj = box 203 | obj_name = self.class_names[obj] 204 | new_f.write("%s %s %s %s %s\n" % (obj_name, left, top, right, bottom)) 205 | 206 | print("Calculate Map.") 207 | try: 208 | temp_map = get_coco_map(class_names = self.class_names, path = self.map_out_path)[1] 209 | except: 210 | temp_map = get_map(self.MINOVERLAP, False, path = self.map_out_path) 211 | self.maps.append(temp_map) 212 | self.epoches.append(epoch) 213 | 214 | with open(os.path.join(self.log_dir, "epoch_map.txt"), 'a') as f: 215 | f.write(str(temp_map)) 216 | f.write("\n") 217 | 218 | plt.figure() 219 | plt.plot(self.epoches, self.maps, 'red', linewidth = 2, label='train map') 220 | 221 | plt.grid(True) 222 | plt.xlabel('Epoch') 223 | plt.ylabel('Map %s'%str(self.MINOVERLAP)) 224 | plt.title('A Map Curve') 225 | plt.legend(loc="upper right") 226 | 227 | plt.savefig(os.path.join(self.log_dir, "epoch_map.png")) 228 | plt.cla() 229 | plt.close("all") 230 | 231 | print("Get map done.") 232 | shutil.rmtree(self.map_out_path) 233 | -------------------------------------------------------------------------------- /utils/dataloader.py: -------------------------------------------------------------------------------- 1 | from random import sample, shuffle 2 | 3 | import cv2 4 | import numpy as np 5 | import torch 6 | from PIL import Image 7 | from torch.utils.data.dataset import Dataset 8 | 9 | from utils.utils import cvtColor, preprocess_input 10 | 11 | 12 | class YoloDataset(Dataset): 13 | def __init__(self, annotation_lines, input_shape, num_classes, epoch_length, \ 14 | mosaic, mixup, mosaic_prob, mixup_prob, train, special_aug_ratio = 0.7): 15 | super(YoloDataset, self).__init__() 16 | self.annotation_lines = annotation_lines 17 | self.input_shape = input_shape 18 | self.num_classes = num_classes 19 | self.epoch_length = epoch_length 20 | self.mosaic = mosaic 21 | self.mosaic_prob = mosaic_prob 22 | self.mixup = mixup 23 | self.mixup_prob = mixup_prob 24 | self.train = train 25 | self.special_aug_ratio = special_aug_ratio 26 | 27 | self.epoch_now = -1 28 | self.length = len(self.annotation_lines) 29 | 30 | def __len__(self): 31 | return self.length 32 | 33 | def __getitem__(self, index): 34 | index = index % self.length 35 | 36 | #---------------------------------------------------# 37 | # 训练时进行数据的随机增强 38 | # 验证时不进行数据的随机增强 39 | #---------------------------------------------------# 40 | if self.mosaic and self.rand() < self.mosaic_prob and self.epoch_now < self.epoch_length * self.special_aug_ratio: 41 | lines = sample(self.annotation_lines, 3) 42 | lines.append(self.annotation_lines[index]) 43 | shuffle(lines) 44 | image, box = self.get_random_data_with_Mosaic(lines, self.input_shape) 45 | 46 | if self.mixup and self.rand() < self.mixup_prob: 47 | lines = sample(self.annotation_lines, 1) 48 | image_2, box_2 = self.get_random_data(lines[0], self.input_shape, random = self.train) 49 | image, box = self.get_random_data_with_MixUp(image, box, image_2, box_2) 50 | else: 51 | image, box = self.get_random_data(self.annotation_lines[index], self.input_shape, random = self.train) 52 | 53 | image = np.transpose(preprocess_input(np.array(image, dtype=np.float32)), (2, 0, 1)) 54 | box = np.array(box, dtype=np.float32) 55 | if len(box) != 0: 56 | box[:, [0, 2]] = box[:, [0, 2]] / self.input_shape[1] 57 | box[:, [1, 3]] = box[:, [1, 3]] / self.input_shape[0] 58 | 59 | box[:, 2:4] = box[:, 2:4] - box[:, 0:2] 60 | box[:, 0:2] = box[:, 0:2] + box[:, 2:4] / 2 61 | return image, box 62 | 63 | def rand(self, a=0, b=1): 64 | return np.random.rand()*(b-a) + a 65 | 66 | def get_random_data(self, annotation_line, input_shape, jitter=.3, hue=.1, sat=0.7, val=0.4, random=True): 67 | line = annotation_line.split() 68 | #------------------------------# 69 | # 读取图像并转换成RGB图像 70 | #------------------------------# 71 | image = Image.open(line[0]) 72 | image = cvtColor(image) 73 | #------------------------------# 74 | # 获得图像的高宽与目标高宽 75 | #------------------------------# 76 | iw, ih = image.size 77 | h, w = input_shape 78 | #------------------------------# 79 | # 获得预测框 80 | #------------------------------# 81 | box = np.array([np.array(list(map(int,box.split(',')))) for box in line[1:]]) 82 | 83 | if not random: 84 | scale = min(w/iw, h/ih) 85 | nw = int(iw*scale) 86 | nh = int(ih*scale) 87 | dx = (w-nw)//2 88 | dy = (h-nh)//2 89 | 90 | #---------------------------------# 91 | # 将图像多余的部分加上灰条 92 | #---------------------------------# 93 | image = image.resize((nw,nh), Image.BICUBIC) 94 | new_image = Image.new('RGB', (w,h), (128,128,128)) 95 | new_image.paste(image, (dx, dy)) 96 | image_data = np.array(new_image, np.float32) 97 | 98 | #---------------------------------# 99 | # 对真实框进行调整 100 | #---------------------------------# 101 | if len(box)>0: 102 | np.random.shuffle(box) 103 | box[:, [0,2]] = box[:, [0,2]]*nw/iw + dx 104 | box[:, [1,3]] = box[:, [1,3]]*nh/ih + dy 105 | box[:, 0:2][box[:, 0:2]<0] = 0 106 | box[:, 2][box[:, 2]>w] = w 107 | box[:, 3][box[:, 3]>h] = h 108 | box_w = box[:, 2] - box[:, 0] 109 | box_h = box[:, 3] - box[:, 1] 110 | box = box[np.logical_and(box_w>1, box_h>1)] # discard invalid box 111 | 112 | return image_data, box 113 | 114 | #------------------------------------------# 115 | # 对图像进行缩放并且进行长和宽的扭曲 116 | #------------------------------------------# 117 | new_ar = iw/ih * self.rand(1-jitter,1+jitter) / self.rand(1-jitter,1+jitter) 118 | scale = self.rand(.25, 2) 119 | if new_ar < 1: 120 | nh = int(scale*h) 121 | nw = int(nh*new_ar) 122 | else: 123 | nw = int(scale*w) 124 | nh = int(nw/new_ar) 125 | image = image.resize((nw,nh), Image.BICUBIC) 126 | 127 | #------------------------------------------# 128 | # 将图像多余的部分加上灰条 129 | #------------------------------------------# 130 | dx = int(self.rand(0, w-nw)) 131 | dy = int(self.rand(0, h-nh)) 132 | new_image = Image.new('RGB', (w,h), (128,128,128)) 133 | new_image.paste(image, (dx, dy)) 134 | image = new_image 135 | 136 | #------------------------------------------# 137 | # 翻转图像 138 | #------------------------------------------# 139 | flip = self.rand()<.5 140 | if flip: image = image.transpose(Image.FLIP_LEFT_RIGHT) 141 | 142 | image_data = np.array(image, np.uint8) 143 | #---------------------------------# 144 | # 对图像进行色域变换 145 | # 计算色域变换的参数 146 | #---------------------------------# 147 | r = np.random.uniform(-1, 1, 3) * [hue, sat, val] + 1 148 | #---------------------------------# 149 | # 将图像转到HSV上 150 | #---------------------------------# 151 | hue, sat, val = cv2.split(cv2.cvtColor(image_data, cv2.COLOR_RGB2HSV)) 152 | dtype = image_data.dtype 153 | #---------------------------------# 154 | # 应用变换 155 | #---------------------------------# 156 | x = np.arange(0, 256, dtype=r.dtype) 157 | lut_hue = ((x * r[0]) % 180).astype(dtype) 158 | lut_sat = np.clip(x * r[1], 0, 255).astype(dtype) 159 | lut_val = np.clip(x * r[2], 0, 255).astype(dtype) 160 | 161 | image_data = cv2.merge((cv2.LUT(hue, lut_hue), cv2.LUT(sat, lut_sat), cv2.LUT(val, lut_val))) 162 | image_data = cv2.cvtColor(image_data, cv2.COLOR_HSV2RGB) 163 | 164 | #---------------------------------# 165 | # 对真实框进行调整 166 | #---------------------------------# 167 | if len(box)>0: 168 | np.random.shuffle(box) 169 | box[:, [0,2]] = box[:, [0,2]]*nw/iw + dx 170 | box[:, [1,3]] = box[:, [1,3]]*nh/ih + dy 171 | if flip: box[:, [0,2]] = w - box[:, [2,0]] 172 | box[:, 0:2][box[:, 0:2]<0] = 0 173 | box[:, 2][box[:, 2]>w] = w 174 | box[:, 3][box[:, 3]>h] = h 175 | box_w = box[:, 2] - box[:, 0] 176 | box_h = box[:, 3] - box[:, 1] 177 | box = box[np.logical_and(box_w>1, box_h>1)] 178 | 179 | return image_data, box 180 | 181 | def merge_bboxes(self, bboxes, cutx, cuty): 182 | merge_bbox = [] 183 | for i in range(len(bboxes)): 184 | for box in bboxes[i]: 185 | tmp_box = [] 186 | x1, y1, x2, y2 = box[0], box[1], box[2], box[3] 187 | 188 | if i == 0: 189 | if y1 > cuty or x1 > cutx: 190 | continue 191 | if y2 >= cuty and y1 <= cuty: 192 | y2 = cuty 193 | if x2 >= cutx and x1 <= cutx: 194 | x2 = cutx 195 | 196 | if i == 1: 197 | if y2 < cuty or x1 > cutx: 198 | continue 199 | if y2 >= cuty and y1 <= cuty: 200 | y1 = cuty 201 | if x2 >= cutx and x1 <= cutx: 202 | x2 = cutx 203 | 204 | if i == 2: 205 | if y2 < cuty or x2 < cutx: 206 | continue 207 | if y2 >= cuty and y1 <= cuty: 208 | y1 = cuty 209 | if x2 >= cutx and x1 <= cutx: 210 | x1 = cutx 211 | 212 | if i == 3: 213 | if y1 > cuty or x2 < cutx: 214 | continue 215 | if y2 >= cuty and y1 <= cuty: 216 | y2 = cuty 217 | if x2 >= cutx and x1 <= cutx: 218 | x1 = cutx 219 | tmp_box.append(x1) 220 | tmp_box.append(y1) 221 | tmp_box.append(x2) 222 | tmp_box.append(y2) 223 | tmp_box.append(box[-1]) 224 | merge_bbox.append(tmp_box) 225 | return merge_bbox 226 | 227 | def get_random_data_with_Mosaic(self, annotation_line, input_shape, jitter=0.3, hue=.1, sat=0.7, val=0.4): 228 | h, w = input_shape 229 | min_offset_x = self.rand(0.3, 0.7) 230 | min_offset_y = self.rand(0.3, 0.7) 231 | 232 | image_datas = [] 233 | box_datas = [] 234 | index = 0 235 | for line in annotation_line: 236 | #---------------------------------# 237 | # 每一行进行分割 238 | #---------------------------------# 239 | line_content = line.split() 240 | #---------------------------------# 241 | # 打开图片 242 | #---------------------------------# 243 | image = Image.open(line_content[0]) 244 | image = cvtColor(image) 245 | 246 | #---------------------------------# 247 | # 图片的大小 248 | #---------------------------------# 249 | iw, ih = image.size 250 | #---------------------------------# 251 | # 保存框的位置 252 | #---------------------------------# 253 | box = np.array([np.array(list(map(int,box.split(',')))) for box in line_content[1:]]) 254 | 255 | #---------------------------------# 256 | # 是否翻转图片 257 | #---------------------------------# 258 | flip = self.rand()<.5 259 | if flip and len(box)>0: 260 | image = image.transpose(Image.FLIP_LEFT_RIGHT) 261 | box[:, [0,2]] = iw - box[:, [2,0]] 262 | 263 | #------------------------------------------# 264 | # 对图像进行缩放并且进行长和宽的扭曲 265 | #------------------------------------------# 266 | new_ar = iw/ih * self.rand(1-jitter,1+jitter) / self.rand(1-jitter,1+jitter) 267 | scale = self.rand(.4, 1) 268 | if new_ar < 1: 269 | nh = int(scale*h) 270 | nw = int(nh*new_ar) 271 | else: 272 | nw = int(scale*w) 273 | nh = int(nw/new_ar) 274 | image = image.resize((nw, nh), Image.BICUBIC) 275 | 276 | #-----------------------------------------------# 277 | # 将图片进行放置,分别对应四张分割图片的位置 278 | #-----------------------------------------------# 279 | if index == 0: 280 | dx = int(w*min_offset_x) - nw 281 | dy = int(h*min_offset_y) - nh 282 | elif index == 1: 283 | dx = int(w*min_offset_x) - nw 284 | dy = int(h*min_offset_y) 285 | elif index == 2: 286 | dx = int(w*min_offset_x) 287 | dy = int(h*min_offset_y) 288 | elif index == 3: 289 | dx = int(w*min_offset_x) 290 | dy = int(h*min_offset_y) - nh 291 | 292 | new_image = Image.new('RGB', (w,h), (128,128,128)) 293 | new_image.paste(image, (dx, dy)) 294 | image_data = np.array(new_image) 295 | 296 | index = index + 1 297 | box_data = [] 298 | #---------------------------------# 299 | # 对box进行重新处理 300 | #---------------------------------# 301 | if len(box)>0: 302 | np.random.shuffle(box) 303 | box[:, [0,2]] = box[:, [0,2]]*nw/iw + dx 304 | box[:, [1,3]] = box[:, [1,3]]*nh/ih + dy 305 | box[:, 0:2][box[:, 0:2]<0] = 0 306 | box[:, 2][box[:, 2]>w] = w 307 | box[:, 3][box[:, 3]>h] = h 308 | box_w = box[:, 2] - box[:, 0] 309 | box_h = box[:, 3] - box[:, 1] 310 | box = box[np.logical_and(box_w>1, box_h>1)] 311 | box_data = np.zeros((len(box),5)) 312 | box_data[:len(box)] = box 313 | 314 | image_datas.append(image_data) 315 | box_datas.append(box_data) 316 | 317 | #---------------------------------# 318 | # 将图片分割,放在一起 319 | #---------------------------------# 320 | cutx = int(w * min_offset_x) 321 | cuty = int(h * min_offset_y) 322 | 323 | new_image = np.zeros([h, w, 3]) 324 | new_image[:cuty, :cutx, :] = image_datas[0][:cuty, :cutx, :] 325 | new_image[cuty:, :cutx, :] = image_datas[1][cuty:, :cutx, :] 326 | new_image[cuty:, cutx:, :] = image_datas[2][cuty:, cutx:, :] 327 | new_image[:cuty, cutx:, :] = image_datas[3][:cuty, cutx:, :] 328 | 329 | new_image = np.array(new_image, np.uint8) 330 | #---------------------------------# 331 | # 对图像进行色域变换 332 | # 计算色域变换的参数 333 | #---------------------------------# 334 | r = np.random.uniform(-1, 1, 3) * [hue, sat, val] + 1 335 | #---------------------------------# 336 | # 将图像转到HSV上 337 | #---------------------------------# 338 | hue, sat, val = cv2.split(cv2.cvtColor(new_image, cv2.COLOR_RGB2HSV)) 339 | dtype = new_image.dtype 340 | #---------------------------------# 341 | # 应用变换 342 | #---------------------------------# 343 | x = np.arange(0, 256, dtype=r.dtype) 344 | lut_hue = ((x * r[0]) % 180).astype(dtype) 345 | lut_sat = np.clip(x * r[1], 0, 255).astype(dtype) 346 | lut_val = np.clip(x * r[2], 0, 255).astype(dtype) 347 | 348 | new_image = cv2.merge((cv2.LUT(hue, lut_hue), cv2.LUT(sat, lut_sat), cv2.LUT(val, lut_val))) 349 | new_image = cv2.cvtColor(new_image, cv2.COLOR_HSV2RGB) 350 | 351 | #---------------------------------# 352 | # 对框进行进一步的处理 353 | #---------------------------------# 354 | new_boxes = self.merge_bboxes(box_datas, cutx, cuty) 355 | 356 | return new_image, new_boxes 357 | 358 | def get_random_data_with_MixUp(self, image_1, box_1, image_2, box_2): 359 | new_image = np.array(image_1, np.float32) * 0.5 + np.array(image_2, np.float32) * 0.5 360 | if len(box_1) == 0: 361 | new_boxes = box_2 362 | elif len(box_2) == 0: 363 | new_boxes = box_1 364 | else: 365 | new_boxes = np.concatenate([box_1, box_2], axis=0) 366 | return new_image, new_boxes 367 | 368 | # DataLoader中collate_fn使用 369 | def yolo_dataset_collate(batch): 370 | images = [] 371 | bboxes = [] 372 | for img, box in batch: 373 | images.append(img) 374 | bboxes.append(box) 375 | images = torch.from_numpy(np.array(images)).type(torch.FloatTensor) 376 | bboxes = [torch.from_numpy(ann).type(torch.FloatTensor) for ann in bboxes] 377 | return images, bboxes 378 | -------------------------------------------------------------------------------- /utils/utils.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | import numpy as np 4 | import torch 5 | from PIL import Image 6 | 7 | #---------------------------------------------------------# 8 | # 将图像转换成RGB图像,防止灰度图在预测时报错。 9 | # 代码仅仅支持RGB图像的预测,所有其它类型的图像都会转化成RGB 10 | #---------------------------------------------------------# 11 | def cvtColor(image): 12 | if len(np.shape(image)) == 3 and np.shape(image)[2] == 3: 13 | return image 14 | else: 15 | image = image.convert('RGB') 16 | return image 17 | 18 | #---------------------------------------------------# 19 | # 对输入图像进行resize 20 | #---------------------------------------------------# 21 | def resize_image(image, size, letterbox_image): 22 | iw, ih = image.size 23 | w, h = size 24 | if letterbox_image: 25 | scale = min(w/iw, h/ih) 26 | nw = int(iw*scale) 27 | nh = int(ih*scale) 28 | 29 | image = image.resize((nw,nh), Image.BICUBIC) 30 | new_image = Image.new('RGB', size, (128,128,128)) 31 | new_image.paste(image, ((w-nw)//2, (h-nh)//2)) 32 | else: 33 | new_image = image.resize((w, h), Image.BICUBIC) 34 | return new_image 35 | 36 | #---------------------------------------------------# 37 | # 获得类 38 | #---------------------------------------------------# 39 | def get_classes(classes_path): 40 | with open(classes_path, encoding='utf-8') as f: 41 | class_names = f.readlines() 42 | class_names = [c.strip() for c in class_names] 43 | return class_names, len(class_names) 44 | 45 | #---------------------------------------------------# 46 | # 获得先验框 47 | #---------------------------------------------------# 48 | def get_anchors(anchors_path): 49 | '''loads the anchors from a file''' 50 | with open(anchors_path, encoding='utf-8') as f: 51 | anchors = f.readline() 52 | anchors = [float(x) for x in anchors.split(',')] 53 | anchors = np.array(anchors).reshape(-1, 2) 54 | return anchors, len(anchors) 55 | 56 | #---------------------------------------------------# 57 | # 获得学习率 58 | #---------------------------------------------------# 59 | def get_lr(optimizer): 60 | for param_group in optimizer.param_groups: 61 | return param_group['lr'] 62 | 63 | #---------------------------------------------------# 64 | # 设置种子 65 | #---------------------------------------------------# 66 | def seed_everything(seed=11): 67 | random.seed(seed) 68 | np.random.seed(seed) 69 | torch.manual_seed(seed) 70 | torch.cuda.manual_seed(seed) 71 | torch.cuda.manual_seed_all(seed) 72 | torch.backends.cudnn.deterministic = True 73 | torch.backends.cudnn.benchmark = False 74 | 75 | #---------------------------------------------------# 76 | # 设置Dataloader的种子 77 | #---------------------------------------------------# 78 | def worker_init_fn(worker_id, rank, seed): 79 | worker_seed = rank + seed 80 | random.seed(worker_seed) 81 | np.random.seed(worker_seed) 82 | torch.manual_seed(worker_seed) 83 | 84 | def preprocess_input(image): 85 | image /= 255.0 86 | return image 87 | 88 | def show_config(**kwargs): 89 | print('Configurations:') 90 | print('-' * 70) 91 | print('|%25s | %40s|' % ('keys', 'values')) 92 | print('-' * 70) 93 | for key, value in kwargs.items(): 94 | print('|%25s | %40s|' % (str(key), str(value))) 95 | print('-' * 70) 96 | -------------------------------------------------------------------------------- /utils/utils_bbox.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | from torchvision.ops import nms 4 | import numpy as np 5 | 6 | class DecodeBox(): 7 | def __init__(self, anchors, num_classes, input_shape, anchors_mask = [[6,7,8], [3,4,5], [0,1,2]]): 8 | super(DecodeBox, self).__init__() 9 | self.anchors = anchors 10 | self.num_classes = num_classes 11 | self.bbox_attrs = 5 + num_classes 12 | self.input_shape = input_shape 13 | #-----------------------------------------------------------# 14 | # 13x13的特征层对应的anchor是[81,82],[135,169],[344,319] 15 | # 26x26的特征层对应的anchor是[10,14],[23,27],[37,58] 16 | #-----------------------------------------------------------# 17 | self.anchors_mask = anchors_mask 18 | 19 | def decode_box(self, inputs): 20 | outputs = [] 21 | for i, input in enumerate(inputs): 22 | #-----------------------------------------------# 23 | # 输入的input一共有三个,他们的shape分别是 24 | # batch_size, 255, 13, 13 25 | # batch_size, 255, 26, 26 26 | #-----------------------------------------------# 27 | batch_size = input.size(0) 28 | input_height = input.size(2) 29 | input_width = input.size(3) 30 | 31 | #-----------------------------------------------# 32 | # 输入为416x416时 33 | # stride_h = stride_w = 32、16、8 34 | #-----------------------------------------------# 35 | stride_h = self.input_shape[0] / input_height 36 | stride_w = self.input_shape[1] / input_width 37 | #-------------------------------------------------# 38 | # 此时获得的scaled_anchors大小是相对于特征层的 39 | #-------------------------------------------------# 40 | scaled_anchors = [(anchor_width / stride_w, anchor_height / stride_h) for anchor_width, anchor_height in self.anchors[self.anchors_mask[i]]] 41 | 42 | #-----------------------------------------------# 43 | # 输入的input一共有三个,他们的shape分别是 44 | # batch_size, 3, 13, 13, 85 45 | # batch_size, 3, 26, 26, 85 46 | #-----------------------------------------------# 47 | prediction = input.view(batch_size, len(self.anchors_mask[i]), 48 | self.bbox_attrs, input_height, input_width).permute(0, 1, 3, 4, 2).contiguous() 49 | 50 | #-----------------------------------------------# 51 | # 先验框的中心位置的调整参数 52 | #-----------------------------------------------# 53 | x = torch.sigmoid(prediction[..., 0]) 54 | y = torch.sigmoid(prediction[..., 1]) 55 | #-----------------------------------------------# 56 | # 先验框的宽高调整参数 57 | #-----------------------------------------------# 58 | w = prediction[..., 2] 59 | h = prediction[..., 3] 60 | #-----------------------------------------------# 61 | # 获得置信度,是否有物体 62 | #-----------------------------------------------# 63 | conf = torch.sigmoid(prediction[..., 4]) 64 | #-----------------------------------------------# 65 | # 种类置信度 66 | #-----------------------------------------------# 67 | pred_cls = torch.sigmoid(prediction[..., 5:]) 68 | 69 | FloatTensor = torch.cuda.FloatTensor if x.is_cuda else torch.FloatTensor 70 | LongTensor = torch.cuda.LongTensor if x.is_cuda else torch.LongTensor 71 | 72 | #----------------------------------------------------------# 73 | # 生成网格,先验框中心,网格左上角 74 | # batch_size,3,13,13 75 | #----------------------------------------------------------# 76 | grid_x = torch.linspace(0, input_width - 1, input_width).repeat(input_height, 1).repeat( 77 | batch_size * len(self.anchors_mask[i]), 1, 1).view(x.shape).type(FloatTensor) 78 | grid_y = torch.linspace(0, input_height - 1, input_height).repeat(input_width, 1).t().repeat( 79 | batch_size * len(self.anchors_mask[i]), 1, 1).view(y.shape).type(FloatTensor) 80 | 81 | #----------------------------------------------------------# 82 | # 按照网格格式生成先验框的宽高 83 | # batch_size,3,13,13 84 | #----------------------------------------------------------# 85 | anchor_w = FloatTensor(scaled_anchors).index_select(1, LongTensor([0])) 86 | anchor_h = FloatTensor(scaled_anchors).index_select(1, LongTensor([1])) 87 | anchor_w = anchor_w.repeat(batch_size, 1).repeat(1, 1, input_height * input_width).view(w.shape) 88 | anchor_h = anchor_h.repeat(batch_size, 1).repeat(1, 1, input_height * input_width).view(h.shape) 89 | 90 | #----------------------------------------------------------# 91 | # 利用预测结果对先验框进行调整 92 | # 首先调整先验框的中心,从先验框中心向右下角偏移 93 | # 再调整先验框的宽高。 94 | #----------------------------------------------------------# 95 | pred_boxes = FloatTensor(prediction[..., :4].shape) 96 | pred_boxes[..., 0] = x.data + grid_x 97 | pred_boxes[..., 1] = y.data + grid_y 98 | pred_boxes[..., 2] = torch.exp(w.data) * anchor_w 99 | pred_boxes[..., 3] = torch.exp(h.data) * anchor_h 100 | 101 | #----------------------------------------------------------# 102 | # 将输出结果归一化成小数的形式 103 | #----------------------------------------------------------# 104 | _scale = torch.Tensor([input_width, input_height, input_width, input_height]).type(FloatTensor) 105 | output = torch.cat((pred_boxes.view(batch_size, -1, 4) / _scale, 106 | conf.view(batch_size, -1, 1), pred_cls.view(batch_size, -1, self.num_classes)), -1) 107 | outputs.append(output.data) 108 | return outputs 109 | 110 | def yolo_correct_boxes(self, box_xy, box_wh, input_shape, image_shape, letterbox_image): 111 | #-----------------------------------------------------------------# 112 | # 把y轴放前面是因为方便预测框和图像的宽高进行相乘 113 | #-----------------------------------------------------------------# 114 | box_yx = box_xy[..., ::-1] 115 | box_hw = box_wh[..., ::-1] 116 | input_shape = np.array(input_shape) 117 | image_shape = np.array(image_shape) 118 | 119 | if letterbox_image: 120 | #-----------------------------------------------------------------# 121 | # 这里求出来的offset是图像有效区域相对于图像左上角的偏移情况 122 | # new_shape指的是宽高缩放情况 123 | #-----------------------------------------------------------------# 124 | new_shape = np.round(image_shape * np.min(input_shape/image_shape)) 125 | offset = (input_shape - new_shape)/2./input_shape 126 | scale = input_shape/new_shape 127 | 128 | box_yx = (box_yx - offset) * scale 129 | box_hw *= scale 130 | 131 | box_mins = box_yx - (box_hw / 2.) 132 | box_maxes = box_yx + (box_hw / 2.) 133 | boxes = np.concatenate([box_mins[..., 0:1], box_mins[..., 1:2], box_maxes[..., 0:1], box_maxes[..., 1:2]], axis=-1) 134 | boxes *= np.concatenate([image_shape, image_shape], axis=-1) 135 | return boxes 136 | 137 | def non_max_suppression(self, prediction, num_classes, input_shape, image_shape, letterbox_image, conf_thres=0.5, nms_thres=0.4): 138 | #----------------------------------------------------------# 139 | # 将预测结果的格式转换成左上角右下角的格式。 140 | # prediction [batch_size, num_anchors, 85] 141 | #----------------------------------------------------------# 142 | box_corner = prediction.new(prediction.shape) 143 | box_corner[:, :, 0] = prediction[:, :, 0] - prediction[:, :, 2] / 2 144 | box_corner[:, :, 1] = prediction[:, :, 1] - prediction[:, :, 3] / 2 145 | box_corner[:, :, 2] = prediction[:, :, 0] + prediction[:, :, 2] / 2 146 | box_corner[:, :, 3] = prediction[:, :, 1] + prediction[:, :, 3] / 2 147 | prediction[:, :, :4] = box_corner[:, :, :4] 148 | 149 | output = [None for _ in range(len(prediction))] 150 | for i, image_pred in enumerate(prediction): 151 | #----------------------------------------------------------# 152 | # 对种类预测部分取max。 153 | # class_conf [num_anchors, 1] 种类置信度 154 | # class_pred [num_anchors, 1] 种类 155 | #----------------------------------------------------------# 156 | class_conf, class_pred = torch.max(image_pred[:, 5:5 + num_classes], 1, keepdim=True) 157 | 158 | #----------------------------------------------------------# 159 | # 利用置信度进行第一轮筛选 160 | #----------------------------------------------------------# 161 | conf_mask = (image_pred[:, 4] * class_conf[:, 0] >= conf_thres).squeeze() 162 | 163 | #----------------------------------------------------------# 164 | # 根据置信度进行预测结果的筛选 165 | #----------------------------------------------------------# 166 | image_pred = image_pred[conf_mask] 167 | class_conf = class_conf[conf_mask] 168 | class_pred = class_pred[conf_mask] 169 | if not image_pred.size(0): 170 | continue 171 | #-------------------------------------------------------------------------# 172 | # detections [num_anchors, 7] 173 | # 7的内容为:x1, y1, x2, y2, obj_conf, class_conf, class_pred 174 | #-------------------------------------------------------------------------# 175 | detections = torch.cat((image_pred[:, :5], class_conf.float(), class_pred.float()), 1) 176 | 177 | #------------------------------------------# 178 | # 获得预测结果中包含的所有种类 179 | #------------------------------------------# 180 | unique_labels = detections[:, -1].cpu().unique() 181 | 182 | if prediction.is_cuda: 183 | unique_labels = unique_labels.cuda() 184 | detections = detections.cuda() 185 | 186 | for c in unique_labels: 187 | #------------------------------------------# 188 | # 获得某一类得分筛选后全部的预测结果 189 | #------------------------------------------# 190 | detections_class = detections[detections[:, -1] == c] 191 | 192 | #------------------------------------------# 193 | # 使用官方自带的非极大抑制会速度更快一些! 194 | #------------------------------------------# 195 | keep = nms( 196 | detections_class[:, :4], 197 | detections_class[:, 4] * detections_class[:, 5], 198 | nms_thres 199 | ) 200 | max_detections = detections_class[keep] 201 | 202 | # # 按照存在物体的置信度排序 203 | # _, conf_sort_index = torch.sort(detections_class[:, 4]*detections_class[:, 5], descending=True) 204 | # detections_class = detections_class[conf_sort_index] 205 | # # 进行非极大抑制 206 | # max_detections = [] 207 | # while detections_class.size(0): 208 | # # 取出这一类置信度最高的,一步一步往下判断,判断重合程度是否大于nms_thres,如果是则去除掉 209 | # max_detections.append(detections_class[0].unsqueeze(0)) 210 | # if len(detections_class) == 1: 211 | # break 212 | # ious = bbox_iou(max_detections[-1], detections_class[1:]) 213 | # detections_class = detections_class[1:][ious < nms_thres] 214 | # # 堆叠 215 | # max_detections = torch.cat(max_detections).data 216 | 217 | # Add max detections to outputs 218 | output[i] = max_detections if output[i] is None else torch.cat((output[i], max_detections)) 219 | 220 | if output[i] is not None: 221 | output[i] = output[i].cpu().numpy() 222 | box_xy, box_wh = (output[i][:, 0:2] + output[i][:, 2:4])/2, output[i][:, 2:4] - output[i][:, 0:2] 223 | output[i][:, :4] = self.yolo_correct_boxes(box_xy, box_wh, input_shape, image_shape, letterbox_image) 224 | return output 225 | 226 | class DecodeBoxNP(): 227 | def __init__(self, anchors, num_classes, input_shape, anchors_mask = [[6,7,8], [3,4,5], [0,1,2]]): 228 | super(DecodeBoxNP, self).__init__() 229 | self.anchors = anchors 230 | self.num_classes = num_classes 231 | self.bbox_attrs = 5 + num_classes 232 | self.input_shape = input_shape 233 | self.anchors_mask = anchors_mask 234 | 235 | def sigmoid(self, x): 236 | return 1 / (1 + np.exp(-x)) 237 | 238 | def decode_box(self, inputs): 239 | outputs = [] 240 | for i, input in enumerate(inputs): 241 | batch_size = np.shape(input)[0] 242 | input_height = np.shape(input)[2] 243 | input_width = np.shape(input)[3] 244 | 245 | #-----------------------------------------------# 246 | # 输入为640x640时 247 | # stride_h = stride_w = 32、16、8 248 | #-----------------------------------------------# 249 | stride_h = self.input_shape[0] / input_height 250 | stride_w = self.input_shape[1] / input_width 251 | #-------------------------------------------------# 252 | # 此时获得的scaled_anchors大小是相对于特征层的 253 | #-------------------------------------------------# 254 | scaled_anchors = [(anchor_width / stride_w, anchor_height / stride_h) for anchor_width, anchor_height in self.anchors[self.anchors_mask[i]]] 255 | 256 | #-----------------------------------------------# 257 | # 输入的input一共有三个,他们的shape分别是 258 | # batch_size, 3, 20, 20, 85 259 | # batch_size, 3, 40, 40, 85 260 | # batch_size, 3, 80, 80, 85 261 | #-----------------------------------------------# 262 | prediction = np.transpose(np.reshape(input, (batch_size, len(self.anchors_mask[i]), self.bbox_attrs, input_height, input_width)), (0, 1, 3, 4, 2)) 263 | 264 | #-----------------------------------------------# 265 | # 先验框的中心位置的调整参数 266 | #-----------------------------------------------# 267 | x = self.sigmoid(prediction[..., 0]) 268 | y = self.sigmoid(prediction[..., 1]) 269 | #-----------------------------------------------# 270 | # 先验框的宽高调整参数 271 | #-----------------------------------------------# 272 | w = prediction[..., 2] 273 | h = prediction[..., 3] 274 | #-----------------------------------------------# 275 | # 获得置信度,是否有物体 276 | #-----------------------------------------------# 277 | conf = self.sigmoid(prediction[..., 4]) 278 | #-----------------------------------------------# 279 | # 种类置信度 280 | #-----------------------------------------------# 281 | pred_cls = self.sigmoid(prediction[..., 5:]) 282 | 283 | #----------------------------------------------------------# 284 | # 生成网格,先验框中心,网格左上角 285 | # batch_size,3,20,20 286 | #----------------------------------------------------------# 287 | grid_x = np.repeat(np.expand_dims(np.repeat(np.expand_dims(np.linspace(0, input_width - 1, input_width), 0), input_height, axis=0), 0), batch_size * len(self.anchors_mask[i]), axis=0) 288 | grid_x = np.reshape(grid_x, np.shape(x)) 289 | grid_y = np.repeat(np.expand_dims(np.repeat(np.expand_dims(np.linspace(0, input_height - 1, input_height), 0), input_width, axis=0).T, 0), batch_size * len(self.anchors_mask[i]), axis=0) 290 | grid_y = np.reshape(grid_y, np.shape(y)) 291 | 292 | #----------------------------------------------------------# 293 | # 按照网格格式生成先验框的宽高 294 | # batch_size,3,20,20 295 | #----------------------------------------------------------# 296 | anchor_w = np.repeat(np.expand_dims(np.repeat(np.expand_dims(np.array(scaled_anchors)[:, 0], 0), batch_size, axis=0), -1), input_height * input_width, axis=-1) 297 | anchor_h = np.repeat(np.expand_dims(np.repeat(np.expand_dims(np.array(scaled_anchors)[:, 1], 0), batch_size, axis=0), -1), input_height * input_width, axis=-1) 298 | anchor_w = np.reshape(anchor_w, np.shape(w)) 299 | anchor_h = np.reshape(anchor_h, np.shape(h)) 300 | #----------------------------------------------------------# 301 | # 利用预测结果对先验框进行调整 302 | # 首先调整先验框的中心,从先验框中心向右下角偏移 303 | # 再调整先验框的宽高。 304 | # x 0 ~ 1 => 0 ~ 2 => -0.5, 1.5 => 负责一定范围的目标的预测 305 | # y 0 ~ 1 => 0 ~ 2 => -0.5, 1.5 => 负责一定范围的目标的预测 306 | # w 0 ~ 1 => 0 ~ 2 => 0 ~ 4 => 先验框的宽高调节范围为0~4倍 307 | # h 0 ~ 1 => 0 ~ 2 => 0 ~ 4 => 先验框的宽高调节范围为0~4倍 308 | #----------------------------------------------------------# 309 | pred_boxes = np.zeros(np.shape(prediction[..., :4])) 310 | pred_boxes[..., 0] = x + grid_x 311 | pred_boxes[..., 1] = y + grid_y 312 | pred_boxes[..., 2] = np.exp(w) * anchor_w 313 | pred_boxes[..., 3] = np.exp(h) * anchor_h 314 | 315 | #----------------------------------------------------------# 316 | # 将输出结果归一化成小数的形式 317 | #----------------------------------------------------------# 318 | _scale = np.array([input_width, input_height, input_width, input_height]) 319 | output = np.concatenate([np.reshape(pred_boxes, (batch_size, -1, 4)) / _scale, 320 | np.reshape(conf, (batch_size, -1, 1)), np.reshape(pred_cls, (batch_size, -1, self.num_classes))], -1) 321 | outputs.append(output) 322 | return outputs 323 | 324 | def bbox_iou(self, box1, box2, x1y1x2y2=True): 325 | """ 326 | 计算IOU 327 | """ 328 | if not x1y1x2y2: 329 | b1_x1, b1_x2 = box1[:, 0] - box1[:, 2] / 2, box1[:, 0] + box1[:, 2] / 2 330 | b1_y1, b1_y2 = box1[:, 1] - box1[:, 3] / 2, box1[:, 1] + box1[:, 3] / 2 331 | b2_x1, b2_x2 = box2[:, 0] - box2[:, 2] / 2, box2[:, 0] + box2[:, 2] / 2 332 | b2_y1, b2_y2 = box2[:, 1] - box2[:, 3] / 2, box2[:, 1] + box2[:, 3] / 2 333 | else: 334 | b1_x1, b1_y1, b1_x2, b1_y2 = box1[:, 0], box1[:, 1], box1[:, 2], box1[:, 3] 335 | b2_x1, b2_y1, b2_x2, b2_y2 = box2[:, 0], box2[:, 1], box2[:, 2], box2[:, 3] 336 | 337 | inter_rect_x1 = np.maximum(b1_x1, b2_x1) 338 | inter_rect_y1 = np.maximum(b1_y1, b2_y1) 339 | inter_rect_x2 = np.minimum(b1_x2, b2_x2) 340 | inter_rect_y2 = np.minimum(b1_y2, b2_y2) 341 | 342 | inter_area = np.maximum(inter_rect_x2 - inter_rect_x1, 0) * \ 343 | np.maximum(inter_rect_y2 - inter_rect_y1, 0) 344 | 345 | b1_area = (b1_x2 - b1_x1) * (b1_y2 - b1_y1) 346 | b2_area = (b2_x2 - b2_x1) * (b2_y2 - b2_y1) 347 | 348 | iou = inter_area / np.maximum(b1_area + b2_area - inter_area, 1e-6) 349 | 350 | return iou 351 | 352 | def yolo_correct_boxes(self, box_xy, box_wh, input_shape, image_shape, letterbox_image): 353 | #-----------------------------------------------------------------# 354 | # 把y轴放前面是因为方便预测框和图像的宽高进行相乘 355 | #-----------------------------------------------------------------# 356 | box_yx = box_xy[..., ::-1] 357 | box_hw = box_wh[..., ::-1] 358 | input_shape = np.array(input_shape) 359 | image_shape = np.array(image_shape) 360 | 361 | if letterbox_image: 362 | #-----------------------------------------------------------------# 363 | # 这里求出来的offset是图像有效区域相对于图像左上角的偏移情况 364 | # new_shape指的是宽高缩放情况 365 | #-----------------------------------------------------------------# 366 | new_shape = np.round(image_shape * np.min(input_shape/image_shape)) 367 | offset = (input_shape - new_shape)/2./input_shape 368 | scale = input_shape/new_shape 369 | 370 | box_yx = (box_yx - offset) * scale 371 | box_hw *= scale 372 | 373 | box_mins = box_yx - (box_hw / 2.) 374 | box_maxes = box_yx + (box_hw / 2.) 375 | boxes = np.concatenate([box_mins[..., 0:1], box_mins[..., 1:2], box_maxes[..., 0:1], box_maxes[..., 1:2]], axis=-1) 376 | boxes *= np.concatenate([image_shape, image_shape], axis=-1) 377 | return boxes 378 | 379 | def non_max_suppression(self, prediction, num_classes, input_shape, image_shape, letterbox_image, conf_thres=0.5, nms_thres=0.4): 380 | #----------------------------------------------------------# 381 | # 将预测结果的格式转换成左上角右下角的格式。 382 | # prediction [batch_size, num_anchors, 85] 383 | #----------------------------------------------------------# 384 | box_corner = np.zeros_like(prediction) 385 | box_corner[:, :, 0] = prediction[:, :, 0] - prediction[:, :, 2] / 2 386 | box_corner[:, :, 1] = prediction[:, :, 1] - prediction[:, :, 3] / 2 387 | box_corner[:, :, 2] = prediction[:, :, 0] + prediction[:, :, 2] / 2 388 | box_corner[:, :, 3] = prediction[:, :, 1] + prediction[:, :, 3] / 2 389 | prediction[:, :, :4] = box_corner[:, :, :4] 390 | 391 | output = [None for _ in range(len(prediction))] 392 | for i, image_pred in enumerate(prediction): 393 | #----------------------------------------------------------# 394 | # 对种类预测部分取max。 395 | # class_conf [num_anchors, 1] 种类置信度 396 | # class_pred [num_anchors, 1] 种类 397 | #----------------------------------------------------------# 398 | class_conf = np.max(image_pred[:, 5:5 + num_classes], 1, keepdims=True) 399 | class_pred = np.expand_dims(np.argmax(image_pred[:, 5:5 + num_classes], 1), -1) 400 | 401 | #----------------------------------------------------------# 402 | # 利用置信度进行第一轮筛选 403 | #----------------------------------------------------------# 404 | conf_mask = np.squeeze((image_pred[:, 4] * class_conf[:, 0] >= conf_thres)) 405 | 406 | #----------------------------------------------------------# 407 | # 根据置信度进行预测结果的筛选 408 | #----------------------------------------------------------# 409 | image_pred = image_pred[conf_mask] 410 | class_conf = class_conf[conf_mask] 411 | class_pred = class_pred[conf_mask] 412 | if not np.shape(image_pred)[0]: 413 | continue 414 | #-------------------------------------------------------------------------# 415 | # detections [num_anchors, 7] 416 | # 7的内容为:x1, y1, x2, y2, obj_conf, class_conf, class_pred 417 | #-------------------------------------------------------------------------# 418 | detections = np.concatenate((image_pred[:, :5], class_conf, class_pred), 1) 419 | 420 | #------------------------------------------# 421 | # 获得预测结果中包含的所有种类 422 | #------------------------------------------# 423 | unique_labels = np.unique(detections[:, -1]) 424 | 425 | for c in unique_labels: 426 | #------------------------------------------# 427 | # 获得某一类得分筛选后全部的预测结果 428 | #------------------------------------------# 429 | detections_class = detections[detections[:, -1] == c] 430 | 431 | # 按照存在物体的置信度排序 432 | conf_sort_index = np.argsort(detections_class[:, 4] * detections_class[:, 5])[::-1] 433 | detections_class = detections_class[conf_sort_index] 434 | # 进行非极大抑制 435 | max_detections = [] 436 | while np.shape(detections_class)[0]: 437 | # 取出这一类置信度最高的,一步一步往下判断,判断重合程度是否大于nms_thres,如果是则去除掉 438 | max_detections.append(detections_class[0:1]) 439 | if len(detections_class) == 1: 440 | break 441 | ious = self.bbox_iou(max_detections[-1], detections_class[1:]) 442 | detections_class = detections_class[1:][ious < nms_thres] 443 | # 堆叠 444 | max_detections = np.concatenate(max_detections, 0) 445 | 446 | # Add max detections to outputs 447 | output[i] = max_detections if output[i] is None else np.concatenate((output[i], max_detections)) 448 | 449 | if output[i] is not None: 450 | output[i] = output[i] 451 | box_xy, box_wh = (output[i][:, 0:2] + output[i][:, 2:4])/2, output[i][:, 2:4] - output[i][:, 0:2] 452 | output[i][:, :4] = self.yolo_correct_boxes(box_xy, box_wh, input_shape, image_shape, letterbox_image) 453 | return output 454 | -------------------------------------------------------------------------------- /utils/utils_fit.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import torch 4 | from tqdm import tqdm 5 | 6 | from utils.utils import get_lr 7 | 8 | 9 | def fit_one_epoch(model_train, model, yolo_loss, loss_history, eval_callback, optimizer, epoch, epoch_step, epoch_step_val, gen, gen_val, Epoch, cuda, fp16, scaler, save_period, save_dir, local_rank=0): 10 | loss = 0 11 | val_loss = 0 12 | 13 | if local_rank == 0: 14 | print('Start Train') 15 | pbar = tqdm(total=epoch_step,desc=f'Epoch {epoch + 1}/{Epoch}',postfix=dict,mininterval=0.3) 16 | model_train.train() 17 | for iteration, batch in enumerate(gen): 18 | if iteration >= epoch_step: 19 | break 20 | 21 | images, targets = batch[0], batch[1] 22 | with torch.no_grad(): 23 | if cuda: 24 | images = images.cuda(local_rank) 25 | targets = [ann.cuda(local_rank) for ann in targets] 26 | #----------------------# 27 | # 清零梯度 28 | #----------------------# 29 | optimizer.zero_grad() 30 | if not fp16: 31 | #----------------------# 32 | # 前向传播 33 | #----------------------# 34 | outputs = model_train(images) 35 | 36 | loss_value_all = 0 37 | #----------------------# 38 | # 计算损失 39 | #----------------------# 40 | for l in range(len(outputs)): 41 | loss_item = yolo_loss(l, outputs[l], targets) 42 | loss_value_all += loss_item 43 | loss_value = loss_value_all 44 | 45 | #----------------------# 46 | # 反向传播 47 | #----------------------# 48 | loss_value.backward() 49 | optimizer.step() 50 | else: 51 | from torch.cuda.amp import autocast 52 | with autocast(): 53 | #----------------------# 54 | # 前向传播 55 | #----------------------# 56 | outputs = model_train(images) 57 | 58 | loss_value_all = 0 59 | #----------------------# 60 | # 计算损失 61 | #----------------------# 62 | for l in range(len(outputs)): 63 | loss_item = yolo_loss(l, outputs[l], targets) 64 | loss_value_all += loss_item 65 | loss_value = loss_value_all 66 | 67 | #----------------------# 68 | # 反向传播 69 | #----------------------# 70 | scaler.scale(loss_value).backward() 71 | scaler.step(optimizer) 72 | scaler.update() 73 | 74 | loss += loss_value.item() 75 | 76 | if local_rank == 0: 77 | pbar.set_postfix(**{'loss' : loss / (iteration + 1), 78 | 'lr' : get_lr(optimizer)}) 79 | pbar.update(1) 80 | 81 | if local_rank == 0: 82 | pbar.close() 83 | print('Finish Train') 84 | print('Start Validation') 85 | pbar = tqdm(total=epoch_step_val, desc=f'Epoch {epoch + 1}/{Epoch}',postfix=dict,mininterval=0.3) 86 | 87 | model_train.eval() 88 | for iteration, batch in enumerate(gen_val): 89 | if iteration >= epoch_step_val: 90 | break 91 | images, targets = batch[0], batch[1] 92 | with torch.no_grad(): 93 | if cuda: 94 | images = images.cuda(local_rank) 95 | targets = [ann.cuda(local_rank) for ann in targets] 96 | #----------------------# 97 | # 清零梯度 98 | #----------------------# 99 | optimizer.zero_grad() 100 | #----------------------# 101 | # 前向传播 102 | #----------------------# 103 | outputs = model_train(images) 104 | 105 | loss_value_all = 0 106 | #----------------------# 107 | # 计算损失 108 | #----------------------# 109 | for l in range(len(outputs)): 110 | loss_item = yolo_loss(l, outputs[l], targets) 111 | loss_value_all += loss_item 112 | loss_value = loss_value_all 113 | 114 | val_loss += loss_value.item() 115 | if local_rank == 0: 116 | pbar.set_postfix(**{'val_loss': val_loss / (iteration + 1)}) 117 | pbar.update(1) 118 | 119 | if local_rank == 0: 120 | pbar.close() 121 | print('Finish Validation') 122 | loss_history.append_loss(epoch + 1, loss / epoch_step, val_loss / epoch_step_val) 123 | eval_callback.on_epoch_end(epoch + 1, model_train) 124 | print('Epoch:'+ str(epoch + 1) + '/' + str(Epoch)) 125 | print('Total Loss: %.3f || Val Loss: %.3f ' % (loss / epoch_step, val_loss / epoch_step_val)) 126 | 127 | #-----------------------------------------------# 128 | # 保存权值 129 | #-----------------------------------------------# 130 | if (epoch + 1) % save_period == 0 or epoch + 1 == Epoch: 131 | torch.save(model.state_dict(), os.path.join(save_dir, "ep%03d-loss%.3f-val_loss%.3f.pth" % (epoch + 1, loss / epoch_step, val_loss / epoch_step_val))) 132 | 133 | if len(loss_history.val_loss) <= 1 or (val_loss / epoch_step_val) <= min(loss_history.val_loss): 134 | print('Save best model to best_epoch_weights.pth') 135 | torch.save(model.state_dict(), os.path.join(save_dir, "best_epoch_weights.pth")) 136 | 137 | torch.save(model.state_dict(), os.path.join(save_dir, "last_epoch_weights.pth")) -------------------------------------------------------------------------------- /utils_coco/coco_annotation.py: -------------------------------------------------------------------------------- 1 | #-------------------------------------------------------# 2 | # 用于处理COCO数据集,根据json文件生成txt文件用于训练 3 | #-------------------------------------------------------# 4 | import json 5 | import os 6 | from collections import defaultdict 7 | 8 | #-------------------------------------------------------# 9 | # 指向了COCO训练集与验证集图片的路径 10 | #-------------------------------------------------------# 11 | train_datasets_path = "coco_dataset/train2017" 12 | val_datasets_path = "coco_dataset/val2017" 13 | 14 | #-------------------------------------------------------# 15 | # 指向了COCO训练集与验证集标签的路径 16 | #-------------------------------------------------------# 17 | train_annotation_path = "coco_dataset/annotations/instances_train2017.json" 18 | val_annotation_path = "coco_dataset/annotations/instances_val2017.json" 19 | 20 | #-------------------------------------------------------# 21 | # 生成的txt文件路径 22 | #-------------------------------------------------------# 23 | train_output_path = "coco_train.txt" 24 | val_output_path = "coco_val.txt" 25 | 26 | if __name__ == "__main__": 27 | name_box_id = defaultdict(list) 28 | id_name = dict() 29 | f = open(train_annotation_path, encoding='utf-8') 30 | data = json.load(f) 31 | 32 | annotations = data['annotations'] 33 | for ant in annotations: 34 | id = ant['image_id'] 35 | name = os.path.join(train_datasets_path, '%012d.jpg' % id) 36 | cat = ant['category_id'] 37 | if cat >= 1 and cat <= 11: 38 | cat = cat - 1 39 | elif cat >= 13 and cat <= 25: 40 | cat = cat - 2 41 | elif cat >= 27 and cat <= 28: 42 | cat = cat - 3 43 | elif cat >= 31 and cat <= 44: 44 | cat = cat - 5 45 | elif cat >= 46 and cat <= 65: 46 | cat = cat - 6 47 | elif cat == 67: 48 | cat = cat - 7 49 | elif cat == 70: 50 | cat = cat - 9 51 | elif cat >= 72 and cat <= 82: 52 | cat = cat - 10 53 | elif cat >= 84 and cat <= 90: 54 | cat = cat - 11 55 | name_box_id[name].append([ant['bbox'], cat]) 56 | 57 | f = open(train_output_path, 'w') 58 | for key in name_box_id.keys(): 59 | f.write(key) 60 | box_infos = name_box_id[key] 61 | for info in box_infos: 62 | x_min = int(info[0][0]) 63 | y_min = int(info[0][1]) 64 | x_max = x_min + int(info[0][2]) 65 | y_max = y_min + int(info[0][3]) 66 | 67 | box_info = " %d,%d,%d,%d,%d" % ( 68 | x_min, y_min, x_max, y_max, int(info[1])) 69 | f.write(box_info) 70 | f.write('\n') 71 | f.close() 72 | 73 | name_box_id = defaultdict(list) 74 | id_name = dict() 75 | f = open(val_annotation_path, encoding='utf-8') 76 | data = json.load(f) 77 | 78 | annotations = data['annotations'] 79 | for ant in annotations: 80 | id = ant['image_id'] 81 | name = os.path.join(val_datasets_path, '%012d.jpg' % id) 82 | cat = ant['category_id'] 83 | if cat >= 1 and cat <= 11: 84 | cat = cat - 1 85 | elif cat >= 13 and cat <= 25: 86 | cat = cat - 2 87 | elif cat >= 27 and cat <= 28: 88 | cat = cat - 3 89 | elif cat >= 31 and cat <= 44: 90 | cat = cat - 5 91 | elif cat >= 46 and cat <= 65: 92 | cat = cat - 6 93 | elif cat == 67: 94 | cat = cat - 7 95 | elif cat == 70: 96 | cat = cat - 9 97 | elif cat >= 72 and cat <= 82: 98 | cat = cat - 10 99 | elif cat >= 84 and cat <= 90: 100 | cat = cat - 11 101 | name_box_id[name].append([ant['bbox'], cat]) 102 | 103 | f = open(val_output_path, 'w') 104 | for key in name_box_id.keys(): 105 | f.write(key) 106 | box_infos = name_box_id[key] 107 | for info in box_infos: 108 | x_min = int(info[0][0]) 109 | y_min = int(info[0][1]) 110 | x_max = x_min + int(info[0][2]) 111 | y_max = y_min + int(info[0][3]) 112 | 113 | box_info = " %d,%d,%d,%d,%d" % ( 114 | x_min, y_min, x_max, y_max, int(info[1])) 115 | f.write(box_info) 116 | f.write('\n') 117 | f.close() 118 | -------------------------------------------------------------------------------- /utils_coco/get_map_coco.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import numpy as np 5 | import torch 6 | from PIL import Image 7 | from pycocotools.coco import COCO 8 | from pycocotools.cocoeval import COCOeval 9 | from tqdm import tqdm 10 | 11 | from utils.utils import cvtColor, preprocess_input, resize_image 12 | from yolo import YOLO 13 | 14 | #---------------------------------------------------------------------------# 15 | # map_mode用于指定该文件运行时计算的内容 16 | # map_mode为0代表整个map计算流程,包括获得预测结果、计算map。 17 | # map_mode为1代表仅仅获得预测结果。 18 | # map_mode为2代表仅仅获得计算map。 19 | #---------------------------------------------------------------------------# 20 | map_mode = 0 21 | #-------------------------------------------------------# 22 | # 指向了验证集标签与图片路径 23 | #-------------------------------------------------------# 24 | cocoGt_path = 'coco_dataset/annotations/instances_val2017.json' 25 | dataset_img_path = 'coco_dataset/val2017' 26 | #-------------------------------------------------------# 27 | # 结果输出的文件夹,默认为map_out 28 | #-------------------------------------------------------# 29 | temp_save_path = 'map_out/coco_eval' 30 | 31 | class mAP_YOLO(YOLO): 32 | #---------------------------------------------------# 33 | # 检测图片 34 | #---------------------------------------------------# 35 | def detect_image(self, image_id, image, results): 36 | #---------------------------------------------------# 37 | # 计算输入图片的高和宽 38 | #---------------------------------------------------# 39 | image_shape = np.array(np.shape(image)[0:2]) 40 | #---------------------------------------------------------# 41 | # 在这里将图像转换成RGB图像,防止灰度图在预测时报错。 42 | # 代码仅仅支持RGB图像的预测,所有其它类型的图像都会转化成RGB 43 | #---------------------------------------------------------# 44 | image = cvtColor(image) 45 | #---------------------------------------------------------# 46 | # 给图像增加灰条,实现不失真的resize 47 | # 也可以直接resize进行识别 48 | #---------------------------------------------------------# 49 | image_data = resize_image(image, (self.input_shape[1],self.input_shape[0]), self.letterbox_image) 50 | #---------------------------------------------------------# 51 | # 添加上batch_size维度 52 | #---------------------------------------------------------# 53 | image_data = np.expand_dims(np.transpose(preprocess_input(np.array(image_data, dtype='float32')), (2, 0, 1)), 0) 54 | 55 | with torch.no_grad(): 56 | images = torch.from_numpy(image_data) 57 | if self.cuda: 58 | images = images.cuda() 59 | #---------------------------------------------------------# 60 | # 将图像输入网络当中进行预测! 61 | #---------------------------------------------------------# 62 | outputs = self.net(images) 63 | outputs = self.bbox_util.decode_box(outputs) 64 | #---------------------------------------------------------# 65 | # 将预测框进行堆叠,然后进行非极大抑制 66 | #---------------------------------------------------------# 67 | outputs = self.bbox_util.non_max_suppression(torch.cat(outputs, 1), self.num_classes, self.input_shape, 68 | image_shape, self.letterbox_image, conf_thres = self.confidence, nms_thres = self.nms_iou) 69 | 70 | if outputs[0] is None: 71 | return results 72 | 73 | top_label = np.array(outputs[0][:, 6], dtype = 'int32') 74 | top_conf = outputs[0][:, 4] * outputs[0][:, 5] 75 | top_boxes = outputs[0][:, :4] 76 | 77 | for i, c in enumerate(top_label): 78 | result = {} 79 | top, left, bottom, right = top_boxes[i] 80 | 81 | result["image_id"] = int(image_id) 82 | result["category_id"] = clsid2catid[c] 83 | result["bbox"] = [float(left),float(top),float(right-left),float(bottom-top)] 84 | result["score"] = float(top_conf[i]) 85 | results.append(result) 86 | return results 87 | 88 | if __name__ == "__main__": 89 | if not os.path.exists(temp_save_path): 90 | os.makedirs(temp_save_path) 91 | 92 | cocoGt = COCO(cocoGt_path) 93 | ids = list(cocoGt.imgToAnns.keys()) 94 | clsid2catid = cocoGt.getCatIds() 95 | 96 | if map_mode == 0 or map_mode == 1: 97 | yolo = mAP_YOLO(confidence = 0.001, nms_iou = 0.65) 98 | 99 | with open(os.path.join(temp_save_path, 'eval_results.json'),"w") as f: 100 | results = [] 101 | for image_id in tqdm(ids): 102 | image_path = os.path.join(dataset_img_path, cocoGt.loadImgs(image_id)[0]['file_name']) 103 | image = Image.open(image_path) 104 | results = yolo.detect_image(image_id, image, results) 105 | json.dump(results, f) 106 | 107 | if map_mode == 0 or map_mode == 2: 108 | cocoDt = cocoGt.loadRes(os.path.join(temp_save_path, 'eval_results.json')) 109 | cocoEval = COCOeval(cocoGt, cocoDt, 'bbox') 110 | cocoEval.evaluate() 111 | cocoEval.accumulate() 112 | cocoEval.summarize() 113 | print("Get map done.") 114 | -------------------------------------------------------------------------------- /voc_annotation.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import xml.etree.ElementTree as ET 4 | 5 | import numpy as np 6 | 7 | from utils.utils import get_classes 8 | 9 | #--------------------------------------------------------------------------------------------------------------------------------# 10 | # annotation_mode用于指定该文件运行时计算的内容 11 | # annotation_mode为0代表整个标签处理过程,包括获得VOCdevkit/VOC2007/ImageSets里面的txt以及训练用的2007_train.txt、2007_val.txt 12 | # annotation_mode为1代表获得VOCdevkit/VOC2007/ImageSets里面的txt 13 | # annotation_mode为2代表获得训练用的2007_train.txt、2007_val.txt 14 | #--------------------------------------------------------------------------------------------------------------------------------# 15 | annotation_mode = 0 16 | #-------------------------------------------------------------------# 17 | # 必须要修改,用于生成2007_train.txt、2007_val.txt的目标信息 18 | # 与训练和预测所用的classes_path一致即可 19 | # 如果生成的2007_train.txt里面没有目标信息 20 | # 那么就是因为classes没有设定正确 21 | # 仅在annotation_mode为0和2的时候有效 22 | #-------------------------------------------------------------------# 23 | classes_path = 'model_data/voc_classes.txt' 24 | #--------------------------------------------------------------------------------------------------------------------------------# 25 | # trainval_percent用于指定(训练集+验证集)与测试集的比例,默认情况下 (训练集+验证集):测试集 = 9:1 26 | # train_percent用于指定(训练集+验证集)中训练集与验证集的比例,默认情况下 训练集:验证集 = 9:1 27 | # 仅在annotation_mode为0和1的时候有效 28 | #--------------------------------------------------------------------------------------------------------------------------------# 29 | trainval_percent = 0.9 30 | train_percent = 0.9 31 | #-------------------------------------------------------# 32 | # 指向VOC数据集所在的文件夹 33 | # 默认指向根目录下的VOC数据集 34 | #-------------------------------------------------------# 35 | VOCdevkit_path = 'VOCdevkit' 36 | 37 | VOCdevkit_sets = [('2007', 'train'), ('2007', 'val')] 38 | classes, _ = get_classes(classes_path) 39 | 40 | #-------------------------------------------------------# 41 | # 统计目标数量 42 | #-------------------------------------------------------# 43 | photo_nums = np.zeros(len(VOCdevkit_sets)) 44 | nums = np.zeros(len(classes)) 45 | def convert_annotation(year, image_id, list_file): 46 | in_file = open(os.path.join(VOCdevkit_path, 'VOC%s/Annotations/%s.xml'%(year, image_id)), encoding='utf-8') 47 | tree=ET.parse(in_file) 48 | root = tree.getroot() 49 | 50 | for obj in root.iter('object'): 51 | difficult = 0 52 | if obj.find('difficult')!=None: 53 | difficult = obj.find('difficult').text 54 | cls = obj.find('name').text 55 | if cls not in classes or int(difficult)==1: 56 | continue 57 | cls_id = classes.index(cls) 58 | xmlbox = obj.find('bndbox') 59 | b = (int(float(xmlbox.find('xmin').text)), int(float(xmlbox.find('ymin').text)), int(float(xmlbox.find('xmax').text)), int(float(xmlbox.find('ymax').text))) 60 | list_file.write(" " + ",".join([str(a) for a in b]) + ',' + str(cls_id)) 61 | 62 | nums[classes.index(cls)] = nums[classes.index(cls)] + 1 63 | 64 | if __name__ == "__main__": 65 | random.seed(0) 66 | if " " in os.path.abspath(VOCdevkit_path): 67 | raise ValueError("数据集存放的文件夹路径与图片名称中不可以存在空格,否则会影响正常的模型训练,请注意修改。") 68 | 69 | if annotation_mode == 0 or annotation_mode == 1: 70 | print("Generate txt in ImageSets.") 71 | xmlfilepath = os.path.join(VOCdevkit_path, 'VOC2007/Annotations') 72 | saveBasePath = os.path.join(VOCdevkit_path, 'VOC2007/ImageSets/Main') 73 | temp_xml = os.listdir(xmlfilepath) 74 | total_xml = [] 75 | for xml in temp_xml: 76 | if xml.endswith(".xml"): 77 | total_xml.append(xml) 78 | 79 | num = len(total_xml) 80 | list = range(num) 81 | tv = int(num*trainval_percent) 82 | tr = int(tv*train_percent) 83 | trainval= random.sample(list,tv) 84 | train = random.sample(trainval,tr) 85 | 86 | print("train and val size",tv) 87 | print("train size",tr) 88 | ftrainval = open(os.path.join(saveBasePath,'trainval.txt'), 'w') 89 | ftest = open(os.path.join(saveBasePath,'test.txt'), 'w') 90 | ftrain = open(os.path.join(saveBasePath,'train.txt'), 'w') 91 | fval = open(os.path.join(saveBasePath,'val.txt'), 'w') 92 | 93 | for i in list: 94 | name=total_xml[i][:-4]+'\n' 95 | if i in trainval: 96 | ftrainval.write(name) 97 | if i in train: 98 | ftrain.write(name) 99 | else: 100 | fval.write(name) 101 | else: 102 | ftest.write(name) 103 | 104 | ftrainval.close() 105 | ftrain.close() 106 | fval.close() 107 | ftest.close() 108 | print("Generate txt in ImageSets done.") 109 | 110 | if annotation_mode == 0 or annotation_mode == 2: 111 | print("Generate 2007_train.txt and 2007_val.txt for train.") 112 | type_index = 0 113 | for year, image_set in VOCdevkit_sets: 114 | image_ids = open(os.path.join(VOCdevkit_path, 'VOC%s/ImageSets/Main/%s.txt'%(year, image_set)), encoding='utf-8').read().strip().split() 115 | list_file = open('%s_%s.txt'%(year, image_set), 'w', encoding='utf-8') 116 | for image_id in image_ids: 117 | list_file.write('%s/VOC%s/JPEGImages/%s.jpg'%(os.path.abspath(VOCdevkit_path), year, image_id)) 118 | 119 | convert_annotation(year, image_id, list_file) 120 | list_file.write('\n') 121 | photo_nums[type_index] = len(image_ids) 122 | type_index += 1 123 | list_file.close() 124 | print("Generate 2007_train.txt and 2007_val.txt for train done.") 125 | 126 | def printTable(List1, List2): 127 | for i in range(len(List1[0])): 128 | print("|", end=' ') 129 | for j in range(len(List1)): 130 | print(List1[j][i].rjust(int(List2[j])), end=' ') 131 | print("|", end=' ') 132 | print() 133 | 134 | str_nums = [str(int(x)) for x in nums] 135 | tableData = [ 136 | classes, str_nums 137 | ] 138 | colWidths = [0]*len(tableData) 139 | len1 = 0 140 | for i in range(len(tableData)): 141 | for j in range(len(tableData[i])): 142 | if len(tableData[i][j]) > colWidths[i]: 143 | colWidths[i] = len(tableData[i][j]) 144 | printTable(tableData, colWidths) 145 | 146 | if photo_nums[0] <= 500: 147 | print("训练集数量小于500,属于较小的数据量,请注意设置较大的训练世代(Epoch)以满足足够的梯度下降次数(Step)。") 148 | 149 | if np.sum(nums) == 0: 150 | print("在数据集中并未获得任何目标,请注意修改classes_path对应自己的数据集,并且保证标签名字正确,否则训练将会没有任何效果!") 151 | print("在数据集中并未获得任何目标,请注意修改classes_path对应自己的数据集,并且保证标签名字正确,否则训练将会没有任何效果!") 152 | print("在数据集中并未获得任何目标,请注意修改classes_path对应自己的数据集,并且保证标签名字正确,否则训练将会没有任何效果!") 153 | print("(重要的事情说三遍)。") 154 | -------------------------------------------------------------------------------- /常见问题汇总.md: -------------------------------------------------------------------------------- 1 | 问题汇总的博客地址为[https://blog.csdn.net/weixin_44791964/article/details/107517428](https://blog.csdn.net/weixin_44791964/article/details/107517428)。 2 | 3 | # 问题汇总 4 | ## 1、下载问题 5 | ### a、代码下载 6 | **问:up主,可以给我发一份代码吗,代码在哪里下载啊? 7 | 答:Github上的地址就在视频简介里。复制一下就能进去下载了。** 8 | 9 | **问:up主,为什么我下载的代码提示压缩包损坏? 10 | 答:重新去Github下载。** 11 | 12 | **问:up主,为什么我下载的代码和你在视频以及博客上的代码不一样? 13 | 答:我常常会对代码进行更新,最终以实际的代码为准。** 14 | 15 | ### b、 权值下载 16 | **问:up主,为什么我下载的代码里面,model_data下面没有.pth或者.h5文件? 17 | 答:我一般会把权值上传到Github和百度网盘,在GITHUB的README里面就能找到。** 18 | 19 | ### c、 数据集下载 20 | **问:up主,XXXX数据集在哪里下载啊? 21 | 答:一般数据集的下载地址我会放在README里面,基本上都有,没有的话请及时联系我添加,直接发github的issue即可**。 22 | 23 | ## 2、环境配置问题 24 | ### a、20系列及以下显卡环境配置 25 | **pytorch代码对应的pytorch版本为1.2,博客地址对应**[https://blog.csdn.net/weixin_44791964/article/details/106037141](https://blog.csdn.net/weixin_44791964/article/details/106037141)。 26 | 27 | **keras代码对应的tensorflow版本为1.13.2,keras版本是2.1.5,博客地址对应**[https://blog.csdn.net/weixin_44791964/article/details/104702142](https://blog.csdn.net/weixin_44791964/article/details/104702142)。 28 | 29 | **tf2代码对应的tensorflow版本为2.2.0,无需安装keras,博客地址对应**[https://blog.csdn.net/weixin_44791964/article/details/109161493](https://blog.csdn.net/weixin_44791964/article/details/109161493)。 30 | 31 | **问:你的代码某某某版本的tensorflow和pytorch能用嘛? 32 | 答:最好按照我推荐的配置,配置教程也有!其它版本的我没有试过!可能出现问题但是一般问题不大。仅需要改少量代码即可。** 33 | 34 | ### b、30系列显卡环境配置 35 | 30系显卡由于框架更新不可使用上述环境配置教程。 36 | 当前我已经测试的可以用的30显卡配置如下: 37 | **pytorch代码对应的pytorch版本为1.7.0,cuda为11.0,cudnn为8.0.5,博客地址对应**[https://blog.csdn.net/weixin_44791964/article/details/120668551](https://blog.csdn.net/weixin_44791964/article/details/120668551)。 38 | 39 | **keras代码无法在win10下配置cuda11,在ubuntu下可以百度查询一下,配置tensorflow版本为1.15.4,keras版本是2.1.5或者2.3.1(少量函数接口不同,代码可能还需要少量调整。)** 40 | 41 | **tf2代码对应的tensorflow版本为2.4.0,cuda为11.0,cudnn为8.0.5,博客地址对应为**[https://blog.csdn.net/weixin_44791964/article/details/120657664](https://blog.csdn.net/weixin_44791964/article/details/120657664)。 42 | 43 | ### c、CPU环境配置 44 | **pytorch代码对应的pytorch-cpu版本为1.2,博客地址对应**[https://blog.csdn.net/weixin_44791964/article/details/120655098](https://blog.csdn.net/weixin_44791964/article/details/120655098) 45 | 46 | **keras代码对应的tensorflow-cpu版本为1.13.2,keras版本是2.1.5,博客地址对应**[https://blog.csdn.net/weixin_44791964/article/details/120653717](https://blog.csdn.net/weixin_44791964/article/details/120653717)。 47 | 48 | **tf2代码对应的tensorflow-cpu版本为2.2.0,无需安装keras,博客地址对应**[https://blog.csdn.net/weixin_44791964/article/details/120656291](https://blog.csdn.net/weixin_44791964/article/details/120656291)。 49 | 50 | 51 | ### d、GPU利用问题与环境使用问题 52 | **问:为什么我安装了tensorflow-gpu但是却没用利用GPU进行训练呢? 53 | 答:确认tensorflow-gpu已经装好,利用pip list查看tensorflow版本,然后查看任务管理器或者利用nvidia命令看看是否使用了gpu进行训练,任务管理器的话要看显存使用情况。** 54 | 55 | **问:up主,我好像没有在用gpu进行训练啊,怎么看是不是用了GPU进行训练? 56 | 答:查看是否使用GPU进行训练一般使用NVIDIA在命令行的查看命令。在windows电脑中打开cmd然后利用nvidia-smi指令查看GPU利用情况** 57 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/f88ef794c9a341918f000eb2b1c67af6.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAQnViYmxpaWlpbmc=,size_20,color_FFFFFF,t_70,g_se,x_16) 58 | **如果要一定看任务管理器的话,请看性能部分GPU的显存是否利用,或者查看任务管理器的Cuda,而非Copy。** 59 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20201013234241524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDc5MTk2NA==,size_16,color_FFFFFF,t_70#pic_center) 60 | 61 | ### e、DLL load failed: 找不到指定的模块 62 | **问:出现如下错误** 63 | ```python 64 | Traceback (most recent call last): 65 | File "C:\Users\focus\Anaconda3\ana\envs\tensorflow-gpu\lib\site-packages\tensorflow\python\pywrap_tensorflow.py", line 58, in 66 | from tensorflow.python.pywrap_tensorflow_internal import * 67 | File "C:\Users\focus\Anaconda3\ana\envs\tensorflow-gpu\lib\site-packages\tensorflow\python\pywrap_tensorflow_internal.py", line 28, in 68 | pywrap_tensorflow_internal = swig_import_helper() 69 | File "C:\Users\focus\Anaconda3\ana\envs\tensorflow-gpu\lib\site-packages\tensorflow\python\pywrap_tensorflow_internal.py", line 24, in swig_import_helper 70 | _mod = imp.load_module('_pywrap_tensorflow_internal', fp, pathname, description) 71 | File "C:\Users\focus\Anaconda3\ana\envs\tensorflow-gpu\lib\imp.py", line 243, in load_modulereturn load_dynamic(name, filename, file) 72 | File "C:\Users\focus\Anaconda3\ana\envs\tensorflow-gpu\lib\imp.py", line 343, in load_dynamic 73 | return _load(spec) 74 | ImportError: DLL load failed: 找不到指定的模块。 75 | ``` 76 | **答:如果没重启过就重启一下,否则重新按照步骤安装,还无法解决则把你的GPU、CUDA、CUDNN、TF版本以及PYTORCH版本私聊告诉我。** 77 | 78 | ### f、no module问题(no module name utils.utils、no module named 'matplotlib' ) 79 | **问:为什么提示说no module name utils.utils(no module name nets.yolo、no module name nets.ssd等一系列问题)啊? 80 | 答:utils并不需要用pip装,它就在我上传的仓库的根目录,出现这个问题的原因是根目录不对,查查相对目录和根目录的概念。查了基本上就明白了。** 81 | 82 | **问:为什么提示说no module name matplotlib(no module name PIL,no module name cv2等等)? 83 | 答:这个库没安装打开命令行安装就好。pip install matplotlib** 84 | 85 | **问:为什么我已经用pip装了opencv(pillow、matplotlib等),还是提示no module name cv2? 86 | 答:没有激活环境装,要激活对应的conda环境进行安装才可以正常使用** 87 | 88 | **问:为什么提示说No module named 'torch' ? 89 | 答:其实我也真的很想知道为什么会有这个问题……这个pytorch没装是什么情况?一般就俩情况,一个是真的没装,还有一个是装到其它环境了,当前激活的环境不是自己装的环境。** 90 | 91 | **问:为什么提示说No module named 'tensorflow' ? 92 | 答:同上。** 93 | 94 | ### g、cuda安装失败问题 95 | 一般cuda安装前需要安装Visual Studio,装个2017版本即可。 96 | 97 | ### h、Ubuntu系统问题 98 | **所有代码在Ubuntu下可以使用,我两个系统都试过。** 99 | 100 | ### i、VSCODE提示错误的问题 101 | **问:为什么在VSCODE里面提示一大堆的错误啊? 102 | 答:我也提示一大堆的错误,但是不影响,是VSCODE的问题,如果不想看错误的话就装Pycharm。 103 | 最好将设置里面的Python:Language Server,调整为Pylance。** 104 | 105 | ### j、使用cpu进行训练与预测的问题 106 | **对于keras和tf2的代码而言,如果想用cpu进行训练和预测,直接装cpu版本的tensorflow就可以了。** 107 | 108 | **对于pytorch的代码而言,如果想用cpu进行训练和预测,需要将cuda=True修改成cuda=False。** 109 | 110 | ### k、tqdm没有pos参数问题 111 | **问:运行代码提示'tqdm' object has no attribute 'pos'。 112 | 答:重装tqdm,换个版本就可以了。** 113 | 114 | ### l、提示decode(“utf-8”)的问题 115 | **由于h5py库的更新,安装过程中会自动安装h5py=3.0.0以上的版本,会导致decode("utf-8")的错误! 116 | 各位一定要在安装完tensorflow后利用命令装h5py=2.10.0!** 117 | ``` 118 | pip install h5py==2.10.0 119 | ``` 120 | 121 | ### m、提示TypeError: __array__() takes 1 positional argument but 2 were given错误 122 | 可以修改pillow版本解决。 123 | ``` 124 | pip install pillow==8.2.0 125 | ``` 126 | ### n、如何查看当前cuda和cudnn 127 | **window下cuda版本查看方式如下: 128 | 1、打开cmd窗口。 129 | 2、输入nvcc -V。 130 | 3、Cuda compilation tools, release XXXXXXXX中的XXXXXXXX即cuda版本。** 131 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/0389ea35107a408a80ab5cb6590d5a74.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAQnViYmxpaWlpbmc=,size_20,color_FFFFFF,t_70,g_se,x_16) 132 | window下cudnn版本查看方式如下: 133 | 1、进入cuda安装目录,进入incude文件夹。 134 | 2、找到cudnn.h文件。 135 | 3、右键文本打开,下拉,看到#define处可获得cudnn版本。 136 | ```python 137 | #define CUDNN_MAJOR 7 138 | #define CUDNN_MINOR 4 139 | #define CUDNN_PATCHLEVEL 1 140 | ``` 141 | 代表cudnn为7.4.1。 142 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/7a86b68b17c84feaa6fa95780d4ae4b4.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAQnViYmxpaWlpbmc=,size_20,color_FFFFFF,t_70,g_se,x_16) 143 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/81bb7c3e13cc492292530e4b69df86a9.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAQnViYmxpaWlpbmc=,size_20,color_FFFFFF,t_70,g_se,x_16) 144 | 145 | ### o、为什么按照你的环境配置后还是不能使用 146 | **问:up主,为什么我按照你的环境配置后还是不能使用? 147 | 答:请把你的GPU、CUDA、CUDNN、TF版本以及PYTORCH版本B站私聊告诉我。** 148 | 149 | ### p、其它问题 150 | **问:为什么提示TypeError: cat() got an unexpected keyword argument 'axis',Traceback (most recent call last),AttributeError: 'Tensor' object has no attribute 'bool'? 151 | 答:这是版本问题,建议使用torch1.2以上版本** 152 | 153 | **其它有很多稀奇古怪的问题,很多是版本问题,建议按照我的视频教程安装Keras和tensorflow。比如装的是tensorflow2,就不用问我说为什么我没法运行Keras-yolo啥的。那是必然不行的。** 154 | 155 | ## 3、目标检测库问题汇总(人脸检测和分类库也可参考) 156 | ### a、shape不匹配问题。 157 | #### 1)、训练时shape不匹配问题。 158 | **问:up主,为什么运行train.py会提示shape不匹配啊? 159 | 答:在keras环境中,因为你训练的种类和原始的种类不同,网络结构会变化,所以最尾部的shape会有少量不匹配。** 160 | 161 | #### 2)、预测时shape不匹配问题。 162 | **问:为什么我运行predict.py会提示我说shape不匹配呀。** 163 | ##### i、copying a param with shape torch.Size([75, 704, 1, 1]) from checkpoint 164 | 在Pytorch里面是这样的: 165 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200722171631901.png) 166 | ##### ii、Shapes are [1,1,1024,75] and [255,1024,1,1]. for 'Assign_360' (op: 'Assign') with input shapes: [1,1,1024,75], [255,1024,1,1]. 167 | 在Keras里面是这样的: 168 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200722171523380.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDc5MTk2NA==,size_16,color_FFFFFF,t_70) 169 | **答:原因主要有仨: 170 | 1、训练的classes_path没改,就开始训练了。 171 | 2、训练的model_path没改。 172 | 3、训练的classes_path没改。 173 | 请检查清楚了!确定自己所用的model_path和classes_path是对应的!训练的时候用到的num_classes或者classes_path也需要检查!** 174 | 175 | ### b、显存不足问题(OOM、RuntimeError: CUDA out of memory)。 176 | **问:为什么我运行train.py下面的命令行闪的贼快,还提示OOM啥的? 177 | 答:这是在keras中出现的,爆显存了,可以改小batch_size,SSD的显存占用率是最小的,建议用SSD; 178 | 2G显存:SSD、YOLOV4-TINY 179 | 4G显存:YOLOV3 180 | 6G显存:YOLOV4、Retinanet、M2det、Efficientdet、Faster RCNN等 181 | 8G+显存:随便选吧。** 182 | **需要注意的是,受到BatchNorm2d影响,batch_size不可为1,至少为2。** 183 | 184 | **问:为什么提示 RuntimeError: CUDA out of memory. Tried to allocate 52.00 MiB (GPU 0; 15.90 GiB total capacity; 14.85 GiB already allocated; 51.88 MiB free; 15.07 GiB reserved in total by PyTorch)? 185 | 答:这是pytorch中出现的,爆显存了,同上。** 186 | 187 | **问:为什么我显存都没利用,就直接爆显存了? 188 | 答:都爆显存了,自然就不利用了,模型没有开始训练。** 189 | ### c、为什么要进行冻结训练与解冻训练,不进行行吗? 190 | **问:为什么要冻结训练和解冻训练呀? 191 | 答:可以不进行,本质上是为了保证性能不足的同学的训练,如果电脑性能完全不够,可以将Freeze_Epoch和UnFreeze_Epoch设置成一样,只进行冻结训练。** 192 | 193 | **同时这也是迁移学习的思想,因为神经网络主干特征提取部分所提取到的特征是通用的,我们冻结起来训练可以加快训练效率,也可以防止权值被破坏。** 194 | 在冻结阶段,模型的主干被冻结了,特征提取网络不发生改变。占用的显存较小,仅对网络进行微调。 195 | 在解冻阶段,模型的主干不被冻结了,特征提取网络会发生改变。占用的显存较大,网络所有的参数都会发生改变。 196 | 197 | ### d、我的LOSS好大啊,有问题吗?(我的LOSS好小啊,有问题吗?) 198 | **问:为什么我的网络不收敛啊,LOSS是XXXX。 199 | 答:不同网络的LOSS不同,LOSS只是一个参考指标,用于查看网络是否收敛,而非评价网络好坏,我的yolo代码都没有归一化,所以LOSS值看起来比较高,LOSS的值不重要,重要的是是否在变小,预测是否有效果。** 200 | 201 | ### e、为什么我训练出来的模型没有预测结果? 202 | **问:为什么我的训练效果不好?预测了没有框(框不准)。 203 | 答:** 204 | 考虑几个问题: 205 | 1、目标信息问题,查看2007_train.txt文件是否有目标信息,没有的话请修改voc_annotation.py。 206 | 2、数据集问题,小于500的自行考虑增加数据集,同时测试不同的模型,确认数据集是好的。 207 | 3、是否解冻训练,如果数据集分布与常规画面差距过大需要进一步解冻训练,调整主干,加强特征提取能力。 208 | 4、网络问题,比如SSD不适合小目标,因为先验框固定了。 209 | 5、训练时长问题,有些同学只训练了几代表示没有效果,按默认参数训练完。 210 | 6、确认自己是否按照步骤去做了,如果比如voc_annotation.py里面的classes是否修改了等。 211 | 7、不同网络的LOSS不同,LOSS只是一个参考指标,用于查看网络是否收敛,而非评价网络好坏,LOSS的值不重要,重要的是是否收敛。 212 | 8、是否修改了网络的主干,如果修改了没有预训练权重,网络不容易收敛,自然效果不好。 213 | 214 | ### f、为什么我计算出来的map是0? 215 | **问:为什么我的训练效果不好?没有map? 216 | 答:** 217 | 首先尝试利用predict.py预测一下,如果有效果的话应该是get_map.py里面的classes_path设置错误。如果没有预测结果的话,解决方法同e问题,对下面几点进行检查: 218 | 1、目标信息问题,查看2007_train.txt文件是否有目标信息,没有的话请修改voc_annotation.py。 219 | 2、数据集问题,小于500的自行考虑增加数据集,同时测试不同的模型,确认数据集是好的。 220 | 3、是否解冻训练,如果数据集分布与常规画面差距过大需要进一步解冻训练,调整主干,加强特征提取能力。 221 | 4、网络问题,比如SSD不适合小目标,因为先验框固定了。 222 | 5、训练时长问题,有些同学只训练了几代表示没有效果,按默认参数训练完。 223 | 6、确认自己是否按照步骤去做了,如果比如voc_annotation.py里面的classes是否修改了等。 224 | 7、不同网络的LOSS不同,LOSS只是一个参考指标,用于查看网络是否收敛,而非评价网络好坏,LOSS的值不重要,重要的是是否收敛。 225 | 8、是否修改了网络的主干,如果修改了没有预训练权重,网络不容易收敛,自然效果不好。 226 | 227 | ### g、gbk编码错误('gbk' codec can't decode byte)。 228 | **问:我怎么出现了gbk什么的编码错误啊:** 229 | ```python 230 | UnicodeDecodeError: 'gbk' codec can't decode byte 0xa6 in position 446: illegal multibyte sequence 231 | ``` 232 | **答:标签和路径不要使用中文,如果一定要使用中文,请注意处理的时候编码的问题,改成打开文件的encoding方式改为utf-8。** 233 | 234 | ### h、我的图片是xxx*xxx的分辨率的,可以用吗? 235 | **问:我的图片是xxx*xxx的分辨率的,可以用吗!** 236 | **答:可以用,代码里面会自动进行resize与数据增强。** 237 | 238 | ### i、我想进行数据增强!怎么增强? 239 | **问:我想要进行数据增强!怎么做呢?** 240 | **答:可以用,代码里面会自动进行resize与数据增强。** 241 | 242 | ### j、多GPU训练。 243 | **问:怎么进行多GPU训练? 244 | 答:pytorch的大多数代码可以直接使用gpu训练,keras的话直接百度就好了,实现并不复杂,我没有多卡没法详细测试,还需要各位同学自己努力了。** 245 | 246 | ### k、能不能训练灰度图? 247 | **问:能不能训练灰度图(预测灰度图)啊? 248 | 答:我的大多数库会将灰度图转化成RGB进行训练和预测,如果遇到代码不能训练或者预测灰度图的情况,可以尝试一下在get_random_data里面将Image.open后的结果转换成RGB,预测的时候也这样试试。(仅供参考)** 249 | 250 | ### l、断点续练问题。 251 | **问:我已经训练过几个世代了,能不能从这个基础上继续开始训练 252 | 答:可以,你在训练前,和载入预训练权重一样载入训练过的权重就行了。一般训练好的权重会保存在logs文件夹里面,将model_path修改成你要开始的权值的路径即可。** 253 | 254 | ### m、我要训练其它的数据集,预训练权重能不能用? 255 | **问:如果我要训练其它的数据集,预训练权重要怎么办啊?** 256 | **答:数据的预训练权重对不同数据集是通用的,因为特征是通用的,预训练权重对于99%的情况都必须要用,不用的话权值太过随机,特征提取效果不明显,网络训练的结果也不会好。** 257 | 258 | ### n、网络如何从0开始训练? 259 | **问:我要怎么不使用预训练权重啊? 260 | 答:看一看注释、大多数代码是model_path = '',Freeze_Train = Fasle**,如果设置model_path无用,**那么把载入预训练权重的代码注释了就行。** 261 | 262 | ### o、为什么从0开始训练效果这么差(修改了网络主干,效果不好怎么办)? 263 | **问:为什么我不使用预训练权重效果这么差啊? 264 | 答:因为随机初始化的权值不好,提取的特征不好,也就导致了模型训练的效果不好,voc07+12、coco+voc07+12效果都不一样,预训练权重还是非常重要的。** 265 | 266 | **问:up,我修改了网络,预训练权重还能用吗? 267 | 答:修改了主干的话,如果不是用的现有的网络,基本上预训练权重是不能用的,要么就自己判断权值里卷积核的shape然后自己匹配,要么只能自己预训练去了;修改了后半部分的话,前半部分的主干部分的预训练权重还是可以用的,如果是pytorch代码的话,需要自己修改一下载入权值的方式,判断shape后载入,如果是keras代码,直接by_name=True,skip_mismatch=True即可。** 268 | 权值匹配的方式可以参考如下: 269 | ```python 270 | # 加快模型训练的效率 271 | print('Loading weights into state dict...') 272 | device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') 273 | model_dict = model.state_dict() 274 | pretrained_dict = torch.load(model_path, map_location=device) 275 | a = {} 276 | for k, v in pretrained_dict.items(): 277 | try: 278 | if np.shape(model_dict[k]) == np.shape(v): 279 | a[k]=v 280 | except: 281 | pass 282 | model_dict.update(a) 283 | model.load_state_dict(model_dict) 284 | print('Finished!') 285 | ``` 286 | 287 | **问:为什么从0开始训练效果这么差(我修改了网络主干,效果不好怎么办)? 288 | 答:一般来讲,网络从0开始的训练效果会很差,因为权值太过随机,特征提取效果不明显,因此非常、非常、非常不建议大家从0开始训练!如果一定要从0开始,可以了解imagenet数据集,首先训练分类模型,获得网络的主干部分权值,分类模型的 主干部分 和该模型通用,基于此进行训练。 289 | 网络修改了主干之后也是同样的问题,随机的权值效果很差。** 290 | 291 | **问:怎么在模型上从0开始训练? 292 | 答:在算力不足与调参能力不足的情况下从0开始训练毫无意义。模型特征提取能力在随机初始化参数的情况下非常差。没有好的参数调节能力和算力,无法使得网络正常收敛。** 293 | 如果一定要从0开始,那么训练的时候请注意几点: 294 | - 不载入预训练权重。 295 | - 不要进行冻结训练,注释冻结模型的代码。 296 | 297 | **问:为什么我不使用预训练权重效果这么差啊? 298 | 答:因为随机初始化的权值不好,提取的特征不好,也就导致了模型训练的效果不好,voc07+12、coco+voc07+12效果都不一样,预训练权重还是非常重要的。** 299 | 300 | ### p、你的权值都是哪里来的? 301 | **问:如果网络不能从0开始训练的话你的权值哪里来的? 302 | 答:有些权值是官方转换过来的,有些权值是自己训练出来的,我用到的主干的imagenet的权值都是官方的。** 303 | 304 | ### q、视频检测与摄像头检测 305 | **问:怎么用摄像头检测呀? 306 | 答:predict.py修改参数可以进行摄像头检测,也有视频详细解释了摄像头检测的思路。** 307 | 308 | **问:怎么用视频检测呀? 309 | 答:同上** 310 | 311 | ### r、如何保存检测出的图片 312 | **问:检测完的图片怎么保存? 313 | 答:一般目标检测用的是Image,所以查询一下PIL库的Image如何进行保存。详细看看predict.py文件的注释。** 314 | 315 | **问:怎么用视频保存呀? 316 | 答:详细看看predict.py文件的注释。** 317 | 318 | ### s、遍历问题 319 | **问:如何对一个文件夹的图片进行遍历? 320 | 答:一般使用os.listdir先找出文件夹里面的所有图片,然后根据predict.py文件里面的执行思路检测图片就行了,详细看看predict.py文件的注释。** 321 | 322 | **问:如何对一个文件夹的图片进行遍历?并且保存。 323 | 答:遍历的话一般使用os.listdir先找出文件夹里面的所有图片,然后根据predict.py文件里面的执行思路检测图片就行了。保存的话一般目标检测用的是Image,所以查询一下PIL库的Image如何进行保存。如果有些库用的是cv2,那就是查一下cv2怎么保存图片。详细看看predict.py文件的注释。** 324 | 325 | ### t、路径问题(No such file or directory、StopIteration: [Errno 13] Permission denied: 'XXXXXX') 326 | **问:我怎么出现了这样的错误呀:** 327 | ```python 328 | FileNotFoundError: 【Errno 2】 No such file or directory 329 | StopIteration: [Errno 13] Permission denied: 'D:\\Study\\Collection\\Dataset\\VOC07+12+test\\VOCdevkit/VOC2007' 330 | …………………………………… 331 | …………………………………… 332 | ``` 333 | **答:去检查一下文件夹路径,查看是否有对应文件;并且检查一下2007_train.txt,其中文件路径是否有错。** 334 | 关于路径有几个重要的点: 335 | **文件夹名称中一定不要有空格。 336 | 注意相对路径和绝对路径。 337 | 多百度路径相关的知识。** 338 | 339 | **所有的路径问题基本上都是根目录问题,好好查一下相对目录的概念!** 340 | ### u、和原版比较问题,你怎么和原版不一样啊? 341 | **问:原版的代码是XXX,为什么你的代码是XXX? 342 | 答:是啊……这要不怎么说我不是原版呢……** 343 | 344 | **问:你这个代码和原版比怎么样,可以达到原版的效果么? 345 | 答:基本上可以达到,我都用voc数据测过,我没有好显卡,没有能力在coco上测试与训练。** 346 | 347 | **问:你有没有实现yolov4所有的tricks,和原版差距多少? 348 | 答:并没有实现全部的改进部分,由于YOLOV4使用的改进实在太多了,很难完全实现与列出来,这里只列出来了一些我比较感兴趣,而且非常有效的改进。论文中提到的SAM(注意力机制模块),作者自己的源码也没有使用。还有其它很多的tricks,不是所有的tricks都有提升,我也没法实现全部的tricks。至于和原版的比较,我没有能力训练coco数据集,根据使用过的同学反应差距不大。** 349 | 350 | ### v、我的检测速度是xxx正常吗?我的检测速度还能增快吗? 351 | **问:你这个FPS可以到达多少,可以到 XX FPS么? 352 | 答:FPS和机子的配置有关,配置高就快,配置低就慢。** 353 | 354 | **问:我的检测速度是xxx正常吗?我的检测速度还能增快吗? 355 | 答:看配置,配置好速度就快,如果想要配置不变的情况下加快速度,就要修改网络了。** 356 | 357 | **问:为什么我用服务器去测试yolov4(or others)的FPS只有十几? 358 | 答:检查是否正确安装了tensorflow-gpu或者pytorch的gpu版本,如果已经正确安装,可以去利用time.time()的方法查看detect_image里面,哪一段代码耗时更长(不仅只有网络耗时长,其它处理部分也会耗时,如绘图等)。** 359 | 360 | **问:为什么论文中说速度可以达到XX,但是这里却没有? 361 | 答:检查是否正确安装了tensorflow-gpu或者pytorch的gpu版本,如果已经正确安装,可以去利用time.time()的方法查看detect_image里面,哪一段代码耗时更长(不仅只有网络耗时长,其它处理部分也会耗时,如绘图等)。有些论文还会使用多batch进行预测,我并没有去实现这个部分。** 362 | 363 | ### w、预测图片不显示问题 364 | **问:为什么你的代码在预测完成后不显示图片?只是在命令行告诉我有什么目标。 365 | 答:给系统安装一个图片查看器就行了。** 366 | 367 | ### x、算法评价问题(目标检测的map、PR曲线、Recall、Precision等) 368 | **问:怎么计算map? 369 | 答:看map视频,都一个流程。** 370 | 371 | **问:计算map的时候,get_map.py里面有一个MINOVERLAP是什么用的,是iou吗? 372 | 答:是iou,它的作用是判断预测框和真实框的重合成度,如果重合程度大于MINOVERLAP,则预测正确。** 373 | 374 | **问:为什么get_map.py里面的self.confidence(self.score)要设置的那么小? 375 | 答:看一下map的视频的原理部分,要知道所有的结果然后再进行pr曲线的绘制。** 376 | 377 | **问:能不能说说怎么绘制PR曲线啥的呀。 378 | 答:可以看mAP视频,结果里面有PR曲线。** 379 | 380 | **问:怎么计算Recall、Precision指标。 381 | 答:这俩指标应该是相对于特定的置信度的,计算map的时候也会获得。** 382 | 383 | ### y、coco数据集训练问题 384 | **问:目标检测怎么训练COCO数据集啊?。 385 | 答:coco数据训练所需要的txt文件可以参考qqwweee的yolo3的库,格式都是一样的。** 386 | 387 | ### z、UP,怎么优化模型啊?我想提升效果 388 | **问:up,怎么修改模型啊,我想发个小论文! 389 | 答:建议看看yolov3和yolov4的区别,然后看看yolov4的论文,作为一个大型调参现场非常有参考意义,使用了很多tricks。我能给的建议就是多看一些经典模型,然后拆解里面的亮点结构并使用。** 390 | 391 | ### aa、UP,有Focal LOSS的代码吗?怎么改啊? 392 | **问:up,YOLO系列使用Focal LOSS的代码你有吗,有提升吗? 393 | 答:很多人试过,提升效果也不大(甚至变的更Low),它自己有自己的正负样本的平衡方式**。改代码的事情,还是自己好好看看代码吧。 394 | 395 | ### ab、部署问题(ONNX、TensorRT等) 396 | 我没有具体部署到手机等设备上过,所以很多部署问题我并不了解…… 397 | 398 | ## 4、语义分割库问题汇总 399 | ### a、shape不匹配问题 400 | #### 1)、训练时shape不匹配问题 401 | **问:up主,为什么运行train.py会提示shape不匹配啊? 402 | 答:在keras环境中,因为你训练的种类和原始的种类不同,网络结构会变化,所以最尾部的shape会有少量不匹配。** 403 | 404 | #### 2)、预测时shape不匹配问题 405 | **问:为什么我运行predict.py会提示我说shape不匹配呀。** 406 | ##### i、copying a param with shape torch.Size([75, 704, 1, 1]) from checkpoint 407 | 在Pytorch里面是这样的: 408 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200722171631901.png) 409 | ##### ii、Shapes are [1,1,1024,75] and [255,1024,1,1]. for 'Assign_360' (op: 'Assign') with input shapes: [1,1,1024,75], [255,1024,1,1]. 410 | 在Keras里面是这样的: 411 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200722171523380.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDc5MTk2NA==,size_16,color_FFFFFF,t_70) 412 | **答:原因主要有二: 413 | 1、train.py里面的num_classes没改。 414 | 2、预测时num_classes没改。 415 | 3、预测时model_path没改。 416 | 请检查清楚!训练和预测的时候用到的num_classes都需要检查!** 417 | 418 | ### b、显存不足问题(OOM、RuntimeError: CUDA out of memory)。 419 | **问:为什么我运行train.py下面的命令行闪的贼快,还提示OOM啥的? 420 | 答:这是在keras中出现的,爆显存了,可以改小batch_size。** 421 | 422 | **需要注意的是,受到BatchNorm2d影响,batch_size不可为1,至少为2。** 423 | 424 | **问:为什么提示 RuntimeError: CUDA out of memory. Tried to allocate 52.00 MiB (GPU 0; 15.90 GiB total capacity; 14.85 GiB already allocated; 51.88 MiB free; 15.07 GiB reserved in total by PyTorch)? 425 | 答:这是pytorch中出现的,爆显存了,同上。** 426 | 427 | **问:为什么我显存都没利用,就直接爆显存了? 428 | 答:都爆显存了,自然就不利用了,模型没有开始训练。** 429 | 430 | ### c、为什么要进行冻结训练与解冻训练,不进行行吗? 431 | **问:为什么要冻结训练和解冻训练呀? 432 | 答:可以不进行,本质上是为了保证性能不足的同学的训练,如果电脑性能完全不够,可以将Freeze_Epoch和UnFreeze_Epoch设置成一样,只进行冻结训练。** 433 | 434 | **同时这也是迁移学习的思想,因为神经网络主干特征提取部分所提取到的特征是通用的,我们冻结起来训练可以加快训练效率,也可以防止权值被破坏。** 435 | 在冻结阶段,模型的主干被冻结了,特征提取网络不发生改变。占用的显存较小,仅对网络进行微调。 436 | 在解冻阶段,模型的主干不被冻结了,特征提取网络会发生改变。占用的显存较大,网络所有的参数都会发生改变。 437 | 438 | ### d、我的LOSS好大啊,有问题吗?(我的LOSS好小啊,有问题吗?) 439 | **问:为什么我的网络不收敛啊,LOSS是XXXX。 440 | 答:不同网络的LOSS不同,LOSS只是一个参考指标,用于查看网络是否收敛,而非评价网络好坏,我的yolo代码都没有归一化,所以LOSS值看起来比较高,LOSS的值不重要,重要的是是否在变小,预测是否有效果。** 441 | 442 | ### e、为什么我训练出来的模型没有预测结果? 443 | **问:为什么我的训练效果不好?预测了没有框(框不准)。 444 | 答:** 445 | **考虑几个问题: 446 | 1、数据集问题,这是最重要的问题。小于500的自行考虑增加数据集;一定要检查数据集的标签,视频中详细解析了VOC数据集的格式,但并不是有输入图片有输出标签即可,还需要确认标签的每一个像素值是否为它对应的种类。很多同学的标签格式不对,最常见的错误格式就是标签的背景为黑,目标为白,此时目标的像素点值为255,无法正常训练,目标需要为1才行。 447 | 2、是否解冻训练,如果数据集分布与常规画面差距过大需要进一步解冻训练,调整主干,加强特征提取能力。 448 | 3、网络问题,可以尝试不同的网络。 449 | 4、训练时长问题,有些同学只训练了几代表示没有效果,按默认参数训练完。 450 | 5、确认自己是否按照步骤去做了。 451 | 6、不同网络的LOSS不同,LOSS只是一个参考指标,用于查看网络是否收敛,而非评价网络好坏,LOSS的值不重要,重要的是是否收敛。** 452 | 453 | **问:为什么我的训练效果不好?对小目标预测不准确。 454 | 答:对于deeplab和pspnet而言,可以修改一下downsample_factor,当downsample_factor为16的时候下采样倍数过多,效果不太好,可以修改为8。** 455 | 456 | ### f、为什么我计算出来的miou是0? 457 | **问:为什么我的训练效果不好?计算出来的miou是0?。** 458 | 答: 459 | 与e类似,**考虑几个问题: 460 | 1、数据集问题,这是最重要的问题。小于500的自行考虑增加数据集;一定要检查数据集的标签,视频中详细解析了VOC数据集的格式,但并不是有输入图片有输出标签即可,还需要确认标签的每一个像素值是否为它对应的种类。很多同学的标签格式不对,最常见的错误格式就是标签的背景为黑,目标为白,此时目标的像素点值为255,无法正常训练,目标需要为1才行。 461 | 2、是否解冻训练,如果数据集分布与常规画面差距过大需要进一步解冻训练,调整主干,加强特征提取能力。 462 | 3、网络问题,可以尝试不同的网络。 463 | 4、训练时长问题,有些同学只训练了几代表示没有效果,按默认参数训练完。 464 | 5、确认自己是否按照步骤去做了。 465 | 6、不同网络的LOSS不同,LOSS只是一个参考指标,用于查看网络是否收敛,而非评价网络好坏,LOSS的值不重要,重要的是是否收敛。** 466 | 467 | ### g、gbk编码错误('gbk' codec can't decode byte)。 468 | **问:我怎么出现了gbk什么的编码错误啊:** 469 | ```python 470 | UnicodeDecodeError: 'gbk' codec can't decode byte 0xa6 in position 446: illegal multibyte sequence 471 | ``` 472 | **答:标签和路径不要使用中文,如果一定要使用中文,请注意处理的时候编码的问题,改成打开文件的encoding方式改为utf-8。** 473 | 474 | ### h、我的图片是xxx*xxx的分辨率的,可以用吗? 475 | **问:我的图片是xxx*xxx的分辨率的,可以用吗!** 476 | **答:可以用,代码里面会自动进行resize与数据增强。** 477 | 478 | ### i、我想进行数据增强!怎么增强? 479 | **问:我想要进行数据增强!怎么做呢?** 480 | **答:可以用,代码里面会自动进行resize与数据增强。** 481 | 482 | ### j、多GPU训练。 483 | **问:怎么进行多GPU训练? 484 | 答:pytorch的大多数代码可以直接使用gpu训练,keras的话直接百度就好了,实现并不复杂,我没有多卡没法详细测试,还需要各位同学自己努力了。** 485 | 486 | ### k、能不能训练灰度图? 487 | **问:能不能训练灰度图(预测灰度图)啊? 488 | 答:我的大多数库会将灰度图转化成RGB进行训练和预测,如果遇到代码不能训练或者预测灰度图的情况,可以尝试一下在get_random_data里面将Image.open后的结果转换成RGB,预测的时候也这样试试。(仅供参考)** 489 | 490 | ### l、断点续练问题。 491 | **问:我已经训练过几个世代了,能不能从这个基础上继续开始训练 492 | 答:可以,你在训练前,和载入预训练权重一样载入训练过的权重就行了。一般训练好的权重会保存在logs文件夹里面,将model_path修改成你要开始的权值的路径即可。** 493 | 494 | ### m、我要训练其它的数据集,预训练权重能不能用? 495 | **问:如果我要训练其它的数据集,预训练权重要怎么办啊?** 496 | **答:数据的预训练权重对不同数据集是通用的,因为特征是通用的,预训练权重对于99%的情况都必须要用,不用的话权值太过随机,特征提取效果不明显,网络训练的结果也不会好。** 497 | 498 | ### n、网络如何从0开始训练? 499 | **问:我要怎么不使用预训练权重啊? 500 | 答:看一看注释、大多数代码是model_path = '',Freeze_Train = Fasle**,如果设置model_path无用,**那么把载入预训练权重的代码注释了就行。** 501 | 502 | ### o、为什么从0开始训练效果这么差(修改了网络主干,效果不好怎么办)? 503 | **问:为什么我不使用预训练权重效果这么差啊? 504 | 答:因为随机初始化的权值不好,提取的特征不好,也就导致了模型训练的效果不好,预训练权重还是非常重要的。** 505 | 506 | **问:up,我修改了网络,预训练权重还能用吗? 507 | 答:修改了主干的话,如果不是用的现有的网络,基本上预训练权重是不能用的,要么就自己判断权值里卷积核的shape然后自己匹配,要么只能自己预训练去了;修改了后半部分的话,前半部分的主干部分的预训练权重还是可以用的,如果是pytorch代码的话,需要自己修改一下载入权值的方式,判断shape后载入,如果是keras代码,直接by_name=True,skip_mismatch=True即可。** 508 | 权值匹配的方式可以参考如下: 509 | ```python 510 | # 加快模型训练的效率 511 | print('Loading weights into state dict...') 512 | device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') 513 | model_dict = model.state_dict() 514 | pretrained_dict = torch.load(model_path, map_location=device) 515 | a = {} 516 | for k, v in pretrained_dict.items(): 517 | try: 518 | if np.shape(model_dict[k]) == np.shape(v): 519 | a[k]=v 520 | except: 521 | pass 522 | model_dict.update(a) 523 | model.load_state_dict(model_dict) 524 | print('Finished!') 525 | ``` 526 | 527 | **问:为什么从0开始训练效果这么差(我修改了网络主干,效果不好怎么办)? 528 | 答:一般来讲,网络从0开始的训练效果会很差,因为权值太过随机,特征提取效果不明显,因此非常、非常、非常不建议大家从0开始训练!如果一定要从0开始,可以了解imagenet数据集,首先训练分类模型,获得网络的主干部分权值,分类模型的 主干部分 和该模型通用,基于此进行训练。 529 | 网络修改了主干之后也是同样的问题,随机的权值效果很差。** 530 | 531 | **问:怎么在模型上从0开始训练? 532 | 答:在算力不足与调参能力不足的情况下从0开始训练毫无意义。模型特征提取能力在随机初始化参数的情况下非常差。没有好的参数调节能力和算力,无法使得网络正常收敛。** 533 | 如果一定要从0开始,那么训练的时候请注意几点: 534 | - 不载入预训练权重。 535 | - 不要进行冻结训练,注释冻结模型的代码。 536 | 537 | **问:为什么我不使用预训练权重效果这么差啊? 538 | 答:因为随机初始化的权值不好,提取的特征不好,也就导致了模型训练的效果不好,voc07+12、coco+voc07+12效果都不一样,预训练权重还是非常重要的。** 539 | 540 | ### p、你的权值都是哪里来的? 541 | **问:如果网络不能从0开始训练的话你的权值哪里来的? 542 | 答:有些权值是官方转换过来的,有些权值是自己训练出来的,我用到的主干的imagenet的权值都是官方的。** 543 | 544 | 545 | ### q、视频检测与摄像头检测 546 | **问:怎么用摄像头检测呀? 547 | 答:predict.py修改参数可以进行摄像头检测,也有视频详细解释了摄像头检测的思路。** 548 | 549 | **问:怎么用视频检测呀? 550 | 答:同上** 551 | 552 | ### r、如何保存检测出的图片 553 | **问:检测完的图片怎么保存? 554 | 答:一般目标检测用的是Image,所以查询一下PIL库的Image如何进行保存。详细看看predict.py文件的注释。** 555 | 556 | **问:怎么用视频保存呀? 557 | 答:详细看看predict.py文件的注释。** 558 | 559 | ### s、遍历问题 560 | **问:如何对一个文件夹的图片进行遍历? 561 | 答:一般使用os.listdir先找出文件夹里面的所有图片,然后根据predict.py文件里面的执行思路检测图片就行了,详细看看predict.py文件的注释。** 562 | 563 | **问:如何对一个文件夹的图片进行遍历?并且保存。 564 | 答:遍历的话一般使用os.listdir先找出文件夹里面的所有图片,然后根据predict.py文件里面的执行思路检测图片就行了。保存的话一般目标检测用的是Image,所以查询一下PIL库的Image如何进行保存。如果有些库用的是cv2,那就是查一下cv2怎么保存图片。详细看看predict.py文件的注释。** 565 | 566 | ### t、路径问题(No such file or directory、StopIteration: [Errno 13] Permission denied: 'XXXXXX') 567 | **问:我怎么出现了这样的错误呀:** 568 | ```python 569 | FileNotFoundError: 【Errno 2】 No such file or directory 570 | StopIteration: [Errno 13] Permission denied: 'D:\\Study\\Collection\\Dataset\\VOC07+12+test\\VOCdevkit/VOC2007' 571 | …………………………………… 572 | …………………………………… 573 | ``` 574 | **答:去检查一下文件夹路径,查看是否有对应文件;并且检查一下2007_train.txt,其中文件路径是否有错。** 575 | 关于路径有几个重要的点: 576 | **文件夹名称中一定不要有空格。 577 | 注意相对路径和绝对路径。 578 | 多百度路径相关的知识。** 579 | 580 | **所有的路径问题基本上都是根目录问题,好好查一下相对目录的概念!** 581 | ### u、和原版比较问题,你怎么和原版不一样啊? 582 | **问:原版的代码是XXX,为什么你的代码是XXX? 583 | 答:是啊……这要不怎么说我不是原版呢……** 584 | 585 | **问:你这个代码和原版比怎么样,可以达到原版的效果么? 586 | 答:基本上可以达到,我都用voc数据测过,我没有好显卡,没有能力在coco上测试与训练。** 587 | 588 | ### v、我的检测速度是xxx正常吗?我的检测速度还能增快吗? 589 | **问:你这个FPS可以到达多少,可以到 XX FPS么? 590 | 答:FPS和机子的配置有关,配置高就快,配置低就慢。** 591 | 592 | **问:我的检测速度是xxx正常吗?我的检测速度还能增快吗? 593 | 答:看配置,配置好速度就快,如果想要配置不变的情况下加快速度,就要修改网络了。** 594 | 595 | **问:为什么论文中说速度可以达到XX,但是这里却没有? 596 | 答:检查是否正确安装了tensorflow-gpu或者pytorch的gpu版本,如果已经正确安装,可以去利用time.time()的方法查看detect_image里面,哪一段代码耗时更长(不仅只有网络耗时长,其它处理部分也会耗时,如绘图等)。有些论文还会使用多batch进行预测,我并没有去实现这个部分。** 597 | 598 | ### w、预测图片不显示问题 599 | **问:为什么你的代码在预测完成后不显示图片?只是在命令行告诉我有什么目标。 600 | 答:给系统安装一个图片查看器就行了。** 601 | 602 | ### x、算法评价问题(miou) 603 | **问:怎么计算miou? 604 | 答:参考视频里的miou测量部分。** 605 | 606 | **问:怎么计算Recall、Precision指标。 607 | 答:现有的代码还无法获得,需要各位同学理解一下混淆矩阵的概念,然后自行计算一下。** 608 | 609 | ### y、UP,怎么优化模型啊?我想提升效果 610 | **问:up,怎么修改模型啊,我想发个小论文! 611 | 答:建议目标检测中的yolov4论文,作为一个大型调参现场非常有参考意义,使用了很多tricks。我能给的建议就是多看一些经典模型,然后拆解里面的亮点结构并使用。** 612 | 613 | ### z、部署问题(ONNX、TensorRT等) 614 | 我没有具体部署到手机等设备上过,所以很多部署问题我并不了解…… 615 | 616 | ## 5、交流群问题 617 | **问:up,有没有QQ群啥的呢? 618 | 答:没有没有,我没有时间管理QQ群……** 619 | 620 | ## 6、怎么学习的问题 621 | **问:up,你的学习路线怎么样的?我是个小白我要怎么学? 622 | 答:这里有几点需要注意哈 623 | 1、我不是高手,很多东西我也不会,我的学习路线也不一定适用所有人。 624 | 2、我实验室不做深度学习,所以我很多东西都是自学,自己摸索,正确与否我也不知道。 625 | 3、我个人觉得学习更靠自学** 626 | 学习路线的话,我是先学习了莫烦的python教程,从tensorflow、keras、pytorch入门,入门完之后学的SSD,YOLO,然后了解了很多经典的卷积网,后面就开始学很多不同的代码了,我的学习方法就是一行一行的看,了解整个代码的执行流程,特征层的shape变化等,花了很多时间也没有什么捷径,就是要花时间吧。 627 | --------------------------------------------------------------------------------