Перейти к содержанию

Пользовательские модели

В данном документе описаны соглашения и правила для создания пользовательских моделей для решения задач детектирования объектов, сегментации и классификации.

Оглавление


Быстрый старт

  1. Создайте директорию модели c файлами: coco_categories.json, model_info.json, model.py, __init__.py и весами weights*.*.
  2. Заполните model_info.json: укажите deploy_profile, task_type, img_size, weights_path, categories_path, при необходимости — model_attributes.
  3. Реализуйте класс Model в model.py по протоколу (CustomModelProtocol/CustomTorchModelProtocol), задайте корректные input_names/output_names.
  4. Все вспомогательные модули поместите в custom_modules/ и используйте абсолютные импорты from custom_modules import ....
  5. Назовите файлы весов по шаблонам ниже. В системе поддерживается только batch_size=1 — зафиксируйте это в именах и при экспорте.
  6. Проверьте загрузку модели и корректность preprocess/postprocess на тестовом изображении.

Структура директории пользовательской модели

Структура файлов

Директория с моделью должна содержать следующие обязательные файлы:

  • __init__.py — Пустой или служебный файл, обозначающий директорию модели как Python-пакет. Необходим для корректной загрузки модели и поддержки относительных импортов внутри model.py и связанных модулей.
  • coco_categories.json — JSON-файл с описанием категорий (классов), которые модель способна распознавать.
  • model_info.json — JSON-файл с метаданными и параметрами модели (например, версия, разрешение входного изображения, и т.д.).
  • model.py — Python-модуль, содержащий код для инициализации и работы модели.
  • Файл весов — Один или набор файлов с весами модели. Имя файла должно начинаться с префикса weights и иметь одно из допустимых расширений: .pt, .pth, .engine, .tar или .onnx.

Опциональная директория:

custom_modules/ — Директория для пользовательских Python-модулей (например, реализация нестандартного NMS, утилиты для предобработки данных и т.д.). Модули из этой директории должны импортироваться в основном файле model.py.

Пример корректной структуры директории:

├── coco_categories.json
├── model_info.json
├── model.py
├── weights.pt
└── custom_modules/       # Опциональная директория со вспомогательным кодом
    ├── __init__.py       # (Рекомендуется) Для упрощения импорта
    ├── nms.py           # Пользовательская реализация Non-Maximum Suppression
    └── utils.py         # Вспомогательные утилиты (постобработка, аннотации и т.д.)

Примечания

  • Все Python-модули внутри директории модели должны использовать относительные импорты.
  • Директория модели рассматривается как изолированный Python-пакет и загружается в собственном runtime-namespace, что исключает конфликты между разными моделями.
  • Отсутствие любого из обязательных файлов приводит к ошибке инициализации модели.

Соглашения на именование файлов

Для обеспечения однозначной идентификации параметров модели приняты строгие правила именования файлов с весами.

  1. Общее правило для всех форматов

Имя файла весов должно начинаться с префикса weights. Необходимо использовать только строчные буквы.

Веса модели ONNX

Имя весов содержит информацию о размерности входа модели, версии onnx и точности модели:

weights_h{height}_w{width}_b{batch}_c{channels}_onnx{onnx_version}_{precision}.onnx
Параметр Описание Пример значения
height Высота входного изображения (тензора) модели в пикселях. 640
width Ширина входного изображения (тензора) модели в пикселях. 640
batch Размер батча (пакета), на который настроена модель. 1
channels Количество каналов входного изображения (тензора). 3
onnx_version Версия спецификации ONNX, использованная для экспорта модели. 1.17.0
precision Числовая точность вычислений в модели. fp16 (FP16), fp32 (FP32)

Пример корректно сформированного имени файла:
weights_h224_w224_b1_c3_onnx1.17.0_fp16.onnx

Важно: Данный шаблон именования применяется исключительно к файлам с расширением .onnx.


Веса модели Torch

Особенных требований для именования весов для моделей в формате torch не предъявляется. Рекомендуется следовать общему правилу (п.1)

Примеры корректно сформированных имен файлов:
weights.pt, weights_resnet50-0676ba61.pth


Веса модели TensorRT

Имя файла должно содержать информацию о размерности входа модели, версии compute capability целевой GPU архитектуры, версии CUDA Toolkit, версии TensorRT и числовой точности модели:

weights_h{height}_w{width}_b{batch}_cc{compute_capability}_cuda{cuda_version}_trt{tensorrt_version}_{precision}.engine

Расшифровка параметров в имени файла:

Параметр Описание Пример значения
height Высота входного тензора модели в пикселях 224
width Ширина входного тензора модели в пикселях 224
batch Размер батча (пакета), на который оптимизирована модель 1
compute_capability Версия вычислительной возможности целевой GPU архитектуры (compute capability) 8.6, 7.5, 8.9
cuda_version Версия CUDA Toolkit, использованная для сборки TensorRT runtime 11.8, 12.2, 12.4
tensorrt_version Версия TensorRT, использованная для построения engine файла 8.6.1, 10.4.0
precision Числовая точность вычислений в модели fp16, fp32, int8

Пример корректно сформированного имени файла:
weights_h224_w224_b1_cc8.6_cuda11.8_trt8.6.1_fp16.engine

Важно: Данный шаблон именования применяется исключительно к файлам с расширением .engine.


Веса модели Tensorflow

Имя файла для весов в формате tensorflow ограничено наименованием и расширением.

Пример корректно сформированного имени файла:
weights_tf.tar

Внутри архива с весами обычно содержится

├── weights_tf
│   ├── assets  # (Опционально) Дополнительные ресурсы, используемые графом   ├── fingerprint.pb  # (Опционально) Фингерпринт для контроля версий   ├── saved_model.pb  # Основной файл графа модели и метаданных   └── variables   # Директория с обученными весами модели       ├── variables.data-00000-of-00001   # Файл с данными весов       └── variables.index     # Индексный файл для отображения тензоров

Описание содержимого: - saved_model.pb: Содержит определение архитектуры модели (граф операций) и метаинформацию (сигнатуры для inference). - assets/: Может содержать вспомогательные файлы, такие как словари для токенизации или пользовательские операторы. - variables/: Содержит непосредственно значения обученных параметров модели: - variables.index: Служебный файл, сопоставляющий имена тензоров с их местоположением в файле данных. - variables.data-00000-of-00001: Бинарный файл с сохраненными значениями весов (может иметь шаблон имени variables.data-XXXXX-of-YYYYY при шардировании). - fingerprint.pb: Используется фреймворком для проверки целостности и совместимости модели.

Важно: Данный шаблон именования и структура применяются исключительно к файлам с расширением .tar, содержащим модель в формате SavedModel.


Требование к расположению Python-модулей

Каждая модель загружается системой как изолированный Python-пакет с собственным runtime-namespace. Это обеспечивает корректную работу относительных импортов и полностью исключает конфликты между разными моделями.

artifacts/
├── __init__.py                # ОБЯЗАТЕЛЬНО: обозначает директорию модели как Python-пакет (может быть пустым)
├── model.py                   # Основной модуль модели
├── coco_categories.json
├── model_info.json
├── custom_modules/            # Рекомендуемая директория для вспомогательного кода   ├── __init__.py            # ОБЯЗАТЕЛЬНО   ├── nms.py                 # Пользовательская реализация Non-Maximum Suppression   ├── utils.py               # Вспомогательные утилиты (постобработка, аннотации и т.д.)   └── ...
└── weights*.*

Общие правила размещения кода

  • Все Python-модули модели находятся внутри одного пакета модели (artifacts).
  • Вспомогательный и переиспользуемый код рекомендуется размещать в директории custom_modules/.
  • Размещение вспомогательных модулей в корне директории модели допускается, но не рекомендуется, за исключением model.py.

Такое разделение улучшает читаемость структуры и упрощает сопровождение моделей.

Строгое требование к импортам:

Все импорты внутри модели ДОЛЖНЫ использовать относительную нотацию, так как модель загружается как изолированный Python-пакет.

В model.py:

# КОРРЕКТНО
from .custom_modules import nms, utils
from .custom_modules.yolo_utils import letterbox

# НЕКОРРЕКТНО
from custom_modules import nms, utils
import custom_modules.yolo_utils

Импорты внутри custom_modules/ (например, в nms.py):

# КОРРЕКТНО
from .utils import some_function

# НЕКОРРЕКТНО
from custom_modules.utils import some_function

Важно: Данное требование является обязательным. Модели, не следующие этому стандарту, не будут корректно работать в нашей системе. Это обусловлено фундаментальными ограничениями механизма импорта Python и архитектурными решениями системы загрузки моделей.


Описание содержимого файлов пользовательской модели

Описание файла model.py

Модуль model.py предназначен для реализации пользовательской модели. Он должен соответствовать одному из протоколов:

  • CustomModelProtocol — базовый протокол для моделей;
  • CustomTorchModelProtocol — расширение базового протокола для моделей на PyTorch.

Эти протоколы описывают контракт, которому должна соответствовать пользовательская модель: какие методы и атрибуты она должна иметь (например, preprocess, postprocess, input_names, output_names). Протоколы представлены в python пакете vlmodels_types

Класс модели

Ниже представлен код модуля model.py с реализацией пользовательского классификатора ResNet-18:

  1. Класс модели должен называться Model.

  2. В конструктор модели __init__ передаются именованные аргументы из:

  3. пользовательского интерфейса Node-RED (через model_attributes),

  4. или файла model_info.json (если model_attributes отсутствует или пуст).

  5. Класс обязан реализовать методы препроцессинга и постпроцессинга данных, как это описано в протоколе.

  6. Класс может быть параметризирован дженериком через протокол.

  7. Каждая пользовательская модель должна обязательно определять атрибуты input_names и output_names, пример:

    input_names = ["input"]
    output_names = ["logits"]
    

Эти списки фиксируют, какие тензоры подаются на вход и какие выдаются на выход. Таким образом система знает:

  • какие данные ожидать от вычислительного графа
  • и как связать результаты с последующим этапом postprocess.

Правила заполнения

input_names — список строк, описывающий все входные тензоры модели. Для классификатора это может быть один элемент ["input"], а для моделей с несколькими входами список может быть длиннее например, ["image", "metadata"].

output_names — список строк, описывающий все выходные тензоры. Для классификатора это, как правило, ["logits"], для детекции может быть, например, ["boxes", "scores", "labels"].

Имена должны совпадать с ключами словарей, которые возвращаются из метода preprocess.

Пример

Ниже приведён пример реализации ResNet-18 в файле model.py. В нём класс Model параметризован типом данных Classification, так как модель возвращает один класс и его вероятность.

class Model(CustomModelProtocol[Classification]):
    input_names = ["input"]
    output_names = ["logits"]

    def __init__(self, **model_attributes: AllowedModelAttributeValue) -> None:
        """
        Инициализирует модель ResNet18 из torchvision с заданными параметрами.

        Аргументы:
            **model_attributes: Словарь с возможными ключами:
                - 'conf_thres' (float): Порог уверенности для детекций.

            Параметры model_attributes могут поступать либо из пользовательского интерфейса Node-RED,
            либо из файла model_info.json (если model_attributes не передан или пуст).
        """
        self.conf_thres: float = model_attributes["conf_thres"]

        self.img_size = 224
        self.imagenet_mean = [0.485, 0.456, 0.406]
        self.imagenet_std  = [0.229, 0.224, 0.225]

    def preprocess(self, data: np.ndarray) -> Mapping[str, torch.Tensor]:
        img_rgb = cv2.cvtColor(data, cv2.COLOR_BGR2RGB)
        t = torch.from_numpy(img_rgb).permute(2, 0, 1).float() / 255.0  # (3,H,W), [0..1]
        t = TF.resize(t, 256, interpolation=transforms.InterpolationMode.BILINEAR)     # короткая сторона -> 256
        t = TF.center_crop(t, [self.img_size, self.img_size])                         # 224x224
        t = TF.normalize(t, mean=self.imagenet_mean, std=self.imagenet_std)
        return {self.input_names[0]: t.unsqueeze(0)}

    def postprocess(self, outputs: Mapping[str, torch.Tensor], categories: Sequence[str]) -> Classification:
        if self.output_names != list(outputs.keys()):
            raise ValueError("Output names mismatch")

        for name, tensor in outputs.items():
            if tensor.ndim == 1:
                tensor = tensor.unsqueeze(0)
            score, class_id = torch.max(F.softmax(tensor, dim=1), dim=1)

            return Classification(
                label=categories[int(class_id.item())],
                score=float(score.item()),
            )

Примечание: с полным кодом модели можно ознакомиться в приложении А.


Описание файла model_info.json

Файл model_info.json содержит метаинформацию и параметры модели, необходимые для её корректной загрузки и выполнения.

Поле Тип Обязательность Описание Пример значения Допустимые значения
model_attributes object Нет Словарь с настраиваемыми параметрами модели. Каждый параметр должен содержать поля type и default, а также может включать дополнительные метаданные для интерфейса. "conf_thres": {"type": "float", "default": 0.4, "ru_name": "Порог уверенности"} Любые пользовательские параметры следующих типов: int, float, str, bool, None. Поддерживаемые поля для каждого атрибута:
- type (обязательно): тип данных
- default (обязательно): значение по умолчанию
- ru_name (опционально): название параметра на русском языке для отображения в интерфейсе Node-RED
deploy_profile object Да Профили развертывания, поддерживаемые моделью. "deploy_profile": {"default_profile": "inline_cuda_trt_fp16", "profiles": ["inline_cuda_trt_fp16"]}
default_profile string Да Профиль развертывания по умолчанию. Должен быть одним из значений из списка profiles. "inline_cuda_trt_fp16" Должен соответствовать одному из значений в profiles
profiles array[string] Да Список поддерживаемых профилей развертывания. ["inline_cuda_trt_fp16", "inline_cpu_onnx_fp32"] ["inline_cuda_trt_fp16", "inline_cuda_trt_fp32", "inline_cuda_onnx_fp32", "inline_cpu_onnx_fp32", "inline_cuda_torch_fp32", "inline_cuda_torch_fp16", "inline_cpu_torch_fp32", "inline_cpu_torch_fp16", "inline_cuda_tf_fp32", "inline_cpu_tf_fp32"]
task_type string Да Тип задачи, которую решает модель. "image_detection" "image_detection", "image_instance_segmentation", "image_classification", "object_detection"
model_name string Да Уникальное имя модели. "yolov7tiny_coco_custom" Любая строка (латинские буквы, цифры, нижние подчеркивания)
model_version string Да Версия модели. "2" Строковое представление версии
img_size object Да Размер входного изображения модели. "img_size": {"height": 640, "width": 640}
height integer Да Высота входного изображения в пикселях. 640 Положительные целые числа (обычно степени двойки: 224, 320, 416, 512, 640, 1280)
width integer Да Ширина входного изображения в пикселях. 640 Положительные целые числа (обычно степени двойки: 224, 320, 416, 512, 640, 1280)
weights_path string Да Путь к файлу весов модели. Должен соответствовать шаблону weights*.*. "weights*.*"
categories_path string Да Путь к файлу с описанием категорий (классов), которые модель способна распознавать. "categories_path": "coco_categories.json"

Примечание

  1. Для model_attributes: Поле ru_name используется для локализации интерфейса в Node-RED. Если указано, значение из этого поля будет отображаться как название параметра вместо имени ключа.
  2. Профиль развертывания указывает системе устройство, бэкенд и точность с которой нужно инициализировать и выполнять модель. Например inline_cuda_trt_fp16:
  3. Устройство: CUDA (GPU)
  4. Бэкенд: TensorRT
  5. Точность: Float16

Пример содержимого файла model_info.json:

{
    "model_type": "custom_model",
    "model_attributes": {
        "conf_thres": {
            "type": "float",
            "default": 0.4,
            "ru_name": "Порог уверенности"
        },
        "iou_thres": {
            "type": "float",
            "default": 0.45
        }
    },
    "deploy_profile": {
        "default_profile": "inline_cuda_trt_fp16",
        "profiles": [
            "inline_cuda_trt_fp16"
        ]
    },
    "task_type": "image_detection",
    "model_name": "custom_yolov7tiny_coco",
    "model_version": "2",
    "img_size": {
        "height": 640,
        "width": 640
    },
    "weights_path": "weights*.*",
    "categories_path": "coco_categories.json"
}

Описание файла coco_categories.json

Файл coco_categories.json содержит описание категорий (классов), которые модель способна распознавать, в формате COCO с дополнительными полями для расширенной функциональности.

{
    "categories": [
        {
            "supercategory": "person",
            "id": 1,
            "name": "person",
            "is_violation": false,
            "is_primary": true,
            "exclude": false,
            "threshold": 0.65,
            "color": [0, 255, 0],
            "keypoints": [],
            "skeleton": [],
            "skeleton_color": [],
            "ru_name": "Человек",
            "full_ru_name": "Человек в кузове"
        }
    ]
}
Поле Тип Обязательность Описание Пример значения
supercategory string Да Родительская категория (общая группа). "person", "vehicle", "animal"
id integer Да Уникальный числовой идентификатор категории. Должен соответствовать ID, используемым моделью при инференсе. 1, 2, 3
name string Да Уникальное имя категории на английском языке (латинские символы, нижний регистр). "person", "car", "dog"
is_violation boolean Нет Флаг, указывающий, является ли категория нарушением. true, false
is_primary boolean Нет Флаг, указывающий, является ли категория основной (приоритетной) для отображения. true, false
exclude boolean Да Флаг, указывающий, следует ли исключать категорию из обработки и отображения. true, false
threshold float Да Порог уверенности для данной категории (переопределяет глобальный порог). 0.65, 0.5, 0.7
color array[int] Да Цвет для визуализации категории в формате [R, G, B]. [0, 255, 0], [255, 0, 0]
keypoints array[string] Нет Список ключевых точек для pose estimation моделей.
skeleton array[array[int]] Нет Соединения между ключевыми точками для pose estimation. [[16, 14], [14, 12]]
skeleton_color array[array[int]] Нет Цвета для соединений скелета в формате [R, G, B]. [[0, 255, 0], [255, 0, 0]]
ru_name string Да Локализованное короткое название категории на русском языке для отображения в интерфейсах. "Человек", "Автомобиль"
full_ru_name string Да Локализованное полное название категории на русском языке для отображения в интерфейсах. "Человек в кузове", "Автомобиль с кузовом"

Ограничения

Ограничения на используемые бэкенды

Пользовательские модели ограничены следующими бэкендами: ONNX, TORCH, TF, TRT. Выбор бэкенда определяет требования к формату весов модели и доступный функционал для инференса.

Ограничение на используемые версии и операторы

Пользовательские модели при использовании выбранного бэкенда будут ограничены установленными версиями библиотек, а также набором операторов, поддерживаемых выбранным фреймворком.

Поддерживаемая системой версия CUDA Toolkit - 12.6.0.

1. TensorRT - Версия: Поддерживается TensorRT 10.4.0 - Совместимость: Модели должны быть совместимы с compute capability целевых GPU - Операторы: Поддерживаются операторы из официальной документации NVIDIA - Ограничения: Некоторые custom operators могут требовать дополнительной реализации - Документация: NVIDIA TensorRT Documentation

2. ONNX - Opset: Поддерживается opset min: 7, max: 15 - Версия: ONNX Runtime 1.17.0 - Операторы: Полный список поддерживаемых операторов доступен в документации - Ограничения: Динамические размерности могут иметь ограниченную поддержку - Документация: ONNX Operators

3. Torch - Версия: PyTorch 2.7.0+cu126 - Форматы: Поддерживаются .pt и .pth файлы - Ограничения: Модели должны использовать только стандартные операторы PyTorch - Документация: PyTorch Documentation

4. TensorFlow - Версия: TensorFlow 2.20.0 - Формат: Поддерживается только SavedModel format - Ограничения: Модели должны использовать TF2.x compatible operations - Совместимость: Не поддерживаются устаревшие API TensorFlow 1.x - Документация: TensorFlow SavedModel Guide

Общие ограничения

  • Память: Модели должны укладываться в доступную GPU/CPU память
  • Размеры входов: Максимальные размеры входных тензоров ограничены возможностями бэкенда
  • Custom operators: Требуют дополнительной реализации и могут иметь ограниченную поддержку
  • Производительность: Скорость инференса зависит от совместимости модели с выбранным бэкендом

Рекомендация: Перед развертыванием проверяйте модель на совместимость с целевым бэкендом используя соответствующие инструменты валидации в системе.


Ограничения на размер батча и динамические размерности

Размер батча

  • Строгое ограничение: Поддерживается только размер батча равный 1
  • Причина: Архитектура системы оптимизирована для поточной обработки одиночных изображений в реальном времени
  • Требование к модели: Все модели должны быть сконвертированы и оптимизированы для работы с batch_size=1

Динамические размерности

  • Полное ограничение: Динамические размерности входных и выходных тензоров не поддерживаются
  • Требования к входным тензорам:
  • Все размерности должны быть фиксированными
  • Размеры входного изображения должны быть четко заданы в model_info.json
  • Формат: "img_size": {"height": Y, "width": X}

  • Конкретные ограничения:

  • ❌ Не поддерживается: None, -1, динамические размеры в любых размерностях
  • ✅ Обязательно: Фиксированные значения для всех размерностей тензоров

Пример:

{
    "img_size": {
        "height": 640,    // Фиксированная высота
        "width": 640      // Фиксированная ширина
    }
}

Ограничения на используемые Python-пакеты

Доступные пакеты в системном контейнере

Пользовательские модели могут использовать только те Python-пакеты, которые уже установлены в системном контейнере. Попытки импорта отсутствующих пакетов приведут к ошибке ModuleNotFoundError во время загрузки модели.

Базовый набор предустановленных пакетов:

Базовый набор предустановленных пакетов формируется при сборке контейнера и включает наиболее распространенные библиотеки для машинного обучения и обработки данных. Полный список доступных пакетов можно посмотреть в интерфейсе системы по адресу:

http://адрес_сервера/administrating/inference-packages

Строгие ограничения

  • Запрещено: Установка дополнительных пакетов через pip/apt в коде пользовательской модели
  • Запрещено: Импорт любых пакетов, отсутствующих в системном контейнере
  • Запрещено: Модификация системных путей Python sys.path для обхода ограничений

Приложение A: примеры реализации пользовательских моделей

В данном приложении показаны примеры реализаций пользовательский моделей для различных задач компьютерного зрения.

Интерфейс пользовательской модели

Интерфейс представляет собой формальный контракт (Protocol), который обеспечивает полиморфное взаимодействие между системой выполнения и пользовательскими моделями независимо от используемого бэкенда (ONNX, TensorRT, TensorFlow). Данный протокол гарантирует единообразное API для всех типов моделей, обеспечивая корректную интеграцию в inference pipeline.

Актуальные версии

Актуальные сигнатуры протоколов и типов находятся в библиотеке vlmodels_types.protocol. Реализации должны точно соответствовать актуальной версии интерфейсов, указанных в этой библиотеке.

Канонические определения типов и интерфейсов поддерживаются в пакете vlmodels_types. Для обеспечения совместимости необходимо использовать именно эти эталонные реализации.

Ниже приведены протоколы пользовательских моделей из библиотеки vlmodels_types v1.5.0

# Тип для возможных значений атрибутов модели
type AllowedModelAttributeValue = int | float | str | bool | None


class CustomModelProtocol[T_co: NamedTuple](Protocol):
    """
    Протокол `CustomModelProtocol` описывает интерфейс, который должна реализовать
    пользовательская модель onnx, tensorflow, tensorrt для интеграции с библиотекой.

    Атрибуты:
        input_names (Sequence[str]): Список имён входных тензоров, ожидаемых моделью.
        output_names (Sequence[str]): Список имён выходных тензоров, которые возвращает модель.

    Методы:
        __init__(**model_attributes: AllowedModelAttributeValue):
            Конструктор пользовательской модели.
            При инициализации в него автоматически передаётся набор параметров `model_attributes`,
            полученных либо из пользовательского интерфейса Node-RED, либо из файла `model_info.json`,
            если параметры явно не заданы.
            Каждый параметр должен соответствовать допустимым типам, описанным в `AllowedModelAttributeValue`.

        preprocess(data: NDArray | Sequence[NDArray]) -> Mapping[str, NDArray | torch.Tensor]:
            Выполняет предобработку исходного изображения или массива данных перед подачей в модель.
            На выходе должен вернуть словарь, где ключи соответствуют `input_names`, а значения —
            подготовленные входные тензоры в формате NumPy или PyTorch.
            По-умолчанию тензоры должны быть расположены на устройстве CPU. В обратном случае, могут
            возникать проблемы при использовании CPU модели вместо GPU.

        postprocess(outputs: Mapping[str, torch.Tensor], categories: Sequence[str]) -> T_co:
            Выполняет постобработку результатов работы модели.
            Аргумент `outputs` — словарь, где ключи соответствуют `output_names`, а значения —
            выходные тензоры модели в формате PyTorch.
            Аргумент `categories` — набор доступных классов/меток.
            Метод должен вернуть результат типа `T_co` (например, NamedTuple или другую согласованную структуру данных),
            содержащий итоговые детекции или предсказания.
    """

    input_names: Sequence[str]
    output_names: Sequence[str]

    def __init__(self, **model_attributes: AllowedModelAttributeValue) -> None: ...
    def preprocess(self, data: "NDArray | Sequence[NDArray]") -> "Mapping[str, NDArray | torch.Tensor]": ...
    def postprocess(self, outputs: "Mapping[str, torch.Tensor]", categories: Sequence[str]) -> T_co: ...

Для поддержки PyTorch моделей необходимо использовать CustomTorchModelProtocol, который расширяет возможности CustomModelProtocol добавлением методов для инициализации пользовательской PyTorch модели и постпроцессингом:

class CustomTorchModelProtocol[T_co: NamedTuple](CustomModelProtocol[T_co], Protocol):
    """
    Протокол `CustomTorchModelProtocol` расширяет базовый `CustomModelProtocol`
    и описывает интерфейс пользовательской модели, реализованной с использованием PyTorch.

    Дополнительно к методам и атрибутам базового протокола, модель должна
    предоставлять метод `build_torch_model` для инициализации и загрузки
    архитектуры модели в формате PyTorch.

    Методы:
        build_torch_model(artifacts_path: Path, device: str) -> torch.nn.Module:
            Создаёт и возвращает экземпляр PyTorch-модели, готовой к выполнению инференса.
            Аргументы:
                artifacts_path (Path): Путь к директории с весами модели в формате PyTorch.
                device (str): Устройство, на которое будет загружена модель (например, "cuda:0" или "cpu").
            Возвращает:
                torch.nn.Module: Инициализированная модель PyTorch, готовая к запуску.

        postprocess_torch(outputs: Any, categories: Sequence[str]) -> T_co:
            Выполняет постобработку результатов работы PyTorch-модели модели.
            Аргумент `outputs` — результат инференса модели.
            Аргумент `categories` — набор доступных классов/меток.
            Метод должен вернуть результат типа `T_co` (например, NamedTuple или другую согласованную структуру данных),
            содержащий итоговые детекции или предсказания.
    """

    def build_torch_model(self, artifacts_path: "Path", device: str) -> "torch.nn.Module": ...
    def postprocess_torch(self, outputs: Any, categories: Sequence[str]) -> T_co: ...

Требования к реализации для различных бэкендов

Для ONNX: - input_names/output_names должны точно соответствовать именам в ONNX графе - preprocess может возвращать как NumPy arrays, так и torch.Tensors. Рекомендуется использовать torch.Tensors для предотвращения промежуточных конвертаций при использовании профиля развертывания inline_cuda_onnx_fp32. При использовании профиля развертывания inline_cpu_onnx_fp32 рекомендуется использовать NumPy arrays. - postprocess получает outputs как torch.Tensor - не поддерживаются динамические размерности модели

Для PyTorch: - Поддерживаются только eager mode модели - preprocess может возвращать как NumPy arrays, так и torch.Tensors. Рекомендуется использовать torch.Tensors для предотвращения промежуточных конвертаций. - Гарантируется выполнение на том же device, что и модель - не использовать аргумент map_location при загрузке модели. Замечены проблемы совместимости при использовании профиля развертывания inline_cpu_torch_fp32

Для TensorRT: - input_names/output_names должны соответствовать именам в TensorRT engine - preprocess может возвращать как NumPy arrays, так и torch.Tensors. Рекомендуется использовать torch.Tensors для предотвращения промежуточных конвертаций. - не поддерживаются динамические размерности модели

Для TensorFlow: - Поддерживается только SavedModel format - input_names/output_names должны соответствовать сигнатуре SavedModel - preprocess может возвращать как NumPy arrays, так и torch.Tensors. Рекомендуется использовать NumPy arrays для предотвращения промежуточных конвертаций.

Гарантии системы: - Автоматическая обработка device placement (CPU/GPU) - Консистентность типов данных между preprocess/postprocess - Изоляция выполнения между различными моделями - Автоматическая валидация соответствия указанных input_names/output_name в модели

Пример пользовательской модели классификатора

Ниже представлена реализация пользовательской модели классификатора, которая использует предобученную на ImageNet модель ResNet18 из torchvision и работает на бэкендах ONNX, Torch, TensorRT. Модель имеет один вход ["input"], и возвращает ["logits"] который после постпроцессинга упаковывается в выходной тип данных Classification

Структура файлов директории пользовательской модели:

├── coco_categories.json    # JSON-файл с описанием категорий
├── model_info.json         # JSON-файл с метаданными и параметрами модели
├── model.py                # Python-модуль, содержащий код для инициализации и работы модели.
├── weights_h224_w224_b1_c3_onnx1.17.0_fp16.onnx                # сконвертированные веса ONNX в fp16
├── weights_h224_w224_b1_c3_onnx1.17.0_fp32.onnx                # сконвертированные веса ONNX в fp32
├── weights_h224_w224_b1_cc8.6_cuda11.8_trt8.6.1_fp16.engine    # сконвертированные веса TensorRT для целевой GPU архитектуры в fp16
├── weights_h224_w224_b1_cc8.6_cuda11.8_trt8.6.1_fp32.engine    # сконвертированные веса TensorRT для целевой GPU архитектуры в fp32
└── weights.pt              # веса модели для PyTorch

model.py:

from pathlib import Path
from collections.abc import Sequence, Mapping

import cv2
from numpy.typing import NDArray
import torch
import torch.nn.functional as F
import torchvision.transforms.functional as TF
from torchvision.models import resnet18
from torchvision import transforms
from vlmodels_types import Classification
from vlmodels_types.protocol.custom_model_protocol import AllowedModelAttributeValue, CustomTorchModelProtocol


class Model(CustomTorchModelProtocol[Classification]):
    input_names = ["input"]
    output_names = ["logits"]

    def __init__(self, **model_attributes: AllowedModelAttributeValue) -> None:
        """
        Инициализирует модель ResNet18 из torchvision с заданными параметрами.

        Аргументы:
            **model_attributes: Словарь с возможными ключами:
                - 'conf_thres' (float): Порог уверенности для детекций.

            Параметры model_attributes могут поступать либо из пользовательского интерфейса Node-RED,
            либо из файла model_info.json (если model_attributes не передан или пуст).
        """
        self.conf_thres: float = model_attributes["conf_thres"]

        self.img_size = 224
        self.imagenet_mean = [0.485, 0.456, 0.406]
        self.imagenet_std  = [0.229, 0.224, 0.225]

    # ==== Доп. метод для torch-бэкенда (опционально) ====
    def build_torch_model(self, artifacts_path: Path, device: str) -> torch.nn.Module:
        model = resnet18(weights=None)
        checkpoint = torch.load(artifacts_path / "weights.pt")  # не используем map_location
        model.load_state_dict(checkpoint)
        model.to(device)
        model.eval()
        return model

    def preprocess(self, data: NDArray) -> Mapping[str, torch.Tensor]:
        img_rgb = cv2.cvtColor(data, cv2.COLOR_BGR2RGB)
        t = torch.from_numpy(img_rgb).permute(2, 0, 1).float() / 255.0  # (3,H,W), [0..1]
        t = TF.resize(t, 256, interpolation=transforms.InterpolationMode.BILINEAR)     # короткая сторона -> 256
        t = TF.center_crop(t, [self.img_size, self.img_size])                         # 224x224
        t = TF.normalize(t, mean=self.imagenet_mean, std=self.imagenet_std)
        return {self.input_names[0]: t.unsqueeze(0)}

    def postprocess(self, outputs: Mapping[str, torch.Tensor], categories: Sequence[str]) -> Classification:
        if self.output_names != list(outputs.keys()):
            raise ValueError("Output names mismatch")

        for name, tensor in outputs.items():
            if tensor.ndim == 1:
                tensor = tensor.unsqueeze(0)
            score, class_id = torch.max(F.softmax(tensor, dim=1), dim=1)

            return Classification(
                label=categories[int(class_id.item())],
                score=float(score.item()),
            )

    def postprocess_torch(self, outputs: torch.Tensor, categories: Sequence[str]) -> Classification:
        if outputs.ndim == 1:
            outputs = outputs.unsqueeze(0)
        score, class_id = torch.max(F.softmax(outputs, dim=1), dim=1)

        return Classification(
            label=categories[int(class_id.item())],
            score=float(score.item()),
        )

model_info.json:

{
    "model_type": "custom_model",
    "model_attributes": {
        "conf_thres": {
            "type": "float",
            "default": 0.4,
            "ru_name": "Порог уверенности"
        }
    },
    "deploy_profile": {
        "default_profile": "inline_cuda_trt_fp16",
        "profiles": [
            "inline_cuda_trt_fp16",
            "inline_cuda_trt_fp32",
            "inline_cuda_onnx_fp32",
            "inline_cpu_onnx_fp32",
            "inline_cuda_torch_fp32",
            "inline_cpu_torch_fp32"
        ]
    },
    "task_type": "image_classification",
    "model_name": "custom_resnet18_imagenet",
    "model_version": "1",
    "img_size": {
        "height": 224,
        "width": 224
    },
    "weights_path": "weights*.*",
    "categories_path": "coco_categories.json"
}

Пример пользовательской модели детектора

Ниже представлена реализация пользовательской модели детектора, которая использует предобученную на MS COCO модель YoloV7 tiny из https://github.com/WongKinYiu/yolov7/releases и работает на бэкендах ONNX, Torch, TensorRT, Tensorflow. Модель имеет один вход ["images"], и возвращает ["output"] который после постпроцессинга упаковывается в выходной тип данных Detections

Структура файлов директории пользовательской модели:

├── coco_categories.json    # JSON-файл с описанием категорий
├── custom_modules          # Опциональная директория со вспомогательным кодом   ├── autoanchor.py           # https://github.com/WongKinYiu/yolov7/blob/main/utils/autoanchor.py   ├── common.py               # https://github.com/WongKinYiu/yolov7/blob/main/models/common.py   ├── datasets.py             # https://github.com/WongKinYiu/yolov7/blob/main/utils/datasets.py   ├── experimental.py         # https://github.com/WongKinYiu/yolov7/blob/main/models/experimental.py   ├── general.py              # https://github.com/WongKinYiu/yolov7/blob/main/utils/general.py   ├── __init__.py
│   ├── loss.py                 # https://github.com/WongKinYiu/yolov7/blob/main/utils/loss.py   ├── torch_utils.py          # https://github.com/WongKinYiu/yolov7/blob/main/utils/torch_utils.py   ├── yolo.py                 # https://github.com/WongKinYiu/yolov7/blob/main/models/yolo.py   └── yolo_utils.py           # вспомогательный файл для реализации letterbox, non_max_suppression, scale_coords
├── model_info.json         # JSON-файл с метаданными и параметрами модели
├── model.py                # Python-модуль, содержащий код для инициализации и работы модели
├── weights_h640_w640_b1_c3_onnx1.17.0_fp32.onnx                # сконвертированные веса ONNX в fp32
├── weights_h640_w640_b1_cc8.6_cuda11.8_trt8.6.1_fp16.engine    # сконвертированные веса TensorRT для целевой GPU архитектуры в fp16
├── weights_h640_w640_b1_cc8.6_cuda11.8_trt8.6.1_fp32.engine    # сконвертированные веса TensorRT для целевой GPU архитектуры в fp32
├── weights.pt              # веса модели для PyTorch
└── weights_tf.tar          # веса модели для Tensorflow

model.py:

from pathlib import Path
from collections.abc import Sequence, Mapping

from numpy.typing import NDArray
import torch

# модуль с вспомогательными функциями находится на одном уровне с моделью
from .custom_modules.yolo_utils import letterbox, non_max_suppression, scale_coords
from .custom_modules.experimental import attempt_load

from vlmodels_types import CustomTorchModelProtocol, Detections
from vlmodels_types.protocol.custom_model_protocol import AllowedModelAttributeValue


class Model(CustomTorchModelProtocol[Detections]):
    input_names = ["images"]
    output_names = ["output"]

    def __init__(self, **model_attributes: AllowedModelAttributeValue) -> None:
        """
        Инициализирует модель с заданными параметрами.

        Аргументы:
            **model_attributes: Словарь с возможными ключами:
                - 'iou_thres' (float): Порог Intersection over Union для non-max suppression.
                - 'conf_thres' (float): Порог уверенности для детекций.

            Параметры model_attributes могут поступать либо из пользовательского интерфейса Node-RED,
            либо из файла model_info.json (если model_attributes не передан или пуст).

        Атрибуты:
            height (int): Высота изображения в пикселях входного изображения.
            width (int): Ширина изображения в пикселях входного изображения.
            iou_thres (float): Порог IoU для non-max suppression.
            conf_thres (float): Порог уверенности для детекций.
            current_img_size (tuple[int, int] | None): Текущий размер изображения в формате (height, width).
        """
        self.iou_thres: float = model_attributes["iou_thres"]
        self.conf_thres: float = model_attributes["conf_thres"]

        self.height: int = 640
        self.width: int = 640
        self.current_img_size: tuple[int, int] | None = None    # (height, width)

    # ==== Доп. метод для torch-бэкенда (опционально) ====
    def build_torch_model(self, artifacts_path: Path, device: str) -> torch.nn.Module:
        model = attempt_load(artifacts_path / "weights.pt") # не используем map_location
        model.to(device)
        model.eval()
        return model

    def preprocess(self, data: NDArray) -> Mapping[str, torch.Tensor]:
        self.current_img_size = (data.shape[0], data.shape[1])
        image, _, _ = letterbox(data, (self.height, self.width), auto=False)
        tensor = torch.from_numpy(image)
        tensor = (tensor.permute(2, 0, 1).flip(0).unsqueeze(0) / 255).contiguous()
        return {self.input_names[0]: tensor}

    def postprocess(self, outputs: Mapping[str, torch.Tensor], categories: Sequence[str]) -> Detections:
        if self.output_names != list(outputs.keys()):
            raise ValueError("Output names mismatch")

        for name, tensor in outputs.items():
            pred = non_max_suppression(tensor, self.conf_thres, self.iou_thres)[0]
            pred[:, :4] = scale_coords(
                (self.height, self.width),
                pred[:, :4],
                (self.current_img_size[0], self.current_img_size[1]),
            ).round()

            bboxes = pred[:, :4].cpu().numpy().astype(int)
            labels = tuple(map(lambda x: categories[int(x)], pred[:, 5].cpu().numpy()))
            scores = pred[:, 4].cpu().numpy()
            return Detections(bboxes, labels, scores)

    def postprocess_torch(self, outputs: torch.Tensor, categories: Sequence[str]) -> Detections:
        pred = non_max_suppression(outputs, self.conf_thres, self.iou_thres)[0]
        pred[:, :4] = scale_coords(
            (self.height, self.width),
            pred[:, :4],
            (self.current_img_size[0], self.current_img_size[1]),
        ).round()

        bboxes = pred[:, :4].cpu().numpy().astype(int)
        labels = tuple(map(lambda x: categories[int(x)], pred[:, 5].cpu().numpy()))
        scores = pred[:, 4].cpu().numpy()
        return Detections(bboxes, labels, scores)

model_info.json:

{
    "model_type": "custom_model",
    "model_attributes": {
        "conf_thres": {
            "type": "float",
            "default": 0.4,
            "ru_name": "Порог уверенности"
        },
        "iou_thres": {
            "type": "float",
            "default": 0.45,
            "ru_name": "Порог IoU"
        }
    },
    "deploy_profile": {
        "default_profile": "inline_cuda_trt_fp16",
        "profiles": [
            "inline_cuda_trt_fp16",
            "inline_cuda_trt_fp32",
            "inline_cuda_onnx_fp32",
            "inline_cpu_onnx_fp32",
            "inline_cuda_torch_fp32",
            "inline_cuda_torch_fp16",
            "inline_cpu_torch_fp32",
            "inline_cpu_torch_fp16",
            "inline_cuda_tf_fp32",
            "inline_cpu_tf_fp32"
        ]
    },
    "task_type": "image_detection",
    "model_name": "custom_yolov7tiny_coco",
    "model_version": "2",
    "img_size": {
        "height": 640,
        "width": 640
    },
    "weights_path": "weights*.*",
    "categories_path": "coco_categories.json"
}

Пример пользовательской модели сегментатора

Ниже представлена реализация пользовательской модели instance segmentation, которая использует предобученную на MS COCO модель MaskRCNN из torchvision и работает на бэкенде Torch. Модель имеет один вход ["images"], и возвращает ["boxes", "labels", "scores", "masks"] которые упаковываются в выходной тип данных Detections

Структура файлов директории пользовательской модели:

├── coco_categories.json            # JSON-файл с описанием категорий
├── model_info.json                 # JSON-файл с метаданными и параметрами модели
├── model.py                        # Python-модуль, содержащий код для инициализации и работы модели
├── weights.pt                      # веса модели maskrcnn
└── weights_resnet50-0676ba61.pth   # веса модели для backbone resnet50

model.py:

from collections.abc import Sequence, Mapping
from pathlib import Path

from numpy.typing import NDArray
import numpy as np
import torch
from PIL import Image
from torchvision.models.detection import maskrcnn_resnet50_fpn, MaskRCNN_ResNet50_FPN_Weights
from vlmodels_types import Detections
from vlmodels_types.protocol.custom_model_protocol import AllowedModelAttributeValue, CustomTorchModelProtocol


class Model(CustomTorchModelProtocol[Detections]):
    input_names = ["images"]
    output_names = ["boxes", "labels", "scores", "masks"]

    def __init__(self, **model_attributes: AllowedModelAttributeValue) -> None:
        """
        Инициализирует модель MaskRCNN из torchvision с заданными параметрами.

        Аргументы:
            **model_attributes: Словарь с возможными ключами:
                - 'iou_thres' (float): Порог Intersection over Union для non-max suppression.
                - 'conf_thres' (float): Порог уверенности для детекций.

            Параметры model_attributes могут поступать либо из пользовательского интерфейса Node-RED,
            либо из файла model_info.json (если model_attributes не передан или пуст).
        """
        self.iou_thres: float = model_attributes["iou_thres"]
        self.conf_thres: float = model_attributes["conf_thres"]

        weights = MaskRCNN_ResNet50_FPN_Weights.DEFAULT
        self.preprocess_func = weights.transforms()

    # ==== Доп. метод для torch-бэкенда (опционально) ====
    def build_torch_model(self, artifacts_path: Path, device: str) -> torch.nn.Module:
        model = maskrcnn_resnet50_fpn(
            weights=None,
            weights_backbone=None,
            box_score_thresh=self.conf_thres,
            box_nms_thresh=self.iou_thres
        )

        # Загружаем веса backbone (ResNet50)
        backbone_state_dict = torch.load(artifacts_path / "weights_resnet50-0676ba61.pth")  # не используем map_location
        # Удаляем ключи, связанные с FC слоем
        keys_to_remove = [k for k in backbone_state_dict.keys() if k.startswith('fc.')]
        for key in keys_to_remove:
            backbone_state_dict.pop(key)

        checkpoint = torch.load(artifacts_path / "weights.pt")  # не используем map_location

        # Загружаем веса в модель
        model.backbone.load_state_dict(backbone_state_dict, strict=False)
        model.load_state_dict(checkpoint, strict=False)

        model.to(device)
        model.eval()
        return model

    def preprocess(self, data: NDArray) -> Mapping[str, torch.Tensor]:
        img = self.preprocess_func(Image.fromarray(data))
        return {self.input_names[0]: img.unsqueeze(0).cpu()}

    def postprocess_torch(self, outputs: torch.Tensor, categories: Sequence[str]) -> Detections:
        for out in outputs:
            # Избавиться от канала в каждой маске
            boxes = out["boxes"].cpu().numpy().astype(int)
            masks = np.squeeze(out["masks"].cpu().numpy(), axis=1)  # [N, 1, H, W] -> [N, H, W]
            # Применяем порог
            binary_masks = (masks > self.conf_thres).astype(np.uint8) * 255.0
            # Обрезаем маски
            binary_masks = [bin_mask[y1:y2, x1:x2] for bin_mask, (x1,y1,x2,y2) in zip(binary_masks, boxes, strict=True)]
            return Detections(
                boxes=boxes,
                labels=tuple(map(lambda x: categories[int(x)], out["labels"].cpu().numpy())),
                scores=out["scores"].cpu().numpy(),
                masks=binary_masks,
            )

Приложение Б: архитектура выполнения пользовательской модели

В данном приложении детально описывается внутренний механизм работы системы с экземплярами классов пользовательских моделей. Это поможет разработчикам понять, как система управляет жизненным циклом модели, обеспечивает изоляцию выполнения и автоматически передает все необходимые контексты и параметры.

Описание внутреннего механизма работы системы с пользовательской моделью

Для эффективной разработки и отладки пользовательских моделей важно понимать внутренний механизм их взаимодействия с системой. Ниже представлен упрощенный участок кода, демонстрирующий полный цикл инференса для бэкенда TensorRT.

Система автоматически управляет всем жизненным циклом модели: 1. Загружает модуль model.py пользовательской модели 2. Инициализирует экземпляр класса Model 3. Обеспечивает выполнение полного пайплайна предсказания 4. Автоматически передает все необходимые аргументы в методы модели 5. Обеспечивает очистку ресурсов модели и её выгрузку из памяти

```python def predict(self, image: "NDArray") -> T_co: """ Выполняет полный цикл предсказания модели на входном изображении.

Данный метод представляет собой унифицированный интерфейс, который 
система использует для взаимодействия с пользовательской моделью 
независимо от бэкенда (ONNX, Torch, TensorRT, TensorFlow).

Этапы выполнения:
1. **Препроцессинг**: Система вызывает метод `preprocess()` пользовательской модели,
   передавая входное изображение. Пользовательская модель отвечает за преобразование
   данных в формат, ожидаемый конкретным бэкендом (нормализация, ресайз, преобразование
   в тензор и т.п.).

2. **Инференс**: Подготовленные данные передаются во внутренний метод `forward()`
   выбранного бэкенда, который выполняет предсказание на акселераторе (GPU/CPU).

3. **Постпроцессинг**: Система автоматически передает raw output от модели и 
   дополнительные параметры (категории) в метод `postprocess()`
   пользовательской модели для формирования финального результата.

:param image: NDArray
    Входное изображение в формате numpy array. Система гарантирует корректность
    типа и формы данных перед передачей в модель.

:return: T_co
    Итоговый объект с результатами предсказания. Тип возвращаемого значения 
    определяется пользовательской моделью в Generic параметре T_co (обычно 
    это NamedTuple с четко описанной структурой).

Важно: Разработчику не нужно беспокоиться о передаче аргументов в методы - система 
автоматически обеспечивает передачу всех необходимых параметров:
- `categories` загружаются из coco_categories.json
- `outputs` содержат raw tensors непосредственно от inference движка
"""
# Этап 1: Пользовательский препроцессинг
inputs: Mapping[str, NDArray | torch.Tensor] = self.custom_model_instance.preprocess(image)

# Этап 2: Нативный инференс выбранного бэкенда
outputs: Mapping[str, torch.Tensor] = self.forward(inputs)

# Этап 3: Пользовательский постпроцессинг с автоматической передачей аргументов
return self.custom_model_instance.postprocess(
    outputs=outputs,
    categories=self.labels  # Автоматически загружается системой
)