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

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

А если требуется распознавать кошек в принципе, то вариантов становится в разы больше. Видов кошек в природе тысячи, они все разных цветов и размеров. Почему это важно? Представьте, что наш набор данных может содержать изображения кошек и собак. Кошки в наборе смотрят исключительно влево с точки зрения наблюдателя. Неудивительно, что обученная модель может неправильно классифицировать кошек, смотрящих вправо.

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

Что делать, если у нас дефицит данных

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

Тогда остаётся второй путь. Мы можем создать новые примеры искусственно. Этот метод называется аугментацией. Если у вас есть алгоритм генерации новых образцов, их можно смело использовать для обучения.

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

Принцип разумности

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

Реализация

Создание данных, неотличимых от настоящих, требует немалых усилий. Однако существует набор «стандартных» аугментаций, которые применяются повсеместно. Для них в современных фреймворках глубокого обучения уже реализованы готовые высокоуровневые функции. Разумеется, возможность писать собственные функции преобразования также поддерживается.

В Keras (TensorFlow) аугментации изображений происходят через класс ImageDataGenerator в модуле tensorflow.keras.preprocessing.image.

Специальный объект-генератор, который берёт исходные картинки и выдаёт их изменённые версии.

Возьмём изображение кота из интернета:

import random
import requests
import numpy as np
import cv2
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow.keras.preprocessing.image import load_img, img_to_array, ImageDataGenerator
from tensorflow.keras.applications.resnet50 import preprocess_input

# Ссылка на картинку
target_link = 'https://i.pinimg.com/1200x/b3/bd/2a/b3bd2a055c99e034b131f3545163892b.jpg'
response = requests.get(target_link, allow_redirects=True)

filename = 'local_sample.jpg'
with open(filename, 'wb') as file:
    file.write(response.content)

raw_img = load_img(filename)
pixel_array = img_to_array(raw_img).astype('uint8')
img_tensor = np.expand_dims(pixel_array, 0)  # batch из одного элемента

plt.axis('off')
plt.imshow(img_tensor[0])
plt.show()
Наш подопытный
Наш подопытный

Вот с этим изображением мы будем проводить аугментации.

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

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

Скрытый текст
def create_base_gen():
    """Инициализация базового генератора Keras."""
    gen = ImageDataGenerator(fill_mode='constant', dtype='uint8')
    gen.fit(img_tensor)
    return gen


def display_batch(generator, input_data, rows=1, cols=5, fix_resnet_colors=False):
    """Вывод сетки аугментированных изображений."""
    total_imgs = rows * cols
    # Создаем поток
    iterator = generator.flow(input_data, batch_size=1)

    plt.figure(figsize=(cols * 4, rows * 3))

    for i in range(total_imgs):
        batch_item = next(iterator)
        single_img = batch_item[0]

        # Корректировка цветовой схемы для ResNet
        if fix_resnet_colors:
            single_img = single_img.copy()
            
            mean_values = [103.939, 116.779, 123.68]
            for c in range(3):
                single_img[..., c] += mean_values[c]
            # Разворот BGR в RGB
            single_img = single_img[..., ::-1]

        
        display_img = np.clip(single_img, 0, 255).astype('uint8')

        plt.subplot(rows, cols, i + 1)
        plt.axis('off')
        plt.imshow(display_img)
    plt.show()

Аугментации

Берём наш начальный генератор, добавляем ему поля для нужных аугментаций.

Сдвиг

gen_obj = create_base_gen()
gen_obj.width_shift_range = 0.2
gen_obj.height_shift_range = 0.2
display_batch(gen_obj, img_tensor)

Отражения

datagen = default_datagen()
datagen.horizontal_flip = True # добавляем отражения по горизонтали
datagen.vertical_flip = True # добавляем отражения по вертикали
plot_augmentation(datagen, data) 

Вращение

datagen = default_datagen() 
datagen.rotation_range = 25 # добавляем повороты (в градусах)
plot_augmentation(datagen, data) 

Масштабирование

gen_obj = create_base_gen()
gen_obj.zoom_range = [0.2, 1.8]
display_batch(gen_obj, img_tensor)

Наклоны

gen_obj = create_base_gen()
gen_obj.shear_range = 30 #
display_batch(gen_obj, img_tensor)

Яркость

gen_obj = create_base_gen()
gen_obj.brightness_range = [0.5, 2.0]
display_batch(gen_obj, img_tensor)

Сдвиг цветовых каналов

gen_obj = create_base_gen()
gen_obj.channel_shift_range = 70.0
display_batch(gen_obj, img_tensor)

Комбинация всех вариантов

Аугментации можно и нужно применять одновременно. Протестируем комбинации:

mixed_gen = create_base_gen()
mixed_gen.fill_mode = 'nearest'
mixed_gen.horizontal_flip = True
mixed_gen.vertical_flip = True
mixed_gen.width_shift_range = 0.2
mixed_gen.height_shift_range = 0.2
mixed_gen.zoom_range = [0.8, 1.2]
mixed_gen.rotation_range = 25
mixed_gen.shear_range = 30
mixed_gen.brightness_range = [0.75, 1.5]
mixed_gen.channel_shift_range = 70.0
display_batch(mixed_gen, img_tensor, rows=3, cols=5)

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

Все эти методы аугментации пытаются компенсировать «бедность» маленького да��асета. Проблема с кошками, которые смотрят только влево, решается аугментацией отражения. Отражение — одна из самых интуитивно понятных стратегий для увеличения размера или разнообразия данных. Однако это может быть неуместно, когда данные имеют уникальные свойства. Например, асимметричные или чувствительные к направлению данные, такие как буквы или цифры, не могут использовать стратегию отражения, поскольку это приводит к неточным меткам у модели или даже к противоположным меткам.

Ещё один подводный камень существует у аугментации вращения. Изображения поворачиваются на заданный угол, и вновь созданные изображения используются вместе с оригиналами в качестве обучающих образцов. Недостатком вращения является то, что оно может привести к потере информации на границах изображения (потому что поворот прямоугольной картинки по траектории круга приводит к появлению пробелов). Существует несколько возможных решений, например, вращение со случайным заполнением ближайшим соседом (RNR), вращение со случайным отражением (RRR) и вращение со случайным циклическим переносом (RWR) для исправления проблемы границ повернутых изображений. В частности, метод RNR повторяет значения ближайших пикселей для заполнения чёрных областей, метод RRR использует подход на основе зеркального отображения, а метод RWR использует стратегию периодических границ для заполнения пробелов

Создание своего генератора данных

Но базовые аугментации не предел! Библиотека позволяет реализовывать собственные генераторы данных, если стандартных инструментов недо��таточно. Для внедрения уникальных методов аугментации необходимо создать новый класс и унаследовать его от базового ImageDataGenerator. Внутри этого класса описываются требуемые параметры и логика обработки.

Скрытый текст
import requests
import numpy as np
target_link = 'https://i.pinimg.com/736x/63/5b/89/635b891ba4049eed0914f5a036bd6ce5.jpg'
response = requests.get(target_link, allow_redirects=True)

filename = 'local_sample.jpg'
with open(filename, 'wb') as file:
    file.write(response.content)

# Подготовка тензора
raw_img = load_img(filename)
pixel_array = img_to_array(raw_img).astype('uint8')
img_tensor = np.expand_dims(pixel_array, 0)
Второй подопытный
Второй подопытный

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

Скрытый текст
class ComplexAugmentor(ImageDataGenerator):
    def __init__(self,
                 r_factor=None,      # Диапазон красного
                 g_factor=None,      # Диапазон зеленого
                 b_factor=None,      # Диапазон синего
                 noise_lvl=None,     # Уровень шума
                 mask_dim=None,      # Размер вырезаемого сектора
                 p_cutout=0.0,       # Вероятность вырезания сектора
                 p_mix=0.0,          # Вероятность наложения 
                 p_warp=0.0,         # Вероятность искажения перспективы
                 p_blur=0.0,         # Вероятность размытия
                 **kwargs):

        
        self._external_preprocessor = kwargs.pop('preprocessing_function', None)

        
        super().__init__(
            preprocessing_function=self._pipeline_handler,
            **kwargs
        )

        
        self.rgb_factors = (r_factor, g_factor, b_factor)
        self.noise_lvl = noise_lvl
        self.mask_dim = mask_dim
        self.probs = {
            'cutout': p_cutout,
            'mix': p_mix,
            'warp': p_warp,
            'blur': p_blur
        }

    def _pipeline_handler(self, input_img):
        """Пайплан обработки."""
        
        proc_img = input_img.copy().astype(np.float32)

        # 1. Перспектива
        if self.probs['warp'] > 0 and random.random() < self.probs['warp']:
            proc_img = self._transform_perspective(proc_img)

        # 2. Наложение фрагментов
        if self.probs['mix'] > 0 and random.random() < self.probs['mix']:
            proc_img = self._patch_overlay(proc_img)

        # 3. Цветокоррекция каналов
        proc_img = self._adjust_channels(proc_img)

        # 4. Эффекты камеры (размытие и шум)
        if self.probs['blur'] > 0 and random.random() < self.probs['blur']:
            proc_img = self._add_motion_blur(proc_img)

        if self.noise_lvl:
            proc_img = self._add_gaussian_noise(proc_img)

        # 5. Удаление секторов
        if self.mask_dim and self.probs['cutout'] > 0:
            if random.random() < self.probs['cutout']:
                proc_img = self._apply_cutout_mask(proc_img)

        
        proc_img = np.clip(proc_img, 0, 255)

        
        if self._external_preprocessor:
            proc_img = self._external_preprocessor(proc_img)

        return proc_img


    def _adjust_channels(self, img):
        for idx, bounds in enumerate(self.rgb_factors):
            if bounds:
                coeff = random.uniform(bounds[0], bounds[1])
                img[:, :, idx] *= coeff
        return img

    def _add_gaussian_noise(self, img):
        noise_map = np.random.normal(0, self.noise_lvl, img.shape)
        return img + noise_map

    def _apply_cutout_mask(self, img):
        h_img, w_img, _ = img.shape
        sz = self.mask_dim

        
        c_y = np.random.randint(0, h_img)
        c_x = np.random.randint(0, w_img)

        
        y_min = max(0, c_y - sz // 2)
        y_max = min(h_img, c_y + sz // 2)
        x_min = max(0, c_x - sz // 2)
        x_max = min(w_img, c_x + sz // 2)

        
        img[y_min:y_max, x_min:x_max, :] = 127.0
        return img

    def _patch_overlay(self, img):
        """Берет часть изображения и вставляет в другое место."""
        h, w, _ = img.shape
        
        p_h, p_w = h // 3, w // 3

        start_y = np.random.randint(0, h - p_h)
        start_x = np.random.randint(0, w - p_w)

        crop = img[start_y:start_y + p_h, start_x:start_x + p_w].copy()

        
        if random.random() > 0.5:
            crop = np.flip(crop, axis=1)
        else:
            crop *= random.uniform(0.8, 1.2)

        dest_y = np.random.randint(0, h - p_h)
        dest_x = np.random.randint(0, w - p_w)

        img[dest_y:dest_y + p_h, dest_x:dest_x + p_w] = crop
        return img

    def _transform_perspective(self, img):
        """Имитация изменения угла обзора."""
        rows, cols = img.shape[:2]
        src_points = np.float32([[0, 0], [cols, 0], [0, rows], [cols, rows]])

        
        shift_x = cols * 0.2
        shift_y = rows * 0.2

        
        dst_points = np.float32([
            [random.uniform(0, shift_x), random.uniform(0, shift_y)],
            [cols - random.uniform(0, shift_x), random.uniform(0, shift_y)],
            [random.uniform(0, shift_x), rows - random.uniform(0, shift_y)],
            [cols - random.uniform(0, shift_x), rows - random.uniform(0, shift_y)]
        ])

        matrix = cv2.getPerspectiveTransform(src_points, dst_points)
        return cv2.warpPerspective(img, matrix, (cols, rows), borderMode=cv2.BORDER_REFLECT)

    def _add_motion_blur(self, img):
        """Фильтр размытия в движении."""
        k_size = random.randint(5, 15)
        kernel = np.zeros((k_size, k_size))

        
        mid = int((k_size - 1) / 2)
        kernel[mid, :] = np.ones(k_size)
        kernel /= k_size  

        return cv2.filter2D(img, -1, kernel)

Теперь наша аугментация будет куда разнообразнее.

custom_aug = ComplexAugmentor(
    # Стандартные параметры
    rotation_range=30,
    width_shift_range=0.2,
    horizontal_flip=True,
    vertical_flip=True,

    # Цветовые параметры
    r_factor=(0.5, 1.2),
    b_factor=(0.7, 1.1),

    # Шум и дефекты
    noise_lvl=15.0,
    mask_dim=130,
    p_cutout=0.8,

    # Наложени, Искажения и блюр
    p_mix=0.5,
    p_warp=0.7,
    p_blur=0.3,

  
    preprocessing_function=preprocess_input
)

custom_aug.fit(img_tensor)
display_batch(custom_aug, img_tensor, rows=4, cols=5, fix_resnet_colors=True)

К базовым аугментациям мы добавили несколько новых. Например случайное вырезание (cutout). Это метод аугментации данных, который, как правило, не пытается изменить значения отдельных пикселей изображения. Вместо этого он заменяет значения пикселей внутри прямоугольника произвольного размера на изображении случайным значением. Можно рассматривать случайное вырезание как своего рода шумовую технику, фокусирующуюся на локальных областях, а не на отдельных пикселях. Она предназначена для того, чтобы сделать модель устойчивой к перекрытию объектов на изображениях и, таким образом, снизить вероятность переобучения. Cutout повышает разнообразие данных без увеличения их размера, потому что нам не надо сохранять изображения с вырезанием: оно применяется по время обучения.

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

Интересный факт! Помните такую штуку, когда на изображение добавляли определенную «маску шума», и модель начинала галлюцинировать и путать панду с гиббоном.

Вот этот пример
Вот этот пример

Этот эффект назвали adversarial attacks, также известныё как машинная иллюзия. Adversarial attacks также можно рассматривать как часть семейства а��гментации данных путем внедрения шума. При внедрении систематического шума в данное изображение свёрточная нейронная сеть выдаёт совершенно другой прогноз, даже если человеческий глаз не может обнаружить разницу.

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


Эксперимент, очевидно... удачный.

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

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

© 2026 ООО «МТ ФИНАНС»