1. 项目缘起与整体设计思路1.1 为什么还要折腾图片分类图片分类这件事听起来像是深度学习里的Hello World但真到了要落地的时候坑一点都不少。我最近在做一个工业质检方向的小项目需要把流水线上的零件图分成合格划痕缺角三类样本量不大标注成本却很高。选来选去最后还是回到了darknet这套框架上。原因很直接darknet的图片分类classifier模块足够轻编译完就是一个可执行文件没有一堆依赖要装推理速度在CPU上都能接受。更重要的是它的数据加载逻辑非常朴素没有那么多花里胡哨的封装出了问题能一眼看到底。对于我这种需要快速验证、快速迭代的场景来说这种透明感比什么都重要。这篇文章想聊的核心就是训练和预测图片分类时数据加载这一块到底该怎么理解、怎么调、怎么避坑。很多人跑分类任务模型结构抄一抄就完事了结果训练loss不降、预测结果乱跳最后发现是数据加载环节出了问题。我会把darknet分类任务里数据加载的完整链路拆开讲包括数据格式、加载流程、参数配置、常见报错以及我在实际项目里踩过的坑。适合谁看如果你已经跑通过darknet的检测任务想试试分类或者你正在用darknet做分类但训练效果总是不对劲再或者你只是想找一个轻量级的分类方案做快速验证那这篇内容应该能帮到你。不需要你有很深的深度学习背景但至少要能看懂基本的命令行操作和配置文件。1.2 整体方案选型的几个考量在动手之前我先说一下为什么选darknet而不是其他框架。这不是说darknet有多好而是它在特定场景下确实合适。第一部署简单。darknet编译出来就是一个二进制文件加上几个动态库扔到目标机器上就能跑。不像某些框架部署的时候还要考虑Python版本、CUDA版本、各种依赖包的兼容性。对于工业现场那种能跑就行、别给我整幺蛾子的环境这一点太关键了。第二数据加载逻辑清晰。darknet分类任务的数据加载核心就是读一个list文件然后按行加载图片。没有复杂的Dataset抽象没有多进程预取的花活就是老老实实一张一张读。这种设计在数据量不大的时候完全够用而且调试起来非常直观——你甚至可以在源码里加一行printf就能看到每张图是怎么被读进来的。第三训练和预测的代码路径一致。darknet在训练和预测时数据预处理的方式是统一的都是走load_data这一套。这意味着你在训练时看到的图片处理效果预测时也是一样的不会出现训练时正常、预测时翻车的情况。这一点在我之前用其他框架的时候经常出问题darknet反而没这个毛病。当然darknet也有它的局限。比如它不支持复杂的数据增强策略多GPU训练的支持也比较粗糙。但对于我这种小样本、快速验证的场景来说这些都不是问题。选型这件事从来都是看场景的没有银弹。2. 数据加载的核心细节与实操要点2.1 数据格式list文件才是关键darknet分类任务的数据加载核心就是一个list文件。这个文件长什么样其实非常简单每一行就是一张图片的路径后面跟一个类别编号。比如/data/train/part_001.jpg 0 /data/train/part_002.jpg 1 /data/train/part_003.jpg 2 /data/train/part_004.jpg 0就这么简单。类别编号从0开始依次递增。这里有几个细节需要注意第一路径问题。list文件里的路径可以是绝对路径也可以是相对路径。如果是相对路径它是相对于你运行darknet命令时的工作目录而不是相对于list文件所在的目录。这一点非常容易搞错。我建议直接用绝对路径省得后面出问题找不到原因。第二类别编号的连续性。darknet在加载数据时会根据list文件里出现的最大类别编号来确定类别总数。如果你的类别编号是0、1、2那类别数就是3。但如果你的编号是0、2、5那darknet会认为你有6个类别中间那些没有样本的类别会导致训练出问题。所以一定要保证类别编号从0开始连续。第三文件编码和换行符。这个问题在Windows上编辑list文件时特别常见。如果list文件是Windows格式的换行符\r\ndarknet在读取时可能会把\r也读进去导致路径解析错误。解决办法很简单用dos2unix转一下或者在Linux下用sed处理一下。提示list文件里不要有空行也不要有注释行。darknet不会跳过空行遇到空行会直接报错或者读入奇怪的路径。2.2 数据加载流程从list到batchdarknet分类任务的数据加载流程可以拆成几个步骤。理解了这个流程你就能明白为什么有些参数会影响训练效果。第一步读取list文件。darknet会把整个list文件读进内存每一行解析成图片路径和类别编号。这一步是在训练开始前一次性完成的所以list文件不要太大否则会占用大量内存。不过对于分类任务来说list文件本身不大这个问题不突出。第二步打乱顺序。darknet在训练时会打乱数据顺序这个打乱是在每个epoch开始时进行的。打乱的逻辑比较简单就是随机交换。这里有一个细节darknet的打乱是在list文件层面进行的也就是说它打乱的是图片的顺序而不是图片和标签的对应关系。这个逻辑是对的但如果你自己改了源码要注意别把这个关系搞反了。第三步加载图片。这是最核心的一步。darknet会根据list文件里的路径一张一张读取图片。读取的时候会做几件事解码图片、调整大小、归一化。调整大小的尺寸由配置文件里的width和height决定。归一化就是把像素值从0-255缩放到0-1之间。第四步组成batch。darknet会把加载好的图片组成一个batch然后送给网络训练。batch的大小由配置文件里的batch参数决定。这里有一个坑darknet的batch是真batch也就是说它会等一个batch的图片全部加载完才开始训练。如果某张图片加载特别慢整个batch都会被拖慢。2.3 配置文件里的关键参数darknet分类任务的配置文件通常是.cfg文件里有几个参数直接关系到数据加载和训练效果。我逐个说一下。batch和subdivisions。这两个参数决定了每次送给网络的图片数量。实际batch大小是batch / subdivisions。比如batch64、subdivisions8那实际每次送8张图。这个设计是为了在显存有限的情况下模拟更大的batch效果。但要注意subdivisions越大梯度累积的次数越多训练速度会变慢。width和height。这两个参数决定了输入图片的尺寸。darknet会把所有图片都调整到这个尺寸。这里有一个关键点调整尺寸的方式是直接resize不是裁剪。也就是说如果你的原始图片是长方形resize成正方形后图片会被拉伸变形。这一点在分类任务里影响很大尤其是当物体的形状是重要特征时。我的做法是在准备数据时就把图片处理成接近正方形的比例或者在配置文件里把width和height设置成和原始图片比例接近的值。learning_rate和policy。这两个参数虽然不直接管数据加载但它们和batch大小是联动的。batch越大学习率通常也要相应调大。darknet默认的learning_rate是0.001这个值在batch64时比较合适。如果你把batch调小了学习率也要相应调小否则训练容易震荡。max_batches。这个参数决定了训练多少轮。darknet的分类任务里通常建议设置成2000 * 类别数。比如3个类别就设成6000。但这个不是绝对的还要看你的数据量和收敛情况。2.4 数据增强darknet能做什么、不能做什么darknet分类任务的数据增强能力比较有限。它支持的主要是随机裁剪、随机翻转、调整亮度对比度这几种。这些增强是在加载图片时实时进行的不需要你提前处理数据。但darknet不支持复杂的增强策略比如CutMix、MixUp、RandAugment这些。如果你需要这些要么自己改源码要么在准备数据时离线做增强。我的建议是对于小样本任务离线增强更可控。你可以用Python脚本把原始图片做各种变换生成多份副本然后把这些副本都写进list文件。这样虽然会增加磁盘占用但训练时的行为更可预测。注意darknet的随机裁剪是在resize之后进行的也就是说它先把图片resize到配置的尺寸然后再随机裁剪。这个顺序和很多框架不一样需要注意。3. 实操过程与核心环节实现3.1 数据准备从原始图片到list文件假设你有一批原始图片放在/data/raw目录下分成三个子目录ok、scratch、broken。你需要生成一个list文件供darknet训练使用。第一步整理目录结构。我建议把图片按类别放到不同的子目录里这样生成list文件时不容易搞错。目录结构大概是这样/data/raw/ ok/ img_001.jpg img_002.jpg ... scratch/ img_001.jpg img_002.jpg ... broken/ img_001.jpg img_002.jpg ...第二步生成list文件。可以用一个简单的shell脚本#!/bin/bash DATA_DIR/data/raw LIST_FILE/data/train.list $LIST_FILE for class_name in ok scratch broken; do case $class_name in ok) class_id0 ;; scratch) class_id1 ;; broken) class_id2 ;; esac for img in $DATA_DIR/$class_name/*.jpg; do echo $img $class_id $LIST_FILE done done这个脚本会遍历每个类别目录把图片路径和对应的类别编号写入list文件。注意这里用的是绝对路径避免后面出问题。第三步检查list文件。生成完之后一定要检查一下。可以用wc -l看看行数对不对用head和tail看看内容格式对不对。我还会随机抽几行确认图片路径是真实存在的。wc -l /data/train.list head -5 /data/train.list tail -5 /data/train.list第四步划分训练集和验证集。darknet本身不提供自动划分的功能你需要自己分。我的做法是在生成list文件时随机抽10%作为验证集写到另一个文件里。注意验证集的图片不要出现在训练集里否则评估结果会虚高。3.2 配置文件从零开始写一个分类cfgdarknet的分类配置文件可以从官方提供的模板改起。我以classifier.cfg为例说一下关键部分。[net] batch64 subdivisions8 width224 height224 channels3 max_crop256 learning_rate0.001 policypoly power4 max_batches6000 [convolutional] batch_normalize1 filters32 size3 stride1 pad1 activationleaky [maxpool] size2 stride2 # ... 中间省略若干层 ... [avgpool] [softmax] groups1 [cost] typesse这里有几个地方需要根据你的任务调整width和height。我设的是224x224这是一个比较通用的尺寸。如果你的图片本身很小比如128x128那就设成128。如果图片很大比如512x512那设成224会丢失很多细节需要权衡。max_crop。这个参数决定了随机裁剪的最大尺寸。通常设成比width和height大一些比如256。这样在训练时darknet会先resize到256然后随机裁剪到224。这个操作相当于一种数据增强。max_batches。我设的是6000对应3个类别。这个值不是固定的你可以根据训练loss的变化来调整。如果loss还在降就继续训练如果loss已经平稳了就可以停了。learning_rate和policy。我用的poly策略学习率会随着训练轮数逐渐下降。这个策略在小样本任务上表现比较稳定。如果你用的是step策略需要额外设置steps和scales参数。3.3 训练命令与参数解析darknet分类任务的训练命令基本格式是这样的./darknet classifier train /data/train.list /data/classifier.cfg这个命令会启动训练并在终端输出训练日志。日志里会显示当前的loss、学习率、训练轮数等信息。如果你想从之前的权重继续训练可以加上权重文件./darknet classifier train /data/train.list /data/classifier.cfg /data/backup/classifier_1000.weights训练过程中darknet会定期保存权重文件默认保存在backup目录下。保存的频率由配置文件里的save_every参数决定默认是1000轮保存一次。我建议设成500这样万一训练崩了损失小一点。提示训练时最好把日志重定向到文件方便后面分析。可以用tee命令同时输出到终端和文件./darknet classifier train /data/train.list /data/classifier.cfg 21 | tee train.log3.4 预测命令与结果解读训练完之后就可以用训练好的权重做预测了。预测命令的格式是./darknet classifier predict /data/classifier.cfg /data/backup/classifier_final.weights /data/test/img_001.jpg这个命令会输出图片属于每个类别的概率。比如ok: 0.95 scratch: 0.03 broken: 0.02这表示模型认为这张图有95%的概率是合格。如果你想批量预测可以写一个简单的shell脚本#!/bin/bash for img in /data/test/*.jpg; do echo Processing $img ./darknet classifier predict /data/classifier.cfg /data/backup/classifier_final.weights $img done这里有一个细节darknet的预测命令每次只能处理一张图不支持批量。如果你要预测很多图这样效率会比较低。我的做法是把预测逻辑写成一个循环或者自己改源码支持批量预测。3.5 训练和预测的数据加载差异虽然darknet在训练和预测时都走load_data这一套但还是有一些差异需要注意。第一训练时会做数据增强预测时不会。训练时的随机裁剪、翻转等操作在预测时都会关闭。这意味着预测时看到的图片处理效果和训练时是不一样的。这一点在评估模型时要注意最好用和预测时一致的处理方式来做验证。第二训练时会打乱顺序预测时不会。这个差异不影响结果但会影响你观察数据的顺序。如果你在调试时想按顺序看每张图的预测结果要注意这一点。第三训练时的batch是动态的预测时是单张。训练时一个batch有多张图预测时一次只有一张。这个差异会导致训练和预测时的内存占用、计算效率不一样。如果你在训练时显存刚好够用预测时一般不会有问题。4. 常见问题与排查技巧实录4.1 训练loss不降先查数据加载训练loss不降是最常见的问题。很多人第一反应是模型结构不对、学习率不对但我的经验是先查数据加载。排查步骤一确认list文件里的路径都能访问。写一个脚本逐行读取list文件检查每张图片是否存在while read line; do img_path$(echo $line | awk {print $1}) if [ ! -f $img_path ]; then echo Missing: $img_path fi done /data/train.list如果有缺失的图片darknet在加载时会报错或者跳过导致实际参与训练的样本变少。排查步骤二确认类别编号正确。用awk统计一下每个类别的样本数awk {print $2} /data/train.list | sort | uniq -c如果某个类别的样本数特别少或者类别编号不连续都会导致训练出问题。排查步骤三确认图片能正常解码。有些图片虽然文件存在但可能损坏了darknet读取时会失败。可以用file命令检查一下图片格式file /data/raw/ok/img_001.jpg如果输出不是JPEG或PNG说明图片格式有问题。4.2 预测结果乱跳检查归一化和尺寸预测结果乱跳比如同一张图多次预测结果差异很大或者明显错误的分类通常和数据处理有关。问题一归一化不一致。darknet在训练时会把像素值归一化到0-1预测时也是一样。但如果你自己做了预处理比如把图片保存成归一化后的格式那darknet再归一化一次就会出问题。解决办法是保持原始图片格式让darknet自己处理归一化。问题二尺寸不一致。训练时用的width和height预测时也要用同样的值。如果你在预测时改了配置文件里的尺寸结果肯定不对。我的做法是把训练时的配置文件复制一份专门用于预测避免搞混。问题三图片通道顺序。darknet默认使用RGB顺序。如果你的图片是BGR顺序比如用OpenCV读取后保存的预测结果会出问题。解决办法是在保存图片时确认通道顺序或者用工具转换一下。4.3 训练速度慢优化数据加载训练速度慢很多时候不是模型的问题而是数据加载拖了后腿。优化一把图片放到SSD上。这个是最直接的。机械硬盘的随机读取速度慢加载大量小文件时尤其明显。如果条件允许把训练数据放到SSD上速度会有明显提升。优化二减少图片尺寸。如果原始图片很大比如4000x3000加载和解码都会很慢。可以在准备数据时就把图片缩小到接近训练尺寸比如256x256。这样darknet加载时就不用做太大的resize速度会快很多。优化三调整subdivisions。subdivisions越大每个batch加载的图片越少内存占用越低但训练速度会变慢。如果显存够用可以适当减小subdivisions提高训练速度。优化四使用更快的图片格式。JPEG的解码速度比PNG快如果图片质量要求不高可以统一转成JPEG。4.4 常见问题速查表问题现象可能原因排查方法解决办法训练loss不降list文件路径错误检查图片是否存在修正list文件路径训练loss不降类别编号不连续统计类别分布重新生成list文件预测结果乱跳归一化不一致检查预处理逻辑保持原始图片格式预测结果乱跳尺寸不一致对比训练和预测配置使用同一份配置文件训练速度慢图片太大检查原始图片尺寸提前缩小图片训练速度慢硬盘读取慢检查数据存放位置迁移到SSD训练报错list文件有空行检查list文件删除空行训练报错图片损坏用file命令检查删除或修复损坏图片4.5 几个容易被忽略的细节细节一list文件的换行符。这个问题我在前面提过但值得再强调一次。如果你在Windows上编辑list文件一定要转成Unix换行符。我遇到过好几次list文件看起来没问题但darknet就是读不对最后发现是换行符的问题。细节二图片文件名里的空格。如果图片文件名里有空格list文件解析时会出问题。darknet用空格分隔路径和类别编号如果路径里有空格它会把路径截断。解决办法是把文件名里的空格替换成下划线。细节三类别编号的顺序。类别编号的顺序会影响预测结果的输出顺序。比如你把ok设为0scratch设为1broken设为2那预测结果里第一行就是ok的概率。如果你后面改了编号顺序预测结果的解读也要跟着改。细节四备份权重文件。darknet训练时会定期保存权重文件但这些文件可能会被覆盖。我建议在训练脚本里加一个步骤把重要的权重文件复制到另一个目录避免意外丢失。细节五验证集的使用。darknet本身不提供自动评估的功能你需要自己写脚本用验证集跑一遍预测统计准确率。这个步骤很重要可以帮你判断模型是否过拟合。5. 一些实操心得与扩展思路5.1 小样本任务的几个技巧小样本任务数据加载的策略和常规任务不太一样。我的经验是宁可多花时间在数据准备上也不要指望模型自己学会。第一个技巧是离线增强。darknet的在线增强能力有限我通常会在准备数据时用Python脚本做一批离线增强包括旋转、缩放、加噪声、调整亮度等。每张原始图片生成5-10张增强后的副本这样训练样本量就上来了。注意增强后的图片要写到新的list文件里不要和原始图片混在一起。第二个技巧是类别平衡。如果某个类别的样本特别少训练时模型会偏向样本多的类别。解决办法是对样本少的类别做更多增强或者在list文件里重复出现提高它的采样概率。第三个技巧是迁移学习。darknet支持加载预训练权重你可以先用一个大数据集训练一个基础模型然后在小样本任务上微调。微调时学习率要调小比如设成0.0001避免破坏预训练学到的特征。5.2 数据加载的扩展思路darknet的数据加载逻辑虽然简单但也不是不能扩展。如果你需要更复杂的功能可以考虑改源码。一个常见的扩展是支持多标签分类。darknet默认是单标签分类每张图只有一个类别。如果你需要多标签可以改load_data函数让它读取多个类别编号然后改损失函数用binary cross entropy代替softmax。另一个扩展是支持自定义数据增强。你可以在load_data里加自己的增强逻辑比如CutMix、MixUp等。这个改动稍微大一点但也不复杂核心就是在图片加载后、归一化前插入你的增强代码。还有一个扩展是支持批量预测。darknet的预测命令一次只能处理一张图如果你要预测大量图片效率很低。可以改classifier_predict函数让它接受一个list文件批量输出预测结果。5.3 最后再分享一个小技巧如果你在训练时发现loss震荡得厉害可以试试减小学习率、增大batch。darknet的默认学习率是0.001这个值在batch64时比较合适。如果你把batch调小了比如设成16那学习率也要相应调小比如设成0.00025。这个比例关系不是绝对的但可以作为一个起点。另外训练时最好把日志保存下来后面分析的时候有用。我通常会用tee命令把日志同时输出到终端和文件然后用grep和awk分析loss的变化趋势。如果loss在某个点突然跳变那可能是数据加载出了问题比如某张图片损坏了或者某个batch里混入了错误类别的样本。这个内容后续还可以这样扩展把darknet的分类任务和检测任务结合起来先做检测定位再做分类细分类别。或者把darknet的模型导出成其他格式部署到移动端。这些方向我后面会陆续整理有兴趣的可以关注一下。
阅读完成 · 觉得有帮助?