Caffe训练数据转换实战:一键生成LMDB数据库与避坑指南 简介一套面向Caffe用户的图片转LMDB格式脚本更新包针对多分类超过10类数据集做了专门优化适合正在训练CNN模型、需要快速准备数据的中高级开发者无论单机调试还是批量实验都能明显节省预处理时间。压缩包共13个文件以5个shell脚本为核心覆盖生成train.txt/test.txt、调用转换工具完成LMDB格式转换、均值计算等关键步骤另含3个txt文本列表、2个运行日志和1份说明文档方便核对图片路径与标签、检查转换进度与报错信息。整个资源仅40KB轻量便捷目前已有354人学习下载。通过这套脚本读者可修改路径后直接运行一键生成训练与测试列表并完成图像集到LMDB格式的转换免去手动拼命令的繁琐日志还能辅助排查转换中的异常尤其适合需频繁处理大规模、多类别图像数据的实验与工程场景。1. Caffe 训练前的第一道坎一键把图片变成 lmdb 到底解决了什么Caffe 训练任何图像模型第一步都要面对同一个问题把图片变成 lmdb。这个格式在你没接触过时会觉得神秘但它本质上只是给数据集做了一层“索引 序列化”的包装。真正麻烦的是当你想自己动手时train.txt 和 test.txt 要怎么写、标签怎么填、路径要相对还是绝对、用工具转完会不会悄悄漏掉图片。本文拆一个直接运行版脚本让你把图片目录放对位置、改好几个路径变量就能一次性跑出两个 lmdb 数据库训练测试的列表文件也顺带生成。适合正在复现旧模型、维护遗留 Caffe 工程或者只是急着用已有图片验证一个网络结构的你。2. LMDB 与 convert_imageset先搞懂数据是怎么被“塞”进去的2.1 一张图在 LMDB 里是什么Datum 就是那个标准信封LMDBLightning Memory-Mapped Database本质上是一个内存映射数据库Caffe 用它把图像和标签打包成一个单文件库。数据库里的每条记录key 是一个整数索引value 是一个序列化之后的 Datum protobuf。Datum 的定义大致长这样message Datum { optional int32 channels 1; optional int32 height 2; optional int32 width 3; optional bytes data 4; // 图像像素或者编码后的图像字节 optional int32 label 5; // 单标签任务的标签 repeated float float_data 6; // 多标签或者特征向量用这个字段 optional bool encoded 7; // 是否以jpg/png编码存储 }也就是说一张输入图片在转换阶段会被解码成一维像素数组放进 data 字段标签填进 label 字段。Caffe 的数据层读取时再把 Datum 反序列化并 reshape 成 channel × height × width 的张量喂给网络。我经常被问到一个问题为什么不直接在训练时读 jpg 文件当然可以Caffe 也有 ImageData 层但它的缺陷很明显每次迭代都要做磁盘 I/O 和解码遇到上万张图就开始拖慢节奏。LMDB 是内存映射读取数据被操作系统直接映射到内存地址空间读起来快得多。加上 Caffe 数据层本身有预取线程LMDB 是它的默认最优搭档。2.2 convert_imageset 的每个 flag 到底管什么转换工具就是 Caffe 编译产物里的convert_imageset用法很固定convert_imageset [FLAGS] ROOTFOLDER/ LISTFILE DB_NAME其中 ROOTFOLDER 是图片根目录LISTFILE 是 train.txt 或 test.txtDB_NAME 是输出 lmdb 的路径。我在脚本里一般都显式把这些参数写上不把命运交给默认值。常用 flag 整理成下面这张表参数作用默认值实际建议--resize_height缩放图像高度0 不缩放配合网络输入尺寸设置--resize_width缩放图像宽度0 不缩放同上一般两个一起设置--shuffle写库前随机打乱false训练集建议开启--gray转成单通道灰度false输入是灰度图时开启--backendleveldb 或 lmdb版本不同默认值不同直接写死 lmdb--encoded以 JPEG/PNG 编码存放false图片多、磁盘紧张时开启--encode_type编码类型jpg一般不用改--check_size检查所有图片尺寸一致false数据不规整时先开着这里面最值得注意的坑是默认值的版本差异。老版本 Caffe 的--backend默认可能是 leveldb新版本又变成 lmdb所以脚本里我会直接写--backendlmdb避免在不同机器上行为不一致。还有--encoded默认不开启时LMDB 里存放的是解码后的 BGRA 像素一张 256×256 的图就要占 256KB 左右数据量大了磁盘直接失控。你在任何一台机器上拿到新的 convert_imageset第一件事都是先执行一次convert_imageset -h把它实际支持的参数和默认值看清楚。3. 一键直接运行版从图片目录到 train.txt / test.txt 再到两个 lmdb3.1 动手前的目录和工具准备脚本能直接跑的前提是图片目录结构固定。常见的做法是按类别分子目录一个目录一类图myimages/ ├── cat/ │ ├── cat_001.jpg │ ├── cat_002.jpg │ └── cat_003.png ├── dog/ │ ├── dog_001.jpg │ └── dog_002.jpg标签会按子目录名的字典序自动编号cat 是 0dog 是 1。这种结构的好处是脚本不用你去手工维护类别和标签的对应关系多一个目录就多一类标签跟着目录走。动手之前先确认两件事第一Caffe 已经编译完成convert_imageset存在第二图片目录路径里不要有中文和空格。很多版本对中文路径的兼容性不好而路径带空格会让 txt 里路径 标签的解析直接断掉。为省掉后面所有麻烦建议所有数据路径使用纯英文和数字。3.2 直接运行版脚本与逐段逻辑下面这个脚本是核心。它做三件事扫描目录生成带路径和标签的样本列表、按比例随机划分训练测试集、分别调用 convert_imageset 生成两个 lmdb。#!/bin/bash # # 一键生成 train.txt/test.txt 并转为 lmdb # 用法: bash generate_lmdb.sh # # ---------- 路径配置区 ---------- DATA_ROOT/home/user/myimages # 图片根目录 CAFFE_TOOL/home/user/caffe/build/tools/convert_imageset OUTPUT_DIR/home/user/myimages_lmdb # 输出目录 TRAIN_RATIO0.8 # 训练集占比 RESIZE_H256 RESIZE_W256 # --------------------------------- mkdir -p $OUTPUT_DIR # 1) 遍历子目录生成 train.txt 和 test.txt python3 - $DATA_ROOT $OUTPUT_DIR $TRAIN_RATIO PYEOF import os import sys import random root, out_dir, ratio sys.argv[1], sys.argv[2], float(sys.argv[3]) classes sorted([d for d in os.listdir(root) if os.path.isdir(os.path.join(root, d))]) samples [] for label, cls in enumerate(classes): cls_dir os.path.join(root, cls) for f in sorted(os.listdir(cls_dir)): if f.lower().endswith((.jpg, .jpeg, .png)): samples.append((os.path.join(cls, f), label)) if not samples: sys.exit(没有找到任何图片请检查 DATA_ROOT 目录结构) random.seed(42) random.shuffle(samples) n_train int(len(samples) * ratio) for txt_name, part in [(train.txt, samples[:n_train]), (test.txt, samples[n_train:])]: if not part: print(f警告: {txt_name} 为空不写入) continue with open(os.path.join(out_dir, txt_name), w) as fp: for rel_path, label in part: fp.write(f{rel_path} {label}\n) print(f已生成 {txt_name}: {len(part)} 条样本) PYEOF # 2) 调用 convert_imageset 生成训练集和测试集 lmdb for subset in train test; do if [ ! -f $OUTPUT_DIR/${subset}.txt ]; then echo 跳过 $subset${subset}.txt 不存在 continue fi $CAFFE_TOOL \ --resize_height$RESIZE_H \ --resize_width$RESIZE_W \ --shuffle \ --backendlmdb \ $DATA_ROOT \ $OUTPUT_DIR/${subset}.txt \ $OUTPUT_DIR/${subset}_lmdb done echo 全部完成检查 $OUTPUT_DIR 目录脚本里最关键的一点是 txt 中的路径用相对路径比如cat/cat_001.jpg 0。convert_imageset 拿到相对路径后会用第一个参数 DATA_ROOT 去拼接完整路径。如果在 txt 里写绝对路径工具一旦换了运行目录或者从别的地方迁移数据马上就会翻车。用相对路径后整个数据集只要带着 DATA_ROOT 一起移动脚本就不用改。random.seed(42)这行有意保留它保证每次运行划分结果一致。对需要复现实验结果的人来说随机划分的可复现性与模型参数的可复现性同等重要。另外脚本里 txt 生成时已经 shuffle 过一次convert_imageset 时又带上了--shuffle相当于打了两次乱序这不会改变样本集只是让写库顺序更随机训练时 batch 的类别分布更均匀。3.3 运行后如何确认 txt 和 lmdb 都生成正确运行完先别急着训练花两分钟看一眼产出bash generate_lmdb.sh # 查看生成的文件列表 ls -lh /home/user/myimages_lmdb/ # 看 train.txt 的前几行确认路径和标签格式 head -5 /home/user/myimages_lmdb/train.txt输出目录下应该出现train_lmdb和test_lmdb两个子目录里面各有一个data.mdb和lock.mdb。data.mdb 是实际数据文件lock.mdb 是数据库锁文件不用理会。train.txt 里的内容应该是类似下面这种格式cat/cat_003.jpg 0 dog/dog_002.jpg 1 cat/cat_001.jpg 0每一行都是“相对路径 空格 整数标签”。只要格式一致convert_imageset 就能正常解析。如果这一步输出不对往往是图片目录结构或者扩展名过滤条件出了问题往下看排查章节。4. 转换避坑与排查4 个高频翻车点4.1 “No valid image files found”却明明有图片现象convert_imageset 刚启动就报No valid image files found退出码非 0txt 目录和图片都在肉眼怎么看都找得到文件。原因这个报错的本质是 txt 里的路径在工具的拼接规则下找不到文件。绝大多数情况是 txt 里写成了绝对路径而 convert_imageset 会拿 DATA_ROOT 直接拼接 txt 里的路径/home/user/myimages拼上/home/user/myimages/cat/cat_001.jpg路径翻倍自然失败。另一种常见原因是 txt 里的路径用了反斜杠分隔符这是从 Windows 上直接拷贝 txt 到 Linux 跑出来的典型症状。还有一类是图片扩展名全大写.JPG但过滤脚本只认小写导致样本列表为空。解决统一用相对路径分隔符全部换成正斜杠/。在生成 txt 的代码里强制做一次字符串替换把\换成/扩展名判断时统一转成小写再比较。这样无论数据从哪个平台来生成结果都是规范的。提示如果数据已经在 txt 里写错了不必重新跑整个生成流程直接用 sed 批量替换即可但治标不治本建议还是回到脚本里把路径规范化逻辑补上。4.2 标签从 1 开始分类器越训越不对现象模型训练损失在下降但每一轮测试精度都乱跳最终 accuracy 始终上不去。仔细看数据会发现类别数量是 5但标签值里有 5 出现。原因很多人习惯从 1 开始给人眼标注类别编号觉得第一类就应该是 1。但 Caffe 的 SoftmaxWithLoss 层期望的标签是从 0 开始计数的整数。标签从 1 开始时类别索引会整体向后偏移一位网络会认为有一个根本不存在的第 6 类。训练时 loss 看似正常实际是模型在强行拟合一个错位的映射关系。解决标签从 0 开始且编号与类别目录的字典序严格对应。脚本里用enumerate(classes)天然就是从 0 编号的不要在后续处理中再对标签做1操作。如果你是从标注文件转过来的数据先把标签读出来看看最大值是否等于类别数减 1这一步排查能省掉后面好几天的时间。4.3 一个 lmdb 撑满磁盘体积失控式膨胀现象转换完成后的 data.mdb 比原始图片总大小大出一个数量级。原本 500MB 的 jpg 数据集转完 lmdb 变成 6GB磁盘直接被塞满。原因默认情况下 convert_imageset 会把图像解码成未压缩的 BGRA 像素放进 lmdb。256×256 的图就是 256KB如果原图是 1920×1080一层就要占 8MB几十万张图叠加起来非常恐怖。很多人忽略这个机制看到磁盘撑爆第一反应是“是不是有死循环”其实只是没开编码存储。解决转换时加上--encoded参数让 lmdb 直接存放原始 JPEG 字节体积能回到和原图相近的量级。代价是训练时数据层读取后需要先解码再送进网络会多占一些 CPU。如果你的 GPU 利用率经常因为 CPU 解码跟不上而掉下来建议用 Im2 等层配合取舍。我的习惯是数据集图片总量在 5 万张以下且磁盘不紧张时不开启 encoded训练速度更稳图片量巨大时用 encoded 换磁盘。4.4 训练起步就报 crop 尺寸或 channel 不匹配现象训练启动后第一个 iteration 就报错日志里出现类似crop_size must be datum_height或者channel mismatch的提示。原因lmdb 里存储的图像尺寸小于网络数据层设置的 crop_size。比如网络要随机裁剪 227×227但转换时 resize_height 和 resize_width 只设成 100裁剪范围直接溢出。另一个常见原因是通道数不一致数据中有灰度图没有开启--gray结果有的样本是单通道有的三通道数据层无法统一 reshape。解决转换前先确认网络输入尺寸resize 的宽高至少要和 crop_size 保持一致最稳妥的做法是让 resize 尺寸大于 crop_size。像 AlexNet 这种输入 227×227 的网络我一般 resize 到 256×256留出裁剪空间。灰度图在转换时统一加--graytrue不要混合多种通道数的图片进入同一个 lmdb。注意不同尺寸图片混在一个 lmdb 里有时候并不会在转换时报错而是在训练的某个 epoch 才爆出来这种问题最难定位。数据来源杂乱时转换阶段加上--check_sizetrue先跑一轮能提前发现不齐的样本。5. 进阶跑通之后我还会再做这三件事5.1 用 Python 接口抽查 lmdb 内容验证标签和尺寸脚本跑通不等于数据正确。视觉任务里最常见的就是类别混进一个小文件里或者某类图片数量异常少。用一小段 Python 把 lmdb 打开抽样能直观看到标签分布和图像尺寸import lmdb import caffe.proto.caffe_pb2 as caffe_pb2 def inspect_lmdb(lmdb_path, max_items10): env lmdb.open(lmdb_path, readonlyTrue, lockFalse) with env.begin(buffersTrue) as txn: cur txn.cursor() count 0 for key, value in cur: datum caffe_pb2.Datum() datum.ParseFromString(value) print(fkey{key.decode()} label{datum.label} fshape{datum.channels}x{datum.height}x{datum.width} fencoded{datum.encoded}) count 1 if count max_items: break env.close() inspect_lmdb(/home/user/myimages_lmdb/train_lmdb)这段代码打印出样本索引、标签、通道数、高度宽度和是否编码存储。走一遍之后花不了几分钟却能确认很多事标签是否从 0 连续编号、通道数是否符合预期、图像尺寸有没有被 normalize 到一个合理范围。我接手过的遗留工程里有不少训练曲线异常的问题最终都定位在数据层而不是网络结构。5.2 顺手把 mean.binaryproto 也算了Caffe 训练前一般都要减去均值这步在数据转换之后做最合适因为它可以直接拿 lmdb 作为输入/home/user/caffe/build/tools/compute_image_mean \ /home/user/myimages_lmdb/train_lmdb \ /home/user/myimages_lmdb/mean.binaryproto生成的是 protobuf 格式的均值文件在网络的 transform_param 里直接引用即可。注意 convert_imageset 基于 OpenCV 解码存储顺序是 BGR所以算出来的 mean 也是 BGR 顺序。用预训练模型时如果模型本来是用 RGB 均值跑的通道顺序对不上会让效果明显变差这一点靠肉眼观察不出来。5.3 数据增强不要一股脑塞进 lmdb旋转、翻转、色彩抖动这些增强操作我一般不建议写进转换脚本里提前生成。原因很直接lmdb 是训练时的读入源把增强后的副本存进 lmdb数据集容量会膨胀若干倍而且增强方式固定后后期想调整策略还得重新转换数据。Caffe 数据层自带的 random_crop、mirror 已经覆盖了大部分常见需求在线增强不占额外磁盘调整起来只是改一个网络配置文件的事。我的习惯是准备一个专门的数据预处理脚本保持唯一职责把原始图片归一化成网络可读的格式不做任何增强。增强交给数据层配置标签清洗交给独立的数据检查脚本。这样多迭代几次整个流程变得越来越稳定换数据、换网络、换机器都能快速迁移。希望这套直接转换的思路能帮你少走弯路把时间留在真正需要调的网络结构和训练策略上。本文还有配套的精品资源点击获取