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

Подходы к проектированию нейронных регуляторов

Вебинар Разработка перспективных видов регуляторов для объектов управления в Engee состоял из нескольких примеров:

  1. Управление автофокусом камеры смартфона

  2. Применение нечёткого регулятора для управления давлением

  3. Реализация адаптивных и нейросетевых регуляторов

В этом проекте собраны сопроводительные материалы для третьей части вебинара, остальные вы можете изучить в сообществе по приведенным выше ссылкам.

Пример с обычным PID регулятором

Это основная модель, в которой мы будем заменять регулятор.

image.png

Модель liquid_pressure_regulator.engee

Адаптивный PID регулятор

Первым делом мы настроим адаптивный PID регулятор, коэффициенты которого с каждым временным шагом проходят следующую процедуру обновления:

    if abs(e) > c.thr
        c.Kp = max(0, c.Kp + clamp(c.α*e*c.pe, -c.ΔK, c.ΔK))
        c.Ki = max(0, c.Ki + clamp(c.α*e*c.pie, -c.ΔK, c.ΔK))
        c.Kd = max(0, c.Kd + clamp(c.α*e*c.pde, -c.ΔK, c.ΔK))
    end

Во-первых, мы не будем реагировать на скачки управляюшего сигнала больше некоторого размера c.thr (параметр threshold). Во-вторых, параметр α ограничивает скорость изменения всех параметроы регулятора, при этом шаг изменения ограничен по модулю параметром ΔK.

image.png

Модель liquid_pressure_regulator_adaptive_pid.engee

Нейронный регулятор на Julia (RNN)

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

image.png

Модель liquid_pressure_regulator_neural_test.engee

Создаем нейронный регулятор на Си (FNN)

Запустим модель и соберем датасет

In [ ]:
Pkg.add("CSV")
In [ ]:
using DataFrames, CSV
In [ ]:
engee.open("$(@__DIR__)/liquid_pressure_regulator.engee")
data = engee.run( "liquid_pressure_regulator" )
Out[0]:
SimulationResult(
    "pressure" => WorkspaceArray{Float64}("liquid_pressure_regulator/pressure")
,
    "error" => WorkspaceArray{Float64}("liquid_pressure_regulator/error")
,
    "set_point" => WorkspaceArray{Float64}("liquid_pressure_regulator/set_point")
,
    "control" => WorkspaceArray{Float64}("liquid_pressure_regulator/control")

)
In [ ]:
function simout_to_df( data )
    vec_names = [i for i in keys(data) if length(data[i].value) > 0];
    df = DataFrame( hcat([collect(data[v]).value for v in vec_names]...), vec_names );
    df.time = collect(data[vec_names[1]]).time;
    return df
end
Out[0]:
simout_to_df (generic function with 1 method)
In [ ]:
df = simout_to_df( data );
CSV.write("Режим 1.csv", df);
plot(
    plot( df.time, df.set_point ), plot( df.time, df.control ), plot( df.time, df.pressure ), layout=(3,1)
)
Out[0]:

Изменим параметры модели и запустим ее в другом сценарии.

In [ ]:
engee.set_param!( "liquid_pressure_regulator/Сигнал утечки", "Amplitude"=>"0.001" )
data = engee.run( "liquid_pressure_regulator" )
df = simout_to_df( data );
CSV.write("Режим 2.csv", df);
plot(
    plot( df.time, df.set_point ), plot( df.time, df.control ), plot( df.time, df.pressure ), layout=(3,1)
)
Out[0]:
In [ ]:
engee.set_param!( "liquid_pressure_regulator/Сигнал утечки", "Amplitude"=>"0.0005", "Frequency"=>"0.2" )
data = engee.run( "liquid_pressure_regulator" )
df = simout_to_df( data );
CSV.write("Режим 3.csv", df);
plot(
    plot( df.time, df.set_point ), plot( df.time, df.control ), plot( df.time, df.pressure ), layout=(3,1)
)
Out[0]:

Вернем все параметры модели на место

In [ ]:
engee.set_param!( "liquid_pressure_regulator/Сигнал утечки", "Amplitude"=>"0.0005" )
engee.set_param!( "liquid_pressure_regulator/Сигнал утечки", "Frequency"=>"0.1" )

Обучим нейросеть аппроксимировать несколько регуляторов

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

In [ ]:
Pkg.add(["Flux", "BSON", "Glob", "MLUtils"])
In [ ]:
using Flux, MLUtils
using CSV, DataFrames
using Statistics, Random
using BSON, Glob

# Инициализируем генератор случайных чисел ради воспроизводимости эксперимента
Random.seed!(42)

# 1. Подготовим данные
function load_and_preprocess_data_fnn()
    # Загрузим все CSV файлы из текущей папки
    files = glob("*.csv")
    dfs = [CSV.read(file, DataFrame, types=Float32) for file in files]
    
    # Совместим данные в одну таблицу
    combined_df = vcat(dfs...)
    
    # Извлечем нужные нам столбцы (time, error, control)
    vtime = combined_df.time
    error = combined_df.error
    control = combined_df.control
    
    # Нормализация данных (очень поможет с ускорением обучения нейросети)
    error_mean, error_std = mean(error), std(error)
    control_mean, control_std = mean(control), std(control)
    
    error_norm = (error .- error_mean) ./ error_std
    control_norm = (control .- control_mean) ./ control_std
    
    # Разделим на небольшие последовательности чтобы обучить RNN
    sequence_length = 20  # сколько прошлых шагов мы учитываем для прогноза сигнала управления
    X = []
    Y = []
    
    for i in 1:(length(vtime)-sequence_length)
        push!(X, error_norm[i:i+sequence_length-1])
        push!(Y, control_norm[i+sequence_length])
    end
    
    # Оформим как массивы
    #X = reshape(hcat(X...), sequence_length, 1, :) # С батчами
    X = hcat(X...)'
    Y = hcat(Y...)'
    
    return (X, Y), (error_mean, error_std, control_mean, control_std)
end


# 2. Определяем структуру модели
function create_fnn_controller(input_size=20, hidden_size=5, output_size=1)
    return Chain(
        Dense(input_size, hidden_size, relu),
        Dense(hidden_size, hidden_size, relu),
        Dense(hidden_size, output_size)
    )
end


# 3. Обучение с новым API Flux
function train_fnn_model(X, Y; epochs=100, batch_size=32)
    # Разделение данных
    split_idx = floor(Int, 0.8 * size(X, 1))
    X_train, Y_train = X[1:split_idx, :], Y[1:split_idx, :]
    X_val, Y_val = X[split_idx+1:end, :], Y[split_idx+1:end, :]
    
    # Создание модели и оптимизатора
    model = create_fnn_controller()
    optimizer = Flux.setup(Adam(0.001), model)
    
    # Функция потерь
    loss(x, y) = Flux.mse(model(x), y)
    
    # Подготовка DataLoader
    train_loader = Flux.DataLoader((X_train', Y_train'), batchsize=batch_size, shuffle=true)
    
    # Цикл обучения
    train_losses = []
    val_losses = []
    
    for epoch in 1:epochs
        # Обучение
        Flux.train!(model, train_loader, optimizer) do m, x, y
            y_pred = m(x)
            Flux.mse(y_pred, y)
        end
        
        # Расчет ошибки
        train_loss = loss(X_train', Y_train')
        val_loss = loss(X_val', Y_val')
        push!(train_losses, train_loss)
        push!(val_losses, val_loss)
        
        # Логирование
        if epochs % 10 == 0
            @info "Epoch $epoch" train_loss val_loss
        end
    end
    
    # Визуализация обучения
    plot(1:epochs, train_losses, label="Training Loss")
    plot!(1:epochs, val_losses, label="Validation Loss")
    xlabel!("Epoch")
    ylabel!("Loss")
    title!("Training Progress")
    
    return model
end

# 4. Оценка модели (без изменений)
function evaluate_fnn_model(model, X, Y, norm_params)
    predictions = model(X')
    
    # Денормализация
    _, _, control_mean, control_std = norm_params
    Y_true = Y .* control_std .+ control_mean
    Y_pred = predictions' .* control_std .+ control_mean
    
    # Расчет метрик
    rmse = sqrt(mean((Y_true - Y_pred).^2))
    println("RMSE: ", rmse)
    
    # Визуализация
    plot(Y_true[1:100], label="True Control Signal")
    plot!(Y_pred[1:100], label="Predicted Control Signal")
    xlabel!("Time Step")
    ylabel!("Control Signal")
    title!("FNN Controller Performance")
end
In [ ]:
# Загрузка данных
(X, Y), norm_params = load_and_preprocess_data_fnn()

# Обучение
model = train_fnn_model(X, Y, epochs=100, batch_size=32)

# Сохранение модели
using BSON
BSON.@save "fnn_controller_v2.bson" model norm_params
┌ Info: Epoch 1
│   train_loss = 0.8938878f0
└   val_loss = 0.065423325f0
┌ Info: Epoch 2
│   train_loss = 0.6930624f0
└   val_loss = 0.08745527f0
┌ Info: Epoch 3
│   train_loss = 0.6121955f0
└   val_loss = 0.0670045f0
┌ Info: Epoch 4
│   train_loss = 0.5751492f0
└   val_loss = 0.0673555f0
┌ Info: Epoch 5
│   train_loss = 0.5150536f0
└   val_loss = 0.084629685f0
┌ Info: Epoch 6
│   train_loss = 0.43911234f0
└   val_loss = 0.060537163f0
┌ Info: Epoch 7
│   train_loss = 0.34446087f0
└   val_loss = 0.06451357f0
┌ Info: Epoch 8
│   train_loss = 0.26789767f0
└   val_loss = 0.08551992f0
┌ Info: Epoch 9
│   train_loss = 0.21148369f0
└   val_loss = 0.10232961f0
┌ Info: Epoch 10
│   train_loss = 0.18503676f0
└   val_loss = 0.14733629f0
┌ Info: Epoch 11
│   train_loss = 0.15013915f0
└   val_loss = 0.14483832f0
┌ Info: Epoch 12
│   train_loss = 0.12138271f0
└   val_loss = 0.11467917f0
┌ Info: Epoch 13
│   train_loss = 0.10577717f0
└   val_loss = 0.045897737f0
┌ Info: Epoch 14
│   train_loss = 0.101480916f0
└   val_loss = 0.057284135f0
┌ Info: Epoch 15
│   train_loss = 0.10315049f0
└   val_loss = 0.065064624f0
┌ Info: Epoch 16
│   train_loss = 0.10541139f0
└   val_loss = 0.056540746f0
┌ Info: Epoch 17
│   train_loss = 0.09681009f0
└   val_loss = 0.045904756f0
┌ Info: Epoch 18
│   train_loss = 0.093385085f0
└   val_loss = 0.0685159f0
┌ Info: Epoch 19
│   train_loss = 0.09045938f0
└   val_loss = 0.041075245f0
┌ Info: Epoch 20
│   train_loss = 0.08972832f0
└   val_loss = 0.055446453f0
┌ Info: Epoch 21
│   train_loss = 0.09329716f0
└   val_loss = 0.03967251f0
┌ Info: Epoch 22
│   train_loss = 0.1414877f0
└   val_loss = 0.07507982f0
┌ Info: Epoch 23
│   train_loss = 0.08920607f0
└   val_loss = 0.051499482f0
┌ Info: Epoch 24
│   train_loss = 0.0801875f0
└   val_loss = 0.03534109f0
┌ Info: Epoch 25
│   train_loss = 0.08018934f0
└   val_loss = 0.030156119f0
┌ Info: Epoch 26
│   train_loss = 0.076051794f0
└   val_loss = 0.03466235f0
┌ Info: Epoch 27
│   train_loss = 0.07765352f0
└   val_loss = 0.056599125f0
┌ Info: Epoch 28
│   train_loss = 0.07806299f0
└   val_loss = 0.055766143f0
┌ Info: Epoch 29
│   train_loss = 0.07484163f0
└   val_loss = 0.0389787f0
┌ Info: Epoch 30
│   train_loss = 0.07139521f0
└   val_loss = 0.033857666f0
┌ Info: Epoch 31
│   train_loss = 0.08287221f0
└   val_loss = 0.042753786f0
┌ Info: Epoch 32
│   train_loss = 0.072352625f0
└   val_loss = 0.054598782f0
┌ Info: Epoch 33
│   train_loss = 0.0717976f0
└   val_loss = 0.031754486f0
┌ Info: Epoch 34
│   train_loss = 0.069250494f0
└   val_loss = 0.043800574f0
┌ Info: Epoch 35
│   train_loss = 0.068584874f0
└   val_loss = 0.035793144f0
┌ Info: Epoch 36
│   train_loss = 0.098963626f0
└   val_loss = 0.085103f0
┌ Info: Epoch 37
│   train_loss = 0.067444935f0
└   val_loss = 0.06019559f0
┌ Info: Epoch 38
│   train_loss = 0.080243886f0
└   val_loss = 0.027890693f0
┌ Info: Epoch 39
│   train_loss = 0.0734144f0
└   val_loss = 0.038119968f0
┌ Info: Epoch 40
│   train_loss = 0.06261252f0
└   val_loss = 0.036735766f0
┌ Info: Epoch 41
│   train_loss = 0.06626095f0
└   val_loss = 0.04766762f0
┌ Info: Epoch 42
│   train_loss = 0.061304063f0
└   val_loss = 0.038307376f0
┌ Info: Epoch 43
│   train_loss = 0.06375473f0
└   val_loss = 0.049757667f0
┌ Info: Epoch 44
│   train_loss = 0.06929615f0
└   val_loss = 0.031478032f0
┌ Info: Epoch 45
│   train_loss = 0.06482848f0
└   val_loss = 0.041360646f0
┌ Info: Epoch 46
│   train_loss = 0.08152386f0
└   val_loss = 0.031685207f0
┌ Info: Epoch 47
│   train_loss = 0.06899811f0
└   val_loss = 0.03166399f0
┌ Info: Epoch 48
│   train_loss = 0.06471846f0
└   val_loss = 0.057980362f0
┌ Info: Epoch 49
│   train_loss = 0.073723935f0
└   val_loss = 0.0419289f0
┌ Info: Epoch 50
│   train_loss = 0.062251564f0
└   val_loss = 0.05569823f0
┌ Info: Epoch 51
│   train_loss = 0.08304988f0
└   val_loss = 0.047913346f0
┌ Info: Epoch 52
│   train_loss = 0.13036466f0
└   val_loss = 0.06255697f0
┌ Info: Epoch 53
│   train_loss = 0.0668965f0
└   val_loss = 0.023209779f0
┌ Info: Epoch 54
│   train_loss = 0.0641162f0
└   val_loss = 0.026421405f0
┌ Info: Epoch 55
│   train_loss = 0.0606432f0
└   val_loss = 0.03451042f0
┌ Info: Epoch 56
│   train_loss = 0.05806036f0
└   val_loss = 0.047697715f0
┌ Info: Epoch 57
│   train_loss = 0.08080194f0
└   val_loss = 0.030021664f0
┌ Info: Epoch 58
│   train_loss = 0.075242944f0
└   val_loss = 0.027072791f0
┌ Info: Epoch 59
│   train_loss = 0.07018888f0
└   val_loss = 0.03701796f0
┌ Info: Epoch 60
│   train_loss = 0.20293671f0
└   val_loss = 0.039905872f0
┌ Info: Epoch 61
│   train_loss = 0.08879273f0
└   val_loss = 0.068481795f0
┌ Info: Epoch 62
│   train_loss = 0.06276142f0
└   val_loss = 0.044078235f0
┌ Info: Epoch 63
│   train_loss = 0.060167953f0
└   val_loss = 0.030940093f0
┌ Info: Epoch 64
│   train_loss = 0.05863377f0
└   val_loss = 0.033402573f0
┌ Info: Epoch 65
│   train_loss = 0.09663699f0
└   val_loss = 0.033131924f0
┌ Info: Epoch 66
│   train_loss = 0.053091682f0
└   val_loss = 0.026231134f0
┌ Info: Epoch 67
│   train_loss = 0.053288568f0
└   val_loss = 0.03478807f0
┌ Info: Epoch 68
│   train_loss = 0.062837504f0
└   val_loss = 0.032456074f0
┌ Info: Epoch 69
│   train_loss = 0.14382939f0
└   val_loss = 0.039194208f0
┌ Info: Epoch 70
│   train_loss = 0.057213098f0
└   val_loss = 0.03217173f0
┌ Info: Epoch 71
│   train_loss = 0.059472032f0
└   val_loss = 0.050639316f0
┌ Info: Epoch 72
│   train_loss = 0.061687153f0
└   val_loss = 0.041273717f0
┌ Info: Epoch 73
│   train_loss = 0.057540856f0
└   val_loss = 0.043549165f0
┌ Info: Epoch 74
│   train_loss = 0.06940068f0
└   val_loss = 0.030464195f0
┌ Info: Epoch 75
│   train_loss = 0.055994995f0
└   val_loss = 0.06194949f0
┌ Info: Epoch 76
│   train_loss = 0.06127405f0
└   val_loss = 0.035824254f0
┌ Info: Epoch 77
│   train_loss = 0.08695382f0
└   val_loss = 0.03958839f0
┌ Info: Epoch 78
│   train_loss = 0.068831f0
└   val_loss = 0.044648975f0
┌ Info: Epoch 79
│   train_loss = 0.06480649f0
└   val_loss = 0.049541153f0
┌ Info: Epoch 80
│   train_loss = 0.054308545f0
└   val_loss = 0.03448179f0
┌ Info: Epoch 81
│   train_loss = 0.062011868f0
└   val_loss = 0.033419278f0
┌ Info: Epoch 82
│   train_loss = 0.058451455f0
└   val_loss = 0.041000187f0
┌ Info: Epoch 83
│   train_loss = 0.056441877f0
└   val_loss = 0.040258046f0
┌ Info: Epoch 84
│   train_loss = 0.065997876f0
└   val_loss = 0.02149929f0
┌ Info: Epoch 85
│   train_loss = 0.14329454f0
└   val_loss = 0.024663093f0
┌ Info: Epoch 86
│   train_loss = 0.07271247f0
└   val_loss = 0.057003014f0
┌ Info: Epoch 87
│   train_loss = 0.05535151f0
└   val_loss = 0.050590448f0
┌ Info: Epoch 88
│   train_loss = 0.10478283f0
└   val_loss = 0.041117538f0
┌ Info: Epoch 89
│   train_loss = 0.05750017f0
└   val_loss = 0.027143845f0
┌ Info: Epoch 90
│   train_loss = 0.05710252f0
└   val_loss = 0.029968357f0
┌ Info: Epoch 91
│   train_loss = 0.058521483f0
└   val_loss = 0.04156654f0
┌ Info: Epoch 92
│   train_loss = 0.056501415f0
└   val_loss = 0.031160006f0
┌ Info: Epoch 93
│   train_loss = 0.06948501f0
└   val_loss = 0.036111105f0
┌ Info: Epoch 94
│   train_loss = 0.056522947f0
└   val_loss = 0.042170122f0
┌ Info: Epoch 95
│   train_loss = 0.07270187f0
└   val_loss = 0.045176893f0
┌ Info: Epoch 96
│   train_loss = 0.09828934f0
└   val_loss = 0.060680926f0
┌ Info: Epoch 97
│   train_loss = 0.059696835f0
└   val_loss = 0.03424492f0
┌ Info: Epoch 98
│   train_loss = 0.07078414f0
└   val_loss = 0.032400988f0
┌ Info: Epoch 99
│   train_loss = 0.05980099f0
└   val_loss = 0.041180395f0
┌ Info: Epoch 100
│   train_loss = 0.05841915f0
└   val_loss = 0.038568825f0
In [ ]:
# Оценка
evaluate_fnn_model(model, X, Y, norm_params)
RMSE: 0.00137486
Out[0]:

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

Применим библиотеку Symbolics чтобы сгенерировать код и выполним несколько доработок, чтобы его можно было вызывать из блока C Function.

In [ ]:
Pkg.add("Symbolics")
In [ ]:
using Symbolics
@variables X[1:20]
c_model_code = build_function( model( collect(X) ), collect(X); target=Symbolics.CTarget(), fname="neural_net", lhsname=:y, rhsnames=[:x] )
Out[0]:
"#include <math.h>\nvoid neural_net(double* y, const double* x) {\n  y[0] = -0.17745794f0 + 0.50926423f0 * ifelse(-0.049992073f0 + -0.23742697f0 * ifelse(-1.8439064f0 + -0.54531264f0 * x[0] + 0.69540715f0 * x[9] + 0.13691874f0 * x[10] + 0.4638261f0 * x[11] + 0.68296975f0 *" ⋯ 48502 bytes ⋯ "8941267f0 * x[16] + -0.20798773f0 * x[17] + 0.046037998f0 * x[18] + 0.2163552f0 * x[1] + 0.17670809f0 * x[19] + 0.48885685f0 * x[2] + 0.33982033f0 * x[3] + 0.23923202f0 * x[4] + 0.44608107f0 * x[5] + -0.22155789f0 * x[6] + 0.18008044f0 * x[7] + 0.3575259f0 * x[8]));\n}\n"
In [ ]:
# Заменим несколько инструкций в коде
c_fixed_model_code = replace( c_model_code,
                              "double" => "float",
                              "f0" => "f",
                              "  y[0]"=>"y" );

println( c_fixed_model_code[1:200] )
println("...")
#include <math.h>
void neural_net(float* y, const float* x) {
y = -0.17745794f + 0.50926423f * ifelse(-0.049992073f + -0.23742697f * ifelse(-1.8439064f + -0.54531264f * x[0] + 0.69540715f * x[9] + 0.1
...
In [ ]:
# Оставим только третью строчку от этого кода
c_fixed_model_code = split(c_fixed_model_code, "\n")[3]

c_code_standalone = """
float ifelse(bool cond, float a, float b) { return cond ? a : b; }

$c_fixed_model_code""";

println( c_code_standalone[1:200] )
println("...")
float ifelse(bool cond, float a, float b) { return cond ? a : b; }

y = -0.17745794f + 0.50926423f * ifelse(-0.049992073f + -0.23742697f * ifelse(-1.8439064f + -0.54531264f * x[0] + 0.69540715f * x[9]
...

Сохраним код в файл

In [ ]:
open("$(@__DIR__)/neural_net_fc.c", "w") do f
    println( f, "$c_code_standalone" )
end
image.png

Модель liquid_pressure_regulator_neural_fc.engee

Заключение

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