Вспомнить всё. Спектр весов нейросети

от автора

В данной публикации попробуем сформировать простейшую нейросеть. Будем использовать Colab. Данный выбор также хорош тем, что то, что позволено Юpyтеру не позволено быку. Иметь локальные вычислительные мощности. В принципе довольно неплохая инфраструктура для проверки базовых алгоритмов налету. Если есть что то подобное на других платформах или можно сделать с использованием иных агентов, пожалуйста, прокомментируйте.

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

Вместо кода будут md-саммари по разделам, их можно использовать для генерации в качестве промптов для ИИ-агента.

Генератор случайных прямоугольников

Описание функции generate_random_rectangle

Эта ячейка определяет функцию generate_random_rectangle. Она использует библиотеку random для генерации следующих параметров:

  • Соотношение сторон (ширина/высота): от 0.3 до 3.0.

  • Ширина и высота: регулируются, чтобы находиться в диапазоне от 1 до 10.

  • Координаты центра (cx, cy): от -5 до 5 для обеих осей.

Прямоугольник считается квадратом (is_square=True), если его соотношение сторон находится в диапазоне от 0.8 до 1.2.

Демонстрация generate_random_rectangle

Эта ячейка демонстрирует работу функции generate_random_rectangle, вызывая ее 3 раза в цикле for i in range(3). Результаты каждого вызова функции (rect_data) выводятся на консоль с использованием форматированных строк (f-string) и ограничением до двух знаков после запятой (:.2f) для числовых значений, таких как координаты центра, ширина, высота и соотношение сторон.

Эта ячейка использует библиотеки matplotlib.pyplot (как plt) и matplotlib.patches для визуализации 15 случайных прямоугольников, сгенерированных функцией generate_random_rectangle.

  • Фигуры добавляются на оси (ax.add_patch(patches.Rectangle(...))) с прозрачностью alpha_value = 0.3.

  • Квадраты (is_square равно True) заливаются красным ('red'), а остальные прямоугольники — зеленым ('green').

  • Пределы осей установлены от -10 до 10 по x и y (ax.set_xlimax.set_ylim).

  • Для отображения графика используется plt.show().

Генератор случайных прямоугольников, если выполняется соотношение сторон - это квадрат

Генератор случайных прямоугольников, если выполняется соотношение сторон — это квадрат

Далее преобразуем прямоугольники в датасет. Используем координаты вершин а также классы — квадрат или прямоугольник

Формирование датасета

Определение функции generate_pytorch_dataset

Эта ячейка определяет функцию generate_pytorch_dataset, которая генерирует набор данных для PyTorch. Для каждого из num_samples образцов она вызывает generate_random_rectangle и вычисляет 8 координат вершин (x, y для нижнего левого, нижнего правого, верхнего правого и верхнего левого углов).

Метка (label) присваивается как 'square' или 'rectangle' в зависимости от поля is_square.

Демонстрация generate_pytorch_dataset

Эта ячейка демонстрирует использование функции generate_pytorch_dataset, создавая небольшой набор данных из 5 образцов (num_samples=5). Она выводит метку (label) и 8 координат (coordinates) для каждого сгенерированного образца, чтобы показать формат данных.

Класс RectangleDataset для PyTorch

Эта ячейка определяет класс RectangleDataset, наследующийся от torch.utils.data.Dataset, для подготовки данных к обучению PyTorch.

  • Он преобразует 8 координат в тензор torch.float32.

  • Метки (‘rectangle’: 0, ‘square’: 1) преобразуются в тензор torch.long.

Для демонстрации создается набор данных из 20000 образцов (pytorch_dataset_tensors).

Пример образца прямоугольника в виде координат вершин, обратить внимание что образован вектор как сигнал, порядок определяется порядком следования в этом векторе (нижний левый… верхний правый…)

Образец 1: Метка: rectangle Координаты (НЛ, НП, ВП, ВЛ): [-6.5377609951629, -5.900628595832362, 3.2412660734930583, -5.900628595832362, 3.2412660734930583, -1.991917979473778, -6.5377609951629, -1.991917979473778]

По образцам создаются тензоры с входным сигналом, который является координатами

Создаем PyTorch RectangleDataset с 20000 образцами для демонстрации…
Всего образцов в наборе данных PyTorch: 20000
Количество прямоугольников: 17083
Количество квадратов: 2917
Образец 1 (из набора данных PyTorch): Тензор координат: tensor([-5.9753, -2.5356, -2.9142, -2.5356, -2.9142, -1.1667, -5.9753, -1.1667]) Тип данных координат: torch.float32 Тензор метки: 0 Тип данных метки: torch.int64

Определение параметров обучения. Задаётся конфигурация и тип функции активации ReLU

Параметры обучения и модель

Определение модели Classifier и параметров обучения

Эта ячейка определяет архитектуру нейронной сети Classifier с использованием torch.nn. Модель состоит из двух полносвязных слоев (nn.Linear) и функции активации nn.ReLU():

  • Первый слой (fc1) имеет 8 входных нейронов (координаты) и 32 выходных.

  • Второй слой (fc2) имеет 32 входных нейрона и 2 выходных (для классификации ‘rectangle’ или ‘square’).

Функция потерь nn.CrossEntropyLoss() и оптимизатор optim.Adam с шагом обучения lr=0.001 инициализируются. DataLoader с batch_size = 64 используется для формирования батчей данных.

Архитектура модели: Classifier(
(fc1): Linear(in_features=8, out_features=32, bias=True) (relu): ReLU()
(fc2): Linear(in_features=32, out_features=2, bias=True) )

После чего можно произвести обучение нейросети.

Процесс обучения

Обучение модели Classifier

Эта ячейка выполняет обучение модели Classifier в течение 10 эпох (num_epochs = 10).

В каждой эпохе:

  1. Модель переводится в режим обучения (model.train()).

  2. Градиенты обнуляются (optimizer.zero_grad()).

  3. Выполняется прямой проход (model(inputs)).

  4. Вычисляются потери (criterion(outputs, labels)).

  5. Обратный проход (loss.backward()) и обновление весов (optimizer.step()).

Затем модель переводится в режим оценки (model.eval()) для расчета точности на обучающем наборе, отключая вычисление градиентов (torch.no_grad()). Веса обученной модели сохраняются в глобальную переменную original_trained_model_state.

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

Начинаем обучение на 10 эпох… Эпоха 1, Потери: 0.3943, Точность: 85.61% Эпоха 2, Потери: 0.2185, Точность: 93.08% Эпоха 3, Потери: 0.1358, Точность: 97.33%

Необходимо создать модель для валидации параметров, иными словами, для проверки работоспособности модели. Дополнительно с использованием pip устанавливаются библиотеки для дальнейшей визуализации структуры модели.

Валидация модели

Эта ячейка создает отдельный набор данных для валидации, используя RectangleDataset с num_samples=2000. Для удобства батчирования данных создается validation_dataloader с batch_size=64.

Оценка модели на валидационном наборе

Эта ячейка оценивает производительность обученной модели на validation_dataset. Модель переводится в режим оценки (model.eval()) и вычисление градиентов отключается (torch.no_grad()).

  • Модель предсказывает метки для входных данных.

  • Вычисляются потери (loss.item()) и общая точность.

Установка библиотек torchviz и graphviz

Эта ячейка устанавливает необходимые библиотеки torchviz и graphviz с помощью команды !pip install. Эти библиотеки используются для визуализации графа вычислений нейронной сети.

Вывод:

Всего образцов для валидации: 2000 Количество прямоугольников в наборе валидации: 1706 Количество квадратов в наборе валидации: 294
Потери на валидации: 0.0441 Точность на валидации: 99.10%

Далее строится график, так как это первая итерация получаются первые 10 эпох

Скрытый текст

Эта ячейка использует библиотеку matplotlib.pyplot для построения графика истории потерь обучения.

  • Используется история обучения

  • График отображает изменение loss (Потери) в зависимости от epoch (Эпоха) с маркерами o и синей линией.

  • На оси X показаны номера эпох от 1 до 10, что позволяет визуально отслеживать процесс сходимости модели.

loss-функция при первичном обучении

loss-функция при первичном обучении

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

Построение структуры модели

Визуализация архитектуры модели с torchviz

Эта ячейка использует библиотеку torchviz для визуализации архитектуры модели Classifier. Для этого:

  1. Создается фиктивный входной тензор dummy_input размером (1, 8) с случайными значениями с помощью torch.randn().

  2. torchviz.make_dot() используется для генерации графа вычислений на основе прямого прохода модели с этим входным тензором.

  3. Граф отображается непосредственно в окне или png файл

После чего с использованием модели фильтруем сгенерированные прямоугольники на прямоугольники и квадраты

Скрытый текст

Предсказание и визуализация на тестовых данных

Эта ячейка генерирует 50 новых тестовых образцов (num_test_samples = 50) с использованием generate_pytorch_dataset. Затем обученная модель (model.eval() и torch.no_grad()) предсказывает метки для этих образцов. matplotlib.pyplot и matplotlib.patches используются для построения двух графиков:

  • Левый график: показывает фактические фигуры (квадраты красные, прямоугольники зеленые).

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

Вывод отображает все фигуры и выделенные нейросетью
Количество фактических меток: Прямоугольники=43, Квадраты=7
Количество предсказанных меток: Прямоугольники=43, Квадраты=7
Точность на 50 тестовых образцах: 100.00%

Распознавание квадратов по координатам прямоугольников

Распознавание квадратов по координатам прямоугольников

Переходим к непосредственно исследованию весов нейросети. Построим «тепловую» карту для каждого слоя, отражающую значения весов нейронов.

Скрытый текст

Визуализация весов линейных слоев модели

Эта ячейка визуализирует веса (weights) каждого линейного слоя (fc1fc2) нейронной сети с помощью matplotlib.pyplot. Перед этим загружаются сохраненные исходные веса (original_trained_model_state) в модель.

  • Веса преобразуются в массив numpy (layer_module.weight.cpu().detach().numpy()).

  • Отображаются в виде тепловых карт (ax.imshow(weights.T, cmap='viridis', aspect='auto')).

  • Цикл по всем весам сохраняя их на графике

  • Для первого слоя :

    • Ось Y подписана входные признаки 

    • Ось X — Нейроны скрытого слоя 

      Для последнего слоя:

    • Ось Y подписана Нейроны скрытого слоя

    • Ось X — Выходные классы

Основные веса представлены на рис. 1.

Рис 1 - исходные веса после полного обучения

Рис 1 — исходные веса после полного обучения

После чего осуществляем прямое дискретное преобразование Фурье. Переводим двумерные графики весов слоёв в комплексно-частотную область, для мнимой и действительных составляющих, не АЧХ/ФЧХ.

Скрытый текст

Применение Дискретного Преобразования Фурье (ДПФ) к весам

Эта ячейка применяет Дискретное Преобразование Фурье (np.fft.fft) к весам (weights) каждого линейного слоя модели вдоль оси входных признаков (axis=1).

  • Затем визуализируются действительная (np.real(dft_result)) и мнимая (np.imag(dft_result)) части полученных Фурье-коэффициентов.

  • Отображение происходит в виде тепловых карт (ax.imshow(..., cmap='coolwarm')).

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

Двумерная, комплексно-частотная характеристика исходных весов представлена на рис. 2.

Рис. 2. результат работы ДПФ над тепловыми картами слоёв

Рис. 2. результат работы ДПФ над тепловыми картами слоёв

Для реализации идеи используем сглаживание с применением усредняющего фильтра с параметром 2 точки.

Сглаживание спектральных составляющих

Сглаживание ДПФ весов

Эта ячейка продолжает анализ весов в частотной области, применяя сглаживание к действительной и мнимой частям результатов ДПФ.

  • Для этого используется scipy.ndimage.uniform_filter() с filter_size пользователем.

  • Сглаженные действительные (smoothed_real_part) и мнимые (smoothed_imag_part) части весов каждого линейного слоя визуализируются в виде тепловых карт (ax.imshow(..., cmap='coolwarm')).

Это помогает выявить более крупные структурные паттерны в Фурье-представлении весов.

Результирующий спектр показан на рис. 3, 4, и 5 для параметров сглаживания 2,3 и 4 соответственно.

Рис. 3 - сглаживание с параметром 2 точки

Рис. 3 — сглаживание с параметром 2 точки
Рис. 4 - сглаживание с параметром 3 точки

Рис. 4 — сглаживание с параметром 3 точки
Рис. 5 - сглаживание с параметром 4 точки

Рис. 5 — сглаживание с параметром 4 точки

Далее производится постеризация с использованием заданного количества уровней. Для упрощения приведём пример для 2 и 3 уровней для графика на рис. 4 с параметром n = 3.

Постеризация по уровню

Постеризация ДПФ весов

Эта ячейка вводит функцию posterize_array для квантования значений массива до n_levels.

  • Эта функция применяется к действительной и мнимой частям результатов ДПФ весов каждого линейного слоя.

  • Полученные постеризованные значения (posterized_real_partposterized_imag_part) визуализируются в виде тепловых карт (ax.imshow(..., cmap='coolwarm')).

  • Данные сохраняются в глобальную переменную saved_posterized_dft_data.

На рис. 6 представлен результат постеризации для 3 уровней и количества точек усреднения 3, на рис. 7 — результат постеризации для 2 уровня и такого же количества точек усреднения 3. Постеризация означает «забыть» некоторые спектральные составляющие распределения весов.

Рис. 6. Три уровня постеризации и 3 точки усреднения

Рис. 6. Три уровня постеризации и 3 точки усреднения
Рис. 7. Два уровня постеризации и 3 точки усреднения

Рис. 7. Два уровня постеризации и 3 точки усреднения

Следует отметить что на рис. 7 фактически имеем двоичную карту спектра весов, состоящую только из -k и k.

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

Обратное двумерное дискретное преобразование Фурье

Восстановление весов после обратного ДПФ

Эта ячейка выполняет обратное Дискретное Преобразование Фурье (np.fft.ifft) над постеризованными действительной и мнимой частями ДПФ, которые были сохранены в saved_posterized_dft_data.

  • Восстановленные веса (reconstructed_weights) для каждого слоя затем визуализируются в виде тепловых карт (ax.imshow(reconstructed_weights.T, cmap='viridis')).

  • Это позволяет увидеть, как выглядят веса после этапов ДПФ, постеризации и ОДПФ.

  • Восстановленные веса для слоев сохранены в reconstructed_weights_per_layer.

После чего восстанавливаются веса, содержащие усреднённые спектральные составляющие, представленные на рис. 8. Веса распределяются согласно восстановленному спектру, включая эффекты Гиббса (для сигнала) и прочие характерные детали.

Рис. 8. Восстановленные коэффициенты для фильтрации 3 точки и постеризации 3 уровня

Рис. 8. Восстановленные коэффициенты для фильтрации 3 точки и постеризации 3 уровня

Далее проведём эксперимент с первой итерацией. Предположим количество точек фильтрации 2 и уровней 3.

Загружаем эти веса в модель:

Скрытый текст

Загрузка восстановленных весов в модель

Эта ячейка загружает восстановленные веса, полученные после обратного ДПФ постеризованных значений (reconstructed_weights_per_layer), обратно в линейные слои (fc1fc2) модели Classifier.

  • Для каждого слоя проверяется совпадение форм весов.

  • Затем новые веса присваиваются атрибуту .data (module.weight.data = new_weights) соответствующего слоя torch.nn.Linear.

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

Вывод, проверяем что веса успешно загружены из постеризованной модели
Загружаем восстановленные веса в модель… Веса для слоя ‘fc1’ успешно загружены. Веса для слоя ‘fc2’ успешно загружены. Восстановленные веса успешно загружены в модель.

Дообучаем модель

Дообучение модели

Дообучение модели с постеризованными весами

Эта ячейка продолжает обучение модели с загруженными ‘постеризованными’ весами в течение еще 10 эпох (num_epochs_for_retraining = 10). Процесс обучения аналогичен первоначальному, с вычислением потерь (epoch_loss) и точности (epoch_accuracy) на обучающем наборе данных.

  • Результаты этого дополнительного этапа обучения добавляются в общую training_history.

И производим объединение результата.

Визуализация истории обучения

Визуализация объединенной истории обучения

Эта ячейка визуализирует объединенную историю обучения модели, используя matplotlib.pyplot. Она строит два графика — один для потерь (losses) и один для точности (accuracies), разделяя их по экспериментам (начальное обучение, первое дообучение).

  • Каждый эксперимент представлен отдельной линией с различными colors и linestyles.

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

Начиная с ячейки загрузки постеризованных весов варьируем условия. Пока что вручную но можно автоматизировать. Задаём последовательно уровней фильтрации 2,3,4 для каждого уровня количество уровней постеризации 3 затем 2. Получается 6 значений экспериментов. Выведем общий график

Пример для 4 точек фильтрации - спектр весов

Пример для 4 точек фильтрации — спектр весов
Пример для 4 точек фильтрации и 2 уровней постеризации - спектр весов

Пример для 4 точек фильтрации и 2 уровней постеризации — спектр весов

Пробуем самый плавный вариант — 6 точек и 2 уровня дискретизации и 8 уровней и 2 уровня дискретизации.

На главном графике представлены следующие эксперименты. Начальное обучение — без фильтра, базовые веса. Первое дообучение — сглаживание 2 уровней 3, второе — сглаживание 2 уровней 2, эксп 4 — сглаживание 3 уровней 3, экс 5 — сглаживание 3 уровней 2, экс 6 — сглаживание 4 уровней 3, эк 7 — сглаживание 4 уровней 2, эк 8 — сглаживание 6 уровней 2, эк 9 — сглаживание 8 уровней 2, эк 10 — сглаживание 8 уровней 3.

Как видно, ключевое различие в основном в уровнях постеризации. И, действительно, идеальный фильтр уменьшает амплитуду «пульсаций» весов, но не стирает информацию (!), просто её амплитуда становится меньше, если фильтр имеет ограниченную разрядную сетку — тогда происходят потери. Поэтому, для лучшей имитации даже с фильтром с плавающей запятой, необходимо применять не нормализованную постеризацию, то есть в абсолютном значении. Тем не менее, результат следующий — использование весов, восстановленных из доведённой до двоичного значения структуры спектра, позволяет дообучить модель начиная со 2 эпохи уже до релизного варианта полного обучения. Самое интересное что точность при «стирании» части спектра остаётся относительно высокой. В процессе обучения модели с начальными условиями с тремя уровнями дают лучший результат, с двумя — сходятся практически к модели обучаемой с нуля, однако, дают большую точность.

Пример вывода запроса по оценке данных при упаковке спектра весов
Количество уровней постеризации: 3Теоретически необходимо 2 бит на каждое значение для 3 уровней.Слой: fc1  Размерность данных (каждая часть): (32, 8)  Объем действительной части (float32): 1024 байт  Объем мнимой части (float32): 1024 байт  Объем действительной части (uint8): 256 байт  Объем мнимой части (uint8): 256 байт  Теоретический мин. объем (биты): 1024 битСлой: fc2  Размерность данных (каждая часть): (2, 32)  Объем действительной части (float32): 256 байт  Объем мнимой части (float32): 256 байт  Объем действительной части (uint8): 64 байт  Объем мнимой части (uint8): 64 байт  Теоретический мин. объем (биты): 256 бит--- Сводка --- Общее количество элементов для постеризованных весов: 640Общий объем данных (float32): 2560 байтОбщий объем данных (uint8): 640 байтОбщий теоретический мин. объем данных (биты): 1280 битОбщий теоретический мин. объем данных (байты): 160 байтОтносительное сокращение объема (float32 -> uint8): 75.00%Относительное сокращение объема (float32 -> теоретический мин.): 93.75%

Таким образом, не смотря на то, что стираем «память» в виде квантования спектра по уровню, оставляя определённые ноты, характерные для шаблона распределения весов нейросети, можно её довольно быстро восстановить дообучением. Данное явление по всей видимости связано также с особенностями самих методов, которые обеспечивают реализацию алгоритмов первого и второго порядка, практически точно предсказывая градиент от заданных начальных условий, содержащей крупицы информации от предыдущего обучения, плюс ещё функция активации ReLU имеющая линейный участок. Таким образом, сеть может «забыть» до 75%-90% спектрального распределения весов, затем, в процессе переобучения быстро восстановиться от этих начальных условий.

ссылка на оригинал статьи https://habr.com/ru/articles/1030520/