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

Как мы пытались научить ML считать трудоёмкость в промышленности — и что из этого вышло

19м. чтение

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

В последние годы как представители управленческого звена, так и специалисты IT-подразделений промышленных предприятий всё чаще рассматривают методы анализа данных и машинного обучения как универсальный инструмент, способный «автоматически» решить задачу расчёта норм времени. Иногда — с избыточным оптимизмом. Часто при этом забывается простая, но важная мысль: если существующий процесс расчёта неточен и данные в нём некачественные, то автоматизация такого процесса лишь ускоряет распространение ошибок. Особенно если применять вероятностные модели к накопленным за годы «грязным» данным.

Именно с такой реальностью нам (мне и коллегам) и пришлось столкнуться.

Постановка задачи

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

В рамках договора требовалось реализовать HTTP-сервис, который:

  • принимает данные из 1С в формате JSON;

  • возвращает рассчитанную норму времени;

  • дополнительно сообщает степень уверенности модели в прогнозе.

Сервис должен был быть реализован «максимально быстро и просто», как сформулировал заказчик.

На входе — порядка 20 параметров (название детали, обрабатывающий центр, масса, инструмент и т. п.), многие из которых были заполнены частично или нерегулярно. Обучающая выборка содержала около 86 000 строк. разброс значений нормы времени составлял от 0,1 до ~2000 часов;

Требования к точности были сформулированы достаточно мягко:

  • 80 % прогнозов должны укладываться в погрешность не более 5 % от максимального значения диапазона нормы времени, но не ниже 0,1 часа;

  • модель должна проходить F-тест (критерий Фишера) при уровне значимости 0,05 как на обучающей, так и на тестовой выборке.

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

Общая архитектура решения

В дальнейшем я не буду подробно останавливаться на инфраструктурной части. Отмечу лишь, что для скорости разработки сервис был реализован на Python с использованием FastAPI, в качестве СУБД выбрана SQLite.

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

И начался этот путь, как водится, с предобработки данных — уже на этом этапе появились первые сомнения в том, что заказчик будет полностью доволен результатом.

Предобработка данных

Процесс предобработки данных включал в себя следующие этапы: 1) проверка типов признаков; 2) обработка пропущенных значений; 3) кодирование категориальных признаков; 4) масштабирование признаков; 5) анализ выбросов и исключение незначимых признаков.

Всего исходный набор данных содержал 19 входных признаков и одну целевую переменную — норму времени. Из них 5 признаков были числовыми, остальные — категориальными. Данные загружались в структуры DataFrame и Series библиотеки pandas. Для числовых признаков пропущенные значения заменялись средним, для категориальных — наиболее часто встречающимся значением. Код начальной обработки выглядел следующим образом:

Xdf = pd.DataFrame(X) columns_of_X = list(X.keys()) # === Числовые колонки === num_cols = Xdf.select_dtypes(include=['int64', 'float64']).columns num_imputer = SimpleImputer(strategy='mean') Xdf[num_cols] = num_imputer.fit_transform(Xdf[num_cols]) # === Категориальные колонки === cat_cols = Xdf.select_dtypes(include=['object', 'category']).columns cat_imputer = SimpleImputer(strategy='most_frequent') Xdf[cat_cols] = cat_imputer.fit_transform(Xdf[cat_cols]) # Формирование словаря заменяющих значений impute_values = {} for col, val in zip(num_cols, num_imputer.statistics_): impute_values[col] = float(val) # Преобразуем np.float64 -> float for col, val in zip(cat_cols, cat_imputer.statistics_): impute_values[col] = val

Для кодирования категориальных признаков была выбрана технология TF-IDF (Term Frequency – Inverse Document Frequency) – это метод представления текстов в виде числовых векторов, который позволяет оценить важность слов в документе относительно корпуса текстов. Он основан на двух компонентах: частоте термина (TF), отражающей, как часто слово встречается в документе, и обратной частоте документа (IDF), уменьшающей вес слов, часто встречающихся во всех текстах (например, предлогов или союзов). Формально вес термина t в документе d вычисляется как:

TFIDF\left(t,d\right)=TF\left(t,d\right) \cdot IDF\left(t\right)

где

IDF\left(t\right)=log\left(N_d/\left(1+n_t\right)\right)

Nd — общее количество документов, а nt — число документов, содержащих термин t.

Процесс векторизации с помощью TF-IDF включает несколько шагов. Сначала строится словарь всех уникальных терминов в коллекции текстов. Каждый документ преобразуется в вектор, где каждому термину соответствует числовое значение его TF-IDF-веса. Часто применяются дополнительные этапы предобработки, такие как токенизация, приведение слов к нормальной форме и удаление стоп-слов. В результате текстовые данные преобразуются в матрицу "документ-термин", где каждая строка — это документ, а каждый столбец — взвешенный показатель важности конкретного термина.

Для числовых признаков было выполнено масштабирование (вычтено среднее и разница поделена на стандартное отклонение).

Ниже приведен код создания объекта preprocessor класса ColumnTransformer библиотеки Scikit-learn для выполнения описанных выше преобразований данных в столбцах входного набора данных. Названия столбцов входного набора преобразуются в латиницу (если они на кириллице) и нижний регистр с помощью функции translit_to_eng.

from sklearn.compose import ColumnTransformer def translit_to_eng(s: str) -> str: d = {'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo', 'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'ij', 'к': 'k', 'л': 'l', 'м': 'm', 'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u', 'ф': 'f', 'х': 'h', 'ц': 'c', 'ч': 'ch', 'ш': 'sh', 'щ': 'shch', 'ь': '', 'ы': 'y', 'ъ': '', 'э': 'r', 'ю': 'yu', 'я': 'ya', ' ': '_'} return "".join(map(lambda x: d[x] if d.get(x, False) else x, s.lower())) list_for_transformer = [] for item in columns_of_X: if item in cat_cols: # Исключение чисел Xdf[item] = Xdf[item].astype(str) list_for_transformer.append((translit_to_eng(item)[0:6], TfidfVectorizer(), item)) else: list_for_transformer.append((translit_to_eng(item)[0:6], StandardScaler(), [item])) preprocessor = ColumnTransformer( transformers=list_for_transformer, remainder='drop' )

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

Для целевой переменной рассчитывались основные описательные статистики (минимум, максимум, среднее, стандартное отклонение, квартили), а также коэффициент асимметрии. Наличие длинного хвоста оценивалось по сравнению межквантильных расстояний: распределение считалось асимметричным при существенном превышении разности 𝑞99 − 𝑞50 над 𝑞50−𝑞01. Такой анализ позволяет выявить дисбаланс значений и потенциальные риски смещения моделей в сторону редких экстремальных наблюдений.

Для числовых признаков дополнительно анализировались: доля пропущенных значений; количество уникальных значений; степень дисбаланса (признак считался несбалансированным, если одно значение занимало более 90% наблюдений); статистическая связь с целевой переменной с использованием F-критерия линейной регрессии.

Для категориальных признаков оценивались: частота наиболее и наименее представленных категорий; наличие редких категорий; значимость различий целевой переменной между категориями с применением однофакторного дисперсионного анализа (ANOVA).

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

import pandas as pd import json import numpy as np from sklearn.feature_selection import f_regression from scipy.stats import f_oneway, skew import matplotlib.pyplot as plt def load_json_or_raise(path: str) -> pd.DataFrame: with open(path) as f: json_dict = json.load(f) X = json_dict["X"] y = np.asarray(json_dict["Y"]).reshape(((len(json_dict["Y"]),))) X["Нормочасы"] = y Xdf = pd.DataFrame(X) return Xdf # ==== ЗАГРУЗКА ==== data_path = "D:\\Data.xlsx" target_col = "Нормочасы" df = pd.read_excel(data_path) if target_col not in df.columns: raise ValueError(f"Целевая переменная '{target_col}' не найдена") y = df[target_col] X = df.drop(columns=[target_col]) results = [] plt.hist(y,40) plt.show() # ==== ЦЕЛЕВАЯ ПЕРЕМЕННАЯ — ОЦЕНКА СБАЛАНСИРОВАННОСТИ ==== target_stats = { "min": y.min(), "max": y.max(), "mean": y.mean(), "std": y.std(), "skew": skew(y, nan_policy='omit'), "q01": y.quantile(0.01), "q25": y.quantile(0.25), "q50": y.quantile(0.50), "q75": y.quantile(0.75), "q99": y.quantile(0.99) } print("=== Анализ целевой переменной ===") for k, v in target_stats.items(): print(f"{k}: {v}") # Критерий длинного хвоста: если (q99 - q50) >> (q50 - q01) if (target_stats["q99"] - target_stats["q50"]) > 3 * (target_stats["q50"] - target_stats["q01"]): print("Целевая переменная имеет длинный хвост (дисбаланс значений)") # ==== Разделение признаков ==== num_features = X.select_dtypes(include=[np.number]).columns.tolist() cat_features = [c for c in X.columns if c not in num_features] # ЧИСЛОВЫЕ if num_features: from sklearn.impute import SimpleImputer X_num = X[num_features] illoc_idx = [] for idx in range(len(num_features)): miss_ratio = X_num[num_features[idx]].isna().mean() if (miss_ratio==1): illoc_idx.append(idx) for idx in illoc_idx: X_num = X_num.drop(num_features[idx], axis=1) num_features = [elem for idx, elem in enumerate(num_features) if idx not in illoc_idx] valid_idx = ~y.isna() X_num = X_num[valid_idx] y_num = y[valid_idx] f_vals, p_vals = f_regression(SimpleImputer(strategy="mean").fit_transform(X_num), y_num) for col, fval, pval in zip(num_features, f_vals, p_vals): miss_ratio = X_num[col].isna().mean() uniq = X_num[col].nunique() print(col + ": min: " + str(X_num[col].min()) + ": max: " + str(X_num[col].max())) # Дисбаланс уникальных значений (если одно значение занимает > 90%) top_freq = X_num[col].value_counts(normalize=True, dropna=False).max() imbalance_flag = top_freq > 0.9 results.append({ "Признак": col, "Тип": "Числовой", "Пропуски_%": round(miss_ratio * 100, 2), "Уникальных": uniq, "Доля_частого_значения": round(top_freq * 100, 2), "Дисбаланс": "Да" if imbalance_flag else "Нет", "F_value": round(fval, 4), "p_value": round(pval, 4), "Рекомендация": "Использовать" if pval < 0.05 and not imbalance_flag else "Низкая значимость или дисбаланс" }) # КАТЕГОРИАЛЬНЫЕ if cat_features: for col in cat_features: s = X[col].astype(str) miss_ratio = s.isna().mean() uniq = s.nunique() freq = s.value_counts(normalize=True) top_freq = freq.max() rare_freq = freq.min() imbalance_flag = top_freq > 0.9 or rare_freq < 0.01 try: groups = [y[s == cat] for cat in s.unique() if len(y[s == cat]) > 1] if len(groups) > 1: fval, pval = f_oneway(*groups) rec = "Использовать" if pval < 0.05 and not imbalance_flag else "Низкая значимость или дисбаланс" else: fval, pval, rec = np.nan, np.nan, "Мало категорий" except Exception: fval, pval, rec = np.nan, np.nan, "Ошибка расчёта" results.append({ "Признак": col, "Тип": "Категориальный", "Пропуски_%": round(miss_ratio * 100, 2), "Уникальных": uniq, "Доля_частой_категории": round(top_freq * 100, 2), "Редкая_категория_%": round(rare_freq * 100, 2), "Дисбаланс": "Да" if imbalance_flag else "Нет", "F_value": fval, "p_value": pval, "Рекомендация": rec }) # ==== РЕЗУЛЬТАТ ==== df_results = pd.DataFrame(results).sort_values(by=["Тип", "p_value"], na_position='last') print("\n=== Оценка признаков ===") print(df_results) df_results.to_excel("feature_adequacy_with_balance.xlsx", index=False)

На основании проведенного анализа было оставлено 12 признаков. На рисунке 1 приведена гистограмма распределения прогнозируемой величины по диапазонам.

Рисунок 1 – Распределение трудоёмкостей на 16 интервалах
Рисунок 1 – Распределение трудоёмкостей на 16 интервалах

Минимальное значение прогнозируемой величины составило 0,01 ч, среднее 4,69 ч и медиана 0,25 ч. За границу погрешности прогноза была взята величина 4 ч, что составляет менее 1% от максимального значения диапазона обучающей выборки.

Первые модели и выбор архитектуры

Изначально предполагалось использовать одну регрессионную модель. Были протестированы: гребневая регрессия (Kernel Ridge Regression, KRR), Метод опорных векторов (SVR), Случайный лес (RF) и нейронная сеть в архитектуре многослойного персептрона (MLP). Обучение и тестирование проводились с кросс-валидацией и подбором гиперпараметров. Ниже код, в котором показано использование модели MLPRegressor библиотеки scikit-learn, в сочетании с предобработкой данных (preprocessor), рассмотренном ранее, для обучения и сохранения регрессионной модели:

from sklearn.neural_network import MLPRegressor from sklearn.model_selection import train_test_split from sklearn.pipeline import Pipeline import pickle def train_predict(X, y, preprocessor): """ :param X: DataSet входных параметров :param y: целевая переменная :param preprocessor: :return: y_pred_train, y_pred, y_train, y_test - прогнозы и истинные значения целевой переменной на обучении и тесте """ parameters = [(512, 256, 128, 64), 'relu', 'lbfgs', 0.001, 'constant', 0.01, 500] plTrain = 1 saveFileName = 'MLPRegressor.pkl' model = MLPRegressor(early_stopping=True, hidden_layer_sizes=parameters[0], activation=parameters[1], solver=parameters[2], alpha=parameters[3], learning_rate=parameters[4], learning_rate_init=parameters[5], max_iter=parameters[6], random_state=42) # Пайплайн с регрессором pipeline = Pipeline(steps=[ ('preprocess', preprocessor), ('regressor', model) ]) # Разбить на обучение и тест X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.9, random_state=42) if (plTrain == 1): #Обучение pipeline.fit(X_train, y_train) pickle.dump(pipeline, open(saveFileName, 'wb')) else: pipeline = pickle.load(open(saveFileName, 'rb')) #Результаты прогноза обучающей и тестовой выборки y_pred_train = pipeline.predict(X_train) y_pred = pipeline.predict(X_test) # Округление результатов y_pred_train = np.round(y_pred_train, 2) y_pred = np.round(y_pred, 2) return y_pred_train, y_pred, y_train, y_test

Поиск параметров и точности прогнозов выполнялся на небольшой части выборки (10000 экземпляров) взятой несколько раз случайным образом. Обучение и тестирование моделей проводилось по следующему плану: стандартизация признаков; 5-кратная кросс-валидация; случайный поиск с использованием метрик коэффициент детерминации R2 и среднеквадратичная ошибка MSE.

R2 — коэффициент детерминации в регрессионном анализе. Он показывает долю вариации зависимой переменной, объясняемую независимыми переменными в модели. Фактически, R2 отвечает на вопрос: «Насколько хорошо модель описывает изменения в наблюдаемых данных?».

MSE=1/n\sum_{j=1}^{n} {\left(y^f_j-y_j\right)^2}

где n - общее количество наблюдений; yjf - прогнозируемые значения; yj – наблюдаемые (истинные) значения.

from sklearn.metrics import r2_score from sklearn.metrics import mean_squared_error def r_mse_score(y, predictions): """ :param y: истина :param predictions: прогноз :return:r2 - коэффициент детерминации; MSE - среднеквадратическая ошибка """ r2 = r2_score(y, predictions) MSE = mean_squared_error(y, predictions) return r2, MSE

После подбора гиперпараметров, на финальном тестировании всей генеральной совокупности точность оценивалось по комплексу метрик, включающий: R2 , F - статистика, точность acl в зависимости от выбранной границы l.

F-статистика (критерий Фишера) — оценка того, являются ли наблюдаемые различия или взаимосвязи в данных статистически значимыми.

Точность acl вычисляется по формуле:

ac_l=N_l/N

где Nl – количество верно спрогнозированных величин, входящих в допуск; N – общее количество прогнозируемых величин.

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

from sklearn.metrics import r2_score from scipy.stats import f def f_test_function(y, ypred,m,n): F = (np.sum((ypred - np.mean(ypred)) ** 2)) / (np.sum((y - ypred) ** 2)) * ((n-1)/(n - m - 1)) dfn = y.size - 1 #число степеней свободы (числитель) dfd = ypred.size - 1 #знаменатель степеней свободы p = 1 - f.cdf(F, dfn, dfd) #расчет p-значение F-критерия return F, p def accuracy_score_handmade_stat(y, predictions,accuracy_boundary, m): """ :param y: истина :param predictions: прогноз :param accuracy_boundary: граница, выше которой прогноз не допустимо ошибочен :param m: количество входных параметров модели регрессии :return: accuracy - точность, r2 - коэффициент детерминации, F - критерий Фишера, p - p-значение F-критерия """ dif = predictions-y idx = np.where(np.abs(dif) < accuracy_boundary[0])[0] accuracy = idx.shape[0]/y.shape[0]*100 r2 = r2_score(y, predictions) F, p = f_test_function(y, predictions, m, y.shape[0]) return accuracy,r2, F, p

При финальном тестировании моделей с подобранными гиперпараметрами использовалась обучающая выборка 77692 случая и тестовая 8632 случая. Как отмечалось в разделе про предобработку данных, граница l была принята равной 4 ч. В таблице 7 приведены метрики точности обучающей и тестовой выборок для четырех моделей.

Таблица 1 – Метрики точности обучающей и тестовой выборок для рассматриваемых моделей

Модель

Обучение

Тест

acl

R2

F-статистика

acl

R2

F-статистика

Случайный лес

90,181

0,934

12,170

89,586

0,874

7,050

Гребневая регрессия

98,358

0,994

163,773

87,399

0,665

2,003

Метод опорных векторов

93,618

0,502

0,636

90,989

0,623

0,993

Нейронные сети

94,218

0,984

61,724

92,354

0,908

10,942

Табличные значения критерия Фишера для обучающей и тестовой выборок равны 1,79 в обоих случаях (при превышении этого значения F-статистики доказывается статистическая значимость модели). По этому критерию не проходит модель SVR. Лучшие результаты по точности, критерию R2 и F-статистике для тестовой выборки показала модель MLPRegressor, используемая в дальнейшем.

Когда теория встретилась с реальностью

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

Результаты тестирования подходили под требования технического задания. Приведу значения acl для разных границ допуска, от 0,01 до 0,4.

Таблица 2 – Точность acl для разных границ на тесте

Величина границы (допуска), ч

acl , % случаев, значение ошибок в которых ниже границы

0,01

7,69

0,02

10

0,05

30

0,1

51,54

0,2

72,31

0,3

77,69

0,4

87,69

И именно здесь стало очевидно, что формальное выполнение ТЗ не означает практической пригодности модели. Хотя абсолютные метрики выглядели приемлемо, требование заказчика ужесточилось: не менее 50 % прогнозов должны иметь относительную ошибку менее 20 %.Напомню, что такое относительная погрешность:

\delta=\frac{\left|y^f- y\right|}{y}\cdot100

В текущем же решении на тесте указанному выше критерию погрешности соответствовало менее 7%.

Следующий шаг – каскад моделей

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

Принцип работы каскада следующий.

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

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

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

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

Таблица 3 - Точность acl при каскаде моделей

Величина границы (допуска), ч

acl, % случаев, значение ошибок в которых ниже границы

0,01

21,54

0,02

45,38

0,05

75,38

0,1

87,69

0,2

100

0,3

100

0,4

100

Результаты существенно улучшились, но даже в этом случае только у 35,38% случаев относительная ошибка была меньше 20%, что все равно не проходило по новым требованиям.

Разбивка по производственным участкам

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

Для рассматриваемого участка обучающая выборка составила 10122 значений. Был убран «длинный хвост» и оставлены только данные, где норма времени не превышала 0,5 ч – 9929 значений.

Была использована одна модель и в этот раз в 20% относительно погрешности уложилось 43,75% результатов.

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

Удаление «грязных» данных

По рассматриваемому участку было выявлено, что существуют «грязные» данные – когда в целевой переменной при внесении в систему человек делал опечатки, например, вместо 0,015 ч вносил 0,15 ч. В разрезе второго параметра (инструмента) нормы времени не должны были сильно отличаться – точно не на порядок. Но зачастую картинка выглядела следующим образом:

Рисунок 2 – Пример норм времени и прогноза для одного инструмента
Рисунок 2 – Пример норм времени и прогноза для одного инструмента

На рисунке горизонтальные линии ±1STD – это среднеквадратическое отклонение по рассматриваемой выборке от среднего значения на ней. Видим, что 9 значений из 35 на рисунке – это выбросы, влияющие на качество обучения моделей и снижающих точность метрик. По этой причине эти выбросы были исключены при превышении нижней или верхней границы среднеквадратического отклонения от среднего. В дополнение к двум параметрам фильтрации еще подвергся массив дат записей (из исходного DataSet), они пригодятся в дальнейшем улучшении. Код функции для отсева выпадающих значений приведен ниже.

def filter_of_emissions_dates(Xdf1, y_all, dates, field_name): """ :param Xdf1: DataSet входных данных :param y_all: целевая переменная :param dates: массив дат записей :param field_name: поле входных данных, по которому они группируются для фильтрации :return: X, y, d - очищенные DataSet входных параметров, массив целевой переменной и массив дат """ list_of_compl = list(set(Xdf1[field_name])) X = Xdf1.copy() X.drop(X.index, inplace=True) d = dates.copy() d.drop(d.index, inplace=True) y = np.array([]) for item in list_of_compl: # Основной цикл: выбираем данные по конкретному инструменту, находим среднее (mean_) и СКО (std_), удаляем выходящие за границы данные indexes = Xdf1.index[Xdf1[field_name] == item].tolist() if (len(indexes) < 3): continue Xdf = Xdf1.iloc[indexes].reset_index().drop('level_0', axis=1) d_ = dates.iloc[indexes].reset_index().drop('index', axis=1) y_ = y_all[indexes] mean_ = np.mean(y_) std_ = np.std(y_) idx_min = np.where(y_>mean_-1*std_)[0].astype("int64") y_ = y_[idx_min] Xdf = Xdf.iloc[idx_min].reset_index().drop('level_0', axis=1) d_ = d_.iloc[idx_min].reset_index().drop('index', axis=1) idx_max = np.where(y_ < mean_ + 1 * std_)[0].astype("int64") y_ = y_[idx_max] Xdf = Xdf.iloc[idx_max].reset_index().drop('level_0', axis=1) d_ = d_.iloc[idx_max].reset_index().drop('index', axis=1) X = pd.concat([X, Xdf], axis=0, ignore_index=True) d = pd.concat([d, d_], axis=0, ignore_index=True) y=np.append(y, y_, axis=None) return X, y, d

После описанной выше нехитрой фильтрации (которую, по-хорошему, нужно сделать записывающим эти данные специалистам вручную для исключения возможных ошибок), осталось 6326 значений, то есть 36% исходных данных были с высокой долей вероятности не верными! После фильтрации уже 49,22% теста укладывалось в требуемую погрешность.

Количество слоев нейронной сети было уменьшено (с четырех до двух) что позволило повысить результат еще до 53,13%.

Добавление исторических данных

Кроме отбрасывания «длинного хвоста», оптимизации состава входных признаков и модели, фильтрации данных была проверена идея увеличить веса модели при обучении новым данным и уменьшить у старых. Однако в MLPRegressor нет возможности напрямую сделать возрастание весов по мере «новизны» данных при обучении. Поэтому был выбран обходной путь, эмулирующий этот процесс: у экземпляров с «большим» весом повторены несколько раз в обучающей выборке; экземпляры с «малым» весом – оставлены в меньшем числе копий. Конкретно данные были разбиты на 5 категорий по времени и количество повышалось линейно, самые старые поступали в одном экземпляре, а самые новые – в пяти. Код для этого процесса при веден ниже:

import numpy as np import pandas as pd #columns_of_X - список названий столбцов X # Веса: новые данные важнее weights = np.linspace(1, 5, y.shape[0]).astype('int64') # Эмулируем веса повторением объектов X_weighted = pd.DataFrame(np.repeat(X, weights, axis=0), columns=columns_of_X) y_weighted = np.repeat(y, weights, axis=0)

Обучение таким образом было проведено с X_weighted и y_weighted, что принесло повышение количества «подходящих» результатов до 58,59% на тесте.

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

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

from sklearn.pipeline import Pipeline from sklearn.linear_model import Ridge from sklearn.base import BaseEstimator, TransformerMixin class TargetEncoder(BaseEstimator, TransformerMixin): def __init__(self, min_samples_leaf=10, smoothing=5): self.min_samples_leaf = min_samples_leaf self.smoothing = smoothing self.mapping_ = None self.global_mean_ = None self.colname_ = None def fit(self, X, y): # X приходит как DataFrame -> берём первый столбец self.colname_ = X.columns[0] X_series = X[self.colname_] y_series = pd.Series(y) self.global_mean_ = y_series.mean() stats = y_series.groupby(X_series).agg(["mean", "count"]) smoothing_weights = 1 / (1 + np.exp(-(stats["count"] - self.min_samples_leaf) / self.smoothing)) means = self.global_mean_ * (1 - smoothing_weights) + stats["mean"] * smoothing_weights self.mapping_ = means.to_dict() return self def transform(self, X): X_series = X[self.colname_] return X_series.map(self.mapping_).fillna(self.global_mean_).values.reshape(-1, 1) parameters = [0.5, 0.1, 0.0001] #Подобранные оптимальные параметры pipeline = Pipeline(steps=[ ("preprocess", TargetEncoder(min_samples_leaf=parameters[0], smoothing=parameters[1])), ("regressor", Ridge(alpha=parameters[2], random_state=42)) ]) #Обучение pipeline.fit(X_train, y_train)

Результат тестирования обученной линейной модели не превзошел MLPRegressor: дал только 47,65% значений, не превышающих 20% порог погрешности.

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

Таблица 4 – Сравнение точностей acl при постепенной эволюции подхода к обучению

l, ч

acl, %

Модель в разрезе участка

После оптимизации входных параметров фильтра «грязных» данных

После оптимизации структуры модели

После увеличения доли «новых» данных

Простая линейная модель

0,01

21,88

35,94

35,94

46,62

35,16

0,02

46,09

53,12

58,59

57,03

56,25

0,05

76,56

89,06

92,19

89,84

88,28

0,1

88,28

97,66

97,66

97,66

96,09

0,2

100

99,22

99,22

100

99,22

0,3

100

100

100

100

100

0,4

100

100

100

100

100

Эпилог

Описанный в предыдущих разделах путь проработки моделей был выполнен для еще пяти участков. Для данных, не относящихся к ним, была оставлена общая «каскадная» модель. В итоге модель стала заметно точнее и в тоже время значительно сложнее. Итоговый объём хранимых моделей вырос с 114 МБ до 816 МБ, а логика принятия решений — стала многоступенчатой.

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

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

Источник

Возможности рынка
Логотип Mintlayer
Mintlayer Курс (ML)
$0.01049
$0.01049$0.01049
-1.68%
USD
График цены Mintlayer (ML) в реальном времени
Отказ от ответственности: Статьи, размещенные на этом веб-сайте, взяты из общедоступных источников и предоставляются исключительно в информационных целях. Они не обязательно отражают точку зрения MEXC. Все права принадлежат первоисточникам. Если вы считаете, что какой-либо контент нарушает права третьих лиц, пожалуйста, обратитесь по адресу [email protected] для его удаления. MEXC не дает никаких гарантий в отношении точности, полноты или своевременности контента и не несет ответственности за любые действия, предпринятые на основе предоставленной информации. Контент не является финансовой, юридической или иной профессиональной консультацией и не должен рассматриваться как рекомендация или одобрение со стороны MEXC.