Ансамбли детекторов
LibreEnsemble прогоняет два или более детектора по одному и тому же декодированному изображению и сливает их рамки в один объект Results. Каждый участник сохраняет свои веса, пороги, устройства и список классов.
Что такое ансамбль
LibreEnsemble берёт два или более детектора, прогоняет каждый по одному и
тому же изображению и сливает их рамки в один Results. Это конструкция
уровня предсказания: обучать здесь нечего, а участники остаются независимыми
моделями, которые можно валидировать и экспортировать по отдельности.
Единственная поддерживаемая задача — детекция. Участник с любой другой задачей
вызывает ValueError при создании ансамбля, и в сообщении указаны индекс
участника и его задача.
Оба имени импортируются лениво, поэтому до использования ничего не стоят:
from libreyolo import LibreEnsemble, ExternalDetectorСоздание ансамбля
from libreyolo import LibreEnsemble, SAMPLE_IMAGE # Участниками могут быть пути к чекпойнтам или уже загруженные модели.ensemble = LibreEnsemble(["LibreYOLO9s.pt", "LibreRFDETRs.pt"]) result = ensemble(SAMPLE_IMAGE)for xyxy, conf, cls in zip( result.boxes.xyxy.tolist(), result.boxes.conf.tolist(), result.boxes.cls.tolist(),): print(result.names[int(cls)], round(float(conf), 3), xyxy)from libreyolo import LibreEnsemble, SAMPLE_IMAGE ensemble = LibreEnsemble( ["LibreYOLO9s.pt", "LibreRFDETRs.pt"], weights=[1.0, 1.3], # по соглашению, пропорционально mAP на валидации fusion="wbf", fusion_iou=0.55, min_votes=2, # оставить только рамки, найденные обоими участниками) result = ensemble(SAMPLE_IMAGE)print(len(result.boxes), "agreed detections")from libreyolo import LibreEnsemble, SAMPLE_IMAGE ensemble = LibreEnsemble(["LibreYOLO9s.pt", "LibreRFDETRs.pt"]) # Скаляр применяется ко всем участникам; список читается по участникам.result = ensemble(SAMPLE_IMAGE, conf=[0.3, 0.5], iou=0.5)print(len(result.boxes))LibreEnsemble(
members,
*,
weights=None,
fusion="wbf",
fusion_iou=0.55,
min_votes=1,
)members — последовательность из двух и более элементов. Элемент типа str
или Path загружается через LibreYOLO(); всё остальное должно быть
вызываемым и иметь словарь names. Если элементов меньше двух, будет
ValueError, а если передать одну строку — TypeError, а не перебор её
символов.
По умолчанию weights равен None — это одинаковые веса для всех. Если веса
заданы, их должно быть ровно по одному на участника, и все они должны быть
строго положительными, поэтому нулевой вес приводит к ошибке, а не молча
отбрасывает участника. По документированному соглашению их задают
пропорционально mAP участника на валидации.
По умолчанию fusion_iou равен 0.55; это IoU, при котором рамки от разных
участников собираются в один кластер. Это не тот же порог, что iou в
вызове, — тот отвечает за собственный NMS каждого участника.
По умолчанию min_votes равен 1: рамку может провести в результат любой
отдельный участник. Если поднять значение, останутся только кластеры, подтверждённые
таким числом разных участников. Значение должно быть положительным целым не
больше числа участников, и по каждому классу оно ограничивается сверху числом
участников, которые этот класс вообще знают, — так класс, на котором обучался
лишь один участник, не стирается молча.
Методы слияния
Три метода принимаются по имени, а ещё можно передать вызываемый объект.
fusion | Поведение |
|---|---|
"wbf" | Weighted boxes fusion: последовательный алгоритм, в точности как в статье. Значение по умолчанию |
"wbf_seeded" | Weighted boxes fusion в один проход; затравки кластеров выбирает NMS с учётом классов |
"nms" | Склеить рамки всех участников, затем NMS с учётом классов |
Weighted boxes fusion усредняет координаты кластера с весами по уверенности и
выдаёт рамку, которую не предлагал ни один участник. Два взвешенных варианта
совпадают, когда кластеры однозначны, и могут слегка расходиться на цепочках
перекрывающихся кластеров. "nms" вместо усреднения выбирает выжившую рамку,
поэтому выжившие сохраняют исходные оценки, а веса влияют только на то, какая
рамка победит. Поскольку он отбирает, а не кластеризует, считать голоса он не
может: сочетание fusion="nms" с min_votes больше 1 вызывает ValueError.
Weighted boxes fusion пересчитывает оценку кластера пропорционально доле веса
участников, которые его поддержали. При двух участниках с одинаковыми весами
рамка, найденная только одним из них, сохраняет половину оценки: 0.9
превращается в 0.45. Поэтому итоговая уверенность может оказаться ниже conf, с которым
запускался каждый участник, — фильтруйте по итоговой оценке и не рассчитывайте,
что порог участника всё ещё действует.
Участники с разными списками классов
Участникам не обязательно иметь общий список классов. Их пространства меток
объединяются по именам, и каждый участник получает таблицу соответствия,
переводящую его собственные id классов в объединение. ensemble.names — это
объединение, и именно его несёт возвращаемый Results.
Рамки сливаются только в пределах одного имени класса. Класс, который знает лишь один участник, проходит насквозь без слияния и не штрафуется за это: пересчёт оценки использует знаменатель по каждому классу, поэтому класс, известный только одному участнику, сохраняет свою оценку.
При частичном пересечении в лог пишется предупреждение с перечислением
классов, которые есть не у всех участников. Именно это предупреждение стоит
читать внимательно, потому что чекпойнт, у которого имена классов — заглушки
вроде class_0, строит объединение, не пересекающееся ни с одним другим
участником, и слияния между участниками не происходит вовсе.
Участник, вернувший id класса за пределами собственного names, вызывает
RuntimeError.
Сторонние детекторы
from libreyolo import ExternalDetector, LibreEnsemble, SAMPLE_IMAGE def my_detector(pil_image): # Вернуть (boxes, scores, labels): xyxy в пикселях исходного изображения. return ([[100.0, 100.0, 200.0, 300.0]], [0.9], [0]) external = ExternalDetector(my_detector, names={0: "person"}) ensemble = LibreEnsemble(["LibreYOLO9s.pt", external])result = ensemble(SAMPLE_IMAGE)print(len(result.boxes))ExternalDetector(fn, names) оборачивает любой вызываемый объект, который
принимает PIL-изображение и возвращает (boxes, scores, labels), где рамки
заданы как xyxy в пикселях исходного изображения. Он проверяет число
аргументов, форму рамок, совпадение длин и то, что каждый id класса есть в
names, и сам применяет порог conf.
Так в слиянии участвует детектор, который LibreYOLO не загружал.
Вызов
from libreyolo import LibreEnsemble ensemble = LibreEnsemble(["LibreYOLO9s.pt", "LibreRFDETRs.pt"]) # Замените clip.mp4 на видеофайл на диске.for result in ensemble("clip.mp4", stream=True, vid_stride=2): print(result.frame_idx, len(result.boxes))Сигнатура вызова повторяет сигнатуру одиночной модели и принимает те же
источники: изображения, папки, списки, видео, захват экрана, веб-камеры и
сетевые потоки. Живым источникам нужен stream=True — по той же причине, что
и везде.
| Аргумент | По умолчанию | Примечания |
|---|---|---|
conf | 0.25 | Задаётся по участникам: скаляр применяется ко всем, либо по одному значению на участника |
iou | 0.45 | Собственный порог NMS каждого участника, а не порог слияния |
imgsz | None | list читается по участникам; int или кортеж применяется ко всем |
device | None | Скаляр или по одному на участника, так что участники могут находиться на разных устройствах |
classes | None | Фильтрует итоговый результат по id объединённых классов |
max_det | 300 | Применяется к итоговому результату |
Поскольку list для imgsz означает «по участникам», imgsz=[480, 640] — это
480 для первого участника и 640 для второго, а imgsz=(480, 640) — один
прямоугольный размер для всех. На этом различии легко споткнуться.
Участники вызываются с max_det не меньше 300 независимо от того, что вы
запросили, поэтому каждый работает с запасом, а ансамбль обрезает результат
один раз в конце.
Изображение декодируется один раз, и каждому участнику передаётся один и тот же
объект. batch принимается для совместимости и игнорируется; изображения
обрабатываются последовательно.
Что возвращается
Обычный Results — тот же тип, что возвращает одиночная модель, — с names,
равным объединённому пространству классов. Всё, что написано на странице
Работа с результатами, действует без изменений.
Единственное отличие — result.speed, который ансамбль всё-таки заполняет. Его
ключи — member_0, member_1 и так далее, плюс fusion, в миллисекундах. Это
единственное место в библиотеке, где speed заполняется.
Строки с неконечными значениями в рамках или оценках отбрасываются до слияния. Если участники находятся на разных устройствах, слияние выполняется на устройстве первого участника, который что-то вернул.
Чего ансамбль не умеет
val() и export() вызывают NotImplementedError и отсылают к участникам:
валидировать и экспортировать нужно каждого по отдельности. Метода train нет
вовсе, поэтому его вызов приводит к AttributeError.
Половинная точность на уровне ансамбля не обрабатывается. half=True попадает
в тот же путь с предупреждением и без эффекта, что и везде; настраивайте
точность на каждом участнике.
Интерфейса командной строки для ансамблей нет. Это Python API.