Перейти к основному содержимому

8.06. Алгоритмы ИИ

Всем

Алгоритмы, используемые в ИИ

Обучающие, тестовые и валидационные выборки

Разделение данных в машинном обучении

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

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

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

Историческое развитие подхода к разделению данных

В ранних работах по распознаванию образов 1960-х годов исследователи использовали единственный набор данных как для обучения, так и для оценки. Такой подход приводил к завышенным оценкам качества моделей. В 1974 году Томас М. Ковер и Питер Е. Харт в работе о методе ближайших соседей впервые формализовали необходимость разделения данных на независимые подмножества.

К середине 1990-х годов с развитием нейронных сетей и увеличением сложности моделей появилась потребность в трёхкомпонентном разделении. Джон Моудер и его коллеги в 1995 году предложили схему с отдельной валидационной выборкой для контроля остановки обучения. Современная практика кросс-валидации, особенно k-fold, получила широкое распространение после публикаций Рональда Кёхлера в начале 2000-х годов.

Практические аспекты разделения

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

В библиотеке scikit-learn версии 1.4 реализована функция train_test_split с параметром stratify для сохранения распределения классов:

from sklearn.model_selection import train_test_split

X_train, X_temp, y_train, y_temp = train_test_split(
X, y,
test_size=0.4,
random_state=42,
stratify=y
)

X_val, X_test, y_val, y_test = train_test_split(
X_temp, y_temp,
test_size=0.5,
random_state=42,
stratify=y_temp
)

В экосистеме .NET библиотека ML.NET версии 3.0 предоставляет компонент TrainTestSplit:

var splitDataView = mlContext.Data.TrainTestSplit(dataView, testFraction: 0.2);
var trainData = splitDataView.TrainSet;
var testData = splitDataView.TestSet;

Для языка Java библиотека Weka версии 3.8 включает класс RandomSplitClassifier:

RandomSplitClassifier splitter = new RandomSplitClassifier();
splitter.setTrainPercent(70.0);
splitter.setInputFormat(data);
Instances train = Filter.useFilter(data, splitter);
Instances test = Filter.useFilter(data, splitter);

Дерево решений

Принцип работы и структура

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

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

Исторический контекст развития

Первые концепции деревьев решений появились в работе Уильяма Белла в 1954 году в контексте медицинской диагностики. Формальный алгоритм ID3 разработал Джон Росс Куинлан в 1979 году. Этот алгоритм использовал энтропию для выбора оптимальных разбиений. В 1986 году Куинлан представил улучшенную версию C4.5, поддерживающую непрерывные признаки и обработку пропущенных значений.

Алгоритм CART (Classification and Regression Trees) был предложен Лео Брейманом, Джеромом Фридманом, Ричардом Олшеном и Чарльзом Стоуном в 1984 году. CART стал основой для многих современных ансамблевых методов, включая случайный лес и градиентный бустинг.

Пример реализации на Python

Библиотека scikit-learn версии 1.4 предоставляет класс DecisionTreeClassifier:

from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_iris

# Загрузка данных
iris = load_iris()
X, y = iris.data, iris.target

# Создание и обучение модели
clf = DecisionTreeClassifier(
criterion='gini',
max_depth=3,
min_samples_split=2,
random_state=42
)
clf.fit(X, y)

# Визуализация структуры дерева
from sklearn.tree import export_text
tree_rules = export_text(clf, feature_names=iris.feature_names)
print(tree_rules)

Особенности настройки гиперпараметров

Глубина дерева напрямую влияет на сложность модели. Малая глубина приводит к недообучению, избыточная — к переобучению. Параметр min_samples_split определяет минимальное количество образцов, необходимое для разделения узла. Значение min_samples_leaf задаёт минимальное количество образцов в листовом узле.

Обрезка дерева (pruning) применяется после построения полной структуры. Алгоритм последовательно удаляет узлы, вклад которых в улучшение качества модели оказывается незначительным. В реализации библиотеки scikit-learn обрезка выполняется через параметр ccp_alpha, реализующий cost-complexity pruning.

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

Ансамблевый подход и механизм работы

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

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

Историческое развитие метода

Идея объединения множества слабых моделей восходит к работе Томаса Ховкинса в 1990 году. Лео Брейман формализовал концепцию случайного леса в 2001 году, опубликовав статью «Random Forests» в журнале Machine Learning. Ключевым нововведением Бреймана стало случайное ограничение признаков при построении каждого узла дерева.

Первоначальная реализация Бреймана на языке Fortran получила распространение после создания пакета randomForest для языка R в 2002 году. Версия для Python появилась в составе библиотеки scikit-learn в 2010 году. Современные оптимизации, включая параллельное построение деревьев, реализованы в библиотеке Ranger для R и в расширениях scikit-learn.

Пример реализации на нескольких языках

Реализация на Python с использованием scikit-learn версии 1.4:

from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import make_classification

# Генерация синтетических данных
X, y = make_classification(
n_samples=1000,
n_features=20,
n_informative=15,
n_redundant=5,
random_state=42
)

# Создание и обучение случайного леса
clf = RandomForestClassifier(
n_estimators=100,
max_depth=10,
min_samples_split=2,
max_features='sqrt',
bootstrap=True,
oob_score=True,
n_jobs=-1,
random_state=42
)
clf.fit(X, y)

# Оценка важности признаков
importances = clf.feature_importances_

Реализация на C# с использованием ML.NET версии 3.0:

var pipeline = mlContext.Transforms.Conversion
.MapValueToKey("Label", "Label")
.Append(mlContext.Transforms.Concatenate("Features", featureColumnNames))
.Append(mlContext.MulticlassClassification
.Trainers.RandomForest(
new SymbolicSgdLogisticRegressionBinaryTrainer.Options
{
NumberOfTrees = 100,
MinimumExampleCountPerLeaf = 1
}))
.Append(mlContext.Transforms.Conversion.MapKeyToValue("PredictedLabel"));

var model = pipeline.Fit(trainData);

Реализация на Java с использованием библиотеки Smile версии 3.0:

import smile.classification.RandomForest;
import smile.data.DataFrame;
import smile.validation.metric.Accuracy;

// Загрузка данных
DataFrame df = DataFrame.read("data.csv");
double[][] x = df.drop("target").toArray();
int[] y = df.column("target").toIntArray();

// Обучение модели
RandomForest forest = RandomForest.fit(
x,
y,
100, // количество деревьев
2, // минимальное количество образцов в листе
1.0, // доля бутстрап-выборки
Math.sqrt(x[0].length) // количество признаков для разбиения
);

// Оценка качества
int[] predictions = forest.predict(x);
double accuracy = Accuracy.of(y, predictions);

Механизм оценки важности признаков

Случайный лес предоставляет встроенную метрику важности признаков. Алгоритм измеряет среднее уменьшение нечистоты (impurity decrease) при использовании признака для разбиения во всех деревьях леса. Альтернативный подход — перестановочная важность — измеряет снижение качества модели при случайной перестановке значений признака в валидационной выборке.

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

Геометрическая интерпретация и принцип работы

Метод опорных векторов строит оптимальную разделяющую гиперплоскость между классами в признаковом пространстве. Оптимальность определяется максимальной шириной полосы (margin), свободной от объектов обучающей выборки. Граничные объекты, лежащие на краях этой полосы, называются опорными векторами и полностью определяют положение разделяющей гиперплоскости.

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

Исторический контекст

Владимир Вапник и Алексей Червоненкис разработали теоретические основы метода в 1963 году в рамках теории структурного риска. Первый практический алгоритм SVM опубликован Вапником и Коринной Кортес в 1995 году. Ядерный метод получил развитие в работах Бернарда Бозера, Изабель Гийон и Вапника в 1992 году.

Алгоритм последовательного минимального оптимизации (SMO), разработанный Джоном Платтом в 1998 году, сделал SVM практически применимым для задач среднего масштаба. Библиотека LIBSVM, созданная Чих-Чунг Чангом и Чих-Джень Лином в 2001 году, стала стандартной реализацией метода.

Пример реализации на Python

Реализация с использованием scikit-learn версии 1.4:

from sklearn.svm import SVC
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.datasets import make_classification

# Генерация данных
X, y = make_classification(
n_samples=1000,
n_features=20,
n_informative=15,
n_redundant=5,
random_state=42
)

# Создание конвейера с масштабированием и классификатором
pipeline = Pipeline([
('scaler', StandardScaler()),
('svm', SVC(
kernel='rbf',
C=1.0,
gamma='scale',
decision_function_shape='ovr',
max_iter=-1,
random_state=42
))
])

# Обучение модели
pipeline.fit(X, y)

# Получение опорных векторов
support_vectors = pipeline.named_steps['svm'].support_vectors_

Особенности масштабирования признаков

Метод опорных векторов чувствителен к масштабу признаков. Признаки с большим диапазоном значений доминируют в расчёте расстояний. Обязательное предварительное масштабирование выполняется с помощью StandardScaler или MinMaxScaler. В конвейере обработки данных масштабирование размещается перед этапом обучения SVM.

Параметр C регулирует компромисс между шириной разделяющей полосы и количеством ошибок на обучающих данных. Большое значение C приводит к узкой полосе с малым количеством ошибок. Малое значение C допускает больше ошибок ради увеличения ширины полосы. Параметр gamma для RBF-ядра управляет радиусом влияния отдельных опорных векторов.

Метод ближайших соседей

Принцип работы и метрики расстояния

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

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

Исторический контекст

Эвелин Фикс и Джозеф Ходжес младший описали базовую концепцию метода в техническом отчёте Стэнфордского университета в 1951 году. Томас Ковер и Питер Харт формализовали теоретические свойства метода в 1967 году, доказав сходимость ошибки классификации к байесовскому оптимуму при увеличении объёма выборки.

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

Пример реализации на Python

Реализация с использованием scikit-learn версии 1.4:

from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.datasets import load_wine

# Загрузка данных
wine = load_wine()
X, y = wine.data, wine.target

# Создание конвейера с масштабированием
pipeline = Pipeline([
('scaler', StandardScaler()),
('knn', KNeighborsClassifier(
n_neighbors=5,
weights='distance',
algorithm='auto',
leaf_size=30,
p=2,
metric='minkowski'
))
])

# Обучение модели
pipeline.fit(X, y)

# Получение расстояний до соседей для нового объекта
distances, indices = pipeline.named_steps['knn'].kneighbors(X[:1])

Оптимизация поиска ближайших соседей

Полный перебор всех объектов выборки имеет вычислительную сложность O(n). Для ускорения поиска применяются структуры данных: kd-деревья для размерностей до двадцати, ball-деревья для более высоких размерностей. Признаки с разным масштабом требуют обязательного предварительного масштабирования.

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

Логистическая регрессия

Математическая основа без формул

Логистическая регрессия моделирует вероятность принадлежности объекта к положительному классу. Линейная комбинация признаков преобразуется с помощью сигмоидной функции в значение от нуля до единицы. Это значение интерпретируется как вероятность класса. Пороговое значение 0.5 разделяет объекты на два класса.

Алгоритм оптимизирует параметры модели методом максимального правдоподобия. Процесс обучения минимизирует кросс-энтропийную функцию потерь. Регуляризация предотвращает переобучение путём ограничения величины коэффициентов. Типы регуляризации включают L1 (Lasso), L2 (Ridge) и их комбинацию (Elastic Net).

Исторический контекст

Пьер Франсуа Верюль ввёл логистическую функцию в 1844 году для описания демографических процессов. Джозеф Беркун применил логистическую регрессию в биостатистике в 1944 году. Дэвид Кокс формализовал обобщённую линейную модель с логит-связью в 1958 году, получившую название модели Кокса.

Современные реализации с регуляризацией появились в библиотеке LIBLINEAR, разработанной Фаном Чжэном и его коллегами в 2008 году. Метод стал стандартным инструментом для задач бинарной классификации благодаря интерпретируемости коэффициентов и вычислительной эффективности.

Пример реализации на нескольких языках

Реализация на Python с использованием scikit-learn версии 1.4:

from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.datasets import make_classification

# Генерация данных
X, y = make_classification(
n_samples=1000,
n_features=20,
n_informative=15,
n_redundant=5,
random_state=42
)

# Создание конвейера
pipeline = Pipeline([
('scaler', StandardScaler()),
('logreg', LogisticRegression(
penalty='l2',
C=1.0,
solver='lbfgs',
max_iter=100,
multi_class='auto',
class_weight=None,
random_state=42
))
])

# Обучение модели
pipeline.fit(X, y)

# Получение коэффициентов модели
coefficients = pipeline.named_steps['logreg'].coef_
intercept = pipeline.named_steps['logreg'].intercept_

Реализация на C# с использованием ML.NET версии 3.0:

var pipeline = mlContext.Transforms.Conversion
.MapValueToKey("Label", "Label")
.Append(mlContext.Transforms.Concatenate("Features", featureColumnNames))
.Append(mlContext.BinaryClassification
.Trainers.LbfgsLogisticRegression(
new LbfgsLogisticRegressionBinaryTrainer.Options
{
L2Regularization = 1.0f,
NumberOfIterations = 100
}));

var model = pipeline.Fit(trainData);

Реализация на Java с использованием библиотеки Weka версии 3.8:

import weka.classifiers.functions.Logistic;
import weka.core.Instances;
import weka.core.converters.ConverterUtils.DataSource;

// Загрузка данных
DataSource source = new DataSource("data.arff");
Instances data = source.getDataSet();
data.setClassIndex(data.numAttributes() - 1);

// Создание и обучение модели
Logistic logistic = new Logistic();
logistic.setRidge(1.0); // L2 регуляризация
logistic.buildClassifier(data);

// Получение коэффициентов
double[] coefficients = logistic.coefficients();

Интерпретация коэффициентов модели

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

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


Градиентный бустинг

Принцип последовательного улучшения моделей

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

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

Эволюция реализаций бустинга

Алгоритм градиентного бустинга описан Джеромом Фридманом в 1999 году в работе «Greedy Function Approximation: A Gradient Boosting Machine». Первая популярная реализация — библиотека GBM для языка R — появилась в 2005 году. Ограничения скорости и потребления памяти стимулировали развитие оптимизированных версий.

Библиотека XGBoost, разработанная Тяньци Ченом и Карлосом Гуэстрином, представлена в 2014 году. Ключевые улучшения включают регуляризацию деревьев, обработку пропущенных значений без предварительной импутации и параллельное построение узлов дерева. Версия 1.0, выпущенная в 2020 году, добавила поддержку квантизации признаков для ускорения обучения.

Библиотека LightGBM от Microsoft, анонсированная в 2017 году, применяет градиентное одностороннее выборочное сканирование для ускорения поиска оптимальных разбиений. Поддержка монотонных ограничений позволяет гарантировать логику влияния признаков на прогноз. Версия 4.0 (2023 год) включает оптимизации для работы с категориальными признаками без предварительного кодирования.

Библиотека CatBoost от Яндекса, выпущенная в 2017 году, решает проблему смещения при работе с категориальными признаками через упорядоченное кодирование. Алгоритм строит симметричные деревья, что ускоряет применение модели в производственной среде. Версия 1.2 (2024 год) добавила поддержку мультиклассовой классификации с оптимизацией под распределённые вычисления.

Пример реализации на Python

Реализация с использованием XGBoost версии 2.0:

import xgboost as xgb
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split

# Генерация данных
X, y = make_classification(
n_samples=10000,
n_features=50,
n_informative=30,
n_redundant=20,
random_state=42
)

# Разделение данных
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)

# Создание DMatrix для эффективной работы с памятью
dtrain = xgb.DMatrix(X_train, label=y_train)
dtest = xgb.DMatrix(X_test, label=y_test)

# Параметры модели
params = {
'objective': 'binary:logistic',
'max_depth': 6,
'eta': 0.1,
'subsample': 0.8,
'colsample_bytree': 0.8,
'min_child_weight': 1,
'eval_metric': 'logloss'
}

# Обучение модели
model = xgb.train(
params,
dtrain,
num_boost_round=100,
evals=[(dtest, 'eval')],
early_stopping_rounds=10,
verbose_eval=False
)

# Прогнозирование
predictions = model.predict(dtest)

Реализация с использованием LightGBM версии 4.0:

import lightgbm as lgb
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split

# Генерация данных
X, y = make_classification(
n_samples=10000,
n_features=50,
n_informative=30,
n_redundant=20,
random_state=42
)

# Разделение данных
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)

# Создание датасета LightGBM
train_data = lgb.Dataset(X_train, label=y_train)
test_data = lgb.Dataset(X_test, label=y_test, reference=train_data)

# Параметры модели
params = {
'objective': 'binary',
'metric': 'binary_logloss',
'max_depth': 6,
'learning_rate': 0.1,
'num_leaves': 31,
'feature_fraction': 0.8,
'bagging_fraction': 0.8,
'bagging_freq': 5,
'min_child_samples': 20
}

# Обучение модели
model = lgb.train(
params,
train_data,
num_boost_round=100,
valid_sets=[test_data],
callbacks=[lgb.early_stopping(stopping_rounds=10)]
)

# Прогнозирование
predictions = model.predict(X_test)

Особенности работы с категориальными признаками

Категориальные признаки требуют специальной обработки в алгоритмах бустинга. Традиционный подход — однократное кодирование — увеличивает размерность данных и может создать ложные отношения между категориями. Алгоритм CatBoost применяет упорядоченное целочисленное кодирование: для каждого объекта вычисляется статистика целевой переменной по предыдущим объектам в случайной перестановке данных.

Пример подготовки категориальных признаков для CatBoost:

from catboost import CatBoostClassifier, Pool
import pandas as pd

# Создание данных с категориальными признаками
df = pd.DataFrame({
'age': [25, 32, 47, 51, 35],
'city': ['Moscow', 'SPB', 'Moscow', 'Kazan', 'SPB'],
'income': [50000, 70000, 90000, 120000, 80000],
'target': [0, 1, 1, 1, 0]
})

# Указание категориальных признаков
cat_features = ['city']

# Создание пула данных
train_pool = Pool(
data=df[['age', 'city', 'income']],
label=df['target'],
cat_features=cat_features
)

# Обучение модели
model = CatBoostClassifier(
iterations=100,
learning_rate=0.1,
depth=6,
loss_function='Logloss',
verbose=False
)
model.fit(train_pool)

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

Архитектура многослойных перцептронов

Многослойный перцептрон состоит из входного слоя, одного или нескольких скрытых слоёв и выходного слоя. Каждый нейрон соединён со всеми нейронами предыдущего слоя. Входной сигнал проходит через взвешенную сумму и нелинейную функцию активации. Распространённые функции активации включают ReLU для скрытых слоёв и сигмоиду или softmax для выходного слоя в задачах классификации.

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

История развития нейросетевых архитектур

Фрэнк Розенблатт представил перцептрон в 1958 году как модель распознавания образов. Марвин Минский и Сеймур Пейперт в 1969 году показали ограничения однослойных перцептронов, что привело к первому спаду интереса к нейросетям. Алгоритм обратного распространения ошибки независимо переоткрыт несколькими исследователями в середине 1980-х годов, включая Дэвида Румельхарта и Рональда Уильямса.

Свёрточные нейронные сети разработаны Яном Лекуном в 1989 году для распознавания рукописных цифр. Архитектура LeNet-5, представленная в 1998 году, стала основой для современных свёрточных сетей. Конволюционные слои выделяют локальные признаки изображения, а пулинговые слои обеспечивают инвариантность к небольшим сдвигам объекта.

Рекуррентные нейронные сети появились в работах Джеффри Хинтона и коллег в конце 1980-х годов. Проблема затухающих градиентов в длинных последовательностях решена через архитектуру долгой краткосрочной памяти (LSTM), предложенную Зеппом Хочрайтером и Юргеном Шмидхубером в 1997 году. Ячейки памяти и вентили в LSTM контролируют поток информации во времени.

Пример реализации на Python с TensorFlow

Реализация многослойного перцептрона с использованием TensorFlow 2.15:

import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# Генерация данных
X, y = make_classification(
n_samples=10000,
n_features=50,
n_informative=30,
n_redundant=20,
random_state=42
)

# Разделение и масштабирование
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

# Построение модели
model = Sequential([
Dense(128, activation='relu', input_shape=(50,)),
Dropout(0.3),
Dense(64, activation='relu'),
Dropout(0.3),
Dense(32, activation='relu'),
Dense(1, activation='sigmoid')
])

# Компиляция модели
model.compile(
optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy']
)

# Обучение модели
model.fit(
X_train, y_train,
epochs=20,
batch_size=64,
validation_split=0.2,
verbose=1
)

# Оценка качества
loss, accuracy = model.evaluate(X_test, y_test)

Реализация свёрточной сети для изображений:

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense
from tensorflow.keras.datasets import cifar10
from tensorflow.keras.utils import to_categorical

# Загрузка данных CIFAR-10
(x_train, y_train), (x_test, y_test) = cifar10.load_data()

# Нормализация и кодирование меток
x_train = x_train.astype('float32') / 255.0
x_test = x_test.astype('float32') / 255.0
y_train = to_categorical(y_train, 10)
y_test = to_categorical(y_test, 10)

# Построение модели
model = Sequential([
Conv2D(32, (3, 3), activation='relu', padding='same', input_shape=(32, 32, 3)),
Conv2D(32, (3, 3), activation='relu', padding='same'),
MaxPooling2D((2, 2)),
Conv2D(64, (3, 3), activation='relu', padding='same'),
Conv2D(64, (3, 3), activation='relu', padding='same'),
MaxPooling2D((2, 2)),
Flatten(),
Dense(512, activation='relu'),
Dense(10, activation='softmax')
])

# Компиляция и обучение
model.compile(
optimizer='adam',
loss='categorical_crossentropy',
metrics=['accuracy']
)

model.fit(
x_train, y_train,
epochs=30,
batch_size=64,
validation_split=0.1,
verbose=1
)

Реализация на C# с ML.NET

ML.NET версии 3.0 поддерживает нейронные сети через компонент TensorFlow.NET:

using Microsoft.ML;
using Microsoft.ML.Data;
using TensorFlow;

// Загрузка предобученной модели TensorFlow
var mlContext = new MLContext();
var tensorFlowModel = mlContext.Model.LoadTensorFlowModel("model.pb");

// Создание конвейера
var pipeline = mlContext.Transforms
.ApplyOnnxModel(
modelFile: "model.onnx",
outputColumnNames: new[] { "output" },
inputColumnNames: new[] { "input" }
);

// Применение модели к данным
var predictions = pipeline.Fit(dataView).Transform(dataView);

Алгоритмы кластеризации

Метод K-средних

Алгоритм K-средних разбивает данные на K кластеров путём итеративного обновления центроидов. Инициализация центроидов выполняется случайным выбором K объектов из данных или методом K-means++. На каждой итерации каждый объект относится к ближайшему центроиду, после чего центроиды пересчитываются как среднее арифметическое всех объектов кластера.

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

Исторический контекст кластеризации

Стюарт Ллойд описал базовый алгоритм K-средних в техническом отчёте Bell Labs в 1957 году, хотя публикация появилась только в 1982 году. Эдвард Форджи независимо предложил аналогичный метод в 1965 году. Метод максимизации ожидания для гауссовых смесей, предложенный Демпстером, Лэрдом и Рубиным в 1977 году, расширил возможности вероятностной кластеризации.

Алгоритм DBSCAN, разработанный Мартином Эстером и коллегами в 1996 году, кластеризует объекты на основе плотности. Объекты в областях высокой плотности формируют кластеры, а объекты в разреженных областях помечаются как шум. Алгоритм не требует указания числа кластеров и обнаруживает кластеры произвольной формы.

Пример реализации K-средних на Python

Реализация с использованием scikit-learn версии 1.4:

from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import make_blobs
import numpy as np

# Генерация данных с естественными кластерами
X, true_labels = make_blobs(
n_samples=1000,
centers=5,
cluster_std=0.6,
random_state=42
)

# Масштабирование признаков
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# Обучение модели K-средних
kmeans = KMeans(
n_clusters=5,
init='k-means++',
n_init=10,
max_iter=300,
random_state=42
)
cluster_labels = kmeans.fit_predict(X_scaled)

# Центроиды кластеров в исходном масштабе
centroids = scaler.inverse_transform(kmeans.cluster_centers_)

# Оценка качества кластеризации
from sklearn.metrics import silhouette_score
silhouette = silhouette_score(X_scaled, cluster_labels)

Реализация алгоритма DBSCAN:

from sklearn.cluster import DBSCAN
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import make_moons
import matplotlib.pyplot as plt

# Генерация данных с нелинейной структурой
X, _ = make_moons(n_samples=1000, noise=0.05, random_state=42)

# Масштабирование
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# Обучение DBSCAN
dbscan = DBSCAN(eps=0.3, min_samples=10)
cluster_labels = dbscan.fit_predict(X_scaled)

# Количество обнаруженных кластеров
n_clusters = len(set(cluster_labels)) - (1 if -1 in cluster_labels else 0)
n_noise = list(cluster_labels).count(-1)

Алгоритмы понижения размерности

Метод главных компонент

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

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

История развития методов понижения размерности

Карл Пирсон предложил геометрическую интерпретацию метода главных компонент в 1901 году. Гарольд Хотеллинг формализовал метод в 1933 году в работе «Analysis of a Complex of Statistical Variables into Principal Components». Алгоритм сингулярного разложения, лежащий в основе современных реализаций, разработан в работах Эккарта и Янга в 1936 году.

Метод t-SNE, предложенный Лоренсом ван дер Маатеном и Джеффри Хинтоном в 2008 году, сохраняет локальные отношения между объектами в пространстве низкой размерности. Алгоритм минимизирует расходимость Кульбака-Лейблера между распределениями вероятностей соседства в исходном и преобразованном пространствах. Параметр перплексии управляет балансом между вниманием к локальной и глобальной структуре данных.

Пример реализации на Python

Реализация PCA с использованием scikit-learn версии 1.4:

from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import load_breast_cancer
import matplotlib.pyplot as plt

# Загрузка данных
data = load_breast_cancer()
X = data.data
y = data.target

# Масштабирование
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# Применение PCA
pca = PCA(n_components=0.95) # Сохранение 95% дисперсии
X_pca = pca.fit_transform(X_scaled)

# Количество компонент для достижения цели
n_components = pca.n_components_

# Доля дисперсии каждой компоненты
explained_variance_ratio = pca.explained_variance_ratio_

# Визуализация первых двух компонент
plt.scatter(X_pca[:, 0], X_pca[:, 1], c=y, cmap='viridis', alpha=0.6)
plt.xlabel('Первая главная компонента')
plt.ylabel('Вторая главная компонента')

Реализация t-SNE:

from sklearn.manifold import TSNE
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import load_digits
import matplotlib.pyplot as plt

# Загрузка данных цифр
digits = load_digits()
X = digits.data
y = digits.target

# Масштабирование
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# Применение t-SNE
tsne = TSNE(
n_components=2,
perplexity=30,
learning_rate='auto',
init='pca',
random_state=42,
n_iter=1000
)
X_tsne = tsne.fit_transform(X_scaled)

# Визуализация
plt.scatter(X_tsne[:, 0], X_tsne[:, 1], c=y, cmap='tab10', alpha=0.6)
plt.title('Визуализация цифр через t-SNE')

Наивный байесовский классификатор

Вероятностная основа классификации

Наивный байесовский классификатор применяет теорему Байеса для вычисления апостериорной вероятности принадлежности объекта к классу. Алгоритм предполагает условную независимость признаков при заданном классе. Это упрощение редко выполняется в реальных данных, но на практике классификатор демонстрирует устойчивую работу.

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

Исторический контекст

Томас Байес сформулировал теорему, носящую его имя, в работе, опубликованной посмертно в 1763 году. Пьер-Симон Лаплас независимо переоткрыл и расширил теорему в 1774 году. Применение байесовского подхода к классификации получило распространение в 1960-х годах в задачах распознавания текста.

Первые практические реализации наивного байеса для фильтрации спама появились в конце 1990-х годов. Пол Грэм описал применение алгоритма для фильтрации электронной почты в эссе 2002 года «A Plan for Spam». Простота реализации и эффективность на текстовых данных сделали метод стандартным инструментом обработки естественного языка.

Пример реализации на Python

Реализация с использованием scikit-learn версии 1.4:

from sklearn.naive_bayes import MultinomialNB, GaussianNB
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.model_selection import train_test_split
from sklearn.datasets import fetch_20newsgroups

# Классификация текстов
categories = ['alt.atheism', 'soc.religion.christian', 'comp.graphics']
newsgroups = fetch_20newsgroups(subset='all', categories=categories, remove=('headers', 'footers', 'quotes'))

# Векторизация текста
vectorizer = CountVectorizer(stop_words='english', max_features=5000)
X = vectorizer.fit_transform(newsgroups.data)
y = newsgroups.target

# Разделение данных
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Обучение модели
clf = MultinomialNB(alpha=0.1)
clf.fit(X_train, y_train)

# Прогнозирование
predictions = clf.predict(X_test)

# Анализ важности слов для классов
feature_names = vectorizer.get_feature_names_out()
for i, class_label in enumerate(clf.classes_):
top_indices = clf.feature_log_prob_[i].argsort()[-10:][::-1]
top_words = [feature_names[idx] for idx in top_indices]

Реализация для непрерывных признаков:

from sklearn.naive_bayes import GaussianNB
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# Загрузка данных
iris = load_iris()
X, y = iris.data, iris.target

# Разделение и масштабирование
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# Обучение модели
clf = GaussianNB()
clf.fit(X_train_scaled, y_train)

# Прогнозирование и вероятности
predictions = clf.predict(X_test_scaled)
probabilities = clf.predict_proba(X_test_scaled)

Градиентный спуск и его варианты

Базовый алгоритм оптимизации

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

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

Развитие алгоритмов оптимизации

Дэвид Румельхарт и коллеги популяризировали алгоритм обратного распространения ошибки с градиентным спуском в 1986 году. Ян Лекун ввёл импульс (momentum) в 1989 году для ускорения сходимости в узких оврагах функции потерь. Импульс накапливает скорость движения в стабильных направлениях и сглаживает колебания в шумных направлениях.

Оптимизатор AdaGrad, предложенный Джоном Дучи и коллегами в 2011 году, адаптирует скорость обучения для каждого параметра на основе истории градиентов. Параметры с большими градиентами получают меньшую скорость обучения. Алгоритм эффективен для разреженных данных, но может преждевременно снижать скорость обучения до нуля.

Адам (Adaptive Moment Estimation), разработанный Дидериком Кингмой и Джимми Ба в 2014 году, комбинирует идеи импульса и адаптивной скорости обучения. Алгоритм оценивает первый и второй моменты градиента и корректирует их на смещение. Адам стал стандартным оптимизатором для обучения глубоких нейронных сетей благодаря устойчивой сходимости и малой чувствительности к настройке гиперпараметров.

Пример реализации на Python

Реализация мини-пакетного градиентного спуска:

import numpy as np
from sklearn.datasets import make_regression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# Генерация данных
X, y = make_regression(n_samples=10000, n_features=50, noise=0.1, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Масштабирование
scaler_X = StandardScaler()
scaler_y = StandardScaler()
X_train_scaled = scaler_X.fit_transform(X_train)
y_train_scaled = scaler_y.fit_transform(y_train.reshape(-1, 1)).ravel()

# Инициализация параметров
n_features = X_train_scaled.shape[1]
weights = np.zeros(n_features)
bias = 0.0
learning_rate = 0.01
batch_size = 64
n_epochs = 50

# Обучение
for epoch in range(n_epochs):
indices = np.random.permutation(len(X_train_scaled))
for i in range(0, len(X_train_scaled), batch_size):
batch_indices = indices[i:i + batch_size]
X_batch = X_train_scaled[batch_indices]
y_batch = y_train_scaled[batch_indices]

# Прогноз и ошибка
predictions = X_batch @ weights + bias
errors = predictions - y_batch

# Градиенты
grad_weights = (X_batch.T @ errors) / len(batch_indices)
grad_bias = errors.mean()

# Обновление параметров
weights -= learning_rate * grad_weights
bias -= learning_rate * grad_bias

Реализация оптимизатора Адам:

# Инициализация для Адама
weights = np.zeros(n_features)
bias = 0.0
m_w = np.zeros(n_features) # Первый момент весов
v_w = np.zeros(n_features) # Второй момент весов
m_b = 0.0
v_b = 0.0
beta1 = 0.9
beta2 = 0.999
epsilon = 1e-8
t = 0

for epoch in range(n_epochs):
indices = np.random.permutation(len(X_train_scaled))
for i in range(0, len(X_train_scaled), batch_size):
t += 1
batch_indices = indices[i:i + batch_size]
X_batch = X_train_scaled[batch_indices]
y_batch = y_train_scaled[batch_indices]

predictions = X_batch @ weights + bias
errors = predictions - y_batch

grad_weights = (X_batch.T @ errors) / len(batch_indices)
grad_bias = errors.mean()

# Обновление первого момента
m_w = beta1 * m_w + (1 - beta1) * grad_weights
m_b = beta1 * m_b + (1 - beta1) * grad_bias

# Обновление второго момента
v_w = beta2 * v_w + (1 - beta2) * (grad_weights ** 2)
v_b = beta2 * v_b + (1 - beta2) * (grad_bias ** 2)

# Коррекция смещения
m_w_hat = m_w / (1 - beta1 ** t)
v_w_hat = v_w / (1 - beta2 ** t)
m_b_hat = m_b / (1 - beta1 ** t)
v_b_hat = v_b / (1 - beta2 ** t)

# Обновление параметров
weights -= learning_rate * m_w_hat / (np.sqrt(v_w_hat) + epsilon)
bias -= learning_rate * m_b_hat / (np.sqrt(v_b_hat) + epsilon)

Генетические алгоритмы

Эволюционный подход к оптимизации

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

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

Историческое развитие эволюционных вычислений

Джон Холланд заложил теоретические основы генетических алгоритмов в книге «Адаптация в естественных и искусственных системах», опубликованной в 1975 году. Холланд ввёл ключевые концепции схем и теорему о схемах, объясняющую эффективность эволюционного поиска. Дэвид Голдберг популяризировал метод в инженерных приложениях через книгу «Генетические алгоритмы в поиске, оптимизации и машинном обучении» (1989 год).

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

Пример реализации на Python

Реализация базового генетического алгоритма для оптимизации функции:

import numpy as np
import random

class GeneticAlgorithm:
def __init__(self, population_size=100, chromosome_length=20,
mutation_rate=0.01, crossover_rate=0.8, generations=50):
self.population_size = population_size
self.chromosome_length = chromosome_length
self.mutation_rate = mutation_rate
self.crossover_rate = crossover_rate
self.generations = generations

def create_individual(self):
return np.random.randint(0, 2, self.chromosome_length)

def create_population(self):
return [self.create_individual() for _ in range(self.population_size)]

def fitness_function(self, individual):
target = np.ones(self.chromosome_length)
return np.sum(individual == target)

def selection(self, population, fitnesses):
total_fitness = sum(fitnesses)
probabilities = [f / total_fitness for f in fitnesses]
selected = random.choices(population, weights=probabilities, k=2)
return selected[0], selected[1]

def crossover(self, parent1, parent2):
if random.random() < self.crossover_rate:
point = random.randint(1, self.chromosome_length - 1)
child1 = np.concatenate([parent1[:point], parent2[point:]])
child2 = np.concatenate([parent2[:point], parent1[point:]])
return child1, child2
return parent1.copy(), parent2.copy()

def mutate(self, individual):
for i in range(len(individual)):
if random.random() < self.mutation_rate:
individual[i] = 1 - individual[i]
return individual

def evolve(self):
population = self.create_population()

for generation in range(self.generations):
fitnesses = [self.fitness_function(ind) for ind in population]
best_fitness = max(fitnesses)
best_individual = population[fitnesses.index(best_fitness)]

print(f"Поколение {generation}: лучшая приспособленность = {best_fitness}")

if best_fitness == self.chromosome_length:
print("Найдено оптимальное решение!")
return best_individual

new_population = []
while len(new_population) < self.population_size:
parent1, parent2 = self.selection(population, fitnesses)
child1, child2 = self.crossover(parent1, parent2)
new_population.append(self.mutate(child1))
if len(new_population) < self.population_size:
new_population.append(self.mutate(child2))

population = new_population

fitnesses = [self.fitness_function(ind) for ind in population]
best_individual = population[fitnesses.index(max(fitnesses))]
return best_individual

# Запуск алгоритма
ga = GeneticAlgorithm(
population_size=200,
chromosome_length=30,
mutation_rate=0.02,
crossover_rate=0.9,
generations=100
)
solution = ga.evolve()
print("Полученное решение:", solution)

Реализация для задачи коммивояжера с использованием библиотеки DEAP:

from deap import base, creator, tools, algorithms
import random
import numpy as np

# Создание типов
creator.create("FitnessMin", base.Fitness, weights=(-1.0,))
creator.create("Individual", list, fitness=creator.FitnessMin)

# Генерация городов
np.random.seed(42)
cities = np.random.rand(20, 2)

def distance(city1, city2):
return np.sqrt((city1[0] - city2[0])**2 + (city1[1] - city2[1])**2)

def total_distance(individual):
total = 0
for i in range(len(individual) - 1):
total += distance(cities[individual[i]], cities[individual[i+1]])
total += distance(cities[individual[-1]], cities[individual[0]])
return total,

def create_individual():
return random.sample(range(len(cities)), len(cities))

toolbox = base.Toolbox()
toolbox.register("indices", create_individual)
toolbox.register("individual", tools.initIterate, creator.Individual, toolbox.indices)
toolbox.register("population", tools.initRepeat, list, toolbox.individual)
toolbox.register("evaluate", total_distance)
toolbox.register("mate", tools.cxOrdered)
toolbox.register("mutate", tools.mutShuffleIndexes, indpb=0.05)
toolbox.register("select", tools.selTournament, tournsize=3)

# Создание популяции и запуск алгоритма
population = toolbox.population(n=300)
hof = tools.HallOfFame(1)
stats = tools.Statistics(lambda ind: ind.fitness.values)
stats.register("avg", np.mean)
stats.register("min", np.min)

algorithms.eaSimple(
population,
toolbox,
cxpb=0.7,
mutpb=0.2,
ngen=400,
stats=stats,
halloffame=hof,
verbose=True
)

print("Лучший маршрут:", hof[0])
print("Длина маршрута:", total_distance(hof[0])[0])

Алгоритмы ассоциативных правил

Методика выявления закономерностей в транзакциях

Алгоритмы ассоциативных правил обнаруживают регулярные взаимосвязи между элементами в наборах данных. Каждая транзакция представляет собой множество элементов, приобретённых вместе. Правило записывается в форме «если элементы A, то элементы B» с указанием поддержки и достоверности.

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

Алгоритм Apriori и его развитие

Ракеш Агравал и Рамакришнан Срикант представили алгоритм Apriori в 1994 году. Ключевое наблюдение алгоритма — свойство антимонотонности поддержки: если множество элементов нечасто, все его надмножества также будут нечастыми. Алгоритм генерирует кандидатов размера k из частых наборов размера k-1 и проверяет их поддержку в данных.

Алгоритм FP-Growth, предложенный Агравалом и коллегами в 2000 году, устраняет необходимость генерации кандидатов. Структура FP-дерева сжимает информацию о транзакциях, сохраняя частоту элементов и связи между ними. Рекурсивный метод извлечения частых наборов работает напрямую с деревом, что повышает производительность на порядок по сравнению с Apriori.

Пример реализации на Python

Реализация алгоритма Apriori с использованием библиотеки mlxtend версии 0.23:

from mlxtend.frequent_patterns import apriori, association_rules
from mlxtend.preprocessing import TransactionEncoder
import pandas as pd

# Создание набора транзакций
transactions = [
['хлеб', 'молоко', 'яйца'],
['хлеб', 'молоко', 'сыр'],
['хлеб', 'яйца', 'сыр'],
['молоко', 'яйца', 'сыр'],
['хлеб', 'молоко', 'яйца', 'сыр'],
['хлеб', 'молоко'],
['молоко', 'сыр'],
['хлеб', 'сыр'],
['хлеб', 'молоко', 'яйца', 'масло'],
['хлеб', 'яйца', 'масло']
]

# Кодирование транзакций в двоичную матрицу
te = TransactionEncoder()
te_ary = te.fit(transactions).transform(transactions)
df = pd.DataFrame(te_ary, columns=te.columns_)

# Поиск частых наборов с минимальной поддержкой 0.3
frequent_itemsets = apriori(df, min_support=0.3, use_colnames=True)

# Генерация правил с минимальной достоверностью 0.7
rules = association_rules(
frequent_itemsets,
metric="confidence",
min_threshold=0.7
)

# Сортировка правил по подъёму
rules_sorted = rules.sort_values('lift', ascending=False)

# Вывод значимых правил
print("Частые наборы элементов:")
print(frequent_itemsets)
print("\nАссоциативные правила:")
print(rules_sorted[['antecedents', 'consequents', 'support', 'confidence', 'lift']])

Реализация алгоритма FP-Growth:

from mlxtend.frequent_patterns import fpgrowth

# Используем тот же датафрейм df из предыдущего примера

# Поиск частых наборов методом FP-Growth
frequent_itemsets_fp = fpgrowth(
df,
min_support=0.3,
use_colnames=True
)

print("Частые наборы, найденные FP-Growth:")
print(frequent_itemsets_fp.sort_values('support', ascending=False))

Методы ансамблирования моделей

Бэггинг и его особенности

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

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

Стекинг и каскадное объединение моделей

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

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

Пример реализации стекинга на Python

Реализация стекинга с использованием scikit-learn версии 1.4:

from sklearn.ensemble import StackingClassifier
from sklearn.linear_model import LogisticRegression, RidgeClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline

# Генерация данных
X, y = make_classification(
n_samples=5000,
n_features=30,
n_informative=20,
n_redundant=10,
random_state=42
)

# Разделение данных
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)

# Создание базовых моделей с предварительным масштабированием
base_models = [
('logreg', Pipeline([
('scaler', StandardScaler()),
('clf', LogisticRegression(max_iter=1000, random_state=42))
])),
('rf', RandomForestClassifier(n_estimators=100, max_depth=10, random_state=42)),
('gb', GradientBoostingClassifier(n_estimators=100, max_depth=5, random_state=42)),
('svm', Pipeline([
('scaler', StandardScaler()),
('clf', SVC(probability=True, random_state=42))
])),
('knn', Pipeline([
('scaler', StandardScaler()),
('clf', KNeighborsClassifier(n_neighbors=7))
]))
]

# Создание мета-модели
meta_model = LogisticRegression(max_iter=1000, random_state=42)

# Создание стекинг-классификатора
stacking_clf = StackingClassifier(
estimators=base_models,
final_estimator=meta_model,
cv=5,
stack_method='predict_proba',
passthrough=True,
n_jobs=-1
)

# Обучение ансамбля
stacking_clf.fit(X_train, y_train)

# Оценка качества
train_score = stacking_clf.score(X_train, y_train)
test_score = stacking_clf.score(X_test, y_test)

print(f"Точность на обучающей выборке: {train_score:.4f}")
print(f"Точность на тестовой выборке: {test_score:.4f}")

# Анализ весов мета-модели
meta_coefs = stacking_clf.final_estimator_.coef_[0]
print(f"Коэффициенты мета-модели: {meta_coefs}")

Реализация кастомного стекинга с контролем процесса:

from sklearn.model_selection import cross_val_predict
from sklearn.metrics import accuracy_score
import numpy as np

# Обучение базовых моделей и получение прогнозов для мета-уровня
def generate_meta_features(models, X_train, y_train, X_test, cv=5):
meta_train = np.zeros((X_train.shape[0], len(models)))
meta_test = np.zeros((X_test.shape[0], len(models)))

for i, (name, model) in enumerate(models):
# Кросс-валидационные прогнозы для обучающей выборки
meta_train[:, i] = cross_val_predict(
model, X_train, y_train, cv=cv, method='predict_proba'
)[:, 1]

# Обучение на полной выборке и прогноз для теста
model.fit(X_train, y_train)
meta_test[:, i] = model.predict_proba(X_test)[:, 1]

return meta_train, meta_test

# Формирование ансамбля
base_models_list = [
('rf', RandomForestClassifier(n_estimators=150, random_state=42)),
('gb', GradientBoostingClassifier(n_estimators=150, random_state=42)),
('dt', DecisionTreeClassifier(max_depth=15, random_state=42))
]

# Генерация мета-признаков
meta_X_train, meta_X_test = generate_meta_features(
base_models_list, X_train, y_train, X_test, cv=5
)

# Обучение мета-модели
meta_classifier = LogisticRegression(random_state=42)
meta_classifier.fit(meta_X_train, y_train)

# Прогноз и оценка
meta_predictions = meta_classifier.predict(meta_X_test)
accuracy = accuracy_score(y_test, meta_predictions)

print(f"Точность стекинга: {accuracy:.4f}")
print(f"Коэффициенты мета-модели: {meta_classifier.coef_[0]}")

Алгоритмы обнаружения аномалий

Статистические методы выявления выбросов

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

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

Изолирующий лес

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

Глубина изоляции объекта усредняется по всем деревьям леса. Нормальные объекты требуют большей глубины изоляции, аномалии — меньшей. Нормализованная глубина преобразуется в оценку аномальности от нуля до единицы. Значения выше порога 0.5 указывают на аномальные объекты. Изолирующий лес эффективен для многомерных данных и не требует предположений о распределении.

Пример реализации на Python

Реализация изолирующего леса с использованием scikit-learn версии 1.4:

from sklearn.ensemble import IsolationForest
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import make_blobs
import numpy as np

# Генерация данных с аномалиями
np.random.seed(42)
X_normal, _ = make_blobs(n_samples=1000, centers=3, cluster_std=1.0, random_state=42)
X_anomalies = np.random.uniform(low=-10, high=10, size=(50, 2))
X = np.vstack([X_normal, X_anomalies])

# Масштабирование данных
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# Создание и обучение изолирующего леса
iso_forest = IsolationForest(
n_estimators=100,
max_samples='auto',
contamination=0.05,
random_state=42,
n_jobs=-1
)
iso_forest.fit(X_scaled)

# Предсказание аномалий (-1 для аномалий, 1 для нормальных)
predictions = iso_forest.predict(X_scaled)
anomaly_scores = iso_forest.score_samples(X_scaled)

# Идентификация аномальных объектов
anomaly_indices = np.where(predictions == -1)[0]
print(f"Обнаружено аномалий: {len(anomaly_indices)}")
print(f"Индексы аномалий: {anomaly_indices[:10]}")
print(f"Оценки аномальности для первых 10 объектов: {anomaly_scores[:10]}")

Реализация метода локальной факторизации выбросов (LOF):

from sklearn.neighbors import LocalOutlierFactor
import matplotlib.pyplot as plt

# Применение LOF
lof = LocalOutlierFactor(
n_neighbors=20,
contamination=0.05,
novelty=False
)
lof_predictions = lof.fit_predict(X_scaled)
lof_scores = -lof.negative_outlier_factor_

# Визуализация результатов
plt.figure(figsize=(12, 5))

# Исходные данные
plt.subplot(1, 2, 1)
plt.scatter(X[:, 0], X[:, 1], c='blue', alpha=0.6, label='Нормальные')
plt.scatter(X[lof_predictions == -1, 0], X[lof_predictions == -1, 1],
c='red', alpha=0.8, label='Аномалии')
plt.title('LOF: обнаруженные аномалии')
plt.legend()

# Распределение оценок аномальности
plt.subplot(1, 2, 2)
plt.hist(lof_scores, bins=50, color='green', alpha=0.7)
plt.axvline(x=np.percentile(lof_scores, 95), color='red', linestyle='--', label='Порог 95%')
plt.title('Распределение оценок аномальности LOF')
plt.xlabel('Оценка аномальности')
plt.ylabel('Частота')
plt.legend()

plt.tight_layout()

Метрики качества моделей машинного обучения

Метрики для задач классификации

Матрица ошибок предоставляет детальную информацию о качестве классификации. Четыре основных компонента матрицы: истинно положительные, истинно отрицательные, ложно положительные и ложно отрицательные объекты. На основе матрицы ошибок вычисляются точность, полнота и F-мера.

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

Площадь под кривой ошибок (ROC-AUC) оценивает качество ранжирования объектов по вероятности принадлежности к классу. Кривая строится по точкам с разными порогами классификации. Значение AUC равно вероятности того, что случайно выбранный положительный объект получит более высокую оценку, чем случайный отрицательный.

Метрики для задач регрессии

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

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

Пример вычисления метрик на Python

Реализация расчёта метрик классификации:

from sklearn.metrics import (
accuracy_score, precision_score, recall_score, f1_score,
roc_auc_score, confusion_matrix, classification_report
)
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier

# Генерация данных
X, y = make_classification(
n_samples=1000,
n_features=20,
n_classes=2,
weights=[0.85, 0.15], # Несбалансированные классы
random_state=42
)

# Разделение данных
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)

# Обучение модели
clf = RandomForestClassifier(n_estimators=100, random_state=42)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
y_proba = clf.predict_proba(X_test)[:, 1]

# Вычисление метрик
accuracy = accuracy_score(y_test, y_pred)
precision = precision_score(y_test, y_pred)
recall = recall_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
roc_auc = roc_auc_score(y_test, y_proba)
cm = confusion_matrix(y_test, y_pred)

print("Матрица ошибок:")
print(cm)
print(f"\nТочность: {accuracy:.4f}")
print(f"Точность (precision): {precision:.4f}")
print(f"Полнота (recall): {recall:.4f}")
print(f"F1-мера: {f1:.4f}")
print(f"ROC-AUC: {roc_auc:.4f}")

# Детальный отчёт по классам
print("\nДетальный отчёт:")
print(classification_report(y_test, y_pred, target_names=['Класс 0', 'Класс 1']))

Реализация метрик регрессии:

from sklearn.metrics import (
mean_absolute_error, mean_squared_error, r2_score, mean_absolute_percentage_error
)
from sklearn.datasets import make_regression
from sklearn.model_selection import train_test_split
from sklearn.ensemble import GradientBoostingRegressor
import numpy as np

# Генерация данных регрессии
X, y = make_regression(
n_samples=1000,
n_features=30,
n_informative=20,
noise=10.0,
random_state=42
)

# Разделение данных
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)

# Обучение модели
reg = GradientBoostingRegressor(
n_estimators=200,
max_depth=5,
learning_rate=0.1,
random_state=42
)
reg.fit(X_train, y_train)
y_pred = reg.predict(X_test)

# Вычисление метрик
mae = mean_absolute_error(y_test, y_pred)
mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)
r2 = r2_score(y_test, y_pred)
mape = mean_absolute_percentage_error(y_test, y_pred)

print(f"Средняя абсолютная ошибка (MAE): {mae:.4f}")
print(f"Средняя квадратичная ошибка (MSE): {mse:.4f}")
print(f"Корень из MSE (RMSE): {rmse:.4f}")
print(f"Коэффициент детерминации (R²): {r2:.4f}")
print(f"Средняя абсолютная процентная ошибка (MAPE): {mape:.2%}")

Кросс-валидация и подбор гиперпараметров

Стратегии кросс-валидации

Кросс-валидация оценивает качество модели через многократное разделение данных на обучающие и проверочные подмножества. K-блочная кросс-валидация разбивает данные на K равных частей. Модель обучается K раз, каждый раз используя K-1 блоков для обучения и один блок для проверки. Среднее качество по всем блокам даёт устойчивую оценку обобщающей способности.

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

Поиск оптимальных гиперпараметров

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

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

Пример реализации на Python

Реализация стратифицированной кросс-валидации:

from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import make_classification
import numpy as np

# Генерация несбалансированных данных
X, y = make_classification(
n_samples=2000,
n_features=40,
n_informative=30,
n_redundant=10,
weights=[0.9, 0.1],
random_state=42
)

# Создание стратифицированных блоков
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

# Модель для оценки
clf = RandomForestClassifier(n_estimators=100, random_state=42)

# Выполнение кросс-валидации
scores = cross_val_score(
clf, X, y,
cv=skf,
scoring='f1', # F1-мера для несбалансированных данных
n_jobs=-1
)

print(f"F1-мера по блокам: {scores}")
print(f"Средняя F1-мера: {scores.mean():.4f} ± {scores.std():.4f}")

# Анализ распределения классов в блоках
print("\nРаспределение классов в блоках:")
for fold, (train_idx, test_idx) in enumerate(skf.split(X, y), 1):
train_dist = np.bincount(y[train_idx])
test_dist = np.bincount(y[test_idx])
print(f"Блок {fold}: обучение {train_dist}, тест {test_dist}")

Реализация случайного поиска гиперпараметров:

from sklearn.model_selection import RandomizedSearchCV
from sklearn.ensemble import GradientBoostingClassifier
from scipy.stats import randint, uniform
import numpy as np

# Генерация данных
X, y = make_classification(
n_samples=5000,
n_features=50,
n_informative=35,
n_redundant=15,
random_state=42
)

# Модель
clf = GradientBoostingClassifier(random_state=42)

# Пространство поиска гиперпараметров
param_dist = {
'n_estimators': randint(50, 300),
'max_depth': randint(3, 10),
'learning_rate': uniform(0.01, 0.2),
'min_samples_split': randint(2, 20),
'min_samples_leaf': randint(1, 10),
'subsample': uniform(0.7, 0.3),
'max_features': ['sqrt', 'log2', None]
}

# Настройка случайного поиска
random_search = RandomizedSearchCV(
estimator=clf,
param_distributions=param_dist,
n_iter=50,
cv=5,
scoring='roc_auc',
random_state=42,
n_jobs=-1,
verbose=1
)

# Выполнение поиска
random_search.fit(X, y)

# Результаты
print(f"Лучшая оценка ROC-AUC: {random_search.best_score_:.4f}")
print(f"Лучшие параметры: {random_search.best_params_}")

# Анализ важности параметров
cv_results = random_search.cv_results_
for param in param_dist.keys():
if param in cv_results:
print(f"\nВлияние параметра {param}:")
# Вывод топ-3 комбинаций по этому параметру
indices = np.argsort(cv_results['mean_test_score'])[-3:][::-1]
for idx in indices:
print(f" Значение {cv_results['param_' + param][idx]}: оценка {cv_results['mean_test_score'][idx]:.4f}")

Алгоритмы обучения с подкреплением

Архитектура взаимодействия агента и среды

Обучение с подкреплением строится на циклическом взаимодействии между агентом и окружающей средой. Агент воспринимает состояние среды через набор наблюдаемых признаков. На основе текущего состояния агент выбирает действие согласно стратегии поведения. Среда переходит в новое состояние и возвращает агенту скалярное вознаграждение, отражающее качество выполненного действия.

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

Классические алгоритмы временных различий

Алгоритм Q-обучения обновляет таблицу значений действий для каждой пары состояние-действие. Обновление выполняется на основе разницы между полученным вознаграждением плюс максимальное значение будущего действия и текущей оценкой Q-функции. Коэффициент скорости обучения регулирует влияние нового опыта на существующие оценки.

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

История развития методов

Ричард Саттон и Эндрю Барто заложили теоретические основы временных различий в 1988 году. Алгоритм Q-обучения впервые описан Чарльзом Уоткиным в 1989 году как метод обучения без модели среды. В 1992 году Геральд Тезаурус применил Q-обучение для игры в бэкгаммон, продемонстрировав практическую применимость метода.

Глубокое Q-обучение представлено исследователями DeepMind в 2013 году. Система DQN достигла уровня человека в играх Atari 2600, обучаясь исключительно по пиксельному изображению экрана и сигналу вознаграждения. В 2015 году улучшенная версия алгоритма победила чемпиона мира в игре «Го», используя комбинацию глубоких свёрточных сетей и метода Монте-Карло для древовидного поиска.

Пример реализации на Python

Реализация агента глубокого Q-обучения с использованием TensorFlow 2.15:

import tensorflow as tf
import numpy as np
import random
from collections import deque

class DQNAgent:
def __init__(self, state_size, action_size):
self.state_size = state_size
self.action_size = action_size
self.memory = deque(maxlen=2000)
self.gamma = 0.95
self.epsilon = 1.0
self.epsilon_min = 0.01
self.epsilon_decay = 0.995
self.learning_rate = 0.001
self.model = self._build_model()
self.target_model = self._build_model()
self.update_target_model()

def _build_model(self):
model = tf.keras.Sequential([
tf.keras.layers.Dense(24, input_dim=self.state_size, activation='relu'),
tf.keras.layers.Dense(24, activation='relu'),
tf.keras.layers.Dense(self.action_size, activation='linear')
])
model.compile(loss='mse', optimizer=tf.keras.optimizers.Adam(learning_rate=self.learning_rate))
return model

def update_target_model(self):
self.target_model.set_weights(self.model.get_weights())

def remember(self, state, action, reward, next_state, done):
self.memory.append((state, action, reward, next_state, done))

def act(self, state):
if np.random.rand() <= self.epsilon:
return random.randrange(self.action_size)
act_values = self.model.predict(state, verbose=0)
return np.argmax(act_values[0])

def replay(self, batch_size):
if len(self.memory) < batch_size:
return

minibatch = random.sample(self.memory, batch_size)
states = np.zeros((batch_size, self.state_size))
next_states = np.zeros((batch_size, self.state_size))
actions, rewards, dones = [], [], []

for i, (state, action, reward, next_state, done) in enumerate(minibatch):
states[i] = state
next_states[i] = next_state
actions.append(action)
rewards.append(reward)
dones.append(done)

targets = self.model.predict(states, verbose=0)
next_q_values = self.target_model.predict(next_states, verbose=0)

for i in range(batch_size):
if dones[i]:
targets[i][actions[i]] = rewards[i]
else:
targets[i][actions[i]] = rewards[i] + self.gamma * np.amax(next_q_values[i])

self.model.fit(states, targets, epochs=1, verbose=0)

if self.epsilon > self.epsilon_min:
self.epsilon *= self.epsilon_decay

# Пример использования агента в среде с непрерывными состояниями
import gymnasium as gym

env = gym.make('CartPole-v1')
state_size = env.observation_space.shape[0]
action_size = env.action_space.n
agent = DQNAgent(state_size, action_size)
batch_size = 32

for episode in range(500):
state, _ = env.reset()
state = np.reshape(state, [1, state_size])
total_reward = 0

for step in range(500):
action = agent.act(state)
next_state, reward, terminated, truncated, _ = env.step(action)
done = terminated or truncated
next_state = np.reshape(next_state, [1, state_size])

agent.remember(state, action, reward, next_state, done)
state = next_state
total_reward += reward

if done:
agent.update_target_model()
print(f"Эпизод {episode}, награда: {total_reward}, epsilon: {agent.epsilon:.2f}")
break

if len(agent.memory) > batch_size:
agent.replay(batch_size)

Реализация алгоритма актёра-критика для непрерывных действий:

class ActorCriticAgent:
def __init__(self, state_size, action_size):
self.state_size = state_size
self.action_size = action_size
self.actor = self._build_actor()
self.critic = self._build_critic()
self.gamma = 0.99

def _build_actor(self):
inputs = tf.keras.layers.Input(shape=(self.state_size,))
x = tf.keras.layers.Dense(256, activation='relu')(inputs)
x = tf.keras.layers.Dense(256, activation='relu')(x)
outputs = tf.keras.layers.Dense(self.action_size, activation='tanh')(x)
model = tf.keras.Model(inputs, outputs)
model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.0001))
return model

def _build_critic(self):
state_input = tf.keras.layers.Input(shape=(self.state_size,))
action_input = tf.keras.layers.Input(shape=(self.action_size,))

x = tf.keras.layers.Concatenate()([state_input, action_input])
x = tf.keras.layers.Dense(256, activation='relu')(x)
x = tf.keras.layers.Dense(256, activation='relu')(x)
outputs = tf.keras.layers.Dense(1, activation='linear')(x)

model = tf.keras.Model([state_input, action_input], outputs)
model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss='mse')
return model

def act(self, state):
state = np.reshape(state, [1, self.state_size])
action = self.actor.predict(state, verbose=0)[0]
noise = np.random.normal(0, 0.1, size=self.action_size)
return np.clip(action + noise, -1, 1)

def train(self, state, action, reward, next_state, done):
state = np.reshape(state, [1, self.state_size])
next_state = np.reshape(next_state, [1, self.state_size])
action = np.reshape(action, [1, self.action_size])

target = reward
if not done:
next_action = self.actor.predict(next_state, verbose=0)
target += self.gamma * self.critic.predict([next_state, next_action], verbose=0)[0][0]

target_value = np.array([[target]])
self.critic.fit([state, action], target_value, epochs=1, verbose=0)

Алгоритмы обработки естественного языка

Трансформерные архитектуры

Трансформерная архитектура заменяет рекуррентные и свёрточные слои механизмом внимания. Механизм внимания вычисляет веса взаимного влияния всех слов последовательности друг на друга. Каждое слово получает контекстуальное представление, учитывающее всю последовательность независимо от расстояния до других слов.

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

Эволюция языковых моделей

Архитектура трансформера представлена исследователями Google в работе «Внимание — это всё» в 2017 году. Модель BERT, выпущенная в 2018 году, обучается предсказывать пропущенные слова в тексте и следующее предложение. Двунаправленная архитектура позволяет модели учитывать контекст как слева, так и справа от текущего слова.

Модель GPT применяет автогрессивный подход с односторонним вниманием. Обучение выполняется предсказанием следующего слова в последовательности. Версия GPT-3, выпущенная в 2020 году, содержит 175 миллиардов параметров и демонстрирует способность к выполнению задач без дополнительного обучения при предоставлении примеров в запросе. Архитектура последовательного масштабирования показала, что качество языковых моделей предсказуемо растёт с увеличением количества параметров и объёма обучающих данных.

Пример реализации на Python

Работа с предобученной моделью BERT через библиотеку transformers версии 4.38:

from transformers import BertTokenizer, BertModel
import torch

# Загрузка токенизатора и модели
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertModel.from_pretrained('bert-base-uncased')

# Подготовка текста
text = "Машинное обучение преобразует современные технологии"
inputs = tokenizer(text, return_tensors='pt', padding=True, truncation=True, max_length=128)

# Получение эмбеддингов
with torch.no_grad():
outputs = model(**inputs)
last_hidden_states = outputs.last_hidden_state

# Эмбеддинг предложения как среднее по токенам
sentence_embedding = last_hidden_states.mean(dim=1)
print(f"Размерность эмбеддинга предложения: {sentence_embedding.shape}")

# Эмбеддинги отдельных токенов
tokens = tokenizer.convert_ids_to_tokens(inputs['input_ids'][0])
for i, token in enumerate(tokens):
if token not in ['[CLS]', '[SEP]', '[PAD]']:
token_embedding = last_hidden_states[0, i]
print(f"Токен '{token}': вектор размерности {token_embedding.shape}")

Классификация текста с использованием предобученной модели:

from transformers import BertTokenizer, BertForSequenceClassification
import torch.nn.functional as F

# Загрузка модели для классификации
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertForSequenceClassification.from_pretrained(
'bert-base-uncased',
num_labels=3 # три класса: позитивный, нейтральный, негативный
)

# Подготовка нескольких текстов
texts = [
"Отличный продукт, очень доволен покупкой",
"Товар пришёл повреждённым, разочарован",
"Нормально работает, ничего особенного"
]

inputs = tokenizer(texts, return_tensors='pt', padding=True, truncation=True, max_length=64)

# Прогнозирование
with torch.no_grad():
outputs = model(**inputs)
probabilities = F.softmax(outputs.logits, dim=1)
predictions = torch.argmax(probabilities, dim=1)

# Вывод результатов
sentiment_labels = {0: 'негативный', 1: 'нейтральный', 2: 'позитивный'}
for text, pred, probs in zip(texts, predictions, probabilities):
print(f"Текст: '{text}'")
print(f"Прогноз: {sentiment_labels[pred.item()]}")
print(f"Вероятности: негативный={probs[0]:.2f}, нейтральный={probs[1]:.2f}, позитивный={probs[2]:.2f}\n")

Алгоритмы рекомендательных систем

Коллаборативная фильтрация

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

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

Гибридные подходы

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

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

Пример реализации матричной факторизации

Реализация с использованием библиотеки implicit версии 0.7:

import implicit
import numpy as np
from scipy.sparse import csr_matrix

# Генерация синтетических данных о взаимодействиях
np.random.seed(42)
n_users = 1000
n_items = 500
density = 0.05

# Создание разреженной матрицы взаимодействий
interactions = np.random.choice(
[0, 1, 2, 3, 4, 5],
size=(n_users, n_items),
p=[0.95, 0.01, 0.01, 0.01, 0.01, 0.01]
)
interactions_matrix = csr_matrix(interactions)

# Обучение модели ALS (Alternating Least Squares)
model = implicit.als.AlternatingLeastSquares(
factors=50,
regularization=0.01,
iterations=15,
calculate_training_loss=True,
random_state=42
)

# Обучение на данных (необходимо преобразование для implicit)
model.fit(interactions_matrix.T) # implicit ожидает матрицу объекты×пользователи

# Получение рекомендаций для пользователя
user_id = 42
user_items = interactions_matrix[user_id]
recommendations = model.recommend(user_id, user_items, N=10)

print(f"Рекомендации для пользователя {user_id}:")
for item_id, score in recommendations:
print(f" Объект {item_id}: оценка {score:.4f}")

# Поиск похожих объектов
item_id = 100
similar_items = model.similar_items(item_id, N=5)
print(f"\nОбъекты, похожие на объект {item_id}:")
for similar_id, score in similar_items:
print(f" Объект {similar_id}: сходство {score:.4f}")

Реализация нейронной рекомендательной системы:

import tensorflow as tf
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Embedding, Flatten, Dense, Concatenate, Dropout

def build_neural_recommender(n_users, n_items, n_factors=64):
# Входные слои
user_input = Input(shape=(1,), name='user_input')
item_input = Input(shape=(1,), name='item_input')

# Эмбеддинги пользователей и объектов
user_embedding = Embedding(n_users, n_factors, name='user_embedding')(user_input)
item_embedding = Embedding(n_items, n_factors, name='item_embedding')(item_input)

# Выравнивание размерности
user_vec = Flatten()(user_embedding)
item_vec = Flatten()(item_embedding)

# Объединение представлений
concat = Concatenate()([user_vec, item_vec])

# Полносвязные слои для нелинейного взаимодействия
x = Dense(128, activation='relu')(concat)
x = Dropout(0.3)(x)
x = Dense(64, activation='relu')(x)
x = Dropout(0.3)(x)
x = Dense(32, activation='relu')(x)

# Выходной слой — прогноз оценки
output = Dense(1, activation='linear', name='rating_output')(x)

model = Model(inputs=[user_input, item_input], outputs=output)
model.compile(optimizer='adam', loss='mse', metrics=['mae'])
return model

# Создание модели
n_users = 10000
n_items = 5000
model = build_neural_recommender(n_users, n_items, n_factors=128)

# Генерация синтетических данных для обучения
sample_size = 50000
user_ids = np.random.randint(0, n_users, size=sample_size)
item_ids = np.random.randint(0, n_items, size=sample_size)
ratings = np.random.randint(1, 6, size=sample_size).astype(np.float32)

# Обучение модели
model.fit(
[user_ids, item_ids],
ratings,
epochs=10,
batch_size=256,
validation_split=0.1,
verbose=1
)

# Прогнозирование оценки для пары пользователь-объект
test_user = np.array([123])
test_item = np.array([456])
predicted_rating = model.predict([test_user, test_item], verbose=0)
print(f"Прогнозируемая оценка пользователя 123 для объекта 456: {predicted_rating[0][0]:.2f}")

Алгоритмы временных рядов

Автогрессионные модели

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

Сезонная компонента учитывает периодические колебания с фиксированным периодом. Полная модель SARIMA объединяет все компоненты: сезонную и несезонную автогрессию, интегрирование, сезонное и несезонное скользящее среднее. Подбор порядков компонент выполняется через анализ автокорреляционной и частичной автокорреляционной функций или автоматический поиск по критерию Акаике.

Прогнозирование с помощью градиентного бустинга

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

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

Пример реализации на Python

Реализация модели SARIMA с использованием statsmodels версии 0.14:

import numpy as np
import pandas as pd
from statsmodels.tsa.statespace.sarimax import SARIMAX
from sklearn.metrics import mean_absolute_error, mean_squared_error
import matplotlib.pyplot as plt

# Генерация синтетического временного ряда с сезонностью
np.random.seed(42)
date_range = pd.date_range(start='2020-01-01', periods=400, freq='D')
trend = np.linspace(0, 50, 400)
seasonality = 20 * np.sin(2 * np.pi * np.arange(400) / 365)
noise = np.random.normal(0, 5, 400)
values = trend + seasonality + noise

series = pd.Series(values, index=date_range)

# Разделение на обучающую и тестовую выборки (хронологически)
train_size = int(len(series) * 0.8)
train_series = series[:train_size]
test_series = series[train_size:]

# Подбор и обучение модели SARIMA
model = SARIMAX(
train_series,
order=(2, 1, 2), # ARIMA компоненты
seasonal_order=(1, 1, 1, 365), # Сезонные компоненты с годовым периодом
enforce_stationarity=False,
enforce_invertibility=False
)

results = model.fit(disp=False)

# Прогнозирование на тестовом периоде
forecast = results.get_forecast(steps=len(test_series))
forecast_mean = forecast.predicted_mean
forecast_ci = forecast.conf_int()

# Оценка качества
mae = mean_absolute_error(test_series, forecast_mean)
rmse = np.sqrt(mean_squared_error(test_series, forecast_mean))

print(f"MAE: {mae:.2f}")
print(f"RMSE: {rmse:.2f}")

# Визуализация результатов
plt.figure(figsize=(14, 6))
plt.plot(train_series.index, train_series, label='Обучающие данные')
plt.plot(test_series.index, test_series, label='Фактические значения', alpha=0.6)
plt.plot(test_series.index, forecast_mean, label='Прогноз', color='red')
plt.fill_between(
test_series.index,
forecast_ci.iloc[:, 0],
forecast_ci.iloc[:, 1],
color='pink',
alpha=0.3,
label='95% доверительный интервал'
)
plt.title('Прогноз временного ряда с помощью SARIMA')
plt.xlabel('Дата')
plt.ylabel('Значение')
plt.legend()
plt.grid(True, alpha=0.3)

Реализация прогнозирования с градиентным бустингом:

import pandas as pd
import numpy as np
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline

# Создание временного ряда с несколькими сезонностями
np.random.seed(42)
date_range = pd.date_range(start='2022-01-01', periods=1000, freq='H')
hourly_pattern = 10 * np.sin(2 * np.pi * np.arange(1000) / 24)
daily_pattern = 15 * np.sin(2 * np.pi * np.arange(1000) / (24 * 7))
trend = np.linspace(0, 30, 1000)
noise = np.random.normal(0, 3, 1000)
values = hourly_pattern + daily_pattern + trend + noise

df = pd.DataFrame({'timestamp': date_range, 'value': values})

# Создание признаков для временного ряда
def create_time_features(df, target_col='value', max_lag=48):
df = df.copy()

# Лаговые признаки
for lag in range(1, max_lag + 1):
df[f'lag_{lag}'] = df[target_col].shift(lag)

# Скользящие статистики
for window in [6, 12, 24]:
df[f'rolling_mean_{window}'] = df[target_col].shift(1).rolling(window=window).mean()
df[f'rolling_std_{window}'] = df[target_col].shift(1).rolling(window=window).std()

# Временные признаки
df['hour'] = df['timestamp'].dt.hour
df['day_of_week'] = df['timestamp'].dt.dayofweek
df['is_weekend'] = df['day_of_week'].isin([5, 6]).astype(int)

# Удаление строк с пропущенными значениями из-за лагов
df = df.dropna()
return df

# Подготовка данных
df_features = create_time_features(df)
feature_cols = [col for col in df_features.columns if col not in ['timestamp', 'value']]

# Хронологическое разделение
split_idx = int(len(df_features) * 0.8)
train_df = df_features[:split_idx]
test_df = df_features[split_idx:]

X_train = train_df[feature_cols]
y_train = train_df['value']
X_test = test_df[feature_cols]
y_test = test_df['value']

# Создание и обучение модели
pipeline = Pipeline([
('scaler', StandardScaler()),
('gbr', GradientBoostingRegressor(
n_estimators=300,
learning_rate=0.05,
max_depth=6,
min_samples_split=10,
min_samples_leaf=5,
subsample=0.8,
random_state=42
))
])

pipeline.fit(X_train, y_train)

# Прогнозирование и оценка
y_pred = pipeline.predict(X_test)
mae = mean_absolute_error(y_test, y_pred)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))

print(f"MAE на тесте: {mae:.2f}")
print(f"RMSE на тесте: {rmse:.2f}")

# Анализ важности признаков
feature_importance = pipeline.named_steps['gbr'].feature_importances_
importance_df = pd.DataFrame({
'feature': feature_cols,
'importance': feature_importance
}).sort_values('importance', ascending=False)

print("\nТоп-10 важных признаков:")
print(importance_df.head(10))

Алгоритмы передачи обучения

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

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

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

Практические сценарии применения

Компьютерное зрение использует предобученные свёрточные сети в качестве экстракторов признаков. Слои, обученные на ImageNet, эффективно выделяют иерархические визуальные признаки — от простых форм до сложных объектов. Замена выходного слоя позволяет адаптировать сеть к новым классам изображений при ограниченном объёме размеченных данных.

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

Пример реализации на Python

Тонкая настройка свёрточной сети для классификации изображений:

import tensorflow as tf
from tensorflow.keras.applications import EfficientNetB0
from tensorflow.keras.models import Model
from tensorflow.keras.layers import GlobalAveragePooling2D, Dense, Dropout
from tensorflow.keras.preprocessing.image import ImageDataGenerator

# Загрузка предобученной базы без выходного слоя
base_model = EfficientNetB0(
weights='imagenet',
include_top=False,
input_shape=(224, 224, 3)
)

# Замораживание ранних слоёв
for layer in base_model.layers[:200]:
layer.trainable = False

# Построение головы классификации
x = base_model.output
x = GlobalAveragePooling2D()(x)
x = Dense(256, activation='relu')(x)
x = Dropout(0.5)(x)
predictions = Dense(10, activation='softmax')(x) # 10 классов

model = Model(inputs=base_model.input, outputs=predictions)

# Компиляция модели
model.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=0.0001),
loss='categorical_crossentropy',
metrics=['accuracy']
)

# Подготовка генераторов данных с аугментацией
train_datagen = ImageDataGenerator(
rescale=1./255,
rotation_range=20,
width_shift_range=0.2,
height_shift_range=0.2,
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True
)

test_datagen = ImageDataGenerator(rescale=1./255)

# Пример использования (требует реальных данных в папках)
# train_generator = train_datagen.flow_from_directory(
# 'data/train',
# target_size=(224, 224),
# batch_size=32,
# class_mode='categorical'
# )
#
# validation_generator = test_datagen.flow_from_directory(
# 'data/validation',
# target_size=(224, 224),
# batch_size=32,
# class_mode='categorical'
# )
#
# model.fit(
# train_generator,
# epochs=15,
# validation_data=validation_generator
# )

# Второй этап: размораживание части слоёв для полной тонкой настройки
for layer in base_model.layers:
layer.trainable = True

model.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=0.00001),
loss='categorical_crossentropy',
metrics=['accuracy']
)

# Продолжение обучения с малой скоростью
# model.fit(
# train_generator,
# epochs=10,
# validation_data=validation_generator
# )

Адаптация языковой модели для классификации текста:

from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments
from datasets import load_dataset
import numpy as np
from sklearn.metrics import accuracy_score, f1_score

# Загрузка предобученной модели и токенизатора
model_name = "DeepPavlov/rubert-base-cased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSequenceClassification.from_pretrained(
model_name,
num_labels=3, # три класса тональности
ignore_mismatched_sizes=True # разрешить изменение размера выходного слоя
)

# Подготовка данных (пример с синтетическими данными)
texts = [
"Отличный сервис и быстрая доставка",
"Ужасное качество, больше не куплю",
"Нормально, но можно лучше",
"Восхитительно! Рекомендую всем",
"Полное разочарование, деньги на ветер"
]
labels = [2, 0, 1, 2, 0] # 0 - негатив, 1 - нейтральный, 2 - позитив

# Токенизация
encodings = tokenizer(texts, truncation=True, padding=True, max_length=128)

# Создание датасета
class SentimentDataset:
def __init__(self, encodings, labels):
self.encodings = encodings
self.labels = labels

def __getitem__(self, idx):
item = {key: val[idx] for key, val in self.encodings.items()}
item['labels'] = self.labels[idx]
return item

def __len__(self):
return len(self.labels)

dataset = SentimentDataset(encodings, labels)

# Функция вычисления метрик
def compute_metrics(pred):
labels = pred.label_ids
preds = pred.predictions.argmax(-1)
acc = accuracy_score(labels, preds)
f1 = f1_score(labels, preds, average='weighted')
return {'accuracy': acc, 'f1': f1}

# Настройка обучения
training_args = TrainingArguments(
output_dir='./results',
num_train_epochs=3,
per_device_train_batch_size=2,
learning_rate=2e-5,
weight_decay=0.01,
logging_steps=10,
save_strategy='no'
)

trainer = Trainer(
model=model,
args=training_args,
train_dataset=dataset,
compute_metrics=compute_metrics
)

# Обучение модели
trainer.train()

# Прогнозирование на новых данных
test_texts = ["Качество превзошло ожидания", "Не советую покупать"]
test_encodings = tokenizer(test_texts, truncation=True, padding=True, max_length=128, return_tensors='pt')

with torch.no_grad():
outputs = model(**test_encodings)
predictions = torch.argmax(outputs.logits, dim=1)

sentiment_map = {0: 'негативный', 1: 'нейтральный', 2: 'позитивный'}
for text, pred in zip(test_texts, predictions):
print(f"'{text}' -> {sentiment_map[pred.item()]}")

Генеративные состязательные сети

Архитектура генератора и дискриминатора

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

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

Эволюция архитектур и стабилизация обучения

Иан Гудфеллоу и коллеги представили базовую архитектуру GAN в 2014 году. Ранние реализации страдали от проблем режимного коллапса, когда генератор производил ограниченное разнообразие выходов, и нестабильности сходимости. Архитектура DCGAN (Deep Convolutional GAN), предложенная в 2015 году, ввела свёрточные слои с конкретными правилами проектирования: использование свёрток вместо пулинга, пакетной нормализации во всех слоях кроме выходного генератора и входного дискриминатора, применение активации ReLU в генераторе и LeakyReLU в дискриминаторе.

Wasserstein GAN, представленный в 2017 году, заменил функцию потерь на расстояние Вассерштейна. Критически важным усовершенствованием стало применение градиентного штрафа вместо весового клиппирования для обеспечения липшицевости дискриминатора. Эта модификация обеспечила стабильную сходимость и коррелирующую с визуальным качеством метрику обучения.

Пример реализации на Python

Реализация базовой GAN для генерации рукописных цифр с использованием TensorFlow 2.15:

import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.datasets import mnist
from tensorflow.keras.layers import Dense, Reshape, Flatten, LeakyReLU, BatchNormalization, Dropout
from tensorflow.keras.models import Sequential, Model
from tensorflow.keras.optimizers import Adam

# Загрузка и подготовка данных
(x_train, _), (_, _) = mnist.load_data()
x_train = x_train.astype('float32') / 255.0
x_train = x_train.reshape(-1, 784)

# Параметры модели
latent_dim = 100
batch_size = 128
epochs = 20000
steps_per_epoch = x_train.shape[0] // batch_size

# Построение генератора
def build_generator():
model = Sequential([
Dense(256, input_dim=latent_dim),
LeakyReLU(alpha=0.2),
BatchNormalization(momentum=0.8),
Dense(512),
LeakyReLU(alpha=0.2),
BatchNormalization(momentum=0.8),
Dense(1024),
LeakyReLU(alpha=0.2),
BatchNormalization(momentum=0.8),
Dense(784, activation='tanh')
])
return model

# Построение дискриминатора
def build_discriminator():
model = Sequential([
Dense(512, input_dim=784),
LeakyReLU(alpha=0.2),
Dropout(0.3),
Dense(256),
LeakyReLU(alpha=0.2),
Dropout(0.3),
Dense(1, activation='sigmoid')
])
return model

# Создание моделей
generator = build_generator()
discriminator = build_discriminator()
discriminator.compile(
optimizer=Adam(learning_rate=0.0002, beta_1=0.5),
loss='binary_crossentropy',
metrics=['accuracy']
)

# Составная модель для обучения генератора
noise_input = tf.keras.Input(shape=(latent_dim,))
generated_image = generator(noise_input)
discriminator.trainable = False
validity = discriminator(generated_image)
combined = Model(noise_input, validity)
combined.compile(
optimizer=Adam(learning_rate=0.0002, beta_1=0.5),
loss='binary_crossentropy'
)

# Цикл обучения
for epoch in range(epochs):
# Обучение дискриминатора
idx = np.random.randint(0, x_train.shape[0], batch_size)
real_images = x_train[idx]
noise = np.random.normal(0, 1, (batch_size, latent_dim))
fake_images = generator.predict(noise, verbose=0)

real_labels = np.ones((batch_size, 1)) * 0.9 # label smoothing
fake_labels = np.zeros((batch_size, 1))

d_loss_real = discriminator.train_on_batch(real_images, real_labels)
d_loss_fake = discriminator.train_on_batch(fake_images, fake_labels)
d_loss = 0.5 * np.add(d_loss_real, d_loss_fake)

# Обучение генератора
noise = np.random.normal(0, 1, (batch_size, latent_dim))
valid_labels = np.ones((batch_size, 1))
g_loss = combined.train_on_batch(noise, valid_labels)

# Вывод прогресса и сохранение изображений
if epoch % 1000 == 0:
print(f"Эпоха {epoch}: потери дискриминатора {d_loss[0]:.4f}, "
f"точность {100*d_loss[1]:.2f}%, потери генератора {g_loss:.4f}")

noise = np.random.normal(0, 1, (25, latent_dim))
gen_imgs = generator.predict(noise, verbose=0)
gen_imgs = 0.5 * gen_imgs + 0.5 # масштабирование в [0, 1]

fig, axs = plt.subplots(5, 5, figsize=(8, 8))
cnt = 0
for i in range(5):
for j in range(5):
axs[i, j].imshow(gen_imgs[cnt].reshape(28, 28), cmap='gray')
axs[i, j].axis('off')
cnt += 1
plt.tight_layout()
plt.savefig(f'gan_epoch_{epoch}.png')
plt.close()

Реализация генератора условных изображений (cGAN):

# Модификация для условной генерации
def build_conditional_generator():
noise = tf.keras.Input(shape=(latent_dim,))
label = tf.keras.Input(shape=(10,)) # 10 классов цифр

merged = tf.keras.layers.Concatenate()([noise, label])

x = Dense(256)(merged)
x = LeakyReLU(alpha=0.2)(x)
x = BatchNormalization(momentum=0.8)(x)

x = Dense(512)(x)
x = LeakyReLU(alpha=0.2)(x)
x = BatchNormalization(momentum=0.8)(x)

x = Dense(1024)(x)
x = LeakyReLU(alpha=0.2)(x)
x = BatchNormalization(momentum=0.8)(x)

output = Dense(784, activation='tanh')(x)

return Model([noise, label], output)

def build_conditional_discriminator():
image = tf.keras.Input(shape=(784,))
label = tf.keras.Input(shape=(10,))

merged = tf.keras.layers.Concatenate()([image, label])

x = Dense(512)(merged)
x = LeakyReLU(alpha=0.2)(x)
x = Dropout(0.3)(x)

x = Dense(256)(x)
x = LeakyReLU(alpha=0.2)(x)
x = Dropout(0.3)(x)

output = Dense(1, activation='sigmoid')(x)

return Model([image, label], output)

# Подготовка меток в one-hot формате
from tensorflow.keras.utils import to_categorical
(_, y_train), (_, _) = mnist.load_data()
y_train_cat = to_categorical(y_train, 10)

# Обучение условной GAN аналогично базовой, с передачей меток
# генератору и дискриминатору на каждом шаге

Вариационные автокодировщики

Вероятностная интерпретация кодирования

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

Функция потерь VAE объединяет реконструкционную ошибку и регуляризационный член Кульбака-Лейблера. Реконструкционная ошибка измеряет качество восстановления исходных данных. Дивергенция Кульбака-Лейблера ограничивает отклонение предсказанного распределения от стандартного нормального, обеспечивая непрерывность и полноту латентного пространства. Этот баланс позволяет генерировать новые объекты путём выборки из любых точек латентного пространства.

Архитектурные особенности и применения

Первые вариационные автокодировщики описаны Дарио Кингма и Максом Веллингом в 2013 году. Ключевым техническим приёмом стала репараметризация: вместо прямой выборки из нормального распределения с параметрами μ и σ, генерируется шум из стандартного нормального распределения, который преобразуется как μ + σ·ε. Этот приём обеспечивает дифференцируемость операции выборки относительно параметров μ и σ.

Бета-VAE вводит гиперпараметр β для усиления регуляризации латентного пространства. Увеличение β приводит к более независимым и интерпретируемым латентным факторам, что полезно для задач дизентанглмента признаков. Иерархические VAE строят многоуровневые латентные представления, где каждый уровень кодирует признаки различной абстракции — от низкоуровневых деталей до высокоуровневых семантических концепций.

Пример реализации на Python

Реализация вариационного автокодировщика для изображений MNIST:

import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.datasets import mnist
from tensorflow.keras.layers import Input, Dense, Lambda, Layer, Reshape, Flatten
from tensorflow.keras.models import Model
from tensorflow.keras import backend as K

# Загрузка данных
(x_train, _), (x_test, _) = mnist.load_data()
x_train = x_train.astype('float32') / 255.0
x_test = x_test.astype('float32') / 255.0
x_train = x_train.reshape(-1, 784)
x_test = x_test.reshape(-1, 784)

# Параметры модели
original_dim = 784
intermediate_dim = 512
latent_dim = 2 # 2D для визуализации

# Слои энкодера
inputs = Input(shape=(original_dim,))
h = Dense(intermediate_dim, activation='relu')(inputs)
z_mean = Dense(latent_dim)(h)
z_log_var = Dense(latent_dim)(h)

# Функция репараметризации
def sampling(args):
z_mean, z_log_var = args
epsilon = K.random_normal(shape=K.shape(z_mean), mean=0., stddev=1.0)
return z_mean + K.exp(0.5 * z_log_var) * epsilon

z = Lambda(sampling, output_shape=(latent_dim,))([z_mean, z_log_var])

# Слои декодера
decoder_h = Dense(intermediate_dim, activation='relu')
decoder_mean = Dense(original_dim, activation='sigmoid')
h_decoded = decoder_h(z)
x_decoded_mean = decoder_mean(h_decoded)

# Создание моделей
vae = Model(inputs, x_decoded_mean)

# Кастомный слой для вычисления потерь
class VAE_Loss(Layer):
def __init__(self, **kwargs):
super(VAE_Loss, self).__init__(**kwargs)

def call(self, inputs):
x, x_decoded, z_mean, z_log_var = inputs
xent_loss = original_dim * tf.keras.losses.binary_crossentropy(x, x_decoded)
kl_loss = -0.5 * K.sum(1 + z_log_var - K.square(z_mean) - K.exp(z_log_var), axis=-1)
vae_loss = K.mean(xent_loss + kl_loss)
self.add_loss(vae_loss)
return x_decoded_mean

outputs = VAE_Loss()([inputs, x_decoded_mean, z_mean, z_log_var])
vae = Model(inputs, outputs)

# Компиляция и обучение
vae.compile(optimizer='adam')
vae.fit(x_train, x_train, shuffle=True, epochs=50, batch_size=128, validation_data=(x_test, x_test))

# Отдельная модель генератора
decoder_input = Input(shape=(latent_dim,))
_h_decoded = decoder_h(decoder_input)
_x_decoded_mean = decoder_mean(_h_decoded)
generator = Model(decoder_input, _x_decoded_mean)

# Визуализация латентного пространства
x_test_encoded = tf.keras.models.Model(inputs, z_mean).predict(x_test, batch_size=128)
plt.figure(figsize=(10, 8))
plt.scatter(x_test_encoded[:, 0], x_test_encoded[:, 1], c=np.argmax(x_test, axis=1), cmap='tab10')
plt.colorbar()
plt.title('Латентное пространство VAE (MNIST)')
plt.xlabel('z[0]')
plt.ylabel('z[1]')
plt.savefig('vae_latent_space.png')
plt.close()

# Генерация изображений по сетке латентного пространства
n = 15
digit_size = 28
figure = np.zeros((digit_size * n, digit_size * n))

grid_x = np.linspace(-3, 3, n)
grid_y = np.linspace(-3, 3, n)[::-1]

for i, yi in enumerate(grid_y):
for j, xi in enumerate(grid_x):
z_sample = np.array([[xi, yi]])
x_decoded = generator.predict(z_sample, verbose=0)
digit = x_decoded[0].reshape(digit_size, digit_size)
figure[i * digit_size: (i + 1) * digit_size,
j * digit_size: (j + 1) * digit_size] = digit

plt.figure(figsize=(10, 10))
plt.imshow(figure, cmap='gray')
plt.title('Генерация изображений по латентному пространству')
plt.axis('off')
plt.savefig('vae_generation.png')
plt.close()

Алгоритмы онлайн-обучения

Инкрементальное обновление моделей

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

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

Алгоритмы стохастического градиентного спуска

Стохастический градиентный спуск является основой большинства онлайн-алгоритмов. На каждом шаге вычисляется градиент функции потерь по одному объекту и веса корректируются в обратном направлении. Адаптивные варианты алгоритма, такие как AdaGrad и Adam, регулируют скорость обучения для каждого веса на основе истории градиентов.

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

Пример реализации на Python

Реализация онлайн-классификатора с использованием scikit-learn версии 1.4:

from sklearn.linear_model import SGDClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import make_classification
import numpy as np

# Генерация синтетических данных
X, y = make_classification(
n_samples=10000,
n_features=50,
n_informative=30,
n_redundant=20,
random_state=42
)

# Разделение на пакеты для имитации потоковых данных
batch_size = 100
n_batches = len(X) // batch_size

# Инициализация онлайн-классификатора
scaler = StandardScaler()
classifier = SGDClassifier(
loss='log_loss', # логистическая регрессия
penalty='l2',
alpha=0.0001,
max_iter=1,
tol=None,
shuffle=False,
random_state=42,
warm_start=True # сохранение весов между вызовами fit
)

# Онлайн-обучение по пакетам
accuracies = []
for batch_idx in range(n_batches):
start = batch_idx * batch_size
end = start + batch_size
X_batch = X[start:end]
y_batch = y[start:end]

# Масштабирование на основе накопленной статистики
if batch_idx == 0:
scaler.partial_fit(X_batch)
else:
scaler.partial_fit(X_batch)

X_batch_scaled = scaler.transform(X_batch)

# Обучение на текущем пакете
classifier.partial_fit(X_batch_scaled, y_batch, classes=np.unique(y))

# Оценка качества на всех обработанных данных
X_seen = X[:end]
y_seen = y[:end]
X_seen_scaled = scaler.transform(X_seen)
accuracy = classifier.score(X_seen_scaled, y_seen)
accuracies.append(accuracy)

if batch_idx % 10 == 0:
print(f"Пакет {batch_idx}/{n_batches}, точность: {accuracy:.4f}")

# Визуализация динамики качества
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 6))
plt.plot(range(len(accuracies)), accuracies, linewidth=2)
plt.xlabel('Номер пакета')
plt.ylabel('Точность на накопленных данных')
plt.title('Динамика качества онлайн-обучения')
plt.grid(True, alpha=0.3)
plt.savefig('online_learning_progress.png')
plt.close()

Реализация адаптивного окна для обработки концепт-дрейфа:

class AdaptiveWindowClassifier:
def __init__(self, base_classifier, window_size=1000, drift_threshold=0.1):
self.base_classifier = base_classifier
self.window_size = window_size
self.drift_threshold = drift_threshold
self.X_window = []
self.y_window = []
self.accuracy_history = []
self.drift_points = []

def partial_fit(self, X, y):
# Добавление новых данных в окно
self.X_window.extend(X)
self.y_window.extend(y)

# Ограничение размера окна
if len(self.X_window) > self.window_size:
removed = len(self.X_window) - self.window_size
self.X_window = self.X_window[-self.window_size:]
self.y_window = self.y_window[-self.window_size:]

# Обучение на текущем окне
self.base_classifier.partial_fit(
np.array(self.X_window),
np.array(self.y_window),
classes=np.unique(y)
)

def detect_drift(self, X_new, y_new):
# Оценка качества на новом пакете
accuracy_new = self.base_classifier.score(X_new, y_new)
self.accuracy_history.append(accuracy_new)

# Проверка дрейфа при достаточном объёме истории
if len(self.accuracy_history) > 5:
recent_mean = np.mean(self.accuracy_history[-5:])
historical_mean = np.mean(self.accuracy_history[:-5]) if len(self.accuracy_history) > 10 else recent_mean

if historical_mean - recent_mean > self.drift_threshold:
self.drift_points.append(len(self.accuracy_history))
print(f"Обнаружен концепт-дрейф на шаге {len(self.accuracy_history)}: "
f"точность упала с {historical_mean:.4f} до {recent_mean:.4f}")
return True
return False

# Имитация данных с изменяющимся распределением
np.random.seed(42)
X_stream = []
y_stream = []

# Первый сегмент данных
X1, y1 = make_classification(n_samples=2000, n_features=20, n_informative=15, random_state=42)
X_stream.append(X1)
y_stream.append(y1)

# Второй сегмент с изменёнными параметрами
X2, y2 = make_classification(n_samples=2000, n_features=20, n_informative=15,
flip_y=0.2, random_state=100) # повышенный шум
X_stream.append(X2)
y_stream.append(y2)

# Третий сегмент с новыми информативными признаками
X3, y3 = make_classification(n_samples=2000, n_features=20, n_informative=10,
n_redundant=10, random_state=200)
X_stream.append(X3)
y_stream.append(y3)

X_full = np.vstack(X_stream)
y_full = np.hstack(y_stream)

# Обучение с обнаружением дрейфа
classifier = SGDClassifier(loss='log_loss', random_state=42)
adaptive_clf = AdaptiveWindowClassifier(classifier, window_size=500, drift_threshold=0.08)

batch_size = 100
for i in range(0, len(X_full), batch_size):
X_batch = X_full[i:i+batch_size]
y_batch = y_full[i:i+batch_size]

adaptive_clf.partial_fit(X_batch, y_batch)

# Проверка дрейфа каждые 5 пакетов
if i % (5 * batch_size) == 0 and i > 0:
adaptive_clf.detect_drift(X_full[max(0, i-500):i], y_full[max(0, i-500):i])

Алгоритмы активного обучения

Стратегии выбора информативных объектов

Активное обучение минимизирует затраты на разметку данных путём выбора наиболее информативных объектов для запроса меток у эксперта. Модель обучается на небольшом начальном наборе размеченных данных и последовательно запрашивает метки для объектов, которые максимально улучшат её качество.

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

Применение в реальных сценариях

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

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

Пример реализации на Python

Реализация активного обучения с несколькими стратегиями выбора:

from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
import numpy as np

class ActiveLearningSystem:
def __init__(self, model, strategy='uncertainty'):
self.model = model
self.strategy = strategy
self.X_labeled = None
self.y_labeled = None
self.X_pool = None
self.y_pool_true = None

def initialize(self, X_labeled, y_labeled, X_pool, y_pool_true):
self.X_labeled = X_labeled.copy()
self.y_labeled = y_labeled.copy()
self.X_pool = X_pool.copy()
self.y_pool_true = y_pool_true.copy()

def query_uncertainty(self, n_queries):
probabilities = self.model.predict_proba(self.X_pool)
uncertainties = 1 - np.max(probabilities, axis=1)
query_indices = np.argsort(uncertainties)[-n_queries:][::-1]
return query_indices

def query_margin(self, n_queries):
probabilities = self.model.predict_proba(self.X_pool)
sorted_probs = np.sort(probabilities, axis=1)
margins = sorted_probs[:, -1] - sorted_probs[:, -2]
query_indices = np.argsort(margins)[:n_queries]
return query_indices

def query_entropy(self, n_queries):
probabilities = self.model.predict_proba(self.X_pool)
probabilities = np.clip(probabilities, 1e-10, 1.0)
entropy = -np.sum(probabilities * np.log2(probabilities), axis=1)
query_indices = np.argsort(entropy)[-n_queries:][::-1]
return query_indices

def query(self, n_queries):
if self.strategy == 'uncertainty':
indices = self.query_uncertainty(n_queries)
elif self.strategy == 'margin':
indices = self.query_margin(n_queries)
elif self.strategy == 'entropy':
indices = self.query_entropy(n_queries)
else:
raise ValueError(f"Неизвестная стратегия: {self.strategy}")

return indices

def update(self, query_indices):
# Получение истинных меток из пула
new_X = self.X_pool[query_indices]
new_y = self.y_pool_true[query_indices]

# Добавление в размеченный набор
self.X_labeled = np.vstack([self.X_labeled, new_X])
self.y_labeled = np.hstack([self.y_labeled, new_y])

# Удаление из пула
mask = np.ones(len(self.X_pool), dtype=bool)
mask[query_indices] = False
self.X_pool = self.X_pool[mask]
self.y_pool_true = self.y_pool_true[mask]

def train(self):
self.model.fit(self.X_labeled, self.y_labeled)

# Генерация данных для демонстрации
X, y = make_classification(
n_samples=5000,
n_features=50,
n_informative=30,
n_redundant=20,
n_classes=3,
random_state=42
)

# Разделение на начальный размеченный набор и пул для запросов
X_init, X_pool, y_init, y_pool = train_test_split(
X, y, train_size=50, stratify=y, random_state=42
)

# Инициализация системы активного обучения
model = RandomForestClassifier(n_estimators=100, random_state=42)
al_system = ActiveLearningSystem(model, strategy='uncertainty')
al_system.initialize(X_init, y_init, X_pool, y_pool)

# Цикл активного обучения
n_iterations = 30
queries_per_iteration = 10
accuracy_history = []

for iteration in range(n_iterations):
# Обучение модели
al_system.train()

# Оценка качества на всём исходном наборе данных
accuracy = al_system.model.score(X, y)
accuracy_history.append(accuracy)

print(f"Итерация {iteration}: размечено {len(al_system.X_labeled)} объектов, "
f"точность {accuracy:.4f}")

# Запрос новых объектов для разметки
if len(al_system.X_pool) >= queries_per_iteration:
query_indices = al_system.query(queries_per_iteration)
al_system.update(query_indices)
else:
break

# Сравнение с пассивным обучением
passive_accuracies = []
X_passive_labeled = X_init.copy()
y_passive_labeled = y_init.copy()
X_passive_pool = X_pool.copy()
y_passive_pool = y_pool.copy()

for iteration in range(n_iterations):
model_passive = RandomForestClassifier(n_estimators=100, random_state=42)
model_passive.fit(X_passive_labeled, y_passive_labeled)
accuracy = model_passive.score(X, y)
passive_accuracies.append(accuracy)

# Случайный выбор объектов для разметки
if len(X_passive_pool) >= queries_per_iteration:
random_indices = np.random.choice(
len(X_passive_pool),
queries_per_iteration,
replace=False
)
X_passive_labeled = np.vstack([X_passive_labeled, X_passive_pool[random_indices]])
y_passive_labeled = np.hstack([y_passive_labeled, y_passive_pool[random_indices]])

mask = np.ones(len(X_passive_pool), dtype=bool)
mask[random_indices] = False
X_passive_pool = X_passive_pool[mask]
y_passive_pool = y_passive_pool[mask]

# Визуализация эффективности активного обучения
plt.figure(figsize=(12, 6))
labeled_counts = np.arange(len(accuracy_history)) * queries_per_iteration + len(X_init)
plt.plot(labeled_counts, accuracy_history, 'b-o', label='Активное обучение (неопределённость)', linewidth=2)
plt.plot(labeled_counts, passive_accuracies, 'r--s', label='Пассивное обучение (случайный выбор)', linewidth=2)
plt.xlabel('Количество размеченных объектов')
plt.ylabel('Точность на полном наборе данных')
plt.title('Эффективность активного обучения')
plt.legend()
plt.grid(True, alpha=0.3)
plt.savefig('active_learning_efficiency.png')
plt.close()

Алгоритмы самообучения

Принципы использования неразмеченных данных

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

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

Ко-обучение с множественными представлениями

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

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

Пример реализации на Python

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

from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import numpy as np

class SelfTrainingClassifier:
def __init__(self, base_classifier, threshold=0.75, max_iterations=10):
self.base_classifier = base_classifier
self.threshold = threshold
self.max_iterations = max_iterations
self.X_labeled = None
self.y_labeled = None
self.X_unlabeled = None

def fit(self, X_labeled, y_labeled, X_unlabeled):
self.X_labeled = X_labeled.copy()
self.y_labeled = y_labeled.copy()
self.X_unlabeled = X_unlabeled.copy()

for iteration in range(self.max_iterations):
# Обучение на текущем размеченном наборе
self.base_classifier.fit(self.X_labeled, self.y_labeled)

# Прогнозирование на неразмеченных данных
probabilities = self.base_classifier.predict_proba(self.X_unlabeled)
max_probs = np.max(probabilities, axis=1)
predictions = np.argmax(probabilities, axis=1)

# Выбор объектов для псевдоразметки
mask = max_probs >= self.threshold
n_new = np.sum(mask)

if n_new == 0:
print(f"Итерация {iteration}: нет объектов с уверенностью выше порога {self.threshold:.2f}")
break

# Добавление псевдоразмеченных объектов
new_X = self.X_unlabeled[mask]
new_y = predictions[mask]

self.X_labeled = np.vstack([self.X_labeled, new_X])
self.y_labeled = np.hstack([self.y_labeled, new_y])

# Удаление из неразмеченного пула
self.X_unlabeled = self.X_unlabeled[~mask]

print(f"Итерация {iteration}: добавлено {n_new} псевдоразмеченных объектов, "
f"всего размечено {len(self.X_labeled)}, осталось неразмеченных {len(self.X_unlabeled)}")

if len(self.X_unlabeled) == 0:
break

# Финальное обучение на расширенном наборе
self.base_classifier.fit(self.X_labeled, self.y_labeled)
return self

def predict(self, X):
return self.base_classifier.predict(X)

def predict_proba(self, X):
return self.base_classifier.predict_proba(X)

# Генерация данных с большим объёмом неразмеченных примеров
X, y = make_classification(
n_samples=10000,
n_features=100,
n_informative=50,
n_redundant=50,
n_classes=5,
random_state=42
)

# Разделение: 50 размеченных, 9950 неразмеченных
X_labeled, X_unlabeled, y_labeled, y_unlabeled = train_test_split(
X, y, train_size=50, stratify=y, random_state=42
)

# Базовый классификатор
base_clf = RandomForestClassifier(n_estimators=100, random_state=42)

# Обучение самообучающегося классификатора
self_training_clf = SelfTrainingClassifier(
base_classifier=base_clf,
threshold=0.85,
max_iterations=15
)
self_training_clf.fit(X_labeled, y_labeled, X_unlabeled)

# Оценка качества
final_accuracy = accuracy_score(y, self_training_clf.predict(X))
print(f"\nФинальная точность после самообучения: {final_accuracy:.4f}")

# Сравнение с обучением только на начальных 50 примерах
base_clf.fit(X_labeled, y_labeled)
baseline_accuracy = accuracy_score(y, base_clf.predict(X))
print(f"Точность без самообучения (50 примеров): {baseline_accuracy:.4f}")

# Сравнение с обучением на всех 10000 размеченных примерах
full_clf = RandomForestClassifier(n_estimators=100, random_state=42)
full_clf.fit(X, y)
full_accuracy = accuracy_score(y, full_clf.predict(X))
print(f"Точность при полной разметке (10000 примеров): {full_accuracy:.4f}")

Алгоритмы обработки последовательностей

Рекуррентные нейронные сети и их модификации

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

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

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

История развития архитектур для последовательностей

Джон Элман представил простейшую рекуррентную сеть с контекстными единицами в 1990 году. Сеть Элмана сохраняла выход скрытого слоя для использования на следующем шаге, создавая краткосрочную память. Майкл Манжард и Йорген Шмидхубер в 1997 году предложили архитектуру LSTM как решение проблемы экспоненциального затухания градиентов в глубоких рекуррентных сетях.

Кёнг-Хью Чо и коллеги представили сеть с гейтами обновления в 2014 году как упрощённую альтернативу LSTM. Эксперименты показали сопоставимое качество при меньших вычислительных затратах. Параллельно развивался подход к обработке последовательностей через свёрточные сети с дилатацией, предложенный Шоу-Ченг Ваном в 2016 году. Дилатированные свёртки увеличивают рецептивное поле без увеличения глубины сети, обеспечивая эффективную обработку длинных последовательностей.

Пример реализации на Python

Реализация классификации текста с использованием LSTM в TensorFlow 2.15:

import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Embedding, LSTM, Dense, Dropout, Bidirectional
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
import numpy as np

# Генерация синтетического текстового корпуса
texts = [
"отличный продукт рекомендую всем",
"ужасное качество не покупайте",
"нормально но дорого",
"восхитительно превзошло ожидания",
"полное разочарование деньги на ветер",
"хорошо работает без нареканий",
"кошмарный опыт никогда больше",
"среднее качество за эти деньги",
"просто замечательно всем доволен",
"отвратительно не соответствует описанию"
] * 100 # повторение для увеличения объёма данных

labels = [1, 0, 1, 1, 0, 1, 0, 1, 1, 0] * 100 # 1 - позитив, 0 - негатив

# Токенизация и векторизация текста
max_words = 1000
max_len = 20

tokenizer = Tokenizer(num_words=max_words, oov_token="<OOV>")
tokenizer.fit_on_texts(texts)
sequences = tokenizer.texts_to_sequences(texts)
padded_sequences = pad_sequences(sequences, maxlen=max_len, padding='post')

# Разделение данных
split_idx = int(len(padded_sequences) * 0.8)
X_train = padded_sequences[:split_idx]
y_train = np.array(labels[:split_idx])
X_test = padded_sequences[split_idx:]
y_test = np.array(labels[split_idx:])

# Построение модели с двунаправленным LSTM
model = Sequential([
Embedding(input_dim=max_words, output_dim=64, input_length=max_len),
Bidirectional(LSTM(64, return_sequences=True)),
Dropout(0.3),
Bidirectional(LSTM(32)),
Dropout(0.3),
Dense(24, activation='relu'),
Dense(1, activation='sigmoid')
])

# Компиляция и обучение
model.compile(
optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy']
)

model.fit(
X_train, y_train,
epochs=10,
batch_size=32,
validation_split=0.1,
verbose=1
)

# Оценка качества
loss, accuracy = model.evaluate(X_test, y_test)
print(f"Точность на тестовой выборке: {accuracy:.4f}")

# Прогнозирование новых текстов
new_texts = ["отличное качество и быстрая доставка", "ужасная поддержка клиентов"]
new_sequences = tokenizer.texts_to_sequences(new_texts)
new_padded = pad_sequences(new_sequences, maxlen=max_len, padding='post')
predictions = model.predict(new_padded)

for text, pred in zip(new_texts, predictions):
sentiment = "позитивный" if pred[0] > 0.5 else "негативный"
print(f"'{text}' -> {sentiment} (уверенность {pred[0]:.2%})")

Реализация обработки временных рядов с использованием GRU:

from tensorflow.keras.layers import GRU, TimeDistributed
import pandas as pd

# Генерация многомерного временного ряда
np.random.seed(42)
n_samples = 1000
n_timesteps = 50
n_features = 5

# Создание временных рядов с сезонными компонентами
time_index = np.arange(n_timesteps)
series_data = []
targets = []

for _ in range(n_samples):
sample = np.zeros((n_timesteps, n_features))
for feature in range(n_features):
# Сезонная компонента разной частоты для каждого признака
freq = 2 * np.pi * (feature + 1) / n_timesteps
seasonal = 10 * np.sin(freq * time_index)
noise = np.random.normal(0, 2, n_timesteps)
sample[:, feature] = seasonal + noise

series_data.append(sample)
# Целевая переменная — значение первого признака на следующем шаге
targets.append(sample[-1, 0] + np.random.normal(0, 1))

X = np.array(series_data)
y = np.array(targets)

# Разделение данных
split_idx = int(n_samples * 0.8)
X_train, X_test = X[:split_idx], X[split_idx:]
y_train, y_test = y[:split_idx], y[split_idx:]

# Модель с GRU для регрессии
regression_model = Sequential([
GRU(128, input_shape=(n_timesteps, n_features), return_sequences=True),
Dropout(0.2),
GRU(64),
Dropout(0.2),
Dense(32, activation='relu'),
Dense(1)
])

regression_model.compile(
optimizer='adam',
loss='mse',
metrics=['mae']
)

regression_model.fit(
X_train, y_train,
epochs=30,
batch_size=64,
validation_split=0.1,
verbose=1
)

# Оценка
mae = regression_model.evaluate(X_test, y_test, verbose=0)[1]
print(f"Средняя абсолютная ошибка прогноза: {mae:.2f}")

Алгоритмы обработки несбалансированных данных

Методы изменения распределения классов

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

Алгоритм SMOTE создаёт синтетические примеры миноритарного класса через интерполяцию между ближайшими соседями. Для каждого примера миноритарного класса выбираются k ближайших соседей из того же класса. Синтетический пример генерируется на отрезке между исходным примером и случайно выбранным соседом. Коэффициент генерации определяет количество создаваемых синтетических примеров. Расширенная версия Borderline-SMOTE фокусируется на примерах, расположенных на границе раздела классов, что повышает качество синтетических данных.

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

Взвешивание классов и пороговая оптимизация

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

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

Пример реализации на Python

Реализация обработки несбалансированных данных с использованием imbalanced-learn версии 0.12:

from imblearn.over_sampling import SMOTE, ADASYN
from imblearn.under_sampling import RandomUnderSampler
from imblearn.combine import SMOTETomek
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, roc_auc_score
from sklearn.datasets import make_classification
import numpy as np

# Генерация сильно несбалансированных данных
X, y = make_classification(
n_samples=10000,
n_features=50,
n_informative=30,
n_redundant=20,
n_classes=2,
weights=[0.98, 0.02], # 98% негативных, 2% позитивных
flip_y=0.01,
random_state=42
)

# Разделение данных
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)

print(f"Распределение классов в обучающей выборке: {np.bincount(y_train)}")
print(f"Распределение классов в тестовой выборке: {np.bincount(y_test)}")

# Базовая модель без обработки дисбаланса
base_model = RandomForestClassifier(n_estimators=100, random_state=42)
base_model.fit(X_train, y_train)
base_pred = base_model.predict(X_test)
base_proba = base_model.predict_proba(X_test)[:, 1]

print("\nБазовая модель (без обработки дисбаланса):")
print(classification_report(y_test, base_pred))
print(f"ROC-AUC: {roc_auc_score(y_test, base_proba):.4f}")

# Применение SMOTE
smote = SMOTE(sampling_strategy=0.5, k_neighbors=5, random_state=42)
X_smote, y_smote = smote.fit_resample(X_train, y_train)

print(f"\nПосле SMOTE: {np.bincount(y_smote)}")

smote_model = RandomForestClassifier(n_estimators=100, random_state=42)
smote_model.fit(X_smote, y_smote)
smote_pred = smote_model.predict(X_test)
smote_proba = smote_model.predict_proba(X_test)[:, 1]

print("\nМодель после SMOTE:")
print(classification_report(y_test, smote_pred))
print(f"ROC-AUC: {roc_auc_score(y_test, smote_proba):.4f}")

# Применение комбинированного метода SMOTETomek
smt = SMOTETomek(sampling_strategy=0.5, random_state=42)
X_smt, y_smt = smt.fit_resample(X_train, y_train)

print(f"\nПосле SMOTETomek: {np.bincount(y_smt)}")

smt_model = RandomForestClassifier(n_estimators=100, random_state=42)
smt_model.fit(X_smt, y_smt)
smt_pred = smt_model.predict(X_test)
smt_proba = smt_model.predict_proba(X_test)[:, 1]

print("\nМодель после SMOTETomek:")
print(classification_report(y_test, smt_pred))
print(f"ROC-AUC: {roc_auc_score(y_test, smt_proba):.4f}")

# Взвешивание классов без изменения данных
weighted_model = RandomForestClassifier(
n_estimators=100,
class_weight='balanced',
random_state=42
)
weighted_model.fit(X_train, y_train)
weighted_pred = weighted_model.predict(X_test)
weighted_proba = weighted_model.predict_proba(X_test)[:, 1]

print("\nМодель с взвешиванием классов:")
print(classification_report(y_test, weighted_pred))
print(f"ROC-AUC: {roc_auc_score(y_test, weighted_proba):.4f}")

# Оптимизация порога классификации
from sklearn.metrics import precision_recall_curve

precision, recall, thresholds = precision_recall_curve(y_test, smote_proba)
f1_scores = 2 * (precision * recall) / (precision + recall + 1e-10)
optimal_idx = np.argmax(f1_scores[:-1]) # исключаем последний элемент
optimal_threshold = thresholds[optimal_idx]

print(f"\nОптимальный порог классификации: {optimal_threshold:.3f}")
print(f"Максимальная F1-мера при этом пороге: {f1_scores[optimal_idx]:.4f}")

# Применение оптимального порога
optimized_pred = (smote_proba >= optimal_threshold).astype(int)
print("\nМодель с оптимизированным порогом:")
print(classification_report(y_test, optimized_pred))

Реализация на C# с использованием ML.NET версии 3.0 для обработки несбалансированных данных:

using Microsoft.ML;
using Microsoft.ML.Data;
using Microsoft.ML.Trainers;
using System;
using System.Collections.Generic;
using System.Linq;

public class TransactionData
{
[LoadColumn(0)] public float Amount;
[LoadColumn(1)] public float Time;
[LoadColumn(2)] public float V1;
[LoadColumn(3)] public float V2;
[LoadColumn(4)] public float V3;
[LoadColumn(5)] public bool Label; // true - мошенничество, false - нормальная транзакция
}

public class TransactionPrediction
{
[ColumnName("PredictedLabel")] public bool Prediction;
public float Probability;
public float Score;
}

// Создание контекста ML.NET
var mlContext = new MLContext(seed: 42);

// Загрузка данных (пример с синтетическими данными)
var dataView = mlContext.Data.LoadFromEnumerable(GetSyntheticData());

// Разделение на обучающую и тестовую выборки
var splitDataView = mlContext.Data.TrainTestSplit(dataView, testFraction: 0.2);

// Создание конвейера с взвешиванием классов
var pipeline = mlContext.Transforms.Concatenate("Features",
"Amount", "Time", "V1", "V2", "V3")
.Append(mlContext.BinaryClassification.Trainers.SdcaLogisticRegression(
new SdcaLogisticRegressionBinaryTrainer.Options
{
LabelColumnName = "Label",
FeatureColumnName = "Features",
ExampleWeightColumnName = "Weight" // колонка с весами примеров
}));

// Добавление весов для балансировки классов
var weightedData = mlContext.Data.ApplyOnEnumerable(splitDataView.TrainSet, AddWeights);

// Обучение модели
var model = pipeline.Fit(weightedData);

// Оценка качества
var predictions = model.Transform(splitDataView.TestSet);
var metrics = mlContext.BinaryClassification.Evaluate(predictions, labelColumnName: "Label");

Console.WriteLine($"Точность: {metrics.Accuracy:F4}");
Console.WriteLine($"Полнота (миноритарный класс): {metrics.Recall:F4}");
Console.WriteLine($"F1-мера: {metrics.F1Score:F4}");

// Метод генерации синтетических данных
IEnumerable<TransactionData> GetSyntheticData()
{
var random = new Random(42);
var transactions = new List<TransactionData>();

// 9900 нормальных транзакций
for (int i = 0; i < 9900; i++)
{
transactions.Add(new TransactionData
{
Amount = (float)(random.NextDouble() * 1000),
Time = (float)(random.NextDouble() * 86400),
V1 = (float)random.NextDouble(),
V2 = (float)random.NextDouble(),
V3 = (float)random.NextDouble(),
Label = false
});
}

// 100 мошеннических транзакций
for (int i = 0; i < 100; i++)
{
transactions.Add(new TransactionData
{
Amount = (float)(random.NextDouble() * 5000 + 1000), // обычно крупнее
Time = (float)(random.NextDouble() * 86400),
V1 = (float)(random.NextDouble() + 0.5),
V2 = (float)(random.NextDouble() + 0.5),
V3 = (float)(random.NextDouble() + 0.5),
Label = true
});
}

return transactions;
}

// Метод добавления весов для балансировки
IEnumerable<(TransactionData data, float Weight)> AddWeights(IDataView dataView)
{
var enumerable = mlContext.Data.CreateEnumerable<TransactionData>(dataView, reuseRowObject: false);
var totalCount = enumerable.Count();
var fraudCount = enumerable.Count(x => x.Label);
var normalCount = totalCount - fraudCount;

var fraudWeight = (float)totalCount / (2 * fraudCount);
var normalWeight = (float)totalCount / (2 * normalCount);

foreach (var item in enumerable)
{
yield return (item, item.Label ? fraudWeight : normalWeight);
}
}

Алгоритмы интерпретируемости моделей

Методы объяснения прогнозов отдельных объектов

Метод LIME аппроксимирует сложную модель локально интерпретируемой моделью в окрестности конкретного объекта. Алгоритм генерирует синтетические примеры путём небольших возмущений признаков исходного объекта. Сложная модель выдаёт прогнозы для синтетических примеров. Линейная модель обучается предсказывать эти прогнозы на основе возмущённых признаков. Коэффициенты линейной модели интерпретируются как важность признаков для конкретного прогноза.

Метод SHAP основан на теории кооперативных игр и концепции значения Шепли. Алгоритм оценивает вклад каждого признака в отклонение прогноза от среднего значения по всему набору данных. Для каждого признака вычисляется средний вклад при добавлении признака в различные комбинации других признаков. Сумма значений Шепли всех признаков равна разнице между прогнозом модели и средним прогнозом по обучающей выборке.

Глобальная интерпретируемость моделей

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

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

Пример реализации на Python

Реализация интерпретации прогнозов с использованием SHAP версии 0.44:

import shap
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt

# Генерация данных
X, y = make_classification(
n_samples=5000,
n_features=20,
n_informative=15,
n_redundant=5,
random_state=42
)

feature_names = [f'feature_{i}' for i in range(20)]

# Разделение данных
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)

# Обучение модели
model = RandomForestClassifier(n_estimators=100, max_depth=10, random_state=42)
model.fit(X_train, y_train)

# Создание эксплайнера SHAP
explainer = shap.TreeExplainer(model)

# Вычисление значений Шепли для тестовой выборки
shap_values = explainer.shap_values(X_test)

# Визуализация важности признаков (глобальная интерпретация)
plt.figure(figsize=(12, 8))
shap.summary_plot(
shap_values[1],
X_test,
feature_names=feature_names,
plot_type="bar",
show=False
)
plt.title('Глобальная важность признаков (значения Шепли)')
plt.tight_layout()
plt.savefig('shap_global_importance.png')
plt.close()

# Визуализация распределения влияния признаков
plt.figure(figsize=(12, 8))
shap.summary_plot(
shap_values[1],
X_test,
feature_names=feature_names,
show=False
)
plt.title('Распределение влияния признаков на прогнозы')
plt.tight_layout()
plt.savefig('shap_summary_plot.png')
plt.close()

# Интерпретация отдельного прогноза
sample_idx = 42
sample = X_test[sample_idx:sample_idx+1]

plt.figure(figsize=(10, 6))
shap.waterfall_plot(
shap.Explanation(
values=shap_values[1][sample_idx],
base_values=explainer.expected_value[1],
data=sample[0],
feature_names=feature_names
)
)
plt.title(f'Объяснение прогноза для объекта {sample_idx}')
plt.tight_layout()
plt.savefig('shap_waterfall.png')
plt.close()

# Силовые графики для нескольких объектов
plt.figure(figsize=(12, 10))
shap.force_plot(
explainer.expected_value[1],
shap_values[1][:50, :],
X_test[:50, :],
feature_names=feature_names,
matplotlib=True,
show=False
)
plt.title('Силовые графики для 50 объектов')
plt.tight_layout()
plt.savefig('shap_force_plot.png')
plt.close()

# Зависимость прогноза от двух ключевых признаков
plt.figure(figsize=(12, 5))
shap.dependence_plot(
0, # индекс первого признака
shap_values[1],
X_test,
feature_names=feature_names,
interaction_index=1, # взаимодействие с вторым признаком
show=False
)
plt.title('Зависимость влияния признака 0 от значения признака 1')
plt.tight_layout()
plt.savefig('shap_dependence_plot.png')
plt.close()

Реализация метода LIME для интерпретации текстовых классификаторов:

from lime.lime_text import LimeTextExplainer
from sklearn.pipeline import make_pipeline
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
import numpy as np

# Подготовка текстовых данных
texts = [
"отличный продукт быстрая доставка рекомендую",
"ужасное качество деньги на ветер не покупайте",
"нормальный товар среднее качество",
"восхитительно превзошло все ожидания",
"кошмар полное разочарование",
"хорошо работает без проблем",
"отвратительно не соответствует описанию",
"приемлемо за эти деньги",
"просто замечательно всем доволен",
"ужасная поддержка клиентов"
] * 100

labels = [1, 0, 1, 1, 0, 1, 0, 1, 1, 0] * 100 # 1 - позитив, 0 - негатив

# Создание конвейера классификации
vectorizer = TfidfVectorizer(max_features=1000, ngram_range=(1, 2))
classifier = LogisticRegression(max_iter=1000, random_state=42)
pipeline = make_pipeline(vectorizer, classifier)

# Обучение модели
pipeline.fit(texts, labels)

# Создание эксплайнера LIME
class_names = ['негативный', 'позитивный']
explainer = LimeTextExplainer(class_names=class_names)

# Интерпретация прогноза для нового текста
text_to_explain = "отличное качество и очень быстрая доставка"
model_prediction = pipeline.predict([text_to_explain])[0]
model_proba = pipeline.predict_proba([text_to_explain])[0]

print(f"Текст для анализа: '{text_to_explain}'")
print(f"Прогноз модели: {class_names[model_prediction]}")
print(f"Вероятности: негативный={model_proba[0]:.2%}, позитивный={model_proba[1]:.2%}")

# Получение объяснения
explanation = explainer.explain_instance(
text_to_explain,
pipeline.predict_proba,
num_features=10,
top_labels=1
)

# Вывод важных слов
print("\nВажные слова для позитивного прогноза:")
for word, weight in explanation.as_list(label=1):
sentiment = "↑" if weight > 0 else "↓"
print(f" {word}: {weight:+.4f} {sentiment}")

# Визуализация объяснения
fig = explanation.as_pyplot_figure(label=1)
fig.set_size_inches(10, 6)
plt.title('LIME: Важность слов для прогноза')
plt.tight_layout()
plt.savefig('lime_explanation.png')
plt.close()

Алгоритмы многоклассовой классификации

Стратегии декомпозиции задачи

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

Стратегия один-против-одного обучает бинарный классификатор для каждой пары классов. Количество классификаторов растёт квадратично с числом классов по формуле N×(N-1)/2. При прогнозировании каждый классификатор голосует за один из двух классов своей пары. Объект относится к классу, набравшему наибольшее количество голосов. Эта стратегия эффективна для алгоритмов, плохо масштабирующихся на многоклассовые задачи напрямую.

Многоклассовая логистическая регрессия обобщает бинарную версию через функцию softmax. Линейная комбинация признаков вычисляется для каждого класса. Функция softmax преобразует эти значения в вероятности, суммирующиеся в единицу. Обучение выполняется минимизацией кросс-энтропийной функции потерь. Этот подход обеспечивает калиброванные вероятности прогнозов и интерпретируемость коэффициентов.

Иерархическая классификация

Иерархическая классификация организует классы в древовидную структуру с отношениями «родитель-потомок». Модель последовательно принимает решения на каждом уровне иерархии, сужая пространство возможных классов. Этот подход эффективен когда классы естественным образом образуют таксономию — например, биологические виды, категории товаров или тематические рубрики документов.

Локальный подход обучает отдельный классификатор для каждого узла иерархии, предсказывающий потомков этого узла. Глобальный подход обучает единую модель для всех классов с учётом иерархических ограничений — прогноз должен соответствовать путю от корня к листу в таксономии. Комбинированный подход использует глобальную модель для верхних уровней иерархии и локальные модели для нижних уровней.

Пример реализации на Python

Реализация многоклассовой классификации с различными стратегиями:

from sklearn.ensemble import RandomForestClassifier
from sklearn.multiclass import OneVsRestClassifier, OneVsOneClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, accuracy_score
import numpy as np

# Генерация данных для задачи с 5 классами
X, y = make_classification(
n_samples=5000,
n_features=40,
n_informative=30,
n_redundant=10,
n_classes=5,
n_clusters_per_class=1,
random_state=42
)

# Разделение данных
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)

# Базовая модель с нативной поддержкой многоклассовой классификации
base_model = RandomForestClassifier(
n_estimators=100,
max_depth=15,
random_state=42,
n_jobs=-1
)
base_model.fit(X_train, y_train)
base_pred = base_model.predict(X_test)
base_proba = base_model.predict_proba(X_test)

print("Базовая модель (нативная многоклассовая поддержка):")
print(f"Точность: {accuracy_score(y_test, base_pred):.4f}")
print(classification_report(y_test, base_pred))

# Стратегия один-против-всех
ovr_model = OneVsRestClassifier(
LogisticRegression(max_iter=1000, random_state=42)
)
ovr_model.fit(X_train, y_train)
ovr_pred = ovr_model.predict(X_test)

print("\nСтратегия один-против-всех:")
print(f"Точность: {accuracy_score(y_test, ovr_pred):.4f}")
print(classification_report(y_test, ovr_pred))

# Стратегия один-против-одного
ovo_model = OneVsOneClassifier(
LogisticRegression(max_iter=1000, random_state=42)
)
ovo_model.fit(X_train, y_train)
ovo_pred = ovo_model.predict(X_test)

print("\nСтратегия один-против-одного:")
print(f"Точность: {accuracy_score(y_test, ovo_pred):.4f}")
print(classification_report(y_test, ovo_pred))

# Многоклассовая логистическая регрессия с функцией softmax
softmax_model = LogisticRegression(
multi_class='multinomial',
solver='lbfgs',
max_iter=1000,
random_state=42
)
softmax_model.fit(X_train, y_train)
softmax_pred = softmax_model.predict(X_test)
softmax_proba = softmax_model.predict_proba(X_test)

print("\nМногоклассовая логистическая регрессия (softmax):")
print(f"Точность: {accuracy_score(y_test, softmax_pred):.4f}")
print(classification_report(y_test, softmax_pred))

# Анализ калибровки вероятностей
from sklearn.calibration import calibration_curve
import matplotlib.pyplot as plt

plt.figure(figsize=(10, 8))

for i in range(5): # для каждого класса
fraction_of_positives, mean_predicted_value = calibration_curve(
(y_test == i).astype(int),
softmax_proba[:, i],
n_bins=10
)

plt.plot(
mean_predicted_value,
fraction_of_positives,
marker='o',
label=f'Класс {i}'
)

plt.plot([0, 1], [0, 1], 'k--', label='Идеальная калибровка')
plt.xlabel('Среднее предсказанное значение')
plt.ylabel('Доля положительных случаев')
plt.title('Калибровка вероятностей для каждого класса')
plt.legend()
plt.grid(True, alpha=0.3)
plt.savefig('multiclass_calibration.png')
plt.close()

# Важность признаков для каждого класса в логистической регрессии
coef_df = pd.DataFrame(
softmax_model.coef_,
index=[f'Класс_{i}' for i in range(5)],
columns=[f'feature_{j}' for j in range(40)]
)

print("\nТоп-5 важных признаков для каждого класса (логистическая регрессия):")
for class_idx in range(5):
top_features = coef_df.loc[f'Класс_{class_idx}'].abs().nlargest(5)
print(f"\nКласс {class_idx}:")
for feature, importance in top_features.items():
raw_coef = coef_df.loc[f'Класс_{class_idx}', feature]
direction = "положительное" if raw_coef > 0 else "отрицательное"
print(f" {feature}: {importance:.4f} ({direction} влияние)")

Реализация иерархической классификации для таксономии товаров:

from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from collections import defaultdict

class HierarchicalClassifier:
def __init__(self):
self.hierarchy = defaultdict(dict)
self.models = {}

def define_hierarchy(self, categories):
"""
Определение иерархии категорий.
Формат: {родитель: [потомок1, потомок2, ...]}
"""
self.hierarchy = categories

def fit(self, X, y_path):
"""
Обучение иерархической модели.
y_path: список путей к листовым категориям для каждого объекта
Пример пути: ['электроника', 'мобильные_устройства', 'смартфоны']
"""
# Группировка объектов по уровням иерархии
level_data = defaultdict(lambda: defaultdict(list))

for idx, path in enumerate(y_path):
for level, category in enumerate(path):
level_data[level][category].append(idx)

# Обучение моделей для каждого узла иерархии
for level in sorted(level_data.keys()):
categories_at_level = list(level_data[level].keys())

if level == 0:
# Корневой уровень — обучение на всех данных
X_level = X
y_level = [next((cat for cat in categories_at_level
if idx in level_data[level][cat]), None)
for idx in range(len(X))]
self.models['root'] = RandomForestClassifier(
n_estimators=50,
max_depth=10,
random_state=42
)
self.models['root'].fit(X_level, y_level)
else:
# Обучение моделей для каждого родительского узла
parent_level = level - 1
for parent_cat in level_data[parent_level].keys():
# Сбор данных для потомков данного родителя
child_indices = []
child_labels = []

for child_cat in categories_at_level:
indices = level_data[level][child_cat]
# Фильтрация объектов, принадлежащих этому родителю
valid_indices = [idx for idx in indices
if idx in level_data[parent_level][parent_cat]]
if valid_indices:
child_indices.extend(valid_indices)
child_labels.extend([child_cat] * len(valid_indices))

if child_indices:
X_child = X[child_indices]
model_key = f"{parent_cat}_level_{level}"
self.models[model_key] = DecisionTreeClassifier(
max_depth=8,
random_state=42
)
self.models[model_key].fit(X_child, child_labels)

def predict(self, X):
"""
Прогнозирование полного пути категорий для объектов.
Возвращает список путей той же длины, что и X.
"""
predictions = []

for x in X:
path = []
current_node = 'root'
current_x = x.reshape(1, -1)

while current_node in self.models:
model = self.models[current_node]
pred = model.predict(current_x)[0]
path.append(pred)

# Формирование ключа для следующего уровня
next_node = f"{pred}_level_{len(path)}"
if next_node in self.models:
current_node = next_node
else:
break

predictions.append(path)

return predictions

# Пример использования с синтетическими данными таксономии
np.random.seed(42)

# Определение иерархии категорий товаров
taxonomy = {
'root': ['электроника', 'одежда', 'книги'],
'электроника': ['мобильные_устройства', 'компьютеры', 'аудиотехника'],
'мобильные_устройства': ['смартфоны', 'планшеты'],
'компьютеры': ['ноутбуки', 'настольные_пк'],
'одежда': ['верхняя_одежда', 'обувь'],
'верхняя_одежда': ['куртки', 'пальто'],
'книги': ['художественные', 'технические']
}

# Генерация синтетических признаков для товаров
n_samples = 1000
n_features = 30
X = np.random.randn(n_samples, n_features)

# Генерация путей категорий
category_paths = []
for _ in range(n_samples):
path = ['электроника']
if np.random.rand() < 0.5:
path.append('мобильные_устройства')
path.append('смартфоны' if np.random.rand() < 0.7 else 'планшеты')
else:
path.append('компьютеры')
path.append('ноутбуки' if np.random.rand() < 0.6 else 'настольные_пк')
category_paths.append(path)

# Создание и обучение иерархического классификатора
hierarchical_clf = HierarchicalClassifier()
hierarchical_clf.define_hierarchy(taxonomy)
hierarchical_clf.fit(X, category_paths)

# Прогнозирование для новых объектов
test_X = np.random.randn(5, n_features)
predictions = hierarchical_clf.predict(test_X)

print("Прогнозируемые категории для тестовых объектов:")
for i, pred_path in enumerate(predictions):
print(f"Объект {i}: {' -> '.join(pred_path)}")

Алгоритмы сегментации изображений

Семантическая и экземплярная сегментация

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

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

История развития архитектур сегментации

Оливер Роннебергер, Филипп Фишер и Томас Брок представили архитектуру U-Net в 2015 году для биомедицинской сегментации клеток. Ключевым нововведением стали пропускные соединения, решающие проблему потери пространственной информации в глубоких сетях. FCN (Fully Convolutional Network), предложенная Джонатаном Лонгом и коллегами в 2014 году, заменила полносвязные слои свёртками, обеспечив обработку изображений произвольного размера.

Mask R-CNN расширил архитектуру Faster R-CNN добавлением параллельной ветви для предсказания масок сегментации. Метод предсказывает ограничивающую рамку, класс объекта и бинарную маску для каждого экземпляра. Пирамида признаков обеспечивает обработку объектов различных масштабов. Версия 2.0 библиотеки Detectron2 от Facebook AI Research реализует оптимизированные версии этих архитектур с поддержкой распределённого обучения.

Пример реализации на Python

Реализация семантической сегментации с использованием библиотеки segmentation_models_pytorch версии 0.3:

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import albumentations as A
from albumentations.pytorch import ToTensorV2
import numpy as np
import cv2
import os

# Синтетический датасет для демонстрации
class SyntheticSegmentationDataset(Dataset):
def __init__(self, size=1000, transform=None):
self.size = size
self.transform = transform
self.shapes = ['circle', 'rectangle', 'triangle']

def __len__(self):
return self.size

def __getitem__(self, idx):
# Генерация синтетического изображения с фигурами
img = np.zeros((256, 256, 3), dtype=np.uint8)
mask = np.zeros((256, 256), dtype=np.uint8)

# Случайное количество фигур
n_shapes = np.random.randint(1, 4)
for _ in range(n_shapes):
shape_type = np.random.choice(self.shapes)
color = np.random.randint(50, 255, 3).tolist()
x, y = np.random.randint(50, 200, 2)
size = np.random.randint(20, 60)

if shape_type == 'circle':
cv2.circle(img, (x, y), size, color, -1)
cv2.circle(mask, (x, y), size, 1, -1)
elif shape_type == 'rectangle':
cv2.rectangle(img, (x-size, y-size), (x+size, y+size), color, -1)
cv2.rectangle(mask, (x-size, y-size), (x+size, y+size), 1, -1)
else: # triangle
pts = np.array([[x, y-size], [x-size, y+size], [x+size, y+size]], np.int32)
cv2.fillPoly(img, [pts], color)
cv2.fillPoly(mask, [pts], 1)

if self.transform:
augmented = self.transform(image=img, mask=mask)
img = augmented['image']
mask = augmented['mask']

return img, mask.long()

# Аугментации и нормализация
train_transform = A.Compose([
A.HorizontalFlip(p=0.5),
A.VerticalFlip(p=0.5),
A.RandomRotate90(p=0.5),
A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),
ToTensorV2()
])

val_transform = A.Compose([
A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),
ToTensorV2()
])

# Создание датасетов
train_dataset = SyntheticSegmentationDataset(size=800, transform=train_transform)
val_dataset = SyntheticSegmentationDataset(size=200, transform=val_transform)

train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True, num_workers=4)
val_loader = DataLoader(val_dataset, batch_size=16, shuffle=False, num_workers=4)

# Загрузка предобученной модели U-Net
import segmentation_models_pytorch as smp

model = smp.Unet(
encoder_name="resnet34",
encoder_weights="imagenet",
in_channels=3,
classes=2 # фон + объект
)

# Функция потерь Dice + кросс-энтропия
class DiceLoss(nn.Module):
def __init__(self, smooth=1.0):
super(DiceLoss, self).__init__()
self.smooth = smooth

def forward(self, logits, targets):
probs = torch.sigmoid(logits)
intersection = (probs * targets).sum()
dice = (2. * intersection + self.smooth) / (probs.sum() + targets.sum() + self.smooth)
return 1 - dice

dice_loss = DiceLoss()
bce_loss = nn.BCEWithLogitsLoss()

def combined_loss(logits, targets):
return dice_loss(logits, targets) + bce_loss(logits, targets.float())

# Обучение модели
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
optimizer = optim.Adam(model.parameters(), lr=1e-4)

for epoch in range(15):
model.train()
train_loss = 0.0

for images, masks in train_loader:
images = images.to(device)
masks = masks.to(device)

optimizer.zero_grad()
outputs = model(images)
loss = combined_loss(outputs, masks)
loss.backward()
optimizer.step()

train_loss += loss.item()

# Валидация
model.eval()
val_loss = 0.0

with torch.no_grad():
for images, masks in val_loader:
images = images.to(device)
masks = masks.to(device)
outputs = model(images)
loss = combined_loss(outputs, masks)
val_loss += loss.item()

print(f"Эпоха {epoch+1}: потери обучения {train_loss/len(train_loader):.4f}, "
f"потери валидации {val_loss/len(val_loader):.4f}")

# Визуализация результатов сегментации
import matplotlib.pyplot as plt

model.eval()
images, masks = next(iter(val_loader))
images = images.to(device)
with torch.no_grad():
outputs = model(images)
preds = (torch.sigmoid(outputs) > 0.5).float()

# Отображение первых 4 изображений из батча
fig, axes = plt.subplots(4, 3, figsize=(12, 16))
for i in range(4):
# Исходное изображение
img = images[i].cpu().permute(1, 2, 0).numpy()
img = img * np.array([0.229, 0.224, 0.225]) + np.array([0.485, 0.456, 0.406])
img = np.clip(img, 0, 1)
axes[i, 0].imshow(img)
axes[i, 0].set_title('Исходное изображение')
axes[i, 0].axis('off')

# Истинная маска
axes[i, 1].imshow(masks[i].cpu().numpy(), cmap='gray')
axes[i, 1].set_title('Истинная маска')
axes[i, 1].axis('off')

# Предсказанная маска
axes[i, 2].imshow(preds[i][0].cpu().numpy(), cmap='gray')
axes[i, 2].set_title('Предсказанная маска')
axes[i, 2].axis('off')

plt.tight_layout()
plt.savefig('segmentation_results.png')
plt.close()

Алгоритмы детекции объектов

Двухступенчатые и одноступенчатые детекторы

Двухступенчатые детекторы разделяют процесс обнаружения на генерацию предложений регионов и их классификацию. Faster R-CNN использует региональную свёрточную сеть для предложения потенциальных областей объектов. Каждое предложение преобразуется в фиксированный размер через пространственное пирамидальное пулирование и классифицируется полносвязным слоем. Архитектура обеспечивает высокую точность при умеренной скорости обработки.

Одноступенчатые детекторы предсказывают ограничивающие рамки и классы напрямую с карты признаков без этапа предложения регионов. YOLO (You Only Look Once) разделяет изображение на сетку ячеек и предсказывает несколько рамок в каждой ячейке. Алгоритм обрабатывает изображение единожды через свёрточную сеть, обеспечивая высокую скорость работы в реальном времени. RetinaNet решает проблему дисбаланса между фоновыми и объектными примерами через фокальную функцию потерь, усиливающую вклад трудных примеров в обучение.

Эволюция архитектур детекции

Росс Гиршик представил архитектуру R-CNN в 2014 году, комбинирующую селективный поиск для генерации регионов с классификацией через свёрточную сеть. Fast R-CNN в 2015 году ускорил обработку через общее вычисление признаков для всего изображения. Faster R-CNN в 2015 году заменил внешний алгоритм селективного поиска на обучаемую региональную сеть, завершив переход к полностью свёрточным детекторам.

Джозеф Редмон представил YOLO в 2016 году как радикально отличающийся подход к детекции. Версия YOLOv3 в 2018 году ввела пирамидальную обработку признаков для обнаружения объектов различных масштабов. Современная версия YOLOv8 от Ultralytics поддерживает задачи детекции, сегментации и классификации в единой архитектуре с оптимизацией для промышленного развёртывания.

Пример реализации на Python

Реализация детекции объектов с использованием библиотеки Ultralytics YOLOv8 версии 8.2:

from ultralytics import YOLO
import cv2
import numpy as np
import matplotlib.pyplot as plt

# Загрузка предобученной модели
model = YOLO('yolov8n.pt') # nano версия для быстрой работы

# Генерация синтетических изображений для демонстрации
def create_synthetic_image():
img = np.ones((480, 640, 3), dtype=np.uint8) * 240

# Добавление объектов разных классов
cv2.rectangle(img, (100, 100), (200, 250), (0, 0, 255), -1) # красный прямоугольник (человек)
cv2.circle(img, (400, 150), 60, (255, 0, 0), -1) # синий круг (машина)
cv2.rectangle(img, (250, 300), (350, 400), (0, 255, 0), -1) # зелёный прямоугольник (стул)

return img

# Создание набора синтетических изображений
test_images = [create_synthetic_image() for _ in range(5)]

# Детекция объектов на изображениях
results = model(test_images, conf=0.25, iou=0.45)

# Визуализация результатов
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
axes = axes.flatten()

for i, (img, result) in enumerate(zip(test_images, results)):
if i >= 5:
break

# Получение аннотированного изображения
annotated = result.plot()

axes[i].imshow(cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB))
axes[i].set_title(f'Изображение {i+1}: {len(result.boxes)} объектов')
axes[i].axis('off')

# Добавление легенды
axes[5].axis('off')
legend_text = "Классы объектов:\n0: человек\n2: машина\n59: стул"
axes[5].text(0.5, 0.5, legend_text, ha='center', va='center', fontsize=12, family='monospace')

plt.tight_layout()
plt.savefig('object_detection_results.png')
plt.close()

# Детальный анализ результатов для первого изображения
first_result = results[0]
boxes = first_result.boxes

print("Детектированные объекты на первом изображении:")
print(f"{'Класс':<15} {'Уверенность':<15} {'Координаты рамки'}")
print("-" * 50)

for box in boxes:
cls_id = int(box.cls[0])
conf = float(box.conf[0])
xyxy = box.xyxy[0].cpu().numpy()

# Картирование индексов классов на названия (упрощённое)
class_names = {0: 'человек', 2: 'машина', 59: 'стул'}
cls_name = class_names.get(cls_id, f'класс_{cls_id}')

print(f"{cls_name:<15} {conf:.2%} [{xyxy[0]:.0f}, {xyxy[1]:.0f}, {xyxy[2]:.0f}, {xyxy[3]:.0f}]")

# Экспорт модели в формат ONNX для развёртывания
model.export(format='onnx', imgsz=640, simplify=True)
print("\nМодель экспортирована в формат ONNX для промышленного использования")

Реализация кастомного детектора на базе PyTorch:

import torch
import torch.nn as nn
import torchvision
from torchvision.models.detection import FasterRCNN
from torchvision.models.detection.rpn import AnchorGenerator

# Создание энкодера на основе ResNet
backbone = torchvision.models.resnet50(weights=torchvision.models.ResNet50_Weights.IMAGENET1K_V1)
modules = list(backbone.children())[:-2] # Удаление полносвязных слоёв и адаптивного пулинга
backbone = nn.Sequential(*modules)
backbone.out_channels = 2048

# Настройка генератора якорей
anchor_generator = AnchorGenerator(
sizes=((32, 64, 128, 256, 512),),
aspect_ratios=((0.5, 1.0, 2.0),)
)

# ROI пулер для извлечения признаков регионов
roi_pooler = torchvision.ops.MultiScaleRoIAlign(
featmap_names=['0'],
output_size=7,
sampling_ratio=2
)

# Создание модели Faster R-CNN
model = FasterRCNN(
backbone=backbone,
num_classes=3, # фон + 2 класса объектов
rpn_anchor_generator=anchor_generator,
box_roi_pool=roi_pooler
)

# Подготовка синтетических данных для обучения
def generate_synthetic_detection_data(n_samples=100):
images = []
targets = []

for _ in range(n_samples):
# Создание изображения с фоном
img = torch.rand(3, 256, 256)
images.append(img)

# Генерация случайных ограничивающих рамок
n_boxes = torch.randint(1, 4, (1,)).item()
boxes = torch.zeros((n_boxes, 4))
labels = torch.zeros((n_boxes,), dtype=torch.int64)

for i in range(n_boxes):
x1 = torch.randint(20, 150, (1,)).item()
y1 = torch.randint(20, 150, (1,)).item()
x2 = x1 + torch.randint(30, 100, (1,)).item()
y2 = y1 + torch.randint(30, 100, (1,)).item()

boxes[i] = torch.tensor([x1, y1, x2, y2], dtype=torch.float)
labels[i] = torch.randint(1, 3, (1,)).item() # классы 1 и 2

targets.append({
'boxes': boxes,
'labels': labels
})

return images, targets

# Обучение модели
images, targets = generate_synthetic_detection_data(50)
model.train()

params = [p for p in model.parameters() if p.requires_grad]
optimizer = torch.optim.SGD(params, lr=0.005, momentum=0.9, weight_decay=0.0005)

for epoch in range(10):
loss_dict = model(images, targets)
losses = sum(loss for loss in loss_dict.values())

optimizer.zero_grad()
losses.backward()
optimizer.step()

print(f"Эпоха {epoch+1}: общие потери {losses.item():.4f}, "
f"потери классификации RPN {loss_dict['loss_objectness'].item():.4f}, "
f"потери регрессии рамок {loss_dict['loss_box_reg'].item():.4f}")

# Инференс на новых данных
model.eval()
test_image = torch.rand(1, 3, 256, 256)
with torch.no_grad():
predictions = model(test_image)

print(f"\nОбнаружено объектов: {len(predictions[0]['boxes'])}")
for i, (box, label, score) in enumerate(zip(
predictions[0]['boxes'],
predictions[0]['labels'],
predictions[0]['scores']
)):
if score > 0.5:
print(f"Объект {i+1}: класс {label.item()}, уверенность {score.item():.2%}, "
f"рамка [{box[0]:.0f}, {box[1]:.0f}, {box[2]:.0f}, {box[3]:.0f}]")

Алгоритмы обработки графов

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

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

Атрибуты узлов и рёбер расширяют базовое представление дополнительной информацией. Узлы могут содержать признаковые векторы, описывающие свойства объектов. Рёбра могут включать веса, отражающие силу связи, или категориальные признаки, описывающие тип отношения. Графы с атрибутами поддерживают более сложные задачи машинного обучения по сравнению с неатрибутированными графами.

Графовые нейронные сети

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

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

Архитектура GAT (Graph Attention Network) вводит механизм внимания для взвешивания вклада соседей при агрегации. Каждое ребро получает вес внимания, вычисляемый на основе совместимости представлений соединённых узлов. Многоголовое внимание параллельно вычисляет несколько независимых представлений внимания, повышая выразительную способность модели. Механизм внимания адаптируется к структуре конкретного графа без необходимости ручной настройки.

Пример реализации на Python

Реализация графовой нейронной сети с использованием библиотеки PyTorch Geometric версии 2.4:

import torch
import torch.nn.functional as F
from torch_geometric.nn import GCNConv, GATConv, SAGEConv
from torch_geometric.data import Data
import numpy as np
import matplotlib.pyplot as plt

# Генерация синтетического графа с сообществами
def generate_community_graph(n_nodes=200, n_communities=4, p_in=0.1, p_out=0.01):
# Инициализация матрицы смежности
adj = np.zeros((n_nodes, n_nodes))

# Создание сообществ
community_size = n_nodes // n_communities
for comm in range(n_communities):
start = comm * community_size
end = start + community_size if comm < n_communities - 1 else n_nodes

# Плотные связи внутри сообщества
for i in range(start, end):
for j in range(i + 1, end):
if np.random.rand() < p_in:
adj[i, j] = adj[j, i] = 1

# Разреженные связи между сообществами
for i in range(n_nodes):
for j in range(i + 1, n_nodes):
if adj[i, j] == 0 and np.random.rand() < p_out:
adj[i, j] = adj[j, i] = 1

# Создание списка рёбер
edge_index = []
for i in range(n_nodes):
for j in range(i + 1, n_nodes):
if adj[i, j] == 1:
edge_index.append([i, j])
edge_index.append([j, i]) # неориентированный граф

edge_index = torch.tensor(edge_index, dtype=torch.long).t().contiguous()

# Признаки узлов — координаты в пространстве сообществ
x = torch.zeros(n_nodes, n_communities)
for node in range(n_nodes):
comm = node // community_size
x[node, comm] = 1.0

# Метки классов — номер сообщества
y = torch.tensor([node // community_size for node in range(n_nodes)], dtype=torch.long)

return Data(x=x, edge_index=edge_index, y=y), n_communities

# Создание синтетического графа
data, n_communities = generate_community_graph(n_nodes=300, n_communities=5)
print(f"Граф содержит {data.num_nodes} узлов и {data.edge_index.size(1) // 2} рёбер")

# Базовая графовая свёрточная сеть
class GCN(torch.nn.Module):
def __init__(self, in_channels, hidden_channels, out_channels):
super(GCN, self).__init__()
self.conv1 = GCNConv(in_channels, hidden_channels)
self.conv2 = GCNConv(hidden_channels, out_channels)

def forward(self, x, edge_index):
x = self.conv1(x, edge_index)
x = F.relu(x)
x = F.dropout(x, p=0.5, training=self.training)
x = self.conv2(x, edge_index)
return F.log_softmax(x, dim=1)

# Сеть с механизмом внимания
class GAT(torch.nn.Module):
def __init__(self, in_channels, hidden_channels, out_channels, heads=4):
super(GAT, self).__init__()
self.conv1 = GATConv(in_channels, hidden_channels, heads=heads, dropout=0.6)
self.conv2 = GATConv(hidden_channels * heads, out_channels, heads=1, concat=False, dropout=0.6)

def forward(self, x, edge_index):
x = self.conv1(x, edge_index)
x = F.elu(x)
x = F.dropout(x, p=0.5, training=self.training)
x = self.conv2(x, edge_index)
return F.log_softmax(x, dim=1)

# Сеть GraphSAGE
class GraphSAGE(torch.nn.Module):
def __init__(self, in_channels, hidden_channels, out_channels):
super(GraphSAGE, self).__init__()
self.conv1 = SAGEConv(in_channels, hidden_channels)
self.conv2 = SAGEConv(hidden_channels, out_channels)

def forward(self, x, edge_index):
x = self.conv1(x, edge_index)
x = F.relu(x)
x = F.dropout(x, p=0.5, training=self.training)
x = self.conv2(x, edge_index)
return F.log_softmax(x, dim=1)

# Разделение узлов на обучающие, валидационные и тестовые
def split_nodes(num_nodes, train_ratio=0.6, val_ratio=0.2):
indices = torch.randperm(num_nodes)
train_end = int(num_nodes * train_ratio)
val_end = train_end + int(num_nodes * val_ratio)

train_mask = torch.zeros(num_nodes, dtype=torch.bool)
val_mask = torch.zeros(num_nodes, dtype=torch.bool)
test_mask = torch.zeros(num_nodes, dtype=torch.bool)

train_mask[indices[:train_end]] = True
val_mask[indices[train_end:val_end]] = True
test_mask[indices[val_end:]] = True

return train_mask, val_mask, test_mask

data.train_mask, data.val_mask, data.test_mask = split_nodes(data.num_nodes)

# Обучение и сравнение архитектур
def train_model(model, data, epochs=200):
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
data = data.to(device)

optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)

train_losses = []
val_accuracies = []

for epoch in range(epochs):
model.train()
optimizer.zero_grad()
out = model(data.x, data.edge_index)
loss = F.nll_loss(out[data.train_mask], data.y[data.train_mask])
loss.backward()
optimizer.step()
train_losses.append(loss.item())

model.eval()
with torch.no_grad():
out = model(data.x, data.edge_index)
pred = out.argmax(dim=1)
correct = pred[data.val_mask].eq(data.y[data.val_mask]).sum().item()
acc = correct / data.val_mask.sum().item()
val_accuracies.append(acc)

return model, train_losses, val_accuracies

# Обучение трёх архитектур
print("Обучение графовых нейронных сетей...")
gcn_model, gcn_losses, gcn_accs = train_model(
GCN(data.num_features, 64, n_communities), data
)
gat_model, gat_losses, gat_accs = train_model(
GAT(data.num_features, 16, n_communities, heads=4), data
)
sage_model, sage_losses, sage_accs = train_model(
GraphSAGE(data.num_features, 64, n_communities), data
)

# Оценка качества на тестовой выборке
def evaluate_model(model, data):
model.eval()
with torch.no_grad():
out = model(data.x, data.edge_index)
pred = out.argmax(dim=1)
correct = pred[data.test_mask].eq(data.y[data.test_mask]).sum().item()
acc = correct / data.test_mask.sum().item()
return acc

gcn_test_acc = evaluate_model(gcn_model, data)
gat_test_acc = evaluate_model(gat_model, data)
sage_test_acc = evaluate_model(sage_model, data)

print(f"\nТочность на тестовой выборке:")
print(f"GCN: {gcn_test_acc:.2%}")
print(f"GAT: {gat_test_acc:.2%}")
print(f"SAGE: {sage_test_acc:.2%}")

# Визуализация процесса обучения
plt.figure(figsize=(15, 5))

plt.subplot(1, 2, 1)
plt.plot(gcn_losses, label='GCN', alpha=0.8)
plt.plot(gat_losses, label='GAT', alpha=0.8)
plt.plot(sage_losses, label='GraphSAGE', alpha=0.8)
plt.xlabel('Эпоха')
plt.ylabel('Потери обучения')
plt.title('Динамика потерь обучения')
plt.legend()
plt.grid(True, alpha=0.3)

plt.subplot(1, 2, 2)
plt.plot(gcn_accs, label='GCN', alpha=0.8)
plt.plot(gat_accs, label='GAT', alpha=0.8)
plt.plot(sage_accs, label='GraphSAGE', alpha=0.8)
plt.xlabel('Эпоха')
plt.ylabel('Точность на валидации')
plt.title('Динамика точности на валидации')
plt.legend()
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('gnn_training_dynamics.png')
plt.close()

# Визуализация вложений узлов после обучения
from sklearn.manifold import TSNE

def visualize_embeddings(model, data, title):
model.eval()
with torch.no_grad():
out = model(data.x, data.edge_index)
embeddings = out.cpu().numpy()

# Уменьшение размерности для визуализации
tsne = TSNE(n_components=2, random_state=42, perplexity=30)
embeddings_2d = tsne.fit_transform(embeddings)

plt.figure(figsize=(8, 6))
scatter = plt.scatter(
embeddings_2d[:, 0],
embeddings_2d[:, 1],
c=data.y.cpu().numpy(),
cmap='tab10',
alpha=0.8,
s=50
)
plt.colorbar(scatter, label='Сообщество')
plt.title(title)
plt.xlabel('t-SNE компонента 1')
plt.ylabel('t-SNE компонента 2')
plt.grid(True, alpha=0.3)
plt.savefig(f'gnn_embeddings_{title.replace(" ", "_").lower()}.png')
plt.close()

visualize_embeddings(gcn_model, data, 'Вложения узлов (GCN)')
visualize_embeddings(gat_model, data, 'Вложения узлов (GAT)')
visualize_embeddings(sage_model, data, 'Вложения узлов (GraphSAGE)')

Алгоритмы контрастного обучения

Принципы обучения через различение

Контрастное обучение формирует представления данных через задачу различения положительных и отрицательных пар примеров. Положительные пары содержат разные преобразования одного и того же объекта — например, два разных аугментированных изображения одного предмета. Отрицательные пары содержат примеры разных объектов. Модель обучается сближать представления положительных пар и раздвигать представления отрицательных пар в пространстве признаков.

Функция потерь контрастного обучения измеряет косинусное сходство между представлениями примеров. Для каждого запроса вычисляется сходство с положительным примером и со всеми отрицательными примерами в пакете. Логарифмическая функция потерь максимизирует вероятность выбора положительного примера среди всех кандидатов. Большое количество отрицательных примеров критично для качества обучения — современные методы используют механизм очереди для накопления отрицательных примеров за пределами текущего мини-пакета.

Архитектурные решения и методы аугментации

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

Архитектура MoCo (Momentum Contrast) поддерживает динамическую очередь отрицательных примеров фиксированного размера. Энкодер ключей обновляется через экспоненциальное скользящее среднее весов энкодера запросов, обеспечивая стабильность представлений ключей. Очередь позволяет использовать десятки тысяч отрицательных примеров без увеличения потребления памяти, что критично для эффективного контрастного обучения.

Пример реализации на Python

Реализация базового контрастного обучения с использованием PyTorch версии 2.1:

import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision.transforms as transforms
from torchvision import models
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image

# Контрастные аугментации изображений
class ContrastiveTransformations:
def __init__(self, size=224):
self.transform = transforms.Compose([
transforms.RandomResizedCrop(size=size, scale=(0.2, 1.0)),
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomApply([
transforms.ColorJitter(brightness=0.4, contrast=0.4, saturation=0.4, hue=0.1)
], p=0.8),
transforms.RandomGrayscale(p=0.2),
transforms.GaussianBlur(kernel_size=23, sigma=(0.1, 2.0)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

def __call__(self, x):
return self.transform(x), self.transform(x)

# Симметричная архитектура контрастного обучения
class SimCLR(nn.Module):
def __init__(self, base_encoder, hidden_dim=2048, out_dim=128):
super(SimCLR, self).__init__()

# Базовый энкодер (без классификационной головы)
self.encoder = nn.Sequential(*list(base_encoder.children())[:-1])

# Голова проекции
self.projection = nn.Sequential(
nn.Linear(2048, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, out_dim)
)

def forward(self, x):
# Извлечение признаков
h = self.encoder(x)
h = h.view(h.size(0), -1)

# Проекция в пространство сравнения
z = self.projection(h)

# Нормализация представлений
z = F.normalize(z, dim=1)
return z

# Функция потерь контрастного обучения
class NT_XentLoss(nn.Module):
def __init__(self, temperature=0.5):
super(NT_XentLoss, self).__init__()
self.temperature = temperature

def forward(self, z_i, z_j):
batch_size = z_i.size(0)

# Объединение представлений в один тензор
z = torch.cat([z_i, z_j], dim=0)

# Матрица сходства
sim_matrix = torch.mm(z, z.t()) / self.temperature

# Маска для исключения самосравнения
mask = torch.eye(2 * batch_size, dtype=torch.bool).to(z.device)
sim_matrix.masked_fill_(mask, -9e9)

# Индексы положительных пар
pos_indices = torch.arange(batch_size, device=z.device)
pos_indices_j = pos_indices + batch_size
pos_indices_i = pos_indices + batch_size

# Логарифм вероятностей
log_prob = F.log_softmax(sim_matrix, dim=1)

# Потери для пар (i, j) и (j, i)
loss_i = -log_prob[torch.arange(batch_size), pos_indices_j]
loss_j = -log_prob[torch.arange(batch_size) + batch_size, pos_indices_i]

loss = (loss_i + loss_j).mean()
return loss

# Генерация синтетических изображений для демонстрации
def generate_synthetic_images(n_images=100, size=224):
images = []
for _ in range(n_images):
img = np.random.rand(size, size, 3) * 255
# Добавление структурированных паттернов
for _ in range(np.random.randint(3, 7)):
x, y = np.random.randint(0, size, 2)
radius = np.random.randint(10, 40)
color = np.random.rand(3)
for i in range(size):
for j in range(size):
if (i - x)**2 + (j - y)**2 < radius**2:
img[i, j] = color * 255
images.append(Image.fromarray(img.astype(np.uint8)))
return images

# Создание синтетического датасета
synthetic_images = generate_synthetic_images(n_images=200)
transform = ContrastiveTransformations(size=128)

# Инициализация модели и оптимизатора
base_encoder = models.resnet50(weights=None)
model = SimCLR(base_encoder, hidden_dim=512, out_dim=64)
criterion = NT_XentLoss(temperature=0.5)
optimizer = torch.optim.Adam(model.parameters(), lr=3e-4)

# Обучение контрастной модели
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)

print("Обучение контрастной модели...")
train_losses = []

for epoch in range(25):
epoch_loss = 0.0
n_batches = 0

for img in synthetic_images[:100]: # используем подмножество для демонстрации
# Применение двух аугментаций
x_i, x_j = transform(img)
x_i = x_i.unsqueeze(0).to(device)
x_j = x_j.unsqueeze(0).to(device)

# Прямой проход
z_i = model(x_i)
z_j = model(x_j)

# Вычисление потерь
loss = criterion(z_i, z_j)

# Обратное распространение
optimizer.zero_grad()
loss.backward()
optimizer.step()

epoch_loss += loss.item()
n_batches += 1

avg_loss = epoch_loss / n_batches
train_losses.append(avg_loss)
print(f"Эпоха {epoch+1}: потери {avg_loss:.4f}")

# Визуализация динамики обучения
plt.figure(figsize=(10, 6))
plt.plot(train_losses, marker='o', linewidth=2, markersize=4)
plt.xlabel('Эпоха')
plt.ylabel('Контрастные потери')
plt.title('Динамика контрастного обучения')
plt.grid(True, alpha=0.3)
plt.savefig('contrastive_learning_progress.png')
plt.close()

# Оценка качества представлений
model.eval()
embeddings = []
labels = []

with torch.no_grad():
for idx, img in enumerate(synthetic_images[100:150]): # тестовое подмножество
# Извлечение представления без аугментаций
transform_simple = transforms.Compose([
transforms.Resize(128),
transforms.CenterCrop(128),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
x = transform_simple(img).unsqueeze(0).to(device)
z = model(x)
embeddings.append(z.cpu().numpy()[0])
labels.append(idx // 10) # группировка по 10 изображений в класс

embeddings = np.array(embeddings)
labels = np.array(labels)

# Визуализация представлений через t-SNE
from sklearn.manifold import TSNE

tsne = TSNE(n_components=2, random_state=42, perplexity=15)
embeddings_2d = tsne.fit_transform(embeddings)

plt.figure(figsize=(10, 8))
scatter = plt.scatter(
embeddings_2d[:, 0],
embeddings_2d[:, 1],
c=labels,
cmap='tab10',
alpha=0.8,
s=100
)
plt.colorbar(scatter, label='Группа изображений')
plt.title('Пространство представлений после контрастного обучения')
plt.xlabel('t-SNE компонента 1')
plt.ylabel('t-SNE компонента 2')
plt.grid(True, alpha=0.3)
plt.savefig('contrastive_embeddings.png')
plt.close()

print("Контрастное обучение завершено. Представления демонстрируют кластеризацию по группам изображений.")