Игра в имитацию: используем Python для генерации синтетических данных для ML и не только. data science.. data science. machine learning.. data science. machine learning. python.. data science. machine learning. python. генерация данных.. data science. machine learning. python. генерация данных. данные.. data science. machine learning. python. генерация данных. данные. Машинное обучение.. data science. machine learning. python. генерация данных. данные. Машинное обучение. наука о данных.. data science. machine learning. python. генерация данных. данные. Машинное обучение. наука о данных. нейронные сети.. data science. machine learning. python. генерация данных. данные. Машинное обучение. наука о данных. нейронные сети. Программирование.. data science. machine learning. python. генерация данных. данные. Машинное обучение. наука о данных. нейронные сети. Программирование. синтетические данные.
Игра в имитацию: используем Python для генерации синтетических данных для ML и не только - 1

Введение

Ручной сбор данных — это всегда боль. Он съедает время, деньги и нервы, особенно в таких областях, как медицина или финансы, где затраты могут быть космическими, а юридические барьеры — непреодолимыми. По прогнозу Gartner, к 2030 году синтетические данные полностью затмят реальные данные в моделях ИИ. Почему? Потому что это работает.

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

Преимущества синтетических данных👍🏻

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

  • Моделирование редких событий и edge-cases. Редкие события, такие как финансовые мошенничества или редкие заболевания, по определению трудно зафиксировать в достаточном количестве для обучения моделей. Обучение на таких несбалансированных данных чревато созданием модели, которая будет склонна предсказывать только доминирующий класс, игнорируя редкие, но важные случаи. Синтетические данные позволяют искусственно сбалансировать классы, создавая дополнительные примеры для миноритарных классов.

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

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

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

И область их применения выходят далеко за рамки задача data science. В разработке ПО они позволяют проводить тщательное тестирование без раскрытия конфиденциальной информации. В кибербезопасности команды могут моделировать шаблоны атак, не подвергая риску реальные системы. Для стартапов в отраслях с ограниченным доступом к данным синтетика может сократить цикл разработки с месяцев до недель, не заставляя ждать, пока соберётся достаточная выборка из реального мира.

Недостатки синтетических данных👎🏻

Но, конечно, не всё так гладко, как может показаться. У синтетики есть свои подводные камни, которые важно учитывать:

  • Риск нереалистичности. Основным недостатком синтетических данных является риск того, что они не будут в полной мере отражать сложность реального мира. Даже самые продвинутые алгоритмы генерации не могут учесть все нюансы и взаимосвязи, присутствующие в реальных данных. Это может привести к созданию моделей, которые хорошо работают на синтетических данных, но дают плохие результаты при применении к реальным ситуациям. Здесь в полной мере работает принцип “мусор на входе – мусор на выходе“: ошибки в алгоритмах генерации неизбежно аукнутся проблемами с качеством обученных моделей.

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

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

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

Но если подойти к делу с умом, синтетические данные — это мощный инструмент. Далее мы разберем, как создавать такие данные с помощью Python: от базовых библиотек вроде Faker до продвинутых инструментов вроде GAN. Посмотрим примеры кода, обсудим плюсы и минусы каждого инструмента и подходы к оценке результатов генерации.

Инструменты Python для генерации синтетических данных

Итак, мы разобрались, зачем нужны синтетические данные и в чем их сила. Но как же, собственно, создавать эту “синтетику” на практике? К счастью, Python предлагает целый арсенал инструментов для решения этой задачи. Начнем с самых простых и постепенно перейдем к более продвинутым техникам.

Первым на очереди у нас – Faker.

Faker: универсальный генератор данных для разработки и тестирования

Игра в имитацию: используем Python для генерации синтетических данных для ML и не только - 2

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

Как работает?

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

Важно: Faker не гарантирует уникальность или статистическую корректность данных. Его цель — правдоподобие, а не точность.

Установить проще простого через pip: pip install faker.

Давайте рассмотрим, как Faker помогает решать задачи, с конкретными примерами кода и пояснениями.

Пример 1: Тестирование функционала интернет-магазина

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

import pandas as pd
from faker import Faker
import json

fake = Faker('ru_RU')

def generate_user_profile():
    """Генерируем реалистичный профиль пользователя для интернет-магазина"""
    profile = {
        "name": fake.name(),
        "email": fake.email(),
        "phone": fake.phone_number(),
        "address": fake.address(),
        "payment_method": fake.credit_card_provider(),
        "order_history": [
            {"order_id": fake.uuid4(), "date": fake.date_between(start_date='-1y').strftime('%Y-%m-%d'), "total": fake.random_int(min=1000, max=10000)}
            for _ in range(fake.random_int(min=0, max=10))]  # История заказов (случайное количество)
    }
    return profile

# Генерируем 100 профилей пользователей
user_profiles = [generate_user_profile() for _ in range(100)]

# Преобразуем данные в DataFrame
df = pd.json_normalize(user_profiles, sep='_')

# Выводим таблицу
display(df)

# Сохраняем в JSON
with open('user_profiles.json', 'w', encoding='utf-8') as f:
    json.dump(user_profiles, f, indent=4, ensure_ascii=False)

Этот код генерирует профили пользователей с именами, адресами, email, телефонами, даже историей заказов и способами оплаты. Эти данные можно использовать для:

  • Нагрузочного тестирования в качестве имитация действий пользователей на сайте.

  • Создания правдоподобного демо-стенда для презентации заказчику.

  • Тестирования UI/UX: проверка, как интерфейс магазина выглядит с данными пользователей.

Пример генерации:

Игра в имитацию: используем Python для генерации синтетических данных для ML и не только - 3

Пример 2: Мокирование API для фронтенд-разработки

Представим ситуацию, когда Backend API нашего сервиса ещё не готов, а интерфейс нужно разрабатывать уже сейчас. Faker позволяет создать “заглушку” API с реалистичными данными:

from faker import Faker
from flask import Flask, jsonify

fake = Faker('ru_RU')
app = Flask(__name__)

@app.route('/api/products')
def get_products():
    """Эндпоинт API, возвращающий список фейковых продуктов"""
    products = [
        {
            "product_id": fake.uuid4(),
            "name": fake.word().capitalize() + " " + fake.word().capitalize(), #  Название продукта
            "description": fake.sentence(),
            "price": fake.random_int(min=100, max=5000),
            "image_url": fake.image_url() #  URL фейкового изображения
        } for _ in range(10) #  Генерируем 10 продуктов
    ]
    return jsonify(products)

if __name__ == '__main__':
    app.run(debug=True)

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

Пример 3: Анонимизация данных для Dev/Test среды

Представим, что у нас есть боевая база данных с персональными данными пользователей, которую нужно скопировать для разработки и тестирования. Но переносить реальные персональные данные в Dev/Test – это опасно. Faker поможет анонимизировать данные, заменяя чувствительную информацию на правдоподобную, но фейковую:

from faker import Faker
import sqlite3

fake = Faker('ru_RU')

def anonymize_database(db_name="users.db"):
    """Анонимизируем базу данных SQLite, заменяя персональные данные на фейковые"""
    conn = sqlite3.connect(db_name)
    cursor = conn.cursor()

    #  Предположим, у нас есть таблица 'users' с полями 'name', 'email', 'phone', 'address'
    cursor.execute("SELECT id, name, email, phone, address FROM users")
    users = cursor.fetchall()

    for user in users:
        user_id, _, _, _, _ = user #  Извлекаем ID пользователя
        new_name = fake.name()
        new_email = fake.email()
        new_phone = fake.phone_number()
        new_address = fake.address()

        cursor.execute("""
            UPDATE users
            SET name = ?, email = ?, phone = ?, address = ?
            WHERE id = ?
        """, (new_name, new_email, new_phone, new_address, user_id)) #  Обновляем данные

    conn.commit()
    conn.close()
    print(f"База данных '{db_name}' анонимизирована.")

anonymize_database()

Этот скрипт подключается к базе данных, заменяет реальные имена, email, телефоны и адреса, сохраняя при этом структуру и связи данных. Теперь Dev/Test среда наполнена правдоподобными, но безопасными данными.

Пример 4: QA в банке нужно проверить валидацию полей в форме перевода

Нужно проверить 500 комбинаций нестандартных вводов:

  • Города с дефисами (Ростов-на-Дону)

  • Двойные фамилии (Салтыков-Щедрин)

  • Адреса с корпусами/строениями.

Генерируем такие кейсы:

# Генератор  данных для тестирования  
fake = Faker('ru_RU')

def generate_edge_case():  
    templates = [  
        lambda: f"{fake.city()} {fake.street_name()} д. {random.randint(1, 200)} корпус {random.choice(['А', 'Б', 'В'])}",  
        lambda: f"{fake.last_name()}-{fake.last_name()}",  
        lambda: f"{fake.city()}-{fake.city()}"  
    ]  
    return random.choice(templates)()  

generate_edge_case()
# Примеры выводов:  
# "Санкт-Петербург Невский пр. д. 15 корпус Б"  
# "Иванов-Сидоров"  
# "Москва-Тверь"  

И тестируем при помощи нужных правил:

import pytest  

@pytest.mark.parametrize('input_text', [generate_edge_case() for _ in range(500)])  
def test_address_validation(input_text):  
    assert validate_address(input_text) is True  

Пример 5 (плохое использование): тестирования медклассификатора

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

Можно сгенерировать возраст людей равномерным распределением:

# Плохо:
age = fake.random_int(min=18, max=90)

# Чуть лучше: бимодальное распределение (молодые и пожилые)
age = fake.random.choice([fake.random_int(18-35), fake.random_int(65-90)])

Или сгенерировать рост и вес людей независимо друг от друга:

# Плохо:
{'height': 180, 'weight': 50}  # Нереалистично для человека ростом 180 см

# Получше:
bmi = fake.random.uniform(18.5, 30)
weight = int(height/100 * height/100 * bmi)

Или нагенерить диагнозы независимо друг от друга, хотя они могут быть связаны:

# Синтетика без учёта коморбидности:
diagnoses = ['J45', 'E11']  # Астма + диабет 2 типа — редкое сочетание

# Как лучше:
base_diagnosis = fake.random.choice(['J45', 'I10', 'E66'])
secondary = complications_map[base_diagnosis]  # Связь диагнозов через имющийся справочник

Далее мы, конечно, посмотрим инструменты, которые лучшим образом помогут нам в таких ситуациях.

Преимущества и недостатки Faker

➕:

  • Простота и скорость работы. Как мы видим из примеров, Faker действительно легко использовать и быстро интегрировать в разные процессы. Не нужно тратить время на сложную настройку.

  • Большой выбор типов данных (имена, адреса, даты и т.д.). Faker охватывает широкий спектр данных, достаточный для многих типовых задач тестирования, мокирования и анонимизации. Не нужно “изобретать велосипед” для генерации базовых типов данных.

  • Локализация позволяет генерировать данные специфичные для определенной страны.

  • Хорошо сочетается с другими библиотеками. Например с pandas для дальнейшей обработки и анализа сгенерированных данных и с SQLAlchemy для прямой загрузки данных в базы данных. Примеры как раз приведены выше.

➖:

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

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

  • Не подходит для генерации временных рядов и связанных данных. Faker генерирует данные изолированно, не учитывая временную динамику или взаимосвязи между разными объектами. Для генерации временных рядов, графовых данных или связанных табличных данных нужны специализированные подходы.

Когда стоит использовать?

Faker очень хорош, когда нужно быстро, просто и без лишних хлопот получить большое количество правдоподобных данных для решения широкого круга типовых IT-задач:

  • Тестирование UI/UX, нагрузочное тестирование, автоматизированное тестирование.

  • Мокирование API и сервисов для фронтенд-разработки и интеграции.

  • Анонимизация данных для Dev/Test сред.

  • Создание демо-стендов, прототипов, макетов.

  • Генерация примеров данных для документации, туториалов, обучающих материалов.

  • На ранних стадий разработки, где отсутствие реальных данных не должно мешать тестированию функционала.

Но если нам нужно больше контроля или сложные структуры, то нужно рассматривать другие инструменты. Далее ознакомимся с инструментарием Scikit-learn для генерации синтетики “на минималках” для задач машинного обучения.

Scikit-learn: генерация синтетических данных для машинного обучения

Игра в имитацию: используем Python для генерации синтетических данных для ML и не только - 4

Scikit-learn – это, в первую очередь, библиотека для машинного обучения, но в ней есть и полезные функции для генерации синтетических данных. Это не специализированная библиотека для синтетики (как тот же Faker), но если нам нужно быстро сгенерировать синтетический датасет для классификации, регрессии или кластеризации, да ещё и с полным контролем над его свойствами, Scikit-learn нам поможет.

Как работает?

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

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

  • make_regression. Генерирует данные для задач регрессии. Здесь тоже полный контроль над зависимостями между признаками и целевой переменной, плюс возможность добавить случайный шум.

  • make_blobs. Генерирует наборы для задач кластеризации в виде “пятен” (blobs), которые визуально хорошо разделяются и удобны для тестирования алгоритмов и визуализации результатов.

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

  • Наборы данных из sklearn.datasets (например, load_iris, load_diabetes): Хотя это и реальные наборы данных, их можно использовать как шаблоны или основу для создания синтетических вариаций. Например, можно изменить распределения признаков, добавить шум, перемешать данные, чтобы получить новые, контролируемые наборы данных.

Чтобы рассмотреть примеры далее, давайте установим библиотеку, если у вас её ещё нет: pip install scikit-learn.

Готово, можно приступать к экспериментам 👇🏻

Пример 1: Сравнение алгоритмов классификации на синтетических данных

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

from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
import pandas as pd

# Функция для генерации данных и тестирования моделей
def benchmark_classifiers(n_informative):
    # Генерируем датасет: 1000 объектов, 20 признаков, из них n_informative — полезные
    X, y = make_classification(n_samples=1000, n_features=20, n_informative=n_informative, n_classes=2, random_state=42)
    X_train, X_test, y_train, y_test = train_test_split(X, y,random_state=42)

    # Словарь с моделями
    models = {
        "Logistic Regression": LogisticRegression(max_iter=1000),
        "K-Nearest Neighbors": KNeighborsClassifier(),
        "Random Forest": RandomForestClassifier()
    }

    # Проверяем каждую модель
    results = {}
    for name, model in models.items():
        model.fit(X_train, y_train)
        y_pred = model.predict(X_test)
        accuracy = accuracy_score(y_test, y_pred)
        results[name] = accuracy

    return results

# Создаем общий DataFrame для всех результатов
all_results = pd.DataFrame()

# Проверяем на разной сложности данных
for n_informative in [5, 10, 15]:
    results = benchmark_classifiers(n_informative)
    results_df = pd.DataFrame(list(results.items()), columns=['Model', 'Accuracy'])
    results_df['Informative Features'] = n_informative
    all_results = pd.concat([all_results, results_df])

# Сортируем общий DataFrame по убыванию точности
all_results = all_results.sort_values(by=['Informative Features', 'Accuracy'], ascending=[True, False])

print("nОбщая таблица результатов:")
display(all_results)
Игра в имитацию: используем Python для генерации синтетических данных для ML и не только - 5

Что тут происходит?

  1. Мы генерируем датасет с 1000 объектов и 20 признаками, из которых только часть (n_informative) влияет на целевые классы.

  2. Делим данные на тренировочную и тестовую выборки.

  3. Обучаем три модели и сравниваем их точность.

При малом количестве информативных признаков (например, 5) случайный лес в общем случае может демонстрировать преимущество за счёт свой устойчивости к шуму и эффективного отбора признаков. А при 15 признаках разница может сократиться — данные становятся проще для всех моделей.

Пример 2: Кластеризация клиентских сегментов

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

import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
from sklearn.cluster import KMeans

# Генерируем данные: 300 покупателей, 2 признака (сумма покупок и частота заказов), 3 сегмента
X, y = make_blobs(n_samples=300, centers=3, random_state=42)

# Применяем K-means
kmeans = KMeans(n_clusters=3)
kmeans.fit(X)

# Визуализируем
plt.scatter(X[:, 0], X[:, 1], c=kmeans.labels_, cmap='viridis')
plt.scatter(kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1], s=200, c='red', marker='X')
plt.title("Сегментация клиентов с K-means")
plt.xlabel("Сумма покупок")
plt.ylabel("Частота заказов")
plt.show()
Игра в имитацию: используем Python для генерации синтетических данных для ML и не только - 6

Этот код создает три кластера покупателей и показывает, как K-means их разделяет. Красные крестики — центры кластеров. Такой пример идеально подойдет для статьи или презентации, чтобы объяснить, как алгоритм группирует данные.

Пример 3: Оценка влияния шума на качество регрессионных моделей

А что, если мы хотим проверить, как линейная регрессия справляется с зашумлёнными данными? Scikit-learn позволяет легко настроить уровень шума и измерить ошибку.

from sklearn.datasets import make_regression
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
import matplotlib.pyplot as plt

# Функция для генерации и тестирования
def test_regression(noise):
    X, y = make_regression(n_samples=100, n_features=1, noise=noise, random_state=42)
    model = LinearRegression()
    model.fit(X, y)
    y_pred = model.predict(X)
    mse = mean_squared_error(y, y_pred)
    return X, y, y_pred, mse

# Создаем фигуру и подграфики
fig, axes = plt.subplots(nrows=3, ncols=1, figsize=(8, 12))

# Тестируем с разным шумом
noise_levels = [10, 50, 100]
for i, noise in enumerate(noise_levels):
    X, y, y_pred, mse = test_regression(noise)

    # Визуализация
    axes[i].scatter(X, y, color='blue', label='Данные')
    axes[i].plot(X, y_pred, color='red', label='Регрессия')
    axes[i].set_title(f"Регрессия с шумом {noise}")
    axes[i].legend()
    axes[i].text(0.95, 0.05, f"MSE: {mse:.2f}", transform=axes[i].transAxes, fontsize=12,
                 verticalalignment='bottom', horizontalalignment='right', bbox=dict(boxstyle='round,pad=0.3', facecolor='wheat', alpha=0.5))

plt.tight_layout()
plt.show()
Игра в имитацию: используем Python для генерации синтетических данных для ML и не только - 7

С ростом шума (от 10 до 100) точки всё сильнее разлетаются вокруг красной линии регрессии, а ошибка (MSE) увеличивается. Так мы можем оценить оценить робастность (устойчивость) какой-либо модели к зашумленным данным.

Как сделать синтетику реалистичнее?

Синтетические данные из Scikit-learn по умолчанию слишком “идеальны”. Но с небольшими хитростями можно добавить им реализма. Например:

  • Добавляем ассимметрии:

from scipy.stats import skewnorm
X = skewnorm.rvs(5, size=1000)  # Правосторонняя асимметрия
  • Подмешиваем пропуски:

import numpy as np
X[np.random.choice(X.size, int(X.size * 0.1))] = np.nan  # добавляем 10% пропусков
  • Кастомизируем выбросы:

outliers = np.random.pareto(2, 50) # добавляем выбросы при помощи Парето-распределения
y[-50:] += outliers * 100
  • Варьируем важность признаков через параметры n_informative и n_redundant.

Преимущества и недостатки Scikit-learn

➕:

  • Простота и легкость использования. Функции Scikit-learn для генерации данных очень просты в использовании и интуитивно понятны. Не нужно глубоко погружаться в детали алгоритмов генерации.

  • Широкая распространённость. Scikit-learn – одна из самых популярных библиотек для машинного обучения в Python. Большинство специалистов, работающих с данными, уже знакомы с ней, что упрощает использование её функций для генерации синтетики.

  • Ориентированность на задачи ML. Синтетические данные, сгенерированные Scikit-learn, очень хорошо подходят для тестирования, бенчмаркинга и демонстрации работы ML-алгоритмов.

  • Хорошо сочетается с другими библиотеками. Сгенерированные данные легко преобразовать в pandas.DataFrame для анализа или отправить в matplotlib для визуализации.

➖:

  • Ограниченный набор типов данных. Scikit-learn в основном ориентирован на генерацию табличных данных и не подходит для генерации более сложных типов, таких как временные ряды, графы, тексты или изображения.

  • Ограниченный контроль над статистическими свойствами. Scikit-learn хоть и предоставляет контроль над статистическими свойствами генерируемых данных, но всё же он менее детальный, чем в некоторых других библиотеках, таких как SDV.

  • Не предназначен для генерации “реалистичных” данных. Scikit-learn создает скорее упрощенные, “игрушечные” наборы данных, чем по-настоящему реалистичные синтетические данные, которые бы имитировали сложность и нюансы реального мира. Для задач, требующих высокой реалистичности Scikit-learn может оказаться недостаточно мощным и гибким.

Когда стоит использовать?

Scikit-learn – отличный выбор, когда нужно быстро и просто сгенерировать базовые синтетические наборы данных для подобных задач:

  • Бенчмаркинг и сравнение ML-алгоритмов.

  • Тестирование и отладка ML-моделей на ранних этапах разработки.

  • Визуализация работы ML-алгоритмов и образовательные цели.

  • Быстрое прототипирование идей и эксперименты.

Мы рассмотрели Scikit-learn и Faker – “базовый уровень” генерации синтетических данных. Но что, если задача – создавать не просто наборы, а сложные структуры, учитывающие взаимосвязи между признаками, временные зависимости или целые базы данных?

Synthetic Data Vault: генерация сложных синтетических данных

Игра в имитацию: используем Python для генерации синтетических данных для ML и не только - 8

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

Как работает?

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

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

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

  • CTGAN (Conditional Tabular GAN). Модель, основанная на генеративно-состязательных сетях (GANs), специально разработана для генерации табличных данных с учетом категориальных и непрерывных признаков.

  • TVAE (Tabular Variational Autoencoder). Еще одна модель на основе глубокого обучения, использующая вариационные автоэнкодеры для генерации табличных данных. TVAE хорошо справляется с моделированием сложных нелинейных зависимостей.

  • Модели временных рядов. SDV предлагает набор моделей, которые “клонируют” временные ряды, воспроизводя их временную динамику и статистические свойства.

  • И многие другие модели для разных типов данных и задач.

SDV также позволяет сохранять обученные модели в специальном “хранилище” (Vault). Это позволяет их переиспользовать без необходимости повторного обучения, что сэкономит ресурсы.

Устанавливаем, как обычно, через pip: pip install sdv. И приступаем к примерам 👇🏻

Пример 1: Генерация синтетических данных для анализа оттока клиентов банка

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

import pandas as pd
from sdv.metadata import SingleTableMetadata
from sdv.single_table import GaussianCopulaSynthesizer

# Грузим данные с GitHub
url = 'https://raw.githubusercontent.com/obulygin/content/refs/heads/main/bank_churn/Churn.csv'
data = pd.read_csv(url).drop(columns=['RowNumber'])

# Задаём метаданные
metadata = SingleTableMetadata()
metadata.detect_from_dataframe(data)
metadata.set_primary_key('CustomerId')
metadata.update_column(column_name='Surname', sdtype='categorical')
metadata.update_column(column_name='Geography', sdtype='categorical')
metadata.update_column(column_name='Gender', sdtype='categorical')

# Создаем и обучаем модель
model = GaussianCopulaSynthesizer(
    metadata,
    enforce_min_max_values=True,
    numerical_distributions={'Balance': 'truncnorm'}
)
model.fit(data)

# Генерируем синтетику
synthetic_data = model.sample(num_rows=len(data))
synthetic_data

Тут мы используем модель GaussianCopula, которая отлично подходит для табличных данных с числовыми и категориальными признаками. Она сохраняет корреляциии создает синтетический набор, который можно применять для разработки и тестирования без риска утечки данных. Для Balance мы попробуем гамма-распределение (gamma) — оно хорошо подходит для положительных значений с правой скошенностью. После обучения получаем набор данных, который можно кинуть в модель предсказания оттока без риска утечек.

Пример сгенерированных данных:

Игра в имитацию: используем Python для генерации синтетических данных для ML и не только - 9

Теперь сравним распределения признаков для проверки качества генерации:

# Визуальное сравнение распределений на одной сетке
fig, axes = plt.subplots(4, 2, figsize=(15, 20))

# Список признаков
numerical_cols = ['Balance', 'Age', 'CreditScore', 'EstimatedSalary']
categorical_cols = ['Exited', 'Geography', 'Gender', 'HasCrCard']

# Индекс для размещения графиков
plot_idx = 0

# Числовые признаки: гистограммы с наложением
for col in numerical_cols:
    ax = axes[plot_idx // 2, plot_idx % 2]
    sns.histplot(data[col], kde=True, color='#2c7bb6', label='Реальные', ax=ax, alpha=0.6)
    sns.histplot(synthetic_data[col], kde=True, color='#d7191c', label='Синтетические', ax=ax, alpha=0.6)
    ax.set_title(f'Распределение {col}')
    ax.set_xlabel(col)
    ax.set_ylabel('Частота')
    ax.legend()
    plot_idx += 1

# Категориальные признаки: столбчатые диаграммы
for col in categorical_cols:
    ax = axes[plot_idx // 2, plot_idx % 2]
    real_counts = data[col].value_counts(normalize=True)
    synthetic_counts = synthetic_data[col].value_counts(normalize=True)
    comparison = pd.DataFrame({'Реальные': real_counts, 'Синтетические': synthetic_counts}).fillna(0)
    comparison.plot(kind='bar', ax=ax, color=['#2c7bb6', '#d7191c'], alpha=0.7)
    ax.set_title(f'Распределение {col}')
    ax.set_xlabel(col)
    ax.set_ylabel('Доля')
    ax.legend()
    plot_idx += 1

plt.tight_layout()
plt.show()
Игра в имитацию: используем Python для генерации синтетических данных для ML и не только - 10

Почти все распределения очень похожи друг на друга. Но с Balance загвоздка: GaussianCopula плохо работает с zero-inflated данными. Для таких случаев есть альтернативы вроде CTGAN, хотя они требуют больше ресурсов.

Пример 2: Генерация синтетических данных о загрязнении воздуха

Представим, что нам требуется создать синтетические временные ряды концентраций загрязняющих веществ в воздухе. За основу возьмём данные о загрязнении воздуха в городах Индии и будем генерировать показатели PM2.5 и NO2 для Дели.

import pandas as pd
import matplotlib.pyplot as plt
from sdv.sequential import PARSynthesizer
from sdv.metadata import SingleTableMetadata

# Загрузка данных
file_path = 'https://raw.githubusercontent.com/obulygin/content/refs/heads/main/india_air_pollution/city_day.csv'
data = pd.read_csv(file_path, encoding='ISO-8859-1', parse_dates=['Date'])

# Будет генерить синтетику только по показателям PM2.5 и NO2 в Дели
delhi_data = data[data['City'] == 'Delhi'][['Date', 'PM2.5', 'NO2']]

# Заполнение пропусков в исходных данных
delhi_data['PM2.5'] = delhi_data['PM2.5'].interpolate(method='linear')
delhi_data['NO2'] = delhi_data['NO2'].interpolate(method='linear')

delhi_data['SequenceID'] = 'Delhi_001'

# Создание метаданных
metadata = SingleTableMetadata()
metadata.detect_from_dataframe(delhi_data)

metadata_columns = {
    'SequenceID': 'id',
    'Date': 'datetime',
    'PM2.5': 'numerical',
    'NO2': 'numerical'
}

for column, sdtype in metadata_columns.items():
    metadata.update_column(column, sdtype=sdtype)

metadata.set_sequence_key('SequenceID')
metadata.set_sequence_index('Date')


# Инициализация и обучение модели
model = PARSynthesizer(
    metadata=metadata,
    epochs=70, 
    verbose=True,
    cuda=True  # Используем GPU если доступно
)

model.fit(delhi_data)

# Генерация данных
synthetic_data = model.sample(num_sequences=1)
synthetic_data

Тут мы используем модель PAR (Probabilistic Autoregressive Model) из SDV, разработанную специально для генерации синтетических временных рядов. Модель анализирует реальные данные, изучает их временную динамику, сезонные колебания и взаимосвязи. PAR учитывает не только распределение значений, но и автокорреляции во временных рядах.

Пример результата генерации:

Игра в имитацию: используем Python для генерации синтетических данных для ML и не только - 11

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

plt.figure(figsize=(14, 8))

# График PM2.5
plt.subplot(2, 1, 1)
plt.plot(delhi_data['Date'], delhi_data['PM2.5'], 
         label='Реальные данные', color='#2c7bb6', alpha=0.7)
plt.plot(synthetic_data['Date'], synthetic_data['PM2.5'],
         label='Синтетика SDV', linestyle='--', color='#d7191c')
plt.title('Динамика PM2.5: реальные vs синтетические данные')
plt.ylabel('µg/m³')
plt.grid(alpha=0.3)

# График NO2
plt.subplot(2, 1, 2)
plt.plot(delhi_data['Date'], delhi_data['NO2'], 
         color='#2c7bb6', alpha=0.7)
plt.plot(synthetic_data['Date'], synthetic_data['NO2'],
         linestyle='--', color='#d7191c')
plt.title('Динамика NO2: реальные vs синтетические данные')
plt.ylabel('µg/m³')

plt.tight_layout()
plt.legend()
plt.show()
Игра в имитацию: используем Python для генерации синтетических данных для ML и не только - 12
real_stats = delhi_data[['PM2.5', 'NO2']].describe().T
synthetic_stats = synthetic_data[['PM2.5', 'NO2']].describe().T

pd.concat([real_stats, synthetic_stats], 
          axis=1, 
          keys=['Реальные', 'Синтетические']).round(2)

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

Пример 3: Анализ поведения игроков в онлайн-игре

А теперь давайте сгенерируем данные для аналитики в онлайн-игре. Представим, что мы работаем над MMORPG или шутером вроде PUBG, и нужно протестировать модель предсказания оттока игроков. У меня нет примера данных, на которые тут можно опереться для генерации, поэтому будем генерить синтетику на основе придуманных данных 😱:

import sdv
from sdv.metadata import MultiTableMetadata
from sdv.multi_table import HMASynthesizer
import pandas as pd
from datetime import datetime

# Создаём метаданные
metadata = MultiTableMetadata()

# Таблица players
metadata.add_table(table_name='players')
metadata.add_column(table_name='players', column_name='player_id', sdtype='id')
metadata.add_column(table_name='players', column_name='region', sdtype='categorical')
metadata.add_column(table_name='players', column_name='level', sdtype='numerical')
metadata.add_column(table_name='players', column_name='registration_date', sdtype='datetime')

# Таблица sessions
metadata.add_table(table_name='sessions')
metadata.add_column(table_name='sessions', column_name='session_id', sdtype='id')
metadata.add_column(table_name='sessions', column_name='player_id', sdtype='id')
metadata.add_column(table_name='sessions', column_name='duration_minutes', sdtype='numerical')
metadata.add_column(table_name='sessions', column_name='kills', sdtype='numerical')

# Таблица purchases
metadata.add_table(table_name='purchases')
metadata.add_column(table_name='purchases', column_name='purchase_id', sdtype='id')
metadata.add_column(table_name='purchases', column_name='session_id', sdtype='id')
metadata.add_column(table_name='purchases', column_name='item_type', sdtype='categorical')
metadata.add_column(table_name='purchases', column_name='cost', sdtype='numerical')

# Устанавливаем связи
metadata.set_primary_key(table_name='players', column_name='player_id')
metadata.add_relationship(parent_table_name='players', child_table_name='sessions', 
                         parent_primary_key='player_id', child_foreign_key='player_id')
metadata.set_primary_key(table_name='sessions', column_name='session_id')
metadata.add_relationship(parent_table_name='sessions', child_table_name='purchases', 
                         parent_primary_key='session_id', child_foreign_key='session_id')

# Реальные данные (пример)
real_data = {
    'players': pd.DataFrame({
        'player_id': [1, 2, 3],
        'region': ['EU', 'ASIA', 'NA'],
        'level': [15, 5, 30],
        'registration_date': [datetime(2024, 1, 10), datetime(2025, 2, 15), datetime(2023, 11, 1)]
    }),
    'sessions': pd.DataFrame({
        'session_id': [101, 102, 103],
        'player_id': [1, 2, 3],
        'duration_minutes': [45, 20, 60],
        'kills': [8, 2, 15]
    }),
    'purchases': pd.DataFrame({
        'purchase_id': [1001, 1002, 1003],
        'session_id': [101, 102, 103],
        'item_type': ['skin', 'booster', 'weapon'],
        'cost': [500, 200, 1000]
    })
}

# Создаём и обучаем синтезатор
synthesizer = HMASynthesizer(metadata=metadata)
synthesizer.fit(real_data)

# Генерируем данные
synthetic_data = synthesizer.sample(scale=16.67)  # ~50 игроков

Что мы тут сделали? Определили три таблицы: players (кто играет), sessions (как играют) и purchases (что покупают). Связали их через ключи: один игрок — много сессий, одна сессия — много покупок. Дали SDV маленький кусочек данных (3 записи) и попросили увеличить его в 16.67 раз, чтобы получить около 50 игроков. После запуска получаем что-то вроде:

Синтетические игроки:
   player_id region  level registration_date
0          0     EU     14        2024-02-03
1          1   ASIA      6        2025-01-20
2          2     NA     28        2023-10-15
...

Синтетические сессии:
   session_id  player_id  duration_minutes  kills
0         100          0                40      7
1         101          1                18      1
2         102          2                65     13
...

Синтетические покупки:
   purchase_id  session_id item_type  cost
0         1000         100      skin   480
1         1001         101   booster   220
2         1002         102    weapon   950
...

Преимущества и недостатки SDV

➕:

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

  • Моделирование зависимостей. SDV умеет учитывать зависимости между признаками, временные зависимости и иерархические связи в данных, что критически важно для реалистичности синтетики.

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

  • Интеграция с другими инструментами. SDV легко интегрируется с pandas для обработки данных и может быть встроен в пайплайны машинного обучения или ETL-процессы.

➖:

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

  • Более ресурсоемкий процесс обучения моделей. Обучение моделей SDV, особенно сложных для больших объемов данных, может потребовать значительных вычислительных ресурсов и времени.

  • Может быть избыточен для простых задач. Для простых задач, где достаточно генерации базовых типов данных, использование SDV может быть избыточным и неоправданно сложным.

Когда стоит использовать?

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

  • Высокой реалистичности и статистического соответствия синтетики реальным данным.

  • Генерации сложных структурированных данных, временных рядов, связанных данных, графовых данных.

  • Моделирования зависимостей и сложных сценариев.

  • Задач, где качество синтетических данных критически важно для обучения моделей или имитации бизнес-процессов.

А в каких случаях этот инструмент не очень подойдет?

  • Небольшие наборы данных с низкой вариативностью (SDV может начать просто копировать исходные объекты)

  • Сложные текстовые поля (SDV не предназначен для генерации качественных текстов и будет выдавать бессмыслицу)

  • Данные со сверхредкими событиями и аномалиями (из-за сильного дисбаланса подобные редкие кейсы SDV может вообще упустить)

Далее мы рассмотрим Gretel Synthetics, библиотеку ориентированную на генерацию приватных синтетических данных.

Gretel Synthetics: генерация приватных синтетических данных

Игра в имитацию: используем Python для генерации синтетических данных для ML и не только - 13

Gretel Synthetics ориентирована на создание Privacy-Preserving Synthetic Data. В основе библиотеки лежат передовые методы анонимизации и генеративного моделирования, реализованные в виде набора конфигурируемых моделей и инструментов. Библиотека предлагает различные генеративные модели, включая LSTM, Transformer и другие, для работы с табличными, текстовыми и временными рядами данных.

Устанавливаем либу через pip install gretel-synthetics и смотрим примеры 👇🏻

Пример 1: Генерация реалистичных логов ошибок

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

from gretel_synthetics import TextGenerator
import pandas as pd

# Шаблон для обучения модели (логи)
training_data = [
    "ERROR [2023-11-05 12:30:45] Service 'payment_gateway' timeout (500ms > 300ms threshold)",
    "WARN [2023-11-05 12:31:10] Database connection pool 80% full",
    "INFO [2023-11-05 12:31:22] User 2345 initiated USD to EUR transfer"
]

pd.DataFrame(training_data, columns=["log"]).to_csv("logs.csv", index=False)

# Конфигурация модели с анонимизацией
config = {
    "model_type": "LSTM",
    "epochs": 50,
    "anonymize": True  # Маскируем цифры и названия сервисов
}

# Обучение и генерация
generator = TextGenerator(config=config)
generator.train("logs.csv")
synth_logs = generator.generate(num_records=100)

print(synth_logs[0])
# Пример: "ERROR [2023-12-18 09:15:33] Service 'invoice_processor' timeout (620ms > 250ms threshold)"

Что происходит под капотом:

  • Модель LSTM обучается на предоставленных шаблонах логов, выявляя структуру и закономерности текстовых сообщений.

  • Параметр anonymize: True активирует встроенные фильтры анонимизации, которые маскируют цифровые значения (даты, время, ID) и названия сервисов, заменяя их на синтетические аналоги.

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

Пример 2: Синтетические транзакции с контекстом

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

from gretel_synthetics import TextGenerator
import pandas as pd

# Пример данных транзакций
training_data = [
    "user_1234, 2023-11-05, 45.67, 'Supermarket'",
    "user_5678, 2023-11-05, 120.00, 'Electronics Store'",
    "user_1234, 2023-11-06, 22.50, 'Coffee Shop'"
]
pd.DataFrame([x.split(", ") for x in training_data], 
             columns=["user_id", "date", "amount", "merchant"]).to_csv("transactions.csv", index=False)

# Конфигурация: Transformer для точности
config = {
    "model_type": "Transformer",
    "epochs": 100,
    "vocab_size": 5000,
    "anonymize_fields": ["user_id", "amount"]  # Анонимизируем ID и суммы
}

# Обучаем и генерируем
generator = TextGenerator(config=config)
generator.train("transactions.csv")
synth_transactions = generator.generate(num_records=500)

# Пример результата
print(synth_transactions[0])
# "user_5842, 2023-12-18, 234.50, 'Coffee House'"

Transformer улавливает зависимости: если в реальных данных больше трат по понедельникам, синтетика это повторит. Суммы остаются в реалистичном диапазоне, но точные значения и ID маскируются.

Преимущества и недостатки Gretel Synthetics

➕:

  • Фокус на приватности. Это не просто инструмент общего назначения, а специализированное решение для задач, где защита конфиденциальной информации является приоритетом.

  • Гибкость и конфигурируемость. Gretel Synthetics предоставляет широкие возможности для настройки процесса генерации. Можно выбирать различные генеративные модели (LSTM, Transformer и др.), конфигурировать параметры обучения, настраивать стратегии анонимизации и управлять качеством и приватностью генерируемых данных. Конфигурация возможна через YAML или JSON-like словари, а Python SDK позволяет расширять функциональность библиотеки и интегрировать ее в существующие воркфлоу.

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

➖:

  • Сложна в освоении. Будучи более мощным и специализированным инструментом, может быть сложнее в освоении для новичков, чем, например, Faker.

  • Требования к вычислительным ресурсам. Генерация синтетических данных с использованием сложных моделей, таких как Transformer, может потребовать значительных вычислительных ресурсов, особенно для больших датасетов.

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

Но что, если мы хотим работать с изображениями и прочими сложными мультимодальными  данными? В следующем разделе мы разберем, как с помощью PyTorch обучить свою первую GAN.

GANs с примерами на PyTorch

Игра в имитацию: используем Python для генерации синтетических данных для ML и не только - 14

Теперь давайте рассмотрим использование Generative Adversarial Networks (GANs) — генеративных состязательных сетей. GANs — инструмент для генерации синтетических данных, таких как изображения, текст или аудио, и широко применяются там, где требуется высокая степень реализма. Хотя SDV и Gretel Synthetics включают в себя GAN-модели и их вариации, но построение GAN с нуля даёт нам полный контроль над процессом и позволяет адаптировать модель под любые задачи.

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

Как работает?

Идея GANs достаточно элегантна: столкнуть лбами две нейросети – генератор и дискриминатор.

  • Генератор (Generator) — это “фальшивомонетчик”, который из случайного шума (например, вектора из случайных чисел) создаёт данные, похожие на реальные. Его цель – создать что-то настолько правдоподобное, чтобы дискриминатор поверил, что это реально.

  • Дискриминатор (Discriminator) — это “детектор подделок”, который учится отличать синтетику от оригинала. Получает на вход два типа данных: настоящие и “фейковые” (от генератора). И определяет, что перед ним: реальный объект или сгенерированный. Его цель – научиться определять генерации так хорошо, что генератор не сможет его “обмануть”.

Таким образом, процесс обучения GANs – это соревнование между генератором и дискриминатором:

  1. Генератор создаёт пакет поддельных данных.

  2. Дискриминатор оценивает их на “подлинность”, сравнивая с реальными данными.

  3. Обе сети обновляют веса, чтобы стать лучше в своей роли.

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

Приступаем к примерам 👇🏻

Пример 1: Простая GAN на PyTorch для MNIST

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

Сначала установим PyTorch: pip install torch torchvision и сделаем все импорты, а также загрузим данные:

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torchvision.utils import save_image
import os  # Импортируем модуль os для работы с путями

# Преобразование данных в тензоры и нормализация
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5,), (0.5,))
])

# Загрузка MNIST
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=64, shuffle=True)

Теперь определим класс для генератора, сделаем простую сеть, которая из шума создаёт изображение 28×28:

class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()
        self.fc1 = nn.Linear(128, 1024) # Входной шум размером 128
        self.fc2 = nn.Linear(1024, 2048)
        self.fc3 = nn.Linear(2048, 784)
        self.activation = nn.ReLU() # ReLU активация

    def forward(self, x):
        x = self.activation(self.fc1(x))
        x = self.activation(self.fc2(x))
        x = self.fc3(x)
        x = x.view(-1, 1, 28, 28)
        return nn.Tanh()(x) # Tanh активация на выходе

И создадим дискриминатор — архитектуру, которая будет отличать реальные изображения от сгенерированных:

class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()
        self.fc1 = nn.Linear(784, 512)
        self.fc2 = nn.Linear(512, 1)
        self.activation = nn.LeakyReLU(0.1) # LeakyReLU с наклоном 0.1

    def forward(self, x):
        x = x.view(-1, 784)
        x = self.activation(self.fc1(x))
        x = self.fc2(x)
        return nn.Sigmoid()(x) # Sigmoid активация на выходе

Теперь определяем все необходимые объекты и запускаем обучение:

# Инициализация
G = Generator() 
D = Discriminator() 
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
G.to(device)
D.to(device)

# Оптимизаторы
G_optimizer = optim.Adam(G.parameters(), lr=0.0002, betas=(0.5, 0.999))
D_optimizer = optim.Adam(D.parameters(), lr=0.0002, betas=(0.5, 0.999)) 
loss = nn.BCELoss() # Функция потерь — BCELoss

# Параметры обучения
epochs = 50 # Количество эпох
latent_dim = 128 # Размерность входного шума для генератора
num_examples_to_generate = 16 # Количество изображений для сохранения

# Директория для сохранения сгенерированных изображений
output_dir = 'generated_images'
if not os.path.exists(output_dir):
    os.makedirs(output_dir)

# Сохранение изображений до начала обучения
with torch.no_grad(): # Отключаем отслеживание градиента для генерации изображений
    fixed_noise = torch.randn(num_examples_to_generate, latent_dim).to(device)
    generated_images = G(fixed_noise) # Генерируем изображения
    save_image(generated_images, os.path.join(output_dir, f'generated_epoch_0.png'), normalize=True) # Сохраняем изображения
    print(f'Примеры сгенерированных изображений на нулевой эпохе сохранены в {output_dir}/generated_epoch_0.png')


for epoch in range(epochs):
    for idx, (imgs, _) in enumerate(train_loader):
        idx += 1

        # Обучение дискриминатора
        real_inputs = imgs.to(device)
        real_outputs = D(real_inputs)
        real_label = torch.ones(real_inputs.shape[0], 1).to(device) # Метки для реальных изображений - 1
        noise = (torch.rand(real_inputs.shape[0], latent_dim) - 0.5) / 0.5 # Генерация шума в диапазоне [-1, 1]
        noise = noise.to(device)
        fake_inputs = G(noise)
        fake_outputs = D(fake_inputs)
        fake_label = torch.zeros(fake_inputs.shape[0], 1).to(device) # Метки для фейковых изображений - 0

        # Расчет потерь дискриминатора
        outputs = torch.cat((real_outputs, fake_outputs), 0) # Объединение выходов дискриминатора на реальных и фейковых изображениях
        targets = torch.cat((real_label, fake_label), 0) # Объединение меток
        D_loss = loss(outputs, targets) # Расчет BCELoss

        # Оптимизация дискриминатора
        D_optimizer.zero_grad()
        D_loss.backward()
        D_optimizer.step()

        # бучение генератора
        noise = (torch.rand(real_inputs.shape[0], latent_dim) - 0.5) / 0.5 # Генерация шума для генератора
        noise = noise.to(device)
        fake_inputs = G(noise)
        fake_outputs = D(fake_inputs)
        fake_targets = torch.ones([fake_inputs.shape[0], 1]).to(device) # Метки для генератора - хотим, чтобы дискриминатор думал, что фейковые изображения - реальные (1)

        # Расчет потерь генератора
        G_loss = loss(fake_outputs, fake_targets) # Расчет BCELoss

        # Оптимизация генератора
        G_optimizer.zero_grad()
        G_loss.backward()
        G_optimizer.step()

        # Вывод прогресса
        if idx % 100 == 0 or idx == len(train_loader):
            print(f'Эпоха [{epoch}/{epochs}] Итерация [{idx}/{len(train_loader)}]: discriminator_loss: {D_loss.item():.3f} generator_loss: {G_loss.item():.3f}')

    # Сохранение модели генератора каждые 10 эпох
    if (epoch+1) % 10 == 0:
        torch.save(G, f'Generator_epoch_{epoch+1}.pth') # Сохраняем модель G
        print('Модель генератора сохранена.')

    # Сохранение сгенерированных изображений каждые 10 эпох
    if (epoch+1) % 10 == 0:
        with torch.no_grad(): 
            fixed_noise = torch.randn(num_examples_to_generate, latent_dim).to(device)
            generated_images = G(fixed_noise) # Генерируем изображения
            save_image(generated_images, os.path.join(output_dir, f'generated_epoch_{epoch+1}.png'), normalize=True)
            print(f'Примеры сгенерированных изображений сохранены в {output_dir}/generated_epoch_{epoch+1}.png')

Нейросетка начинает работать со случайным шумом:

Игра в имитацию: используем Python для генерации синтетических данных для ML и не только - 15

Вот так через 10 эпох:

Игра в имитацию: используем Python для генерации синтетических данных для ML и не только - 16

А вот так, если подождём ещё:

Игра в имитацию: используем Python для генерации синтетических данных для ML и не только - 17

Пример 2: Усложняем — GAN для цветных изображений (CIFAR-10)

Можно попробовать что-то и посложнее, например, сгенерировать цветные изображения на основе набора CIFAR-10 (32×32 пикселя, 3 канала RGB). Без свёрток тут уже не обойтись.

Так можно загрузить данные.

transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])

train_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=64, shuffle=True)

Теперь генератор должен создавать изображения 3x32x32 вместо 1x28x28, а дискриминатор, соответственно, должен принимать на вход изображения 3x32x32.

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

# Параметры обучения
epochs = 100 
latent_dim = 128
num_examples_to_generate = 16

class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()
        self.main = nn.Sequential(
            nn.ConvTranspose2d(128, 256, 4, 1, 0, bias=False),
            nn.BatchNorm2d(256),
            nn.ReLU(True),
            nn.ConvTranspose2d(256, 128, 4, 2, 1, bias=False),
            nn.BatchNorm2d(128),
            nn.ReLU(True),
            nn.ConvTranspose2d(128, 64, 4, 2, 1, bias=False),
            nn.BatchNorm2d(64),
            nn.ReLU(True),
            nn.ConvTranspose2d(64, 3, 4, 2, 1, bias=False),
            nn.Tanh()
        )

    def forward(self, x):
        return self.main(x.view(-1, 128, 1, 1))

class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()
        self.main = nn.Sequential(
            nn.Conv2d(3, 64, 4, 2, 1, bias=False),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Conv2d(64, 128, 4, 2, 1, bias=False),
            nn.BatchNorm2d(128),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Conv2d(128, 256, 4, 2, 1, bias=False),
            nn.BatchNorm2d(256),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Conv2d(256, 1, 4, 1, 0, bias=False),
            nn.Sigmoid()
        )

    def forward(self, x):
        return self.main(x).view(-1)

Сгенерированные изображения со временем начнут напоминать объекты из CIFAR-10: самолёты, машины, животных. Но времени и компьюта на это понадобится сильно больше.

Пример 3: Использования GANs в PyTorch для генерации временных рядов

Рассмотрим, как с помощью PyTorch можно сгенерировать синтетические данные временных рядов, например, для моделирования финансовых данных или временных рядов IoT-устройств.

import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
from torch.utils.data import DataLoader, TensorDataset

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# Генерация синтетического временного ряда
seq_length = 30
num_sequences = 1000

# Генерация реальных данных (синусоидальные сигналы с шумом)
real_data = np.sin(np.linspace(0, 100, seq_length * num_sequences)).reshape(num_sequences, seq_length, 1) + 
           np.random.normal(0, 0.1, (num_sequences, seq_length, 1))

# Создание даталоадера
tensor_data = torch.tensor(real_data, dtype=torch.float32)
dataset = TensorDataset(tensor_data)
dataloader = DataLoader(dataset, batch_size=32, shuffle=True)

# Определение генератора
class Generator(nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim, num_layers=1):
        super(Generator, self).__init__()
        self.hidden_dim = hidden_dim
        self.num_layers = num_layers
        self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers=num_layers, batch_first=True)
        self.fc = nn.Linear(hidden_dim, 64)
        self.relu = nn.LeakyReLU(0.2)
        self.output = nn.Linear(64, output_dim)
        self.tanh = nn.Tanh()
    
    def forward(self, x):
        # Начальное состояние LSTM
        h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_dim).to(x.device)
        c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_dim).to(x.device)
        
        # Прямой проход через LSTM
        out, _ = self.lstm(x, (h0, c0))
        
        out = self.fc(out)
        out = self.relu(out)
        out = self.output(out)
        out = self.tanh(out)
        return out

# Определение дискриминатора
class Discriminator(nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim, num_layers=1):
        super(Discriminator, self).__init__()
        self.hidden_dim = hidden_dim
        self.num_layers = num_layers
      
        self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers=num_layers, batch_first=True)
        self.fc1 = nn.Linear(hidden_dim, 64)
        self.leaky_relu = nn.LeakyReLU(0.2)
        self.fc2 = nn.Linear(64, output_dim)
        self.sigmoid = nn.Sigmoid()
    
    def forward(self, x):
        # Начальное состояние LSTM
        h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_dim).to(x.device)
        c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_dim).to(x.device)
        
        # Прямой проход через LSTM
        out, _ = self.lstm(x, (h0, c0))
        
        out = self.fc1(out[:, -1, :])
        out = self.leaky_relu(out)
        out = self.fc2(out)
        out = self.sigmoid(out)
        return out

# Параметры модели
input_dim = 1
hidden_dim = 64 
output_dim = 1

# Инициализация моделей
generator = Generator(input_dim, hidden_dim, output_dim).to(device)
discriminator = Discriminator(input_dim, hidden_dim, output_dim).to(device)

# Оптимизаторы и функции потерь
criterion = nn.BCELoss()
optimizer_g = optim.Adam(generator.parameters(), lr=0.0002, betas=(0.5, 0.999))
optimizer_d = optim.Adam(discriminator.parameters(), lr=0.0002, betas=(0.5, 0.999))

# Функция для визуализации результатов
def visualize_results(epoch, generator, real_sample):
    generator.eval()
    with torch.no_grad():
        noise = torch.randn(1, seq_length, input_dim).to(device)
        generated_sequence = generator(noise).cpu().numpy().squeeze()
        real_sequence = real_sample.cpu().numpy().squeeze()
        
        plt.figure(figsize=(12, 6))
        plt.plot(real_sequence, label='Реальная последовательность')
        plt.plot(generated_sequence, label='Сгенерированная последовательность', linestyle='dashed')
        plt.legend()
        plt.title(f'Синтетический временной ряд на эпохе {epoch+1}')
        plt.savefig(f'gan_epoch_{epoch+1}.png')
        plt.close()
    generator.train()

# Обучение моделей
num_epochs = 60 
batch_size = 32

# Списки для отслеживания потерь
g_losses = []
d_losses = []

for epoch in range(num_epochs):
    d_loss_epoch = 0
    g_loss_epoch = 0
    batches = 0
    
    for real_sequences in dataloader:
        real_sequences = real_sequences[0].to(device)
        batch_size = real_sequences.size(0)
        
        # Создание реальных и фейковых меток
        real_labels = torch.ones(batch_size, 1).to(device)
        fake_labels = torch.zeros(batch_size, 1).to(device)
        
        # Обучение дискриминатора
        optimizer_d.zero_grad()
        
        # Потери на реальных данных
        outputs_real = discriminator(real_sequences)
        d_loss_real = criterion(outputs_real, real_labels)
        
        # Генерация фейковых данных
        noise = torch.randn(batch_size, seq_length, input_dim).to(device)
        fake_sequences = generator(noise)
        
        # Потери на фейковых данных
        outputs_fake = discriminator(fake_sequences.detach())
        d_loss_fake = criterion(outputs_fake, fake_labels)
        
        # Суммарная потеря дискриминатора
        d_loss = d_loss_real + d_loss_fake
        d_loss.backward()
        optimizer_d.step()
        
        # Обучение генератора
        optimizer_g.zero_grad()
        
        outputs_fake = discriminator(fake_sequences)
        g_loss = criterion(outputs_fake, real_labels)
        
        g_loss.backward()
        optimizer_g.step()
        
        # Накопление потерь
        d_loss_epoch += d_loss.item()
        g_loss_epoch += g_loss.item()
        batches += 1
    
    # Средние потери за эпоху
    d_loss_epoch /= batches
    g_loss_epoch /= batches
    
    # Сохранение потерь для визуализации
    g_losses.append(g_loss_epoch)
    d_losses.append(d_loss_epoch)
    
    print(f'Эпоха [{epoch+1}/{num_epochs}], Потеря D: {d_loss_epoch:.4f}, Потеря G: {g_loss_epoch:.4f}')
    
    # Визуализация результатов каждые 10 эпох
    if (epoch + 1) % 10 == 0 or epoch == 0:
        visualize_results(epoch, generator, real_sequences[0].unsqueeze(0))

# Визуализация потерь
plt.figure(figsize=(10, 5))
plt.title("Потери генератора и дискриминатора")
plt.plot(g_losses, label="Генератор")
plt.plot(d_losses, label="Дискриминатор")
plt.xlabel("Эпоха")
plt.ylabel("Потеря")
plt.legend()
plt.savefig('losses.png')
plt.show()

# Финальная генерация нескольких примеров
generator.eval()
with torch.no_grad():
    plt.figure(figsize=(15, 10))
    
    # Генерация 5 разных последовательностей
    for i in range(5):
        noise = torch.randn(1, seq_length, input_dim).to(device)
        generated_sequence = generator(noise).cpu().numpy().squeeze()
        
        plt.subplot(5, 1, i+1)
        plt.plot(generated_sequence)
        plt.title(f'Сгенерированная последовательность {i+1}')
    
    plt.tight_layout()
    plt.savefig('final_generated_sequences.png')
    plt.show()

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

В начале получается не очень (1 эпоха):

Игра в имитацию: используем Python для генерации синтетических данных для ML и не только - 18

А вот на 60 эпохе результат уже лучше:

Игра в имитацию: используем Python для генерации синтетических данных для ML и не только - 19

GANs также применяются и для генерации синтетических текстов от новостных статей до кода и диалогов. Для этого используются модели типа TextGAN, SeqGAN, MaliGAN.Примеры кода уже будет сильно более сложными и требовательным к ресурсам, но общий принцип остается тем же: обучение генератора и дискриминатора в соревновательном режиме, где генератор учится генерировать последовательности символов или слов, а дискриминатор – отличать их от реальных текстов.

Преимущества и недостатки GANs

➕:

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

  • Генерация разных типов данных. Как мы уже увидели, GANs не ограничиваются только табличными данными или простыми распределениями. Они позволяют генерировать сложные, высокоразмерные данные разных типов – изображения, тексты, аудио, видео, временные ряды, графы и пр.

  • Постоянное развитие и появление новых архитектур. Область GANs очень динамично развивается, постоянно появляются новые архитектуры, методы обучения и приемы, улучшающие качество и стабильность генерации синтетических данных.

➖:

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

  • Очень ресурсоёмкий процесс обучения. Обучение GANs, особенно для генерации высококачественных изображений или текстов, требует значительных вычислительных ресурсов – мощных GPU и много времени.

Когда стоит использовать?

GANs – хороший выбор, когда нам нужны синтетические данные максимально высокого качества и реалистичности:

  • Генерация фотореалистичных изображений лиц, объектов.

  • Синтез текстов разных жанров и стилей для генерации контента.

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

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

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

Сравнительная таблица инструментов

Критерий

Faker

Scikit-learn

SDV

Gretel Synthetics

GANs

Сложность

🟢Низкая

🟢Низкая

🟡Средний

🟡Средний

🔴Экспертная

Реалистичность

🔴Низкая

🟡Средняя

🟢Высокая

🟢Высокая

🟢Очень высокая

Типы данных

Текст, числа, даты, адреса, имена и т.д.

В основном табличные данные для ML

Таблицы, временные ряды, графы

Таблицы, текст, временные ряды, изображения, логи

По сути любые

Приватность

🟡Частичная (анонимизация)

🔴Нет

🟡Встроенные методы (k-anonymity)

🔴Высокая

🟡Зависит от реализации

Гибкость

🔴Низкая

🟡Средняя

🟢Высокая

🟢Высокая

🟢Максимальная

Ресурсоёмкость

🟢Низкая

🟢Низкая

🟡Средняя

🟡Средняя

🔴Высокая

Когда избегать

Для ML-моделей

Для связанных данных

Для текста/аудио

Для изображений

Для простых табличных данных

Основы оценки качества синтетических данных

Любые синтетические данные бесполезны, если не соответствуют своему назначению. Но как объективно оценить их качество? Инструменты вроде SDV и Gretel Synthetics уже предлагают встроенные способы оценки (например, evaluate_quality в SDV или отчёты Gretel), но знать об универсальных подходах всё равно полезно. Начнем с фундамента – статистических метрик и визуализации.

Статистические тесты и метрики

Первое, что приходит на ум, – сравнить распределения признаков в синтетических и реальных данных. В идеале, они должны быть похожи. Для этого есть разные инструменты, и один из самых известных – тест Колмогорова-Смирнова (KS-тест). Он позволяет проверить, насколько вероятно, что две выборки данных взяты из одного и того же распределения. Чем меньше значение p-value в KS-тесте, тем больше различий между распределениями, и тем хуже.

Давайте применим этот текст к синтетике о клиентах банка, которую нагенерировали в первом примере SDV:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.stats import ks_2samp, pearsonr

# Загрузим реальные и синтетические данные
real_data = pd.read_csv('https://raw.githubusercontent.com/obulygin/content/refs/heads/main/bank_churn/Churn.csv').drop(columns=['RowNumber', 'CustomerId', 'Tenure'])
synthetic_data = pd.read_csv('synthetic_data.csv')

numerical_cols = real_data.select_dtypes(include=[np.float64, np.int64]).columns
for col in numerical_cols:
    stat, p_val = ks_2samp(real_data[col], synthetic_data[col])
    print(f"{col}: KS-статистика = {stat:.4f}, p-value = {p_val:.4f}")
    if p_val < 0.05:
        print(f"  -> Распределения отличаются (p < 0.05)")

Что значит результат?

  • p-value > 0.05 — распределения похожи, синтетика в порядке.

  • p-value < 0.05 — есть отличия, пора копать глубже.

У нас ситуация такая:

Игра в имитацию: используем Python для генерации синтетических данных для ML и не только - 20

Видим, что с распределением возраста и баланса (в том примере мы на баланс как раз обращали внимание) есть проблемы. Они значимо отличаются от распределения исходных данных. Это точки улучшения нашего подхода.

Но KS-тест не идеален, особенно если распределения сложные или многомерные. В таких случаях более информативными могут быть метрика Вассерштейна (Wasserstein distance), она же Earth Mover’s Distance (EMD), и KL-дивергенция (Kullback-Leibler divergence). Вассерштейн, грубо говоря, показывает, сколько “земли” нужно “передвинуть”, чтобы преобразовать одно распределение в другое. KL-дивергенция измеряет, насколько одно распределение отличается от другого с точки зрения информации. Эти метрики более чувствительны к форме распределений и лучше подходят для сложных данных:

from scipy.stats import wasserstein_distance, entropy

for col in numerical_cols:
    w_dist = wasserstein_distance(real_data[col], synthetic_data[col])
    print(f"{col}: Wasserstein Distance = {w_dist:.4f}")
    real_hist, bins = np.histogram(real_data[col], bins=50, density=True)
    synth_hist, _ = np.histogram(synthetic_data[col], bins=bins, density=True)
    kl_div = entropy(real_hist, synth_hist)
    print(f"{col}: KL-дивергенция = {kl_div:.4f}")
Игра в имитацию: используем Python для генерации синтетических данных для ML и не только - 21

Чем меньше значения, тем больше схожесть.

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

Распределения – это важно, но не менее важны корреляции между признаками. В реальных данных признаки часто связаны друг с другом, и синтетика должна это отражать. Чтобы проверить это в синтетических данных, строим матрицы корреляций для реальных и синтетических данных и сравниваем их. Визуализация в виде тепловых карт поможет наглядно увидеть различия. Идеально, если матрицы корреляций для реальных и синтетических данных будут максимально похожи. Здесь для примера используем коэффициент phi_k, как наиболее универсальный вариант:

import phik 
# Расчет корреляции Phi-K для реальных данных
real_corr = real_data[numerical_cols].phik_matrix()

# Расчет корреляции Phi-K для синтетических данных
synthetic_corr = synthetic_data[numerical_cols].phik_matrix()

# Вычисление разности корреляций
corr_diff = abs(real_corr - synthetic_corr)

# Визуализация разности корреляций
plt.figure(figsize=(8, 6))
sns.heatmap(corr_diff, annot=True, cmap='coolwarm', vmin=0, vmax=1)
plt.title('Разность корреляций Phi-K (0 — идеально, 1 — плохо)')
plt.show()
Игра в имитацию: используем Python для генерации синтетических данных для ML и не только - 22

Ну а самый честный способ — проверить синтетику в бою. Обучим модель на синтетических данных и протестировать на реальных

Если метрики будут близки, то синтетика — топ. Если нет — улучшаем генерацию или..

Как избежать переобучения на синтетических данных

Представьте, что мы решили обучить модель машинного обучения исключительно на синтетических данных. Сгенерировали огромный датасет, используя самые продвинутые методы, и вот наша модель на этих данных показывает отличные результаты.Теперь мы запускаем эту модель на реальных данных, и… качество ужасное. Что случилось? А случилось то самое “синтетическое” переобучение.

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

Как бороться с переобучением?

  • Смешиваем синтетику с реальными данными. Стоит использовать в своих задачах те данные, которые есть, но вот если их не хватает – то тогда дополняем их синтетикой. Такой подход часто называют data augmentation с использованием синтетических данных. Он позволяет расширить тренировочную выборку, сделать модель более устойчивой к разнообразию реальных данных и улучшить ее обобщающую способность.

  • Регуляризация при генерации. Еще один интересный подход – это регуляризация не модели, а самого процесса генерации синтетических данных. Идея в том, чтобы изначально генерировать более разнообразные и менее “зашумленные” синтетические данные, которые меньше “запоминают” особенности алгоритма генерации. Например, при использовании GANs можно добавлять noise injection на разных этапах генерации, или применять различные техники сэмплирования, чтобы увеличить вариативность синтетических данных.

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

Баланс “качество-затраты”

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

Конечно, стоит учитывать объем генерируемых данных. Понятно, что сгенерировать 1000 строк данных – это одно, а 100 миллионов – совсем другое. Время генерации, потребление памяти и дискового пространства растут пропорционально объему. Если нам нужны “терабайты синтетики”, то это будет стоить дорого. А когда ресурсы ограничены, приходится искать компромисс.

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

Как найти баланс?

  1. Начинаем с простого. Не стреляйте из пушки по воробьям. Если для вашей задачи достаточно Faker или Scikit-learn, не стоит сразу бросаться на изучение GANs. А если простых инструментов не хватит – переходим к более сложным.

  2. Параллелим все, что можно. Генерация синтетических данных часто отлично распараллеливается. Стоит использовать многопроцессорность, многопоточность, распределенные вычисления, облачные сервисы – все, что позволит ускорить процесс.

  3. Смотрим за кривой обучения. Для генеративных моделей типа GAN часто после определенного количества эпох качество улучшается незначительно при существенном росте затрат на обучение.

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

Наша задача – найти оптимальный баланс, исходя из конкретной задачи и доступных ресурсов. Лучшее – враг хорошего. Почти всегда “достаточно хорошее” качество данных за разумную цену – это именно то, что нужно.

Заключение: синтетические данные — больше, чем просто имитация

Мы посмотрели на разнообразные инструменты для генерации синтетики – от простых решений до более продвинутых. Также мы познакомились с основами оценки качества синтетики, балансировать между затратами и реализмом, и предотвращать “синтетическое” переобучение.

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

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

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

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

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

Делитесь опытом создания синтетики в комментах 👇🏻

👉Если тебе интересны и другие полезные материалы по IT и Python, то можешь подписаться на мой канал PythonTalk или посетить сайт OlegTalks👈

Автор: obulygin

Источник

Рейтинг@Mail.ru
Rambler's Top100