Введение

В данной статье будет рассматриваться задача многомерной интерполяции с помощью нейронной сети. Нейронная сеть построена с помощью среды математического моделирования Engee. Данная задача имеет серьезное практическое применение так как нарушение целостности трубопровода связано с большими экономическими последствиями или даже может привести к техногенной катастрофе (в случае если трубопровод промышленный). Основная проблема обнаружения места утечки что нельзя напрямую зафиксировать факт утечки и определить её можно только алгоритмически измеряя косвенные параметры (давление, скорость потока жидкости или газа в трубопроводе, температура). Для повышения точности измерений данные параметры измеряются на всём протяжении трубопровода. Наиболее простой и распространенный способ. Но у данного способа есть существенный недостаток, он позволяет определить только участок трубопровода и для более точного определения места утечки нужен другой (более точный) алгоритм поиска.

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

Используемые библиотеки

using Flux
using Statistics
using Random
using Plots

Исходные данные для обучения нейронной сети (расстояние, избыточное давление, расход)

data = [
    160	79	0.0355;
    160	60	0.045;
    160	53	0.05;
    160	44	0.057;
    160	40	0.0625;
    120	79	0.035;
    120	60	0.0385;
    120	53	0.043;
    120	44	0.0495;
    120	40	0.0535;
    100	79	0.029;
    100	60	0.036;
    100	53	0.04;
    100	44	0.0465;
    100	40	0.05;
    80	79	0.0265;
    80	60	0.0278;
    80	53	0.0284;
    80	44	0.03;
    80	40	0.0315;
    60	44	0.024;
    60	40	0.0248;
    40	40	0.0195;
]

Подготовка данных

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

# Разделяем таблицу на входы (X) и выходы (Y)
X_raw = Float32.(data[:, 1:2]') # Транспонируем до размера 2 x N
Y_raw = Float32.(reshape(data[:, 3], 1, :)) # Размер 1 x N
# 2. Нормализация данных (критически важно!)
# Нейросети плохо работают с данными разного масштаба (сотни у X и сотые доли у Y).
stats_x = (mean(X_raw, dims=2), std(X_raw, dims=2) .+ eps(Float32))
stats_y = (mean(Y_raw), std(Y_raw) .+ eps(Float32))
function normalize(arr, stats)
    return @. (arr - stats[1]) / stats[2]
end
X_norm = normalize(X_raw, stats_x)
Y_norm = normalize(Y_raw, stats_y)

Сравнение различных моделей

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

Модель 1 (4 слоя, максимальное число узлов 32)

model = Chain(
    Dense(2 => 32, tanh), 
    Dense(32 => 32, tanh),
    Dense(32 => 32, tanh),
    Dense(32 => 1)
)

Модель 2 (8 слоёв, максимальное число узлов 128)

model2 = Chain(
    Dense(2 => 128, tanh), 
    Dense(128 => 128, tanh),
    Dense(128 => 128, tanh),
    Dense(128 => 128, tanh),
    Dense(128 => 64, tanh),
    Dense(64 => 32, tanh),
    Dense(32 => 16, tanh),
    Dense(16 => 1)
)

Модель 3 (12 слоёв, максимальное число узлов 128)

model3 = Chain(
    Dense(2 => 32, tanh), 
    Dense(32 => 32, tanh),
    Dense(32 => 64, tanh),
    Dense(64 => 128, tanh),
    Dense(128 => 64, tanh),
    Dense(64 => 32, tanh),
    Dense(32 => 32, tanh),
    Dense(32 => 32, tanh),
    Dense(32 => 32, tanh),
    Dense(32 => 32, tanh),
    Dense(32 => 16, tanh),
    Dense(16 => 1)
)

Для каждой из моделей создана функция для расчета погрешностей при работе нейронной сети.

loss(x, y) = Flux.Losses.mse(model(x), y)
opt = Flux.setup(Flux.Adam(0.00005), model)
loss2(x, y) = Flux.Losses.mse(model2(x), y)
opt2 = Flux.setup(Flux.Adam(0.00005), model2)
loss3(x, y) = Flux.Losses.mse(model3(x), y)
opt3 = Flux.setup(Flux.Adam(0.00005), model3)

Функция для обучения моделей

function train!(epochs,model)
    losses = Float32[]
    best_model_state = nothing
    best_loss = Inf
    
    println("Начало обучения...")
    for epoch in 1:epochs
        if model == model2
        grads2 = Flux.gradient(m -> loss2(X_norm, Y_norm), model2)[1]
        Flux.Optimise.update!(opt2, model2, grads2)
        current_loss = loss2(X_norm, Y_norm)
        elseif model == model3
        grads3 = Flux.gradient(m -> loss3(X_norm, Y_norm), model3)[1]
        Flux.Optimise.update!(opt3, model3, grads3)
        current_loss = loss3(X_norm, Y_norm)
        else
        grads = Flux.gradient(m -> loss(X_norm, Y_norm), model)[1]
        Flux.Optimise.update!(opt, model, grads)
        current_loss = loss(X_norm, Y_norm)
        end
        
        if current_loss < best_loss
            best_loss = current_loss
            best_model_state = deepcopy(Flux.state(model))
        end
        push!(losses, current_loss)
        if epoch % 1000 == 0 || epoch == 1
            println("Эпоха $epoch | Потери: $(round(current_loss, sigdigits=10))")
        end
    end
    
    return best_loss
end

Обучение моделей

Вызов функции для обучения моделей (вызывается для каждой модели)

train!(10000,model) # обучение модели 1
train!(10000,model2) # обучение модели 2
train!(10000,model3) # обучение модели 3

Отображение результата обработки

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

# 6. Оценка качества на исходных данных (денормализация)
Y_pred_norm = model(X_norm)
Y_pred = Y_pred_norm .* stats_y[2] .+ stats_y[1]
mae = mean(abs.(Y_pred' .- data[:, 3])) # Средняя абсолютная ошибка в исходных единицах
println("\nОбучение завершено.")
println("Оценка точности на узлах таблицы (MAE): $(round(mae, digits=6))")
xg = range(minimum(data[:, 1]), maximum(data[:, 1]), length=50)
yg = range(minimum(data[:, 2]), maximum(data[:, 2]), length=50)
z_grid = [model(normalize([x, y], stats_x))[1] for y in yg, x in xg]
z_grid_denorm = z_grid .* stats_y[2] .+ stats_y[1]
#plotlyjs() 
surface(xg, yg, z_grid_denorm;
    title="Определение места утечки",
    xlabel="Давление", 
    zlabel="Расход",
    ylabel="Расстояние до места утечки",
    legend=false,
    alpha=0.8)
scatter3d!(data[:, 1], data[:, 2], data[:, 3]; 
    color=:red, markersize=4, label="Исходные узлы")

Денормализация результата и отображение на графике (для второй модели)

# 6. Оценка качества на исходных данных (денормализация)
Y_pred_norm = model2(X_norm)
Y_pred = Y_pred_norm .* stats_y[2] .+ stats_y[1]
mae = mean(abs.(Y_pred' .- data[:, 3])) # Средняя абсолютная ошибка в исходных единицах
println("\nОбучение завершено.")
println("Оценка точности на узлах таблицы (MAE): $(round(mae, digits=6))")
xg = range(minimum(data[:, 1]), maximum(data[:, 1]), length=50)
yg = range(minimum(data[:, 2]), maximum(data[:, 2]), length=50)
z_grid = [model2(normalize([x, y], stats_x))[1] for y in yg, x in xg]
z_grid_denorm = z_grid .* stats_y[2] .+ stats_y[1]
#plotlyjs() 
surface(xg, yg, z_grid_denorm;
    title="Определение места утечки",
    xlabel="Давление", 
    ylabel="Расход",
    zlabel="Расстояние до места утечки",
    legend=false,
    alpha=0.8)
scatter3d!(data[:, 1], data[:, 2], data[:, 3]; 
    color=:red, markersize=4, label="Исходные узлы")

Денормализация результата и отображение на графике (для третьей модели)

# 6. Оценка качества на исходных данных (денормализация)
Y_pred_norm = model3(X_norm)
Y_pred = Y_pred_norm .* stats_y[2] .+ stats_y[1]
mae = mean(abs.(Y_pred' .- data[:, 3])) # Средняя абсолютная ошибка в исходных единицах
println("\nОбучение завершено.")
println("Оценка точности на узлах таблицы (MAE): $(round(mae, digits=6))")
xg = range(minimum(data[:, 1]), maximum(data[:, 1]), length=50)
yg = range(minimum(data[:, 2]), maximum(data[:, 2]), length=50)
z_grid = [model3(normalize([x, y], stats_x))[1] for y in yg, x in xg]
z_grid_denorm = z_grid .* stats_y[2] .+ stats_y[1]
#plotlyjs() 
surface(xg, yg, z_grid_denorm;
    title="Определение места утечки",
    xlabel="Давление", 
    zlabel="Расход",
    ylabel="Расстояние до места утечки",
    legend=false,
    alpha=0.8)
scatter3d!(data[:, 1], data[:, 2], data[:, 3]; 
    color=:red, markersize=4, label="Исходные узлы")

Вывод

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