Документация 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.