☰
AI训练数据类型全解析:从标注格式到张量精度
2026/9/29 7:16:07 网站建设 项目流程

做AI训练这几年,被问得最多的不是“哪个模型更厉害”,而是“我的数据到底该怎么准备”。有人拿着一堆原图来训YOLO,有人把JSON标注直接塞进脚本就报错,也有人分不清float32和uint8在训练时到底差在哪。说实话,很多训练跑不起来、精度上不去,问题根本不在模型结构,而在训练数据类型的整个链路。

我理解的“AI训练数据类型”,至少包含三个层面:任务对应的标注格式、数据在磁盘和内存里的存储类型、深度学习框架计算的张量精度;再加上我们写数据处理脚本时接触到的Python组合类型、NumPy的dtype、Pandas的category,甚至C++、PLC等外部系统的变量类型。把这套东西理顺了,后面训练、排查、调优都会顺畅很多。这篇文章不绕弯子,直接用我实际跑过的案例来拆。

1. 先搞懂“训练数据类型”到底包含哪些层次

1.1 业务层的类型:任务决定标注格式

在开始任何训练之前,先想清楚做的是图像分类、目标检测、实例分割、关键点检测、旋转目标、OCR,还是大模型指令微调。任务类型不同,标注数据的格式和“数据类型”完全不一样。

  • 图像分类:通常只需要一个label.txt或CSV,每个文件名对应一个类别索引。
  • 目标检测:主流标注格式有VOC XML、COCO JSON、YOLO txt,三者的坐标类型和存储结构差别很大。
  • 实例分割/全景分割:需要polygon多边形或二值mask,COCO里还经常用RLE压缩字符串。
  • 旋转目标检测:典型用DOTA格式,每个目标用四个角点坐标表示。
  • OCR:需要文本框四点坐标外加文本字符串,字符还要做编码映射。
  • 大模型微调/LoRA:常用JSONL文件,一条instruction/response构成一个样本。

很多新手老爱问“哪种格式最好”,我的回答是“看生态”。YOLO系训练就按YOLO txt来,Mask2Former这类模型就准备COCO JSON,mmrotate则要DOTA格式。数据格式不只是给人看的,更是给数据加载器和Loss函数消费的接口。你选了模型却没选对对应格式,后面每一步都会别扭。

我整理了一个常用对应表:

任务类型主流标注格式关键字段与类型
图像分类txt/csv类别索引:int
目标检测YOLO txt / COCO JSON / VOC XML坐标:float;类别:int
实例分割COCO JSON + RLE/polygonmask:uint8 / string
旋转目标DOTA txt8个坐标:float
OCR自定list/JSON文本:str;字符索引:int
指令微调JSONLinstruction/output:str

格式一旦定下来,数据协议也就有了雏形。这份业务层的“类型表”应该最先写进文档里,而不是等到写代码时再拍脑袋。

1.2 存储层的类型:图像编码与数值类型

一张JPEG图片在磁盘上其实是压缩后的字节流,解码之后变成numpy数组。标准RGB图像是uint8类型,shape为(H, W, 3),每个通道的取值是0到255。分割mask常用uint8或uint16,每个像素值代表一个类别索引;医学影像里还经常出现uint16甚至float32灰度,比如CT值范围很大,不能只用uint8表示。

模型训练时,如果直接把uint8图像丢给卷积层,有些框架会直接报dtype错误,因为权重是float32,输入输出类型不一致。所以数据加载到内存后,通常要除以255,转成float32的0到1范围,或者按数据集的mean/std做标准化。

这里有个容易搞混的点:磁盘存储格式和内存计算格式不是一回事。磁盘上可以继续存uint8的JPEG/PNG,体积小、解码快;内存里计算才转float32。有些工程喜欢把图片预处理成float32的npy文件缓存起来,省去每次训练读图解码的开销,但代价是磁盘占用直接变成原来4倍。我一般只在小数据集上这么做,大数据集还是老老实实原始图片加实时预处理。

1.3 计算层的类型:框架张量与混合精度

进入模型内部后,默认计算精度是float32。权重、梯度、激活大多用float32,稳定、不容易溢出。想要提速和减少显存时,会做混合精度训练(AMP):一部分计算落到float16或bfloat16,一部分保留float32。

但是float16的动态范围很小,容易上溢或下溢。所以AMP会做loss scaling,先把loss放大,反向传播得到梯度后再缩放回来。这也是为什么不能简单地把所有层都设成half(),需要用torch.cuda.amp.autocast等机制来管理。

除了图像,标签张量的类型更是踩坑重灾区。比如CrossEntropyLoss的target必须用LongTensor,不能是float;坐标回归Loss通常要float32;注意力mask一般用int64或bool。这些类型不一致,是训练脚本里最常见的报错来源。

所以说,数据类型这个事贯穿三个层次:业务层管“标的是什么”,存储层管“数据怎么编码”,计算层管“模型怎么算”。后面几节我按实际操作顺序展开。

2. 从标注文件到内存张量:类型设计与转换细节

2.1 用 Python 组合类型组织训练样本

写数据加载器时,Python的list、tuple、dict是绝对主力。list保存所有样本路径,dict保存标注信息,tuple保存不可变样本,set做id去重。热词里提到的“python的组合数据类型”,在AI训练脚本里的意义就在这里。

比如一个目标检测任务的自定义Dataset,结构通常是:

self.images = [] # list[str] self.labels = [] # list[dict] self.class_names = [...] # list[str]

而__getitem__返回的样本,我习惯用一个dict包起来:

def __getitem__(self, idx): return { "image": image_tensor, # torch.float32, (3,H,W) "boxes": boxes_tensor, # torch.float32, (N,4) "labels": labels_tensor, # torch.int64, (N,) "image_id": idx, # int }

为什么box用float32、label用int64?坐标要参与坐标变换、归一化和回归Loss,全是浮点计算;类别只是下标索引,后面CrossEntropyLoss要求LongTensor。如果定义颠倒,轻则报警告,重则崩训练。

在复杂一些的项目里,我推荐用dataclass或TypedDict把样本结构固定下来:

from dataclasses import dataclass @dataclass class DetectionSample: image_path: str boxes: list # [[x1,y1,x2,y2], ...] labels: list # [cls_index, ...]

这样每个字段的类型一目了然,后续写collate_fn、写可视化脚本也不容易出错。

2.2 NumPy/Pandas/C/外部系统的类型转换

标签和元信息很多时候存在CSV里。Pandas读进来常见object、int64、float64三种类型。训练前要执行astype转换,比如将字符串类别列转成category类型,再.cat.codes得到数值索引;用pd.to_numeric把脏数据里的数字字符串统一转成数值。这些操作看着基础,实际项目中90%的数据异常都是类型转换时不注意造成的。

NumPy的dtype更加底层。uint8、float32、int64之间用.astype()切换,但要记住大数组转换会复制一份内存,峰值内存很容易翻倍。我之前处理一个几十GB的分割数据集时,就是因为uint8mask转float32没分批做,机器直接内存爆掉。

C语言的数据类型存储在AI训练里也常遇到。写自定义C++/CUDA算子时,int在不同平台可能是4字节,long可能是8字节,结构体还有内存对齐。如果Python端和C++端的字段大小对不上,数据解析就会错位,表现是“训练loss直接nan”或者“输出全是垃圾”。字节序也得注意,x86小端,网络传输大端,跨端读写文件时经常踩坑。

还有一些工业场景,比如从PLC采集数据做AI训练,变量表里的INT、DINT、REAL类型不搞清楚就解析,得到的数据全是乱的。这部分大家平时聊得少,但真碰到了很浪费时间。

2.3 常见数据格式互转:VOC、COCO、YOLO 的坐标类型

目标检测最常做的互转就是VOC/COCO/YOLO三种格式。它们的类型定义差异很大:

  • YOLO txt:每行class cx cy w h,四个坐标都是归一化到图像宽高的float,范围0~1。
  • COCO JSON:annotation里的bbox = [x,y,w,h],是左上角绝对像素坐标,float;类别id一般从1开始。
  • VOC XML:bndbox里是xmin,ymin,xmax,ymax,通常是绝对整数像素。

从VOC或COCO转成YOLO格式时,最关键的公式是:

cx = (xmin + xmax) / 2 / img_w cy = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h

如果原格式是COCO的[x,y,w,h],则要这样转:

cx = (x + w / 2) / img_w cy = (y + h / 2) / img_h w = w / img_w h = h / img_h

很多标注工具导出的COCO类别id不是从0开始而是从1开始,直接转给YOLO,模型会默认从0开始,硬生生多出一个背景类,精度必然崩。还有一个隐藏坑:坐标小数位保留几位也会影响小目标训练。YOLO格式的归一化坐标一般建议保留6位以上,否则两个挨得很近的box可能变成同一条线。

2.4 特殊场景的数据类型映射:点云、工业采集、Redis

现在多模态和自动驾驶模型越来越多,比如BEVFusion这类工作要同时处理相机图像和雷达点云。点云坐标是float32,强度是float32或int16,时间戳是int64,标定外参矩阵用float64。在矿山等场景做融合时,雷达标定仪给的平移向量和旋转矩阵一旦精度不对,融合到BEV空间后目标坐标全偏,训练再久也没用。

工业场景里,采集设备出来的数据经常是二进制文件,里面按结构体排列各种类型的字段。用Python解析时,除了字段类型要对,还要注意结构体对齐。如果有pack/unpack,必须严格指定字节序,比如<小端、>大端。

另外,很多分布式训练系统会拿Redis缓存数据集索引或样本状态,这也要明确用哪种Redis数据类型。用String存JSON,在样本量大的时候反复序列化和反序列化非常慢;用Hash存每条样本的字段、用Set存已完成列表、用List做队列,才是合理的类型选择。这里的数据类型已经不是模型张量,但同样是训练数据链路的一部分。

3. 五类训练任务的数据组织实例

3.1 YOLOv8 目标检测:从预处理到训练一条线

YOLOv8是目前最常用目标检测模型之一,网上资料多,但很多人第一次训练自己的数据集还是会被数据组织绕晕。以YOLOv8训练自己的数据集为例,标准目录结构如下:

datasets/mydata/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml

其中data.yaml内容:

path: datasets/mydata train: images/train val: images/val names: 0: person 1: car

YOLO标签txt的每一行是:

class_id cx cy w h

class_id是整数,cx,cy,w,h是归一化后的小数。如果我用的是CV2读图,必须记得cv2.imread读出来是BGR顺序,而YOLOv8训练管线里默认按RGB处理,所以读取后要转一下:

image = cv2.imread(image_path) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

训练命令倒是很简单:

yolo detect train data=data.yaml model=yolov8s.pt epochs=100 batch=16 imgsz=640

网上已经有人聊到yolov13这种新版本,但不管版本号怎么变,YOLO系列的数据协议基本没变:一张图对应一个同名txt,每行一个目标,坐标全部归一化。只要把这一套组织好,换模型版本、换规模,都不用重做数据。

3.2 旋转目标检测:mmrotate 与 DOTA

旋转目标检测和普通水平框最大的区别是:目标可能带角度,不能简单用xmin,ymin,xmax,ymax,而是用四个角点表示。DOTA数据集是遥感场景的标杆,标注txt每一行格式是:

x1 y1 x2 y2 x3 y3 x4 y4 class_name difficult

四个坐标点是有顺序的,通常按边界框的角点顺序排列,模型预测后也要按同样顺序解码。如果用mmrotate训练DOTA数据集,不能直接把原始大图塞进去,要先按1024x1024的窗口把大图切块,同时把落在切块边缘的目标框做过滤或保留处理。转换DOTA到mmrotate格式时,还要过滤掉difficult为1的目标,因为这类边缘目标算作“待定区”,训练时不该参与损失。

旋转框的中间表示通常还是float32,但在做数据增强旋转时,四个角点要乘旋转矩阵,这里如果精度不足,框和图像会错位。还有一点:DOTA里图像非常大,动辄几千像素,直接训练会爆显存,切图之后每个子图的尺寸变小,数据量却翻了数倍,类型和坐标均要保持一致。

3.3 实例分割:Mask2Former 的 COCO 与 RLE

Mask2Former属于掩码级预测模型,训练数据通常是COCO格式,尤其依赖panoptic或instance标注。COCO的annotation里,segmentation可以是多边形列表,也可以是RLE压缩字符串。如果直接用原始RLE字符串去算Loss,肯定不行。必须先对它解码成二进制mask:

from pycocotools import mask as mask_util import numpy as np def rle_to_mask(rle_obj): return mask_util.decode(rle_obj).astype(np.uint8) # shape (H,W)

解码后mask是uint8的(H,W),类别处是1,背景处是0。进入模型后,一般会转成float32并做下采样,或者把target转成LongTensor做分类。注意mask和类别索引要对齐,尤其是实例segmentation,每个实例可能共享同一个类别,但在模型输出里需要用instance_id区分。

Mask2Former训练时还有一个坑:num_queries、num_classes要严格按照数据集统计,类别id不能有空洞。如果原始COCO里类别id从1开始,而代码里num_classes不包含0,就要重新映射。不然训练时候损失函数里的索引越界是常事。

3.4 OCR 自定义模型:EasyOCR 训练

EasyOCR支持自定义训练,但许多人误以为拿图片和文本字符串就能直接训练。实际上,EasyOCR训练需要准备两条文本文件:training_list和validation_list,每行是:

图片路径,标注文本

比如:

/train/001.jpg,Hello World /train/002.jpg,你好世界

标注文本是字符串,但训练时模型不能直接吃字符串。EasyOCR训练脚本会先建立字符集character,把每个字符映射成索引。中文场景下,如果没有把字符集设置完整,验证集里的某个汉字不在训练字符集里,loss会越界或输出乱码。所以做中文OCR时,一般把语料里所有字符提前收集一遍,生成char_list,再统一编码。

图片路径和文本之间也要注意逗号、空格等分隔符,如果文件名本身带逗号,解析就废了。这个问题我在整理公开数据集时踩过,后来改成用制表符分隔就安稳了。

3.5 大模型与 LoRA 微调:JSONL 与 token 类型

做大模型微调,尤其是LoRA,数据组织又是另一套路子。常见格式是JSONL,每行一条独立样本:

{"instruction": "写一首关于秋天的诗", "input": "", "output": "秋风萧瑟天气凉,草木摇落露为霜。"}

或者用更流行的对话格式:

{"messages": [{"role": "system", "content": "你是一个AI助手"}, {"role": "user", "content": "你好"}, {"role": "assistant", "content": "你好,有什么可以帮助你?"}]}

最近有公开的智能体训练新方法,核心之一就是把多轮对话和工具调用过程结构化成messages列表,让模型学习system、user、assistant角色切换。数据类型上,原始数据全是字符串,但经过tokenizer后,会变成input_ids、attention_mask、labels三种张量,前两个通常是int64,labels和input_ids同形状。

指令微调有一个关键细节:Loss只计算输出部分,prompt部分的labels需要置为-100,训练时自动忽略。如果忘了处理,模型会拼命学习复述问题,表现就是“回答驴唇不对马嘴”。另外,用LoRA加载基座模型时,很多大模型checkpoint是用bfloat16存储的,如果用float32强行加载,显存直接翻倍,很容易爆。这个dtype选择直接影响你能不能在单卡上跑起来。

4. 数据类型踩坑清单与调试经验

4.1 高频报错与排查速查表

把这么多年在AI训练里遇到的典型类型问题整理成一张速查表,遇到报错先来这里找。

报错/现象原因解决方式
Expected object of scalar type Long but got Float标签用了float将target转成labels.long()或读取时用np.int64
Expected dtype float32 but found uint8图像没转float图片先img.astype(np.float32) / 255.0
IndexError: index N is out of bounds类别id不连续或越界检查数据集中类别映射,重新映射为0~N-1
mask训练时全部为0或loss不下降RLE未正确解码用pycocotools先解码成uint8mask
坐标训练loss很大坐标单位不统一检查yolo归一化/绝对像素是否混用
文本训练乱码字符集缺失提前收集字符集并统一UTF-8
CUDA illegal memory access数据错位导致越界检查Python端与C++/CUDA的结构体/数组类型是否匹配
显存比预期高很多全用float32加载大模型改用bfloat16/float16加载,训练保持AMP

4.2 dtype 影响显存和速度:一个估算示例

数据类型最直观的影响就是显存。以一张640x640的RGB图为例:

  • uint8存储:640 * 640 * 3 = 1,228,800字节,约1.17MB。
  • 转成float32后:直接变成4倍,约4.69MB。
  • 一个batch=32的图片输入就是150MB,还没算模型参数和中间激活。

模型权重也一样。10亿参数模型,float32需要约4GB内存,bfloat16只占2GB。这就是为什么大模型微调普遍用bfloat16。混合精度训练的实际收益:

  • 显存占用减半左右;
  • 在Ampere及以上架构上训练速度提升明显;
  • 但保留float32的master weight和优化器状态,因此不是所有内存都减半。

如果想要进一步压显存,还可以用梯度累积、激活重计算、8bit优化器,但这些都是后话。理解了类型位宽,你才能算明白显存到底够不够。

4.3 多传感器与跨语言数据类型的隐藏坑

我做过一次多传感器融合项目,相机图像、激光雷达点云、惯导数据各有各的时间戳和坐标系。图像是uint8,点云是float32,标定矩阵是float64,时间戳是int64。一开始为了省事,把标定矩阵用float32存了,结果几米外的目标直接偏了一截。后来全部统一用float64保存外参,训练数据才稳定。

跨语言传输也常踩坑。C++端写二进制文件时用结构体存储:

struct Label { int id; // 4 bytes float score; // 4 bytes float bbox[4]; };

到了Python端,如果用默认的Pythonint去对应C++int,解析出来大概率对不上。正确做法是用numpy.dtype明确结构:

dtype = np.dtype([ ('id', np.int32), ('score', np.float32), ('bbox', np.float32, (4,)) ])

这样做虽然麻烦,但保证两边数据不会错位。在工控和物联网场景里,PLC变量表读取也是同理,INT是16位有符号,DINT是32位有符号,REAL是32位浮点。读错了,轻则数值差一倍,重则整个数据翻转。

4.4 我的习惯:先定义数据协议,再写训练逻辑

最后说一个我自己的习惯:不管项目多急,动训练脚本之前,一定先把“数据协议”写出来。协议里明确每个字段叫什么、是什么类型、取值范围多大、坐标是绝对值还是归一化、掩码用什么编码、标签索引从几开始。

这个协议可以是JSON Schema,也可以是dataclass,哪怕是Markdown表格都行。写清楚之后,拿着标注文件和协议逐项对一遍,再写Dataset和collate_fn。协议里最容易忽略的细节:

  • 类别id是否从0开始;
  • 坐标闭区间还是开区间;
  • 图像通道是RGB还是BGR;
  • 空标注文件怎么处理;
  • 多标签时label维度是(N,)还是(N,num_classes)。

我这几年遇到的项目事故,绝大多数不是模型结构问题,而是这些“类型细节”没对齐。有时候两组数据只是坐标归一化方式不一样,合在一起没做统一,训练出来的模型在A数据上正常,在B数据上全瞎。先定义协议,等于在代码和数据之间立了一份契约,双方都按契约做事,能省掉后期大量排查时间。

这个习惯帮我避开了很多坑,希望你少走几步弯路。

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

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

立即咨询