DINOv3 元数据生成详解
元数据(Metadata)是描述数据集的预处理索引文件,用于加速训练时的数据加载。传统加载# 将整个文件加载到内存data = np.load('entries-TRAIN.npy') # 占用 ~50 MB 内存内存映射# 不占用内存,按需加载data = np.load('entries-TRAIN.npy', mmap_mode='r') # 占用 ~0 MB 内存。
DINOv3 元数据生成详解
📋 目录
什么是元数据
元数据(Metadata)是描述数据集的预处理索引文件,用于加速训练时的数据加载。
元数据包含的信息
| 信息类型 | 说明 | 示例 |
|---|---|---|
| 图像索引 | 图像在数据集中的实际位置 | actual_index: 10026 |
| 类别索引 | 图像所属类别的数字索引 | class_index: 0 |
| 类别 ID | ImageNet 类别标识符 | class_id: "n01440764" |
| 类别名称 | 类别的可读名称 | class_name: "tench" |
为什么需要元数据
1. 加速数据加载 ⚡
没有元数据:
# 每次训练都需要遍历整个目录树
for class_dir in os.listdir('train/'):
for image_file in os.listdir(f'train/{class_dir}/'):
# 解析文件名、查找类别...
有元数据:
# 直接从预处理的 NumPy 数组读取
entries = np.load('entries-TRAIN.npy', mmap_mode='r')
image_info = entries[index] # O(1) 访问
2. 内存映射加载 💾
使用 mmap_mode='r' 可以:
- ✅ 不占用内存(按需加载)
- ✅ 多进程共享(节省内存)
- ✅ 快速随机访问
3. 统一数据格式 📦
- 标准化的数据结构
- 跨平台兼容
- 易于调试和验证
元数据文件列表
训练集 (TRAIN)
<EXTRA>/
├── entries-TRAIN.npy # 所有图像的详细信息
├── class-ids-TRAIN.npy # 类别 ID 列表
└── class-names-TRAIN.npy # 类别名称列表
验证集 (VAL)
<EXTRA>/
├── entries-VAL.npy # 所有图像的详细信息
├── class-ids-VAL.npy # 类别 ID 列表
└── class-names-VAL.npy # 类别名称列表
测试集 (TEST) - 可选
<EXTRA>/
└── entries-TEST.npy # 仅包含图像索引(无标签)
生成过程详解
整体流程
步骤 1: 读取类别标签
输入文件: <ROOT>/labels.txt
格式:
n01440764,tench
n01443537,goldfish
n01484850,great_white_shark
...
代码:
def _load_labels(self, labels_path: str) -> List[Tuple[str, str]]:
labels_full_path = os.path.join(self.root, labels_path)
labels = []
with open(labels_full_path, "r") as f:
reader = csv.reader(f)
for row in reader:
class_id, class_name = row
labels.append((class_id, class_name))
return labels
步骤 2: 扫描图像目录
使用 torchvision.ImageFolder:
from torchvision.datasets import ImageFolder
dataset_root = os.path.join(self.root, split.get_dirname())
dataset = ImageFolder(dataset_root)
ImageFolder 自动:
- 遍历所有子目录
- 识别图像文件
- 分配类别索引
步骤 3: 创建 entries 数组
数据结构:
dtype = np.dtype([
("actual_index", "<u4"), # 图像实际索引 (uint32)
("class_index", "<u4"), # 类别索引 (uint32)
("class_id", f"U{max_len}"), # 类别 ID (Unicode 字符串)
("class_name", f"U{max_len}") # 类别名称 (Unicode 字符串)
])
填充数据:
for index in range(sample_count):
image_full_path, class_index = dataset.samples[index]
image_relpath = os.path.relpath(image_full_path, self.root)
class_id, actual_index = split.parse_image_relpath(image_relpath)
class_name = class_names[class_id]
entries_array[index] = (actual_index, class_index, class_id, class_name)
示例数据:
entries_array[0] = (10026, 0, "n01440764", "tench")
entries_array[1] = (10027, 0, "n01440764", "tench")
...
步骤 4: 保存 entries 文件
np.save('entries-TRAIN.npy', entries_array)
文件大小估算:
- ImageNet-1k 训练集: ~50 MB
- Tiny ImageNet: ~4 MB
步骤 5: 提取类别信息
从 entries 中提取唯一类别:
max_class_index = max(entry["class_index"] for entry in entries_array)
class_count = max_class_index + 1
class_ids_array = np.empty(class_count, dtype=f"U{max_class_id_length}")
class_names_array = np.empty(class_count, dtype=f"U{max_class_name_length}")
for entry in entries_array:
class_index = entry["class_index"]
class_ids_array[class_index] = entry["class_id"]
class_names_array[class_index] = entry["class_name"]
步骤 6: 保存类别文件
np.save('class-ids-TRAIN.npy', class_ids_array)
np.save('class-names-TRAIN.npy', class_names_array)
代码实现分析
核心类: ImageNet
位置: dinov3/data/datasets/image_net.py
关键方法
1. dump_extra()
入口方法:
def dump_extra(self) -> None:
self._dump_entries() # 生成 entries
self._dump_class_ids_and_names() # 生成 class-ids 和 class-names
2. _dump_entries()
功能: 生成 entries-SPLIT.npy
关键步骤:
- 加载
labels.txt - 使用
ImageFolder扫描目录 - 创建结构化 NumPy 数组
- 保存到文件
进度显示:
old_percent = -1
for index in range(sample_count):
percent = 100 * (index + 1) // sample_count
if percent > old_percent:
logger.info(f"creating entries: {percent}%")
old_percent = percent
3. _dump_class_ids_and_names()
功能: 生成 class-ids-SPLIT.npy 和 class-names-SPLIT.npy
关键步骤:
- 加载
entries-SPLIT.npy - 提取唯一类别
- 创建类别数组
- 保存到文件
实际操作示例
示例 1: Tiny ImageNet
命令:
from dinov3.data.datasets import ImageNet
root = "/media/weiyr/新加卷/cv_dataset/tiny-imagenet-200"
extra = root
for split in ImageNet.Split:
if split.name in ['TRAIN', 'VAL']:
print(f"Generating metadata for {split.name}...")
dataset = ImageNet(split=split, root=root, extra=extra)
dataset.dump_extra()
print(f"✓ {split.name} completed")
输出:
Generating metadata for TRAIN...
creating entries: 10%
creating entries: 20%
...
creating entries: 100%
saving entries to "entries-TRAIN.npy"
saving class IDs to "class-ids-TRAIN.npy"
saving class names to "class-names-TRAIN.npy"
✓ TRAIN completed
Generating metadata for VAL...
...
✓ VAL completed
生成的文件:
/media/weiyr/新加卷/cv_dataset/tiny-imagenet-200/
├── entries-TRAIN.npy # ~4 MB
├── class-ids-TRAIN.npy # ~10 KB
├── class-names-TRAIN.npy # ~20 KB
├── entries-VAL.npy # ~400 KB
├── class-ids-VAL.npy # ~10 KB
└── class-names-VAL.npy # ~20 KB
示例 2: ImageNet-1k
命令:
from dinov3.data.datasets import ImageNet
root = "/nas01/data/ImageNet-1k"
extra = root
for split in ImageNet.Split:
if split.name in ['TRAIN', 'VAL']:
print(f"Generating metadata for {split.name}...")
dataset = ImageNet(split=split, root=root, extra=extra)
dataset.dump_extra()
print(f"✓ {split.name} completed")
生成的文件:
/nas01/data/ImageNet-1k/
├── entries-TRAIN.npy # ~50 MB
├── class-ids-TRAIN.npy # ~50 KB
├── class-names-TRAIN.npy # ~100 KB
├── entries-VAL.npy # ~2 MB
├── class-ids-VAL.npy # ~50 KB
└── class-names-VAL.npy # ~100 KB
示例 3: 使用自动化脚本
使用 z_setup_tiny_imagenet.py:
python z_setup_tiny_imagenet.py
脚本会自动:
- 检查数据集结构
- 验证
labels.txt - 生成元数据文件
- 验证生成结果
元数据文件详解
1. entries-SPLIT.npy
结构:
dtype = [
('actual_index', '<u4'), # 4 bytes
('class_index', '<u4'), # 4 bytes
('class_id', 'U10'), # 40 bytes (10 chars × 4 bytes/char)
('class_name', 'U50') # 200 bytes (50 chars × 4 bytes/char)
]
# 总计: ~248 bytes per entry
示例数据:
import numpy as np
entries = np.load('entries-TRAIN.npy', mmap_mode='r')
print(entries[0])
# (10026, 0, 'n01440764', 'tench')
print(entries.shape)
# (100000,) # Tiny ImageNet
print(entries.dtype)
# [('actual_index', '<u4'), ('class_index', '<u4'),
# ('class_id', '<U10'), ('class_name', '<U50')]
访问方式:
# 按索引访问
entry = entries[42]
print(f"Image index: {entry['actual_index']}")
print(f"Class index: {entry['class_index']}")
print(f"Class ID: {entry['class_id']}")
print(f"Class name: {entry['class_name']}")
# 批量访问
class_indices = entries['class_index'] # 获取所有类别索引
print(class_indices.shape) # (100000,)
2. class-ids-SPLIT.npy
结构:
dtype = 'U10' # Unicode string, max 10 characters
示例数据:
class_ids = np.load('class-ids-TRAIN.npy', mmap_mode='r')
print(class_ids.shape)
# (200,) # Tiny ImageNet has 200 classes
print(class_ids[0])
# 'n01440764'
print(class_ids[:5])
# ['n01440764' 'n01443537' 'n01484850' 'n01491361' 'n01494475']
用途:
# 根据类别索引查找类别 ID
class_index = 0
class_id = class_ids[class_index]
print(f"Class {class_index} ID: {class_id}")
3. class-names-SPLIT.npy
结构:
dtype = 'U50' # Unicode string, max 50 characters
示例数据:
class_names = np.load('class-names-TRAIN.npy', mmap_mode='r')
print(class_names.shape)
# (200,)
print(class_names[0])
# 'tench'
print(class_names[:5])
# ['tench' 'goldfish' 'great white shark' 'tiger shark' 'hammerhead']
用途:
# 根据类别索引查找类别名称
class_index = 0
class_name = class_names[class_index]
print(f"Class {class_index} name: {class_name}")
内存映射 (Memory Mapping)
什么是内存映射
传统加载:
# 将整个文件加载到内存
data = np.load('entries-TRAIN.npy') # 占用 ~50 MB 内存
内存映射:
# 不占用内存,按需加载
data = np.load('entries-TRAIN.npy', mmap_mode='r') # 占用 ~0 MB 内存
优势
| 特性 | 传统加载 | 内存映射 |
|---|---|---|
| 内存占用 | 全部加载 | 按需加载 |
| 加载速度 | 慢 | 快 |
| 多进程 | 每个进程独立副本 | 共享内存 |
| 随机访问 | 快 | 快 |
实际效果
ImageNet-1k 训练集:
- 传统加载: 50 MB × 10 workers = 500 MB
- 内存映射: 50 MB × 1 = 50 MB (共享)
验证元数据
检查文件是否存在
ls -lh /media/weiyr/新加卷/cv_dataset/tiny-imagenet-200/*.npy
预期输出:
-rw-r--r-- 1 user user 4.0M Nov 22 00:00 entries-TRAIN.npy
-rw-r--r-- 1 user user 10K Nov 22 00:00 class-ids-TRAIN.npy
-rw-r--r-- 1 user user 20K Nov 22 00:00 class-names-TRAIN.npy
-rw-r--r-- 1 user user 400K Nov 22 00:00 entries-VAL.npy
-rw-r--r-- 1 user user 10K Nov 22 00:00 class-ids-VAL.npy
-rw-r--r-- 1 user user 20K Nov 22 00:00 class-names-VAL.npy
验证数据完整性
import numpy as np
# 加载元数据
entries = np.load('entries-TRAIN.npy', mmap_mode='r')
class_ids = np.load('class-ids-TRAIN.npy', mmap_mode='r')
class_names = np.load('class-names-TRAIN.npy', mmap_mode='r')
# 检查数量
print(f"Total images: {len(entries)}")
print(f"Total classes: {len(class_ids)}")
# 检查类别索引范围
max_class_index = entries['class_index'].max()
print(f"Max class index: {max_class_index}")
assert max_class_index < len(class_ids), "Class index out of range!"
# 检查数据一致性
for i in range(min(10, len(entries))):
entry = entries[i]
class_idx = entry['class_index']
assert entry['class_id'] == class_ids[class_idx]
assert entry['class_name'] == class_names[class_idx]
print(f"✓ Entry {i}: {entry['class_name']} ({entry['class_id']})")
print("\n✓ All checks passed!")
常见问题
Q1: 元数据生成需要多长时间?
| 数据集 | 图像数量 | 生成时间 |
|---|---|---|
| Tiny ImageNet | 100,000 | ~30 秒 |
| ImageNet-1k | 1,281,167 | ~5 分钟 |
| ImageNet-22k | 14,197,122 | ~30 分钟 |
Q2: 元数据文件可以删除吗?
❌ 不可以!训练时必须使用元数据文件。
如果删除,会报错:
FileNotFoundError: [Errno 2] No such file or directory: 'entries-TRAIN.npy'
Q3: 修改数据集后需要重新生成元数据吗?
✅ 是的!任何以下情况都需要重新生成:
- 添加/删除图像
- 修改类别
- 移动文件位置
- 修改
labels.txt
Q4: 元数据文件可以在不同机器间共享吗?
✅ 可以!元数据文件是平台无关的 NumPy 格式。
但需要注意:
- 文件路径可能不同
- 需要同时复制数据集和元数据
Q5: 如何加速元数据生成?
方法 1: 使用 SSD 存储数据集
方法 2: 减少日志输出
import logging
logging.getLogger("dinov3").setLevel(logging.WARNING)
方法 3: 并行生成(不推荐,可能导致冲突)
技术细节
NumPy 结构化数组
定义:
dtype = np.dtype([
('field1', '<u4'), # Little-endian unsigned 32-bit integer
('field2', 'U10'), # Unicode string, 10 characters
])
字节序:
<: Little-endian (小端)>: Big-endian (大端)=: Native (本机)
数据类型:
u4: Unsigned 32-bit integer (0 to 4,294,967,295)U10: Unicode string, 10 characters (40 bytes)f4: 32-bit floatf8: 64-bit float
文件格式
NumPy .npy 格式:
[Header: 128 bytes]
[Data: variable size]
Header 包含:
- Magic number:
\x93NUMPY - Version:
\x01\x00 - Header length
- dtype 描述
- Shape 信息
- Fortran order flag
相关文档
文档版本: 1.0
更新日期: 2025-11-22
作者: DINOv3 项目文档
更多推荐



所有评论(0)