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

Код на Си из суррогатной нейросети

Представляем проект простого генератора для полносвязанных нейросетей, позволяющего поместить нейросеть в блок кода Engee или в микроконтроллер.

Подготовка окружения

Установите необходимые библиотеки:

In [ ]:
# Pkg.add(["ChainPlots", "Flux"])
using DataFrames, CSV, Flux, Random, Statistics
using Flux: mse
using ChainPlots
gr();

Мы собрали данные для некоторого диапазона входных значений и сохранили выходные значения модели из проекта Система топливных элементов автора shestakoviktor. Для изучения предшествующих шагов обратитесь к проекту Обучение суррогатной нейросети.

Информация о входах и выходах нам понадобится и в текущем проекте, поэтому повторим декларацию входных и выходных данных.

In [ ]:
out_vars = ["Мощность кВт", "Voltage Sensor.V", "Топливный элемент.thermal_port.T", "Топливный элемент.i_FC"]
v1 = Dict(:block=>"FuelCell/Давление водорода (бар)",:param=>"Value", :units=>"", :delta=>2.0, :lower=>2.0, :upper=>20.0)
v2 = Dict(:block=>"FuelCell/Подача топлива",:param=>"slope", :units=>"", :delta=>10/60, :lower=>50/60, :upper=>150/60)
adj_vars = vcat(DataFrame.([v1, v2])...)

n_features, n_targets = nrow(adj_vars), length(out_vars)
Out[0]:
(2, 4)

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

Обучение суррогатной нейросети

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

In [ ]:
include("$(@__DIR__)/scripts/create_plots.jl");
include("$(@__DIR__)/scripts/prepare_data.jl");
include("$(@__DIR__)/scripts/plot_predictions.jl");
In [ ]:
l1_neurons = 11 # @param {type:"slider",min:1,max:30,step:1}
l2_neurons = 8 # @param {type:"slider",min:1,max:30,step:1}
l3_neurons = 8 # @param {type:"slider",min:1,max:30,step:1}
l4_neurons = 8 # @param {type:"slider",min:1,max:30,step:1}
l5_neurons = 8 # @param {type:"slider",min:1,max:30,step:1}
n_epochs = 1500 # @param {type:"slider",min:1,max:1500,step:1}
learning_rate_base = 1 # @param {type:"slider",min:1,max:9,step:1}
learning_rate_exp = -3 # @param {type:"slider",min:-6,max:1,step:1}

models_list = []
loss_list = []
loss_plot_list = []
for i = 1:5
    include("$(@__DIR__)/scripts/prepare_and_train_net.jl")
    append!(models_list, [model])
    append!(loss_plot_list, [train_losses])
    append!(loss_list, loss(model, X_train_norm, y_train_norm))
end

min_id = findmin(loss_list)[2]
train_losses = loss_plot_list[min_id]
model = models_list[min_id]
best_loss = loss_list[min_id]

# График 1: Кривые обучения
p_loss = plot(1:n_epochs, train_losses, label="Train Loss", lw=2, marker=:circle, markersize=2)
title!(p_loss, "Динамика обучения (нормализованный MSE)")
xlabel!(p_loss, "Эпоха")
ylabel!(p_loss, "MSE")

println("Полученное значение ошибки (MSE): ", best_loss)

# Графики поверхностей для всех целевых переменных
surface_plots = [plot_predictions(model, X_train_norm, y_train_norm, 
                   X_train_mean, X_train_std, y_train_mean, y_train_std, i,
                   title=out_vars[i]) 
       for i in 1:n_targets]

#surface_plots = create_plots(predict_df, :reds, out_var, in_vars, result_df) for out_var in out_vars]

# Отображаем все графики вместе
plot(plot(p_loss, plot(surface_plots...)), plot(model), layout=(2,1), size=(1200,900))
Полученное значение ошибки (MSE): 0.00035328887
Out[0]:
No description has been provided for this image

Генерация кода для нейросети и настройка модели

Мы превратили данные в алгоритм, все коэффициенты модели расположены в нейросети model. Поскольку эта нейросеть содержит только полносвязанные слои, нам будет легко сгенерировать из неё переносимый код на Си:

In [ ]:
include("$(@__DIR__)/scripts/generate_inline_c_code.jl");
In [ ]:
cd(@__DIR__)
engee.open("nnet_model.engee")
engee.set_param!( "nnet_model/C Function", "InputPort1Size" => "($n_features,)")
engee.set_param!( "nnet_model/C Function", "OutputPort1Size" => "($n_targets,)")
engee.set_param!( "nnet_model/C Function", "OutputCode" => generate_inline_c_code(model, n_features, n_targets))

Этот код автоматически помещается в следующую модель, где выполняется нормализация входных данных и обратная операция для выходных данных алгоритма:

image.png

Проверим полученную модель

Чтобы проверить качество этой нейросети прямо на целевой модели, выполним ее на всех точках исходной таблицы и проверим прогнозы:

In [ ]:
predict_df = CSV.read("$(@__DIR__)/data/outputfile.csv", DataFrame)
predict_df = filter(row -> all(isfinite, row), predict_df)

in_vars = names(predict_df)[1:n_features];
out_vars = names(predict_df)[n_features+1:end];

for var in out_vars predict_df[!, Symbol(var)] = missings(Float64, nrow(predict_df)); end

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

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

In [ ]:
for row in eachrow(predict_df)
    const_value = "[" * join([string(row[col]) for col in names(predict_df)[1:n_features]], ',') * "]"
    engee.set_param!( "nnet_model/Константа", "Value"=>const_value )
    data = engee.run()
    for (i,var) in enumerate(out_vars) row[Symbol(var)] = data["Y"].value[end][i]; end
end

predict_df = coalesce.(predict_df, NaN);
CSV.write("$(@__DIR__)/data/outputfile_predictions.csv", predict_df);

Чтобы не выполнять модель многократно для каждого эксперимента, подготовим себе возможность открыть данные из таблицы результатов и построить графики:

In [ ]:
# Исходные данные обучения
result_df = DataFrame(CSV.File("$(@__DIR__)/data/outputfile.csv"))

# Данные предсказанные нейросетью
predict_df = DataFrame(CSV.File("$(@__DIR__)/data/outputfile_predictions.csv"))
p = [create_plots(predict_df, :reds, out_var, in_vars, result_df) for out_var in out_vars]
plot(p..., legend=false, size=(1500,500), titlefont=font(9), guidefont=font(7))
Out[0]:
No description has been provided for this image

Точки на графике (прогнозы) хорошо совпадают с поверхностями, полученными после эксперимента с физической моделью.

Заключение

Мы реализовали подход переноса нейросети в блок кода на Си, который позволит нам работать с данными любой размерности (любое количество входов и входов нейросети).

Визуализация результатов работает для двух входных переменных, если данных будет больше, стоит просто ограничить визуализацию двумя переменными или убрать ее.