30秒速览
- 现实中等事故数据跟等中彩票差不多,合成数据直接造,想啥场景来啥场景。
- 我们用CARLA搭场景,ControlNet美颜,批量生成带免费标注的事故图,一口气攒了三万张。
- 训练时真数据和假数据得按7:3混着喂,配比对了模型长尾召回率能翻倍,全假数据会上吐下泻。
“鬼探头”把我们的模型直接戳瞎,真数据根本补不上这个窟窿
去年接手公司自动驾驶感知模型优化的时候,我自信满满。我们在常规数据集上跑得挺漂亮,Cityscapes、BDD100K,mAP都能到0.6以上,老板也觉得差不多了。直到有一天测试组的哥们甩给我一段行车记录仪拍的片子:夜间小雨,一辆公交车靠站,一个外卖小哥突然从车头前面窜出来横穿马路,我们的模型连个检测框都没画。我反复看了五遍,确认不是后处理过滤掉,而是backbone压根没注意到那里有人——因为公交车遮挡了大部分身体,只露出半边肩膀和一条腿,而且雨水反光把特征冲得一塌糊涂。
我赶紧去翻训练集,想知道这种“鬼探头”场景到底有多少。结果让我心凉了半截:整个数据集里,行人从遮挡物后突然出现的样本只有两段视频,总共不到60帧能用的。而且这两段全是白天晴天,跟真实测试的那个雨夜场景完全对不上。标注3D框更是一言难尽,一个熟练标注员花四十分钟才能标好一帧,因为要脑补被遮挡部分的3D位置。这怎么玩?事故数据的采集成本本来就高得离谱,没人会故意制造碰撞来喂模型,只能靠路上偶发。而现实世界的事故分布极度长尾,撞车、行人闯入、动物横穿、雾天连环追尾……每种可能得等大半年才能攒够几个样本,多样性还一塌糊涂。
我试过用传统数据增强硬撑,翻转、旋转、颜色抖动、甚至CutMix都上了,结果mAP纹丝不动。因为那些操作只能在像素层面变换,改变不了场景本身的语义结构。我需要的是把公交车挪个位置,在它后面塞一个穿黑衣服的人,再把整个场景变成雨夜,还得有正确标注。这靠增强做不到,靠采集更做不到。当时我盯着那段漏检视频,脑子里就一个念头:如果能像游戏一样随意编辑场景,然后直接生成照片级真实感的图像,标签还能自动给出来,那该多爽。这就是我们开始搞合成数据的起点。
我拿ControlNet当美颜滤镜,把游戏截图变成了逼真事故照,标签还免费送
一开始我们想得很简单:用CARLA模拟器搭建事故场景,把渲染图直接扔去训练。CARLA确实强,能精确控制每辆车的轨迹、速度,还能设置行人突然冲出、天气突变。我们写了个脚本,随机采样道路拓扑、交通流参数,瞬间就能生成上百个追尾或侧面碰撞的变体。但问题是,CARLA渲染出来的图一眼假,纹理像十年前的游戏,模型学到的都是这种“塑料感”特征,一上真实路测就拉胯。域差距大到令人发指,mAP直接腰斩。
后来组里有个做AIGC的实习生提了一嘴:为啥不用扩散模型把渲染图“转”成真实照片?我突然开窍。ControlNet这东西刚好能拿边缘图、分割图当条件,生成的照片严格保留原图的结构布局,这不就是为合成数据量身定做的么?我们的生成管线就这么搭起来了:
第一步,在CARLA里把事故参数化。比如我想生成“夜间雨天的侧面碰撞”,就在Python脚本里设定两车相对速度、撞击角度,把路灯、雨滴粒子效果都调好,同时开启传感器,输出RGB图、语义分割图、深度图和所有物体的3D包围盒投影到2D的坐标。这一步全自动,一晚上能跑出三千个不同场景变体,覆盖晴天、阴天、雨、雪、雾,以及各种遮挡关系和行人姿态。标签不用人标,引擎直接算。
第二步,美颜。我们拿CARLA渲染的RGB图提取Canny边缘,当作ControlNet的conditioning,再用Stable Diffusion v1.5配合真实风格的提示词来生成照片级图像。例如提示词是:“A photorealistic rainy night street, two cars colliding in intersection, wet road reflections, highly detailed, realistic lighting”。生成出来的图纹理质感极其逼真,道路积水反光、车漆划痕都出来了,但车的位置、形状、行人位置跟渲染原图严格对齐,因为扩散过程被控制住了。标注框原封不动就能用,最多根据细微边缘偏移做一点后处理微调。
最让我兴奋的是事故种类的多样性一下子打开了。行人闯入场景,我可以在CARLA里让行人从任何遮挡物后面突然跑出,步速、方向、衣服颜色随机化,然后针对同一个场景用不同ControlNet强度、不同提示词生成多张变体——白天、黄昏、暴雨、大雾,甚至积雪覆盖的路面。异常天气以前想都不敢想,现在成了批处理。我们甚至能人为制造一些极少见的组合,比如泥石流冲毁护栏、前车掉落货物砸向镜头,这些现实中可能十年遇不到一次,但模型必须见过才能应对。
给你看一个核心生成代码片段,我们用HuggingFace Diffusers搞的,非常简单:
from diffusers import StableDiffusionControlNetPipeline, ControlNetModel
import torch
from PIL import Image
controlnet = ControlNetModel.from_pretrained(
"lllyasviel/sd-controlnet-canny", torch_dtype=torch.float16
)
pipe = StableDiffusionControlNetPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5", controlnet=controlnet, torch_dtype=torch.float16
).to("cuda")
# canny_image是从Carla渲染图提取的边缘图,已经预处理过
canny_image = Image.open("rendered_scene_edges.png").convert("RGB")
prompt = "a rainy city street traffic accident, cars colliding, photorealistic, high detail, dark lighting"
output = pipe(prompt, image=canny_image, num_inference_steps=20).images[0]
output.save("synthetic_accident_rainy.png")
这个管线一天能跑出上万张高质合成图,每张都带着精确的2D/3D框标注,标注成本为零。我们把这些图全部灌进训练集,总共攒了超过3万张“假事故”。这时我底气足了,开始正儿八经地做对照实验。
混合训练后,模型对“鬼探头”的召回率翻了一倍多,但全吃假数据会拉稀
咱们用的基模型是YOLOv8x,在真实数据集上预训练过。真实数据包括BDD100K里的部分事故标注子集、Cityscapes,以及我们自己在测试路段采集并人工标注的约1.2万张正常行驶图片,其中事故场景只有可怜的300多张。测试集我们用了一个“压箱底”的真实极端场景集,包含500张各种长尾案例:鬼探头、路口闯红灯碰撞、高速遗撒、动物突然出现、夜间弯道侧滑等,都是人工从大量视频里截取并仔细标注的。
实验设了四组:
A组:只用真实数据(baseline)
B组:真实数据+合成数据,总合成占比20%
C组:真实+合成,合成占比50%
D组:纯合成数据训练(约3万张)
所有组训练超参一致:SGD优化器,初始学习率0.01,余弦退火,batch size 32,训练300个epoch。混合组我们写了一个简单的PyTorch Dataset来控制比例:
class MixedDataset(Dataset):
def __init__(self, real_dataset, synth_dataset, synth_ratio=0.2):
self.real = real_dataset
self.synth = synth_dataset
self.synth_len = int(len(self.real) * synth_ratio)
self.synth_indices = np.random.choice(len(self.synth), self.synth_len, replace=False)
def __len__(self):
return len(self.real) + self.synth_len
def __getitem__(self, idx):
if idx < len(self.real):
return self.real[idx]
else:
return self.synth[self.synth_indices[idx - len(self.real)]]
你直接用DataLoader加载这个MixedDataset就完事了,每次epoch会从合成集随机采样相当于真实集20%的样本量,保证合成数据不占主导。
跑出来的结果让我大呼过瘾。在真实测试集的“行人闯入”类别,A组baseline的mAP@0.5只有0.45,因为样本太少模型根本学不到遮挡后突然出现的人的特征。B组(20%合成)直接蹿到0.68,C组(50%)到了0.74,D组纯合成则是0.61。碰撞类别也类似,从0.38爬升到0.69(B组)和0.72(C组)。整体所有类别的平均mAP,A组0.52,B组0.63,提升超过20%,C组略有下降到0.61,因为真实数据被过度稀释,常规类别有点受影响,但长尾类别依然受益。D组纯合成虽然也有0.55,但已经比baseline高,说明合成数据本身教了不少东西,可域差距明显拖了后腿。
最直观的对比是“鬼探头”场景的召回率。A组只有可怜的0.28,很多漏检;B组到了0.59,接近翻倍;C组0.62。我终于看到了那个雨夜行人闯入的测试视频被正确框出,检测框稳稳锁住小人,虽然置信度才0.6左右,但至少不瞎了。我意识到合成数据不仅补充了样本量,更关键的是提供了真数据里几乎没有的多样性,让模型被迫去学习更鲁棒的底层特征,而不是死记特定纹理。比如我们生成了各种颜色、各种姿态的行人从各种车型后面窜出,模型学会了通过局部肢体轮廓和运动模糊来判断,而不是依赖完整的直立人体。
但D组的回测说明一个硬道理:全用假数据,模型会在真实世界里犯傻。一些合成图特有的伪影被当成特征学进去了,比如ControlNet偶尔在边缘处产生的模糊失真,或者渲染引擎残留的规则化反光。所以混合是关键,不能让假数据占大头。
真实和合成数据到底几比几?我试了8种比例,发现最佳配比根本不是五五开
既然20%合成效果拔群,50%就开始下滑,我就好奇边界在哪。于是我们又做了一轮更细的配比实验:合成数据占比从10%一直拉到90%,步长10%,看真实测试集mAP的变化。结果画出来的曲线很有意思:在10%~30%之间mAP稳步攀升,到30%左右达到峰值0.64,随后缓慢下降,超过70%后加速下滑,到90%时已经掉到0.56,只比全合成好一点。这说明域迁移带来的负面影响在合成超过一定阈值后开始反噬,但长尾类别的AP依然坚挺,甚至到70%合成时还在涨。也就是说,存在一个甜蜜点,既能最大化长尾收益,又不至于让常规类别崩盘。
我们最终稳定在了真实:合成≈7:3(合成占比30%),并且对不同类别做了差异化配比。对于出现频率极高的“正常行驶车辆”类别,合成数据比例压得很低,仅5%~10%,用于引入一些异常光照变化;而“行人闯入”“碰撞”等长尾类别,合成比例放宽到50%甚至更高,相当于针对这些类别单独加小灶。具体做法是在MixedDataset里根据类别权重调整采样概率,代码稍微复杂点但逻辑清楚。
域迁移问题我们一开始想用对抗训练(DANN)来解,加了个域分类器试图让backbone输出域不变特征,结果训练收敛极不稳定,经常振荡,试了两个星期放弃了。后来一个搞域适应的朋友告诉我,对于这种生成-真实域差距,最简单的反而是mixup。我们在batch中随机把真实图和合成图按一定比例混合,强行让模型学习插值后的特征空间,效果出奇地好,直接把真实场景测试mAP又拉了1.5个点。此外label smoothing也不能少,因为合成标注太“硬”,框边界非常精准,真实标注反而带噪声,直接用会造成过度自信,加一点0.05的平滑能缓解。
另一个我们踩的坑是合成图质量波动。ControlNet生成的图偶尔会出奇怪伪影,比如车轮消失、人物多长出一只手。如果不加筛选直接训,模型会学到这些“幽灵特征”,在真实场景里把路边石墩认成轮胎。我们后来用了一个简单的质量过滤器:用预训练CLIP ViT-L/14计算生成图与对应文本提示的相似度,低于0.26的直接丢掉,再人工抽查。筛选后总图量从3万降到2.4万,但模型mAP反而涨了,因为脏数据少了。
最后提一嘴泛化。我们拿了另一个城市的测试视频(模型没见过该地区路况)跑了一遍,发现加入合成数据的模型泛化能力更强,因为合成数据里包含了大量随机化场景背景,比如不同风格的建筑、路牌、植被,相当于给模型做了地理泛化训练。这算是意外收获。
现在这套管线已经跑进了项目迭代,每次有新的事故类型需求,比如最近要求的“高速前车掉落货物”,我们不用等采集了,一天之内就能生成3000张变体并完成训练验证。合成数据不是银弹,但解决长尾问题时,它比任何数据增强和迁移学习都直接、都暴力。说实话,我现在看到事故视频都不紧张了,因为可以随时“再造”一万条来教模型怎么活下来。