DINOv3 元数据生成详解

📋 目录

  1. 什么是元数据
  2. 为什么需要元数据
  3. 元数据文件列表
  4. 生成过程详解
  5. 代码实现分析
  6. 实际操作示例

什么是元数据

元数据(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           # 仅包含图像索引(无标签)

生成过程详解

整体流程

开始
读取 labels.txt
使用 torchvision.ImageFolder 扫描目录
创建 entries 数组
保存 entries-SPLIT.npy
提取唯一类别
保存 class-ids-SPLIT.npy
保存 class-names-SPLIT.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

关键步骤:

  1. 加载 labels.txt
  2. 使用 ImageFolder 扫描目录
  3. 创建结构化 NumPy 数组
  4. 保存到文件

进度显示:

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.npyclass-names-SPLIT.npy

关键步骤:

  1. 加载 entries-SPLIT.npy
  2. 提取唯一类别
  3. 创建类别数组
  4. 保存到文件

实际操作示例

示例 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

脚本会自动:

  1. 检查数据集结构
  2. 验证 labels.txt
  3. 生成元数据文件
  4. 验证生成结果

元数据文件详解

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 float
  • f8: 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 项目文档

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐