474 KiB
Данные по инсультам¶
Выведем информацию о столбцах датасета:
import pandas as pd
from sklearn import set_config
set_config(transform_output="pandas")
random_state=9
df = pd.read_csv("./csv/option4.csv", index_col="id")
df
Бизнес-цели¶
Классификация¶
Цель: сделать модель, которая на основе данных про здоровье, образ жизни и соцдем-факторы будет предсказывать риск инсульта.¶
Применение: Для врачей: чтобы находить пациентов с высоким риском и вовремя их спасать. Для медсистем: можно встроить в карты пациентов, чтобы система сама предупреждала врача. Для людей: повышать осведомленность о факторах риска и как их избежать.
Регрессия¶
Цель: сделать модель, которая будет предсказывать уровень глюкозы на основе тех же факторов. Поможет отслеживать изменения и оценивать риски.¶
Применение: Для врачей: находить пациентов с риском диабета и другими проблемами, чтобы сразу назначать профилактику. Для медсистем: встроить в записи пациентов, чтобы врачи видели уровень глюкозы даже без лабораторий. Для населения: обучать, как lifestyle влияет на глюкозу, и рекомендовать изменения.
Качество моделей¶
Классификация (инсульт):¶
Признаки норм: возраст, гипертония, сердце — инфа полезная, модель будет неплохо различать риск. Проблемы: нет данных про гены, детальную историю болезней, питание, физнагрузки. Это режет потолок точности.
Регрессия (глюкоза):¶
Признаки норм: возраст, курение, давление — связь с глюкозой есть, что-то предскажет. Проблемы: не хватает данных про еду, активность, гормоны, поэтому точность ограничена.
Классификация¶
Разделим набор данных на на обучающую и тестовые выборки (80/20). Целевой признак - stroke
from typing import Tuple
from pandas import DataFrame
from sklearn.model_selection import train_test_split
def split_stratified_into_train_val_test(
df_input,
stratify_colname="y",
frac_train=0.6,
frac_val=0.15,
frac_test=0.25,
random_state=None,
) -> Tuple[DataFrame, DataFrame, DataFrame, DataFrame, DataFrame, DataFrame]:
"""
Splits a Pandas dataframe into three subsets (train, val, and test)
following fractional ratios provided by the user, where each subset is
stratified by the values in a specific column (that is, each subset has
the same relative frequency of the values in the column). It performs this
splitting by running train_test_split() twice.
Parameters
----------
df_input : Pandas dataframe
Input dataframe to be split.
stratify_colname : str
The name of the column that will be used for stratification. Usually
this column would be for the label.
frac_train : float
frac_val : float
frac_test : float
The ratios with which the dataframe will be split into train, val, and
test data. The values should be expressed as float fractions and should
sum to 1.0.
random_state : int, None, or RandomStateInstance
Value to be passed to train_test_split().
Returns
-------
df_train, df_val, df_test :
Dataframes containing the three splits.
"""
if frac_train + frac_val + frac_test != 1.0:
raise ValueError(
"fractions %f, %f, %f do not add up to 1.0"
% (frac_train, frac_val, frac_test)
)
if stratify_colname not in df_input.columns:
raise ValueError("%s is not a column in the dataframe" % (stratify_colname))
X = df_input # Contains all columns.
y = df_input[
[stratify_colname]
] # Dataframe of just the column on which to stratify.
# Split original dataframe into train and temp dataframes.
df_train, df_temp, y_train, y_temp = train_test_split(
X, y, stratify=y, test_size=(1.0 - frac_train), random_state=random_state
)
if frac_val <= 0:
assert len(df_input) == len(df_train) + len(df_temp)
return df_train, pd.DataFrame(), df_temp, y_train, pd.DataFrame(), y_temp
# Split the temp dataframe into val and test dataframes.
relative_frac_test = frac_test / (frac_val + frac_test)
df_val, df_test, y_val, y_test = train_test_split(
df_temp,
y_temp,
stratify=y_temp,
test_size=relative_frac_test,
random_state=random_state,
)
assert len(df_input) == len(df_train) + len(df_val) + len(df_test)
return df_train, df_val, df_test, y_train, y_val, y_test
X_train, X_val, X_test, y_train, y_val, y_test = split_stratified_into_train_val_test(
df, stratify_colname="stroke", frac_train=0.80, frac_val=0, frac_test=0.20, random_state=random_state
)
display("X_train", X_train)
display("y_train", y_train)
display("X_test", X_test)
display("y_test", y_test)
Выберем ориентир для задачи классификации. Для этого применим алгоритм случайного предсказания, т.е. в каждом случае в качестве предсказания выберем случайный класс.
import numpy as np
from sklearn.metrics import precision_score, recall_score, accuracy_score, f1_score
# Получаем уникальные классы для целевого признака из тренировочного набора данных
unique_classes = np.unique(y_train)
# Генерируем случайные предсказания, выбирая случайное значение из области значений целевого признака
random_predictions = np.random.choice(unique_classes, size=len(y_test))
# Вычисление метрик для ориентира
baseline_accuracy = accuracy_score(y_test, random_predictions)
baseline_precision = precision_score(y_test, random_predictions)
baseline_recall = recall_score(y_test, random_predictions)
baseline_f1 = f1_score(y_test, random_predictions)
print('Baseline Accuracy:', baseline_accuracy)
print('Baseline Precision:', baseline_precision)
print('Baseline Recall:', baseline_recall)
print('Baseline F1 Score:', baseline_f1)
Метрики модели¶
- Accuracy: доля правильных предсказаний от общего числа примеров. Простая, но бесполезная метрика в задачах с дисбалансом классов — не учитывает, как модель работает с редким классом.
- Precision: доля правильных предсказаний положительного класса среди всех предсказанных положительных. Полезна, если критичны ложные срабатывания (например, чтобы не ошибаться с инсультом).
- Recall: доля найденных объектов положительного класса среди всех реальных примеров положительного класса. Помогает понять, насколько хорошо модель "ловит" положительный класс. Важна, чтобы минимизировать пропуски инсультов.
- F1 Score: гармоническое среднее между precision и recall. Учитывает и точность, и полноту, что важно в задачах с несбалансированными классами. Эти метрики показывают разные аспекты работы модели: от общего уровня точности до способности находить редкие классы и балансировать между precision и recall. Это позволяет оценить модель всесторонне.
Сформируем конвейер для классификации¶
from sklearn.compose import ColumnTransformer
from sklearn.discriminant_analysis import StandardScaler
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder
columns_to_drop = ["work_type", "stroke"]
columns_not_to_modify = ["hypertension", "heart_disease"]
num_columns = [
column
for column in df.columns
if column not in columns_to_drop
and column not in columns_not_to_modify
and df[column].dtype != "object"
]
cat_columns = [
column
for column in df.columns
if column not in columns_to_drop
and column not in columns_not_to_modify
and df[column].dtype == "object"
]
num_imputer = SimpleImputer(strategy="median")
num_scaler = StandardScaler()
preprocessing_num = Pipeline(
[
("imputer", num_imputer),
("scaler", num_scaler),
]
)
cat_imputer = SimpleImputer(strategy="constant", fill_value="unknown")
cat_encoder = OneHotEncoder(handle_unknown="ignore", sparse_output=False, drop="first")
preprocessing_cat = Pipeline(
[
("imputer", cat_imputer),
("encoder", cat_encoder),
]
)
features_preprocessing = ColumnTransformer(
verbose_feature_names_out=False,
transformers=[
("prepocessing_num", preprocessing_num, num_columns),
("prepocessing_cat", preprocessing_cat, cat_columns),
],
remainder="passthrough"
)
drop_columns = ColumnTransformer(
verbose_feature_names_out=False,
transformers=[
("drop_columns", "drop", columns_to_drop),
],
remainder="passthrough",
)
pipeline_end = Pipeline(
[
("features_preprocessing", features_preprocessing),
("drop_columns", drop_columns),
]
)
Теперь проверим работу конвейера:
preprocessing_result = pipeline_end.fit_transform(X_train)
preprocessed_df = pd.DataFrame(
preprocessing_result,
columns=pipeline_end.get_feature_names_out(),
)
preprocessed_df
Подберем оптимальные гиперпараметры для каждой из выбранных моделей методом поиска по сетке и сформируем их набор.
knn -- k-ближайших соседей
random_forest -- метод случайного леса (набор деревьев решений)
mlp -- многослойный персептрон (нейронная сеть)
from sklearn.model_selection import GridSearchCV
from sklearn import neighbors, ensemble, neural_network
# Словарь с вариантами гиперпараметров для каждой модели
param_grids = {
"knn": {
"n_neighbors": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30],
"weights": ['uniform', 'distance']
},
"random_forest": {
"n_estimators": [10, 20, 30, 40, 50, 100, 150, 200, 250, 500],
"max_features": ["sqrt", "log2", 2],
"max_depth": [2, 3, 4, 5, 6, 7, 8, 9, 10],
"criterion": ["gini", "entropy", "log_loss"],
"random_state": [random_state],
"class_weight": ["balanced", "balanced_subsample"]
},
"mlp": {
"solver": ['adam'],
"max_iter": [1000, 1100, 1200, 1300, 1400, 1500, 1600, 1700, 1800, 1900, 2000],
"alpha": 10.0 ** -np.arange(1, 10),
"hidden_layer_sizes":np.arange(10, 15),
"early_stopping": [True, False],
"random_state": [random_state]
}
}
# Создаем экземпляры моделей
models = {
"knn": neighbors.KNeighborsClassifier(),
"random_forest": ensemble.RandomForestClassifier(),
"mlp": neural_network.MLPClassifier()
}
# Словарь для хранения моделей с их лучшими параметрами
class_models = {}
# Выполнение поиска по сетке для каждой модели
for model_name, model in models.items():
# Создаем GridSearchCV для текущей модели
gs_optimizer = GridSearchCV(estimator=model, param_grid=param_grids[model_name], scoring="f1", n_jobs=-1)
# Обучаем GridSearchCV
gs_optimizer.fit(preprocessed_df, y_train.values.ravel())
# Получаем лучшие параметры
best_params = gs_optimizer.best_params_
print(f"Лучшие параметры для {model_name}: {best_params}")
class_models[model_name] = {
"model": model.set_params(**best_params) # Настраиваем модель с лучшими параметрами
}
ЖЕСТЬ ЭТА ХРЕНЬ 12 МИНУТ СОЗДАВАЛАСЬ...¶
что ж теперь... обучим модели разными способами модели и посмотрим на качество обучения
from sklearn.metrics import confusion_matrix
for model_name in class_models.keys():
print(f"Model: {model_name}")
model = class_models[model_name]["model"]
model_pipeline = Pipeline([("pipeline", pipeline_end), ("model", model)])
model_pipeline = model_pipeline.fit(X_train, y_train.values.ravel())
y_train_predict = model_pipeline.predict(X_train)
y_test_probs = model_pipeline.predict_proba(X_test)[:, 1]
y_test_predict = np.where(y_test_probs > 0.5, 1, 0)
class_models[model_name]["pipeline"] = model_pipeline
class_models[model_name]["probs"] = y_test_probs
class_models[model_name]["preds"] = y_test_predict
class_models[model_name]["Precision_train"] = precision_score(
y_train, y_train_predict
)
class_models[model_name]["Precision_test"] = precision_score(
y_test, y_test_predict
)
class_models[model_name]["Recall_train"] = recall_score(
y_train, y_train_predict
)
class_models[model_name]["Recall_test"] = recall_score(
y_test, y_test_predict
)
class_models[model_name]["Accuracy_train"] = accuracy_score(
y_train, y_train_predict
)
class_models[model_name]["Accuracy_test"] = accuracy_score(
y_test, y_test_predict
)
class_models[model_name]["F1_train"] = f1_score(y_train, y_train_predict)
class_models[model_name]["F1_test"] = f1_score(y_test, y_test_predict)
class_models[model_name]["Confusion_matrix"] = confusion_matrix(
y_test, y_test_predict
)
Матрицы неточностей:
from sklearn.metrics import ConfusionMatrixDisplay
import matplotlib.pyplot as plt
import numpy as np
# Цветовая схема: светло-голубой для правильных, бледно-красный для ошибок
def custom_color_map(c_matrix):
colors = np.empty_like(c_matrix, dtype=str)
for i in range(c_matrix.shape[0]):
for j in range(c_matrix.shape[1]):
if i == j:
colors[i, j] = "#add8e6" # Светло-голубой
else:
colors[i, j] = "#f08080" # Бледно-красный
return colors
fig, ax = plt.subplots(2, 2, figsize=(12, 10))
for index, (key, model_info) in enumerate(class_models.items()):
c_matrix = model_info["Confusion_matrix"]
# Генерация кастомных цветов
disp = ConfusionMatrixDisplay(
confusion_matrix=c_matrix, display_labels=["Not stroke", "Stroke"]
)
disp.plot(ax=ax.flat[index], cmap=custom_color_map(c_matrix), colorbar=False)
disp.ax_.set_title(key)
if len(class_models) < len(ax.flat):
for i in range(len(class_models), len(ax.flat)):
fig.delaxes(ax.flat[i])
plt.subplots_adjust(top=0.9, bottom=0.1, hspace=0.4, wspace=0.3)
plt.show()
Precision, Recall, Accuracy, F1:
class_metrics = pd.DataFrame.from_dict(class_models, "index")[
[
"Precision_train",
"Precision_test",
"Recall_train",
"Recall_test",
"Accuracy_train",
"Accuracy_test",
"F1_train",
"F1_test",
]
]
class_metrics.sort_values(
by="Accuracy_test", ascending=False
).style.background_gradient(
cmap="plasma",
low=0.3,
high=1,
subset=["Accuracy_train", "Accuracy_test", "F1_train", "F1_test"],
).background_gradient(
cmap="viridis",
low=1,
high=0.3,
subset=[
"Precision_train",
"Precision_test",
"Recall_train",
"Recall_test",
],
)
Анализ моделей:¶
MLP (многослойный перцептрон):¶
Точность (Accuracy): 95% (обучение и тест). Precision и Recall: крайне низкие (0.40 и 0.02 на обучении, 0.20 и 0.02 на тесте). F1-метрика: практически нулевая (0.038 и 0.037). Вывод: модель хорошо определяет общий класс, но почти не замечает положительные примеры.
KNN (K-ближайшие соседи):¶
Обучение: идеальные метрики (1.0). Тест: резкое падение (Precision 0.118, Recall 0.12, Accuracy 91%). Вывод: переобучение. Отлично работает на обучении, но плохо обобщает на новых данных.
Random Forest (случайный лес):¶
Accuracy: 85% (обучение) и 82% (тест). Precision и Recall: умеренные, но низкие на тесте (0.135 и 0.50). F1: лучше других моделей (0.213 на тесте).
Вывод: баланс метрик лучше, чем у других, но точность распознавания положительных примеров всё еще оставляет желать лучшего.¶
Сравнение с baseline: Baseline (простая модель): Accuracy 52%, Precision 0.058, Recall 0.58, F1 0.106. Победитель по Accuracy: все модели значительно превосходят baseline. Recall: Random Forest лучше baseline, MLP и KNN уступают. F1-метрика: Random Forest снова впереди, но до желаемого уровня ещё далеко.
Заключение:¶
MLP: сильно смещена, игнорирует положительные примеры. KNN: высокая дисперсия, сильно переобучена. Random Forest: самый сбалансированный вариант, но precision нужно улучшать. Итог: Random Forest – лучший выбор из предложенных, но требует доработки.
Регрессия¶
Разделим набор данных на на обучающую и тестовые выборки (80/20). Целевой признак - avg_glucose_level
features = ['gender', 'age', 'hypertension', 'heart_disease', 'ever_married', 'work_type', 'Residence_type', 'bmi', 'smoking_status', 'stroke']
target = 'avg_glucose_level'
X_train, X_test, y_train, y_test = train_test_split(df[features], df[target], test_size=0.2, random_state=random_state)
display("X_train", X_train)
display("y_train", y_train)
display("X_test", X_test)
display("y_test", y_test)
Выберем ориентир для задачи регрессии. Для этого применим алгоритм правила нуля, т.е. в каждом случае в качестве предсказания выберем среднее значение из области значений целевого признака.
import math
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
# Базовое предсказание: среднее значение по y_train
baseline_predictions = [y_train.mean()] * len(y_test)
# Вычисление метрик качества для ориентира
baseline_rmse = math.sqrt(
mean_squared_error(y_test, baseline_predictions)
)
baseline_rmae = math.sqrt(
mean_absolute_error(y_test, baseline_predictions)
)
baseline_r2 = r2_score(y_test, baseline_predictions)
print('Baseline RMSE:', baseline_rmse)
print('Baseline RMAE:', baseline_rmae)
print('Baseline R2:', baseline_r2)
Метрики:¶
RMSE: корень из MSE, измеряет среднеквадратическую ошибку.¶
Удобен, так как результат в тех же единицах, что и данные. Штрафует за большие отклонения. Хорош для задач, где важна интерпретируемость.
RMAE: корень из MAE, измеряет среднюю абсолютную ошибку.¶
Менее чувствителен к выбросам, что полезно для данных с редкими сильными отклонениями.
R²: коэффициент детерминации, показывает, насколько модель объясняет изменчивость данных. Значение ближе к 1 — модель хорошо описывает данные¶
Используется для сравнения моделей на одинаковых данных. Эти метрики помогают оценить, насколько точна модель по сравнению с простым усреднением.
Сформируем конвейер для регрессии¶
columns_to_drop = []
columns_not_to_modify = ["hypertension", "heart_disease", "stroke", "avg_glucose_level"]
num_columns = [
column
for column in df.columns
if column not in columns_to_drop
and column not in columns_not_to_modify
and df[column].dtype != "object"
]
cat_columns = [
column
for column in df.columns
if column not in columns_to_drop
and column not in columns_not_to_modify
and df[column].dtype == "object"
]
num_imputer = SimpleImputer(strategy="median")
num_scaler = StandardScaler()
preprocessing_num = Pipeline(
[
("imputer", num_imputer),
("scaler", num_scaler),
]
)
cat_imputer = SimpleImputer(strategy="constant", fill_value="unknown")
cat_encoder = OneHotEncoder(handle_unknown="ignore", sparse_output=False, drop="first")
preprocessing_cat = Pipeline(
[
("imputer", cat_imputer),
("encoder", cat_encoder),
]
)
features_preprocessing = ColumnTransformer(
verbose_feature_names_out=False,
transformers=[
("prepocessing_num", preprocessing_num, num_columns),
("prepocessing_cat", preprocessing_cat, cat_columns),
],
remainder="passthrough"
)
drop_columns = ColumnTransformer(
verbose_feature_names_out=False,
transformers=[
("drop_columns", "drop", columns_to_drop),
],
remainder="passthrough",
)
pipeline_end_reg = Pipeline(
[
("features_preprocessing", features_preprocessing),
("drop_columns", drop_columns),
]
)
Теперь проверим работу конвейера:
preprocessing_result = pipeline_end_reg.fit_transform(X_train)
preprocessed_df = pd.DataFrame(
preprocessing_result,
columns=pipeline_end_reg.get_feature_names_out(),
)
preprocessed_df
Подберем оптимальные гиперпараметры для каждой из выбранных моделей методом поиска по сетке и сформируем их набор.
knn -- k-ближайших соседей
random_forest -- метод случайного леса (набор деревьев решений)
mlp -- многослойный персептрон (нейронная сеть)
# Словарь с вариантами гиперпараметров для каждой модели
param_grids = {
"knn": {
"n_neighbors": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30],
"weights": ['uniform', 'distance'],
"n_jobs": [-1]
},
"random_forest": {
"n_estimators": [10, 20, 30, 40, 50, 100, 150, 200, 250, 500],
"max_features": ["sqrt", "log2", 2],
"max_depth": [2, 3, 4, 5, 6, 7, 8, 9, 10],
"criterion": ["squared_error", "absolute_error", "poisson"],
"random_state": [random_state],
"n_jobs": [-1]
},
"mlp": {
"solver": ['adam'],
"max_iter": [1000, 1100, 1200, 1300, 1400, 1500, 1600, 1700, 1800, 1900, 2000],
"alpha": 10.0 ** -np.arange(1, 10),
"hidden_layer_sizes":np.arange(10, 15),
"early_stopping": [True, False],
"random_state": [random_state]
}
}
# Создаем экземпляры моделей
models = {
"knn": neighbors.KNeighborsRegressor(),
"random_forest": ensemble.RandomForestRegressor(),
"mlp": neural_network.MLPRegressor()
}
# Словарь для хранения моделей с их лучшими параметрами
class_models = {}
# Выполнение поиска по сетке для каждой модели
for model_name, model in models.items():
# Создаем GridSearchCV для текущей модели
gs_optimizer = GridSearchCV(estimator=model, param_grid=param_grids[model_name], scoring='neg_mean_squared_error', n_jobs=-1)
# Обучаем GridSearchCV
gs_optimizer.fit(preprocessed_df, y_train.values.ravel())
# Получаем лучшие параметры
best_params = gs_optimizer.best_params_
print(f"Лучшие параметры для {model_name}: {best_params}")
class_models[model_name] = {
"model": model.set_params(**best_params) # Настраиваем модель с лучшими параметрами
}
Далее обучим модели и оценим их качество.
for model_name in class_models.keys():
print(f"Model: {model_name}")
model = class_models[model_name]["model"]
model_pipeline = Pipeline([("pipeline", pipeline_end_reg), ("model", model)])
model_pipeline = model_pipeline.fit(X_train, y_train.values.ravel())
y_train_pred = model_pipeline.predict(X_train)
y_test_pred = model_pipeline.predict(X_test)
class_models[model_name]["pipeline"] = model_pipeline
class_models[model_name]["train_preds"] = y_train_pred
class_models[model_name]["preds"] = y_test_pred
class_models[model_name]["RMSE_train"] = math.sqrt(
mean_squared_error(y_train, y_train_pred)
)
class_models[model_name]["RMSE_test"] = math.sqrt(
mean_squared_error(y_test, y_test_pred)
)
class_models[model_name]["RMAE_test"] = math.sqrt(
mean_absolute_error(y_test, y_test_pred)
)
class_models[model_name]["R2_test"] = r2_score(y_test, y_test_pred)
RMSE, RMAE, R2:
reg_metrics = pd.DataFrame.from_dict(class_models, "index")[
["RMSE_train", "RMSE_test", "RMAE_test", "R2_test"]
]
reg_metrics.sort_values(by="RMSE_test").style.background_gradient(
cmap="viridis", low=1, high=0.3, subset=["RMSE_train", "RMSE_test"]
).background_gradient(cmap="plasma", low=0.3, high=1, subset=["RMAE_test", "R2_test"])
Результаты графиками:
# Создаем графики для всех моделей
for model_name, model_data in class_models.items():
print(f"Model: {model_name}")
y_pred = model_data["preds"]
plt.figure(figsize=(10, 6))
plt.scatter(y_test, y_pred, alpha=0.5)
plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 'k--', lw=2)
plt.xlabel('Фактический уровень глюкозы')
plt.ylabel('Прогнозируемый уровень глюкозы')
plt.title(f"Model: {model_name}")
plt.show()
На представленных графиках можно заметить, что модели в целом не демонстрируют высокого качества. Визуализация их предсказаний показывает сильное рассеивание вокруг идеальной линии y = x, что указывает на значительные отклонения предсказаний от фактических значений.
Тем не менее ориентир, хоть возможно и не столь значительно, каждая из моделей превосходит по всем показателям. Особенно заметное улучшение в R2, которая переходит из отрицательного значения в положительное, что говорит о том, что модели хотя бы частично объясняют дисперсию данных.
Кроме того, можно сказать, что все модели имеет умеренную дисперсию и не сильно подвержены переобучению, потому что разница между RMSE на обучении и тесте незначительна.
Итоговые выводы:
Наиболее качественная модель: MLP, так как она показывает наименьшее значение RMSE и наибольшее значение R2, что указывает на лучшую точность и объяснение дисперсии целевой переменной.
Random Forest: Близок по производительности к MLP, с чуть большим RMSE, но является более устойчивой моделью с небольшими отклонениями между обучением и тестом.
KNN: Худшая модель, демонстрирующая наибольшие ошибки и низкое R2, что указывает на необходимость улучшения или использования другой модели для данной задачи.