☰
Caffe训练数据转换实战:一键生成LMDB数据库与避坑指南
2026/10/13 14:27:54 网站建设 项目流程

简介:一套面向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 就是那个标准信封

LMDB(Lightning 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.txt,DB_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,所以脚本里我会直接写--backend=lmdb,避免在不同机器上行为不一致。还有--encoded,默认不开启时,LMDB 里存放的是解码后的 BGRA 像素,一张 256×256 的图就要占 256KB 左右,数据量大了磁盘直接失控。你在任何一台机器上拿到新的 convert_imageset,第一件事都是先执行一次convert_imageset -h,把它实际支持的参数和默认值看清楚。

3. 一键直接运行版:从图片目录到 train.txt / test.txt 再到两个 lmdb

3.1 动手前的目录和工具准备

脚本能直接跑的前提是图片目录结构固定。常见的做法是按类别分子目录,一个目录一类图:

myimages/ ├── cat/ │ ├── cat_001.jpg │ ├── cat_002.jpg │ └── cat_003.png ├── dog/ │ ├── dog_001.jpg │ └── dog_002.jpg

标签会按子目录名的字典序自动编号,cat 是 0,dog 是 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_RATIO=0.8 # 训练集占比 RESIZE_H=256 RESIZE_W=256 # --------------------------------- 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 \ --backend=lmdb \ "$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,退出码非 0,txt 目录和图片都在,肉眼怎么看都找得到文件。

原因:这个报错的本质是 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,留出裁剪空间。灰度图在转换时统一加--gray=true,不要混合多种通道数的图片进入同一个 lmdb。

注意:不同尺寸图片混在一个 lmdb 里,有时候并不会在转换时报错,而是在训练的某个 epoch 才爆出来,这种问题最难定位。数据来源杂乱时,转换阶段加上--check_size=true先跑一轮,能提前发现不齐的样本。

5. 进阶:跑通之后,我还会再做这三件事

5.1 用 Python 接口抽查 lmdb 内容,验证标签和尺寸

脚本跑通不等于数据正确。视觉任务里最常见的就是类别混进一个小文件里,或者某类图片数量异常少。用一小段 Python 把 lmdb 打开抽样,能直观看到标签分布和图像尺寸:

import lmdb import caffe.proto.caffe_pb2 as caffe_pb2 def inspect_lmdb(lmdb_path, max_items=10): env = lmdb.open(lmdb_path, readonly=True, lock=False) with env.begin(buffers=True) as txn: cur = txn.cursor() count = 0 for key, value in cur: datum = caffe_pb2.Datum() datum.ParseFromString(value) print(f"key={key.decode()} label={datum.label} " f"shape={datum.channels}x{datum.height}x{datum.width} " f"encoded={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 已经覆盖了大部分常见需求,在线增强不占额外磁盘,调整起来只是改一个网络配置文件的事。

我的习惯是准备一个专门的数据预处理脚本,保持唯一职责:把原始图片归一化成网络可读的格式,不做任何增强。增强交给数据层配置,标签清洗交给独立的数据检查脚本。这样多迭代几次,整个流程变得越来越稳定,换数据、换网络、换机器都能快速迁移。

希望这套直接转换的思路能帮你少走弯路,把时间留在真正需要调的网络结构和训练策略上。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询