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

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

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

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

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

In [ ]:
Pkg.add(["Flux"])
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)) ); # <- ожидаемые от нейросети выходные данные
data = [(Xs', Ys')];                          # В таком формате данные передаются в функцию loss
model = Dense( 1 => 1 )                       # Архитектура нейросети: один FC-слой
opt_state = Flux.setup( Adam( learning_rate ), model ); # Алгоритм оптимизации
for i in 1:epochs
    Flux.train!( model, data, opt_state) do m, x, y
        Flux.mse( m(x), y ) # Функция потерь - ошибка на каждом элементе датасета
    end
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, когда мы до него дойдем, происходит быстрее с этим типом данных.

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

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

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

model = Dense( 1 => 1 )

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

opt_state = Flux.setup( Adam( learning_rate ), model_cpu )

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

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

for i in 1:epochs
    Flux.train!(model, data, opt_state) do m, x, y
        Flux.mse( m(x), y )
    end
end

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

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

Заключение

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

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

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