Привет Хабр!

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

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

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

  • Создавать точные модели прогнозирования эрозионного износа

  • Интегрировать ML-модели в существующие системы инженерного моделирования

  • Автоматизировать процесс оценки прочности конструкций

  • Повышать эффективность эксплуатации газодобывающего оборудования

Практическая ценность материала заключается в пошаговом руководстве создания и внедрения ML-решений в реальную производственную среду. В данном руководстве представлены готовые инструменты и практические рекомендации для самостоятельной реализации проекта.

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

Готовы погрузиться в мир промышленного ML? Тогда начнем.

1. Общая информация о газопроводах-шлейфах

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

../../../_images/gas_pipeline_with_annotations.png
Газопровод-шлейф на кусте двух газовых скважин.

Газопровод-шлейф (№1 на рисунке выше) в процессе эксплуатации подвержен следующим воздействиям:

  • Внутренняя поверхность газопровода нагружена давлением и температурой газа;

  • Наружная поверхность газопровода нагружена переменной температурой внешней среды.

1.1.  Проблема эрозионного износа газопровода-шлейфа

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

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

Также эрозионный износ может носить циклический характер с периодическим выносом воды и механических примесей.

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

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

1.2.  Способы снижения риска

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

  • Применение физико-математического моделирования процесса эксплуатации трубопроводной системы и его системы управления;

  • Применение автоматизированных средств диагностики технического состояния элементов газопровода;

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

  • Применение физико-математического моделирования процесса эксплуатации трубопроводной системы и его системы управления;

  • Применение автоматизированных средств диагностики технического состояния элементов газопровода;

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

2. Средства диагностики технического состояния газопроводов-шлейфов

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

  • Системы контроля расхода;

  • Датчики температуры внешней среды и системы контроля давления и температуры газа;

  • Интрузивные датчики измерения эрозии и акустические системы контроля песка;

  • Системы точечной и распределённой толщинометрии.

3. Цели и задачи моделирования

Цель. Повышение производственной эффективности компании и предупреждение аварий промысловых объектов.

Задачи моделирования:

  • Расчёт параметров прочности на основании актуальных данных диагностики технического состояния газопроводов-шлейфов;

  • Расчёт параметров системы сбора на соответствие техническому проекту разработки месторождения;

  • Обеспечение максимальной добычи газа в пиковый сезонный период (зима, спрос) и минимальной добычи (лето);

  • Обеспечение оптимальных параметров работы системы сбора при незапланированном отключении скважин или при режимах работы скважин, не соответствующих проектным;

  • Определение параметров системы сбора при изменении входных параметров системы подготовки газа и конденсата.

Способ решения задачи №1 приводится в следующем разделе. Решение остальных задач сводится к моделированию трубопроводных систем с помощью REPEAT и решению задач глобальной и локальной оптимизации.

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

Выполнить оценку напряжённо-деформированного состояния (НДС) отвода на основании исходных данных и граничных условий.

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

  • Исходные данные и граничные условия:

    • Минимальная толщина стенки,

    • Давление газа,

    • Температура газа,

    • Температура окружающей среды,

  • Результат расчёта НДС (напряжённо-деформированное состояние): Максимальные напряжения по Мизесу.

Расчётные случаи основаны на технологическом регламенте конкретного месторождения:

  • Давление газа изменяется в диапазоне: от 8.5 до 6.0 МПа,

  • Температура газа изменяется в диапазоне: от 7 до 20 ,

  • Температура окружающей среды изменяется в диапазоне: от -55 до 40 ,

  • Расчётные случаи включают минимальные толщины стенки отвода от 6.45 до 4.9 мм.

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

../../../_images/stress_results.png
Пример распределений напряжений по Мизесу в отводе.

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

5. Подготовка таблицы расчётных случаев

Расчёт статической прочности отвода выполняется в программном комплексе, реализующего метод конечных элементов (МКЭ).

5.1.  Модель статической прочности

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

Линейные участки трубопроводов газопровода-шлейфа и опорные конструкции моделировались в балочном приближении.

Отводы и тройниковые соединения моделировались с помощью объёмных КЭ.

5.2.  Основные этапы подготовки данных

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

Подсказка

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

Основные этапы процесса параметрических исследований приведены на рисунке ниже.

../../../_images/param_study_proc.png
Процесс подготовки данных на основе моделирования статической прочности отвода.

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

6. Построение модели нейронной сети

Примечание

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

Также вы можете скачать файл модели и сразу использовать её в проекте на платформе REPEAT с помощью блока Аппроксимационная модель.

Для написания модели будет использован язык Python и фреймворк PyTorch, поэтому необходимы базовые знания этого языка и навыки работы с PyTorch.

Важно

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

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

  • NumPy и Pandas — работа с массивами, линейная алгебра и обработка табличных данных соответственно.

  • MatplotlibSeaborn — визуализация статических графиков для отслеживания качества работы модели.

  • Plotly — интерактивные графики, для наглядного представления объекта исследования.

  • scikit-learn — подсчет метрик и разбиение данных.

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

# Импорт библиотек

import numpy as np
import pandas as pd

import seaborn as sns
import matplotlib.pyplot as plt
import plotly.graph_objects as go

 

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader

from sklearn.model_selection import train_test_split
from sklearn.metrics import (mean_squared_error, 
                             mean_absolute_error,
                             mean_absolute_percentage_error,
                             r2_score)

from tqdm.notebook import tqdm
from typing import Tuple, List, Dict, Union

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

# Воспроизводимость и устройство

SEED = 42
torch.manual_seed(SEED)
np.random.seed(SEED)

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(device)

6.1.  Исследование данных

Для наглядного отображения распределения напряжений в отводе визуализируем трехмерное распределение точек используя интерактивную 3D-визуализация с помощью Plotly.

# Визуализация напряжений

def branch_plot(df: pd.DataFrame) -> go.Figure:
    """
    Создает 3D визуализацию распределения напряжений.

    Параметры:
        df: pd.DataFrame
            DataFrame с данными о координатах и напряжениях

    Возвращает:
        go.Figure: Объект графика Plotly
    """

    marker_config = dict(
        size=3,
        color=df['Stress'],
        colorscale='Jet',
        opacity=0.8,
        colorbar=dict(title="Напряжение (МПа)")
    )

    hover_template = (
        '<b>Узел</b>: %{customdata}<br>'
        '<b>X</b>: %{x:.2f} мм<br>'
        '<b>Y</b>: %{y:.2f} мм<br>'
        '<b>Z</b>: %{z:.2f} мм<br>'
        '<b>Напряжение</b>: %{marker.color:.2f} МПа'
    )

    fig = go.Figure(
        data=[
            go.Scatter3d(
                x=df['X'],
                y=df['Y'],
                z=df['Z'],
                mode='markers',
                marker=marker_config,
                hovertemplate=hover_template,
                customdata=df['Node']
            )
        ]
    )

    scene_config = dict(
        xaxis_title='X (мм)',
        yaxis_title='Y (мм)',
        zaxis_title='Z (мм)',
        aspectmode='data',
        camera=dict(
            eye=dict(x=0, y=0, z=2.5),
            center=dict(x=0, y=0, z=0),
            up=dict(x=0, y=1, z=0)
        )
    )

    margin_config = dict(l=0, r=0, b=0, t=30)
    fig.update_layout(

        title='Распределение напряжений',
        scene=scene_config,
        margin=margin_config
    )

    return fig

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

# Построение визуализации напряжений

df = pd.read_csv('stress_branch.csv')
fig = branch_plot(df)
fig.show()
Stress_distribution
Распределение напряжений в отводе.

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

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

  • Входные параметры (признаки):

    • Давление (pressure), меняется в диапазоне: 6.0 - 8.5 МПа.

    • Температура газа (gas_temperature), меняется в диапазоне: 7.0 - 20.0 .

    • Температура окружающей среды (ambient_temperature), меняется в диапазоне: -55.0 - 40.0 .

    • Минимальная толщина стенки (wall_thickness_min), меняется в диапазоне: 4.90 - 6.45 мм.

  • Целевая переменная:

    • Максимальное напряжение по Мизесу в конструкции (stress_max), меняется в диапазоне: 45.31 - 140.40 МПа.

Распределение данных для обучения

df = pd.read_csv('branch_data.csv')
df.describe()

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

Отображение данных для обучения

features = ['pressure', 'gas_temp', 'ambient_temp', 'wall_thickness']
target = 'stress_max'

df[features + [target]].head()

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

Для эффективной работы с данными в PyTorch используется специальный класс StressDataset, который наследуется от torch.utils.data.Dataset. Родительский класс является абстрактным и требует реализации двух основных методов: len() и getitem(). В итоге, этот класс выполняет несколько важных функций:

  • Инкапсулирует логику загрузки и предобработки данных. В данном случае - преобразует данные в torch.Tensor и переносит их на необходимое устройство.

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

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

  • Упрощает интеграцию с DataLoader для разбиения на батчи и перемешивания данных.

Определение класса StressDataset

class StressDataset(Dataset):
    """
    Класс датасета для обучения модели.
    """
    def init(
        self,
        X: np.ndarray,
        y: np.ndarray,
        device: torch.device = torch.device('cpu')
    ) -> None:
        """
        Инициализация набора данных.
        Параметры:
            X: np.ndarray
                Массив входных признаков
            y: np.ndarray
                Массив целевых значений
            device: torch.device
                Устройство для вычислений
        """
        super().__init__()
        self.device = device
        self.X = X
        self.y = torch.FloatTensor(y, device=self.device).reshape(-1, 1)

    def len(self) -> int:
        """
        Методы получения количества элементов.
        Возвращает:
            int: Количество элементов
        """
        return len(self.X)

    def getitem(self, idx: int) -> Tuple[Tuple[torch.Tensor, ...], torch.Tensor]:
        """
        Возвращает пару (признаки, целевое значение) для заданного индекса.
        Параметры:
            idx: int
                Индекс образца
        Возвращает:
            Tuple[Tuple[torch.Tensor, ...], torch.Tensor]: 
                Кортеж из кортежа признаков и целевого значения
        """

        return (
            tuple(torch.Tensor([feature], device=self.device) 
                  for feature in self.X[idx]),
            self.y[idx]
        )

Важно

Важно отметить, что каждый признак возвращается отдельным тензором, что важно для экспорта в ONNX и интеграции с REPEAT.

Воспользуемся созданным классом для подготовки данных для обучения.

Подготовка данных для обучения и тестирования

X = df[features].values
y = df[target].values

X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.2,
    random_state=42
)

train_dataset = StressDataset(X_train, y_train, device)
test_dataset = StressDataset(X_test, y_test, device)

batch_size = 64
train_loader = DataLoader(
    train_dataset, 
    batch_size=batch_size, 
    shuffle=True
)
test_loader = DataLoader(
    test_dataset, 
    batch_size=batch_size, 
    shuffle=False
)

В результате выполнения этого кода происходит:

  • Строки 1-2. Отделение численных значений признаков и целевой переменной.

  • Строки 4-8. Разбиение на обучающую и тестовую выборки.

  • Строка 10-11. Создание экземпляров класса StressDataset для обучения и тестирования

  • Строка 13-23. Создание экземпляров класса DataLoader для эффективной загрузки данных батчами.

6.3.  Архитектура нейронной сети

В задаче заданы 4 входных признака: давление (pressure), температура газа (gas_temp), температура окружающей среды (ambient_temp) и толщина стенки (wall_thickness). Все они склеиваются в один вектор размера 4. Выходом же является одно численное значение (предсказанное максимальное напряжение), то есть задача является примером задачи регрессии:

Для табличных данных могут быть применены различные модели, от классического машинного обучения до глубоких нейронных сетей. В данном случае используется MLP (Multi Layer Perceptron) модель.

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

Матричное представление трехслойной MLP модели с 4 входными каналами, 1 выходным и скрытым состоянием :

В модели применено несколько идей:

  • Для стабилизации обучения модель использует предварительно вычисленные статистики (mean и std) из тренировочного набора данных. Нормализация входных признаков в методе forward позволяет привести каждый пример к единому распределению, не требуя отдельного нормализатора, что осложнило бы импорт в REPEAT.

  • В качестве регуляризации применяется комбинация Dropout, L2-регуляризации (weight decay) и ранней остановкой (early stopping). Функция активации ReLU выбрана на основе анализа данных, хотя альтернативно можно использовать и более сглаженные варианты. Для инициализации весов используется метод Кайминга, который стабилизирует дисперсию выходов слоев и предотвращает проблемы с градиентами. Смещения инициализируются нулями, так как они быстро обучаются сами.

  • Вместо популярного BatchNorm предпочтение отдано LayerNorm, что обеспечивает более стабильное обучение на консистентных данных при существенных статистических различиях между батчами.

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

Определение класса StressNet

class StressNet(nn.Module):
    """
    Нейронная сеть.
    """
    def init(
        self,
        input_size: int = 4,
        hidden_size: int = 32,
        mean: torch.Tensor = torch.zeros(4),
        std: torch.Tensor = torch.ones(4),
        dropout_rate: float = 0.2
    ) -> None:

        """
        Инициализация модели.
        Параметры:
            input_size: int
                Размер входного слоя
            hidden_size: int
                Размер скрытых слоев
            mean: torch.Tensor
                Средние значения для нормализации
            std: torch.Tensor
                Стандартные отклонения для нормализации
            dropout_rate: float
                Вероятность отключения нейронов
        """

        super().__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.norm1 = nn.LayerNorm(hidden_size)
        self.fc2 = nn.Linear(hidden_size, hidden_size)
        self.norm2 = nn.LayerNorm(hidden_size)
        self.fc3 = nn.Linear(hidden_size, hidden_size)
        self.norm3 = nn.LayerNorm(hidden_size)
        self.out = nn.Linear(hidden_size, 1)
        
        self.act = nn.ReLU()
        self.dropout = nn.Dropout(dropout_rate)

        self.mean = mean
        self.std = std
        
        self.apply(self._init_weights)
        
    def initweights(self, module: nn.Module) -> None:
        """
        Инициализация весов слоев.
        Параметры:
            module: nn.Module
                Модуль для инициализации весов
        """

        if isinstance(module, nn.Linear):
            nn.init.kaiming_uniform_(
                module.weight,
                nonlinearity='relu'
            )

            nn.init.zeros_(module.bias)

    def forward(
        self,
        pressure: torch.Tensor,
        gas_temp: torch.Tensor,
        ambient_temp: torch.Tensor,
        wall_thickness: torch.Tensor
    ) -> torch.Tensor:

        """
        Прямой проход через сеть.
        Параметры:
            pressure: torch.Tensor
                Давление

            gas_temp: torch.Tensor
                Температура газа

            ambient_temp: torch.Tensor
                Температура окружения

            wall_thickness: torch.Tensor
                Толщина стенки

        Возвращает:
            torch.Tensor: Предсказанное значение максимального напряжения
        """

        batch_size = pressure.shape[0]

        x = torch.cat([
            pressure.view(batch_size, -1),
            gas_temp.view(batch_size, -1),
            ambient_temp.view(batch_size, -1),
            wall_thickness.view(batch_size, -1)
        ], dim=1)

        x = (x - self.mean) / self.std

        x = self.fc1(x)
        x = self.norm1(x)
        x = self.act(x)
        x = self.dropout(x)

        x = self.fc2(x)
        x = self.norm2(x)
        x = self.act(x)
        x = self.dropout(x)

        x = self.fc3(x)
        x = self.norm3(x)
        x = self.act(x)
        x = self.dropout(x)

        return self.out(x)
  • Строки 30-39. Объявление трех слоев с LayerNorm и Dropout для регуляризации.

  • Строки 41-42. Поскольку входные данные нормализуются внутри модели, mean и std передаются в конструктор.

  • Строки 44. Применяется начальная инициализация весов (строки 46-59).

  • Строки 61-110. Основной метод, forward, принимает необходимые входные параметры, объединяет их в один тензор и затем пропускает его через все слои в необходимом порядке.

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

В данном случае используется следующая конфигурация:

  • Размер скрытого слоя (hidden_size) установлен на 512 нейронов

  • Коэффициент прореживания (dropout_rate) равен 0.2

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

  • В качестве функции потерь выбрано среднеквадратичное отклонение (MSE)

  • Оптимизатор AdamW с коэффициентом обучения 5e-4 и L2-регуляризацией 1e-4

Объявление задачи и общая постановка задачи

hidden_size = 512
dropout_rate = 0.2

stats = pd.DataFrame(df[features])
mean = torch.tensor(stats.mean().values, dtype=torch.float32)
std = torch.tensor(stats.std().values,  dtype=torch.float32)

model = StressNet(
    input_size=len(features),
    mean=mean,
    std=std,
    hidden_size=hidden_size,
    dropout_rate=dropout_rate 
)

criterion = nn.MSELoss()
optimizer = optim.AdamW(model.parameters(), lr=5e-4, weight_decay=1e-4)

6.4.  Обучение модели

Функция train_model предназначена для обучения нейронной сети с использованием метода градиентного спуска. Вот основные компоненты и этапы работы функции:

  • Подготовка:

    • Перенос модели на указанное устройство.

    • Инициализация списков для отслеживания потерь.

    • Настройка ранней остановки (early stopping).

  • Цикл обучения:

    • Обучение на тренировочных данных.

    • Валидация на проверочных данных.

    • Отслеживание лучшей модели.

    • Проверка условий ранней остановки.

  • Мониторинг:

    • Отслеживание значения функции потерь.

    • Сохранение лучшей модели.

    • Вывод прогресса обучения.

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

Определение функции train_model

def train_model(
    model: nn.Module,
    train_loader: DataLoader,
    val_loader: DataLoader,
    criterion: nn.Module,
    optimizer: optim.Optimizer,
    epochs: int,
    device: torch.device = torch.device('cpu'),
    patience: int = 20
) -> Tuple[nn.Module, List[float], List[float]]:

    """
    Обучает модель нейронной сети.
    Параметры:
        model: nn.Module
            Модель нейронной сети для обучения
        train_loader: DataLoader
            Загрузчик данных для обучающей выборки
        val_loader: DataLoader
            Загрузчик данных для валидационной выборки
        criterion: nn.Module
            Функция потерь
        optimizer: optim.Optimizer
            Оптимизатор для обновления весов
        epochs: int
            Количество эпох обучения
        device: torch.device
            Устройство для вычислений (CPU/GPU)
        patience: int
            Количество проведенных эпох без улучшения, достаточное для ранней остановки обучения

    Возвращает:
        Tuple[nn.Module, List[float], List[float]]: Обученная модель, 
            список потерь на обучающей выборке, список потерь на валидационной выборке
    """

    model.to(device)
    train_losses = []
    val_losses = []

    best_val_loss = float('inf')
    no_improvement = 0
    best_model_state = None

    for epoch in tqdm(range(epochs), desc='Training'):
        model.train()
        train_loss = 0.0

        for inputs, targets in train_loader:
            optimizer.zero_grad()
            
            outputs = model(*inputs)
            loss = criterion(outputs, targets)

            loss.backward()
            optimizer.step()

            train_loss += loss.item() * targets.size(0)
        train_loss = train_loss / len(train_loader.dataset)
        train_losses.append(train_loss)

        model.eval()
        val_loss = 0.0

        with torch.no_grad():
            for inputs, targets in val_loader:
                outputs = model(*inputs)
                loss = criterion(outputs, targets)
                val_loss += loss.item() * targets.size(0)
        val_loss = val_loss / len(val_loader.dataset)
        val_losses.append(val_loss)
        
        if epoch % 10 == 0:
            print(
                f'Epoch {epoch+1}/{epochs}, '
                f'Train Loss: {train_loss:.6f}, '
                f'Val Loss: {val_loss:.6f}'
            )

        if val_loss < best_val_loss:
            best_val_loss = val_loss
            no_improvement = 0
            best_model_state = model.state_dict().copy()
        else:
            no_improvement += 1
            if no_improvement >= patience:
                print(f'Early stopping at epoch {epoch+1}')
                model.load_state_dict(best_model_state)
                break
                
    return model, train_losses, val_losses
  • Строки 49-58. Основной цикл обучения с вычислением потерь на тренировочном наборе данных.

  • Строки 66-71. Валидация модели и вычисление потерь на валидационном наборе.

  • Строки 83-92. Логика ранней остановки и сохранения лучшей модели.

Для обучения модели необходимо вызвать функцию train_model передав ей корректно созданные ранее параметры и сущности.

Вызов функции train_model

model, train_losses, val_losses = train_model(
    model=model,
    train_loader=train_loader,
    val_loader=test_loader,
    criterion=criterion,
    optimizer=optimizer,
    epochs=500,
    device=device,
    patience=10
)

6.5.  Оценка модели

Функция evaluate_model предназначена для оценки производительности обученной нейронной сети на тестовом наборе данных. Она выполняет следующие действия:

  • Подготовка модели:

    • Переводит модель в режим оценки (model.eval()).

    • Отключает вычисление градиентов для оптимизации памяти.

  • Обработка данных:

    • Проходит по всем батчам в DataLoader.

    • Получает предсказания модели.

    • Собирает реальные значения и предсказания в списки.

  • Расчет метрик:

    • MSE (Mean Squared Error) - средняя квадратичная ошибка.

    • RMSE (Root Mean Squared Error) - корень из средней квадратичной ошибки.

    • MAPE (Mean Absolute Percentage Error) - средняя абсолютная процентная ошибка.

    • MAE (Mean Absolute Error) - средняя абсолютная ошибка.

    • R² (R-squared) - коэффициент детерминации.

Определение функции evaluate_model

def evaluate_model(
    model: nn.Module,
    data_loader: DataLoader,
    criterion: nn.Module,
    device: torch.device = torch.device('cpu')
) -> Dict[str, Union[float, np.ndarray]]:

    """
    Оценивает производительность модели на заданном наборе данных.
    Параметры:
        model: nn.Module
            Модель PyTorch для оценки
        data_loader: DataLoader
            DataLoader с данными для оценки
        criterion: nn.Module
            Функция потерь
        device: torch.device
            Устройство для вычислений

    Возвращает:
        Dict[str, Union[float, np.ndarray]]: Словарь с метриками оценки
        и результатами предсказаний
    """

    model.to(device)
    model.eval()

    all_targets = []
    all_predictions = []
    total_loss = 0.0

    with torch.no_grad():
        for inputs, targets in data_loader:
            outputs = model(*inputs)
            loss = criterion(outputs, targets)
            total_loss += loss.item() * targets.size(0)
            all_targets.extend(targets.cpu().numpy())
            all_predictions.extend(outputs.cpu().numpy())

    all_targets = np.array(all_targets).flatten()
    all_predictions = np.array(all_predictions).flatten()

    mse = mean_squared_error(all_targets, all_predictions)
    rmse = np.sqrt(mse)
    mape = mean_absolute_percentage_error(all_targets, all_predictions)
    mae = mean_absolute_error(all_targets, all_predictions)
    r2 = r2_score(all_targets, all_predictions)

    avg_loss = total_loss / len(data_loader.dataset)

    return {
        'loss': avg_loss,
        'mse': mse,
        'rmse': rmse,
        'mape': mape,
        'mae': mae,
        'r2': r2,
        'targets': all_targets,
        'predictions': all_predictions
    }

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

Для получения всех метрик достаточно выполнить следующий код:

Вызов функции evaluate_model

test_results = evaluate_model(model, test_loader, criterion, device)

print("Test metrics:")
print(f"MSE: {test_results['loss']:.3f}")
print(f"RMSE: {test_results['rmse']:.3f}")
print(f"MAPE: {test_results['mape']*100:.3f}%")
print(f"MAE: {test_results['mae']:.3f}")
print(f"R^2: {test_results['r2']:.3f}")

Этот код выведет результаты тестирования модели.

Метрика

Значение

Интерпретация

MSE

5.281

Среднеквадратичная ошибка

RMSE

2.298

Корень из среднеквадратичной ошибки

MAPE

1.066%

Средняя абсолютная процентная ошибка

MAE

0.814

Средняя абсолютная ошибка

0.987

Коэффициент детерминации

6.6.  Визуализация результатов

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

Определение функции visualize_results

def visualize_results(
    train_losses: List[float],
    val_losses: List[float],
    test_results: Dict[str, np.ndarray]
) -> None:

    """
    Визуализирует результаты обучения модели.
    Параметры:
        train_losses: List[float]
            Список значений функции потерь на обучающей выборке
        val_losses: List[float]
            Список значений функции потерь на валидационной выборке
        test_results: Dict[str, np.ndarray]
            Словарь с результатами тестирования модели
    """

    _, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))

    ax1.plot(train_losses, label='Train Loss')
    ax1.plot(val_losses, label='Val Loss')
    ax1.set_xlabel('Epoch')
    ax1.set_ylabel('Loss')
    ax1.legend()
    ax1.grid(True)

    errors: np.ndarray = test_results['predictions'] - test_results['targets']
    sns.histplot(errors, bins=30)
    ax2.set_xlabel('Error')
    ax2.set_ylabel('Frequency')
    ax2.grid(True)

    plt.tight_layout()
    plt.show()

Вызовем эту функцию.

Вызов функции visualize_results

visualize_results(train_losses, val_losses, test_results)

В результате имеем два графика:

  • Первый — динамика функции потерь на обучении и валидации.

  • Второй — гистограмма распределения ошибок предсказания.

LossAndErrors
LossAndГрафики изменения значения функции потерь и распределения ошибок.Errors

6.7.  Экспорт модели в ONNX

Функция export_to_onnx предназначена для экспорта обученной PyTorch модели в формат ONNX. Функция учитывает необходимости перевода в режим double и учитывает имена входных и выходных параметров.

Определение функции export_to_onnx

def export_to_onnx(
    model: nn.Module,
    features: List[str],
    target: str,
    output_path: str = "stress_model.onnx"
) -> None:

    """
    Экспортирует PyTorch модель в формат ONNX.
    Args:
        model: nn.Module
            PyTorch модель для экспорта
        features: List[str]
            Список имен входных признаков
        target: str
            Имя целевой переменной
        output_path: str
            Путь для сохранения ONNX модели
    """

    model.eval()
    model.double()

    dummy_inputs: Dict[str, torch.Tensor] = {
        feature: torch.zeros(1, 1, dtype=torch.float64)
        for feature in features
    }

    torch.onnx.export(
        model,
        dummy_inputs,
        output_path,
        input_names=features,
        output_names=[target],
        opset_version=12
    )

    print(f"Model exported to {output_path}.")

Для конфертации обученной ранее модели необходимо передать ее в объявленную ранее модель и имена признаков и целевой переменной для корректного отображения в REPEAT.

Вызов функции export_to_onnx

export_to_onnx(model, features, target)

На следующем этапе сохранённая модель будет импортирована на платформу REPEAT в проект газопровода-шлейфа для выполнения междисциплинарного моделирования.

7. Интегрируем все в один проект

На рисунке ниже приведён проект на платформе REPEAT для моделирования газопровода-шлейфа с помощью блоков библиотек ПневматикаАвтоматика и Внешние модели. Данная модель газопровода-шлейфа позволяет:

  • Выполнять расчёт параметров системы сбора на соответствие техническому проекту разработки месторождения;

  • Определять параметры системы сбора при изменении входных параметров системы подготовки газа и конденсата.

../../../_images/repeat_project.png
Проект газопровода-шлейфа на платформе REPEAT

В рамках единого проекта на основании актуальных данных диагностики технического состояния газопровода-шлейфа также выполняется расчёт напряжений по Мизесу отвода с помощью блока №12 Аппроксимационная модель и импортированной в данный блок модели машинного обучения.

Проект, изображённый выше позволяет:

  • Выполнять расчётные эксперимены на модели машинного обучения,

  • В общей междисциплинарной модели выполнять разработку системы управления технологическим процессом добычи газа, учитывая оценку максимальных напряжений по Мизесу в отводе;

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

7.1.  Результаты расчётных экспериментов

Динамика уменьшения толщины стенки отвода приведена на рисунке ниже.

../../../_images/branch_wall_thicks.png
Динамика уменьшения минимальной толщины стенки отвода

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

../../../_images/approx_stress.png
Динамика увеличения максимальных напряжений по Мизесу в отводе

Заключение

Что же даёт интеграция ML‑моделей в REPEAT:

  • Открывает новые возможности: теперь можно закрывать больше задач в системном моделировании и создавать цифровые двойники даже для очень сложных систем  без необходимости собирать всё с нуля.

  • Позволяет спокойно работать как с тяжёлыми МКО/МКЭ, так и моделями, опирающимися на реальные данные с объектов, например, на результаты диагностики оборудования в эксплуатации.

  • Если сравнивать с подключением системных моделей напрямую к стороннему инженерному софту, то в нашем случае заметно спокойнее: меньше шансов нарваться на ошибку из‑за того, что расчёт «не сошёлся» (знакомая боль для МКЭ/МКО), плюс сами расчёты идут ощутимо быстрее.

В общем, интеграция не просто добавляет функционал — она реально экономит время!

Подписывайтесь на нас в TG | VK | Дзен | MAX

Комментарии (0)