Документация Engee
Notebook

Регрессия при помощи нейросети (минимальный пример)

В этом примере мы обсудим, какой минимум операций позволяет обучить полносвязанную нейросеть (fully-connected, FC) задаче регрессии.

Описание задачи

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

In [ ]:
# @markdown ## Настройка параметров нейросети
# @markdown *(двойной клик позволяет скрыть код)*
# @markdown
Параметры_по_умолчанию = false #@param {type: "boolean"}
if Параметры_по_умолчанию
    Коэффициент_скорость_обучения = 0.01
    Количество_циклов_обучения = 100
else
    Количество_циклов_обучения = 80 # @param {type:"slider", min:1, max:150, step:1}
    Коэффициент_скорость_обучения = 0.1 # @param {type:"slider", min:0.001, max:0.5, step:0.001}
end

epochs = Количество_циклов_обучения;
learning_rate = Коэффициент_скорость_обучения;
In [ ]:
using Flux
Xs = Float32.( 0:0.1:10 );                    # Генерация данных для обучения
Ys = Float32.( Xs .+ 2 .* rand(length(Xs)) ); # <- ожидаемые от нейросети выходные данные
model = Dense( 1 => 1 )                       # Архитектура нейросети: один FC-слой
loss( x, y ) = Flux.mse( model( x ), y )      # Функция потерь (сумма ошибок на каждом элементе)
opt = ADAM( learning_rate );                  # Алгоритм оптимизации
data = [(Xs', Ys')];                          # В таком формате данные передаются в функцию loss
for i in 1:epochs
    Flux.train!( loss, Flux.params(model), data, opt )
end
X_прогноз = [ [x] for x in Xs ]               # Нейросеть принимает векторы, даже если у нас функция от одного аргумента
Y_прогноз = model.( X_прогноз )               # Для каждого [x] нейросеть вычисляет нам [y]
gr()                                          # Мы получили "вектор из векторов", который преобразуем для вывода на график
plot( Xs, Ys, label="Исходная выборка", legend=:topleft, lw=2 )
plot!( Xs, vec(hcat(Y_прогноз...)), label="Прогноз", lw=2 )
Out[0]:

Поменяйте параметры процесса обучения и перезапустите ячейку при помощи кнопки чтобы оценить, как изменение настроек повлияет на качество прогноза.

Создание нейросети в виде блоков на холсте

Наша нейросеть имеет настолько простую структуру, что ее очень легко перенести "на холст" и использовать в собственной библиотеке блоков.

👉 Эту модель модель можно запускать независимо от скрипта. В "обратных вызовах" модели записан весь код для обучения нейросети, поэтому при первом открытии файла neural_regression_simple.engee, если переменной model еще не существует, нейросеть обучается заново.

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

image.png

Запустим эту модель и сравним результаты:

In [ ]:
if "neural_regression_simple"  getfield.(engee.get_all_models(), :name)
  engee.load( "$(@__DIR__)/neural_regression_simple.engee");
end

data = engee.run( "neural_regression_simple" );

# Поскольку в модели все операции у нас матричные, нам снова приходится "разглаживать" переменную Y
plot!( data["Y"].time, vec(hcat(data["Y"].value...)), label="Блок regression_net", lw=2 )
Out[0]:

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

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

Пояснения к коду

Рассмотрим наш короткий код и прокомментируем интересные моменты.

Мы используем Float32 вместо Float64, который по умолчанию используется в Julia (без этого всё будет работать, но библиотека Flux выдаст однократное предупреждение).

Xs = Float32.( 0:0.1:10 );
Ys = Float32.(Xs .+ 2 .* rand(length(Xs)));

Точности Float32 более чем достаточно для нейросетей, ошибки их прогноза обычно превышают ошибку округления из-за более грубой разрядной сетки. К тому же выполнение на GPU, когда мы до него дойдем, происходит быстрее с этим типом данных.

Нейросеть состоит из одного элемента – линейной комбинации входов и весов с добавлением смещений (без функции активации, или, что то же самое, с линейной функцией активации). Мы даже не стали окружать объект Dense конструкцией Chain(), которая обычно используется для создания многослойных нейросетей (хотя и так, и так, сеть работает одинаково).

model = Dense( 1 => 1 )

Функция потерь – единственное место, где модель явно выполняется в ходе обучения. Функция потерь (loss функция) обычно выражается через сумму ошибок на каждом элементе данных (сумму cost функций), но мы просто используем стандартную функцию среднеквадратичной ошибки (mean squared error, MSE) из библиотеки.

loss( x, y ) = Flux.mse( model( x ), y )

Выбираем оптимизатор Adam (Adaptive Moment Estimation) – один из самых эффективных алгоритмов оптимизации в обучении нейронных сетей. Единственный параметр, который мы ему передаем – коэффициент скорости обучения.

opt = ADAM( learning_rate );

Мы готовим данные для подачи в функцию loss(). Функция train!() организует итерацию по входному вектору значений, внутри которых должны быть расположены кортежи (Tuple) со столбцами данных. Есть несколько других способов подачи данных, пока мы остановимся на том, который приведен ниже.

data = [(Xs', Ys')];

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

for i in 1:epochs
    Flux.train!( loss, Flux.params(model), data, opt )
end

Почему функция train! принимает на вход не модель, а только ее параметры (Flux.params(model))? Потому что не все параметры обязательно нужно обучать, некоторые можно заморозить. Можно передать model в качестве второго аргумента, но нужно будет немного поменять настройку оптимизатора (система скажет, как именно).

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

Y_прогноз = model.( X_прогноз )
X_прогноз = [ [x] for x in Xs ]

Заключение

Нам понадобилось 10 строчек кода чтобы сгенерировать данные и обучить нейросеть, еще 5 чтобы вывести ее прогнозы на график. Небольшие изменения позволят сделать нейросеть многослойной или обучить ее на данных из таблицы XLSX.

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

Блоки, использованные в примере