Главной проблемой при обучении нейросетей остаётся нехватка качественной информации. Всем моделям глубокого обучения может потребоваться большой объём данных для достижения удовлетворительных результатов. Для успешного обучения модели данные должны быть разнообразными и соответствовать поставленной задаче. В противном случае пользы от такой сети будет мало. Хорошо известно, что нехватка данных легко приводит к переобучению.
Но вот беда, трудно предусмотреть и собрать данные, которые покрывали бы все ситуации. Допустим, вы хотите научить систему находить на фото конкретную кошку. Вам потребуются снимки этого животного в самых разных позах — будь то сидя, стоя или обдирающей диван.
А если требуется распознавать кошек в принципе, то вариантов становится в разы больше. Видов кошек в природе тысячи, они все разных цветов и размеров. Почему это важно? Представьте, что наш набор данных может содержать изображения кошек и собак. Кошки в наборе смотрят исключительно влево с точки зрения наблюдателя. Неудивительно, что обученная модель может неправильно классифицировать кошек, смотрящих вправо.
Поэтому всегда нужно проверять свою выборку на разнообразие. Если данные не подходят под реальные условия, то и задачу решить не получится.
Первое, что приходит на ум — попытаться увеличить разнообразие выборки. Самый очевидный путь — просто собрать больше данных. На практике это долго, дорого, а иногда брать их просто негде.
Тогда остаётся второй путь. Мы можем создать новые примеры искусственно. Этот метод называется аугментацией. Если у вас есть алгоритм генерации новых образцов, их можно смело использовать для обучения.
Возвращаясь к нашим кошками. Представьте, что ваша сеть видела животных только в нормальном сидячем положении. Можно программно изменить перспективу картинки и перевернуть её, как будто кошка запечатлена вверх ногами. Пусть физически это отличается от реально перевёрнутого котика, но для обучения такой вариант все равно намного полезнее, чем обычное фото.
В процессе аугментации важно, чтобы придуманные вами изменения действительно напоминали то, что может встретиться в реальности. Нет смысла натягивать текстуру кота на сложную геометрическую фигуру б̶у̶б̶л̶и̶к̶а̶ тора, такое вряд ли вам встретится, но есть смысл повернуть изображение на некоторый угол, так как фотограф мог просто неровно держать камеру.
Создание данных, неотличимых от настоящих, требует немалых усилий. Однако существует набор «стандартных» аугментаций, которые применяются повсеместно. Для них в современных фреймворках глубокого обучения уже реализованы готовые высокоуровневые функции. Разумеется, возможность писать собственные функции преобразования также поддерживается.
В 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 ООО «МТ ФИНАНС»
Источник


