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

iShrimp
20.01.2026 13:04defpatchoverlay(self, img):"""Берет часть изображения и вставляет в другое место."""IMHO, это плохой пример аугментации - таких искажений на фотках, как во втором ряду №4 и №5, в реальности не бывает, и нет смысла мучить нейросеть это распознавать.
Что бывает чаще всего - пересвет/недосвет, тени/блики, разнообразные шумы.
Добавляя светлые/тёмные пятна и шумы (различной амплитуды и частоты, в т.ч. пространственно неоднородные), можно сделать нейросеть менее чувствительной к уже упомянутым adversarial attacks.

rRenegat Автор
20.01.2026 13:04Смысл есть и он много раз проверен в исследованиях на тему аугментации. Он называется и реализуется по разному: Copyout, Copy-Paste, CutMix
Эти техники случайным образом удаляют визуальные признаки на обучающих изображениях и тем самым заставляют нейросеть опираться на более широкий набор различных признаков для распознавания изображений. Это помогает моделям лучше обобщать на ранее невиденных данных. Также это помогает справляться с изображениями, где присутсвует перекрытие. По сути этой аугментацией мы его имитируемЭто похоже на cutout, но с одним существенным различием.
Cutout имитирует перекрытие с помощью простого серого квадрата, который можно считать бесполезными данными. А методы замещения части изображения же заполняет эту область дополнительной визуальной информацией. Получается мы не только удаляем случайные визуальные признаки, но и добавляем новые случайные признаки. Эти случайные признаки лучше соответствуют естественному эффекту перекрытия в реальности.

Filipp42
20.01.2026 13:04Есть ещё такой интересный эффект как гроконие (Grokking). Это когда модель обучается на маленьком датасете, полностью его выучивает, но тестовая точность остаётся очень низкой. Но потом, если оставить модель обучаться очень и очень долго, то качество тестовой выборки подскакивает почти до ста процентов. Модель обобщает после долгого запоминания.

ingeniare
20.01.2026 13:04Да, слышал я историю как множили хаски и добились очень высокой точности распознования. Была очень высокой, пока не попалась фотография хаски летом. ИИ натренировалась не на собаки, а на белом зимнем фоне за собакой.

SerRozan
20.01.2026 13:04хорошая практика - искать баланс между полезными трансформациями и здравым смыслом в данных))

axel_pervoliajnen
20.01.2026 13:04Ссорри не удержался. Здравый смысл в ДАННЫХ.? Гыгы. Тут был пост про сайнтетик биг дата аналитика. Именно так. Куча терминов дао мау непонятные термины. А вот тут он сказал Аномальные выбросы. И куча маодао как отфильровать
arteys
Если у нас нет реальных данных, давайте их придумаем!
Вот так все в Ад и катится
rRenegat Автор
Аугментация реально помогает. Она увеличивает разнообразие данных и при этом не требует сбора дополнительных данных.
Вы как себе представляете работу модели, которая обучалась на "прилизанных" и идеальных данных без капли шума? Хорошо ли такие модели справляются с тем же распознаванием?
alesh1n
А у вас нет случайно примера с цифрами насколько аугментация может улучшить распознавание? Просто интересно
rPman
Аугментация данных может привести к переобученности нейронной сети на основе нереальных признаков. Я думаю, если речь идет о распозновании образов, самыми полезными можно назвать повороты, частичное перекрытие (только чем и как перекрывать тоже думать нужно), размытия и смазы (различные дефекты видеокамер при движениях и расфокусировках), и по возможности, игры с цветом (например замена цвета у одежды, но нужно очень старательно подходить к алгоритму) и освещением, к примеру внедрение искусственных теней (их проще добавлять, особенно если сцена простая, ну сейчас есть алгоритмы построения чуть ли не 3d сцены, а там и свет и тени и что угодно можно поменять).
Я считаю, что вместо того, что бы корежить обучающую (обычно на старте проекта она жалкая, потому что вручную размечать неохота/дорого) выборку, лучше реализовать полуавтоматический сбор настоящих данных (разметку) на основе первоначального слабого алгоритма, т.е. собираем маленькую выборку, пилим что получится, на основе результата размечаем бОльший объем данных, снова обучаем,.. (self-training/pseudo‑labeling). Проверки и контроль можно делать другими, более простыми методами, например детектить движение объекта простыми алгоритмами, когда нам нужно квадратик рисовать вокруг объекта, соседние кадры его рисуют а этот почему то пропускают, считаем простым детектом движение его новый квадрат и если он не вышел за пороговые значения, добавляем в следующую обучающую выборку.
Само собой, если делать глупо, можно сильно умножить ложные срабатывания путем их добавления в выборку с неверными разметками.
axel_pervoliajnen
Есть данные. Реальные. Хоть с коллайдера. Да хоть с камеры. Что? ЛЛМ? Не знаю.