• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Стеганография с использованием нейронных сетей (Вложенные автоэнкодеры)

RodionRaskolnikov

HDD-drive
Пользователь
Регистрация
06.08.2023
Сообщения
23
Реакции
30
1234.jpg

Привет, друзья. Приперся я на этот печально известный форум с мыслью поделиться знаниями и узнать что-то новое, но как-то не сложилось, и мой аккаунт затерялся в глубинах интернета. Да ладно, пусть ЦРУ его и ломает, что там им интересного в моих постах? Немного отдохнул, и вот, решил зарегистрироваться заново, начать все с чистого листа. Статья была готова на 30%, вбил в поиск 'стеганография', и нашел тему: - DildoFagins говорил, что хочет пробудить в нас интерес к стеганографии. Но глядя на наш форум, не нашел больше интересных статей по этой теме. Ну что ж, придется писать еще одну, если, конечно, наша публика заинтересована. Это будет неофициальное продолжение статьи DildoFagins. Так что советую всем почитать этот шедевр, там все классно расписано про LSB. Что касается моей статьи, это мой дебют здесь, и, если говорить шире, первая полезная инфа для форума от меня. XSS дал мне многое. У меня есть опыт написания научных статей, но блог – это что-то другое. На практике – гораздо веселее, честно говоря.

Проблема с JPEG​

jpeg.png

"С другой стороны, в формате JPEG информация о пикселях сжимается с потерями, поэтому хранить и восстанавливать полезные данные таким же образом у нас не получится. Возможно, мы потом рассмотрим, как реализовать стеганографию в формате JPEG, если вам будет это интересно..." - DildoFagins
Для начала нам нужно понять, как работает JPEG: Так что, в отличие от нашего PNG, мы не можем напрямую изменять младший значащий бит изначального изображения, но нам нужно действовать до уровня кодирования Хаффмана и после квантования, потому что все дальше идет без потерь. Но, дорогие товарищи, лично я не нашел таких библиотек на python, которые бы позволяли работать с промежуточными этапами процесса кодирования JPEG. Насколько я понимаю, большинство кодировщиков/декодировщиков JPEG не позволят нам установить контрольные точки в процессе кодирования. Если кто-то знает что-то по этому поводу, обсудите в комментариях, пожалуйста. Конечно, мы можем выбрать открытую библиотеку с github и аккуратно вызвать внутренние функции, но это будет целая заморочка для нас. Если кто-то знает такие библиотеки, дайте знать, или может быть, мы позже напишем ее сами. Всё-таки, чтобы понять, куда умно засунуть наши секретные данные, мне нужно освоить весь процесс. Но это позже, там все скучно.

Нейросети​

Давайте будем считать, что у вас уже есть базовое понимание нейронов, весов, отклонений, MLP, свертки и основ pytorch. Всё это можно найти в Сети, так что мы не будем "тащить лямку" в этой статье. Это и не совсем по теме нашего форума. Если ошибаюсь, admin, поправь меня. Если это подходит для нашего форума, рассмотрю этот вопрос поподробнее. Мне не интересно то, что уже валяется на medium.com, stackoverflow, github. А, кстати, добавьте поддержку markdown вместо устаревшего BBCode. Пора бы уже отправить этого старичка на покой (шучу, шучу! Мы любим и уважаем BBCode, наше детство с начала 2000-х, когда молодой Рунет только ставил свои первые шаги. Но мои молодые коллеги смеяться, если я не буду использовать модный markdown). Добавьте возможность редактирования статей в markdown, это же так удобно!
Итак, перейдем к делу. Эта статья посвящена принципу стеганографии изображений, чтобы создать аналогичное изображение с нашим закодированным изображением. Самый простой метод, который приходит в голову, - это изменение LSB. Преимущество скрытия данных изображения в том, что допускается потеря качества. В целом, наш метод будет потерянным, так что наша конечная цель после создания PoC (то есть каким-то образом создать сеть, чтобы скрыть ее в основном изображении) - это улучшить качество как основного (в приоритете), так и скрытого изображения. Но читая эту статью до конца, вы поймете, почему эти цели неразделимы.
Мы не будем акцентировать внимание на шифровании данных. В первой статье вы, возможно, заметили, что DildoFagins говорил, что шифрование не влияет на алгоритм LSB. Вы можете подумать: "Ну, добавим простой слой шифрования и дешифрования AES-256, какая разница?" Это верно для LSB, но для нейросетей это намного сложнее. Обсудим, возможно ли это в принципе, когда это возможно, потому что если это возможно, то мы как бы подрываем стеганографию нейросети (это ваша подсказка, если вы поняли, о чем я, оставьте свой комментарий после прочтения статьи). Возможно, в другой статье расскажем, как вставить криптографию в нашу стеганографическую нейросеть, если товарищи заинтересованы (это вообще возможно? если да, то насколько сложно?). Теперь можем глупо разделить процесс на две части: генерацию самого основного изображения с шумом + кодирование наших данных.

Создание обложки со шумами
Сначала попробуем создать что-то похожее на изображение, при этом никакой особенный шум мы сами добавлять не будем, но я покажу, как шум добавляется автоматом. Для этого понадобится база изображений. И вот сюрпризик: нам не нужны метки, описания, потому что без понятия нам, что там на картинке вообще. Используй что угодно для обучения: свои личные фотки, порнографию, что душе угодно. Главное для качественного результата – чтобы база была большой и разнообразной с массой разных элементов: цвета, текстуры и так далее. Самой крутой базой, которую можно найти, будет ImageNet, в котором миллионы разных качественных фоток, более 100 ГБ. Но так как наши ресурсы ограничены и большинство из нас на этом форуме — фанаты таких вот стареньких ноутов, как thinkpads, и старых коней вроде меня. Мы просто возьмем Tiny ImageNet с подмножеством 60 000 изображений (64x64) для этой статьи. Это простой наборчик для экспериментов с глубоким обучением, по моему мнению, идеально для парней, у которых не так много вычислительных ресурсов, но которые хотят захватить мир с картой в кармане.
Начнем с создания класса, который будет управлять TinyImageNet на жестком диске. Напишем метод _download_and_extract, который выполнит простой HTTP GET запрос по ссылке, будем скачивать ответ блоками по 1024 байта, чтобы справиться с большим объемом загрузки. Простенький индикатор прогресса tqdm будет использоваться на протяжении всей статьи, так что если ты не в курсе, что такое tqdm, загляни в документацию. Все просто, это, по сути, обертка вокруг итераций в питоне. И, наконец, распакуем это все с помощью простого инструмента unzip (для Linux).
Python:
from pathlib import Path
from tqdm import tqdm
import requests
import os

DATA_DIR = Path("./data/tiny-imagenet-200")

class TinyImageNet:
    def __init__(self, data_dir: Path):
        self.data_dir = data_dir
        self.data_dir.mkdir(parents=True, exist_ok=True)
        self.TINY_IMGNET_URL = "http://cs231n.stanford.edu/tiny-imagenet-200.zip"

    def download(self) -> None:
        archive = self.data_dir / "tiny-imagenet-200.zip"
        if not (self.data_dir / "tiny-imagenet-200").exists():
            self._download_and_extract(archive)

    def _download_and_extract(self, archive: Path) -> None:
        with archive.open('wb') as file, requests.get(self.TINY_IMGNET_URL, stream=True) as response:
            total = int(response.headers.get('content-length', 0))
            for data in tqdm(response.iter_content(1024), total=total // 1024, unit='KB', desc="downloading"):
                file.write(data)

        print("extracting...")
        os.system(f"unzip -q {archive} -d {self.data_dir}")
        archive.unlink()

Давайте с этого и начнём. Сначала загрузим наши скачанные датасеты и разделим их на три части: обучающую, оценочную и тестовую. Как правило, если откроешь какие-то такие отстойные видосы на ютубе, то найдешь всего два набора: обучающий и тестовый. Ну это же нонсенс! Люди говорят, что глубокое обучение - это просто: написал модель, обучил её на каких-то данных и вуаля, магия в деле. На таких детских замечаниях я бы ответил: "Да ладно, попробуй сам-то". Тестовый набор данных мы вообще не будем трогать, и не будем обучать на оценочном и тестовом наборах. Тестовый набор - это только после того, как всё завершено, используем его чтобы проверить, как моделька себя покажет. Оценочный набор помогает нам отловить "косяки" (тут не про насекомых) нашей модели, узнать, учится ли она или просто запоминает обучающие данные. Разделение нашего всего набора TinyImageNet будет таким: (80%, 10%, 10%). Но прежде чем делить, мы добавим немного магии с помощью трансформаций, чтобы ещё улучшить наши данные. Все трансформации PyTorch можно найти в документации. Выберем самое важное для нашей задачи по стеганографии (пока мы просто создаем исходное изображение), пытаемся воссоздать исходное изображение из ряда свёрток. Но так как внедрение данных в исходные изображения влияет на яркость, насыщенность, оттенок и так далее, нам нужно выбрать трансформации, которые обеспечивают наибольшее разнообразие в конечном датасете. Вот что я выбрал:

Прочитай дальше, и узнаешь мой секретный рецепт выбора таких странных значений. Весь фокус в Стохастической Оптимизации. Смешно звучит, да? Но на самом деле это всего лишь метод научного тыка. Я постоянно корректировал значения, смотрел на изображения и выбирал лучшее.
ColorJitter: создаёт небольшие изменения в яркости, контрасте, насыщенности и оттенке. Очень важно для нашей модели стеганографии, потому что эти изменения могут нарушить внедрённый секрет. Исследования показали, что изменения в яркости влияют на человеческий глаз сильнее, чем цвет.
RandomGrayScale: как и говорится в названии, переводит RGB в оттенки серого. С стеганографией в оттенках серого всё не так просто, так что дал ему всего 5% шанса.
RandomHorizontalFlip & RandomVerticalFlip: если увидишь новичков, которые используют только эти трансформации в своих "ультрасовременных" моделях, можешь смело говорить, что это лохи. Изменение ориентации не портит изображение, но меняет отношения пикселей, что делает внедрение данных сложнее.
RandomRotation: поворот меняет положение пикселей, заставляя процесс внедрения и извлечения адаптироваться к изменяющимся пространственным отношениям. 12 градусов - это небольшой поворот без особых искажений.
RandomAdjustSharpness: изменяет остроту изображения. Обучение на разных уровнях резкости помогает нашей модели справляться с такими помехами. Выбрал коэффициент 1.3 - как раз, чтобы добавить остроту без потери качества.
RandomAutocontrast: немного черезчур для нашей маленькой модельки, но в сильной модели можно использовать для максимизации контраста изображения. Контрастные изменения могут нарушить процесс стеганографии, особенно если данные скрыты в изменениях интенсивности пикселей. Запомни эту строчку, потом к ней вернусь. Если забуду, пиши в комментах.
RandomSolarize: можем инвертировать пиксели выше определенного порога. Это нестандартное преобразование, которое может значительно изменить значения пикселей, особенно в светлых областях.
RandomEqualize: распределяет наиболее частые значения интенсивности. Выравнивание может сделать некоторые внедренные данные более видимыми. Модели надо учиться справляться с такими сценариями.
И, наконец, немного гауссовского шума: добавляем случайный шум к изображению. Шум может затруднить извлечение внедренных данных. Модель стеганографии должна быть устойчива к такому шуму. Я сделал интенсивность шума умеренной на 20%, чтобы не убить наш датасет.

Python:
import torch
from torchvision import datasets, transforms
from torch.utils.data import DataLoader, random_split
from typing import Tuple

def load_dataset(self) -> Tuple[torch.utils.data.Dataset, torch.utils.data.Dataset, torch.utils.data.Dataset]:
    transform = transforms.Compose([
        transforms.RandomApply([transforms.ColorJitter(brightness=0.15, contrast=0.15, saturation=0.15, hue=0.15)], p=0.3),
        transforms.RandomGrayscale(p=0.05),
        transforms.RandomHorizontalFlip(p=0.5),
        transforms.RandomVerticalFlip(p=0.25),
        # transforms.RandomRotation(12, fill=(0.5, 0.5, 0.5)),
        transforms.RandomApply([transforms.RandomAdjustSharpness(sharpness_factor=1.3)], p=0.15),
        transforms.RandomApply([transforms.RandomAutocontrast()], p=0.15),
        transforms.RandomApply([transforms.RandomSolarize(threshold=0.5)], p=0.05),
        transforms.RandomApply([transforms.RandomEqualize()], p=0.08),
        transforms.ToTensor(),
        transforms.RandomApply([transforms.Lambda(lambda x: x + 0.05 * torch.randn_like(x))], p=0.2),
        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
    ])
    full_dataset = datasets.ImageFolder(self.data_dir / "tiny-imagenet-200/train", transform=transform)
    train_len = int(0.8 * len(full_dataset))
    val_len = int(0.1 * len(full_dataset))
    test_len = len(full_dataset) - train_len - val_len

    return random_split(full_dataset, [train_len, val_len, test_len])

Теперь нам надо загрузить наши данные по-умному в загрузчики данных, которые сами будут бегать по нашему датасету пачками (в нашем коде размер пачки 64). Я использовал num_workers для количества рабочих потоков. А теперь для всех наших "олигархов" с графическими картами NVIDIA (с поддержкой CUDA) - можно кидать тензоры и операции из медленной оперативки прямиком на быструю память GPU. Само копирование данных стоит дорого, но когда все устроилось на GPU, тензоры будут рубиться параллельно на тысячах ядер CUDA. Одно из забавных фишек pytorch в том, что это можно сделать в одну строчку кода. Сначала мы проверим, доступна ли подходящая графическая карта с помощью torch.cuda.is_available(), а потом установим устройство на torch.device('cuda'), а если нет - 'cpu'. Когда работаем с CUDA, нам важно, чтобы наша линия загрузки данных была на высоте, так что используем pin_memory=True, чтобы ускорить перекидывание на GPU. Передача данных с оперативной памяти (RAM) на GPU будет быстрее, если она идет из закрепленной памяти.

Теперь давай проверим, что все приготовилось как надо, включая и сами преобразования. Это нам пригодится еще и для настройки преобразований. Пишем функцию-помощницу 'visualize_samples', чтобы показать случайные образцы из нашего датасета. Matplotlib работает только на CPU, так что надо вернуть данные обратно на процессор с помощью .cpu(). В разных фреймворках есть свои особенности, и в pytorch изображения обычно представлены в формате (C, H, W), где C - это количество каналов, например, для нашего датасета Tiny ImageNet это RGB, так что 3. H и W - это высота и ширина. А функция imshow от matplotlib ждет изображения в формате (H, W, C). Можно использовать permute(), чтобы поменять местами оси тензора pytorch с параметрами 1, 2, 0.

Если вглядеться в код, то можно найти знакомые, но странные числа - это обратное преобразование нормализации, которое мы применили ранее. Если мы хотим показать изображения в исходном виде, мы применяем обратную нормализацию, чтобы вернуть изображения из нормализованного диапазона обратно к исходному диапазону пикселей 0-255. Но когда мы делаем прямой проход, будь то обучение или оценка, мы не будем использовать обратную нормализацию, потому что наша модель создана для работы с нормализованными данными и ожидает входные данные в том же диапазоне, что и во время обучения. А иначе всякие сюрпризы могут поджидать.
Python:
import matplotlib.pyplot as plt
import numpy as np


def build_dataloaders(datasets, batch_size: int = 64):
    loader_args = {'batch_size': batch_size, 'num_workers': 4, 'pin_memory': True}
    return [DataLoader(dataset, shuffle=is_train, **loader_args) for is_train, dataset in
            zip([True, False, False], datasets)]

def visualize_samples(dataset, title, num_samples=5):
    fig, axes = plt.subplots(1, num_samples, figsize=(15, 3))
    for ax in axes:
        idx = np.random.randint(len(dataset))
        image, _ = dataset[idx]
        # CxHxW to HxWxC format for matplotlib imshow
        image = image.permute(1, 2, 0)
        # Reverse normalization
        mean = np.array([0.485, 0.456, 0.406])
        std = np.array([0.229, 0.224, 0.225])
        image = image.numpy() * std + mean
        image = np.clip(image, 0, 1)
        ax.imshow(image)
        ax.axis('off')
    plt.suptitle(title)
    plt.show()

image_net = TinyImageNet(DATA_DIR)
image_net.download()
train_data, val_data, test_data = image_net.load_dataset()
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
train_loader, val_loader, test_loader = build_dataloaders([train_data, val_data, test_data], batch_size=64)

for name, dataset, loader in [("Train", train_data, train_loader),
                              ("Validation", val_data, val_loader),
                              ("Test", test_data, test_loader)]:
    print(f"\n{name} Dataset:")
    print(f"Number of samples: {len(dataset)}")
    print(f"Shape of one data sample (C, H, W): {dataset[0][0].shape}")
    print(f"{name} loader length: {len(loader)}")
    if name in ["Train", "Validation"]:
        visualize_samples(dataset, f"{name} Samples")

Теперь пишем несколько сверточных и деконволюционных слоев подряд. Воспользуемся размером ядра в 3x3, потому что практика показала его эффективность. Как доказали такие фундаментальные архитектуры, как VGGNet, маленькие ядра могут создавать равнозначные рецептивные поля с меньшим количеством параметров. Несколько маленьких фильтров может имитировать эффект больших фильтров, но с меньшим вычислительным бременем. Я поставил шаг равным 1 на начальных слоях, чтобы сохранить пространственное разрешение изображения, что поможет вытаскивать более мелкие детали. Продвигаясь вглубь сети, используем шаг 2, чтобы уменьшить пространственные размеры, фактически уменьшая карты признаков, при этом увеличивая рецептивное поле. Не забудьте, что для 3x3 ядра отступ установлен равным 1, чтобы сохранить пространственные размеры.

После кодирования пространственные размеры уменьшились, и теперь нам нужно их восстановить на этапе декодирования, вот тут и пригодятся транспонированные свертки (деконволюции). Такой выбор стратегичен в сценариях, когда нам нужно обучить фильтры апсемплинга непосредственно на данных, а не использовать детерминированные методы типа билинейного или метода ближайших соседей. Изначально на этапе кодирования глубина канала увеличивается с 3 (RGB) до 32, затем до 64 и, наконец, до 128. Этот рост позволяет сети преобразовывать три основных входных канала в более абстрактное представление, возможно, выявляя сложные паттерны и иерархии в данных. Не забудьте в следующем разделе добавить визуализации для этого. Мы все знаем, как выглядит RGB, немного напоминающий теорию цвета Ньютона, но со зловредным зеленым вместо желтого. А что эти абстракции средних слоев представляют? Нужно дебажить, чтобы заглянуть в "голову" нашей модели и узнать, что за фокусы этот малый вытворяет на учебе или будет ли он употреблять "цифровые наркотики"! Напомни мне, если забуду проверить. Наш декодер, наоборот, идет по обратному пути, превращая эти глубокие абстракции обратно в формат из 3 каналов (неважно, какой именно, но это будет что-то вроде RGB из-за функции стоимости).

Мы используем простую активацию tanh между каждым слоем. Поверь на слово, tanh в некоторых старых генеративных моделях работает отлично, потому что центрирует данные вокруг 0 (форма S-кривой), что поможет нам при обратном распространении ошибки. Позже будем дебажить это, чтобы удостовериться, что все идет как надо. Если вы заметите, активация tanh не применяется на выходном слое. Это потому, что мы хотим, чтобы наша сеть могла генерировать RGB изображения со значениями во всем диапазоне пикселей. Если бы активация tanh осталась, значения пикселей были бы жестко усечены между -1 и 1, что может не подойти для некоторых задач.
Теперь мы создаем нашу первую модель. Для функции потерь используем среднеквадратичную ошибку и называем ее "criterion". В начале применим простой градиентный спуск, а потом можем переключиться на оптимизатор Adam. Так, всё готово для обучения! Но прежде чем начать тренировать, нужно проверить мощность нашей модельки. Уверен, многие, кто сталкивался с ChatGPT или другими LLM моделями, слышали про такие числа, как 175 миллиардов параметров, 67 миллиардов параметров. Это общее количество обучаемых параметров в нашей модельке (веса и смещения) - самый простой способ понимать мощность модели. Это не только влияет на время обучения, требования к памяти, но и может привести к переобучению или недообучению.

Но знаете что? Хочу рассказать один секретик: ключ к профессионализму в глубоком обучении - понимать, что мощность модели не равносильна интеллекту. Просто наслаивая слой за слоем и сидя с 999999999 NVIDIA A100 GPUs, триллионами параметров, не создашь сверхразума. Так что не ждите, что GPT-12 напишет вам вирус с нуля. Лучший пример - поговорите минут 10 с GPT-3.5 или GPT-4, и все станет ясно. С этим бредовым товарищем все понятно, пока он не начнет рассказывать анекдоты про Васю и медведя!
Python:
from torch import nn, optim

class ConvNet(nn.Module):
    def __init__(self):
        super(ConvNet, self).__init__()

        self.encoder = nn.Sequential(
            nn.Conv2d(3, 32, kernel_size=3, stride=1, padding=1),  # Output: 32 x H x W
            nn.Tanh(),
            nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1),  # Output: 64 x H/2 x W/2
            nn.Tanh(),
            nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1),  # Output: 128 x H/4 x W/4
            nn.Tanh()
        )

        self.decoder = nn.Sequential(
            nn.ConvTranspose2d(128, 64, kernel_size=3, stride=2, padding=1, output_padding=1),  # Output: 64 x H/2 x W/2
            nn.Tanh(),
            nn.ConvTranspose2d(64, 32, kernel_size=3, stride=2, padding=1, output_padding=1),  # Output: 32 x H x W
            nn.Tanh(),
            nn.Conv2d(32, 3, kernel_size=3, stride=1, padding=1),  # Output: 3 x H x W
        )

    def forward(self, x):
        x = self.encoder(x)
        x = self.decoder(x)
        return x

model = ConvNet()
criterion = nn.MSELoss()
SGDoptimizer = optim.SGD(model.parameters(), lr=0.001)

print(sum(p.numel() for p in model.parameters()))


Давайте налепим здесь, небольшую утилитку-тренеровочку, которая будет нашим боссом по обучению и валидации модельки. Сразу же, как только дверь в конструктор захлопывается, модель берется за ушко и пихается на устройство model.to(device), чтобы потом не пришлось носиться с лишними данными, как ведьма на метле (особенно чтобы это не мешало в процессе обучения, особенно на системах производственного уровня). Используем словарик 'dataloaders', чтобы в дальнейшем можно было легко расширять классик и добавлять разные типы данных, например, 'test'.

Теперь давайте наколдуем точку входа в наш класс, чтобы начать учить модель. Используем вложенный tqdm, чтобы в реальном времени наблюдать за потерями, скоростью итераций и прогрессом. Каждая полоска будет за эпоху, что по сути будет показывать, насколько мы продвинулись по итерациям в каждой эпохе. Теперь пишем маленькую функцию-секретику _run_epoch, которая будет крутиться в каждой эпохе, считать потери, делать backpropagation и обновлять весики. Помните, я мог бы засунуть все это в предыдущий метод обучения, но почти тот же процесс, но с небольшими изменениями нужен для валидации и тестирования. Единственное отличие обучения от тестов - нет необходимости следить за градиентами при тестировании, потому что нам не нужно делать backpropagation потерь, и мы не обновляем веса, мы просто смотрим, как себя ведет наша модель. Backpropagation и обновление весов - это как пельмени с мясом на праздник: вкусно, но дорого по ресурсам. Так что нет смысла следить за градиентами для необучающих задач. С pytorch мы просто переключаем модель в режимы model.train() или model.eval() с контекстами torch.enable_grad или no_grad. И помните, это не единственная причина. Еще одна причина - некоторые слои, например BatchNorm, которые мы тут не используем, ведут себя по-разному в зависимости от того, тренируем мы или тестируем.
Python:
class Trainer:
    def __init__(
        self,
        model: nn.Module,
        dataloaders: dict,
        criterion: nn.Module,
        optimizer: optim.Optimizer,
        device: str
    ):
        self.model = model.to(device)
        self.train_loader = dataloaders['train']
        self.val_loader = dataloaders['val']
        self.criterion = criterion
        self.optimizer = optimizer
        self.device = device

    def train(self, num_epochs: int):
        train_losses, val_losses = [], []
      
        for epoch in tqdm(range(num_epochs), desc="Epochs"):
            with tqdm(total=len(self.train_loader), desc="Training", position=0, leave=True) as tqdm_bar:
                train_loss = self._run_epoch(self.train_loader, True, tqdm_bar)
            val_loss = self._run_epoch(self.val_loader, False)

            train_losses.append(train_loss)
            val_losses.append(val_loss)

            self._log_epoch_progress(epoch, num_epochs, train_loss, val_loss)
            self._dynamic_plot(train_losses, val_losses)
            self._visualize_samples()
          
        return train_losses, val_losses

Потери модели после вывода считаются с помощью нашего критерия потерь (среднеквадратичная ошибка) против входных данных, потому что наша цель - как можно точнее воссоздать изображение. Отсюда и далее все будет только по тренировкам, как я уже говорил: мы чистим старые градиенты из предыдущего шага с помощью optimizer.zero_grad() чтобы не накапливались, это очень важно перед backpropagation. Следующий шаг - backpropagation потерь через все тензоры, которые привели к расчету потери в первую очередь. Используем backward() на потерях, потому что pytorch следит за всем. Затем веса обновляются на основе производных (градиентов) в зависимости от оптимизатора (здесь SGD со скоростью обучения 0.001) с помощью optimizer.step(). Все потери за эпоху накапливаются в epoch_loss.
Python:
def _run_epoch(self, dataloader: DataLoader, training: bool, tqdm_bar=None) -> float:
    if training:
        self.model.train()
        context = torch.enable_grad
    else:
        self.model.eval()
        context = torch.no_grad

    epoch_loss = 0.0
    with context():
        for inputs, _ in dataloader:
            inputs = inputs.to(self.device)
            outputs = self.model(inputs)
            loss = self.criterion(outputs, inputs)
          
            if training:
                self.optimizer.zero_grad()
                loss.backward()
                self.optimizer.step()
                tqdm_bar.set_postfix(loss=loss.item())
                tqdm_bar.update()

            epoch_loss += loss.item()
          
    return epoch_loss / len(dataloader)

Теперь закомментированные строки из метода 'train' надо раскомментировать после того, как напишем эти вспомогательные функции. _log_epoch_progress для логирования train_loss и val_loss после каждой эпохи, чтобы мы могли следить за тем, учится ли модель или сошла на нет. Потом напишем функцию для динамического отображения изменения потерь со временем. Затем нам нужно постоянно проверять, насколько хорошо наша модель работает на валидационном наборе (не финальный тестовый набор, к которому мы не будем прикасаться до самого конца). И помни, мы не обучаемся на валидационных данных и уж тем более не на тестовых (это было бы глупо), все похоже на вычисление потерь. Предыдущий метод, который запускает эпоху, будет вызван, и контекст модели будет установлен на eval, причины я уже объяснил раньше. И так как нам, людям, нужны визуальные доказательства, чтобы в что-то верить, мы напишем коротенький статический метод _plot_img. Только помни две вещи: сначала отключи тензор от его вычислительного графа, перенеси на CPU и изменяй порядок, чтобы он соответствовал ожидаемому формату ввода plt.imshow(). Я уже объяснил все это подробно раньше.
Python:
    def _log_epoch_progress(self, epoch: int, num_epochs: int, train_loss: float, val_loss: float):
        print(f"Epoch {epoch+1}/{num_epochs} - Training loss: {train_loss:.4f}, Validation loss: {val_loss:.4f}")

    def _dynamic_plot(self, train_losses: list, val_losses: list):
        plt.clf()
        plt.plot(train_losses, label='Training loss')
        plt.plot(val_losses, label='Validation loss', linestyle='dashed')
        plt.legend()
        plt.xlabel('Epochs')
        plt.pause(0.01)

    def _visualize_samples(self):
        inputs, _ = next(iter(self.val_loader))
        outputs = self.model(inputs.to(self.device))
      
        plt.figure(figsize=(6, 2))
        for idx in range(3):
            self._plot_img(inputs[idx], title='Input', position=idx+1)
            self._plot_img(outputs[idx], title='Output', position=idx+4)
        plt.tight_layout()
        plt.show()

    @staticmethod
    def _plot_img(img_tensor: torch.Tensor, title: str, position: int):
        plt.subplot(2, 3, position)
        plt.imshow(img_tensor.cpu().detach().permute(1, 2, 0))
        plt.title(title)
        plt.axis('off')

dataloaders = {"train": train_loader, "val": val_loader}
trainer = Trainer(model, dataloaders, criterion, SGDoptimizer, device)
train_losses, val_losses = trainer.train(num_epochs=10)

Epoch 1:
one.png


Epoch vs Loss:
plot.png

Epoch 10:
10.png

ONNX Runtime
Итак, допустим, нам захотелось использовать нашу модель в программе на C/C++ для инференса. Используем формат ONNX (Open Neural Network Exchange). Это, по сути, общее представление и операции над моделями для разных фреймворков. Метод torch.onnx.export позволяет сохранить модель PyTorch в формате ONNX. Для процесса экспорта нам нужен образец входных данных, чтобы определить формы и другие детали. Этот ввод должен иметь такую же форму, как и стандартные вводы нашей модели. Но стоп! Здесь косяк, правда? Нам нужны динамические формы ввода во время инференса. Ведь в чём смысл, если мы можем провести стеганографию только на изображении фиксированного размера? Это просто ужас. Из-за этого нам нужно использовать динамические оси в функции экспорта, таким образом определенные размеры наших входных тензоров будут динамически регулироваться. У нас это высота и ширина. Но вам ещё предстоит узнать об этом. Нам все равно нужен фиктивный входной тензор (знаю по себе, как я бился с этим вопросом, когда начал работать с ONNX) для трассировки. Этот тензор не задает фиксированный размер, он служит примером для нашей функции экспорта. Да, это правда даже если вы добавили параметр dynamic_axes, все равно требуется входной тензор для процесса трассировки. Это не зафиксирует размер ввода нашей модели, это просто способ для ONNX понять модель во время экспорта.

Для инференса на нашей модели ONNX мы будем использовать ONNX Runtime. У ONNX Runtime нет официального PPA или apt репозитория для прямой установки. Можно собрать его из исходного кода, но мы такими героическими поступками заниматься не будем. Лично я просто скопировал заголовки и библиотеки из скачивания в папку проекта и экспортировал библиотеку в переменную. Это было плохо. Я все почистил и теперь сплю спокойно. Больше никогда не буду работать с ONNX на моем ноутбуке. Интересно, ONNX действительно хорош при сборке из исходников или у них просто нет людей, которые умеют собирать под Windows? В общем, вам нужно правильно настроить зависимости для нашего кода на C++. Если что-то непонятно или у кого-то есть крутые трюки - пишите в комментариях, обсудим лучшие методы работы с ONNX Runtime.

Ключ к хорошему инференсу - подготовка данных. OpenCV - это круть в операциях обработки изображений. Сначала определим несколько обычных констант для размеров нашего изображения и каналов в IMAGE_SIEZ и NUM_CHANNELS. Создадим простую функцию LoadAndPreprocess, чтобы подготовить изображение для инференса. Сначала загрузим изображение с помощью cv::imread в цветном формате, затем изменяем размер согласно нашим требованиям с помощью cv::resize. Но есть одна тонкость: по умолчанию OpenCV использует порядок каналов BGR, а стандарт глубокого обучения - RGB. Так что преобразуем его с помощью cv::cvtColor. Мы напишем маленькую функциючку для конвертации 2D картинки в 1D вектор, беря наш cv::Mat и выдавая ImageData (что есть вектор с типом float). Помни, мы делаем эту сериализацию рядок за рядком, канал за каналом. А потом можем лепить из этого 1D вектора N-D ONNX тензор, создав ONNX тензор в функции Run внутри класса OnnxModel. Это использует тот самый расплющенный img_data, но на деле дает ему 4D форму, которую мы определили с input_shape, когда на самом деле создавали тензор (batch size, channels, height, width) и опять же вернулись к стандартам глубокого обучения.
C++:
#include <iostream>
#include <vector>
#include <string>
#include "include/cpu_provider_factory.h"
#include "include/onnxruntime_cxx_api.h"
#include <opencv2/opencv.hpp>

namespace ModelInference {

constexpr int IMAGE_SIZE = 64;
constexpr int NUM_CHANNELS = 3;

using ImageData = std::vector<float>;

class ImageProcessor {
public:
    static cv::Mat LoadAndPreprocess(const std::string& imagePath) {
        cv::Mat img = cv::imread(imagePath, cv::IMREAD_COLOR);
        if (img.empty()) {
            throw std::runtime_error("img not found/unsupport format");
        }
      
        cv::resize(img, img, {IMAGE_SIZE, IMAGE_SIZE});
        cv::cvtColor(img, img, cv::COLOR_BGR2RGB);
        return img;
    }

    static ImageData ConvertToTensorData(const cv::Mat& img) {
        ImageData img_data(NUM_CHANNELS * IMAGE_SIZE * IMAGE_SIZE);
        for (int i = 0; i < img.rows; ++i) {
            for (int j = 0; j < img.cols; ++j) {
                cv::Vec3b pixel = img.at<cv::Vec3b>(i, j);
                for (int c = 0; c < NUM_CHANNELS; ++c) {
                    img_data[c * IMAGE_SIZE * IMAGE_SIZE + i * IMAGE_SIZE + j] = static_cast<float>(pixel[c]);
                }
            }
        }
        return img_data;
    }

    static cv::Mat ConvertFromTensorData(float* tensorData) {
        cv::Mat output_img(IMAGE_SIZE, IMAGE_SIZE, CV_8UC3);
        for (int i = 0; i < IMAGE_SIZE; ++i) {
            for (int j = 0; j < IMAGE_SIZE; ++j) {
                for (int c = 0; c < NUM_CHANNELS; ++c) {
                    output_img.at<cv::Vec3b>(i, j)[c] = static_cast<uchar>(tensorData[c * IMAGE_SIZE * IMAGE_SIZE + i * IMAGE_SIZE + j]);
                }
            }
        }
        return output_img;
    }
};

В классе OnnxModel у нас есть экземпляры Ort::Env и Ort::Session. Они тут как среда выполнения и сессия вывода. Создаем такой забавный приватный методчик для задания опций сессии. Устанавливаем количество потоков в 1. Вообще, ONNX Runtime мне кажется сложнее, чем pytorch и tensorflow. Я старался от него держаться подальше, потому что мне он на фиг не нужен - все, что я делаю в работе, я делаю с PyTorch. Есть отдельные инженеры, которые занимаются такой интеграцией ONNX Runtime. Но насколько я в этом разбираюсь, вот что могу сказать. ONNX Runtime предлагает кучу запутанных настроек и оптимизаций для вывода (а про обучение даже не говорю, это целый океан). Эти настройки упакованы в объект Ort::SessionOptions. В нашем методе устанавливаем intra operation thread count в 1, это значит, что операции внутри графа ONNX будут использовать один поток, но можно настроить и по-другому. Еще знаю методчик 'SetGraphOptimizationLevel', который включает дополнительные оптимизации с какой-то магией, до которой мне далеко.

В двух словах, для тех, кто запутался в этом C++: класс 'OnnxModel' - это мостик между нашими функциями обработки изображений и ONNX Runtime. При инициализации настраиваем 'env', загружаем модель через 'session'. Вывод будет выполняться методом 'Run', загружаем один раз. И результатик будет ждать вас в Ort::Value.

Если хочешь встроить модель прямиком в бинарник, можешь конвертировать ее с помощью xxd в Сишный массив и прикрутить к бинарнику. Я лично не пробовал, но так-то это путь. Можешь даже попробовать сжать его простеньким zlib, встроить через xxd и распаковать на лету.

C++:
class OnnxModel {
    Ort::Env env;
    Ort::Session session;

    static Ort::SessionOptions CreateSessionOptions() {
        Ort::SessionOptions session_options;
        session_options.SetIntraOpNumThreads(1);
        session_options.SetGraphOptimizationLevel(ORT_ENABLE_EXTENDED);
        return session_options;
    }

public:
    OnnxModel(const std::string& model_path)
        : env(ORT_LOGGING_LEVEL_WARNING, "ModelInference"),
          session(env, model_path.c_str(), CreateSessionOptions()) { }

    Ort::Value Run(const cv::Mat& img) {
        auto img_data = ImageProcessor::ConvertToTensorData(img);
        std::vector<int64_t> input_shape = {1, NUM_CHANNELS, IMAGE_SIZE, IMAGE_SIZE};
        Ort::MemoryInfo memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
        Ort::Value input_tensor = Ort::Value::CreateTensor<float>(memory_info, img_data.data(), img_data.size(), input_shape.data(), 4);

        const char* input_names[] = {"input"};
        const char* output_names[] = {"output"};
        auto outputs = session.Run(Ort::RunOptions{nullptr}, input_names, &input_tensor, 1, output_names, 1);
        return std::move(outputs.front());
    }
};

} // namespace ModelInference

int main() {
    try {
        ModelInference::OnnxModel model("model.onnx");
        auto img = ModelInference::ImageProcessor::LoadAndPreprocess("test.jpeg");
        auto output = model.Run(img);
        auto output_img = ModelInference::ImageProcessor::ConvertFromTensorData(output.GetTensorMutableData<float>());
        cv::imwrite("final_output.jpeg", output_img);
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
        return 1;
    }
    return 0;
}

Что произойдет, если секретная картинка и картинка-обложка займутся любовью? Рождается новая картинка-обложка, а секретная картинка - как паразит внутри!
Так вот, мы уже видели, что нейросеть может через ряд свёрточных операций выдать ту же самую картинку. А давайте представим себе, если бы мы обучили модель принимать обложку и секретную картинку, а потом заставили ее выдать такую же обложку, но с маленьким секретом внутри? Это замечательная идея автоэнкодеров. Но тут у нас появляется куча вопросов: как сохранить секрет? Какая будет функция потерь? Хитрая фишка здесь - использовать картинки против самих себя. Возвращаясь к нашей модели, создадим две сети: первая будет принимать обложку и секретную картинку и выдавать похожую обложку, допустим, X. Потом этот X пихаем во вторую сеть, чтобы получить обратно секретную картинку. Но это будет работать только если в X где-то затаился наш секретик. В нейросетях, друзья мои, если хочешь, чтобы твоя сеть была послушной, нужно правильно определить функцию потерь. У нас их будет две: L1 для первой сети по сравнению с обложкой и L2 для обеих сетей по сравнению с секретной картинкой.

Достаточно болтовни, давайте к делу. Сначала создадим первую сеть (энкодер), похожую на первую, но с небольшими ухищрениями. Начнем просто - добавим немного "шума" к нашей картинке-обложке. Этот "шум" и будет нашей секретной картинкой. А может быть, использовать одну и ту же картинку для всех данных? Ну это ж как русская рулетка, может обернуться переобучением. Поэтому лучше просто перемешать наши картинки-обложки и использовать их как секретные. Если вы все-таки решите использовать одну картинку, не забудьте добавить размер партии: {batch_size} + (3, 64, 64). Возможно, вы думаете о других размерах картинок. Это классический вопрос стеганографии: если размер секретной картинки меньше, чем у обложки, можно добавить заполнение по краям - дело двух строчек кода. И помните: так как секретные картинки созданы из входных картинок-обложек на GPU, их производные автоматически будут на этом же устройстве. Протестируем, можем ли мы получить обратно картинку-обложку, учитывая, что нейросеть считает секретную картинку "шумом". В обычных нейросетях данные идут последовательно, но здесь на первом слое conv1 мы передаем только секретные картинки и применяем N фильтров. Затем используем torch.cat, чтобы объединить преобразованные фильтры секретных картинок (batch_size, N, 64, 64) с картинками-обложками (batch_size, 3, 64, 64) по измерению 1. В общем, все просто: нам нужно (N+3) изображений размером 64x64 пикселя. Если что не ясно - добро пожаловать в комментарии!

Извлечение признаков без уменьшения разрешения
Сначала мы вытащим наиболее важные черты нашего секретного изображения. Не путайте это с входными данными (изображениями-обложками). Если раньше мы использовали свёртки для изображений-обложек, то теперь – только для секретных изображений. И постепенно увеличиваем глубину канала с 3 до 32, затем до 64, а потом до 128. Это гарантирует богатое иерархически структурированное представление. А теперь внимание, братцы! Мы будем использовать три сверточных слоя для извлечения признаков, при этом сохраняя пространственные размеры за счет шага в 1 и подходящего пэддинга. Наша цель не столько распознавание образов, сколько извлечение признаков и их трансформация для последующего смешивания (объединения особенностей обложки и секретных изображений). Сохраняя пространственные размеры (высоту и ширину), мы гарантируем, что локализованная информация из секретных изображений остается неизменной, что позволяет более точно смешивать признаки с изображениями обложек. Первые слои, как правило, фиксируют базовые черты, такие как края, текстуры, в то время как глубокие слои улавливают более сложные абстракции. Об этом мелком фишечном делении в модели поговорим в следующих разделах. Увеличивая глубину, сеть получает больше возможностей для кодирования различных атрибутов секретных изображений, которые в дальнейшем можно использовать при слиянии.

Как я и говорил раньше, признаки теперь объединяются с помощью команды torch.cat([tensors], dim=1) вдоль dimension=1, что соответствует каналу. В итоге получается 131 канал. А теперь – к нашему декодеру, который генерирует встроенное изображение-обложку снова (не декодер, извлекающий секретное изображение). У нас нет уменьшения разрешения в фазах извлечения признаков, поэтому мы не будем использовать транспонированные свертки. Вместо этого построим серию сверток, которые уточняют комбинированные признаки для создания нашего окончательного изображения. Постепенное сокращение глубины канала (с 131 до 128, затем до 64 и до 3) помогает агрегировать широкое пространство признаков в более компактное и значимое представление, подходящее для воссоздания нашего RGB изображения.
Python:
class ConvNet(nn.Module):
    def __init__(self):
        super(ConvNet, self).__init__()

        self.feature_extractor = nn.Sequential(
            nn.Conv2d(3, 32, kernel_size=3, stride=1, padding=1),
            nn.Tanh(),
            nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1),
            nn.Tanh(),
            nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
            nn.Tanh()
        )

        self.decoder = nn.Sequential(
            nn.Conv2d(131, 128, kernel_size=3, stride=1, padding=1),  # 131 = 3 (inputs) + 128 (feature_extractor)
            nn.Tanh(),
            nn.Conv2d(128, 64, kernel_size=3, stride=1, padding=1),
            nn.Tanh(),
            nn.Conv2d(64, 3, kernel_size=3, stride=1, padding=1)
        )

    def forward(self, inputs, inputs2):
        features = self.feature_extractor(inputs2)
      
        combined = torch.cat((inputs, features), dim=1)
      
        output = self.decoder(combined)
        return output

Python:
def _run_epoch(self, dataloader: DataLoader, training: bool, tqdm_bar=None) -> float:
    if training:
        self.model.train()
        context = torch.enable_grad
    else:
        self.model.eval()
        context = torch.no_grad

    epoch_loss = 0.0
    with context():
        for inputs, _ in dataloader:
            inputs = inputs.to(self.device)
            random_indices = torch.randperm(inputs.shape[0])
            secret_images = inputs[random_indices]
            outputs = self.model(inputs, secret_images)
            loss = self.criterion(outputs, inputs)
          
            if training:
                self.optimizer.zero_grad()
                loss.backward()
                self.optimizer.step()
                tqdm_bar.set_postfix(loss=loss.item())
                tqdm_bar.update()

            epoch_loss += loss.item()
          
    return epoch_loss / len(dataloader)

def _visualize_samples(self):
    inputs, _ = next(iter(self.val_loader))
    inputs = inputs.to(self.device)
    random_indices = torch.randperm(inputs.shape[0])
    secret_images = inputs[random_indices]
    outputs = self.model(inputs, secret_images)
  
    plt.figure(figsize=(6, 2))
    for idx in range(3):
        self._plot_img(inputs[idx], title='Input', position=idx+1)
        self._plot_img(outputs[idx], title='Output', position=idx+4)
    plt.tight_layout()
    plt.show()

Epoch 1:
x.png

Epoch vs Loss:
z.png

Epoch 10:
y.png




Продолжение статьи...

(c) RodionRaskolnikov
для https://xss.pro
 
top work--> finally a deservedly article on topic of working with neural networks
Мы не будем акцентировать внимание на шифровании данных. В первой статье вы, возможно, заметили, что DildoFagins говорил, что шифрование не влияет на алгоритм LSB. Вы можете подумать: "Ну, добавим простой слой шифрования и дешифрования AES-256, какая разница?" Это верно для LSB, но для нейросетей это намного сложнее. Обсудим, возможно ли это в принципе, когда это возможно, потому что если это возможно, то мы как бы подрываем стеганографию нейросети (это ваша подсказка, если вы поняли, о чем я, оставьте свой комментарий после прочтения статьи). Возможно, в другой статье расскажем, как вставить криптографию в нашу стеганографическую нейросеть, если товарищи заинтересованы (это вообще возможно? если да, то насколько сложно?). Теперь можем глупо разделить процесс на две части: генерацию самого основного изображения с шумом + кодирование наших данных.
i am confusing --> this is not hashing + there is no such information loss, collisions --> he cannot even understand meaning of chair or garbageman --> in essence why does he not perform the dance --> if he squeezes such ciphertext correctly?
 
а как вышло

если текст статьи - явный перевод с английского?
По теме - чисто технические средства часто бессмысленны, особенно, в нашей сфере. Но зато часто мульты палятся по прочим факторам, например.
Не обязательно на английском, но да, здесь работают переводчик(и).
 
Не обязательно на английском, но да, здесь работают переводчик(и).
what is your native language?
 
which is your native language?
Сказать нельзя, но да, это сначала было написано на английском. Несколько переводчиков и несколько специальных слоев между, чтобы обойти анализ авторской принадлежности.
 
Продолжение статьи

Теперь я привёл в порядок плохой код. Сеть, которую мы сейчас имеем и которая генерирует встроенное изображение обложки, будем называть 'AliceNet'. Позже мы напишем 'BobNet' (принимает встроенное изображение обложки и извлекает секретное изображение). Архитектура остаётся той же. Нам нужно подготовить класс обучения для одновременного тренировки обеих сетей в одном pipeline. Так что модель теперь принимает список с моделями (nn.Module) и также оптимизатор, потому что, как я сказал, функций потерь будет несколько. Не путайте 'criterion' (функция потерь) — это математическая функция, например MSE, с оптимизатором, который обновляет веса. Так что будет два оптимизатора в списке. Давайте пока оставим alice_optimizer (предыдущий оптимизатор) с learning_rate=0.001. В общем, скорость обучения должна меняться по мере прохождения итераций и эпох, но поскольку мы будем менять нашу архитектуру, добавляя BobNet, это сделает нашу полную модель очень хрупкой. Поэтому первым делом мы наблюдаем, а затем переходим к оптимизатору Adam или оставляем градиентный спуск.

Для нашего 'BobNet', который использует встроенное изображение обложки, сразу напишем самую простую возможную архитектуру: один слой Conv2d с ядрами 3x3 и out_channels = 3. Это нам понадобится для тестирования, расчёта функции потерь, обратного распространения ошибки и обновления весов. Это также докажет, что наша идея работает. Критерий для 'BobNet' будет аналогичным MSE и будет сохранён в 'alice_bob_loss', так как он применяется ко всей сети (не только к 'Bob').

Теперь о главном изменении. Сначала мы обычно обнуляем градиент на alice_optimizer, затем вычисляем обратное распространение через alice_loss (встроенные изображения обложек против оригинальных изображений обложек). Здесь небольшой трюк: нужно установить retain_graph=True. Это критически важно, так как граф вычислений, построенный во время прямого прохода Alice, используется заново при последующем обратном проходе. После второго обратного прохода граф освобождается. Этот порядок может немного снизить нагрузку на память. Затем выполняем alice_optimizer.grad(), обнуляем градиенты на alice_bob_optimizer, распространяем обратно ошибку alice_bob_loss, делаем шаг. Такой порядок может улучшить стабильность обучения. Когда процесс Alice начинает оптимизироваться, механизм встраивания может укрепиться быстрее. К тому времени, когда комбинированный alice_bob_loss начнёт играть более важную роль в обучении, Alice, возможно, уже достигнет стадии, на которой будет создавать достаточно правдоподобные изображения обложек. Это может уменьшить конфликтность двух задач, сделав сходимость более гладкой.


Python:
class AliceNet(nn.Module):
    def __init__(self):
        super(AliceNet, self).__init__()
        self.feature_extractor = nn.Sequential(
            nn.Conv2d(3, 32, kernel_size=3, stride=1, padding=1),
            nn.Tanh(),
            nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1),
            nn.Tanh(),
            nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
            nn.Tanh()
        )
        self.decoder = nn.Sequential(
            nn.Conv2d(131, 128, kernel_size=3, stride=1, padding=1),  # 131 = 3 (inputs) + 128 (feature_extractor)
            nn.Tanh(),
            nn.Conv2d(128, 64, kernel_size=3, stride=1, padding=1),
            nn.Tanh(),
            nn.Conv2d(64, 3, kernel_size=3, stride=1, padding=1),
            nn.Tanh()
        )

    def forward(self, inputs, inputs2):
        features = self.feature_extractor(inputs2)
        combined = torch.cat((inputs, features), dim=1)
        output = self.decoder(combined)
        return output

class BobNet(nn.Module):
    def __init__(self):
        super(BobNet, self).__init__()
        self.secret_decoder = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1),
            nn.Tanh(),
            nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
            nn.Tanh(),
            nn.Conv2d(128, 3, kernel_size=3, stride=1, padding=1)
        )
    def forward(self, x):
        output = self.secret_decoder(x)
        return output


Python:
class Trainer:
    def __init__(
        self,
        models: List[nn.Module],
        dataloaders: dict,
        criterion: nn.Module,
        optimizers: List[optim.Optimizer],
        device: str
    ):
        self.Alice = models[0].to(device)
        self.Bob = models[1].to(device) if len(models) >= 2 else None
        self.train_loader = dataloaders['train']
        self.val_loader = dataloaders['val']
        self.criterion = criterion.to(device)
        self.alice_optimizer = optimizers[0]
        self.alice_bob_optimizer = optimizers[1] if len(optimizers) >= 2 else None
        self.device = device

        self.alice_train_losses : List = []
        self.alice_bob_train_losses : List = []

        self.alice_val_loss : List = []
        self.alice_bob_val_loss : List = []
      
        self.epochs = 0

    def train(self, num_epochs: int):
        print("Current Model:")
        self._visualize_samples()

        for epoch in tqdm(range(num_epochs), desc="Epochs"):
            with tqdm(total=len(self.train_loader), desc="Training", position=0, leave=True) as tqdm_bar:
                self._run_epoch(self.train_loader, True, tqdm_bar)

                fig, axs = plt.subplots(1, 2, figsize=(15, 5))

                axs[0].plot(self.alice_train_losses, label='alice loss')
                if self.Bob:
                    axs[0].plot(self.alice_bob_train_losses, label='alice bob loss', color="orange")
                axs[0].set_xlabel('Iterations')
                axs[0].legend()
                axs[0].set_title("Training Losses")
              
                self._run_epoch(self.val_loader, False)
              
                axs[1].plot(self.alice_val_loss, label='alice loss')
                if self.Bob:
                  axs[1].plot(self.alice_bob_val_loss, label='alice bob loss', color="orange")
                axs[1].set_xlabel('Iterations')
                axs[1].legend()
                axs[1].set_title("Validation Loss")

                plt.tight_layout()
                plt.show()

                self._visualize_samples()

    def _run_epoch(self, dataloader: DataLoader, training: bool, tqdm_bar=None) -> float:
        if training:
            self.Alice.train()
            self.Bob.train() if self.Bob else None
            context = torch.enable_grad
        else:
            self.Alice.eval()
            self.Bob.eval() if self.Bob else None
            context = torch.no_grad

        with context():
            for inputs, _ in dataloader:
                cover_images = inputs.to(self.device)
                random_indices = torch.randperm(cover_images.shape[0])
                secret_images = cover_images[random_indices]
                embedded_cover_images = self.Alice(cover_images, secret_images)
                alice_loss = self.criterion(embedded_cover_images, cover_images)

                if self.Bob:
                    extracted_secret_images = self.Bob(embedded_cover_images)
                    alice_bob_loss = self.criterion(extracted_secret_images, secret_images)
                  
                if training:
                    self.alice_optimizer.zero_grad()
                    alice_loss.backward(retain_graph=True)
                    self.alice_optimizer.step()
                    self.alice_train_losses.append(alice_loss.item())
                    tqdm_bar.set_postfix(alice_loss=alice_loss.item())
              
                    if self.Bob:
                        self.alice_bob_optimizer.zero_grad()
                        alice_bob_loss.backward()
                        self.alice_bob_optimizer.step()
                        self.alice_bob_train_losses.append(alice_bob_loss.item())
                        tqdm_bar.set_postfix(alice_bob_loss=alice_bob_loss.item())
                    tqdm_bar.update()
                else:
                    self.alice_val_loss.append(alice_loss.item())
                    if self.Bob:
                      self.alice_bob_val_loss.append(alice_bob_loss.item())

        self.epochs += 1
          

    def _visualize_samples(self):
        inputs, _ = next(iter(self.val_loader))
        cover_images = inputs.to(self.device)
        random_indices = torch.randperm(cover_images.shape[0])
        secret_images = cover_images[random_indices]
        embedded_cover_images = self.Alice(cover_images, secret_images)

        if self.Bob:
            extracted_secret_images = self.Bob(embedded_cover_images)
      
        num_samples = 3
        plt.figure(figsize=(18, 12))
      
        for idx in range(num_samples):
            position = idx * 4 + 1
          
            self._plot_img(cover_images[idx], title='Cover', position=position)
            self._plot_img(secret_images[idx], title='Secret', position=position + 1)
            self._plot_img(embedded_cover_images[idx], title='Embedded Cover', position=position + 2)
            if self.Bob:
                self._plot_img(extracted_secret_images[idx], title='Extracted Secret', position=position + 3)
      
        plt.tight_layout()
        plt.show()

    @staticmethod
    def _plot_img(img_tensor: torch.Tensor, title: str, position: int):
        plt.subplot(3, 4, position)
        img_tensor_ = img_tensor.cpu().detach().permute(1, 2, 0)
        mean = np.array([0.485, 0.456, 0.406])
        std = np.array([0.229, 0.224, 0.225])
        img_tensor_ = img_tensor_.numpy() * std + mean
        img_tensor_ = np.clip(img_tensor_, 0, 1)
        plt.imshow(img_tensor_)
        plt.title(title)
        plt.axis('off')


alice = AliceNet()
bob = BobNet()
criterion = nn.MSELoss()
alice_optimizer = optim.SGD(alice.parameters(), lr=0.001)
alice_bob_optimizer = optim.SGD(list(bob.parameters()) + list(alice.parameters()), lr=0.001)
dataloaders = {"train": train_loader, "val": val_loader}

print(sum(p.numel() for p in alice.parameters()))
print(sum(p.numel() for p in bob.parameters()))

trainer = Trainer([alice, bob], dataloaders, criterion, [alice_optimizer, alice_bob_optimizer], device)
trainer.train(num_epochs=10)

loss.png


EPOCH (0/16)
0.png



EPOCH (1/16)
1 (1).png



EPOCH (2/16)
2 (1).png

2_.png



EPOCH (3/16)
3 (1).png



EPOCH (4/16)
4.png



EPOCH (5/16)
5.png



EPOCH (6/16)
6.png

6_.png



EPOCH (7/16)
7.png



EPOCH (8/16)
8.png



EPOCH (9/16)
9.png



EPOCH (10/16)
10 (1).png

10_.png



EPOCH (11/16)
11.png



EPOCH (12/16)
12.png



EPOCH (13/16)
13.png



EPOCH (14/16)
14.png

14_.png



EPOCH (15/16)
15.png



EPOCH (16/16)
16.png



Я забыл установить seed для генератора случайных чисел torch, и модель не была сохранена. Среда выполнения Jupyter Notebook была, конечно же, закрыта ещё несколько недель назад. Но каждая эпоха для этой архитектуры занимала примерно 5 минут, что чуть больше, чем 4 итерации в секунду. Я обучал его в течение 16 эпох (40 минут). Seed важен в глубоком обучении, потому что воспроизведение результатов имеет значение. Если молодой товарищ, только начинающий своё знакомство с этой областью, воспроизведет и даже превзойдет текущие результаты, я даю слово показать вам мои методы улучшения сети. Нет смысла постоянно публиковать статьи, если в этом нет интереса. Я предлагаю всем использовать seed, сохранять свои модели pytorch[.]org[/]tutorials[/]beginner[/]saving_loading_models, использовать jupyter notebook, делиться для обучения и воспроизведения результатов.Конечно, результаты далеки от идеала, но концепция доказана и работает. И все дальнейшие шаги сводятся к корректировке и модификации нейронной архитектуры (добавление слоев, различные функции активации (и здесь гораздо больше возможностей, чем просто добавление слоев, о чем мы можем говорить, если кто-то заинтересован). Если бы простое увеличение числа слоев и разные функции активации магическим образом улучшали модель, у нас уже был бы GPT-5, а GPT-4 выглядел бы перед ним как нелепый дилетант)) и самого процесса обучения. Основная тема этой статьи - вложенный автокодировщик. Изменение основной архитектуры будет отдельным проектом, так что, пожалуйста, создайте для этого отдельную ветку обсуждения.

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

(c) RodionRaskolnikov
для XSS
 
Невероятно много буков и усилий, для неочевидного результата. Сколько своих байт в итоге можно спрятать на мегабайт изображений? Есть ли вообще какие-то преимущества перед традиционной стеганографией?
 
Сколько своих байт в итоге можно спрятать на мегабайт изображений?
действительно. работа произведена большая, текста написано много, а самое важное не указано.
 
Невероятно много буков и усилий, для неочевидного результата. Сколько своих байт в итоге можно спрятать на мегабайт изображений? Есть ли вообще какие-то преимущества перед традиционной стеганографией?
действительно. работа произведена большая, текста написано много, а самое важное не указано.
Справедливо замечено, я уже отправил модель в школу.
Training: 22%|██▏ | 276/1250 [03:27<12:07, 1.34it/s, alice_loss=1.16]
 
Справедливо замечено, я уже отправил модель в школу.
Training: 22%|██▏ | 276/1250 [03:27<12:07, 1.34it/s, alice_loss=1.16]
А это ты тут якобы какую-то "хакирскую" прогу публиковал, которой тоже лайков наставили, а когда я её прочитал - оказалось, что это просто пустышка была, куча пустых функций вызывали друг друга и ничего не делали? Тот тоже новорег был, только не такой наглый - после того как спалился всё постирал и исчез.
 
А это ты тут якобы какую-то "хакирскую" прогу публиковал, которой тоже лайков наставили, а когда я её прочитал - оказалось, что это просто пустышка была, куча пустых функций вызывали друг друга и ничего не делали? Тот тоже новорег был, только не такой наглый - после того как спалился всё постирал и исчез.
Я не согласен с тем, как вы говорите. Похоже, вы не читали статью. Если у вас есть сомнения, выразите свою критику корректнее или, если вы прочитали и не поняли, задайте вопрос. Как масштаб результатов может оценить методику из статьи? Это напомнило мне о Анри Беккереле (не сравниваю себя с этой легендой). Случайно обнаружив, что соединение урана может развивать фотопластинку даже без света, он открыл радиоактивность. Это привело к изучению квантовой механики и атомной структуры. Современная ядерная физика базируется на этом.
Вы сомневаетесь в способности архитектуры обрабатывать сложность для лучших обложек? Она способна. Прочтите статью прежде чем писать.
Возьмите свой A100, обучите его в течение 1000 эпох и покажите результаты всем. Добавьте больше слоев, возможно, batchnorm, если сможете с этим справиться, измените на ReLU, если сможете управлять большими градиентными потоками, исчезающими или взрывающимися градиентами. С уважением, прежде чем порочить моё имя, стоит разобраться в технике, которая в основе состоит из двух вещей: архитектуры и интересного способа обновления весов. Это отличная статья для новичков на мой взгляд.
О вашем вопросе о преимуществах перед традиционной стеганографией. Я все еще не понимаю, это вопрос или критика?
Основное ограничение этой техники, помимо архитектуры (с архитектурой все в порядке, как и с GRU для перевода в эпоху "внимания"), это что она позволяет достичь стеганографии. Главная подсказка в том, как вы её применяете.
Причина - это функция потерь, которую мы используем и которую использовали наши предки - перекрестная энтропия, которая преобразует модель обратно в LSB, но с преимуществом из-за способа обучения и инициализации весов. Если хотите улучшить модель, прежде всего измените функцию потерь. С архитектурой все в порядке.
 
Похоже, вы не читали статью.
Расскажи анекдот про Васю и медведя?

если вы прочитали и не поняли, задайте вопрос.
Я в целом понял и вопрос тоже задал.

Основное ограничение этой техники, помимо архитектуры (с архитектурой все в порядке, как и с GRU для перевода в эпоху "внимания"), это что она позволяет достичь стеганографии.
<балашиха.mp4>
 
Так... Какая то херня. Статья конечно збс, но в плане тематики вообще не о чем. Легче было бы рассказать о рате который юзает в качестве админки гугл календарь. Но статья максимально беззубая, не чувствуется драйва, может от текста, потому что язык максимально коверканный. Не раскрыта тема как взломать мамку, без сисек статью не принимаем.
 
Если вы взглянете на функцию потерь кросс-энтропии, обучение приближается к имитации реального алгоритма LSB (что в принципе неплохо, но способ его применения делает его легко обнаруживаемым). Уже учитывая потерю данных и "неясность" (два в некотором роде похожих, но разных термина) в нейронных сетях, модель пытается создать нечеткий LSB. В идеальном мире это должно было бы привести нас к LSB из-за среднеквадратической ошибки. Есть несколько способов решить эту проблему: самый простой - изменить функцию потерь (MSE работает в пиксельном пространстве), можно добавить коэффициент для реконструкции. В любом случае извлеченное секретное изображение может быть с потерей данных (мы можем допустить 75% веса для этой конкретной потери при обратном распространении).
Лучше было бы добавить компоненты потерь на основе восприятия, сравнивая активации признаков изображений в предварительно обученной модели, например, VGG16, ориентируясь на схожесть в пространстве признаков, а не пикселей.
"Суть" статьи заключается во вложенном автоэнкодере и способе его обучения в двухчастной системе (что в какой-то мере напоминает GAN по противоборствующей природе, но способ обновления весов отличается от традиционных нейронных сетей).
Я имел в виду, что вложенная архитектура и обучение уже на месте, и потребуется время и энергия для дополнительной настройки гиперпараметров, добавления слоев, улучшения обучения.
Что касается вопроса о вместимости обложки изображения, его можно рассчитать из существующих результатов, но это было бы бессмысленно, если бы модель была сильной после настройки гиперпараметров и не являлась бы PoC. Но, как я уже сказал, я обучаю модель, уже заменил слои tanh на LeakyReLU, начальная потеря на проверке уменьшилась более чем на 20%, возможно, позже я добавлю активацию swish. Если вы хотите сделать это сами, помните, что не для выходного слоя, потому что изображения должны быть нормализованы [-1, 1]. Я буду медленно обучать и тестировать его, затем добавлю batchnorm, пространственное исключение (возможно, это не требуется, исходя из размера нашей сети). Добавлю еще несколько сверток.
Есть много таких методов, которые мы можем попробовать. Единственное ограничение - это время и энергия. Часто я занят, у меня дома только одна видеокарта, которая уже используется для другого обучения, и времени мало. Но, конечно, я буду переобучать модель и добавлять слои (это будет необходимо, если вы знаете о Tiny-ImageNet).
<балашиха.mp4>
Теперь я вижу, что перевод оставляет желать лучшего. Я, конечно, повторяюсь, но в сути я говорил о двух моментах: С архитектурой все в порядке. and LSB - это лишь результат функции кросс-энтропии и того, как мы учитываем ошибки реконструкции. Я уже предложил два решения этой проблемы ранее
 
Последнее редактирование:
Забудьте о LeakyReLU, я заменил всё на функцию swish, так как её домен - это [-inf, inf], мы отображаем выходной слой в обычный tanh. Как я уже говорил в статье, я перешел на оптимизатор Adam с небольшим дополнением. В то время как MSE потери измеряют ошибки на уровне пикселей, потери по восприятию учитывают разницу в восприятии изображений. Я сделал это с помощью сети VGG в качестве извлекателя признаков. Еще более изобретательно я объединил потери MSE и по восприятию. Вы можете настроить веса самостоятельно, и модель будет концентрироваться либо на точном соответствии пикселей (с MSE), либо на восприятии сходства в зависимости от вашей конфигурации. Я добавил слой с 256 каналами. И я даже не приближался к BobNet.А по поводу AliceNet, были сделаны значительные улучшения, первое и самое заметное - я заменил conv2d на Depthwise Separable Convolutions. Они иногда обеспечивают схожую или даже лучшую производительность при уменьшенных вычислениях. Эти конволюции разделяют операцию на depthwise и pointwise, что является вычислительно эффективным.В общем, я хотел использовать что-то вроде LayerNorm, но в некотором роде стеганографию можно рассматривать как задачу переноса стиля, где стиль (или секрет) - это информация для встраивания. Адаптивная нормализация экземпляра AdaIN может быть полезна для задач переноса стиля.

Python:
seed = 42
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
np.random.seed(seed)
random.seed(seed)

class DepthwiseSeparableConv2d(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1):
        super(DepthwiseSeparableConv2d, self).__init__()
        self.depthwise = nn.Conv2d(in_channels, in_channels, kernel_size, stride, padding, dilation, groups=in_channels)
        self.pointwise = nn.Conv2d(in_channels, out_channels, kernel_size=1)

    def forward(self, x):
        # print('before depthwise and pointwise ', x.shape)
        x = self.depthwise(x)
        # print("aft depthwise ", x.shape)
        x = self.pointwise(x)
        # print("befor pointwise ", x.shape)
        return x

class AdaIN(nn.Module):
    def forward(self, x):
        # normalize using x's own stats
        mean, std = x.mean([2, 3], keepdim=True), x.std([2, 3], keepdim=True)
        normalized_content = (x - mean) / (std + 1e-7)
        return normalized_content



class AliceNet(nn.Module):
    def __init__(self):
        super(AliceNet, self).__init__()
       
        self.feature_extractor = nn.Sequential(
            DepthwiseSeparableConv2d(3, 32, kernel_size=3, stride=1, padding=1),
            nn.SiLU(),
            DepthwiseSeparableConv2d(32, 64, kernel_size=3, stride=1, padding=1),
            nn.SiLU(),
            DepthwiseSeparableConv2d(64, 128, kernel_size=3, stride=1, padding=1),
            nn.SiLU(),
            DepthwiseSeparableConv2d(128, 256, kernel_size=3, stride=1, padding=1),
            nn.SiLU()
        )
       
        self.decoder = nn.Sequential(
            DepthwiseSeparableConv2d(259, 256, kernel_size=3, stride=1, padding=1),
            AdaIN(),
            nn.SiLU(),
            DepthwiseSeparableConv2d(256, 128, kernel_size=3, stride=1, padding=1),
            AdaIN(),
            nn.SiLU(),
            DepthwiseSeparableConv2d(128, 64, kernel_size=3, stride=1, padding=1),
            AdaIN(),
            nn.SiLU(),
            DepthwiseSeparableConv2d(64, 3, kernel_size=3, stride=1, padding=1),
            nn.Tanh()  # bound output to [-1, 1]
        )

    def forward(self, inputs, inputs2):
        features = self.feature_extractor(inputs2)
        combined = torch.cat((inputs, features), dim=1)
        # print(combined.shape)
        # print('here before')
        output = self.decoder(combined)
        # print('here')
        return output

class BobNet(nn.Module):
    def __init__(self):
        super(BobNet, self).__init__()
        self.secret_decoder = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1),
            nn.SiLU(),
            nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
            nn.SiLU(),
            nn.Conv2d(128, 3, kernel_size=3, stride=1, padding=1),
            nn.Tanh()   # # bound output to [-1, 1]
        )
    def forward(self, x):
        output = self.secret_decoder(x)
        return output


class Trainer:
    def __init__(
        self,
        models: List[nn.Module],
        dataloaders: dict,
        criterion: nn.Module,
        optimizers: List[optim.Optimizer],
        device: str
    ):
        self.Alice = models[0].to(device)
        self.Bob = models[1].to(device) if len(models) >= 2 else None
        self.train_loader = dataloaders['train']
        self.val_loader = dataloaders['val']
        self.criterion = criterion
        self.alice_optimizer = optimizers[0]
        self.alice_bob_optimizer = optimizers[1] if len(optimizers) >= 2 else None
        self.device = device

        self.alice_train_losses : List = []
        self.alice_bob_train_losses : List = []

        self.alice_val_loss : List = []
        self.alice_bob_val_loss : List = []
       
        self.epochs = 0

    def train(self, num_epochs: int):
        print("Current Model:")
        self._visualize_samples()

        for epoch in tqdm(range(num_epochs), desc="Epochs"):
            with tqdm(total=len(self.train_loader), desc="Training", position=0, leave=True) as tqdm_bar:
                self._run_epoch(self.train_loader, True, tqdm_bar)

                fig, axs = plt.subplots(1, 2, figsize=(15, 5))

                axs[0].plot(self.alice_train_losses, label='alice loss')
                if self.Bob:
                    axs[0].plot(self.alice_bob_train_losses, label='alice bob loss', color="orange")
                axs[0].set_xlabel('Iterations')
                axs[0].legend()
                axs[0].set_title("Training Losses")
               
                self._run_epoch(self.val_loader, False)
               
                axs[1].plot(self.alice_val_loss, label='alice loss')
                if self.Bob:
                  axs[1].plot(self.alice_bob_val_loss, label='alice bob loss', color="orange")
                axs[1].set_xlabel('Iterations')
                axs[1].legend()
                axs[1].set_title("Validation Loss")

                plt.tight_layout()
                plt.show()

                self._visualize_samples()

    def _run_epoch(self, dataloader: DataLoader, training: bool, tqdm_bar=None) -> float:
        if training:
            self.Alice.train()
            self.Bob.train() if self.Bob else None
            context = torch.enable_grad
        else:
            self.Alice.eval()
            self.Bob.eval() if self.Bob else None
            context = torch.no_grad

        with context():
            for inputs, _ in dataloader:
                cover_images = inputs.to(self.device)
                random_indices = torch.randperm(cover_images.shape[0])
                secret_images = cover_images[random_indices]
                embedded_cover_images = self.Alice(cover_images, secret_images)
                alice_loss = self.criterion(embedded_cover_images, cover_images)

                if self.Bob:
                    extracted_secret_images = self.Bob(embedded_cover_images)
                    alice_bob_loss = 0.75 * self.criterion(extracted_secret_images, secret_images)
                   
                if training:
                    self.alice_optimizer.zero_grad()
                    alice_loss.backward(retain_graph=True)
                    self.alice_optimizer.step()
                    self.alice_train_losses.append(alice_loss.item())
                    tqdm_bar.set_postfix(alice_loss=alice_loss.item())
               
                    if self.Bob:
                        self.alice_bob_optimizer.zero_grad()
                        alice_bob_loss.backward()
                        self.alice_bob_optimizer.step()
                        self.alice_bob_train_losses.append(alice_bob_loss.item())
                        tqdm_bar.set_postfix(alice_bob_loss=alice_bob_loss.item())
                    tqdm_bar.update()
                else:
                    self.alice_val_loss.append(alice_loss.item())
                    if self.Bob:
                      self.alice_bob_val_loss.append(alice_bob_loss.item())

        self.epochs += 1
           

    def _visualize_samples(self):
        inputs, _ = next(iter(self.val_loader))
        cover_images = inputs.to(self.device)
        random_indices = torch.randperm(cover_images.shape[0])
        secret_images = cover_images[random_indices]
        embedded_cover_images = self.Alice(cover_images, secret_images)

        if self.Bob:
            extracted_secret_images = self.Bob(embedded_cover_images)
       
        num_samples = 3
        plt.figure(figsize=(18, 12))
       
        for idx in range(num_samples):
            position = idx * 4 + 1
           
            self._plot_img(cover_images[idx], title='Cover', position=position)
            self._plot_img(secret_images[idx], title='Secret', position=position + 1)
            self._plot_img(embedded_cover_images[idx], title='Embedded Cover', position=position + 2)
            if self.Bob:
                self._plot_img(extracted_secret_images[idx], title='Extracted Secret', position=position + 3)
       
        plt.tight_layout()
        plt.show()

    @staticmethod
    def _plot_img(img_tensor: torch.Tensor, title: str, position: int):
        plt.subplot(3, 4, position)
        img_tensor_ = img_tensor.cpu().detach().permute(1, 2, 0)
        mean = np.array([0.485, 0.456, 0.406])
        std = np.array([0.229, 0.224, 0.225])
        img_tensor_ = img_tensor_.numpy() * std + mean
        img_tensor_ = np.clip(img_tensor_, 0, 1)
        plt.imshow(img_tensor_)
        plt.title(title)
        plt.axis('off')

class PerceptualLoss(nn.Module):
    def __init__(self):
        super(PerceptualLoss, self).__init__()
        self.vgg = models.vgg16(pretrained=True).features[:16].eval().cuda()
        for param in self.vgg.parameters():
            param.requires_grad = False

    def forward(self, x, y):
        x_features = self.vgg(x)
        y_features = self.vgg(y)
        return nn.MSELoss()(x_features, y_features)

def combined_loss(output, target):
    alpha = 0.8
    return alpha * nn.MSELoss()(output, target) + (1-alpha) * criterion_perceptual(output, target)

from torch.optim.lr_scheduler import ReduceLROnPlateau
import torchvision.models as models

alice = AliceNet()
bob = BobNet()
criterion_perceptual = PerceptualLoss()
criterion = combined_loss
alice_optimizer = optim.Adam(alice.parameters(), lr=0.001, betas=(0.9, 0.999))
alice_bob_optimizer = optim.Adam(list(bob.parameters()) + list(alice.parameters()), lr=0.001, betas=(0.9, 0.999))
scheduler_alice = ReduceLROnPlateau(alice_optimizer, 'min', patience=10, factor=0.5)
scheduler_alice_bob = ReduceLROnPlateau(alice_bob_optimizer, 'min', patience=10, factor=0.5)

dataloaders = {"train": train_loader, "val": val_loader}


print(sum(p.numel() for p in alice.parameters()))
print(sum(p.numel() for p in bob.parameters()))

trainer = Trainer([alice, bob], dataloaders, criterion, [alice_optimizer, alice_bob_optimizer], device)


EPOCH 1
n1.png


Это существенный прогресс по сравнению с предыдущей моделью. Уже на первой эпохе модель показала возможности, гораздо превосходящие первую модель. Затем произошло расхождение модели (потери возросли), но это все связано с тонкой настройкой гиперпараметров и небольшими изменениями. В общем, тут больше нечего добавить, кроме как порекомендовать вам самим экспериментировать и тонко настраивать параметры. Также я добавил зерно для генератора псевдослучайных чисел как для torch, так и для numpy. Поэтому, если хотите опираться на это, используйте то же зерно
 
эй, это просто не по теме, не могли бы вы дать совет, как настроить CUDA, если я новичок и не получаю хороших видео?
Можете дать больше деталей о вашей проблеме? Лучший совет - следуйте официальной документации. Нет смысла копировать тот же гайд сюда. Если у вас возникли трудности, ищите решение в интернете. Сообщество Linux уже столкнулось с массой проблем, связанных с драйверами nvidia, и многие из них уже решены, ответы можно найти в сети. Если решения нет, создайте тему с подробным описанием вашей проблемы, добавьте отчет об ошибке, данные отладки и то, что вы уже пробовали. Если кто-то сталкивался с такой же проблемой и знает решение, он с радостью поделится им.


images.jpeg
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх