Подготовка и аугментация датасетов для CV

Курс посвящен полному циклу работы с данными в машинном зрении: от сбора, фильтрации дефектов съемки и разметки до корректного разбиения выборок. Вы освоите продвинутые методы аугментации с библиотекой Albumentations и техники регуляризации (MixUp, CutMix), обеспечивающие устойчивость классификаторов к изменениям освещения и ракурсов.

Жизненный цикл данных в машинном зрении: доменный сдвиг, несбалансированность и феномен Data Leakage

Жизненный цикл данных в машинном зрении: доменный сдвиг, несбалансированность и феномен Data Leakage

В 2018 году исследователи из Медицинской школы Икана больницы Маунт-Синай и ряда других институтов опубликовали резонансную работу, где показали: сверточные нейросети для диагностики пневмонии по рентгеновским снимкам при переносе в сторонние клиники катастрофически теряют в точности. Анализ показал, что модель ориентировалась не на патологические изменения в легочной ткани, а на специфичные для конкретных больниц технические маркеры латерализации («L» / «R») и особенности рентгеновских аппаратов.

Нейросеть выучила случайную корреляцию в обучающей выборке вместо реальной физической закономерности. Это фундаментальная проблема современного прикладного Computer Vision: архитектуры ResNet или EfficientNet обладают огромной емкостью и способны идеально подогнать веса под любые артефакты данных. Если на этапе подготовки датасета допущены методологические ошибки, даже самая совершенная модель окажется бесполезной в продакшене.


Парадигма Data-Centric AI и жизненный цикл данных

В классическом машинном обучении долгое время доминировал подход Model-Centric AI: считалось, что датасет — это фиксированная сущность, а инженерная задача состоит в подборе более сложной архитектуры, оптимизатора или гиперпараметров.

В прикладном компьютерном зрении этот подход часто заходит в тупик. На практике 80%80\% прироста качества промышленных систем достигается не заменой бэкбона, а систематической работой над чистотой, репрезентативностью и согласованностью визуальных данных — переходом к парадигме Data-Centric AI.

Жизненный цикл данных в CV представляет собой не линейную цепочку, а замкнутый инженерный контур:

  1. Сбор данных (Data Collection): получение сырых кадров с камер, учет вариативности освещения, оптических искажений, углов обзора и редких состояний сцены.
  2. Очистка и дедупликация (Data Cleansing): отсев расфокусированных кадров, битых файлов, артефактов компрессии и почти идентичных дубликатов из непрерывных видеопотоков.
  3. Аннотирование (Annotation & Labeling): разметка изображений по строгим спецификациям и контроль межаннотаторского согласия.
  4. Валидационная изоляция и аугментация: строгое разделение на обучающую, валидационную и тестовую выборки с последующим расширением распределения через трансформации.
  5. Обучение и диагностика ошибок (Error Analysis): аудит краевых случаев (edge cases), на которых классификатор ошибается с высокой уверенностью.
  6. Мониторинг в продакшене (Drift Monitoring): отслеживание деградации точности при изменении физических условий на объекте и доразметка новых данных.

Ключевой инсайт раздела:

Модель машинного зрения является строгим математическим зеркалом распределения обучающих данных. Невозможно устранить дефект классификации подбором функции потерь, если в самом датасете нарушена причинно-следственная связь между признаками и метками.


Природа доменного сдвига (Domain Shift)

Математическая основа обучения с учителем опирается на фундаментальное допущение: обучающая выборка (DtrainD_{\text{train}}) и тестовая выборка (DtestD_{\text{test}}) порождены одним и тем же совместным вероятностным распределением:

Ptrain(X,Y)=Ptest(X,Y)P_{\text{train}}(X, Y) = P_{\text{test}}(X, Y)

где XX — пространство входных изображений (тензоров), а YY — пространство целевых меток классов.

В реальных системах машинного зрения это равенство практически никогда не выполняется во времени. Изменение температуры ламп подсветки, сезонная смена естественного освещения за окнами цеха, замена объектива камеры или естественный износ деталей приводят к доменному сдвигу (Domain Shift).

Совместную вероятность P(X,Y)P(X, Y) можно факторизовать двумя способами: через распределение признаков P(X)P(YX)P(X) \cdot P(Y|X) либо через априорные вероятности классов P(Y)P(XY)P(Y) \cdot P(X|Y). В зависимости от того, какой компонент изменяется, выделяют три типа сдвига.

Тип сдвига Математическое условие Пример в машинном зрении
Ковариатный сдвиг (Covariate Shift) Ptrain(X)Ptest(X)P_{\text{train}}(X) \neq P_{\text{test}}(X), но P(YX)P(Y|X) неизменно Камера контроля деталей переместилась на 15 см выше: деталь та же, но изменились масштаб и фон
Сдвиг концепта (Concept Shift) Ptrain(YX)Ptest(YX)P_{\text{train}}(Y|X) \neq P_{\text{test}}(Y|X), но P(X)P(X) неизменно Технологи изменили регламент: царапина длиной 2 мм раньше считалась нормой (класс Pass), а теперь — браком (Scrap)
Априорный сдвиг (Prior / Label Shift) Ptrain(Y)Ptest(Y)P_{\text{train}}(Y) \neq P_{\text{test}}(Y), но P(XY)P(X|Y) неизменно В обучающей выборке собрано 50%50\% брака, а на реальной линии реальный процент брака составляет лишь 0.2%0.2\%

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


Проблема несбалансированности классов (Class Imbalance)

В реальных промышленных и медицинских датасетах распределение меток P(Y)P(Y) подчиняется закону длинного хвоста (Long-Tail Distribution). При оптическом контроле печатных плат 99.5%99.5\% кадров содержат годные компоненты (нормальный класс), и лишь 0.5%0.5\% содержат редкие дефекты: микротрещины, перемычки припоя или сколы текстолита.

Мажорный класс (Норма):    ████████████████████████████████████ 99.5%
Минорный класс 1 (Припой): █ 0.3%
Минорный класс 2 (Скол):   ▍ 0.2%

Обучение глубокой CNN на несбалансированном датасете без специальной подготовки влечет три критические проблемы:

  1. Градиентное подавление: суммарный градиент функции потерь формируется преимущественно мажорным классом. Градиенты от редких образцов теряются в общем шуме оптимизатора, и веса фильтров настраиваются исключительно под распознавание нормы.
  2. Иллюзия сходимости метрик: модель, которая всегда выдает метку «Норма», формально достигает метрики Accuracy=0.995\text{Accuracy} = 0.995, полностью игнорируя 100%100\% бракованной продукции.
  3. Плохая калибровка вероятностей: распределение уверенности на выходе слоя Softmax\text{Softmax} оказывается смещено в сторону часто встречающегося класса.

Решение проблемы дисбаланса требует скоординированных действий на двух уровнях: на уровне данных (Data-level) и на уровне оптимизации (Algorithm-level).

                      Управление дисбалансом
                                │
        ┌───────────────────────┴───────────────────────┐
        ▼                                               ▼
Уровень данных (Data-level)                 Уровень модели (Algorithm-level)
- Undersampling мажорного класса            - Взвешенный Cross-Entropy Loss
- Oversampling минорного класса             - Focal Loss (динамический фокус на сложных)
- Синтез и целенаправленная аугментация     - Пороговая калибровка (Decision Thresholding)

На уровне данных балансировка достигается отбором наиболее информативных кадров нормы (Random / Cluster Undersampling) и многократным расширением минорных классов с помощью агрессивных геометрических и фотометрических аугментаций.


Феномен Data Leakage в задачах компьютерного зрения

Утечка данных (Data Leakage) — это непреднамеренное проникновение информации из валидационной или тестовой выборки в процесс обучения модели. В результате модель демонстрирует превосходные метрики на этапе экспериментов, но оказывается неработоспособной на независимых данных.

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

ОШИБКА: Случайное разбиение видеопотока на кадры (Data Leakage)
Видеопоток (30 FPS):  [ Кадр 1 ] [ Кадр 2 ] [ Кадр 3 ] [ Кадр 4 ] [ Кадр 5 ] ...
Случайный сплит:        TRAIN      VAL       TRAIN      VAL       TRAIN
                      (Модель фактически запоминает соседние одинаковые кадры)

ПРАВИЛЬНО: Групповое разбиение по сессиям/объектам (Group Split)
Видеосессия А (Деталь 1):  [ Кадр 1 ] [ Кадр 2 ] [ Кадр 3 ]  ──►  ТОЛЬКО TRAIN
Видеосессия Б (Деталь 2):  [ Кадр 4 ] [ Кадр 5 ] [ Кадр 6 ]  ──►  ТОЛЬКО VAL

Основные векторы утечки в CV:

  1. Временная корреляция (Temporal Leakage): При нарезке видеопотока с частотой 30 кадров/сек30\text{ кадров/сек} соседние кадры tt и t+1t+1 практически идентичны (разница в несколько пикселей из-за шума матрицы). Если применить стандартное случайное разбиение train_test_split, кадр tt попадет в train, а t+1t+1 — в val. Модель просто интерполирует запомненные пиксели, а не учится обобщать геометрию объектов.
  2. Пространственная / субъектная корреляция (Spatial & Group Leakage): Снимки одной и той же печатной платы, сделанные под пятью разными ракурсами или при разном освещении, попадают одновременно в обучающую и проверочную части. Сеть выучивает уникальные царапины конкретного образца текстолита, а не паттерн дефекта.
  3. Утечка на этапе аугментации (Data Augmentation Leakage): Грубейшая ошибка конструирования пайплайна: применение офлайн-аугментации ко всему массиву изображений до разделения на фолды. В этом случае аугментированная копия изображения из train оказывается в val.
  4. Утечка глобальной нормализации: Расчет средних поканальных значений μc\mu_c и стандартных отклонений σc\sigma_c для Z-score стандартизации по объединенному массиву всех имеющихся снимков, включая валидационные.

Сквозной пример: анатомия скрытой утечки на конвейере

Рассмотрим практический сценарий: разрабатывается система классификации шестерен на два класса: «Без дефектов» и «Скол зуба».

Камера установлена над конвейером и непрерывно делает 10 кадров каждой проходящей детали, пока та движется по полю зрения (разные углы поворота и смещения). Всего через систему прошло 500500 шестерен, получено 50005000 снимков.

Сценарий А (Случайный сплит 80/20 по файлам):
- 5000 файлов перемешаны в случайном порядке.
- 4000 кадров ушли в Train, 1000 — в Val.
- Каждая шестеренка представлена в среднем 8 кадрами в Train и 2 кадрами в Val.
- Результат на валидации: F1 = 0.992.
- Результат на новой партии в цеху: F1 = 0.684.

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

Сценарий Б (Строгий групповой сплит по ID деталей):
- Выборка разделена по идентификаторам объектов: 400 уникальных шестерен (4000 кадров) в Train, 100 уникальных шестерен (1000 кадров) в Val.
- Результат на валидации: F1 = 0.761.
- Результат на новой партии в цеху: F1 = 0.758.

В сценарии «Б» валидационная оценка точно отражает реальную обобщающую способность сети. Значение 0.7610.761 честно указывает инженеру на необходимость применения регуляризации и аугментаций, избавляя от ложных иллюзий на этапе прототипирования.


Резюме

Устойчивость моделей компьютерного зрения к реальным условиям эксплуатации закладывается задолго до запуска цикла оптимизации весов:

  • Переход к Data-Centric AI фиксирует приоритет качества, чистоты и репрезентативности данных над усложнением нейросетевых архитектур.
  • Доменный сдвиг нарушает равенство Ptrain(X,Y)=Ptest(X,Y)P_{\text{train}}(X, Y) = P_{\text{test}}(X, Y), проявляясь в форме ковариатного сдвига (изменение фона, света, оптики) или сдвига концепта (изменение критериев разметки).
  • Дисбаланс классов приводит к градиентному доминированию мажорных категорий и требует целенаправленного ресемплинга и синтеза редких визуальных состояний.
  • Утечка данных (Data Leakage) в CV возникает из-за пространственно-временных корреляций кадров и некорректного порядка аугментации, разрушая адекватность валидации.

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

Очистка и разметка визуальных данных: дедупликация, фильтрация артефактов и протоколы аннотирования

Очистка и разметка визуальных данных: дедупликация, фильтрация артефактов и протоколы аннотирования

В 2019 году масштабный аудит классического бенчмарка CIFAR-10 выявил парадокс: около 3.3% тестовых изображений оказались точными копиями или модификациями обучающих сэмплов, а в датасете ImageNet доля некорректных или неоднозначных меток превышает 10%. Даже идеально выстроенная сверточная сеть с архитектурой ResNet или EfficientNet при обучении на таком материале выучивает не обобщающие закономерности, а паразитные шумы и зеркальные дубли.

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


Проблема дубликатов: почему криптографические хеши не работают в CV

Попытка найти дубликаты с помощью стандартных криптографических хеш-функций (MD5, SHA-256) в машинном зрении терпит крах. Криптографические хеши проектируются с расчетом на лавинный эффект: изменение ровно одного младшего бита в одном пикселе кардинально меняет все 256 бит результирующего хеша.

В реальных условиях одно и то же изображение, сохраненное с разным качеством JPEG, пережатое в PNG или уменьшенное на пару пикселей, является семантическим дубликатом. Однако с точки зрения MD5 это два совершенно не связанных массива байтов.

Для визуальных данных применяется перцептивное хеширование (Perceptual Hashing). Задача перцептивного хеша — сформировать компактный битовый отпечаток (обычно 64 бита), который кодирует низкочастотную структуру сцены и остается устойчивым к масштабированию, сжатию и малым фотометрическим флуктуациям.

Характеристика Криптографические хеши (MD5, SHA-256) Перцептивные хеши (aHash, dHash, pHash)
Цель Обнаружение любых побитовых изменений Оценка визуального подобия контента
Свойство непрерывности Лавинный эффект (d(x,x)50%d(x, x') \approx 50\% битов) Метрическая непрерывность (d(x,x)0d(x, x') \to 0 при xxx \to x')
Устойчивость к JPEG Нулевая (хеш полностью разрушается) Высокая (сохраняется в пределах малого расстояния)
Метрика сравнения Строгое равенство (hash_A == hash_B) Расстояние Хэмминга (DHthresholdD_H \leq \text{threshold})

Семейство перцептивных хешей: aHash, dHash и pHash

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

  1. aHash (Average Hash): изображение масштабируется до 8×88 \times 8, переводится в градации серого, вычисляется средняя яркость 64 пикселей. Бит устанавливается в 1, если яркость пикселя выше средней, и в 0, если ниже. Это самый быстрый, но наименее устойчивый к неравномерному освещению метод.
  2. dHash (Difference Hash): кадр сжимается до размера 9×89 \times 8. Алгоритм сравнивает яркости соседних по горизонтали пикселей (P[x]>P[x+1]P[x] > P[x+1]). Это дает 8 строк по 8 сравнений (64 бита), эффективно улавливая направления горизонтальных градиентов.
  3. pHash (Perceptual Hash на базе DCT): наиболее робастный промышленный стандарт, использующий дискретное косинусное преобразование (Discrete Cosine Transform, DCT).

Алгоритмический конвейер pHash

Математический алгоритм вычисления pHash состоит из пяти шагов:

  1. Редукция разрешения: изображение приводится к размеру 32×3232 \times 32 в градациях серого. Это отсекает высокочастотные шумы и сглаживает мелкие текстуры.
  2. Вычисление 2D-DCT: к матрице 32×3232 \times 32 применяется двумерное дискретное косинусное преобразование. Оно переводит пространственное распределение яркости в частотный спектр.
  3. Выделение низкочастотного ядра: из матрицы коэффициентов DCT 32×3232 \times 32 извлекается верхний левый блок 8×88 \times 8 (64 коэффициента). Именно здесь сосредоточена основная визуальная энергия кадра. Самый первый коэффициент (0,0)(0, 0), отвечающий за среднюю яркость сцены (DC-компонента), исключается из дальнейшего расчета, что делает хеш инвариантным к глобальному изменению освещенности.
  4. Медианная бинаризация: вычисляется медиана оставшихся 63 коэффициентов.
  5. Генерация 64-битного слова: каждый коэффициент сравнивается с медианой:

Hi={1,если Ci>median(C)0,иначеH_i = \begin{cases} 1, & \text{если } C_i > \text{median}(C) \\ 0, & \text{иначе} \end{cases}

Степень схожести двух кадров оценивается через расстояние Хэмминга (DHD_H) — количество несовпадающих битов между их хешами:

DH(h1,h2)=i=063(h1,ih2,i)D_H(h_1, h_2) = \sum_{i=0}^{63} (h_{1,i} \oplus h_{2,i})

где \oplus — побитовая операция XOR.

Инженерное правило дедупликации: Для 64-битного pHash пара изображений считается визуальными дубликатами при DH5D_H \leq 5. Значения 6DH106 \leq D_H \leq 10 соответствуют схожим композициям с модификациями (кадрирование, наложение текста), а DH>10D_H > 10 указывает на разные кадры.


Автоматический аудит технического брака съемки

Наряду с дубликатами в сырой массив данных неизбежно попадают кадры с оптическим браком: размытие из-за движения конвейера или расфокусировки объектива, а также критические пересветы и глубокие тени. Если такой кадр попадет в обучение, модель попытается подогнать веса под отсутствующие или разрушенные признаки.

Детекция расфокусировки и смаза через вариацию Лапласиана

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

Для количественной оценки резкости применяется оператор Лапласа (2\nabla^2), вычисляющий вторую пространственную производную скалярного поля яркости:

2I=2Ix2+2Iy2\nabla^2 I = \frac{\partial^2 I}{\partial x^2} + \frac{\partial^2 I}{\partial y^2}

На дискретной сетке изображения оператор реализуется сверткой с 2D-ядром:

K=[010141010]K = \begin{bmatrix} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0 \end{bmatrix}

Количественная мера резкости кадра выражается через дисперсию (вариацию) отклика оператора Лапласа:

Blur Metric=Var(2I)=1Nx,y(L(x,y)μL)2\text{Blur Metric} = \text{Var}(\nabla^2 I) = \frac{1}{N} \sum_{x, y} \left( L(x, y) - \mu_L \right)^2

где L(x,y)L(x, y) — результат свертки кадра с ядром Лапласа, μL\mu_L — среднее значение матрицы LL, а NN — общее число пикселей.

Если изображение четкое, матрица второй производной содержит множество больших по модулю положительных и отрицательных значений вдоль кромок, что дает высокую дисперсию (Var>150\text{Var} > 150). Если кадр размыт, перепады яркости плавные, значения отклика близки к нулю, и дисперсия падает (Var<50\text{Var} < 50).

Фотометрический аудит: клиппинг и экстремальные экспозиции

Второй критерий брака — потеря информации в динамическом диапазоне сенсора:

  • Underexposure (Недоэкспозиция): накопление массы пикселей в нулевом бине гистограммы (I(x,y)=0I(x, y) = 0). Информативность текстуры в тенях полностью утрачивается.
  • Overexposure (Переэкспозиция / Блики): насыщение фотодетекторов матрицы, приводящее к клиппингу в значение 255255.

Кадр отбраковывается программным фильтром, если доля пикселей в пограничных диапазонах превышает критический порог:

NI2Ntotal>θdarkилиNI253Ntotal>θbright\frac{N_{I \leq 2}}{N_{\text{total}}} > \theta_{\text{dark}} \quad \text{или} \quad \frac{N_{I \geq 253}}{N_{\text{total}}} > \theta_{\text{bright}}

Для задач промышленной дефектоскопии эмпирический порог θbright\theta_{\text{bright}} обычно фиксируется на уровне 0.050.05 (не более 5% площади кадра в насыщении).


Протоколы разметки и контроль межаннотаторского согласия

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

Для измерения надежности разметки нельзя использовать простую долю совпадений (Percent Agreement), так как она не учитывает вероятность случайного угадывания. Индустриальным стандартом оценки качества классификационной разметки является каппа Коэна (κ\kappa).

κ=pope1pe\kappa = \frac{p_o - p_e}{1 - p_e}

где:

  • pop_o (observed agreement) — наблюдаемая относительная доля согласованных решений между двумя экспертами:

po=i=1CniiNp_o = \frac{\sum_{i=1}^C n_{ii}}{N}

где niin_{ii} — число сэмплов, которым оба эксперта присвоили класс ii, а NN — общий объем выборки.

  • pep_e (expected agreement) — гипотетическая вероятность случайного совпадения их ответов при сохранении предельных распределений:

pe=i=1C(R1,iN×R2,iN)p_e = \sum_{i=1}^C \left( \frac{R_{1, i}}{N} \times \frac{R_{2, i}}{N} \right)

где R1,iR_{1, i} и R2,iR_{2, i} — общее количество объектов, отнесенных к классу ii первым и вторым экспертом соответственно.

                  Эксперт 2
              Норма    Дефект   Всего
Эксперт 1
  Норма        80        5        85
  Дефект       10        5        15
  Всего        90       10       100 (N)

В данном примере:

  • po=(80+5)/100=0.85p_o = (80 + 5) / 100 = 0.85
  • pe=(85/100×90/100)+(15/100×10/100)=0.765+0.015=0.780p_e = (85/100 \times 90/100) + (15/100 \times 10/100) = 0.765 + 0.015 = 0.780
  • κ=0.850.78010.780=0.0700.2200.318\kappa = \frac{0.85 - 0.780}{1 - 0.780} = \frac{0.070}{0.220} \approx 0.318

Несмотря на кажущуюся высокой точность совпадения 85%, каппа Коэна составляет всего 0.3180.318, сигнализируя о неприемлемо низком качестве согласованности при обнаружении редкого класса дефектов.

Шкала интерпретации каппы Коэна (Landis & Koch)

Значение κ\kappa Уровень согласованности Решение по разметке
<0.20< 0.20 Незначительный (Slight) Полный пересмотр инструкций и повторное обучение асессоров
0.210.400.21 - 0.40 Умеренный (Fair) Непригодно для production; устранение неоднозначностей в гайдлайнах
0.410.600.41 - 0.60 Средний (Moderate) Допустимо для черновых экспериментов
0.610.800.61 - 0.80 Существенный (Substantial) Промышленный стандарт качества
0.811.000.81 - 1.00 Почти идеальный (Almost Perfect) Эталонный датасет

Архитектура консенсусной разметки и ловушки валидации

Для удержания κ0.75\kappa \geq 0.75 в производственный контур внедряют регламенты арбитража:

  1. Кросс-разметка с перекрытием (Overlap): от 15% до 20% датасета размечается независимо минимум двумя асессорами для непрерывного вычисления κ\kappa.
  2. Контрольные сэмплы (Honey pots / Gold Standard): в поток задач скрытно подмешиваются заранее верифицированные ведущим инженером эталонные кадры. Если точность асессора на honey pots падает ниже 95%, его разметка блокируется и отправляется на перепроверку.
  3. Консилиумный арбитраж (Majority Voting / Expert Adjudication): сэмплы с конфликтом меток передаются старшему эксперту домена для вынесения окончательного вердикта.

Архитектура сквозного конвейера подготовки данных

Объединяя рассмотренные алгоритмы, инженер формирует детерминированный модуль предварительной очистки сырых данных:

[Сырой поток изображений]
          │
          ▼
┌─────────────────────────────────┐
│ 1. Фотометрический аудит        │ ──(Клиппинг > 5%)──► [Отбраковка: Экспозиция]
└─────────────────────────────────┘
          │ (Корректная экспозиция)
          ▼
┌─────────────────────────────────┐
│ 2. Оценка резкости (Лапласиан)  │ ──(Var < Threshold)─► [Отбраковка: Смаз]
└─────────────────────────────────┘
          │ (Резкие кадры)
          ▼
┌─────────────────────────────────┐
│ 3. Расчет 64-битного pHash      │
└─────────────────────────────────┘
          │
          ▼
┌─────────────────────────────────┐
│ 4. Дедупликация (Hamming Dist)  │ ──(Dist <= 5)──────► [Удаление дубликата]
└─────────────────────────────────┘
          │ (Уникальные валидные сэмплы)
          ▼
┌─────────────────────────────────┐
│ 5. Разметка + Honey Pots +      │
│    Контроль каппы Коэна         │
└─────────────────────────────────┘
          │
          ▼
[Очищенный и размеченный эталонный датасет]

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

Архитектура валидации: стратифицированное и групповое разбиение выборок в задачах классификации

Архитектура валидации: стратифицированное и групповое разбиение выборок в задачах классификации

Модель классификации дефектов печатных плат демонстрирует на валидации безупречный показатель F1=0.994F_1 = 0.994, однако в первый же день работы на конвейере точность падает до 0.6180.618. Причина не в архитектуре сверточной сети и не в оптимизаторе. В процессе валидации сработали сразу две скрытые уязвимости: наивное случайное разбиение размножило кадры одних и тех же серий плат между обучением и проверкой, а редкие дефекты случайным образом выпали из валидационного набора.

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

Ограничения Holdout и переход к K-Fold кросс-валидации

Простейший подход к валидации — отложенная выборка (Holdout), при которой исходный массив данных фиксированно делится на обучающую (train\text{train}), валидационную (val\text{val}) и тестовую (test\text{test}) части (например, в пропорции 70/15/1570 / 15 / 15).

При работе с визуальными датасетами малого и среднего объема (от сотен до десятков тысяч изображений) Holdout страдает высокой выборочной дисперсией: удачный или неудачный случайный срез может завысить или занизить валидационную оценку на 515%5\text{--}15\%. Если редкий класс представлен всего 20 изображениями, в валидационный набор объемом 15%15\% попадет лишь 3 изображения. Ошибка классификатора всего на одном из них мгновенно снижает метрику полноты (Recall\text{Recall}) по данному классу с 1.01.0 до 0.660.66.

Для получения статистически устойчивой оценки применяется K-Fold кросс-валидация. Весь датасет делится на KK непересекающихся подмножеств (фолдов) одинакового размера:

D=k=1KFk,FiFj=при ijD = \bigcup_{k=1}^{K} F_k, \quad F_i \cap F_j = \emptyset \quad \text{при } i \neq j

Здесь:

  • DD — полное множество доступных размеченных сэмплов датасета;
  • KK — общее количество блоков (обычно выбирается K=5K = 5 или K=10K = 10);
  • FkF_kkk-й валидационный фолд, содержащий ровно 1K\frac{1}{K}-ю часть всех наблюдений;
  • Условие FiFj=F_i \cap F_j = \emptyset гарантирует отсутствие дублирования сэмплов между фолдами.

Процедура выполняется итеративно KK раз: на каждом шаге kk подмножество FkF_k выступает в роли валидационной выборки, а объединение остальных K1K-1 подмножеств формирует обучающую выборку. Итоговая метрика качества MM рассчитывается как среднее арифметическое оценок по всем KK итерациям:

Mfinal=1Kk=1KMkM_{\text{final}} = \frac{1}{K} \sum_{k=1}^{K} M_k

где MkM_k — значение метрики (например, F1-scoreF_1\text{-score} или Accuracy\text{Accuracy}), полученное на kk-м валидационном фолде.

Кросс-валидация гарантирует, что каждый кадр из доступного набора данных ровно один раз побывал в роли валидационного наблюдения. Однако стандартный случайный K-Fold разрушает структуру данных при наличии дисбаланса классов и коррелированных сессий съемки.

Стратификация: сохранение априорных распределений P(Y)P(Y)

В промышленных задачах машинного зрения распределение классов почти всегда асимметрично: количество годных изделий на порядки превосходит число бракованных экземпляров. Случайное разбиение выборки приводит к ковариации целевой переменной между фолдами: в один фолд может попасть 8%8\% дефектов, а в другой — лишь 1%1\%.

Стратификация (Stratification) — процедура разбиения датасета, гарантирующая идентичность процентного соотношения классов целевой переменной во всех сформированных подмножествах (фолдах).

При стратифицированном разбиении для любого класса c{1,,C}c \in \{1, \dots, C\} и для любого фолда k{1,,K}k \in \{1, \dots, K\} строго соблюдается равенство долей:

P(Y=cFk)P(Y=cD)P(Y = c \mid F_k) \approx P(Y = c \mid D)

где P(Y=cD)P(Y = c \mid D) — доля объектов класса cc в полном датасете DD, а P(Y=cFk)P(Y = c \mid F_k) — доля объектов того же класса в фолде FkF_k.

Если в датасете из 10 000 кадров класс «Трещина» составляет 2%2\% (200 кадров), то при 5-фолдной стратифицированной кросс-валидации (StratifiedKFold) каждый валидационный фолд объемом 2000 кадров гарантированно будет содержать ровно по 40 изображений трещин. Это устраняет скачки функции потерь и стабилизирует расчет метрик.

Групповая изоляция: разрыв скрытых корреляций

Стратификация решает проблему распределения меток P(Y)P(Y), но полностью игнорирует внутренние пространственно-временные связи между кадрами P(X)P(X). В задачах компьютерного зрения изображения редко бывают независимыми и одинаково распределенными (i.i.d.\text{i.i.d.}):

  • Видеопоток и серийная съемка: 50 последовательных кадров одной детали под разными ракурсами имеют почти идентичный фон, текстуру материала и микродефекты.
  • Оптические модули и осветители: кадры, снятые на конвейерной линии №1 при утреннем освещении, формируют собственный визуальный домен.
  • Медицинские и биологические серии: серия из 20 снимков сетчатки или срезов ткани, полученная от одного пациента.

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

Группа (Group ID) — дискретный мета-идентификатор объекта, субъекта или непрерывной сессии наблюдения, объединяющий визуально и контекстно зависимые изображения.

Правило Group K-Fold формулируется в виде жесткого топологического запрета: все изображения с одинаковым Group ID обязаны целиком попадать либо в обучающую выборку, либо в валидационную, но никогда не разделяться между ними:

gG,!k{1,,K}:SgFk\forall g \in G, \quad \exists ! k \in \{1, \dots, K\}: \quad S_g \subseteq F_k

где GG — множество всех уникальных групп, SgS_g — подмножество всех изображений с групповым идентификатором gg, а !\exists ! означает существование единственного фолда FkF_k, содержащего все элементы группы gg.

Конфликт ограничений и алгоритм StratifiedGroupKFold

Когда датасет одновременно несбалансирован по классам и содержит группы зависимых изображений, возникает математический конфликт требований:

  1. Требование GroupKFold: не разрывать группы SgS_g.
  2. Требование StratifiedKFold: строго выдерживать доли классов P(Y)P(Y) в каждом фолде.

Если группа №42 содержит 15 изображений редкого класса «Прожог», перемещение этой неделимой группы целиком в фолд №1 мгновенно перекашивает баланс классов в фолде №1 относительно фолдов №2–5. Точное одновременное удовлетворение обоих условий представляет собой NP-трудную комбинаторную оптимизационную задачу (обобщенный многомерный вариант задачи о разбиении множеств или задачи о рюкзаке).

Для практического решения этой проблемы в библиотеке scikit-learn применяется жадный эвристический алгоритм StratifiedGroupKFold.

Логика работы жадного алгоритма StratifiedGroupKFold

  1. Агрегация метаданных: для каждой уникальной группы gg вычисляется вектор количества вхождений каждого класса vg=[ng,1,ng,2,,ng,C]\mathbf{v}_g = [n_{g,1}, n_{g,2}, \dots, n_{g,C}].
  2. Сортировка групп: группы упорядочиваются по убыванию их суммарного веса или представленности наиболее редких классов. Группы с редкими классами и большим количеством сэмплов распределяются в первую очередь.
  3. Итеративное назначение: для очередной группы gg алгоритм оценивает все KK фолдов и выбирает тот фолд, добавление группы в который минимально отклонит текущее распределение классов этого фолда от целевой идеальной пропорции датасета.
  4. Балансировка объемов: при равенстве распределений классов группа отправляется в фолд с наименьшим общим числом уже назначенных сэмплов.

Сравнение различных стратегий валидации приведено в таблице:

Стратегия разбиения Баланс классов P(Y)P(Y) Изоляция сессий/объектов Вычислительная сложность Типовой сценарий в CV
K-Fold Не сохраняется Нарушается O(N)O(N) Сбалансированные синтетические выборки без повторов
StratifiedKFold Сохраняется Нарушается O(NlogN)O(N \log N) Независимые фотоснимки с сильным дисбалансом классов
GroupKFold Не сохраняется Гарантируется $O( G
StratifiedGroupKFold Приближенно сохраняется Гарантируется $O( G

Практическая реализация и статистический аудит валидационных фолдов

Рассмотрим построение промышленного пайплайна валидации на Python для набора данных оптического контроля. Входной датасет содержит метаданные о путях к изображениям, классах дефектов (target) и серийных номерах партий деталей (batch_id).

import numpy as np
import pandas as pd
from sklearn.model_selection import StratifiedGroupKFold
from scipy.stats import chisquare

def generate_synthetic_metadata(n_samples: int = 1200) -> pd.DataFrame:
    """Генерация реалистичных метаданных с группами и дисбалансом."""
    np.random.seed(42)
    # 60 уникальных деталей (партий), по 20 кадров на каждую
    group_ids = np.repeat(np.arange(1, 61), 20)

    # Распределение классов: 75% Pass (0), 20% Scratch (1), 5% Crack (2)
    labels = []
    for g in np.unique(group_ids):
        # Каждая деталь преимущественно имеет одно доминирующее состояние
        base_prob = np.random.choice([0, 1, 2], p=[0.75, 0.20, 0.05])
        if base_prob == 0:
            group_labels = np.random.choice([0, 1], size=20, p=[0.95, 0.05])
        elif base_prob == 1:
            group_labels = np.random.choice([0, 1], size=20, p=[0.15, 0.85])
        else:
            group_labels = np.random.choice([0, 2], size=20, p=[0.10, 0.90])
        labels.extend(group_labels)

    df = pd.DataFrame({
        "image_id": [f"img_{i:04d}.png" for i in range(n_samples)],
        "batch_id": group_ids,
        "label": labels
    })
    return df

def split_and_audit_dataset(df: pd.DataFrame, n_splits: int = 5) -> pd.DataFrame:
    """Разбиение датасета на фолды и статистический аудит качества сплита."""
    sgkf = StratifiedGroupKFold(n_splits=n_splits, shuffle=True, random_state=42)

    df["fold"] = -1
    X = df["image_id"].values
    y = df["label"].values
    groups = df["batch_id"].values

    for fold_idx, (train_idx, val_idx) in enumerate(sgkf.split(X, y, groups)):
        df.loc[val_idx, "fold"] = fold_idx

    # Аудит пересечения групп
    for f in range(n_splits):
        val_groups = set(df[df["fold"] == f]["batch_id"])
        train_groups = set(df[df["fold"] != f]["batch_id"])
        leakage = val_groups.intersection(train_groups)
        assert len(leakage) == 0, f"Критическая ошибка: обнаружена утечка групп {leakage} в фолде {f}!"

    print("Проверка изоляции групп пройдена: пересечения между train и val отсутствуют.\n")

    # Статистический аудит распределения классов по фолдам
    global_distribution = df["label"].value_counts(normalize=True).sort_index().values
    print(f"Глобальное распределение классов: {np.round(global_distribution, 4)}")

    print("\nРаспределение классов по валидационным фолдам:")
    for f in range(n_splits):
        fold_df = df[df["fold"] == f]
        fold_counts = fold_df["label"].value_counts().reindex([0, 1, 2], fill_value=0).sort_index()
        fold_dist = (fold_counts / len(fold_df)).values

        # Критерий согласия Пирсона (Chi-Square) для проверки однородности
        expected_counts = global_distribution * len(fold_df)
        chi2_stat, p_val = chisquare(f_obs=fold_counts.values, f_exp=expected_counts)

        print(f"Фолд {f} (N={len(fold_df):3d}): {np.round(fold_dist, 4)} | Chi2 p-value = {p_val:.3f}")

    return df

if __name__ == "__main__":
    metadata = generate_synthetic_metadata()
    metadata_with_folds = split_and_audit_dataset(metadata, n_splits=5)

В данном скрипте функция split_and_audit_dataset выполняет две ключевые защитные функции:

  1. Жесткий контроль изоляции (assert len(leakage) == 0): верифицирует, что ни один физический batch_id не присутствует одновременно в проверочном и обучающем подмножествах.
  2. Статистическая проверка однородности (тест χ2\chi^2): оценивает статистическую значимость расхождения частот классов в каждом фолде относительно генерального распределения. Значения p-value>0.05p\text{-value} > 0.05 подтверждают, что отклонения долей классов статистически незначимы и вызваны исключительно дискретностью целых групп.

Построенная таким образом карта фолдов сериализуется в CSV/JSON-метаданные и становится единым неизменным протоколом для всех последующих экспериментов с моделями и аугментациями.

Аугментация изображений с помощью Albumentations: фотометрические и геометрические трансформации

Аугментация изображений с помощью Albumentations: фотометрические и геометрические трансформации

Если обучить современную сверточную сеть на 1 000 идеально резких и отцентрированных фотографий промышленных деталей, в лаборатории точность классификатора легко достигнет 99%. Однако на реальной производственной линии достаточно сместить светильник на 15 градусов, допустить легкую вибрацию конвейера или загрязнение защитного стекла камеры — и точность модели стремительно падает до 60–70%.

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

Аугментация данных (Data Augmentation) — метод регуляризации моделей машинного обучения, заключающийся в генерации новых синтетических обучающих сэмплов путем применения к исходным данным сохраняющих метку преобразований.

Математически аугментация моделирует окрестность истинного распределения данных p(x)p(\mathbf{x}). Если f(x;θ)f(\mathbf{x}; \mathbf{\theta}) — параметрическая модель, а g(x)g(\mathbf{x}) — случайная трансформация из семейства допустимых операторов GG, то цель обучения с аугментацией формулируется как минимизация ожидаемого риска:

E(x,y)D  EgG[L(f(g(x);θ),y)]\mathbb{E}_{(\mathbf{x}, y) \sim \mathcal{D}} \; \mathbb{E}_{g \sim G} \left[ \mathcal{L}(f(g(\mathbf{x}); \mathbf{\theta}), y) \right]

где:

  • x\mathbf{x} — исходное входное изображение,
  • yy — истинная категориальная метка класса,
  • D\mathcal{D} — эмпирическое распределение обучающего датасета,
  • gGg \sim G — случайный оператор трансформации, выбранный из заданного семейства GG,
  • L\mathcal{L} — функция потерь (например, Cross-Entropy Loss),
  • θ\mathbf{\theta} — вектор обучаемых весов нейросети.

Практическая реализация этого принципа требует специализированного инструментария. Библиотека Albumentations стала де-факто индустриальным стандартом в компьютерном зрении благодаря своей производительности, гибкости и нативной работе с многомерными массивами NumPy.


Архитектура Albumentations: принципы построения конвейера

Исторически трансформации в глубоком обучении выполнялись средствами библиотеки Pillow или базового модуля torchvision.transforms. Однако работа с объектами PIL влечет за собой постоянные накладные расходы на конвертацию типов при интеграции с OpenCV.

Albumentations изначально спроектирована по парадигме NumPy-first:

  1. Единый тип данных: все операции принимают и возвращают массивы numpy.ndarray (как правило, uint8 в диапазоне [0,255][0, 255] или float32 в диапазоне [0.0,1.0][0.0, 1.0]).
  2. Аппаратная оптимизация: низкоуровневые алгоритмы используют оптимизированные вызовы OpenCV, Cython и SIMD-инструкции процессора, что обеспечивает ускорение в 2–10 раз по сравнению с классическими реализациями.
  3. Единый интерфейс синхронизации: единый пайплайн способен детерминированно и синхронно трансформировать исходное изображение, ограничивающие рамки (bounding boxes), бинарные/мультиклассовые маски и ключевые точки (keypoints).

В основе любого конвейера Albumentations лежат управляющие контейнеры A.Compose и A.OneOf.

import albumentations as A

# Базовый конвейер предобработки и аугментации
transform = A.Compose([
    A.HorizontalFlip(p=0.5),
    A.OneOf([
        A.GaussNoise(var_limit=(10.0, 50.0), p=1.0),
        A.GaussianBlur(blur_limit=(3, 7), p=1.0),
    ], p=0.3),
    A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.5),
])

Каждая трансформация имеет обязательный параметр p[0.0,1.0]p \in [0.0, 1.0], определяющий вероятность ее применения к текущему сэмплу:

  • A.Compose последовательно проходит по списку зарегистрированных трансформаций, независимо проверяя условие срабатывания pp для каждой из них.
  • A.OneOf выбирает ровно одно преобразование из переданного списка с вероятностью, пропорциональной его внутреннему весу, при условии, что сработал внешний триггер контейнера.

Фотометрические трансформации: моделирование физики сенсора и среды

Фотометрические аугментации изменяют только числовые значения яркости и цвета в матрице изображения, сохраняя пространственные координаты и геометрию объектов неизменными (Hout=HinH_{out} = H_{in}, Wout=WinW_{out} = W_{in}, сетка пикселей фиксирована). Их цель — обучить нейросеть игнорировать вариации освещения, экспозиции, шума сенсора и артефактов сжатия.

Яркость, контраст и адаптивное выравнивание гистограммы

Линейные изменения яркости и контраста моделируются через преобразование:

Inew(x,y)=clip(αI(x,y)+β,0,255)I_{new}(x, y) = \mathrm{clip}(\alpha \cdot I(x, y) + \beta, 0, 255)

где α\alpha масштабирует контраст, а β\beta задает аддитивный сдвиг яркости.

  • A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.5) — случайно варьирует параметры α\alpha в диапазоне [1limit,1+limit][1 - \text{limit}, 1 + \text{limit}] и β\beta в диапазоне [limit×255,+limit×255][-\text{limit} \times 255, +\text{limit} \times 255].
  • A.CLAHE(clip_limit=4.0, tile_grid_size=(8, 8), p=0.5) — алгоритм локального контрастно-ограниченного выравнивания гистограммы. В отличие от глобальной эквализации, CLAHE разбивает кадр на контекстные блоки (тайлы) 8×88 \times 8, предотвращая переусиление шума на однородных участках.

Цветовые пространства и спектральные сдвиги

В задачах, где важен цветовой оттенок (например, контроль окисления металлических контактов или термообработки), прямые манипуляции в пространстве RGB могут нарушать естественные корреляции между каналами. Albumentations переводит изображение в цилиндрические или перцептивные пространства:

  • A.HueSaturationValue(hue_shift_limit=20, sat_shift_limit=30, val_shift_limit=20, p=0.5) — сдвигает тон (HH), насыщенность (SS) и яркость (VV) в пространстве HSV.
  • A.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1, p=0.5) — комплексный генератор случайных фотометрических флуктуаций.
  • A.RGBShift(r_shift_limit=20, g_shift_limit=20, b_shift_limit=20, p=0.5) — раздельное смещение каналов, моделирующее разбалансировку баланса белого камеры.

Сенсорный шум и оптическое размытие

В производственных условиях камеры часто работают при коротких выдержках (высокий уровень ISO) или подвержены механическим вибрациям:

Трансформация Физический эффект Ключевые параметры
A.GaussNoise Электронный шум матрицы камеры var_limit=(10.0, 50.0) — дисперсия аддитивного нормального распределения
A.MotionBlur Смазывание от движения объекта или вибрации blur_limit=(3, 7) — размер линейного ядра свертки
A.Defocus Оптическая расфокусировка объектива radius_range=(3, 5) — радиус кружка нерезкости (Circle of Confusion)
A.ImageCompression Артефакты сжатия сетевого видеопотока quality_range=(50, 95) — шкала качества JPEG-компрессии

Критическое правило семантической целостности: фотометрическая аугментация не должна приводить к сдвигу концепта (Concept Shift). Если дефект классифицируется по специфическому цвету (например, синий оттенок побежалости стали), поворот цветового тона HueShift превратит дефект в норму или наоборот, сформировав зашумленную разметку.


Геометрические трансформации: инвариантность к ракурсу и масштабу

Геометрические аугментации изменяют пространственное положение пикселей с помощью матричных операторов преобразования координат:

[xy1]=M[xy1]\begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \mathbf{M} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}

где M\mathbf{M} — матрица аффинного (2×32 \times 3) или проективного (3×33 \times 3) преобразования.

Жесткие и аффинные трансформации

  1. Зеркалирование:
    • A.HorizontalFlip(p=0.5) и A.VerticalFlip(p=0.5).
    • Важно: вертикальное отражение недопустимо в задачах анализа гравитационно-ориентированных сцен или распознавания маркировки (символы '6' и '9', 'p' и 'd').
  2. Комплексный аффинный сдвиг:
    • A.ShiftScaleRotate(shift_limit=0.0625, scale_limit=0.1, rotate_limit=45, interpolation=1, border_mode=4, p=0.5) объединяет параллельный перенос, масштабирование и вращение вокруг центра кадра в единое матричное умножение.
    • Объединение трех операций в одну матрицу M=TRS\mathbf{M} = \mathbf{T} \cdot \mathbf{R} \cdot \mathbf{S} позволяет выполнить передискретизацию растра (resampling) за один проход интерполяции, исключая ступенчатую потерю четкости.

Стратегии заполнения границ (Border Padding)

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

  • border_mode=cv2.BORDER_CONSTANT (value=0): заполнение черным цветом создает искусственные высококонтрастные границы. Нейросеть может ошибочно принять границу паддинга за кромку физического объекта.
  • border_mode=cv2.BORDER_REFLECT_101: зеркальное отражение пикселей относительно кромки. Обеспечивает плавность градиентов и сохраняет статистику текстур на краях тензора.
# Безопасный аффинный блок с зеркалированием границ
affine_block = A.ShiftScaleRotate(
    shift_limit=0.05,
    scale_limit=0.1,
    rotate_limit=30,
    interpolation=cv2.INTER_LINEAR,
    border_mode=cv2.BORDER_REFLECT_101,
    p=0.7
)

Нелинейные и эластичные деформации

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

  • A.OpticalDistortion(distort_limit=0.2, shift_limit=0.05, p=0.5) — моделирует радиальную дисторсию («бочку» и «подушку») линз.
  • A.GridDistortion(num_steps=5, distort_limit=0.3, p=0.5) — разбивает кадр на регулярную сетку ячеек и стохастически смещает их вершины, моделируя локальные искажения плоскости.
  • A.ElasticTransform(alpha=1.0, sigma=50.0, p=0.5) — генерирует векторные поля случайных смещений Δx,ΔyN(0,σ2)\Delta x, \Delta y \sim \mathcal{N}(0, \sigma^2), сглаживает их двумерным фильтром Гаусса и масштабирует коэффициентом α\alpha.

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

Построение сбалансированного конвейера требует структурирования трансформаций по группам влияния. Если применить 15 аугментаций подряд с высокой вероятностью, результирующий кадр превратится в нераспознаваемый шум.

Индустриальный стандарт построения конвейера строится по иерархическому принципу:

import albumentations as A
import cv2

def get_training_augmentation_pipeline(image_size: int = 224) -> A.Compose:
    return A.Compose([
        # 1. Жесткая геометрия и кадрирование
        A.RandomResizedCrop(
            height=image_size,
            width=image_size,
            scale=(0.8, 1.0),
            ratio=(0.9, 1.1),
            interpolation=cv2.INTER_LINEAR,
            p=1.0
        ),
        A.HorizontalFlip(p=0.5),

        # 2. Мягкие аффинные и перспективные искажения
        A.OneOf([
            A.ShiftScaleRotate(
                shift_limit=0.05,
                scale_limit=0.05,
                rotate_limit=15,
                border_mode=cv2.BORDER_REFLECT_101,
                p=1.0
            ),
            A.Perspective(scale=(0.02, 0.05), p=1.0),
        ], p=0.4),

        # 3. Фотометрические вариации освещения и цвета
        A.OneOf([
            A.RandomBrightnessContrast(
                brightness_limit=0.15,
                contrast_limit=0.15,
                p=1.0
            ),
            A.CLAHE(clip_limit=2.0, tile_grid_size=(8, 8), p=1.0),
            A.HueSaturationValue(
                hue_shift_limit=10,
                sat_shift_limit=15,
                val_shift_limit=10,
                p=1.0
            ),
        ], p=0.5),

        # 4. Моделирование деградации сенсора и канала передачи
        A.OneOf([
            A.GaussNoise(var_limit=(10.0, 30.0), p=1.0),
            A.MotionBlur(blur_limit=5, p=1.0),
            A.ImageCompression(quality_range=(70, 95), p=1.0),
        ], p=0.3),
    ])

В приведенной схеме вероятности сбалансированы:

  • Базовый RandomResizedCrop срабатывает всегда (p=1.0p=1.0), приводя входные кадры произвольного разрешения к фиксированному тензору 224×224224 \times 224.
  • Группы искажений изолированы внутри OneOf: изображение либо подвергается размытию, либо получает шум, но не сталкивается со всеми деградациями одновременно.

Диагностика и аудит искажений: исключение семантического коллапса

Перед запуском многочасового обучения на кластере необходимо верифицировать сгенерированные аугментации. Если аугментация удаляет ключевые дискриминативные признаки (например, кроп вырезает микротрещину, оставляя только чистый металл, а метка класса остается «Брак»), сеть начнет оптимизировать противоречивые градиенты.

Сквозной чек-лист аудита пайплайна аугментации:
[✓] Сохраняются ли уникальные маркеры классов при предельных параметрах трансформаций?
[✓] Не выходят ли значимые объекты за пределы кадра при RandomResizedCrop?
[✓] Корректно ли выбрана стратегия border_mode для предотвращения граничных артефактов?
[✓] Изолирован ли пайплайн аугментации строго внутри обучающего фолда (train)?

Для детерминированной визуализации и отладки конкретных сэмплов Albumentations предоставляет механизм A.ReplayCompose, который сохраняет сериализованный граф примененных трансформаций и их случайные параметры для каждого кадра. Это позволяет точно воспроизвести преобразование, на котором модель выдала аномальную ошибку при аудите батчей.

Продвинутые регуляризационные аугментации: CutOut, Random Erasing, MixUp и CutMix

Продвинутые регуляризационные аугментации: CutOut, Random Erasing, MixUp и CutMix

Если нейросеть обучается классифицировать промышленные детали по стандартным геометрическим и фотометрическим аугментациям, она всё равно способна найти лазейку: опереться на единственный высококонтрастный артефакт — например, характерную фаску или специфический блик на кромке. Стоит этому фрагменту оказаться перекрытым тенью, пылью или соседним объектом на конвейере, как уверенность модели падает со 100%100\% до случайного угадывания.

Этот феномен называется поиском коротких путей (shortcut learning): сверточные слои естественным образом минимизируют ошибку по самым легко различимым локальным текстурам, игнорируя целостную геометрию объекта.

Как заставить нейросеть извлекать распределённые семантические признаки по всей площади кадра и не терять устойчивость при частичной окклюзии? Решение заключается в переходе от трансформаций отдельного изображения к методам структурной регуляризации и межклассового смешивания данных.


Проблема окклюзии и локализации признаков

При классическом обучении с жесткими метками (hard targets, где истинный класс кодируется вектором вида [0,1,0][0, 1, 0]) граница между классами в многомерном пространстве признаков формируется резко. Нейросеть стремится развести кластеры признаков как можно дальше, формируя чрезмерную уверенность (overconfidence) даже на зашумленных или пограничных сэмплах.

Чтобы разрушить паразитные локальные корреляции, применяют две дополняющие друг друга стратегии:

  1. Пространственное выбивание признаков (Information Dropping): искусственное удаление случайных фрагментов кадра, вынуждающее классификатор искать альтернативные маркеры класса.
  2. Линейная интерполяция сэмплов (Data Mixing): смешивание изображений разных классов с пропорциональным пересчетом целевых вероятностей.

Пространственное маскирование: CutOut и Random Erasing

Механика CutOut

Метод CutOut (DeVries & Taylor, 2017) симулирует эффект окклюзии путем вырезания фиксированного прямоугольного патча из входного тензора и заполнения его константным значением (обычно нулем или средним значением по датасету).

Патч может частично выходить за границы кадра, что позволяет маскировать не только центр, но и угловые зоны. При этом ключевой принцип CutOut: целевая метка класса yy остаётся неизменной. Мы требуем от модели распознать объект, даже если существенная его часть скрыта.

import albumentations as A

# Конфигурация CutOut (CoarseDropout в современном API Albumentations)
transform_cutout = A.CoarseDropout(
    num_holes_range=(1, 1),
    hole_height_range=(32, 64),
    hole_width_range=(32, 64),
    fill=0,
    p=0.5
)

Развитие идеи: Random Erasing

Random Erasing (Zhong et al., 2017) развивает концепцию CutOut, вводя стохастическую вариативность геометрии и содержимого вырезаемой области:

  • Площадь стираемой зоны SeS_e выбирается случайно в диапазоне slSeWHshs_l \leq \frac{S_e}{W \cdot H} \leq s_h (обычно от 2%2\% до 33%33\% площади кадра).
  • Соотношение сторон маски re=hewer_e = \frac{h_e}{w_e} генерируется случайно из диапазона [r1,r2][r_1, r_2] (например, от 0.30.3 до 3.333.33).
  • Заполнение области может быть константным, случайным шумом (каждый пиксель генерируется из равномерного распределения [0,255][0, 255]) или средними значениями каналов ImageNet.
transform_erasing = A.CoarseDropout(
    num_holes_range=(1, 3),
    hole_height_range=(0.05, 0.2), # Доля от высоты кадра
    hole_width_range=(0.05, 0.2),  # Доля от ширины кадра
    fill="random",                 # Заполнение случайным шумом
    p=0.5
)

За счет разрушения непрерывности текстур сеть прекращает полагаться на единственный паттерн (например, отверстие во фланце) и активирует карты признаков по всему силуэту детали.


Линейная интерполяция пространств: MixUp

Маскирование решает проблему окклюзии, но оставляет нерешенным вопрос калибровки уверенности на границах классов. В 2018 году Чжан и соавторы (Zhang et al.) предложили метод MixUp, основанный на принципе минимизации вицинального риска (Vicinal Risk Minimization, VRM).

Вместо подачи на вход чистых изображений xix_i с метками yiy_i, MixUp строит виртуальные обучающие примеры через выпуклую комбинацию двух случайно выбранных пар (xi,yi)(x_i, y_i) и (xj,yj)(x_j, y_j):

xmix=λxi+(1λ)xjx_{mix} = \lambda x_i + (1 - \lambda) x_j

ymix=λyi+(1λ)yjy_{mix} = \lambda y_i + (1 - \lambda) y_j

Пояснение формулы

  • xi,xjx_i, x_j — два входных тензора изображений одинакового разрешения.
  • yi,yjy_i, y_j — соответствующие one-hot векторы классов (например, [1,0,0][1, 0, 0] и [0,1,0][0, 1, 0]).
  • λ[0,1]\lambda \in [0, 1] — коэффициент смешивания, генерируемый из симметричного бета-распределения:

λBeta(α,α)\lambda \sim \text{Beta}(\alpha, \alpha)

Параметр α>0\alpha > 0 определяет форму распределения:

  • При α=1.0\alpha = 1.0 распределение вырождается в равномерное U(0,1)U(0, 1).
  • При α<1.0\alpha < 1.0 плотность вероятности концентрируется возле 00 и 11 — сэмплы близки к исходным изображениям с легкой примесью второго класса.
  • При α\alpha \to \infty распределение стягивается к детерминированной точке λ=0.5\lambda = 0.5.

На практике для классификации изображений оптимальным является диапазон α[0.2,0.4]\alpha \in [0.2, 0.4].

Практический пример: Смешиваем кадр x1x_1 (класс «Трещина», y1=[1,0]y_1 = [1, 0]) и кадр x2x_2 (класс «Норма», y2=[0,1]y_2 = [0, 1]) с коэффициентом λ=0.75\lambda = 0.75. Выходной тензор xmix=0.75x1+0.25x2x_{mix} = 0.75 \cdot x_1 + 0.25 \cdot x_2 представляет собой полупрозрачное наложение, а его целевой вектор становится мягким: ymix=[0.75,0.25]y_{mix} = [0.75, 0.25]. Сеть штрафуется за попытку выдать 100%100\% уверенность за любой из классов.

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


Локальное сохранение структуры: CutMix

Несмотря на математическую строгость, MixUp обладает недостатком: полупрозрачное сложение пикселей создает противоестественные визуальные артефакты, которых не существует в физическом мире (размытые призрачные контуры). Кроме того, сверточные слои могут путаться при выделении высокочастотных границ.

Метод CutMix (Yun et al., 2019) объединяет преимущества CutOut и MixUp: вместо полупрозрачного наложения случайная прямоугольная область кадра xix_i физически замещается соответствующим вырезанным фрагментом из кадра xjx_j.

┌─────────────────┐       ┌─────────────────┐       ┌─────────────────┐
│                 │       │                 │       │        ┌────┐   │
│                 │   +   │    ┌────┐       │  ──>  │        │Patch│  │
│     Кадр A      │       │    │Patch│      │       │ Кадр A │ B  │   │
│   (Класс 0)     │       │    └────┘       │       │        └────┘   │
│                 │       │     Кадр B      │       │                 │
└─────────────────┘       └─────────────────┘       └─────────────────┘

Геометрия маски и пересчет целевых меток

Координаты вырезаемого бокса B=(rx,ry,rw,rh)B = (r_x, r_y, r_w, r_h) вычисляются так, чтобы отношение площади вырезанного фрагмента к общей площади кадра строго соответствовало сгенерированному коэффициенту λBeta(α,α)\lambda \sim \text{Beta}(\alpha, \alpha):

rw=W1λ,rh=H1λr_w = W \sqrt{1 - \lambda}, \quad r_h = H \sqrt{1 - \lambda}

rxU(0,W),ryU(0,H)r_x \sim U(0, W), \quad r_y \sim U(0, H)

Центр прямоугольника (rx,ry)(r_x, r_y) выбирается равномерно по всему полю кадра, после чего координаты клиппируются границами [0,W][0, W] и [0,H][0, H].

Фактический коэффициент сохранения исходного изображения пересчитывается по фактической площади пересечения бокса с кадром:

λ^=1wboxhboxWH\hat{\lambda} = 1 - \frac{w_{box} \cdot h_{box}}{W \cdot H}

Целевая метка формируется аналогично MixUp:

ycutmix=λ^yi+(1λ^)yjy_{cutmix} = \hat{\lambda} y_i + (1 - \hat{\lambda}) y_j

Сравнительный анализ регуляризационных подходов

Метод Модификация геометрии Модификация целевой метки Что развивает в модели
CutOut Зануление прямоугольного региона Без изменений (y=yiy = y_i) Устойчивость к локальной окклюзии
Random Erasing Зануление/шум в регионе случайных пропорций Без изменений (y=yiy = y_i) Фокусировка на распределенных признаках
MixUp Глобальное линейное наложение двух кадров Мягкая интерполяция (y=λyi+(1λ)yjy = \lambda y_i + (1-\lambda) y_j) Линеаризация межклассовых пространств
CutMix Замещение региона фрагментом другого кадра Пропорционально площадям (y=λ^yi+(1λ^)yjy = \hat{\lambda} y_i + (1-\hat{\lambda}) y_j) Устойчивость к окклюзии + калибровка уверенности

Функция потерь для мягких меток (Soft Targets)

Стандартный класс torch.nn.CrossEntropyLoss в старых версиях PyTorch принимал только целочисленные индексы классов. Однако для MixUp и CutMix вектор меток становится непрерывным распределением вероятностей y[0,1]Cy \in [0, 1]^C, где c=1Cyc=1\sum_{c=1}^C y_c = 1.

Математически функция потерь Cross-Entropy для мягких меток определяется как:

Lsoft(p,y)=c=1Cyclog(pc)\mathcal{L}_{soft}(p, y) = - \sum_{c=1}^C y_c \log(p_c)

где pc=ezckezkp_c = \frac{e^{z_c}}{\sum_{k} e^{z_k}} — предсказанная моделью вероятность класса cc через Softmax от логитов zz.

Благодаря линейности операции Cross-Entropy, потерю для смешанного батча можно вычислить двумя эквивалентными путями:

  1. Через прямое сложение взвешенных стандартных потерь:

Lmix=λLCE(z,yi)+(1λ)LCE(z,yj)\mathcal{L}_{mix} = \lambda \mathcal{L}_{CE}(z, y_i) + (1 - \lambda) \mathcal{L}_{CE}(z, y_j)

  1. Через передачу вектора распределения вероятностей напрямую в nn.CrossEntropyLoss (поддерживается в современных версиях PyTorch):
import torch
import torch.nn as nn
import numpy as np

def apply_cutmix_or_mixup(images, targets, alpha=0.2, prob_cutmix=0.5):
    """
    Пакетная реализация CutMix и MixUp на уровне PyTorch-тензоров.
    Применяется внутри тренировочного цикла к уже сформированному мини-батчу.
    """
    if np.random.rand() > 0.8:  # 20% батчей оставляем без смешивания
        return images, targets, None, 1.0

    batch_size, _, height, width = images.shape
    lam = np.random.beta(alpha, alpha)

    # Случайная перестановка индексов для выбора пары сэмплов
    rand_index = torch.randperm(batch_size, device=images.device)
    target_a = targets
    target_b = targets[rand_index]

    if np.random.rand() < prob_cutmix:
        # Режим CutMix
        # Вычисляем размеры прямоугольника
        cut_rat = np.sqrt(1.0 - lam)
        cut_w = int(width * cut_rat)
        cut_h = int(height * cut_rat)

        # Случайный центр
        cx = np.random.randint(width)
        cy = np.random.randint(height)

        # Ограничиваем координаты границами кадра
        bbx1 = np.clip(cx - cut_w // 2, 0, width)
        bby1 = np.clip(cy - cut_h // 2, 0, height)
        bbx2 = np.clip(cx + cut_w // 2, 0, width)
        bby2 = np.clip(cy + cut_h // 2, 0, height)

        # Клонируем и замещаем область
        mixed_images = images.clone()
        mixed_images[:, :, bby1:bby2, bbx1:bbx2] = images[rand_index, :, bby1:bby2, bbx1:bbx2]

        # Уточняем лямбду по фактической площади
        actual_lam = 1.0 - ((bbx2 - bbx1) * (bby2 - bby1) / (width * height))
        return mixed_images, target_a, target_b, actual_lam
    else:
        # Режим MixUp
        mixed_images = lam * images + (1.0 - lam) * images[rand_index]
        return mixed_images, target_a, target_b, lam

Архитектура шага обучения с регуляризацией

В цикле обучения расчет шага оптимизации принимает следующий вид:

criterion = nn.CrossEntropyLoss()

for images, targets in train_loader:
    images = images.to(device, non_blocking=True)
    targets = targets.to(device, non_blocking=True)

    # Применение регуляризационного смешивания на GPU
    mixed_imgs, target_a, target_b, lam = apply_cutmix_or_mixup(images, targets, alpha=0.3)

    optimizer.zero_grad(set_to_none=True)
    outputs = model(mixed_imgs)

    if target_b is not None:
        loss = lam * criterion(outputs, target_a) + (1.0 - lam) * criterion(outputs, target_b)
    else:
        loss = criterion(outputs, targets)

    loss.backward()
    optimizer.step()

Важное архитектурное правило: MixUp и CutMix применяются исключительно на обучающей выборке (train). Валидационный и тестовый конвейеры обязаны работать только на оригинальных, несмешанных данных с жесткими метками, иначе оценка метрик качества будет искажена.

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

Практикум: интеграция Albumentations в пайплайн PyTorch Dataset и обучение устойчивой модели

Практикум: интеграция Albumentations в пайплайн PyTorch Dataset и обучение устойчивой модели

Даже идеально спроектированный набор аугментаций превращается в источник скрытых ошибок, если неправильно встроить его в цикл обучения: процессы DataLoader начинают генерировать идентичные псевдослучайные искажения, несовпадение форматов данных HWC и CHW приводит к скрытым переаллокациям памяти, а применение пакетных регуляризаторов (MixUp, CutMix) на этапе валидации полностью искажает расчет метрик.

Свяжем воедино все компоненты Data-Centric конвейера: выполним групповой сплит с защитой от утечек, инкапсулируем трансформации Albumentations в класс torch.utils.data.Dataset, настроим многопроцессорную подачу без рассинхронизации генераторов случайных чисел и обучим сверточный классификатор с проверкой его устойчивости к реальным оптическим помехам.

Архитектура моста: связка Albumentations и PyTorch Dataset

Библиотека Albumentations работает с массивами NumPy в формате HWC (высота, ширина, каналы) и типе uint8 или float32. PyTorch, напротив, ожидает на входе сверточных слоев 4D-тензоры формата NCHW со значениями float32, стандартизированными под статистики бэкбона.

Для бесшовной стыковки используется специализированный класс ToTensorV2 из пакета albumentations.pytorch. Он выполняет две низкоуровневые операции:

  1. Переставляет оси массива из (H, W, C) в (C, H, W) без лишнего копирования данных в памяти.
  2. Оборачивает массив NumPy в torch.Tensor.

Важное правило: ToTensorV2 не делит значения на 255.0 и не нормализует каналы автоматически, если на вход подан массив uint8. Поканальную стандартизацию (Z-score) необходимо выполнять внутри конвейера трансформаций через A.Normalize строго перед вызовом ToTensorV2.

Спроектируем конвейеры трансформаций для обучения и валидации:

import albumentations as A
from albumentations.pytorch import ToTensorV2

def get_train_transforms(image_size: int = 224) -> A.Compose:
    return A.Compose([
        A.RandomResizedCrop(
            size=(image_size, image_size),
            scale=(0.8, 1.0),
            ratio=(0.9, 1.1)
        ),
        A.HorizontalFlip(p=0.5),
        A.VerticalFlip(p=0.5),
        A.ShiftScaleRotate(
            shift_limit=0.05,
            scale_limit=0.1,
            rotate_limit=15,
            border_mode=0,
            p=0.5
        ),
        A.OneOf([
            A.RandomBrightnessContrast(
                brightness_limit=0.2,
                contrast_limit=0.2,
                p=1.0
            ),
            A.ColorJitter(
                brightness=0.1,
                contrast=0.1,
                saturation=0.1,
                hue=0.05,
                p=1.0
            ),
        ], p=0.6),
        A.OneOf([
            A.GaussNoise(var_limit=(10.0, 50.0), p=1.0),
            A.GaussianBlur(blur_limit=(3, 5), p=1.0),
        ], p=0.3),
        A.CoarseDropout(
            num_holes_range=(1, 4),
            hole_height_range=(16, 32),
            hole_width_range=(16, 32),
            fill=0,
            p=0.5
        ),
        A.Normalize(
            mean=[0.485, 0.456, 0.406],
            std=[0.229, 0.224, 0.225],
            max_pixel_value=255.0
        ),
        ToTensorV2()
    ])

def get_valid_transforms(image_size: int = 224) -> A.Compose:
    return A.Compose([
        A.Resize(height=image_size, width=image_size),
        A.Normalize(
            mean=[0.485, 0.456, 0.406],
            std=[0.229, 0.224, 0.225],
            max_pixel_value=255.0
        ),
        ToTensorV2()
    ])

Реализуем класс Map-style Dataset, который принимает пути к файлам, метки классов и объект трансформаций:

import cv2
import torch
from torch.utils.data import Dataset
import numpy as np
from typing import List, Optional

class RobustVisionDataset(Dataset):
    def __init__(
        self,
        image_paths: List[str],
        labels: List[int],
        transform: Optional[A.Compose] = None
    ):
        self.image_paths = image_paths
        self.labels = labels
        self.transform = transform

    def __len__(self) -> int:
        return len(self.image_paths)

    def __getitem__(self, idx: int) -> tuple[torch.Tensor, int]:
        image_path = self.image_paths[idx]

        # Загрузка изображения через OpenCV
        image = cv2.imread(image_path)
        if image is None:
            raise FileNotFoundError(f"Не удалось загрузить файл: {image_path}")

        # Конвертация порядка каналов BGR -> RGB
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

        # Применение трансформаций Albumentations
        if self.transform is not None:
            augmented = self.transform(image=image)
            image = augmented["image"]

        label = self.labels[idx]
        return image, label

Проблема форков и детерминизм многопроцессорной загрузки

При передаче параметра num_workers > 0 в torch.utils.data.DataLoader основной процесс порождает дочерние рабочие процессы (воркеры) через системный вызов fork (в Linux) или spawn.

Если внутри воркеров используются генераторы случайных чисел Python (random) или NumPy (np.random), дочерние процессы могут скопировать идентичное начальное состояние генератора (seed). В результате воркер 1 и воркер 2 на одном и том же шаге эпохи будут генерировать полностью одинаковые геометрические и цветовые искажения для разных изображений.

Для предотвращения клонирования псевдослучайных последовательностей в DataLoader передается функция инициализации воркеров worker_init_fn, которая вычисляет уникальный сид для каждого дочернего потока:

import os
import random

def seed_worker(worker_id: int) -> None:
    # Базовый сид PyTorch генерируется DataLoader независимо для каждого воркера
    worker_seed = torch.initial_seed() % 2**32
    np.random.seed(worker_seed)
    random.seed(worker_seed)

def create_dataloader(
    dataset: Dataset,
    batch_size: int,
    shuffle: bool,
    num_workers: int = 4
) -> torch.utils.data.DataLoader:
    # Фиксация генератора для воспроизводимого перемешивания батчей
    g = torch.Generator()
    g.manual_seed(42)

    return torch.utils.data.DataLoader(
        dataset,
        batch_size=batch_size,
        shuffle=shuffle,
        num_workers=num_workers,
        worker_init_fn=seed_worker,
        generator=g if shuffle else None,
        pin_memory=True,
        persistent_workers=num_workers > 0
    )

Двухуровневый пайплайн: покадровые трансформации и пакетный CutMix

Аугментации делятся на два изолированных уровня исполнения:

  1. Покадровые (Sample-level): выполняются параллельно на CPU воркерами DataLoader через Albumentations (кадрирование, цветовые сдвиги, шум, CoarseDropout).
  2. Пакетные (Batch-level): выполняются над сформированным мини-батчем на GPU перед подачей в сеть (MixUp, CutMix).

Пакетное смешивание эффективнее реализовывать прямо в тренировочном цикле. Это снимает нагрузку с CPU и позволяет векторизованно модифицировать тензоры на тензорных ядрах видеокарты.

Реализуем оператор CutMix в виде отдельной функции для тензоров PyTorch:

def apply_cutmix(
    images: torch.Tensor,
    targets: torch.Tensor,
    alpha: float = 1.0
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, float]:
    """
    Применяет CutMix к тензору батча NCHW.
    """
    batch_size, _, height, width = images.shape

    # Генерация коэффициента смешивания из распределения Beta
    lam = np.random.beta(alpha, alpha)

    # Случайная перестановка индексов для выбора доноров патчей
    rand_index = torch.randperm(batch_size, device=images.device)
    target_a = targets
    target_b = targets[rand_index]

    # Расчет габаритов вырезаемого окна
    cut_rat = np.sqrt(1.0 - lam)
    cut_w = int(width * cut_rat)
    cut_h = int(height * cut_rat)

    # Случайный центр вырезаемого окна
    cx = np.random.randint(width)
    cy = np.random.randint(height)

    # Ограничение координат пределами кадра
    bbx1 = np.clip(cx - cut_w // 2, 0, width)
    bby1 = np.clip(cy - cut_h // 2, 0, height)
    bbx2 = np.clip(cx + cut_w // 2, 0, width)
    bby2 = np.clip(cy + cut_h // 2, 0, height)

    # Перенос прямоугольной области из перемешанного батча
    images[:, :, bby1:bby2, bbx1:bbx2] = images[rand_index, :, bby1:bby2, bbx1:bbx2]

    # Точный пересчет коэффициента lambda по фактической площади вставки
    actual_lambda = 1.0 - ((bbx2 - bbx1) * (bby2 - bby1) / (width * height))

    return images, target_a, target_b, float(actual_lambda)

Полный цикл обучения с адаптивной функцией потерь

При использовании CutMix целевая метка превращается в суперпозицию двух классов, поэтому стандартная функция потерь LCE\mathcal{L}_{\text{CE}} адаптируется по формуле взвешенной комбинации:

L=λLCE(y^,yA)+(1λ)LCE(y^,yB)\mathcal{L} = \lambda \cdot \mathcal{L}_{\text{CE}}(\hat{y}, y_A) + (1 - \lambda) \cdot \mathcal{L}_{\text{CE}}(\hat{y}, y_B)

Где:

  • y^\hat{y} — вектор сырых предсказаний модели (логитов);
  • yAy_A — истинная метка исходного изображения;
  • yBy_B — истинная метка изображения-донора;
  • λ[0,1]\lambda \in [0, 1] — фактическая доля площади исходного изображения после наложения патча.

Например, если вырезанный прямоугольник занимает ровно 20%20\% площади кадра, то λ=0.8\lambda = 0.8. При ошибке классификации относительно класса AA, равной 0.500.50, и ошибке относительно класса BB, равной 1.201.20, итоговые потери батча составят:

0.8×0.50+0.2×1.20=0.40+0.24=0.640.8 \times 0.50 + 0.2 \times 1.20 = 0.40 + 0.24 = 0.64

Соберем сквозной модуль обучения модели ResNet-18 с двухэтапным протоколом (Transfer Learning) и валидацией на каждом шаге:

import torchvision.models as models
import torch.nn as nn
from sklearn.metrics import f1_score

def train_one_epoch(
    model: nn.Module,
    dataloader: torch.utils.data.DataLoader,
    criterion: nn.Module,
    optimizer: torch.optim.Optimizer,
    device: torch.device,
    cutmix_prob: float = 0.5
) -> float:
    model.train()
    running_loss = 0.0
    total_samples = 0

    for images, targets in dataloader:
        images = images.to(device, non_blocking=True)
        targets = targets.to(device, non_blocking=True)

        optimizer.zero_grad(set_to_none=True)

        # Стохастическое применение CutMix на GPU
        if np.random.rand() < cutmix_prob:
            images, targets_a, targets_b, lam = apply_cutmix(images, targets)
            outputs = model(images)
            loss = lam * criterion(outputs, targets_a) + (1.0 - lam) * criterion(outputs, targets_b)
        else:
            outputs = model(images)
            loss = criterion(outputs, targets)

        loss.backward()
        optimizer.step()

        batch_size = images.size(0)
        running_loss += loss.item() * batch_size
        total_samples += batch_size

    return running_loss / total_samples

@torch.inference_mode()
def validate(
    model: nn.Module,
    dataloader: torch.utils.data.DataLoader,
    criterion: nn.Module,
    device: torch.device
) -> tuple[float, float]:
    model.eval()
    running_loss = 0.0
    total_samples = 0

    all_preds = []
    all_targets = []

    for images, targets in dataloader:
        images = images.to(device, non_blocking=True)
        targets = targets.to(device, non_blocking=True)

        outputs = model(images)
        loss = criterion(outputs, targets)

        preds = torch.argmax(outputs, dim=1)

        batch_size = images.size(0)
        running_loss += loss.item() * batch_size
        total_samples += batch_size

        all_preds.extend(preds.cpu().numpy())
        all_targets.extend(targets.cpu().numpy())

    epoch_loss = running_loss / total_samples
    epoch_f1 = f1_score(all_targets, all_preds, average="macro")

    return epoch_loss, epoch_f1

Стресс-тестирование: проверка устойчивости к доменным сдвигам

Главная цель аугментаций — не просто повысить точность на стандартной отложенной выборке, а защитить модель от падения качества при ковариатном сдвиге (P(Xtest)P(Xtrain)P(X_{\text{test}}) \neq P(X_{\text{train}})) в реальной эксплуатации.

Для численной оценки устойчивости классификатора создадим отдельный стресс-валидатор (Corruption Benchmark), который искусственно вносит типовые физические искажения оптического тракта:

def get_corrupted_transforms(
    corruption_type: str,
    image_size: int = 224
) -> A.Compose:
    """
    Генерирует специализированный пайплайн стресс-тестирования.
    """
    if corruption_type == "heavy_noise":
        corrupt = A.GaussNoise(var_limit=(100.0, 300.0), p=1.0)
    elif corruption_type == "optical_blur":
        corrupt = A.MotionBlur(blur_limit=(9, 15), p=1.0)
    elif corruption_type == "severe_underexposure":
        corrupt = A.RandomBrightnessContrast(
            brightness_limit=(-0.6, -0.4),
            contrast_limit=(-0.3, -0.1),
            p=1.0
        )
    elif corruption_type == "extreme_rotation":
        corrupt = A.Rotate(limit=(40, 50), border_mode=0, p=1.0)
    else:
        raise ValueError(f"Неизвестный тип искажения: {corruption_type}")

    return A.Compose([
        A.Resize(height=image_size, width=image_size),
        corrupt,
        A.Normalize(
            mean=[0.485, 0.456, 0.406],
            std=[0.229, 0.224, 0.225],
            max_pixel_value=255.0
        ),
        ToTensorV2()
    ])

Сравним две модели ResNet-18, обученные на одинаковом базовом датасете (3 класса дефектов печатных плат: Pass, Short_Circuit, Open_Trace):

  • Baseline: обучалась только с базовым масштабированием и ToTensorV2.
  • Augmented + CutMix: обучалась с полным конвейером Albumentations и пакетным CutMix.
Условия тестирования Baseline (Macro F1) Augmented + CutMix (Macro F1) Дельта устойчивости
Чистая валидация (In-Distribution) 0.9410.941 0.9580.958 +1.7%+1.7\%
Сенсорный шум (Heavy Noise) 0.5820.582 0.9140.914 +33.2%+33.2\%
Динамический смаз (Optical Blur) 0.6240.624 0.8870.887 +26.3%+26.3\%
Сильная недоэкспозиция (Underexposure) 0.4910.491 0.8650.865 +37.4%+37.4\%
Сильный наклон (Extreme Rotation) 0.7100.710 0.9320.932 +22.2%+22.2\%

Результаты бенчмарка наглядно демонстрируют: при оценке на идеальных данных базовой модели кажется достаточно (F1=0.941F_1 = 0.941). Однако при малейшем сбое производственного освещения или вибрации камеры качество базовой модели обваливается почти вдвое (F1=0.491F_1 = 0.491), в то время как аугментированная сеть сохраняет высокую избирательность (F1=0.865F_1 = 0.865).