Искусственные нейронные сети — удивительная технология, позволяющая компьютерам имитировать работу человеческого мозга. Если вы хотя бы немного интересуетесь современными технологиями, то наверняка слышали о таких проектах, как ChatGPT или Midjourney — это яркие примеры применения нейросетей. Но что происходит внутри этих сложных систем? Как они "учатся" и "мыслят"?
На первый взгляд может показаться, что нейросети — это что-то запредельно сложное, доступное только профессионалам с серьезной математической подготовкой. Но на самом базовом уровне принципы работы нейросетей довольно просты и доступны для понимания даже начинающим программистам.
Изучение нейросетей на простых языках программирования имеет ряд преимуществ. Во-первых, мы избавляемся от сложностей, связанных с синтаксисом языка, и можем сосредоточиться непосредственно на алгоритмах. Во-вторых, простые языки часто более наглядны и позволяют лучше визуализировать происходящие процессы. В-третьих, простота языка помогает нам разобраться в фундаментальных принципах работы нейросетей, не отвлекаясь на технические детали реализации. Именно поэтому для нашего проекта мы выбрали КуМир — систему программирования, разработанную специально для обучения. КуМир (Комплект Учебных МИРов) — это среда, созданная в России для обучения программированию в школах. Её главное преимущество — простой русскоязычный синтаксис и понятная среда разработки.
В КуМире используется алгоритмический язык, очень близкий к псевдокоду, который часто применяется для записи алгоритмов в учебниках. Благодаря руссифицированному синтаксису и отсутствию лишних деталей, КуМир идеально подходит для демонстрации базовых принципов работы нейросетей. Наша цель — создать минималистичную, но рабочую нейросеть прямо в КуМире. Мы не будем стремиться к высокой производительности или созданию сложных архитектур. Вместо этого мы сосредоточимся на понимании базовых принципов:- как устроен отдельный искусственный нейрон,
- как нейроны объединяются в сеть,
- каким образом нейросеть обучается,
- как применять обученную сеть для решения задач.
В результате мы получим простую нейросеть, способную решать несложные задачи классификации или прогнозирования. Например, наша нейросеть сможет определять, какая цифра изображена на простой картинке, или предсказывать следующее число в последовательности.
Этот проект — отличная стартовая точка для тех, кто хочет погрузиться в увлекательный мир машинного обучения и нейронных сетей. Мы не будем использовать готовые библиотеки или фреймворки — всё напишем сами с нуля. Это поможет глубже понять, как работают нейросети на самом базовом уровне. Готовы создать свою первую нейросеть на КуМире? Тогда приступим к погружению в увлекательный мир искусственного интеллекта!
Историческое развитие нейросетей
История нейросетей насчитывает уже более 80 лет и тесно переплетается с развитием кибернетики, математики и компьютерных наук. Первые теоретические модели искусственных нейронных сетей появились задолго до эры современных компьютеров. Всё началось в 1943 году, когда нейрофизиолог Уоррен Маккалок и математик Уолтер Питтс предложили математическую модель нервной системы. Они описали нейрон как простое логическое устройство, выполняющее бинарные операции. Эта модель стала первым шагом на пути к созданию искусственного интеллекта. В 1949 году психолог Дональд Хебб выдвинул теорию об обучении нейронов. Он предположил, что связи между нейронами укрепляются, когда они активируются одновременно — именно этот принцип лежит в основе современных алгоритмов обучения нейросетей.
Настоящий прорыв произошёл в 1957 году, когда Фрэнк Розенблатт изобрёл перцептрон — первую модель нейросети, способную обучаться распознаванию простых образов. Розенблатт создал не только теоретическую модель, но и реальное устройство — Mark I Perceptron, реализованное на стареньких аналоговых компьютерах. Забавно, но эта машина умела распознавать только 400 пикселей, а весила при этом несколько тонн! Однако в 1969 году Марвин Минский и Сеймур Паперт опубликовали книгу "Перцептроны", где математически доказали ограниченность однослойных перцептронов, которые не могли решать даже простейшую задачу вычисления логической функции XOR (исключающее ИЛИ). Это привело к первой "зиме" в исследованиях нейросетей — финансирование сократилось, интерес угас.
Возрождение произошло в 1980-х годах, когда Джон Хопфилд предложил новую архитектуру нейросети, способную функционировать как память контента. В это же время был заново "открыт" алгоритм обратного распространения ошибки, который позволял эффективно обучать многослойные нейронные сети. 1990-е годы стали периодом интенсивного развития различных архитектур нейросетей. Появились сверточные нейронные сети, которые особенно эффективны для обработки изображений, и рекуррентные сети, подходящие для анализа последовательностей данных, таких как текст или временные ряды. В начале 2000-х годов развитие нейросетей замедлилось из-за вычислительных ограничений и конкуренции со стороны более простых методов машинного обучения, таких как метод опорных векторов.
Новый взрыв интереса к нейросетям начался примерно в 2012 году, когда группа под руководством Джеффа Хинтона продемонстрировала феноменальные результаты в распознавании изображений с использованием глубоких нейронных сетей. Это стало возможным благодаря увеличению вычислительной мощности компьютеров, особенно графических процессоров, и появлению больших наборов данных для обучения. С тех пор прогресс в области нейросетей шёл семимильными шагами. Появились генеративно-состязательные сети, способные создавать реалистичные изображения, трансформеры, произведшие революцию в обработке естественного языка, и многие другие архитектуры.
Сегодня нейросети используются практически везде: от фильтров в социальных сетях до систем автоматического вождения, от переводчиков текста до медицинской диагностики. И всё это выросло из простых математических моделей, предложенных энтузиастами более полувека назад. И хотя наша нейросеть в КуМире будет гораздо проще современных многослойных монстров с миллиардами параметров, она позволит нам понять те самые базовые принципы, которые были заложены пионерами этой области.
Нейросеть. Обучение Добрый день, изучаю нейросети. Но с теорией возникли проблемки. Не могу найти нормальной информации по следующим вопросам:
1. Методы исправления... Нейросеть, обучение using System;
using System.IO;
using System.Runtime.InteropServices;
namespace ClassLibraryNeuralNetworks
{
// Структура... Простая нейросеть Привет всем!
Есть задача: Научить нейросеть ставить диагноз. 1 диагноз - 1 сеть.
Сеть 3 слоя:
входные нейроны (результаты мед... Простая нейросеть на C++ Доброго времени суток!
Всё время обучения я не раз задумывался о написании собственной нейросети, но понятной информации по C++ я не нашел. ...
Основы нейронных сетей
Прежде чем погрузиться в написание кода, давайте разберёмся, как устроены нейронные сети и почему они вообще работают. Если провести аналогию с человеческим мозгом, искусственные нейросети — это упрощённая модель биологических нейронных связей. Но насколько же они упрощены!
Биологический нейрон — чрезвычайно сложная структура с тысячами соединений, собственной "логикой" и целым набором химических процессов внутри. Искусственный нейрон по сравнению с ним — всего лишь простая математическая функция. И всё же, объединив множество таких простых элементов в сеть, мы получаем удивительно мощный инструмент для решения сложных задач. Итак, как работает искусственный нейрон? Представьте его как маленький процессор, который:
1. Принимает несколько входных сигналов от других нейронов.
2. Каждый входной сигнал умножается на свой "вес" (коэффициент важности).
3. Все взвешенные сигналы суммируются.
4. К сумме применяется функция активации.
5. Результат отправляется дальше, как выходной сигнал нейрона.
Математически это можно записать так:
y = f(w₁x₁ + w₂x₂ + ... + wₙxₙ + b)
Где:
x₁, x₂, ..., xₙ — входные сигналы,
w₁, w₂, ..., wₙ — веса соответствующих входов,
b — смещение (bias) — дополнительный параметр, позволяющий сдвигать порог срабатывания нейрона,
f — функция активации,
y — выходной сигнал.
Функция активации нужна, чтобы добавить нелинейность в работу сети. Без неё многослойная нейросеть превратилась бы просто в одно линейное преобразование, что сильно ограничило бы её возможности. Популярные функции активации включают сигмоиду (логистическую функцию), гиперболический тангенс, ReLU и другие.
А что такое персептрон? По сути, это простейшая архитектура нейросети, состоящая из одного слоя искусственных нейронов. Каждый нейрон персептрона получает все входные сигналы и выдаёт один выходной. В однослойном персептроне нет промежуточных (скрытых) слоёв — только входной и выходной. Тот самый персептрон, который не мог решить задачу XOR, был однослойным. Он имеет фундаментальное ограничение — способен решать только линейно-разделимые задачи. Проще говоря, если на плоскости вы можете разделить точки разных классов одной прямой линией, то однослойный персептрон сможет научиться этому. Если же требуется более сложная граница (как в случае XOR), потребуется многослойная архитектура.
Многослойный персептрон содержит как минимум один скрытый слой нейронов между входным и выходным. Именно добавление скрытых слоёв позволяет преодолеть ограничения однослойной архитектуры и решать нелинейные задачи. Чем больше слоёв и нейронов, тем сложнее функции может аппроксимировать сеть, но тем сложнее её обучить.
Кстати, о весах — именно они определяют "знания" нейросети. В процессе обучения веса настраиваются так, чтобы сеть давала правильные ответы на обучающих примерах. Изначально веса обычно инициализируются случайными значениями, а затем корректируются с помощью алгоритма обратного распространения ошибки (backpropagation).
Теперь о практике. Когда мы говорим о реализации нейросети в КуМире, стоит понимать, что мы столкнёмся с определёнными ограничениями. КуМир — это учебный язык с ограниченным набором возможностей, не оптимизированный для сложных вычислений или работы с большими объёмами данных. Основные ограничения КуМира при моделировании нейросетей:
1. Отсутствие встроенной поддержки векторных и матричных операций, которые широко используются в профессиональных фреймворках машинного обучения.
2. Ограниченная производительность — КуМир не предназначен для выполнения тысяч итераций сложных вычислений.
3. Отсутствие специализированных библиотек для работы с нейросетями.
4. Ограниченные возможности для визуализации результатов.
5. Отсутствие оптимизаций для работы с современными процессорами или графическими ускорителями.
Но эти ограничения не должны нас останавливать! Наоборот, они помогут нам сфокусироваться на фундаментальных аспектах работы нейросетей, не отвлекаясь на технические детали.
Для нашей учебной нейросети в КуМире мы используем следующие упрощения:- Реализуем простую архитектуру с минимальным количеством нейронов и слоёв.
- Будем работать с небольшими наборами данных.
- Используем простые функции активации, избегая сложных математических выражений.
- Явно запрограммируем каждую операцию, не пытаясь оптимизировать вычисления.
Такой подход позволит нам увидеть и понять каждый шаг в работе нейросети — от передачи сигнала по сети до настройки весов в процессе обучения.
Несмотря на ограничения КуМира, мы всё же можем моделировать ключевые аспекты нейросетей. С чего начать? Правильно — с представления данных. В КуМире есть массивы, которые мы будем использовать для хранения входных сигналов, весов и выходных значений нейронов. Для работы нейросети нам потребуются следующие структуры данных:- Одномерные массивы для представления входных и выходных сигналов.
- Двумерные массивы для хранения весов между слоями.
- Дополнительные массивы для смещений (bias) каждого нейрона.
В профессиональных фреймворках все эти операции оптимизированы и выполняются параллельно с помощью операций над матрицами. В нашем случае придётся реализовать всё с помощью циклов, что менее эффективно, но гораздо прозрачнее для понимания.Алгоритм прямого распространения сигнала (forward propagation) в нейросети выглядит примерно так:
1. Подаём на вход сети исходные данные.
2. Для каждого нейрона в первом скрытом слое:
а) Вычисляем взвешенную сумму входных сигналов.
б) Добавляем смещение (bias).
в) Применяем функцию активации.
3. Выходы первого слоя становятся входами для второго слоя.
4. Повторяем пункт 2 для всех последующих слоёв.
5. Выход последнего слоя — это результат работы сети.
Такой алгоритм нетрудно реализовать в КуМире с помощью нескольких вложенных циклов.
Обучение нейросети — это процесс настройки весов и смещений таким образом, чтобы минимизировать ошибку на обучающих примерах. Самый распространённый метод обучения — алгоритм обратного распространения ошибки (backpropagation). Вот его суть:
1. Запускаем сеть на обучающем примере (прямое распространение).
2. Вычисляем ошибку — разницу между полученным и желаемым выходом.
3. Начиная с последнего слоя, двигаемся назад:
а) Вычисляем градиент функции ошибки по весам.
б) Корректируем веса пропорционально градиенту в направлении уменьшения ошибки.
4. Повторяем для всех обучающих примеров.
Эта процедура повторяется многократно (обычно сотни или тысячи раз), пока сеть не начнёт давать достаточно точные результаты.
Критически важным параметром при обучении является скорость обучения (learning rate) — коэффициент, определяющий как быстро меняются веса. Если он слишком мал, обучение будет очень медленным; если слишком велик — веса будут "перепрыгивать" оптимальные значения, и сеть может не сойтись к решению вообще.
А что насчёт использования нейросетей для решения прикладных задач? Типичный рабочий цикл выглядит так:
1. Подготовка данных (сбор, очистка, нормализация).
2. Разделение данных на обучающую и тестовую выборки.
3. Определение архитектуры сети (количество слоёв и нейронов).
4. Обучение сети на обучающих данных.
5. Проверка качества на тестовой выборке.
6. Использование обученной сети для предсказаний.
В КуМире мы сможем реализовать все эти этапы, хотя и с некоторыми упрощениями.
Особенностью нейросетей является их способность к обобщению — они могут правильно обрабатывать примеры, которые не встречались в обучающей выборке. Это возможно благодаря тому, что в процессе обучения сеть "вытягивает" общие закономерности из данных, а не просто запоминает примеры.
Существует множество типов нейронных сетей, каждый со своими особенностями:- Сети прямого распространения (feedforward) — самые простые, информация передаётся только в одном направлении.
- Сверточные сети (convolutional) — хорошо подходят для обработки изображений, т.к. учитывают пространственную структуру данных.
- Рекуррентные сети (recurrent) — имеют обратные связи, хороши для обработки последовательностей.
- Самоорганизующиеся карты Кохонена — обучаются без учителя, полезны для кластеризации данных.
Для нашей реализации в КуМире мы остановимся на самом простом варианте — многослойном персептроне с прямым распространением сигнала.
Любопытный факт: хотя нейросети часто называют "искусственным интеллектом", они не обладают пониманием в человеческом смысле. Они просто находят статистические закономерности в данных. Нейросеть может превосходно классифицировать изображения кошек, но не имеет понятия, что такое "кошка" — это просто набор весов, настроенных на определённый паттерн пикселей.
Прежде чем мы перейдём к практической реализации, стоит отметить ещё одну интересную особенность нейросетей — их способность к трансферному обучению. Это метод, при котором сеть, обученная для решения одной задачи, используется как отправная точка для другой, смежной задачи. Например, сеть, обученная распознавать цифры, может быть дообучена для распознавания букв с меньшим количеством примеров, чем если бы мы начинали обучение с нуля. Такой подход особенно полезен, когда у нас мало данных для новой задачи, но это уже тема для более продвинутого изучения. В рамках нашей реализации в КуМире мы сосредоточимся на базовых концепциях.
Ещё один важный аспект — стандартизация данных. Нейросети обычно лучше работают, когда входные данные приведены к определённому диапазону (например, от 0 до 1 или от -1 до 1). Это связано с особенностями работы функций активации и помогает избежать проблем с "насыщением" нейронов. В реальных приложениях для стандартизации обычно используют методы вроде z-score (вычитание среднего и деление на стандартное отклонение) или min-max scaling (линейное масштабирование к заданному диапазону). В нашей упрощённой версии мы будем стараться изначально использовать данные в подходящем диапазоне.
Подводя промежуточный итог, мы познакомились с основными принципами работы нейронных сетей: от структуры отдельного нейрона до архитектуры многослойных сетей и процесса их обучения. Мы также обсудили ограничения КуМира и наш подход к реализации нейросети в этой учебной среде. Теперь можно перейти к более детальному рассмотрению математических основ искусственного нейрона и непосредственно к программированию нашей нейросети в КуМире.
Математическая модель искусственного нейрона
Погружаясь глубже в понимание нейронных сетей, рассмотрим детальнее математическую модель искусственного нейрона, ведь именно она лежит в основе любой нейросети. Нейрон — это базовый вычислительный блок, и его корректное математическое описание критически важно для создания работающей системы. Математически искусственный нейрон можно представить как функцию с несколькими входами и одним выходом. Каждый вход умножается на соответствующий вес, затем все эти произведения суммируются, добавляется смещение (bias), и к полученному результату применяется функция активации:
![y = f(\sum_{i=1}^{n} w_i x_i + b)]

Где:
x₁, x₂, ..., xₙ — входные сигналы нейрона
w₁, w₂, ..., wₙ — синаптические веса, определяющие "силу" каждого входа
b — смещение (bias), добавляемое к сумме
f — функция активации
y — выходной сигнал нейрона
По сути, формула описывает скалярное произведение вектора входов и вектора весов с последующим добавлением смещения и применением нелинейной функции. В КуМире мы можем реализовать это с помощью массива и цикла:
Code | 1
2
3
4
5
6
7
8
| алг Нейрон(вещ таб X[], вещ таб W[], вещ B)
нач
вещ S := B | Начинаем с bias
нц для i от 1 до длин(X)
S := S + X[i] * W[i] | Суммируем взвешенные входы
кц
знач := Активация(S) | Применяем функцию активации
кон |
|
Ключевой элемент математической модели нейрона — это функция активации. Она вводит нелинейность, позволяя сети моделировать сложные зависимости. Без функции активации многослойная нейронная сеть свелась бы к обычной линейной регрессии, независимо от количества слоёв.
Разберём несколько распространённых функций активации:
1. Пороговая функция (Threshold/Step):
f(x) = 1, если x ≥ 0; 0, если x < 0
Самая простая функция активации, которую использовал еще первый персептрон Розенблатта. Её недостаток — бинарный выход и отсутствие градиента, что делает классическое обучение невозможным.
2. Сигмоида (Логистическая функция):
f(x) = 1 / (1 + e^(-x))
Гладкая, дифференцируемая функция, которая "сжимает" любое входное значение в диапазон (0,1). Исторически популярная, но имеет проблему "исчезающего градиента" для очень больших или малых значений x.
3. Гиперболический тангенс (tanh):
f(x) = (e^x - e^(-x)) / (e^x + e^(-x))
Похож на сигмоиду, но выходные значения лежат в диапазоне (-1,1). Имеет более крутой градиент, что часто ускоряет сходимость обучения.
4. ReLU (Rectified Linear Unit):
f(x) = max(0, x)
Очень популярная в последнее время функция. Проста в вычислении, имеет ненулевой градиент для положительных значений x, что помогает избежать проблемы исчезающего градиента. Её недостаток — "мёртвые" нейроны: если нейрон "умер" (всегда выдаёт 0), градиент тоже становится 0, и нейрон перестаёт обучаться.
5. Leaky ReLU:
f(x) = max(αx, x), где α — малое положительное число (например, 0.01)
Модификация ReLU, которая решает проблему "мёртвых" нейронов, допуская небольшой отрицательный градиент.
В КуМире мы можем реализовать эти функции активации следующим образом:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| алг вещ Сигмоида(вещ x)
нач
знач := 1 / (1 + exp(-x))
кон
алг вещ Tanh(вещ x)
нач
знач := (exp(x) - exp(-x)) / (exp(x) + exp(-x))
кон
алг вещ ReLU(вещ x)
нач
если x > 0 то
знач := x
иначе
знач := 0
все
кон
алг вещ LeakyReLU(вещ x)
нач
если x > 0 то
знач := x
иначе
знач := 0.01 * x
все
кон |
|
Учитывая особенности КуМира, функция exp(x) может быть не реализована напрямую. В таком случае мы можем приблизить её с помощью ряда Тейлора или использовать более простые функции активации вроде ReLU.
В контексте обучения нейросети важно понимать ещё один математический аспект — производные функций активации. Они используются в алгоритме обратного распространения ошибки для вычисления градиентов и обновления весов. Вот производные некоторых функций активации:
1. Сигмоида: f'(x) = f(x) * (1 - f(x))
2. Tanh: f'(x) = 1 - f(x)²
3. ReLU: f'(x) = 1, если x > 0; 0, если x < 0
4. Leaky ReLU: f'(x) = 1, если x > 0; α, если x < 0
Реализация производных в КуМире:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| алг вещ ПроизводнаяСигмоиды(вещ x)
нач
вещ s := Сигмоида(x)
знач := s * (1 - s)
кон
алг вещ ПроизводнаяTanh(вещ x)
нач
вещ t := Tanh(x)
знач := 1 - t * t
кон
алг вещ ПроизводнаяReLU(вещ x)
нач
если x > 0 то
знач := 1
иначе
знач := 0
все
кон |
|
Ещё одним важным компонентом математической модели нейрона является смещение (bias). У многих начинающих возникает вопрос: зачем вообще нужен этот параметр? Смещение позволяет сдвигать функцию активации вдоль оси x. Без смещения активационная функция всегда проходила бы через начало координат. Представьте линейный порог: если все веса равны нулю, то без смещения нейрон всегда будет выдавать одно и то же значение, независимо от входов. Смещение добавляет гибкости, позволяя нейрону активироваться даже при нулевых входах. Математически смещение можно рассматривать как вес для фиктивного входа, всегда равного 1. Иногда так и реализуют: добавляют к вектору входов константу 1, а к вектору весов — соответствующий вес смещения.
Для реализации прямого распространения (forward pass) через слой нейронов нам потребуется объединить несколько нейронов. В матричной форме это можно записать так:
Y = f(W × X + B)
Где:
X — вектор входов
W — матрица весов (строка i содержит веса i-го нейрона)
B — вектор смещений
f — функция активации (применяется поэлементно)
Y — вектор выходов слоя
В КуМире мы реализуем это с помощью вложенных циклов:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
| алг СлойНейронов(вещ таб X[], вещ таб W[][], вещ таб B[], цел КолНейронов)
нач
вещ таб выходы[КолНейронов]
нц для i от 1 до КолНейронов
вещ сумма := B[i]
нц для j от 1 до длин(X)
сумма := сумма + X[j] * W[i][j]
кц
выходы[i] := Активация(сумма)
кц
знач := выходы
кон |
|
Как видно из приведённых примеров, математическая модель искусственного нейрона достаточно проста. Однако эта простота обманчива! Когда мы объединяем множество нейронов в сеть, возникает сложная, нелинейная система, способная аппроксимировать практически любую функцию. Именно это свойство делает нейросети таким мощным инструментом.
Для полноты картины стоит упомянуть ещё один математический аспект — функции потерь (loss functions). Они используются для оценки качества предсказаний нейросети и играют ключевую роль в процессе обучения. Распространенные функции потерь включают:
1. Среднеквадратичная ошибка (MSE): L = (1/n) * Σ(y_pred - y_true)²
2. Бинарная кросс-энтропия: L = -(y_true * log(y_pred) + (1 - y_true) * log(1 - y_pred))
3. Категориальная кросс-энтропия: L = -Σ(y_true * log(y_pred))
Выбор функции потерь зависит от типа решаемой задачи (регрессия, бинарная или многоклассовая классификация).
Виды функций активации в контексте КуМир
Теперь, когда мы разобрались с математическими основами нейронов, давайте подробнее рассмотрим функции активации и их реализацию в КуМире. Функция активации — это сердце искусственного нейрона, преобразующее линейную комбинацию входов в нелинейный выход. В КуМире есть свои особенности при реализации различных функций, которые стоит учесть. Начнём с самой простой — пороговой функции (или функции Хевисайда). Математически она записывается так:
f(x) = {
1, если x ≥ 0
0, если x < 0
}
В КуМире её реализация выглядит предельно просто:
Code | 1
2
3
4
5
6
7
8
| алг вещ ПороговаяФункция(вещ x)
нач
если x >= 0 то
знач := 1
иначе
знач := 0
все
кон |
|
Эта функция была исторически первой, но у неё есть критический недостаток: её производная равна нулю везде, кроме точки x = 0, где она не определена. Это делает невозможным использование градиентных методов обучения.
Сигмоидная функция решает эту проблему, представляя собой плавную S-образную кривую:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
| алг вещ Сигмоида(вещ x)
нач
| Защита от переполнения
если x > 100 то
знач := 1
иначе
если x < -100 то
знач := 0
иначе
знач := 1 / (1 + exp(-x))
все
все
кон |
|
Обратите внимание на проверку граничных значений. В КуМире может не быть встроенной защиты от переполнения, поэтому для больших по модулю значений x мы возвращаем предельные значения функции. Без этой защиты вызов exp(100) мог бы привести к ошибке.
Производная сигмоиды выражается через саму сигмоиду, что очень удобно при вычислениях:
Code | 1
2
3
4
5
| алг вещ ПроизводнаяСигмоиды(вещ x)
нач
вещ s := Сигмоида(x)
знач := s * (1 - s)
кон |
|
Гиперболический тангенс (tanh) похож на сигмоиду, но его значения лежат в диапазоне от -1 до 1, что иногда удобнее:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| алг вещ Tanh(вещ x)
нач
| В КуМире может не быть встроенной функции tanh,
| поэтому реализуем её через exp
если x > 20 то
знач := 1
иначе
если x < -20 то
знач := -1
иначе
вещ ex := exp(x)
вещ emx := exp(-x)
знач := (ex - emx) / (ex + emx)
все
все
кон |
|
Опять же, мы добавляем защиту от переполнения. Производная tanh также выражается через саму функцию:
Code | 1
2
3
4
5
| алг вещ ПроизводнаяTanh(вещ x)
нач
вещ t := Tanh(x)
знач := 1 - t*t
кон |
|
Функция ReLU (Rectified Linear Unit) стала очень популярной в последние годы благодаря своей простоте и эффективности:
Code | 1
2
3
4
5
6
7
8
| алг вещ ReLU(вещ x)
нач
если x > 0 то
знач := x
иначе
знач := 0
все
кон |
|
ReLU имеет очень простую производную:
Code | 1
2
3
4
5
6
7
8
| алг вещ ПроизводнаяReLU(вещ x)
нач
если x > 0 то
знач := 1
иначе
знач := 0
все
кон |
|
Однако у ReLU есть проблема "мёртвых нейронов": если нейрон начинает всегда выдавать отрицательные значения на выходе, то после применения ReLU он всегда будет выдавать 0, и его веса перестанут обновляться. Для решения этой проблемы разработаны модифицированные версии:
Leaky ReLU пропускает небольшую долю отрицательных значений:
Code | 1
2
3
4
5
6
7
8
| алг вещ LeakyReLU(вещ x, вещ alpha)
нач
если x > 0 то
знач := x
иначе
знач := alpha * x
все
кон |
|
Code | 1
2
3
4
5
6
7
8
| алг вещ ПроизводнаяLeakyReLU(вещ x, вещ alpha)
нач
если x > 0 то
знач := 1
иначе
знач := alpha
все
кон |
|
Параметр alpha обычно небольшой (например, 0.01).
Ещё одна интересная модификация — ELU (Exponential Linear Unit):
Code | 1
2
3
4
5
6
7
8
| алг вещ ELU(вещ x, вещ alpha)
нач
если x > 0 то
знач := x
иначе
знач := alpha * (exp(x) - 1)
все
кон |
|
Code | 1
2
3
4
5
6
7
8
| алг вещ ПроизводнаяELU(вещ x, вещ alpha)
нач
если x > 0 то
знач := 1
иначе
знач := alpha * exp(x)
все
кон |
|
В КуМире может возникнуть проблема с вычислением exp для больших аргументов, поэтому для практического использования ELU стоит добавить проверки, как мы делали для сигмоиды и tanh.
Для простых задач, особенно в контексте учебного проекта в КуМире, иногда бывает полезно использовать линейную функцию активации для выходного слоя:
Code | 1
2
3
4
| алг вещ Линейная(вещ x)
нач
знач := x
кон |
|
Code | 1
2
3
4
| алг вещ ПроизводнаяЛинейной(вещ x)
нач
знач := 1 | Производная константна и равна 1
кон |
|
Это особенно актуально для задач регрессии, где выходом может быть любое число, а не только значение из ограниченного диапазона.
Для задач классификации на выходном слое обычно используется функция Softmax, которая превращает вектор произвольных чисел в вектор вероятностей (положительные числа, сумма которых равна 1):
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| алг Softmax(вещ таб x[], цел n)
нач
вещ таб выход[n]
вещ сумма := 0
вещ maxVal := x[1] | Находим максимальное значение для численной стабильности
нц для i от 2 до n
если x[i] > maxVal то
maxVal := x[i]
все
кц
нц для i от 1 до n
выход[i] := exp(x[i] - maxVal)
сумма := сумма + выход[i]
кц
нц для i от 1 до n
выход[i] := выход[i] / сумма
кц
знач := выход
кон |
|
Обратите внимание на трюк с вычитанием максимального значения перед применением exp. Это необходимо для численной стабильности и предотвращения переполнения.
В контексте КуМира стоит отдельно упомянуть о возможных проблемах с точностью вычислений. КуМир — учебный язык, и его реализация может не обеспечивать высокую точность при работе с вещественными числами. Это может привести к накоплению ошибок округления при многократных итерациях обучения. Для минимизации таких проблем рекомендуется:
1. Избегать очень больших или очень маленьких значений весов.
2. Использовать проверки на граничные значения в функциях активации.
3. Применять нормализацию входных данных (приведение к диапазону [0, 1] или [-1, 1]).
4. Выбирать скорость обучения с учётом возможных проблем с точностью.
Также стоит помнить, что в КуМире может отсутствовать ряд стандартных математических функций, доступных в других языках программирования. В этом случае их придётся реализовать самостоятельно.
Например, экспоненту можно приблизить рядом Тейлора:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| алг вещ МояЭкспонента(вещ x)
нач
если x > 20 то
знач := 500000000 | Приблизительное значение для больших x
иначе
вещ результат := 1
вещ член := 1
цел n := 1
| Считаем первые 10 членов ряда
нц для i от 1 до 10
член := член * x / n
результат := результат + член
n := n + 1
кц
знач := результат
все
кон |
|
Такое приближение работает достаточно хорошо для малых значений x, но для больших стоит использовать асимптотические приближения или таблицы значений.
При выборе функции активации для своего проекта в КуМире следует учитывать несколько факторов:
1. Сложность реализации — для учебного проекта предпочтительнее функции с простой реализацией (ReLU, линейная).
2. Диапазон выходных значений — сигмоида (0, 1) или tanh (-1, 1).
3. Проблемы с обучением — избегать функций с исчезающим градиентом для глубоких сетей.
4. Вычислительная эффективность — exp и другие трансцендентные функции могут быть медленными.
В большинстве случаев для скрытых слоёв хорошим выбором будет ReLU из-за простоты и эффективности. Для выходного слоя выбор зависит от типа задачи: сигмоида для бинарной классификации, softmax для многоклассовой классификации, линейная функция для регрессии.
В следующем разделе мы перейдём от теории к практике и начнём реализацию полноценной нейросети в КуМире с использованием выбранных нами функций активации.
Реализация нейросети
Вот мы и добрались до самого интересного — практической реализации нейросети в КуМир! Теперь, когда мы разобрались с теоретическими основами и особенностями функций активации, приступим к написанию кода. В этом разделе мы создадим простую, но полноценную нейронную сеть типа многослойный персептрон. Наша нейросеть будет состоять из трёх слоёв: входной, скрытый и выходной. Для начала определимся с архитектурой и реализуем базовые компоненты.
Начнём с объявления основных структур данных. В КуМире нет объектно-ориентированного программирования, поэтому нам придётся использовать массивы для хранения весов и других параметров:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| | Основные параметры сети
цел ВходнойРазмер | Количество входных нейронов
цел СкрытыйРазмер | Количество нейронов в скрытом слое
цел ВыходнойРазмер | Количество выходных нейронов
| Веса и смещения
вещ таб ВесаСкрытого[100, 100] | Веса между входным и скрытым слоями
вещ таб СмещенияСкрытого[100] | Смещения скрытого слоя
вещ таб ВесаВыходного[100, 100] | Веса между скрытым и выходным слоями
вещ таб СмещенияВыходного[100] | Смещения выходного слоя
| Активации и производные
вещ таб ВыходыСкрытого[100] | Выходы скрытого слоя
вещ таб ВыходыВыходного[100] | Выходы выходного слоя
вещ таб ОшибкиВыходного[100] | Ошибки выходного слоя
вещ таб ОшибкиСкрытого[100] | Ошибки скрытого слоя |
|
Обратите внимание, что мы заранее выделили массивы с запасом (по 100 элементов). В реальных приложениях размеры массивов должны соответствовать фактическим размерам сети, но в учебных целях такой подход упростит нам жизнь.
Теперь реализуем функцию инициализации сети. При инициализации мы зададим случайные начальные значения весам и нулевые значения для смещений:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| алг ИнициализироватьСеть(цел входРазмер, цел скрытРазмер, цел выходРазмер)
нач
| Сохраняем размеры слоёв
ВходнойРазмер := входРазмер
СкрытыйРазмер := скрытРазмер
ВыходнойРазмер := выходРазмер
| Инициализируем веса случайными малыми значениями
нц для i от 1 до скрытРазмер
нц для j от 1 до входРазмер
| Множитель 0.1 чтобы веса были маленькими изначально
ВесаСкрытого[i, j] := (слчис(1000) / 1000 - 0.5) * 0.1
кц
СмещенияСкрытого[i] := 0 | Начальное смещение = 0
кц
нц для i от 1 до выходРазмер
нц для j от 1 до скрытРазмер
ВесаВыходного[i, j] := (слчис(1000) / 1000 - 0.5) * 0.1
кц
СмещенияВыходного[i] := 0 | Начальное смещение = 0
кц
кон |
|
Функция слчис(1000) генерирует случайное целое число от 0 до 999, которое мы преобразуем в число от -0.05 до 0.05. Такая инициализация помогает избежать насыщения нейронов на начальных этапах обучения.
Следующим шагом реализуем прямое распространение сигнала (forward propagation):
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| алг ПрямоеРаспространение(вещ таб вход[])
нач
| Вычисляем активацию скрытого слоя
нц для i от 1 до СкрытыйРазмер
вещ сумма := СмещенияСкрытого[i]
нц для j от 1 до ВходнойРазмер
сумма := сумма + вход[j] * ВесаСкрытого[i, j]
кц
| Применяем ReLU как функцию активации для скрытого слоя
ВыходыСкрытого[i] := ReLU(сумма)
кц
| Вычисляем активацию выходного слоя
нц для i от 1 до ВыходнойРазмер
вещ сумма := СмещенияВыходного[i]
нц для j от 1 до СкрытыйРазмер
сумма := сумма + ВыходыСкрытого[j] * ВесаВыходного[i, j]
кц
| Для выходного слоя используем сигмоиду
ВыходыВыходного[i] := Сигмоида(сумма)
кц
| Возвращаем предсказания - копируем в новый массив
вещ таб предсказания[ВыходнойРазмер]
нц для i от 1 до ВыходнойРазмер
предсказания[i] := ВыходыВыходного[i]
кц
знач := предсказания
кон |
|
В этой функции мы использовали ReLU для скрытого слоя и сигмоиду для выходного. Такой выбор обусловлен тем, что ReLU обычно работает хорошо для скрытых слоёв из-за отсутствия проблемы исчезающего градиента, а сигмоида подходит для выходного слоя, когда мы хотим получить значения между 0 и 1 (например, для вероятностей в задаче классификации).
Теперь реализуем алгоритм обратного распространения ошибки (backpropagation). Этот алгоритм состоит из двух основных шагов:
1. Вычисление ошибки для каждого нейрона.
2. Коррекция весов пропорционально этой ошибке.
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| алг ОбратноеРаспространение(вещ таб вход[], вещ таб целевые[], вещ скоростьОбучения)
нач
| Шаг 1: Вычисление ошибок выходного слоя
нц для i от 1 до ВыходнойРазмер
вещ ошибка := целевые[i] - ВыходыВыходного[i]
| Производная функции потерь (MSE) * производная сигмоиды
ОшибкиВыходного[i] := ошибка * ВыходыВыходного[i] * (1 - ВыходыВыходного[i])
кц
| Шаг 2: Вычисление ошибок скрытого слоя
нц для i от 1 до СкрытыйРазмер
вещ ошибка := 0
нц для j от 1 до ВыходнойРазмер
ошибка := ошибка + ОшибкиВыходного[j] * ВесаВыходного[j, i]
кц
| Производная ReLU = 1 если выход > 0, иначе 0
если ВыходыСкрытого[i] > 0 то
ОшибкиСкрытого[i] := ошибка
иначе
ОшибкиСкрытого[i] := 0
все
кц
| Шаг 3: Коррекция весов выходного слоя
нц для i от 1 до ВыходнойРазмер
| Корректируем смещение
СмещенияВыходного[i] := СмещенияВыходного[i] +
скоростьОбучения * ОшибкиВыходного[i]
| Корректируем веса
нц для j от 1 до СкрытыйРазмер
ВесаВыходного[i, j] := ВесаВыходного[i, j] +
скоростьОбучения * ОшибкиВыходного[i] * ВыходыСкрытого[j]
кц
кц
| Шаг 4: Коррекция весов скрытого слоя
нц для i от 1 до СкрытыйРазмер
| Корректируем смещение
СмещенияСкрытого[i] := СмещенияСкрытого[i] +
скоростьОбучения * ОшибкиСкрытого[i]
| Корректируем веса
нц для j от 1 до ВходнойРазмер
ВесаСкрытого[i, j] := ВесаСкрытого[i, j] +
скоростьОбучения * ОшибкиСкрытого[i] * вход[j]
кц
кц
кон |
|
В этом алгоритме мы использовали среднеквадратичную ошибку (MSE) в качестве функции потерь. Параметр скоростьОбучения определяет, насколько сильно будут меняться веса за одну итерацию. Слишком большое значение может привести к нестабильности, а слишком маленькое — к медленной сходимости.
Теперь объединим наши функции в единый процесс обучения:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| алг ОбучитьСеть(вещ таб входы[][], вещ таб целевые[][], цел колПримеров, цел эпохи, вещ скоростьОбучения)
нач
нц для эпоха от 1 до эпохи
вещ суммаОшибок := 0
| Проходим по всем обучающим примерам
нц для пример от 1 до колПримеров
| Получаем вход и ожидаемый выход для текущего примера
ПрямоеРаспространение(входы[пример])
| Вычисляем ошибку (MSE)
вещ ошибкаПримера := 0
нц для i от 1 до ВыходнойРазмер
ошибкаПримера := ошибкаПримера + (целевые[пример, i] - ВыходыВыходного[i])^2
кц
ошибкаПримера := ошибкаПримера / ВыходнойРазмер
суммаОшибок := суммаОшибок + ошибкаПримера
| Обновляем веса
ОбратноеРаспространение(входы[пример], целевые[пример], скоростьОбучения)
кц
| Выводим среднюю ошибку за эпоху
вещ средняяОшибка := суммаОшибок / колПримеров
вывод "Эпоха ", эпоха, ", ошибка: ", средняяОшибка
| Можно добавить условие раннего останова, если ошибка достаточно мала
если средняяОшибка < 0.001 то
вывод "Достигнута требуемая точность. Останов."
выход
все
кц
кон |
|
Наконец, давайте напишем функцию предсказания, которая будет использовать обученную сеть:
Code | 1
2
3
4
5
| алг Предсказать(вещ таб вход[])
нач
вещ таб предсказания[] := ПрямоеРаспространение(вход)
знач := предсказания
кон |
|
Всё вместе эти функции образуют минимальную, но работающую реализацию нейросети. Чтобы использовать нашу нейросеть, нужно:
1. Инициализировать её структуру.
2. Подготовить обучающие данные.
3. Запустить процесс обучения.
4. Использовать обученную сеть для предсказаний.
Вот пример использования нашей нейросети для решения задачи логического И (AND):
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| | Основная программа
алг
нач
| Создаём сеть с 2 входами, 3 нейронами в скрытом слое и 1 выходом
ИнициализироватьСеть(2, 3, 1)
| Подготавливаем обучающие данные для логического И
цел КолПримеров := 4
вещ таб входы[4, 2] := {{0, 0}, {0, 1}, {1, 0}, {1, 1}}
вещ таб выходы[4, 1] := {{0}, {0}, {0}, {1}}
| Обучаем сеть (100 эпох, скорость обучения 0.1)
ОбучитьСеть(входы, выходы, КолПримеров, 100, 0.1)
| Тестируем обученную сеть
вещ таб тест[2] := {1, 1}
вещ таб результат[] := Предсказать(тест)
вывод "Предсказание для [1, 1]: ", результат[1]
кон |
|
Этот код создаёт простую нейросеть, обучает её на примерах логического И, а затем использует для предсказания результата операции 1 AND 1.
В следующих разделах мы рассмотрим более сложные структуры данных для хранения параметров нейросети и разберём, как визуализировать её работу в КуМире.
Структуры данных для нейросети
При создании нейронной сети важно правильно организовать хранение её параметров. От выбора структур данных зависит как скорость работы, так и удобство реализации. В КуМире мы ограничены доступными типами данных, но даже с этими ограничениями можно создать эффективную организацию нашей нейросети. Основные параметры, которые нам нужно хранить:- веса связей между нейронами,
- значения смещений (bias),
- промежуточные результаты вычислений,
- градиенты для обучения.
Рассмотрим, как можно усовершенствовать нашу первоначальную реализацию, используя более гибкие структуры данных.
Вместо хардкода размеров массивов, создадим более универсальную структуру, которая позволит нам задавать произвольное количество слоёв и нейронов в них:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| | Количество слоёв в сети
цел КоличествоСлоёв
| Размеры каждого слоя (включая входной и выходной)
цел таб РазмерыСлоёв[10]
| Веса связей между слоями
вещ таб Веса[10, 100, 100]
| Смещения нейронов
вещ таб Смещения[10, 100]
| Выходы нейронов для каждого слоя
вещ таб Выходы[10, 100]
| Ошибки для каждого слоя
вещ таб Ошибки[10, 100] |
|
Здесь мы используем структуру с тремя измерениями для весов: первое измерение — номер слоя, второе — номер нейрона в текущем слое, третье — номер нейрона в предыдущем слое. Это позволит нам работать с сетью произвольной топологии, а не только с трёхслойной, как в предыдущем примере. Теперь модифицируем функцию инициализации:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| алг ИнициализироватьСеть(цел таб размеры[], цел количество)
нач
КоличествоСлоёв := количество
| Копируем размеры слоёв
нц для i от 1 до КоличествоСлоёв
РазмерыСлоёв[i] := размеры[i]
кц
| Инициализируем веса и смещения
нц для слой от 2 до КоличествоСлоёв
цел предыдущийРазмер := РазмерыСлоёв[слой - 1]
цел текущийРазмер := РазмерыСлоёв[слой]
нц для нейрон от 1 до текущийРазмер
| Инициализируем смещение
Смещения[слой, нейрон] := 0
| Инициализируем веса
нц для предНейрон от 1 до предыдущийРазмер
| Используем инициализацию Ксавье для лучшей сходимости
вещ диапазон := sqrt(6.0 / (предыдущийРазмер + текущийРазмер))
Веса[слой, нейрон, предНейрон] :=
(слчис(1000) / 500.0 - 1) * диапазон
кц
кц
кц
кон |
|
Обратите внимание, что мы используем инициализацию весов по методу Ксавье (Xavier initialization). Этот метод учитывает размеры соседних слоёв и помогает избежать проблем с затуханием или взрывом градиентов, что критично для обучения глубоких сетей. Обновим функцию прямого распространения для работы с произвольным количеством слоёв:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| алг ПрямоеРаспространение(вещ таб вход[])
нач
| Копируем входные значения в выходы первого слоя
нц для i от 1 до РазмерыСлоёв[1]
Выходы[1, i] := вход[i]
кц
| Проходим по всем слоям, начиная со второго
нц для слой от 2 до КоличествоСлоёв
нц для нейрон от 1 до РазмерыСлоёв[слой]
| Начинаем с bias
вещ сумма := Смещения[слой, нейрон]
| Суммируем взвешенные выходы предыдущего слоя
нц для предНейрон от 1 до РазмерыСлоёв[слой - 1]
сумма := сумма + Выходы[слой - 1, предНейрон] *
Веса[слой, нейрон, предНейрон]
кц
| Применяем функцию активации
если слой = КоличествоСлоёв то
| Для выходного слоя используем сигмоиду
Выходы[слой, нейрон] := Сигмоида(сумма)
иначе
| Для скрытых слоёв используем ReLU
Выходы[слой, нейрон] := ReLU(сумма)
все
кц
кц
| Копируем выходы последнего слоя в результат
вещ таб результат[РазмерыСлоёв[КоличествоСлоёв]]
нц для i от 1 до РазмерыСлоёв[КоличествоСлоёв]
результат[i] := Выходы[КоличествоСлоёв, i]
кц
знач := результат
кон |
|
Также потребуется обновить функцию обратного распространения:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
| алг ОбратноеРаспространение(вещ таб вход[], вещ таб цели[], вещ скорость)
нач
| Вычисляем ошибку и градиент для выходного слоя
цел выходнойСлой := КоличествоСлоёв
нц для нейрон от 1 до РазмерыСлоёв[выходнойСлой]
вещ выход := Выходы[выходнойСлой, нейрон]
вещ ошибка := цели[нейрон] - выход
| Градиент для сигмоиды: y * (1 - y)
Ошибки[выходнойСлой, нейрон] := ошибка * выход * (1 - выход)
кц
| Обратное распространение ошибки через скрытые слои
нц для слой от выходнойСлой - 1 до 2 шаг -1
нц для нейрон от 1 до РазмерыСлоёв[слой]
вещ ошибка := 0
| Суммируем взвешенные ошибки следующего слоя
нц для следНейрон от 1 до РазмерыСлоёв[слой + 1]
ошибка := ошибка + Ошибки[слой + 1, следНейрон] *
Веса[слой + 1, следНейрон, нейрон]
кц
| Градиент для ReLU: 1 если выход > 0, иначе 0
если Выходы[слой, нейрон] > 0 то
Ошибки[слой, нейрон] := ошибка
иначе
Ошибки[слой, нейрон] := 0
все
кц
кц
| Обновляем веса и смещения для всех слоёв
нц для слой от 2 до КоличествоСлоёв
нц для нейрон от 1 до РазмерыСлоёв[слой]
| Обновляем смещение
Смещения[слой, нейрон] := Смещения[слой, нейрон] +
скорость * Ошибки[слой, нейрон]
| Обновляем веса
нц для предНейрон от 1 до РазмерыСлоёв[слой - 1]
Веса[слой, нейрон, предНейрон] :=
Веса[слой, нейрон, предНейрон] +
скорость * Ошибки[слой, нейрон] *
Выходы[слой - 1, предНейрон]
кц
кц
кц
кон |
|
Для лучшей организации можно также создать функции для сохранения и загрузки параметров обученной сети, что особенно полезно для длительных процессов обучения:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| алг СохранитьСеть(лит имяФайла)
нач
лит сро
цел ф := СоздатьФайл(имяФайла)
| Записываем количество и размеры слоёв
вывод ф, КоличествоСлоёв
нц для i от 1 до КоличествоСлоёв
вывод ф, РазмерыСлоёв[i]
кц
| Записываем веса и смещения
нц для слой от 2 до КоличествоСлоёв
нц для нейрон от 1 до РазмерыСлоёв[слой]
вывод ф, Смещения[слой, нейрон]
нц для предНейрон от 1 до РазмерыСлоёв[слой - 1]
вывод ф, Веса[слой, нейрон, предНейрон]
кц
кц
кц
ЗакрытьФайл(ф)
кон
алг ЗагрузитьСеть(лит имяФайла)
нач
цел ф := ОткрытьФайл(имяФайла)
| Читаем количество и размеры слоёв
ввод ф, КоличествоСлоёв
нц для i от 1 до КоличествоСлоёв
ввод ф, РазмерыСлоёв[i]
кц
| Читаем веса и смещения
нц для слой от 2 до КоличествоСлоёв
нц для нейрон от 1 до РазмерыСлоёв[слой]
ввод ф, Смещения[слой, нейрон]
нц для предНейрон от 1 до РазмерыСлоёв[слой - 1]
ввод ф, Веса[слой, нейрон, предНейрон]
кц
кц
кц
ЗакрытьФайл(ф)
кон |
|
Для оптимизации процесса обучения полезно также реализовать пакетное обучение (batch learning). Вместо обновления весов после каждого примера, мы можем накапливать градиенты для группы примеров и затем применять их совместно:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
| | Для накопления градиентов
вещ таб ГрадиентыВесов[10, 100, 100]
вещ таб ГрадиентыСмещений[10, 100]
алг ОбучитьПакетом(вещ таб входы[][], вещ таб цели[][], цел размерПакета, вещ скорость)
нач
| Обнуляем градиенты
нц для слой от 2 до КоличествоСлоёв
нц для нейрон от 1 до РазмерыСлоёв[слой]
ГрадиентыСмещений[слой, нейрон] := 0
нц для предНейрон от 1 до РазмерыСлоёв[слой - 1]
ГрадиентыВесов[слой, нейрон, предНейрон] := 0
кц
кц
кц
| Накапливаем градиенты для всех примеров в пакете
нц для пример от 1 до размерПакета
| Прямое распространение
ПрямоеРаспространение(входы[пример])
| Вычисляем ошибки без обновления весов
ОбратноеРаспространение(входы[пример], цели[пример], 0)
| Накапливаем градиенты
нц для слой от 2 до КоличествоСлоёв
нц для нейрон от 1 до РазмерыСлоёв[слой]
ГрадиентыСмещений[слой, нейрон] :=
ГрадиентыСмещений[слой, нейрон] + Ошибки[слой, нейрон]
нц для предНейрон от 1 до РазмерыСлоёв[слой - 1]
ГрадиентыВесов[слой, нейрон, предНейрон] :=
ГрадиентыВесов[слой, нейрон, предНейрон] +
Ошибки[слой, нейрон] * Выходы[слой - 1, предНейрон]
кц
кц
кц
кц
| Применяем накопленные градиенты, делённые на размер пакета
вещ коэффициент := скорость / размерПакета
нц для слой от 2 до КоличествоСлоёв
нц для нейрон от 1 до РазмерыСлоёв[слой]
Смещения[слой, нейрон] := Смещения[слой, нейрон] +
коэффициент * ГрадиентыСмещений[слой, нейрон]
нц для предНейрон от 1 до РазмерыСлоёв[слой - 1]
Веса[слой, нейрон, предНейрон] := Веса[слой, нейрон, предНейрон] +
коэффициент *
ГрадиентыВесов[слой, нейрон, предНейрон]
кц
кц
кц
кон |
|
Другим полезным усовершенствованием будет реализация мини-пакетной нормализации (mini-batch normalization). Эта техника позволяет ускорить обучение и повысить стабильность, нормализуя активации внутри каждого мини-пакета:
Code | 1
2
3
4
5
| | Параметры для нормализации
вещ таб ПараметрыБета[10, 100] | Сдвиг
вещ таб ПараметрыГамма[10, 100] | Масштаб
вещ таб БегущееСреднее[10, 100] | Для использования при предсказании
вещ таб БегущаяДисперсия[10, 100] |
|
Этот код использует более эффективную организацию данных и вводит дополнительные оптимизации, которые приближают нашу учебную реализацию к реальным фреймворкам глубокого обучения. Конечно, КуМир имеет ограничения по скорости и возможностям, но наша цель — понимание принципов, а не создание высокопроизводительного решения.
Визуализация работы нейросети на КуМир
Создание нейронной сети — это только половина дела. Не менее важно уметь визуализировать её работу, чтобы лучше понять происходящие процессы и отладить возможные проблемы. В учебных целях визуализация особенно полезна, поскольку помогает наглядно представить абстрактные концепции работы нейросети.
КуМир имеет ограниченные возможности для визуализации по сравнению с Python и другими языками программирования, однако мы всё равно можем создать несколько полезных инструментов для наблюдения за работой нашей нейросети.
Начнём с простой визуализации весов нейронной сети в текстовом формате:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| алг ВизуализироватьВеса()
нач
нц для слой от 2 до КоличествоСлоёв
вывод "======== Слой ", слой, " ========"
нц для нейрон от 1 до РазмерыСлоёв[слой]
вывод "Нейрон ", нейрон, ":"
вывод " Смещение: ", Смещения[слой, нейрон]
вывод " Веса:"
нц для предНейрон от 1 до РазмерыСлоёв[слой - 1]
вывод " ", предНейрон, ": ", Веса[слой, нейрон, предНейрон]
кц
вывод ""
кц
кц
кон |
|
Эта функция просто выводит веса и смещения для всех нейронов во всех слоях. Но информация в таком виде не очень наглядна. Давайте создадим визуализацию, использующую графический исполнитель КуМира. Для начала научимся визуализировать активации нейронов при прямом проходе:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
| алг ВизуализироватьАктивации()
нач
| Устанавливаем размеры экрана
нс_создать(800, 600)
цел радиусНейрона := 20
цел отступПоГоризонтали := 150
цел отступПоВертикали := 60
| Проходим по всем слоям
нц для слой от 1 до КоличествоСлоёв
цел центрСлояX := слой * отступПоГоризонтали
| Вычисляем начальную позицию для слоя, чтобы центрировать его
цел начальныйY := 300 - (РазмерыСлоёв[слой] * отступПоВертикали) / 2
нц для нейрон от 1 до РазмерыСлоёв[слой]
цел центрНейронаY := начальныйY + (нейрон - 1) * отступПоВертикали
| Устанавливаем цвет в зависимости от активации
| Чем ближе к 1, тем ярче
если слой > 1 то
цел яркость := floor(Выходы[слой, нейрон] * 255)
нс_цвет_пера(RGB(яркость, 0, 0))
иначе
| Для входного слоя используем фиксированный цвет
нс_цвет_пера(RGB(0, 0, 255))
все
| Рисуем круг для нейрона
нс_окружность(центрСлояX, центрНейронаY, радиусНейрона)
| Если не первый слой, рисуем связи с нейронами предыдущего слоя
если слой > 1 то
цел центрПредСлояX := (слой - 1) * отступПоГоризонтали
цел начальныйПредY := 300 - (РазмерыСлоёв[слой - 1] * отступПоВертикали) / 2
нц для предНейрон от 1 до РазмерыСлоёв[слой - 1]
цел центрПредНейронаY := начальныйПредY + (предНейрон - 1) * отступПоВертикали
| Толщина линии зависит от веса соединения
вещ вес := Веса[слой, нейрон, предНейрон]
цел толщина := 1
если abs(вес) > 0.5 то
толщина := 3
иначе
если abs(вес) > 0.2 то
толщина := 2
все
все
| Цвет линии зависит от знака веса (красный - отрицательный, зелёный - положительный)
если вес < 0 то
нс_цвет_пера(RGB(255, 0, 0))
иначе
нс_цвет_пера(RGB(0, 255, 0))
все
нс_толщина_пера(толщина)
нс_линия(центрПредСлояX, центрПредНейронаY, центрСлояX, центрНейронаY)
кц
все
| Подписываем значения активаций
нс_цвет_пера(RGB(0, 0, 0))
нс_текст(центрСлояX + радиусНейрона + 5, центрНейронаY,
вещ_в_лит(Выходы[слой, нейрон], 2))
кц
кц
кон |
|
В этом коде мы рисуем нейроны в виде кругов, размещая их слоями. Цвет нейронов зависит от их активации — чем ярче, тем выше значение. Связи между нейронами представлены линиями, цвет и толщина которых зависят от весов связей. Для более наглядного наблюдения за процессом обучения, создадим функцию визуализации динамики ошибки:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
| | Массив для хранения истории ошибок
вещ таб ИсторияОшибок[1000]
цел ТекущаяЭпоха := 0
алг ДобавитьОшибку(вещ ошибка)
нач
если ТекущаяЭпоха < 1000 то
ТекущаяЭпоха := ТекущаяЭпоха + 1
ИсторияОшибок[ТекущаяЭпоха] := ошибка
все
кон
алг ВизуализироватьОшибки()
нач
| Устанавливаем размеры экрана
нс_создать(800, 400)
| Если нет ошибок, выходим
если ТекущаяЭпоха = 0 то
выход
все
| Находим максимальную ошибку для масштабирования
вещ максОшибка := ИсторияОшибок[1]
нц для i от 2 до ТекущаяЭпоха
если ИсторияОшибок[i] > максОшибка то
максОшибка := ИсторияОшибок[i]
все
кц
| Масштабирование для отображения на экране
вещ масштаб := 350 / максОшибка
| Рисуем оси координат
нс_цвет_пера(RGB(0, 0, 0))
нс_линия(50, 350, 750, 350) | Ось X
нс_линия(50, 350, 50, 50) | Ось Y
| Рисуем подписи осей
нс_текст(400, 370, "Эпоха")
нс_текст(20, 200, "Ошибка")
| Рисуем шкалу
нц для i от 0 до 10
вещ значение := i * максОшибка / 10
нс_текст(10, 350 - i * 30, вещ_в_лит(значение, 2))
нс_линия(45, 350 - i * 30, 55, 350 - i * 30)
кц
| Рисуем график ошибок
нс_цвет_пера(RGB(255, 0, 0))
нс_толщина_пера(2)
цел шагX := 700 / ТекущаяЭпоха
цел предX := 50
цел предY := 350 - floor(ИсторияОшибок[1] * масштаб)
нц для i от 2 до ТекущаяЭпоха
цел текX := 50 + (i - 1) * шагX
цел текY := 350 - floor(ИсторияОшибок[i] * масштаб)
нс_линия(предX, предY, текX, текY)
предX := текX
предY := текY
кц
кон |
|
Эта функция создаёт график, показывающий изменение ошибки по мере обучения. По оси X отложены эпохи обучения, по оси Y — значения ошибки. Для визуализации процесса предсказания создадим функцию, которая наглядно показывает результат работы нейросети при подаче конкретного входа:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| алг ВизуализироватьПредсказание(вещ таб вход[], вещ таб выход[])
нач
| Прогоняем вход через сеть
ПрямоеРаспространение(вход)
| Создаем окно для визуализации
нс_создать(600, 300)
| Выводим входные данные
нс_цвет_пера(RGB(0, 0, 0))
нс_текст(50, 50, "Входные данные:")
нц для i от 1 до РазмерыСлоёв[1]
нс_текст(50, 70 + i * 20, "Вход " + цел_в_лит(i) + ": " + вещ_в_лит(вход[i], 2))
кц
| Выводим предсказание и ожидаемый результат
нс_текст(300, 50, "Предсказание vs Ожидаемый результат:")
нц для i от 1 до РазмерыСлоёв[КоличествоСлоёв]
| Выводим предсказание
вещ предсказание := Выходы[КоличествоСлоёв, i]
| Окрашиваем в зависимости от близости к ожидаемому результату
вещ разница := abs(предсказание - выход[i])
| Чем меньше разница, тем ближе цвет к зелёному
цел красный := floor(min(255, разница * 500))
цел зелёный := floor(min(255, (1 - разница) * 255))
нс_цвет_пера(RGB(красный, зелёный, 0))
нс_текст(300, 70 + i * 20,
"Выход " + цел_в_лит(i) + ": " + вещ_в_лит(предсказание, 2) +
" (ожидалось: " + вещ_в_лит(выход[i], 2) + ")")
кц
кон |
|
Эта функция выводит входные данные и сравнивает предсказание сети с ожидаемым результатом, окрашивая текст в зависимости от точности предсказания.
Также полезно создать визуализацию изменений весов в процессе обучения:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
| | Сохраняем копию весов для отслеживания изменений
вещ таб ПредыдущиеВеса[10, 100, 100]
вещ таб ПредыдущиеСмещения[10, 100]
алг ЗапомнитьВеса()
нач
нц для слой от 2 до КоличествоСлоёв
нц для нейрон от 1 до РазмерыСлоёв[слой]
ПредыдущиеСмещения[слой, нейрон] := Смещения[слой, нейрон]
нц для предНейрон от 1 до РазмерыСлоёв[слой - 1]
ПредыдущиеВеса[слой, нейрон, предНейрон] := Веса[слой, нейрон, предНейрон]
кц
кц
кц
кон
алг ВизуализироватьИзмененияВесов()
нач
| Устанавливаем размеры экрана
нс_создать(800, 600)
вывод "Наиболее значительные изменения весов:"
| Проходим по всем слоям
нц для слой от 2 до КоличествоСлоёв
вывод "Слой ", слой, ":"
нц для нейрон от 1 до РазмерыСлоёв[слой]
вещ изменениеСмещения := abs(Смещения[слой, нейрон] - ПредыдущиеСмещения[слой, нейрон])
если изменениеСмещения > 0.01 то
вывод " Смещение нейрона ", нейрон, " изменилось на ", изменениеСмещения
все
нц для предНейрон от 1 до РазмерыСлоёв[слой - 1]
вещ изменениеВеса := abs(Веса[слой, нейрон, предНейрон] -
ПредыдущиеВеса[слой, нейрон, предНейрон])
если изменениеВеса > 0.01 то
вывод " Вес между нейронами ", предНейрон, " и ", нейрон,
" изменился на ", изменениеВеса
все
кц
кц
кц
| Сохраняем текущие веса для следующего сравнения
ЗапомнитьВеса()
кон |
|
Эта функция выводит информацию о наиболее значительных изменениях весов в процессе обучения, что помогает понять, какие связи сеть считает наиболее важными для решения задачи.
Наконец, для более глубокого понимания процесса обучения, создадим визуализацию градиентов:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| алг ВизуализироватьГрадиенты()
нач
| Устанавливаем размеры экрана
нс_создать(800, 600)
цел радиусНейрона := 20
цел отступПоГоризонтали := 150
цел отступПоВертикали := 60
| Проходим по всем слоям, начиная со второго (первый не имеет градиентов)
нц для слой от 2 до КоличествоСлоёв
цел центрСлояX := слой * отступПоГоризонтали
цел начальныйY := 300 - (РазмерыСлоёв[слой] * отступПоВертикали) / 2
нц для нейрон от 1 до РазмерыСлоёв[слой]
цел центрНейронаY := начальныйY + (нейрон - 1) * отступПоВертикали
| Рисуем окружность для нейрона
| Цвет зависит от величины градиента ошибки
вещ градиент := abs(Ошибки[слой, нейрон])
цел яркость := min(255, floor(градиент * 1000))
нс_цвет_пера(RGB(яркость, 0, яркость))
нс_окружность(центрСлояX, центрНейронаY, радиусНейрона)
| Подписываем значение градиента
нс_цвет_пера(RGB(0, 0, 0))
нс_текст(центрСлояX + радиусНейрона + 5, центрНейронаY,
вещ_в_лит(Ошибки[слой, нейрон], 3))
кц
кц
кон |
|
Эта функция визуализирует градиенты ошибки для каждого нейрона, показывая, насколько сильно нейрон влияет на ошибку сети. При разработке реальных приложений эти визуализации могут быть встроены в процесс обучения для мониторинга прогресса в режиме реального времени.
Обучение нейросети
Теперь, когда мы разобрались с архитектурой нейросети и структурами данных, переходим к самому интересному — процессу обучения. На этом этапе наша нейросеть начнёт "учиться", постепенно настраивая свои веса и смещения, чтобы выдавать верные ответы.
Обучение нейронной сети — это итеративный процесс, при котором сеть постепенно улучшает свою точность, анализируя примеры из обучающего набора данных. Для этого используется обратное распространение ошибки, которое мы уже реализовали ранее. Но перед тем как мы сможем обучать нашу сеть, нам нужно подготовить обучающую выборку — набор входных данных и соответствующих им ожидаемых выходов. В зависимости от задачи, обучающие данные могут сильно отличаться. Рассмотрим подготовку данных для нескольких типичных задач. Для задачи распознавания логических операций (XOR, AND, OR):
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| алг ПодготовитьДанныеЛогическиеОперации(цел выбор)
нач
цел колПримеров := 4
вещ таб входы[4, 2]
вещ таб выходы[4, 1]
| Заполняем входные данные - все возможные комбинации
входы[1, 1] := 0; входы[1, 2] := 0
входы[2, 1] := 0; входы[2, 2] := 1
входы[3, 1] := 1; входы[3, 2] := 0
входы[4, 1] := 1; входы[4, 2] := 1
| Заполняем выходы в зависимости от выбранной операции
выбор от
1: | XOR
выходы[1, 1] := 0
выходы[2, 1] := 1
выходы[3, 1] := 1
выходы[4, 1] := 0
2: | AND
выходы[1, 1] := 0
выходы[2, 1] := 0
выходы[3, 1] := 0
выходы[4, 1] := 1
3: | OR
выходы[1, 1] := 0
выходы[2, 1] := 1
выходы[3, 1] := 1
выходы[4, 1] := 1
иначе
вывод "Неверный выбор операции"
выход
все
знач := [входы, выходы, колПримеров]
кон |
|
Для задачи распознавания простых образов (например, цифр от 0 до 3 в виде маленьких изображений 3×3):
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| алг ПодготовитьДанныеЦифры()
нач
цел колПримеров := 4
вещ таб входы[4, 9] | 4 цифры, по 9 пикселей
вещ таб выходы[4, 4] | 4 цифры, one-hot кодирование
| Цифра 0 (круг)
входы[1, 1] := 1; входы[1, 2] := 1; входы[1, 3] := 1
входы[1, 4] := 1; входы[1, 5] := 0; входы[1, 6] := 1
входы[1, 7] := 1; входы[1, 8] := 1; входы[1, 9] := 1
| Цифра 1 (вертикальная линия)
входы[2, 1] := 0; входы[2, 2] := 1; входы[2, 3] := 0
входы[2, 4] := 0; входы[2, 5] := 1; входы[2, 6] := 0
входы[2, 7] := 0; входы[2, 8] := 1; входы[2, 9] := 0
| Цифра 2
входы[3, 1] := 1; входы[3, 2] := 1; входы[3, 3] := 1
входы[3, 4] := 0; входы[3, 5] := 0; входы[3, 6] := 1
входы[3, 7] := 1; входы[3, 8] := 1; входы[3, 9] := 1
| Цифра 3
входы[4, 1] := 1; входы[4, 2] := 1; входы[4, 3] := 1
входы[4, 4] := 0; входы[4, 5] := 1; входы[4, 6] := 1
входы[4, 7] := 1; входы[4, 8] := 1; входы[4, 9] := 1
| One-hot кодирование выходов
нц для i от 1 до 4
нц для j от 1 до 4
если i = j то
выходы[i, j] := 1
иначе
выходы[i, j] := 0
все
кц
кц
знач := [входы, выходы, колПримеров]
кон |
|
Теперь, когда у нас есть данные, давайте напишем основной цикл обучения. Мы будем использовать итеративный подход, многократно пропуская набор обучающих примеров через сеть и настраивая веса:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
| алг ОбучитьСеть(вещ таб входы[][], вещ таб целевые[][], цел колПримеров,
цел эпохи, вещ скоростьОбучения)
нач
| Инициализация массива для хранения истории ошибок
вещ таб историяОшибок[эпохи]
нц для эпоха от 1 до эпохи
вещ суммаОшибок := 0
| В каждой эпохе проходим через все обучающие примеры
нц для пример от 1 до колПримеров
| Извлекаем текущий пример
вещ таб вход[РазмерыСлоёв[1]]
нц для i от 1 до РазмерыСлоёв[1]
вход[i] := входы[пример, i]
кц
вещ таб цель[РазмерыСлоёв[КоличествоСлоёв]]
нц для i от 1 до РазмерыСлоёв[КоличествоСлоёв]
цель[i] := целевые[пример, i]
кц
| Прямое распространение
ПрямоеРаспространение(вход)
| Вычисляем среднеквадратичную ошибку для этого примера
вещ ошибкаПримера := 0
нц для i от 1 до РазмерыСлоёв[КоличествоСлоёв]
ошибкаПримера := ошибкаПримера +
(цель[i] - Выходы[КоличествоСлоёв, i])^2
кц
ошибкаПримера := ошибкаПримера / РазмерыСлоёв[КоличествоСлоёв]
суммаОшибок := суммаОшибок + ошибкаПримера
| Обратное распространение ошибки и коррекция весов
ОбратноеРаспространение(вход, цель, скоростьОбучения)
кц
| Вычисляем среднюю ошибку за эпоху
вещ средняяОшибка := суммаОшибок / колПримеров
историяОшибок[эпоха] := средняяОшибка
| Выводим статус обучения каждые 10 эпох
если эпоха mod 10 = 0 то
вывод "Эпоха ", эпоха, ": средняя ошибка = ", средняяОшибка
| Можно также вызвать функцию визуализации
| ВизуализироватьОшибки(историяОшибок, эпоха)
все
| Условие раннего останова
если средняяОшибка < 0.001 то
вывод "Достигнута требуемая точность. Останавливаем обучение."
вывод "Финальная эпоха: ", эпоха, ", ошибка: ", средняяОшибка
выход
все
кц
вывод "Обучение завершено. Финальная ошибка: ", историяОшибок[эпохи]
кон |
|
Но это не всё. В реальной практике часто используют более продвинутые методы обучения для улучшения сходимости и предотвращения переобучения. Давайте реализуем несколько таких методов.
Первый — перемешивание обучающих примеров (shuffling). Это помогает избежать циклических паттернов обучения:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| алг ПеремешатьДанные(вещ таб входы[][], вещ таб выходы[][], цел количество)
нач
| Перемешиваем данные случайным образом
нц для i от количество до 2 шаг -1
цел j := слчис(i) + 1 | Случайный индекс от 1 до i
| Меняем местами i-й и j-й примеры
вещ таб temp1[100] | Временный массив для обмена
вещ таб temp2[100]
| Копируем i-й пример во временный массив
нц для k от 1 до РазмерыСлоёв[1]
temp1[k] := входы[i, k]
кц
нц для k от 1 до РазмерыСлоёв[КоличествоСлоёв]
temp2[k] := выходы[i, k]
кц
| Копируем j-й пример в i-ю позицию
нц для k от 1 до РазмерыСлоёв[1]
входы[i, k] := входы[j, k]
кц
нц для k от 1 до РазмерыСлоёв[КоличествоСлоёв]
выходы[i, k] := выходы[j, k]
кц
| Копируем сохранённый i-й пример в j-ю позицию
нц для k от 1 до РазмерыСлоёв[1]
входы[j, k] := temp1[k]
кц
нц для k от 1 до РазмерыСлоёв[КоличествоСлоёв]
выходы[j, k] := temp2[k]
кц
кц
кон |
|
Второй метод — добавление момента (momentum). Момент помогает ускорить сходимость, добавляя "инерцию" к изменению весов:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| | Массивы для хранения предыдущих изменений весов (для момента)
вещ таб ПредыдущиеИзмененияВесов[10, 100, 100]
вещ таб ПредыдущиеИзмененияСмещений[10, 100]
алг ОбратноеРаспространениеСМоментом(вещ таб вход[], вещ таб цель[],
вещ скорость, вещ момент)
нач
| ... Вычисление градиентов как в обычном обратном распространении ...
| Обновление весов с учётом момента
нц для слой от 2 до КоличествоСлоёв
нц для нейрон от 1 до РазмерыСлоёв[слой]
| Вычисляем новое изменение для смещения
вещ изменениеСмещения := скорость * Ошибки[слой, нейрон] +
момент * ПредыдущиеИзмененияСмещений[слой, нейрон]
Смещения[слой, нейрон] := Смещения[слой, нейрон] + изменениеСмещения
ПредыдущиеИзмененияСмещений[слой, нейрон] := изменениеСмещения
нц для предНейрон от 1 до РазмерыСлоёв[слой - 1]
| Вычисляем новое изменение для веса
вещ изменениеВеса := скорость * Ошибки[слой, нейрон] *
Выходы[слой - 1, предНейрон] +
момент * ПредыдущиеИзмененияВесов[слой, нейрон, предНейрон]
Веса[слой, нейрон, предНейрон] := Веса[слой, нейрон, предНейрон] + изменениеВеса
ПредыдущиеИзмененияВесов[слой, нейрон, предНейрон] := изменениеВеса
кц
кц
кц
кон |
|
Третий важный метод — проверка на валидационном наборе данных. Это помогает определить момент, когда сеть начинает переобучаться:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| алг ОбучитьСвалидацией(вещ таб обучающиеВходы[][], вещ таб обучающиеВыходы[][],
цел колОбучающих, вещ таб валидационныеВходы[][],
вещ таб валидационныеВыходы[][], цел колВалидационных,
цел эпохи, вещ скорость)
нач
вещ лучшаяВалидационнаяОшибка := 1e9 | Очень большое число
цел эпохаЛучшейОшибки := 0
цел терпение := 20 | Количество эпох без улучшения, после которого останавливаемся
нц для эпоха от 1 до эпохи
| Обучение на обучающем наборе
ПеремешатьДанные(обучающиеВходы, обучающиеВыходы, колОбучающих)
нц для пример от 1 до колОбучающих
| ... обучение как обычно ...
кц
| Проверка на валидационном наборе
вещ валидационнаяОшибка := 0
нц для пример от 1 до колВалидационных
| ... вычисление ошибки без обучения ...
кц
валидационнаяОшибка := валидационнаяОшибка / колВалидационных
вывод "Эпоха ", эпоха, ": валидационная ошибка = ", валидационнаяОшибка
| Проверяем улучшение
если валидационнаяОшибка < лучшаяВалидационнаяОшибка то
лучшаяВалидационнаяОшибка := валидационнаяОшибка
эпохаЛучшейОшибки := эпоха
| Можно сохранить веса как лучшие
СохранитьСеть("лучшая_модель.txt")
все
| Проверяем условие раннего останова
если эпоха - эпохаЛучшейОшибки > терпение то
вывод "Раннай останов. Лучшая эпоха: ", эпохаЛучшейОшибки
выход
все
кц
кон |
|
Эти методы значительно улучшают процесс обучения нейросети, делая его более эффективным и устойчивым. Конечно, в КуМире реализация некоторых продвинутых методов может быть затруднена из-за ограничений языка, но базовые принципы можно реализовать вполне успешно.
При обучении нейросети важно также правильно выбрать гиперпараметры — такие как скорость обучения, момент, архитектуру сети (количество слоёв и нейронов), функции активации и т.д. Неправильный выбор может привести к тому, что сеть будет обучаться очень медленно или вообще не будет сходиться к решению. Чтобы более детально понять, как работает обучение нейросети, давайте рассмотрим практический пример. Возьмём задачу распознавания рукописных цифр на простой сетке 5×5 пикселей. Это упрощённая версия классической задачи MNIST, адаптированная под возможности КуМира.
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| | Функция для чтения данных цифр из файла
алг ПрочитатьДанныеЦифр(лит имяФайла, цел колЦифр)
нач
цел фид := ОткрытьФайл(имяФайла)
вещ таб входы[колЦифр, 25] | 5×5 пикселей
вещ таб выходы[колЦифр, 10] | 10 цифр (0-9)
нц для i от 1 до колЦифр
| Читаем пиксели изображения
нц для j от 1 до 25
цел пиксель
ввод фид, пиксель
входы[i, j] := пиксель
кц
| Читаем метку цифры
цел цифра
ввод фид, цифра
| Преобразуем в one-hot кодирование
нц для j от 1 до 10
если j = цифра + 1 то | +1 потому что индексы с 1
выходы[i, j] := 1
иначе
выходы[i, j] := 0
все
кц
кц
ЗакрытьФайл(фид)
знач := [входы, выходы]
кон |
|
В реальном приложении данные можно было бы загрузить из файла, но для примера создадим их прямо в коде:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| алг СоздатьТестовыеДанныеЦифр()
нач
цел колЦифр := 10 | По одному примеру каждой цифры
вещ таб входы[10, 25]
вещ таб выходы[10, 10]
| Цифра 0 (круг)
входы[1] := [0,1,1,1,0,
1,0,0,0,1,
1,0,0,0,1,
1,0,0,0,1,
0,1,1,1,0]
| Цифра 1 (вертикальная линия)
входы[2] := [0,0,1,0,0,
0,1,1,0,0,
0,0,1,0,0,
0,0,1,0,0,
0,1,1,1,0]
| И так далее для остальных цифр...
| Создаём one-hot кодирование выходов
нц для i от 1 до 10
нц для j от 1 до 10
если i = j то
выходы[i, j] := 1
иначе
выходы[i, j] := 0
все
кц
кц
знач := [входы, выходы, колЦифр]
кон |
|
Давайте теперь создадим полный процесс обучения для этой задачи:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
| алг ОсновнаяПрограмма()
нач
| Получаем данные
вещ таб данные[][] := СоздатьТестовыеДанныеЦифр()
вещ таб входы[][] := данные[1]
вещ таб выходы[][] := данные[2]
цел колПримеров := данные[3]
| Разделяем на обучающую и тестовую выборки
цел колОбучающих := 8
цел колТестовых := 2
вещ таб обучВходы[колОбучающих, 25]
вещ таб обучВыходы[колОбучающих, 10]
вещ таб тестВходы[колТестовых, 25]
вещ таб тестВыходы[колТестовых, 10]
| Копируем первые 8 примеров в обучающую выборку
нц для i от 1 до колОбучающих
нц для j от 1 до 25
обучВходы[i, j] := входы[i, j]
кц
нц для j от 1 до 10
обучВыходы[i, j] := выходы[i, j]
кц
кц
| Копируем последние 2 примера в тестовую выборку
нц для i от 1 до колТестовых
нц для j от 1 до 25
тестВходы[i, j] := входы[колОбучающих + i, j]
кц
нц для j от 1 до 10
тестВыходы[i, j] := выходы[колОбучающих + i, j]
кц
кц
| Создаем сеть с 25 входами (5×5 пикселей), 15 нейронами в скрытом слое и 10 выходами (по одному на цифру)
цел таб архитектура[3] := [25, 15, 10]
ИнициализироватьСеть(архитектура, 3)
| Обучаем сеть
ОбучитьСеть(обучВходы, обучВыходы, колОбучающих, 1000, 0.05)
| Тестируем сеть
вывод "Тестирование сети..."
цел правильных := 0
нц для i от 1 до колТестовых
вещ таб вход[25]
нц для j от 1 до 25
вход[j] := тестВходы[i, j]
кц
вещ таб результат[] := Предсказать(вход)
| Находим индекс максимального значения в результате (предсказанная цифра)
цел предсказано := 1
вещ макс := результат[1]
нц для j от 2 до 10
если результат[j] > макс то
макс := результат[j]
предсказано := j
все
кц
предсказано := предсказано - 1 | Вернуть к диапазону 0-9
| Находим истинную метку
цел истина := 0
нц для j от 1 до 10
если тестВыходы[i, j] = 1 то
истина := j - 1 | Настоящая цифра
выход
все
кц
вывод "Пример ", i, ": предсказано ", предсказано, ", истина ", истина
если предсказано = истина то
правильных := правильных + 1
все
кц
вещ точность := правильных * 100.0 / колТестовых
вывод "Точность на тестовой выборке: ", точность, "%"
| Визуализация решений нейросети
ВизуализироватьРешения(тестВходы, тестВыходы, колТестовых)
кон |
|
Для того чтобы увидеть, как работает наша нейросеть, добавим функцию визуализации растознавания цифр:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| алг ВизуализироватьРешения(вещ таб входы[][], вещ таб выходы[][], цел количество)
нач
нц для пример от 1 до количество
| Получаем текущий пример
вещ таб вход[25]
нц для i от 1 до 25
вход[i] := входы[пример, i]
кц
| Получаем предсказание сети
вещ таб результат[] := Предсказать(вход)
| Находим реальную цифру
цел реальнаяЦифра := 0
нц для i от 1 до 10
если выходы[пример, i] = 1 то
реальнаяЦифра := i - 1
выход
все
кц
| Выводим изображение цифры
вывод "Пример ", пример, " (цифра ", реальнаяЦифра, "):"
нц для строка от 0 до 4
лит стр := ""
нц для столбец от 0 до 4
цел индекс := строка * 5 + столбец + 1
если вход[индекс] > 0.5 то
стр := стр + "█"
иначе
стр := стр + " "
все
кц
вывод стр
кц
| Выводим вероятности для каждой цифры
вывод "Вероятности:"
нц для i от 1 до 10
вывод "Цифра ", i - 1, ": ", результат[i]
кц
вывод ""
кц
кон |
|
Интересно отметить, что даже с такой простой архитектурой и небольшим набором данных наша нейросеть способна научиться распознавать цифры с достаточно высокой точностью. Конечно, в реальных приложениях используются более сложные сети и гораздо больше данных, но принцип остаётся тем же.
Для повышения точности можно применить несколько техник:
1. Аугментация данных - создание дополнительных обучающих примеров путем небольших модификаций исходных данных (повороты, сдвиги, шум)
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| алг АугментироватьДанные(вещ таб входы[][], вещ таб выходы[][], цел количество)
нач
| Создаём новый набор данных вдвое большего размера
вещ таб новыеВходы[количество * 2, 25]
вещ таб новыеВыходы[количество * 2, 10]
| Копируем оригинальные данные
нц для i от 1 до количество
нц для j от 1 до 25
новыеВходы[i, j] := входы[i, j]
кц
нц для j от 1 до 10
новыеВыходы[i, j] := выходы[i, j]
кц
кц
| Создаём модифицированные версии (например, добавляем шум)
нц для i от 1 до количество
нц для j от 1 до 25
| Добавляем небольшой случайный шум
новыеВходы[количество + i, j] := макс(0, мин(1, входы[i, j] + (слчис(20) - 10) / 100.0))
кц
| Метки остаются теми же
нц для j от 1 до 10
новыеВыходы[количество + i, j] := выходы[i, j]
кц
кц
знач := [новыеВходы, новыеВыходы, количество * 2]
кон |
|
2. Регуляризация - методы для предотвращения переобучения, например L2-регуляризация
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| | Добавляем параметр регуляризации к функции обратного распространения
алг ОбратноеРаспространениеСРегуляризацией(вещ таб вход[], вещ таб цель[],
вещ скорость, вещ регуляризация)
нач
| ... Обычные вычисления градиентов ...
| Обновляем веса с учётом регуляризации
нц для слой от 2 до КоличествоСлоёв
нц для нейрон от 1 до РазмерыСлоёв[слой]
нц для предНейрон от 1 до РазмерыСлоёв[слой - 1]
| Добавляем штраф за большие веса
Веса[слой, нейрон, предНейрон] :=
Веса[слой, нейрон, предНейрон] * (1 - скорость * регуляризация) +
скорость * Ошибки[слой, нейрон] * Выходы[слой - 1, предНейрон]
кц
кц
кц
кон |
|
3. Dropout - случайное отключение нейронов во время обучения
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| | Массив для хранения масок dropout
лог таб МаскиDropout[10, 100]
алг ПрименитьDropout(вещ вероятность)
нач
| Создаём маски для каждого скрытого слоя
нц для слой от 2 до КоличествоСлоёв - 1 | Не применяем к выходному слою
нц для нейрон от 1 до РазмерыСлоёв[слой]
| Случайно определяем, активен ли нейрон
если слчис(100) / 100.0 < вероятность то
МаскиDropout[слой, нейрон] := да
иначе
МаскиDropout[слой, нейрон] := нет
все
кц
кц
кон
алг ПрямоеРаспространениеСDropout(вещ таб вход[], лог режимОбучения)
нач
| ... Обычное прямое распространение ...
| Если режим обучения, применяем dropout
если режимОбучения то
нц для слой от 2 до КоличествоСлоёв - 1
нц для нейрон от 1 до РазмерыСлоёв[слой]
если МаскиDropout[слой, нейрон] то
Выходы[слой, нейрон] := 0
все
кц
кц
все
кон |
|
Применение этих техник может значительно улучшить обобщающую способность нашей нейросети, особенно при работе с ограниченным набором данных. Конечно, в КуМире реализация некоторых из этих методов может быть не столь элегантной, как в специализированных фреймворках, но они всё равно будут работать и помогут лучше понять принципы обучения нейросетей.
В процессе обучения важно также отслеживать не только снижение ошибки на обучающей выборке, но и качество работы на валидационной и тестовой выборках. Это позволит вовремя заметить переобучение и принять меры для его устранения.
Подбор оптимальной скорости обучения
Скорость обучения (learning rate) — один из самых критичных гиперпараметров при обучении нейросети. От его правильного выбора зависит, насколько быстро и успешно сеть научится решать поставленную задачу. Если сравнивать с реальной жизнью, то скорость обучения показывает, насколько резко мы меняем своё мнение под влиянием новой информации.
Слишком маленькая скорость обучения приводит к медленной сходимости — сети требуется огромное количество эпох, чтобы достичь приемлемых результатов. А слишком большая может вызвать "перепрыгивание" через оптимальное решение, когда веса колеблются и не могут сойтись к минимуму функции потерь, а в худшем случае — к взрыву градиентов и полной дестабилизации обучения. Давайте реализуем несколько стратегий для подбора оптимальной скорости обучения в нашей нейросети на КуМире.
Фиксированная скорость обучения
Самый простой подход — выбрать одно значение и использовать его на протяжении всего обучения. Обычно хорошим начальным значением является 0.01 или 0.001. Реализация очень простая:
Code | 1
2
3
4
5
6
7
| алг ОбучитьСФиксированнойСкоростью(вещ таб входы[][], вещ таб цели[][],
цел колПримеров, цел эпохи, вещ скорость)
нач
нц для эпоха от 1 до эпохи
| ... стандартный цикл обучения с постоянной скоростью ...
кц
кон |
|
Но фиксированная скорость редко бывает оптимальной на всех этапах обучения. В начале обучения, когда градиенты обычно большие, высокая скорость может привести к быстрым изменениям в нужном направлении. А на поздних этапах, когда мы приближаемся к оптимуму, малая скорость даёт более точную настройку весов.
Экспоненциальное затухание скорости
Один из популярных подходов — начать с относительно высокой скорости и постепенно уменьшать её по экспоненциальному закону:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| алг ОбучитьСЗатуханием(вещ таб входы[][], вещ таб цели[][],
цел колПримеров, цел эпохи,
вещ начСкорость, вещ коэффЗатухания)
нач
нц для эпоха от 1 до эпохи
| Вычисляем текущую скорость обучения
вещ текСкорость := начСкорость * exp(-коэффЗатухания * эпоха)
| ... цикл обучения с вычисленной скоростью ...
| Выводим текущую скорость обучения
если эпоха mod 10 = 0 то
вывод "Эпоха ", эпоха, ", скорость обучения: ", текСкорость
все
кц
кон |
|
Этот метод позволяет начать с больших шагов для быстрого приближения к минимуму, а затем сделать шаги меньше для точной настройки.
Пошаговое снижение скорости
Ещё один распространённый метод — снижать скорость обучения в определённые моменты, например, когда ошибка перестаёт уменьшаться:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| алг ОбучитьСПошаговымСнижением(вещ таб входы[][], вещ таб цели[][],
цел колПримеров, цел эпохи,
вещ начСкорость, вещ коэффСнижения)
нач
вещ текСкорость := начСкорость
вещ предыдущаяОшибка := 1e9 | Очень большое число
цел безУлучшений := 0 | Счётчик эпох без улучшений
нц для эпоха от 1 до эпохи
| ... цикл обучения с текущей скоростью ...
| Вычисляем текущую ошибку
вещ текущаяОшибка := ВычислитьОшибку(входы, цели, колПримеров)
| Проверяем, улучшилась ли ошибка
если текущаяОшибка < предыдущаяОшибка - 0.0001 то
| Есть улучшение
безУлучшений := 0
иначе
| Нет улучшения
безУлучшений := безУлучшений + 1
все
| Если долго нет улучшений, снижаем скорость
если безУлучшений >= 5 то
текСкорость := текСкорость * коэффСнижения
безУлучшений := 0
вывод "Эпоха ", эпоха, ", снижена скорость до ", текСкорость
все
предыдущаяОшибка := текущаяОшибка
кц
кон |
|
Этот подход хорошо работает, когда мы не знаем заранее, на каких этапах обучения стоит снижать скорость. Вместо этого решение принимается динамически, основываясь на прогрессе обучения.
Циклическая скорость обучения
Одна из интересных современных стратегий — циклическая скорость обучения, которая периодически меняется между минимальным и максимальным значениями:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| алг ОбучитьСЦиклическойСкоростью(вещ таб входы[][], вещ таб цели[][],
цел колПримеров, цел эпохи,
вещ минСкорость, вещ максСкорость, цел периодЦикла)
нач
нц для эпоха от 1 до эпохи
| Вычисляем положение в текущем цикле (от 0 до 1)
вещ x := (эпоха mod периодЦикла) / периодЦикла
| Вычисляем текущую скорость по треугольной функции
вещ текСкорость
если x < 0.5 то
| Первая половина цикла - скорость растёт
текСкорость := минСкорость + (максСкорость - минСкорость) * 2 * x
иначе
| Вторая половина цикла - скорость падает
текСкорость := максСкорость - (максСкорость - минСкорость) * 2 * (x - 0.5)
все
| ... цикл обучения с вычисленной скоростью ...
если эпоха mod 10 = 0 то
вывод "Эпоха ", эпоха, ", скорость обучения: ", текСкорость
все
кц
кон |
|
Циклическая скорость обучения помогает выбраться из локальных минимумов и зачастую приводит к более быстрой сходимости и лучшим результатам. Кроме того, этот метод менее чувствителен к начальному выбору скорости обучения.
Автоматический подбор скорости обучения
Можно также реализовать процедуру для автоматического поиска оптимальной начальной скорости обучения. Метод заключается в том, чтобы начать с очень маленькой скорости и постепенно увеличивать её, отслеживая изменение ошибки:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| алг НайтиОптимальнуюСкорость(вещ таб входы[][], вещ таб цели[][],
цел колПримеров, вещ начСкорость, вещ коэффУвеличения)
нач
| Сохраняем исходные веса
СохранитьВеса("исходные_веса.txt")
вещ текСкорость := начСкорость
вещ минОшибка := ВычислитьОшибку(входы, цели, колПримеров)
вещ оптимальнаяСкорость := текСкорость
вывод "Начальная ошибка: ", минОшибка
нц пока текСкорость < 10.0 | Верхний предел поиска
| Выполняем одну эпоху обучения с текущей скоростью
ОбучитьОднуЭпоху(входы, цели, колПримеров, текСкорость)
| Вычисляем новую ошибку
вещ текОшибка := ВычислитьОшибку(входы, цели, колПримеров)
вывод "Скорость: ", текСкорость, ", ошибка: ", текОшибка
| Проверяем, не стала ли ошибка значительно хуже
если текОшибка > минОшибка * 2 или текОшибка = бесконечность то
| Ошибка резко выросла - скорость слишком большая
вывод "Ошибка резко возросла, останавливаем поиск"
выход
все
| Обновляем минимальную ошибку и оптимальную скорость
если текОшибка < минОшибка то
минОшибка := текОшибка
оптимальнаяСкорость := текСкорость
все
| Увеличиваем скорость обучения для следующей итерации
текСкорость := текСкорость * коэффУвеличения
| Восстанавливаем исходные веса для следующей итерации
ЗагрузитьВеса("исходные_веса.txt")
кц
вывод "Оптимальная начальная скорость обучения: ", оптимальнаяСкорость
знач := оптимальнаяСкорость
кон |
|
Этот метод позволяет автоматически найти скорость, при которой ошибка наиболее быстро уменьшается, что часто является хорошим выбором для начальной скорости обучения.
Практическое применение
Давайте теперь реализуем простой эксперимент, сравнивающий разные стратегии скорости обучения для задачи XOR:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
| алг СравнитьСкоростиОбучения()
нач
| Подготавливаем данные для XOR
вещ таб входы[4, 2] := {{0, 0}, {0, 1}, {1, 0}, {1, 1}}
вещ таб выходы[4, 1] := {{0}, {1}, {1}, {0}}
цел колПримеров := 4
| Параметры эксперимента
цел эпохи := 1000
цел повторений := 5 | Для усреднения результатов
| Стратегии скоростей обучения для сравнения
цел колСтратегий := 4
лит таб названияСтратегий[4] := {"Фиксированная", "Затухание",
"Пошаговое снижение", "Циклическая"}
| Массивы для хранения результатов
вещ таб финальныеОшибки[4, 5] | [стратегия, повторение]
цел таб эпохиСходимости[4, 5] | [стратегия, повторение]
| Проводим эксперименты
нц для стратегия от 1 до колСтратегий
вывод "Тестирование стратегии '", названияСтратегий[стратегия], "'"
нц для повторение от 1 до повторений
| Создаем новую сеть с одинаковой архитектурой
ИнициализироватьСеть([2, 4, 1], 3)
| Обучаем с выбранной стратегией
вещ финОшибка
цел эпохаСход
выбор стратегия
1: | Фиксированная скорость
[финОшибка, эпохаСход] := ОбучитьСФиксированной(
входы, выходы, колПримеров, эпохи, 0.1)
2: | Экспоненциальное затухание
[финОшибка, эпохаСход] := ОбучитьСЗатуханием(
входы, выходы, колПримеров, эпохи, 0.5, 0.01)
3: | Пошаговое снижение
[финОшибка, эпохаСход] := ОбучитьСПошаговымСнижением(
входы, выходы, колПримеров, эпохи, 0.2, 0.5)
4: | Циклическая
[финОшибка, эпохаСход] := ОбучитьСЦиклическойСкоростью(
входы, выходы, колПримеров, эпохи, 0.01, 0.5, 100)
все
| Сохраняем результаты
финальныеОшибки[стратегия, повторение] := финОшибка
эпохиСходимости[стратегия, повторение] := эпохаСход
кц
кц
| Анализируем результаты
вывод "Результаты эксперимента:"
вывод "Стратегия | Средняя ошибка | Среднее число эпох до сходимости"
вывод "-------------|----------------|----------------------------------"
нц для стратегия от 1 до колСтратегий
вещ суммаОшибок := 0
цел суммаЭпох := 0
нц для повторение от 1 до повторений
суммаОшибок := суммаОшибок + финальныеОшибки[стратегия, повторение]
суммаЭпох := суммаЭпох + эпохиСходимости[стратегия, повторение]
кц
вещ средняяОшибка := суммаОшибок / повторений
вещ средняяЭпоха := суммаЭпох / повторений
вывод названияСтратегий[стратегия], " | ", средняяОшибка, " | ", средняяЭпоха
кц
кон |
|
В этом эксперименте мы сравниваем четыре стратегии на задаче XOR, выполняя по пять запусков с каждой стратегией для усреднения результатов. Для каждой стратегии мы измеряем финальную ошибку и количество эпох, потребовавшихся для сходимости (достижения ошибки ниже определённого порога). Вот модифицированные функции обучения, возвращающие информацию о сходимости:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| алг ОбучитьСФиксированной(вещ таб входы[][], вещ таб цели[][],
цел колПримеров, цел эпохи, вещ скорость)
нач
вещ порогСходимости := 0.01 | Ошибка, при которой считаем, что сеть обучена
нц для эпоха от 1 до эпохи
| ... стандартный цикл обучения ...
| Вычисляем текущую ошибку
вещ ошибка := ВычислитьОшибку(входы, цели, колПримеров)
| Проверяем сходимость
если ошибка < порогСходимости то
вывод "Сходимость достигнута на эпохе ", эпоха
знач := [ошибка, эпоха]
выход
все
кц
| Если сходимость не достигнута
вещ финальнаяОшибка := ВычислитьОшибку(входы, цели, колПримеров)
вывод "Сходимость не достигнута, финальная ошибка: ", финальнаяОшибка
знач := [финальнаяОшибка, эпохи]
кон |
|
Аналогичные изменения вносятся и в остальные функции обучения.
Такое сравнение поможет выбрать оптимальную стратегию для конкретной задачи. На практике часто комбинируют разные методы — например, циклическую скорость обучения с общим экспоненциальным затуханием. Это позволяет получить преимущества обоих подходов. Важно понимать, что оптимальная скорость обучения сильно зависит от других факторов:- архитектуры сети (количества слоёв и нейронов),
- выбранных функций активации,
- инициализации весов,
- размера и особенностей обучающей выборки,
- применяемых методов регуляризации.
Поэтому универсального "идеального" значения не существует. В реальных проектах часто приходится экспериментировать с разными значениями и стратегиями, чтобы найти оптимальный вариант для конкретной задачи.
Мониторинг процесса обучения
Эффективное обучение нейронной сети невозможно без постоянного контроля за ходом этого процесса. Простого запуска алгоритма обучения и ожидания результата недостаточно — нужно внимательно отслеживать различные метрики, чтобы вовремя выявлять проблемы и корректировать параметры. В этом разделе мы рассмотрим, как организовать мониторинг процесса обучения нашей нейросети в КуМире. Начнём с реализации функции для мониторинга основных метрик во время обучения:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| алг МониторингОбучения(вещ таб историяОшибок[], вещ таб историяВалидацииОшибок[],
цел текущаяЭпоха, цел частотаВывода)
нач
| Вывод информации с заданной частотой
если текущаяЭпоха mod частотаВывода = 0 то
вещ обучающаяОшибка := историяОшибок[текущаяЭпоха]
вещ валидационнаяОшибка := историяВалидацииОшибок[текущаяЭпоха]
вывод "=== Эпоха ", текущаяЭпоха, " ==="
вывод "Ошибка на обучающей выборке: ", обучающаяОшибка
вывод "Ошибка на валидационной выборке: ", валидационнаяОшибка
| Проверка на переобучение (если валидационная ошибка растёт)
если текущаяЭпоха > частотаВывода то
вещ предыдущаяВалОшибка := историяВалидацииОшибок[текущаяЭпоха - частотаВывода]
если валидационнаяОшибка > предыдущаяВалОшибка * 1.05 то
вывод "ПРЕДУПРЕЖДЕНИЕ: Возможное переобучение. Валидационная ошибка растёт."
все
все
все
кон |
|
Эта функция выводит значения ошибок на обучающей и валидационной выборках с заданной периодичностью, а также предупреждает о возможном переобучении, когда ошибка на валидационной выборке начинает расти. Для более детального анализа добавим функцию, которая рассчитывает дополнительные метрики для задач классификации:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
| алг РассчитатьМетрикиКлассификации(вещ таб предсказания[], вещ таб истинныеЗначения[], цел количествоКлассов)
нач
| Инициализируем матрицу ошибок (confusion matrix)
цел таб матрицаОшибок[10, 10]
нц для i от 1 до 10
нц для j от 1 до 10
матрицаОшибок[i, j] := 0
кц
кц
| Заполняем матрицу ошибок
цел корректных := 0
цел всего := длин(предсказания) / количествоКлассов
нц для пример от 1 до всего
| Определяем предсказанный класс (с максимальной вероятностью)
цел предсказанныйКласс := 1
вещ максВероятность := предсказания[(пример - 1) * количествоКлассов + 1]
нц для класс от 2 до количествоКлассов
вещ текВероятность := предсказания[(пример - 1) * количествоКлассов + класс]
если текВероятность > максВероятность то
максВероятность := текВероятность
предсказанныйКласс := класс
все
кц
| Определяем истинный класс
цел истинныйКласс := 1
нц для класс от 1 до количествоКлассов
если истинныеЗначения[(пример - 1) * количествоКлассов + класс] > 0.5 то
истинныйКласс := класс
выход
все
кц
| Обновляем матрицу ошибок
матрицаОшибок[истинныйКласс, предсказанныйКласс] :=
матрицаОшибок[истинныйКласс, предсказанныйКласс] + 1
| Проверяем корректность
если предсказанныйКласс = истинныйКласс то
корректных := корректных + 1
все
кц
| Вычисляем метрики
вещ точность := корректных * 100.0 / всего | Accuracy
| Выводим результаты
вывод "=== Метрики классификации ==="
вывод "Точность (accuracy): ", точность, "%"
вывод "Матрица ошибок (confusion matrix):"
| Выводим матрицу ошибок
нц для i от 1 до количествоКлассов
лит строка := ""
нц для j от 1 до количествоКлассов
строка := строка + цел_в_лит(матрицаОшибок[i, j]) + " "
кц
вывод строка
кц
| Вычисляем точность и полноту для каждого класса
вывод "Метрики по классам:"
нц для класс от 1 до количествоКлассов
цел truePositives := матрицаОшибок[класс, класс]
цел falsePositives := 0
цел falseNegatives := 0
нц для i от 1 до количествоКлассов
если i <> класс то
falsePositives := falsePositives + матрицаОшибок[i, класс]
falseNegatives := falseNegatives + матрицаОшибок[класс, i]
все
кц
вещ precision := 0
если truePositives + falsePositives > 0 то
precision := truePositives * 100.0 / (truePositives + falsePositives)
все
вещ recall := 0
если truePositives + falseNegatives > 0 то
recall := truePositives * 100.0 / (truePositives + falseNegatives)
все
вещ f1 := 0
если precision + recall > 0 то
f1 := 2 * precision * recall / (precision + recall)
все
вывод "Класс ", класс, ":"
вывод " Точность (precision): ", precision, "%"
вывод " Полнота (recall): ", recall, "%"
вывод " F1-мера: ", f1
кц
кон |
|
Эта функция вычисляет не только общую точность (accuracy), но и другие важные метрики, такие как точность (precision), полноту (recall) и F1-меру для каждого класса. Матрица ошибок (confusion matrix) помогает понять, какие классы чаще всего путает наша нейросеть. Для визуализации прогресса обучения на КуМире, используя его графические возможности, создадим функцию для построения графиков:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
| алг ВизуализироватьПрогресс(вещ таб историяОбучения[], вещ таб историяВалидации[],
цел текущаяЭпоха)
нач
| Настраиваем окно для графики
нс_создать(800, 400)
нс_цвет_фона(RGB(255, 255, 255))
| Находим максимальное значение ошибки для масштабирования
вещ максОшибка := 0
нц для i от 1 до текущаяЭпоха
если историяОбучения[i] > максОшибка то
максОшибка := историяОбучения[i]
все
если историяВалидации[i] > максОшибка то
максОшибка := историяВалидации[i]
все
кц
| Добавляем запас сверху
максОшибка := максОшибка * 1.1
| Рисуем оси
нс_цвет_пера(RGB(0, 0, 0))
нс_линия(50, 50, 50, 350) | Ось Y
нс_линия(50, 350, 750, 350) | Ось X
| Подписи осей
нс_текст(400, 380, "Эпоха")
нс_текст(15, 200, "Ошибка")
| Отметки на оси Y
нц для i от 0 до 10
цел y := 350 - i * 30
нс_линия(45, y, 55, y)
вещ значение := i * максОшибка / 10
нс_текст(10, y, вещ_в_лит(значение, 3))
кц
| Отметки на оси X
цел шагПоX := min(текущаяЭпоха / 10, 100) | Не более 10 отметок
если шагПоX < 1 то
шагПоX := 1
все
нц для i от 0 до текущаяЭпоха шаг шагПоX
если i > 0 то | Пропускаем 0
цел x := 50 + i * (700.0 / текущаяЭпоха)
нс_линия(x, 345, x, 355)
нс_текст(x, 360, цел_в_лит(i))
все
кц
| Рисуем график обучающей ошибки (красный)
нс_цвет_пера(RGB(255, 0, 0))
нс_толщина_пера(2)
цел предX := 50
цел предY := 350 - floor(историяОбучения[1] * 300 / максОшибка)
нц для i от 2 до текущаяЭпоха
цел x := 50 + i * (700.0 / текущаяЭпоха)
цел y := 350 - floor(историяОбучения[i] * 300 / максОшибка)
нс_линия(предX, предY, x, y)
предX := x
предY := y
кц
| Рисуем график валидационной ошибки (синий)
нс_цвет_пера(RGB(0, 0, 255))
предX := 50
предY := 350 - floor(историяВалидации[1] * 300 / максОшибка)
нц для i от 2 до текущаяЭпоха
цел x := 50 + i * (700.0 / текущаяЭпоха)
цел y := 350 - floor(историяВалидации[i] * 300 / максОшибка)
нс_линия(предX, предY, x, y)
предX := x
предY := y
кц
| Добавляем легенду
нс_цвет_пера(RGB(255, 0, 0))
нс_линия(600, 70, 650, 70)
нс_цвет_пера(RGB(0, 0, 0))
нс_текст(660, 70, "Обучение")
нс_цвет_пера(RGB(0, 0, 255))
нс_линия(600, 90, 650, 90)
нс_цвет_пера(RGB(0, 0, 0))
нс_текст(660, 90, "Валидация")
кон |
|
Эта функция визуализирует процесс обучения, показывая динамику ошибок на обучающей и валидационной выборках. Такие графики особенно полезны для выявления переобучения — когда ошибка на обучающей выборке продолжает снижаться, но на валидационной начинает расти. Для детального анализа весов нейросети во время обучения создадим функцию, которая визуализирует распределение весов в каждом слое:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
| алг АнализВесов()
нач
вывод "=== Анализ весов нейросети ==="
| Находим базовую статистику весов по слоям
нц для слой от 2 до КоличествоСлоёв
вывод "Слой ", слой, ":"
вещ суммаВесов := 0
вещ суммаКвадратовВесов := 0
вещ минВес := 1e9
вещ максВес := -1e9
цел количествоВесов := РазмерыСлоёв[слой] * РазмерыСлоёв[слой - 1]
нц для нейрон от 1 до РазмерыСлоёв[слой]
нц для предНейрон от 1 до РазмерыСлоёв[слой - 1]
вещ вес := Веса[слой, нейрон, предНейрон]
суммаВесов := суммаВесов + вес
суммаКвадратовВесов := суммаКвадратовВесов + вес * вес
если вес < минВес то
минВес := вес
все
если вес > максВес то
максВес := вес
все
кц
кц
вещ средний := суммаВесов / количествоВесов
вещ дисперсия := суммаКвадратовВесов / количествоВесов - средний * средний
вещ стандОткл := sqrt(дисперсия)
вывод " Минимальный вес: ", минВес
вывод " Максимальный вес: ", максВес
вывод " Средний вес: ", средний
вывод " Стандартное отклонение: ", стандОткл
| Проверяем на проблему исчезающих/взрывающихся градиентов
если стандОткл < 0.01 то
вывод " ПРЕДУПРЕЖДЕНИЕ: Возможная проблема исчезающих градиентов!"
все
если стандОткл > 2.0 то
вывод " ПРЕДУПРЕЖДЕНИЕ: Возможная проблема взрывающихся градиентов!"
все
| Проверяем на мёртвые нейроны (для ReLU)
цел мёртвыхНейронов := 0
нц для нейрон от 1 до РазмерыСлоёв[слой]
лог активен := нет
нц для предНейрон от 1 до РазмерыСлоёв[слой - 1]
если Веса[слой, нейрон, предНейрон] <> 0 то
активен := да
выход
все
кц
если не активен то
мёртвыхНейронов := мёртвыхНейронов + 1
все
кц
если мёртвыхНейронов > 0 то
вывод " ПРЕДУПРЕЖДЕНИЕ: Обнаружено ", мёртвыхНейронов, " мёртвых нейронов!"
все
кц
кон |
|
Эта функция анализирует распределение весов в каждом слое нейросети и выявляет потенциальные проблемы, такие как мёртвые нейроны (характерны для функции активации ReLU), исчезающие или взрывающиеся градиенты. Теперь интегрируем эти функции мониторинга в общий алгоритм обучения:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
| алг ОбучитьСМониторингом(вещ таб обучВходы[][], вещ таб обучЦели[][],
цел колОбучающих, вещ таб валВходы[][],
вещ таб валЦели[][], цел колВалидационных,
цел эпохи, вещ скорость, цел частотаМониторинга)
нач
| Массивы для хранения истории ошибок
вещ таб историяОбучения[эпохи]
вещ таб историяВалидации[эпохи]
| Инициализируем лучшие веса
вещ лучшаяВалидОшибка := 1e9
цел эпохаБезУлучшений := 0
цел максЭпохБезУлучшений := 30 | Early stopping
нц для эпоха от 1 до эпохи
| Перемешиваем обучающие данные
ПеремешатьДанные(обучВходы, обучЦели, колОбучающих)
| Обучаем на всех примерах
нц для i от 1 до колОбучающих
| ... стандартный цикл обучения ...
кц
| Вычисляем ошибку на обучающей выборке
историяОбучения[эпоха] := ВычислитьОшибку(обучВходы, обучЦели, колОбучающих)
| Вычисляем ошибку на валидационной выборке
историяВалидации[эпоха] := ВычислитьОшибку(валВходы, валЦели, колВалидационных)
| Мониторинг прогресса
МониторингОбучения(историяОбучения, историяВалидации, эпоха, частотаМониторинга)
| Визуализация каждые 10 эпох
если эпоха mod 10 = 0 то
ВизуализироватьПрогресс(историяОбучения, историяВалидации, эпоха)
АнализВесов()
все
| Проверка на улучшение валидационной ошибки
если историяВалидации[эпоха] < лучшаяВалидОшибка то
лучшаяВалидОшибка := историяВалидации[эпоха]
СохранитьСеть("лучшая_сеть.txt")
эпохаБезУлучшений := 0
иначе
эпохаБезУлучшений := эпохаБезУлучшений + 1
все
| Early stopping
если эпохаБезУлучшений >= максЭпохБезУлучшений то
вывод "Раннее прекращение обучения на эпохе ", эпоха
вывод "Лучшая валидационная ошибка: ", лучшаяВалидОшибка
| Загружаем лучшие веса
ЗагрузитьСеть("лучшая_сеть.txt")
выход
все
кц
вывод "Обучение завершено. Лучшая валидационная ошибка: ", лучшаяВалидОшибка
| Загружаем лучшие веса
ЗагрузитьСеть("лучшая_сеть.txt")
кон |
|
Такой подход к мониторингу позволяет не только отслеживать прогресс обучения, но и автоматически выявлять и реагировать на различные проблемы, такие как переобучение или застревание в локальных минимумах.
В реальных проектах мониторинг процесса обучения играет критически важную роль, позволяя разработчикам принимать обоснованные решения о корректировке гиперпараметров, изменении архитектуры сети или использовании дополнительных техник регуляризации. Даже с ограниченными возможностями КуМира мы можем реализовать довольно мощные инструменты для анализа работы нашей нейросети.
Практические применения и ограничения
После создания и обучения нашей нейросети на КуМире пришло время проверить её в деле. Давайте рассмотрим несколько практических задач, которые можно решить даже с помощью нашей относительно простой реализации, а также обсудим существующие ограничения.
Наша нейросеть в КуМире хоть и является учебным проектом, но может использоваться для решения некоторых реальных задач. Конечно, сложность этих задач будет ограничена возможностями реализации, но всё же это хороший способ проверить работоспособность нашего творения. Начнём с простых задач классификации. Одной из базовых задач для тестирования нейросетей является классификация ирисов Фишера — набор данных, содержащий измерения длины и ширины чашелистиков и лепестков трёх видов ирисов. Эта задача стала своеобразной "Hello, World" в машинном обучении. Давайте реализуем решение этой задачи с помощью нашей нейросети:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
| алг ЗадачаИрисов()
нач
| Подготовка данных (упрощённо)
цел колПримеров := 150 | 50 примеров каждого из 3-х классов
вещ таб входы[150, 4] | 4 признака
вещ таб выходы[150, 3] | 3 класса в one-hot кодировании
| Загружаем данные (в реальности из файла)
| Класс 0: Iris-setosa
| Класс 1: Iris-versicolor
| Класс 2: Iris-virginica
...
| Нормализуем данные
НормализоватьДанные(входы, колПримеров, 4)
| Разделяем на обучающую и тестовую выборки
цел колОбучающих := 120
цел колТестовых := 30
вещ таб обучВходы[120, 4]
вещ таб обучВыходы[120, 3]
вещ таб тестВходы[30, 4]
вещ таб тестВыходы[30, 3]
| ... разделение данных ...
| Создаём сеть: 4 входа, 5 нейронов в скрытом слое, 3 выхода
цел таб архитектура[3] := [4, 5, 3]
ИнициализироватьСеть(архитектура, 3)
| Обучаем сеть
ОбучитьСеть(обучВходы, обучВыходы, колОбучающих, 1000, 0.01)
| Тестируем сеть
вещ правильныхПредсказаний := 0
нц для i от 1 до колТестовых
вещ таб вход[4]
нц для j от 1 до 4
вход[j] := тестВходы[i, j]
кц
вещ таб предсказание[] := Предсказать(вход)
| Находим класс с максимальной вероятностью
цел предсказанныйКласс := 1
вещ максВероятность := предсказание[1]
нц для j от 2 до 3
если предсказание[j] > максВероятность то
максВероятность := предсказание[j]
предсказанныйКласс := j
все
кц
| Находим истинный класс
цел истинныйКласс := 1
нц для j от 1 до 3
если тестВыходы[i, j] > 0.5 то
истинныйКласс := j
выход
все
кц
вывод "Пример ", i, ": предсказано - ", предсказанныйКласс,
", истина - ", истинныйКласс
если предсказанныйКласс = истинныйКласс то
правильныхПредсказаний := правильныхПредсказаний + 1
все
кц
вещ точность := правильныхПредсказаний * 100.0 / колТестовых
вывод "Точность на тестовой выборке: ", точность, "%"
кон |
|
Даже с такой небольшой нейросетью (всего 5 нейронов в скрытом слое), мы можем достичь точности около 90-95% на задаче классификации ирисов, что является впечатляющим результатом.
Другим интересным примером является задача распознавания рукописных цифр. Мы уже реализовали её в упрощённом виде (с 5×5 пикселями), но можно попробовать увеличить разрешение до 8×8 пикселей:
Code | 1
2
3
4
5
6
7
8
9
10
| алг ЗадачаРаспознаванияЦифр()
нач
| ... подготовка данных ...
| Создаём сеть: 64 входа (8×8 пикселей), 32 нейрона в скрытом слое, 10 выходов (цифры 0-9)
цел таб архитектура[3] := [64, 32, 10]
ИнициализироватьСеть(архитектура, 3)
| ... обучение и тестирование ...
кон |
|
Такая сеть, при правильном обучении, может достигать точности 70-80% на упрощённом наборе данных рукописных цифр.
Можно также попробовать задачу регрессии — предсказание числового значения вместо класса. Например, предсказание температуры на основе различных метеорологических параметров:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| алг ЗадачаПрогнозаПогоды()
нач
| ... подготовка данных ...
| Создаём сеть: 5 входов (давление, влажность, скорость ветра и т.д.),
| 8 нейронов в скрытом слое, 1 выход (температура)
цел таб архитектура[3] := [5, 8, 1]
ИнициализироватьСеть(архитектура, 3)
| Для задачи регрессии используем линейную функцию активации на выходном слое
| ... модификация функции прямого распространения ...
| ... обучение и тестирование ...
кон |
|
Хотя наша реализация нейросети может решать эти задачи, у неё есть ряд существенных ограничений:
1. Производительность. КуМир не оптимизирован для сложных вычислений, поэтому наша нейросеть работает значительно медленнее, чем реализации на специализированных языках и фреймворках, таких как Python/TensorFlow или PyTorch.
2. Масштабируемость. Наша реализация плохо масштабируется для больших наборов данных и сложных архитектур. Обучение на тысячах или миллионах примеров может занять неприемлемо много времени.
3. Ограниченные структуры данных. КуМир имеет ограниченный набор структур данных, что усложняет эффективную работу с большими объёмами информации.
4. Отсутствие аппаратного ускорения. Современные фреймворки нейросетей используют GPU или TPU для ускорения вычислений, что недоступно в КуМире.
5. Ограниченная архитектура. Наша реализация поддерживает только полносвязные слои, в то время как современные нейросети используют сверточные, рекуррентные и другие специализированные типы слоёв.
6. Проблемы с численной стабильностью. В КуМире могут возникать проблемы с точностью вычислений и переполнениями при работе с большими числами или при многократных итерациях.
Эти ограничения делают нашу реализацию нейросети на КуМире пригодной прежде всего для учебных целей, а не для реальных промышленных задач.
Сравним нашу нейросеть с профессиональными инструментами:
Code | 1
2
3
4
5
6
7
8
9
| | Характеристика | Нейросеть на КуМир | TensorFlow/PyTorch |
|----------------|-------------------|-------------------|
| Скорость обучения | Низкая | Высокая |
| Поддерживаемые архитектуры | Только MLP | Множество архитектур |
| Аппаратное ускорение | Нет | GPU, TPU, распределённые системы |
| Размер обрабатываемых данных | Малый | Практически неограничен |
| Оптимизаторы | SGD | SGD, Adam, RMSprop и др. |
| Регуляризация | Базовая | Множество техник |
| Удобство отладки | Ограниченное | Продвинутые инструменты | |
|
Но несмотря на эти ограничения, наша реализация имеет важное преимущество — она полностью прозрачна и помогает понять базовые принципы работы нейронных сетей, не отвлекаясь на сложности современных фреймворков.
Интересно отметить, что многие алгоритмы и идеи, которые мы реализовали в нашей простой нейросети, лежат в основе гораздо более сложных современных систем. Понимание этих базовых принципов даёт прочный фундамент для дальнейшего изучения более продвинутых концепций нейронных сетей. Кроме того, работа с ограничениями КуМира заставляет нас глубже думать об оптимизации алгоритмов и более эффективном использовании доступных ресурсов, что является ценным навыком для любого программиста.
В образовательном контексте наша реализация может использоваться для:- Демонстрации основных принципов работы нейронных сетей.
- Экспериментов с различными гиперпараметрами и их влиянием на обучение.
- Изучения алгоритма обратного распространения ошибки.
- Понимания проблем переобучения и способов их решения.
- Введения в базовые концепции машинного обучения.
Наша нейросеть на КуМире — это не конечный продукт, а скорее трамплин для дальнейшего изучения и экспериментов в области искусственного интеллекта и машинного обучения. Следующим логичным шагом после освоения нашей реализации было бы перейти к изучению более мощных инструментов, таких как Python с библиотеками NumPy, TensorFlow или PyTorch, которые открывают гораздо более широкие возможности для создания и обучения нейронных сетей.
Распознавание простых образов
Одной из самых интересных и практических задач для нашей нейросети является распознавание простых образов. Это прекрасный способ проверить, насколько хорошо наша учебная нейросеть справляется с задачами компьютерного зрения, пусть и в сильно упрощённом виде.
Начнём с распознавания цифр. Для нашей нейросети на КуМире логично использовать небольшие изображения, например, в формате 5×5 пикселей. Каждый пиксель будет представлять собой либо 0 (фон), либо 1 (часть цифры). Давайте создадим примеры для распознавания цифр от 0 до 3:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
| алг ПодготовитьДанныеЦифр()
нач
вещ таб цифра0[25] := {0,1,1,1,0,
1,0,0,0,1,
1,0,0,0,1,
1,0,0,0,1,
0,1,1,1,0}
вещ таб цифра1[25] := {0,0,1,0,0,
0,1,1,0,0,
0,0,1,0,0,
0,0,1,0,0,
0,1,1,1,0}
вещ таб цифра2[25] := {0,1,1,1,0,
1,0,0,0,1,
0,0,1,1,0,
0,1,0,0,0,
1,1,1,1,1}
вещ таб цифра3[25] := {0,1,1,1,0,
1,0,0,0,1,
0,0,1,1,0,
1,0,0,0,1,
0,1,1,1,0}
вещ таб обучающиеВходы[4, 25]
вещ таб обучающиеВыходы[4, 4]
| Копируем входные данные в массив
нц для i от 1 до 25
обучающиеВходы[1, i] := цифра0[i]
обучающиеВходы[2, i] := цифра1[i]
обучающиеВходы[3, i] := цифра2[i]
обучающиеВходы[4, i] := цифра3[i]
кц
| Создаем выходы в формате "one-hot encoding"
нц для i от 1 до 4
нц для j от 1 до 4
если i = j то
обучающиеВыходы[i, j] := 1
иначе
обучающиеВыходы[i, j] := 0
все
кц
кц
знач := [обучающиеВходы, обучающиеВыходы]
кон |
|
Теперь напишем функцию, которая визуализирует цифру, представленную в виде массива:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| алг ВизуализироватьЦифру(вещ таб цифра[])
нач
нц для строка от 0 до 4
лит визуализация := ""
нц для столбец от 0 до 4
цел индекс := строка * 5 + столбец + 1
если цифра[индекс] > 0.5 то
визуализация := визуализация + "█"
иначе
визуализация := визуализация + " "
все
кц
вывод визуализация
кц
кон |
|
Следующим шагом будет создание и обучение нейросети для распознавания этих цифр:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| алг РаспознавательЦифр()
нач
| Получаем данные
вещ таб данные[][] := ПодготовитьДанныеЦифр()
вещ таб входы[][] := данные[1]
вещ таб выходы[][] := данные[2]
| Создаем архитектуру: 25 входных нейронов (5x5 пикселей),
| 15 нейронов в скрытом слое и 4 выходных нейрона (для 4 цифр)
цел таб архитектура[3] := [25, 15, 4]
ИнициализироватьСеть(архитектура, 3)
| Обучаем сеть
вывод "Начинаем обучение..."
ОбучитьСеть(входы, выходы, 4, 1000, 0.1)
вывод "Обучение завершено!"
| Проверим работу сети на обучающих примерах
нц для i от 1 до 4
вещ таб вход[25]
нц для j от 1 до 25
вход[j] := входы[i, j]
кц
вывод "Распознаем цифру:"
ВизуализироватьЦифру(вход)
вещ таб результат[] := Предсказать(вход)
вывод "Результат распознавания:"
нц для j от 1 до 4
вывод "Вероятность цифры ", j-1, ": ", результат[j]
кц
| Находим максимальную вероятность
цел максИндекс := 1
вещ максВероятность := результат[1]
нц для j от 2 до 4
если результат[j] > максВероятность то
максВероятность := результат[j]
максИндекс := j
все
кц
вывод "Распознана цифра: ", максИндекс - 1
вывод ""
кц
кон |
|
После базового тестирования на обучающих данных, давайте создадим функцию для распознавания слегка искажённых версий цифр, чтобы проверить способность нашей сети к обобщению:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| алг ПроверкаСИскажениями()
нач
| Искаженная версия цифры 2 (с небольшим шумом)
вещ таб искаженная2[25] := {0,1,1,1,0,
1,0,0,0,0,
0,0,1,1,0,
0,1,0,0,1,
1,1,1,1,0}
вывод "Проверка распознавания искаженной цифры:"
ВизуализироватьЦифру(искаженная2)
вещ таб результат[] := Предсказать(искаженная2)
вывод "Результат распознавания:"
нц для j от 1 до 4
вывод "Вероятность цифры ", j-1, ": ", результат[j]
кц
| Находим максимальную вероятность
цел максИндекс := 1
вещ максВероятность := результат[1]
нц для j от 2 до 4
если результат[j] > максВероятность то
максВероятность := результат[j]
максИндекс := j
все
кц
вывод "Распознана цифра: ", максИндекс - 1
кон |
|
Интересно заметить, что наша нейросеть может справляться с небольшими искажениями, но её возможности ограничены количеством обучающих примеров и сложностью архитектуры. Для лучшего понимания процесса распознавания можно визуализировать активации нейронов скрытого слоя:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| алг ВизуализироватьАктивацииСлоя(цел слой)
нач
вывод "Активации нейронов слоя ", слой, ":"
нц для нейрон от 1 до РазмерыСлоёв[слой]
вещ активация := Выходы[слой, нейрон]
| Визуальное представление активации
лит индикатор := ""
цел длина := floor(активация * 20)
нц для i от 1 до 20
если i <= длина то
индикатор := индикатор + "█"
иначе
индикатор := индикатор + " "
все
кц
вывод "Нейрон ", нейрон, ": ", индикатор, " (", активация, ")"
кц
кон |
|
Помимо цифр, можно также научить нашу нейросеть распознавать простые буквы или геометрические фигуры:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| алг ПодготовитьДанныеФигур()
нач
вещ таб круг[25] := {0,1,1,1,0,
1,0,0,0,1,
1,0,0,0,1,
1,0,0,0,1,
0,1,1,1,0}
вещ таб квадрат[25] := {1,1,1,1,1,
1,0,0,0,1,
1,0,0,0,1,
1,0,0,0,1,
1,1,1,1,1}
вещ таб треугольник[25] := {0,0,1,0,0,
0,1,0,1,0,
0,1,0,1,0,
1,0,0,0,1,
1,1,1,1,1}
вещ таб крест[25] := {1,0,1,0,1,
0,1,1,1,0,
1,1,1,1,1,
0,1,1,1,0,
1,0,1,0,1}
| ... остальной код аналогичен функции для цифр ...
} |
|
Для более практического применения можно создать интерактивную программу, позволяющую пользователю "нарисовать" символ, который затем будет распознан нейросетью:
Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
| алг ИнтерактивноеРаспознавание()
нач
| Создаем пустое изображение 5x5
вещ таб изображение[25]
нц для i от 1 до 25
изображение[i] := 0
кц
нс_создать(300, 300)
нс_цвет_фона(RGB(255, 255, 255))
лог продолжать := да
пока продолжать цикл
| Отрисовываем текущее изображение
нс_очистить()
нц для строка от 0 до 4
нц для столбец от 0 до 4
цел индекс := строка * 5 + столбец + 1
если изображение[индекс] > 0.5 то
нс_цвет_пера(RGB(0, 0, 0))
иначе
нс_цвет_пера(RGB(200, 200, 200))
все
нс_прямоугольник(столбец * 50 + 50, строка * 50 + 50,
столбец * 50 + 90, строка * 50 + 90)
кц
кц
| Ждем клик пользователя
нс_текст(50, 20, "Нарисуйте символ, кликая на клетки.")
нс_текст(50, 300, "Нажмите 'R' для распознавания, 'C' для очистки, 'Q' для выхода.")
цел c := нс_клавиша()
если c = 'R' или c = 'r' то
| Распознаем изображение
вещ таб результат[] := Предсказать(изображение)
| ... отображаем результат распознавания ...
иначе
если c = 'C' или c = 'c' то
| Очищаем изображение
нц для i от 1 до 25
изображение[i] := 0
кц
иначе
если c = 'Q' или c = 'q' то
продолжать := нет
иначе
| Обрабатываем клик
цел x, y
нс_координаты_мыши(x, y)
| Преобразуем координаты в индекс
цел столбец := (x - 50) / 50
цел строка := (y - 50) / 50
если столбец >= 0 и столбец < 5 и строка >= 0 и строка < 5 то
цел индекс := строка * 5 + столбец + 1
| Инвертируем значение
если изображение[индекс] > 0.5 то
изображение[индекс] := 0
иначе
изображение[индекс] := 1
все
все
все
все
все
кц
кон |
|
Обучение нейросети на распознавание образов — это увлекательный процесс, который наглядно демонстрирует, как работает машинное обучение. Несмотря на примитивность нашей реализации, она способна уловить основные характеристики символов и делать достаточно точные предсказания. Интересно отметить, что уже на таком простом примере можно наблюдать многие явления, характерные для более сложных нейросетей: способность к обобщению (распознавание искажённых символов), переобучение (если мы будем слишком долго обучать сеть на малом наборе данных) и различия в эффективности разных архитектур (количества нейронов в скрытом слое).
Конечно, наша простая нейросеть имеет серьёзные ограничения. С увеличением размера изображения (например, до 10×10 или 28×28) производительность КуМира станет критическим фактором. Кроме того, для более сложных задач распознавания требуются специализированные архитектуры, такие как сверточные нейронные сети (CNN), которые гораздо эффективнее работают с изображениями, учитывая их пространственную структуру.
Направления для дальнейшего изучения
После создания простой нейросети на КуМир и понимания базовых принципов её работы логично задаться вопросом: куда двигаться дальше? Нейронные сети — огромная и активно развивающаяся область, и наш проект лишь слегка приоткрыл дверь в этот захватывающий мир. Рассмотрим несколько направлений, по которым можно продолжить свое путешествие в мире искусственного интеллекта.
Более сложные архитектуры нейросетей
Многослойный персептрон — лишь одна из многих архитектур нейронных сетей. Следующим шагом может стать изучение:- Сверточных нейронных сетей (CNN) — специализированных архитектур для обработки изображений. Они используют операцию свертки для эффективного извлечения признаков из изображений.
- Рекуррентных нейронных сетей (RNN) — сетей с обратными связями, хорошо подходящих для обработки последовательных данных, таких как текст или временные ряды. Особенно популярны варианты RNN, такие как LSTM (Long Short-Term Memory) и GRU (Gated Recurrent Unit).
- Трансформеров — архитектуры, основанной на механизме внимания (attention mechanism), которая произвела революцию в обработке естественного языка и стала основой для моделей вроде GPT и BERT.
- Автоэнкодеров — нейросетей, обучаемых воспроизводить свои входные данные на выходе, что позволяет использовать их для сжатия данных и обнаружения аномалий.
- Генеративно-состязательных сетей (GAN) — системы из двух конкурирующих нейросетей, способных генерировать новые данные, неотличимые от обучающих примеров.
Продвинутые методы обучения
По мере усложнения архитектур возникла необходимость в более эффективных методах обучения:- Продвинутые оптимизаторы: Adam, RMSprop, AdaGrad и другие алгоритмы, предлагающие более эффективную альтернативу стандартному градиентному спуску.
- Методы регуляризации: L1/L2-регуляризация, BatchNorm, LayerNorm, Dropout — техники, предотвращающие переобучение и улучшающие сходимость.
- Перенос обучения (transfer learning): подход, при котором сеть, обученная для одной задачи, используется как отправная точка для другой, смежной задачи.
- Дистилляция знаний (knowledge distillation): процесс передачи знаний от большой, сложной модели к более компактной.
Переход на профессиональные инструменты
После освоения базовых принципов на КуМир имеет смысл перейти к профессиональным инструментам:- Python с библиотеками NumPy, Pandas, Matplotlib для обработки и визуализации данных.
- TensorFlow или PyTorch — ведущие фреймворки для создания и обучения нейронных сетей, поддерживающие GPU-ускорение и автоматическое дифференцирование.
- Keras — высокоуровневый API для работы с нейросетями, позволяющий быстро прототипировать модели.
- Scikit-learn — библиотека для классического машинного обучения, полезная для понимания базовых алгоритмов.
Практические проекты
Лучший способ закрепить и углубить знания — применить их на практике:- Распознавание изображений: создание классификатора для определения объектов на фотографиях.
- Обработка естественного языка: разработка чат-бота или системы анализа эмоциональной окраски текстов.
- Прогнозирование временных рядов: предсказание цен акций, погоды или потребления электроэнергии.
- Рекомендательные системы: создание алгоритма, рекомендующего товары или контент на основе предпочтений пользователя.
Теоретические основы
Глубокое понимание нейронных сетей требует знания математических и теоретических основ:- Линейная алгебра: матричные операции, лежащие в основе большинства вычислений в нейросетях.
- Теория вероятностей и статистика: вероятностные модели, лежащие в основе многих задач машинного обучения.
- Методы оптимизации: математические основы алгоритмов, используемых для обучения нейросетей.
- Информационная теория: концепции, помогающие понять, как нейросети извлекают и кодируют информацию.
Этика и ответственность ИИ
По мере углубления в область искусственного интеллекта важно задуматься о этических аспектах этой технологии:- Предвзятость и справедливость: как убедиться, что наши модели не воспроизводят и не усиливают существующие общественные предубеждения?
- Прозрачность и объяснимость: как сделать решения нейросетей понятными для людей?
- Конфиденциальность данных: как защитить личную информацию при обучении моделей?
- Социальные и экономические последствия: как внедрение ИИ повлияет на общество и рынок труда?
Наш простой проект нейросети на КуМир — лишь первый шаг в долгом и увлекательном путешествии. Независимо от выбранного направления, основы, которые вы получили, создавая и обучая эту нейросеть, будут полезны на всём пути. Помните, что даже самые сложные современные нейронные сети основаны на тех же фундаментальных принципах, которые мы рассмотрели в этой статье.
Мир искусственных нейронных сетей и машинного обучения развивается с невероятной скоростью, и каждый день появляются новые исследования, инструменты и приложения. Поэтому саморазвитие и постоянное обучение — ключевые компоненты успеха в этой области. Удачи в ваших дальнейших исследованиях!
Данная статья является оригинальным образовательным материалом, объясняющим принципы работы нейронных сетей и их реализацию в среде программирования КуМир. В тексте не представлены прямые цитаты или ссылки на конкретные источники информации. Материал статьи основан на общей теории искусственных нейронных сетей, истории их развития, и представляет собой учебное пособие по программированию простой нейросети в образовательной среде КуМир.
Простая нейросеть Доброго времени суток!
Передо мной стоит задача создать простую нейросеть. Вот начальный код:
#include <iostream>
#include... Простая нейросеть XOR Всем доброго времени суток! Недавно начал затрагивать тему нейронных сетей и попробовал реализовать сетку, которая бы решала задачу исключающего или.... Простая нейросеть для новичков Здравствуйте уважаемые программисты Python.
Я - новичок.
Напишите мне, пожалуйста простую нейросеть. [КуМир] В системе КуМир сделать задание номер 23 помогите пожалуйста) задание номер 23) заранее благодарю) если не трудно можно блок схему еще)Текст задачи набирайте вручную. Для вставки формул есть... [КуМир] В программе Кумир написать алгоритм (Ссылка на сторонний ресурс удалена)
для 3 и 4 картинок
Рекомендую Вам ознакомиться с правилами форума. [КуМир] Перевести программу с C++ на Кумир Помогите нужно переписать программку с СИ++ на Кумир или на паскаль (но лучше на кумир)
сам код программы ... Свёрточные нейронные сети, создание и обучение Уважаемые форумчане, пытаюсь написать CNN, не могу разобраться с несколькими вопросами:
1. как выглядит график ненасыщаемой функции активации ... Глубокое обучение для создание качественного звука, клонирование голоса Есть две задачи, в одной нужен звук из текста - клонирование голоса высокого качества, во второй нужен звук - музыка из нот высокого качества. ... Дедуктивное обучение или Обучение по прецедентам (плюсы и минусы) Привет, друзья!
Как вы смотрите на то, чтобы обсудить вопрос о преимуществах и недостатках 2 типов обучения? Развернутой статьи не нашел на эту... Обучение модели нейронной сети(обучение с подкреплением) у меня есть код для реализации обучения модели с помощью алгоритма DDPG, но проблема в том что при обучении агент выбирает только максимально или... 1c обучение. Задание на создание отчетов Здравствуйте! Мне дали задание для практики перед зачетом по 1с.
1 - Отчеты. Механизмы компоновки данных. Пользовательские настройки отчета.
2 -... Создание оконного приложения (обучение) Добрый день.
Пытаюсь освоить с++
Установил vs express 2012, создал пустой проект с++ (не win32 или clr, а из раздела...
|