/// Метрики качества моделей машинного обучения. /// /// Содержит метрики для: /// • регрессии (MSE, RMSE, MAE, R2) /// • классификации (Accuracy, Precision, Recall, F1 и др.) /// • вероятностных моделей (LogLoss, ROC, AUC) /// • кластеризации (Silhouette, Calinski–Harabasz, Davies–Bouldin, ARI) /// /// Также включает класс ConfusionMatrix для анализа результатов классификации. unit MetricsABC; interface uses LinearAlgebraML; type Averaging = (avMacro, avMicro, avWeighted); /// Метрики оценки качества моделей машинного обучения /// для задач регрессии, классификации и кластеризации Metrics = static class public /// Среднеквадратичная ошибка (MSE). /// MSE = (1/n) * Σ (yTrue - yPred)^2. /// Чувствительна к большим отклонениям. static function MSE(yTrue, yPred: Vector): real; /// Вычисляет среднеквадратичную ошибку (Root Mean Squared Error, RMSE) /// между истинными и предсказанными значениями. /// RMSE равна квадратному корню из MSE и измеряется /// в тех же единицах, что и целевая переменная. static function RMSE(yTrue, yPred: Vector): real; /// Средняя абсолютная ошибка (MAE). /// MAE = (1/n) * Σ |yTrue - yPred|. /// Менее чувствительна к выбросам, чем MSE. static function MAE(yTrue, yPred: Vector): real; /// Средняя абсолютная процентная ошибка (MAPE). /// MAPE = (1/n) * Σ |(yTrue - yPred) / yTrue|. /// Значения yTrue = 0 игнорируются. /// Удобна для интерпретации в процентах. static function MAPE(yTrue, yPred: Vector): real; /// Медианная абсолютная ошибка (MedianAE). /// MedianAE = median(|yTrue - yPred|). /// Устойчива к выбросам. static function MedianAE(yTrue, yPred: Vector): real; /// Коэффициент детерминации (R²). /// R2 = 1 - SS_res / SS_tot. /// Показывает долю объяснённой дисперсии. /// R2 = 1 — идеальная модель, /// R2 = 0 — не лучше среднего, /// R2 < 0 — хуже константной модели. static function R2(yTrue, yPred: Vector): real; // ---- Вероятностные метрики классификации ---- /// Логарифмическая функция потерь (LogLoss). /// Оценивает качество вероятностных предсказаний. /// Сильно штрафует уверенные, но ошибочные прогнозы. /// Используется для моделей, возвращающих вероятности /// (например, LogisticRegression). static function LogLoss(yTrue, yPred: Vector): real; // ---- ROC / AUC ---- /// Строит точки ROC-кривой по вероятностным предсказаниям. /// Возвращает кортеж из двух векторов: /// FPR (доля ложных положительных) /// и TPR (доля истинно положительных). static function ROC(yTrue, yPred: Vector): (Vector, Vector); /// Площадь под ROC-кривой (AUC). /// Значение от 0 до 1. /// 0.5 — случайная модель, /// 1 — идеальная модель. static function AUC(yTrue, yPred: Vector): real; /// Доля правильных предсказаний. /// Показывает, какая часть объектов классифицирована верно. /// Удобна, когда классы примерно сбалансированы. /// Пример: Accuracy = 0.9 означает 90% правильных ответов. static function Accuracy(yTrue, yPred: Vector): real; /// Доля правильных предсказаний. /// Показывает, какая часть объектов классифицирована верно. /// Удобна, когда классы примерно сбалансированы. /// Пример: Accuracy = 0.9 означает 90% правильных ответов. static function Accuracy(yTrue, yPred: array of integer): real; /// Доля правильных предсказаний. /// Показывает, какая часть объектов классифицирована верно. /// Удобна, когда классы примерно сбалансированы. /// Пример: Accuracy = 0.9 означает 90% правильных ответов. static function Accuracy(yTrue: Vector; yPred: array of integer): real; /// Точность (Precision) для положительного класса (1). /// Precision = TP / (TP + FP) /// Среди всех объектов, которые модель предсказала как положительные (1), /// показывает долю действительно принадлежащих этому классу. /// Используется только для бинарной классификации. /// Важна, когда критичны ложные положительные результаты («ложные тревоги»). static function Precision(yTrue, yPred: Vector): real; /// Точность (Precision) для многоклассовой классификации. /// Вычисляется для каждого класса отдельно как: /// Precision(c) = TP(c) / (TP(c) + FP(c)), /// после чего агрегируется согласно параметру avg: /// avMacro — среднее по всем классам (каждый класс равновесен); /// avMicro — глобальная метрика по суммарным TP и FP; /// avWeighted — среднее с весами, пропорциональными размеру классов. /// Используется для задач с числом классов больше двух. static function Precision(yTrue, yPred: Vector; avg: Averaging): real; /// Полнота (Recall) для положительного класса (1). /// Recall = TP / (TP + FN) /// Среди всех объектов, которые в действительности принадлежат положительному классу (1), /// показывает долю правильно найденных моделью. /// Используется только для бинарной классификации. /// Важна, когда нежелательны пропуски положительных случаев (например, заболеваний). static function Recall(yTrue, yPred: Vector): real; /// Полнота (Recall) для многоклассовой классификации. /// Вычисляется для каждого класса отдельно как: /// Recall(c) = TP(c) / (TP(c) + FN(c)), /// после чего агрегируется согласно параметру avg: /// avMacro — среднее по всем классам (каждый класс равновесен); /// avMicro — глобальная метрика по суммарным TP и FN; /// avWeighted — среднее с весами, пропорциональными размеру классов. /// Используется для задач с числом классов больше двух. static function Recall(yTrue, yPred: Vector; avg: Averaging): real; /// F1-мера для бинарной классификации. /// F1 = 2 * (Precision * Recall) / (Precision + Recall) /// Гармоническое среднее точности (Precision) и полноты (Recall). /// Принимает высокие значения только тогда, когда оба показателя высоки. /// Используется только для бинарной классификации. /// Особенно полезна при несбалансированных классах. static function F1(yTrue, yPred: Vector): real; /// F1-мера для многоклассовой классификации. /// Вычисляется для каждого класса отдельно как: /// F1(c) = 2 * Precision(c) * Recall(c) / (Precision(c) + Recall(c)), /// после чего агрегируется согласно параметру avg: /// avMacro — среднее по всем классам (каждый класс равновесен); /// avMicro — вычисляется по суммарным TP, FP и FN; /// avWeighted — среднее с весами, пропорциональными размеру классов. /// Используется для задач с числом классов больше двух. static function F1(yTrue, yPred: Vector; avg: Averaging): real; /// Специфичность (Specificity, True Negative Rate) для бинарной классификации. /// Specificity = TN / (TN + FP) /// Доля объектов отрицательного класса (0), правильно распознанных моделью. /// Показывает способность модели корректно распознавать отрицательный класс. /// Используется только для бинарной классификации. static function Specificity(yTrue, yPred: Vector): real; /// Сбалансированная точность (Balanced Accuracy) для бинарной классификации. /// Balanced Accuracy = (Recall + Specificity) / 2 /// Среднее арифметическое полноты (Recall, чувствительности) /// и специфичности (Specificity, доли истинно отрицательных). /// Устойчива к дисбалансу классов. /// Используется только для бинарной классификации static function BalancedAccuracy(yTrue, yPred: Vector): real; static function ClassificationReport(yTrue, yPred: Vector): string; /// Вычисляет силуэт (Silhouette) для каждой точки. /// Для каждой строки матрицы X возвращает значение s(i) в диапазоне [-1, 1]. /// Требует как минимум 2 различных кластера. /// Использует евклидово расстояние (квадратный корень не извлекается). /// Сложность O(n²) - для умеренных размеров данных. static function SilhouetteSamples(X: Matrix; labels: Vector): Vector; /// Вычисляет средний силуэт (Silhouette Score). /// Возвращает среднее значение Silhouette по всем точкам. /// Чем ближе к 1 — тем лучше разделены кластеры. /// Значение около 0 означает пересечение кластеров. /// Отрицательное значение указывает на возможную ошибку кластеризации static function SilhouetteScore(X: Matrix; labels: Vector): real; /// Вычисляет индекс Калински–Харабаса (Calinski–Harabasz Index). /// Возвращает отношение межкластерной дисперсии к внутрикластерной дисперсии. /// Чем больше значение — тем лучше разделены кластеры. /// Требует как минимум 2 кластера и n > k. /// Сложность O(n·p). static function CalinskiHarabaszScore(X: Matrix; labels: Vector): real; /// Вычисляет индекс Дэвиса–Булдина (Davies–Bouldin Index). /// Чем меньше значение — тем лучше разделены кластеры. /// Требует минимум 2 кластера. /// Сложность O(n·p + k²·p). static function DaviesBouldinScore(X: Matrix; labels: Vector): real; /// Adjusted Rand Index (ARI). /// Внешняя метрика качества кластеризации. /// Сравнивает предсказанные кластеры с истинными метками. /// /// Основана на анализе всех пар объектов: /// проверяет, совпадает ли решение алгоритма и истинная разметка /// в вопросе "находятся ли две точки в одном кластере". /// /// Корректирует обычный Rand Index на случайные совпадения. /// /// Диапазон значений: /// 1 — идеальное совпадение кластеризации и истинных меток /// 0 — случайное разбиение /// <0 — хуже случайного static function AdjustedRandIndex(yTrue, yPred: Vector): real; end; type /// Матрица ошибок (Confusion Matrix). /// Представляет собой квадратную матрицу размера K × K, /// где K — число классов. /// Элемент [i, j] содержит количество объектов, /// истинный класс которых равен i, /// а предсказанный — j. /// Используется для анализа качества классификатора /// и вычисления метрик (Accuracy, Precision, Recall, F1 и др.). /// Поддерживает как бинарную, так и многоклассовую классификацию. /// Для бинарной классификации дополнительно доступны /// агрегированные показатели: TP, TN, FP, FN. /// /// Ожидается, что yTrue и yPred содержат закодированные метки классов /// (целые значения 0..K-1, допускается хранение в Vector типа real). /// Для строковых меток необходимо предварительно применить EncodeLabels ConfusionMatrix = class private fMatrix: array[,] of integer; fClassCount: integer; fLabels: array of integer; fLabelToIndex: Dictionary; procedure EnsureBinary; function GetTPBinary: integer; function GetFPBinary: integer; function GetFNBinary: integer; function GetTNBinary: integer; procedure EnsureClassIndex(c: integer); public /// Создаёт матрицу ошибок по истинным меткам yTrue и предсказаниям модели yPred. /// Векторы должны иметь одинаковую длину и содержать метки классов, /// представленные целыми числами (0, 1, 2, ...). /// Автоматически определяет множество классов и строит /// матрицу размера K × K, где K — число различных классов. /// Элемент [i, j] содержит количество объектов, /// истинный класс которых равен i, /// а предсказанный — j. /// Для бинарной классификации дополнительно доступны агрегированные /// значения TP, TN, FP и FN constructor Create(yTrue, yPred: Vector); /// Возвращает исходную метку класса по внутреннему индексу c. /// c — индекс класса в диапазоне [0, ClassCount). /// Результат — метка класса (например, 2, 5, 8), соответствующая этому индексу. /// Используется для интерпретации результатов, так как внутренние индексы /// могут не совпадать с исходными значениями меток. function GetLabel(c: integer): integer; /// Возвращает внутренний индекс класса по его метке label. /// label — исходная метка класса (например, 2, 5, 8). /// Результат — индекс класса в диапазоне [0, ClassCount). /// Используется для преобразования внешних меток в внутренние индексы, /// применяемые внутри матрицы ошибок. function GetIndex(labl: integer): integer; /// Массив меток классов в порядке внутренних индексов. /// Labels[c] — исходная метка класса для индекса c. property Labels: array of integer read fLabels; /// Число различных классов в данных. /// Определяется автоматически по объединению меток из yTrue и yPred. /// Используется для построения матрицы размера K × K. property ClassCount: integer read fClassCount; /// Число истинно положительных предсказаний для класса c. /// TP(c) — количество объектов, для которых истинный и предсказанный класс равны c. /// Соответствует элементу матрицы [c, c]. function TPForClass(c: integer): integer; /// Число ложно положительных предсказаний для класса c. /// FP(c) — количество объектов, предсказанных как класс c, /// но в действительности принадлежащих другим классам. /// Вычисляется как сумма по столбцу c минус TP(c). function FPForClass(c: integer): integer; /// Число ложно отрицательных предсказаний для класса c. /// FN(c) — количество объектов, принадлежащих классу c, /// но предсказанных как другие классы. /// Вычисляется как сумма по строке c минус TP(c). function FNForClass(c: integer): integer; /// Число истинно положительных предсказаний (TP) для бинарной классификации. /// Количество объектов, для которых истинный и предсказанный класс равны 1. /// Доступно только при числе классов, равном 2. property TP: integer read GetTPBinary; /// Число ложно положительных предсказаний (FP) для бинарной классификации. /// Количество объектов, предсказанных как класс 1, /// но в действительности принадлежащих классу 0. /// Доступно только при числе классов, равном 2. property FP: integer read GetFPBinary; /// Число ложно отрицательных предсказаний (FN) для бинарной классификации. /// Количество объектов, принадлежащих классу 1, /// но предсказанных как класс 0. /// Доступно только при числе классов, равном 2. property FN: integer read GetFNBinary; /// Число истинно отрицательных предсказаний (TN) для бинарной классификации. /// Количество объектов, для которых истинный и предсказанный класс равны 0. /// Доступно только при числе классов, равном 2. property TN: integer read GetTNBinary; /// Доля правильных предсказаний (Accuracy). /// Accuracy = (число правильных предсказаний) / (общее число объектов). /// Показывает, какая часть объектов классифицирована верно. /// Применима как для бинарной, так и для многоклассовой классификации. /// Удобна, когда классы примерно сбалансированы. function Accuracy: real; /// Точность (Precision) для положительного класса (1). /// Precision = TP / (TP + FP) /// Среди всех объектов, которые модель предсказала как положительные (1), /// показывает долю действительно принадлежащих этому классу. /// Используется только для бинарной классификации. /// Важна, когда критичны ложные положительные результаты («ложные тревоги»). function Precision: real; /// Полнота (Recall) для положительного класса (1). /// Recall = TP / (TP + FN) /// Среди всех объектов, которые в действительности принадлежат классу 1, /// показывает долю правильно найденных моделью. /// Используется только для бинарной классификации. /// Важна, когда нежелательны пропуски положительных случаев (например, заболеваний). function Recall: real; /// F1-мера для бинарной классификации. /// F1 = 2 * (Precision * Recall) / (Precision + Recall) /// Гармоническое среднее точности и полноты. /// Принимает высокие значения только тогда, когда оба показателя высоки. /// Используется только для бинарной классификации. /// Особенно полезна при несбалансированных классах. function F1: real; /// Специфичность (Specificity, True Negative Rate) для бинарной классификации. /// Specificity = TN / (TN + FP) /// Доля объектов отрицательного класса (0), правильно распознанных моделью. /// Показывает способность модели корректно распознавать отрицательный класс. /// Используется только для бинарной классификации. function Specificity: real; /// Сбалансированная точность (Balanced Accuracy) для бинарной классификации. /// Balanced Accuracy = (Recall + Specificity) / 2 /// Среднее арифметическое полноты (Recall, чувствительности) /// и специфичности (Specificity). /// Устойчива к дисбалансу классов. /// Используется только для бинарной классификации. function BalancedAccuracy: real; /// Точность (Precision) для класса c. /// Precision(c) = TP(c) / (TP(c) + FP(c)) /// Среди всех объектов, предсказанных как класс c, /// показывает долю действительно принадлежащих этому классу. /// Если модель ни разу не предсказала класс c (TP(c) + FP(c) = 0), /// значение Precision(c) считается равным 0. /// Применима для многоклассовой классификации. function PrecisionForClass(c: integer): real; /// Полнота (Recall) для класса c. /// Recall(c) = TP(c) / (TP(c) + FN(c)) /// Среди всех объектов, принадлежащих классу c, /// показывает долю правильно найденных моделью. /// Применима для многоклассовой классификации. function RecallForClass(c: integer): real; /// F1-мера для класса c. /// F1(c) = 2 * Precision(c) * Recall(c) / (Precision(c) + Recall(c)) /// Гармоническое среднее точности и полноты для класса c. /// Принимает высокие значения только тогда, когда оба показателя высоки. /// Применима для многоклассовой классификации. function F1ForClass(c: integer): real; end; /// Метрики для оценки качества классификационных моделей. /// Используются, когда целевая переменная представляет собой категорию (класс) /// (например, определение вида цветка, спам/не спам, диагноз пациента). ClassificationMetrics = static class public /// Доля правильных предсказаний (Accuracy). /// Accuracy = (число правильных предсказаний) / (общее число объектов). /// Показывает долю верно классифицированных объектов. /// Применима как для бинарной, так и для многоклассовой классификации. /// Удобна, когда классы примерно сбалансированы. static function Accuracy(yTrue, yPred: Vector): real := Metrics.Accuracy(yTrue, yPred); /// Доля правильных предсказаний (Accuracy). /// Accuracy = (число правильных предсказаний) / (общее число объектов). /// Показывает долю верно классифицированных объектов. /// Применима как для бинарной, так и для многоклассовой классификации. /// Удобна, когда классы примерно сбалансированы. static function Accuracy(yTrue, yPred: array of integer): real := Metrics.Accuracy(yTrue, yPred); /// Доля правильных предсказаний (Accuracy). /// Accuracy = (число правильных предсказаний) / (общее число объектов). /// Показывает долю верно классифицированных объектов. /// Применима как для бинарной, так и для многоклассовой классификации. /// Удобна, когда классы примерно сбалансированы. static function Accuracy(yTrue: Vector; yPred: array of integer): real := Metrics.Accuracy(yTrue, yPred); /// Точность (Precision) для положительного класса (1). /// Precision = TP / (TP + FP) /// Среди всех объектов, предсказанных как положительные (1), /// показывает долю действительно принадлежащих этому классу. /// Используется только для бинарной классификации. /// Важна, когда критичны ложные положительные результаты («ложные тревоги»). static function Precision(yTrue, yPred: Vector): real := Metrics.Precision(yTrue, yPred); /// Полнота (Recall) для положительного класса (1). /// Recall = TP / (TP + FN) /// Среди всех объектов, принадлежащих положительному классу (1), /// показывает долю правильно найденных моделью. /// Используется только для бинарной классификации. /// Важна, когда нежелательны пропуски положительных случаев (например, заболеваний). static function Recall(yTrue, yPred: Vector): real := Metrics.Recall(yTrue, yPred); /// F1-мера для бинарной классификации. /// F1 = 2 * (Precision * Recall) / (Precision + Recall) /// Гармоническое среднее точности и полноты. /// Принимает высокие значения только тогда, когда оба показателя высоки. /// Особенно полезна при несбалансированных классах. /// Используется только для бинарной классификации. static function F1(yTrue, yPred: Vector): real := Metrics.F1(yTrue, yPred); /// Специфичность (Specificity, True Negative Rate) для бинарной классификации. /// Specificity = TN / (TN + FP) /// Доля объектов отрицательного класса (0), правильно распознанных моделью. /// Показывает способность модели корректно распознавать отрицательный класс. /// Используется только для бинарной классификации. static function Specificity(yTrue, yPred: Vector): real := Metrics.Specificity(yTrue, yPred); /// Сбалансированная точность (Balanced Accuracy) для бинарной классификации. /// Balanced Accuracy = (Recall + Specificity) / 2 /// Среднее арифметическое полноты (Recall, чувствительности) /// и специфичности (Specificity). /// Устойчива к дисбалансу классов. /// Используется только для бинарной классификации. static function BalancedAccuracy(yTrue, yPred: Vector): real := Metrics.BalancedAccuracy(yTrue, yPred); /// Логарифмическая функция потерь (LogLoss) для бинарной классификации. /// LogLoss = - (1/n) * Σ [yTrue * log(yProb) + (1 - yTrue) * log(1 - yProb)] /// Оценивает качество вероятностных предсказаний для класса 1. /// Сильно штрафует уверенные, но ошибочные прогнозы. /// Чем меньше значение, тем лучше модель. /// Используется только для бинарной классификации. static function LogLoss(yTrue, yPred: Vector): real := Metrics.LogLoss(yTrue, yPred); /// Построение ROC-кривой для бинарной классификации. /// Возвращает кортеж из двух векторов: /// FPR (False Positive Rate, доля ложноположительных), /// TPR (True Positive Rate, полнота / Recall). /// Вектор yPred должен содержать вероятности принадлежности к классу 1. /// Используется для анализа качества вероятностных предсказаний. /// Используется только для бинарной классификации. static function ROC(yTrue, yPred: Vector): (Vector, Vector) := Metrics.ROC(yTrue, yPred); /// Площадь под ROC-кривой (AUC) для бинарной классификации. /// Значение от 0 до 1: /// 1.0 — идеальная модель, /// 0.5 — случайная модель, /// <0.5 — модель хуже случайной. /// Вектор yPred должен содержать вероятности принадлежности к классу 1. /// Используется только для бинарной классификации. static function AUC(yTrue, yPred: Vector): real := Metrics.AUC(yTrue, yPred); static function ClassificationReport(yTrue, yPred: Vector): string := Metrics.ClassificationReport(yTrue, yPred); end; /// Метрики для оценки качества регрессионных моделей /// Используются когда целевая переменная является непрерывным числом /// (например, предсказание цены дома, температуры, возраста) RegressionMetrics = static class public /// Среднеквадратичная ошибка (MSE) /// MSE = (1/n) * Σ (yTrue - yPred)^2 /// Чувствительна к большим отклонениям и выбросам /// Чем меньше значение, тем лучше модель static function MSE(yTrue, yPred: Vector): real := Metrics.MSE(yTrue, yPred); /// Корень из среднеквадратичной ошибки (RMSE) /// RMSE = √MSE /// Измеряется в тех же единицах, что и целевая переменная /// Чем меньше значение, тем лучше модель static function RMSE(yTrue, yPred: Vector): real := Metrics.RMSE(yTrue, yPred); /// Средняя абсолютная ошибка (MAE) /// MAE = (1/n) * Σ |yTrue - yPred| /// Менее чувствительна к выбросам, чем MSE /// Чем меньше значение, тем лучше модель static function MAE(yTrue, yPred: Vector): real := Metrics.MAE(yTrue, yPred); /// Средняя абсолютная процентная ошибка (MAPE). /// MAPE = (1/n) * Σ |(yTrue - yPred) / yTrue|. /// Значения yTrue = 0 игнорируются. /// Удобна для интерпретации в процентах. static function MAPE(yTrue, yPred: Vector): real := Metrics.MAPE(yTrue, yPred); /// Медианная абсолютная ошибка (MedianAE). /// MedianAE = median(|yTrue - yPred|). /// Устойчива к выбросам. static function MedianAE(yTrue, yPred: Vector): real := Metrics.MedianAE(yTrue, yPred); /// Коэффициент детерминации (R²) /// R² = 1 - SS_res / SS_tot /// Показывает долю дисперсии, объяснённую моделью /// Значения: /// 1.0 — идеальная модель /// 0.0 — модель не лучше среднего /// <0 — модель хуже простого среднего static function R2(yTrue, yPred: Vector): real := Metrics.R2(yTrue, yPred); end; /// Метрики для оценки качества кластеризации /// Используются когда нужно оценить, насколько хорошо объекты сгруппированы в кластеры /// Делятся на: /// - внутренние (без эталонных меток) — оценивают компактность и разделимость /// - внешние (с эталонными метками) — сравнивают с правильным разбиением ClusteringMetrics = static class public // ========== ВНУТРЕННИЕ МЕТРИКИ (без эталонных меток) ========== /// Вычисляет силуэт (Silhouette) для каждой точки /// Для каждой строки матрицы X возвращает значение s(i) в диапазоне [-1, 1] /// s(i) ≈ 1 — точка хорошо сгруппирована в своём кластере /// s(i) ≈ 0 — точка находится на границе между кластерами /// s(i) < 0 — точка, вероятно, попала не в свой кластер /// Требует как минимум 2 различных кластера /// Сложность O(n²) — для умеренных размеров данных static function SilhouetteSamples(X: Matrix; labels: Vector): Vector := Metrics.SilhouetteSamples(x, labels); /// Вычисляет средний силуэт (Silhouette Score) /// Возвращает среднее значение Silhouette по всем точкам /// Чем ближе к 1 — тем лучше разделены кластеры /// Значение около 0 означает пересечение кластеров /// Отрицательное значение указывает на возможную ошибку кластеризации static function SilhouetteScore(X: Matrix; labels: Vector): real := Metrics.SilhouetteScore(x, labels); /// Индекс Калински–Харабаса (Calinski–Harabasz Index) /// Возвращает отношение межкластерной дисперсии к внутрикластерной дисперсии /// Чем больше значение — тем лучше разделены кластеры /// Требует как минимум 2 кластера и n > k /// Сложность O(n·p) static function CalinskiHarabaszScore(X: Matrix; labels: Vector): real := Metrics.CalinskiHarabaszScore(x, labels); /// Индекс Дэвиса–Булдина (Davies–Bouldin Index) /// Чем меньше значение — тем лучше разделены кластеры /// Минимальное значение 0 (идеальное разделение) /// Требует минимум 2 кластера /// Сложность O(n·p + k²·p) static function DaviesBouldinScore(X: Matrix; labels: Vector): real := Metrics.DaviesBouldinScore(x, labels); // ========== ВНЕШНИЕ МЕТРИКИ (с эталонными метками) ========== /// Скорректированный индекс Рэнда (Adjusted Rand Index, ARI) /// Сравнивает предсказанные кластеры с истинными метками /// Основан на анализе всех пар объектов: /// проверяет, совпадает ли решение алгоритма и истинная разметка /// в вопросе "находятся ли две точки в одном кластере" /// Значения: /// 1.0 — идеальное совпадение кластеризации и истинных меток /// 0.0 — случайное разбиение /// <0 — хуже случайного static function AdjustedRandIndex(yTrue, yPred: Vector): real := Metrics.AdjustedRandIndex(yTrue, yPred); end; implementation uses MLExceptions; uses MLUtilsABC; const ER_INVALID_VALUE = 'Некорректное значение {0} на позиции {1}!!Invalid value in {0} at index {1}'; ER_DIV_BY_ZERO = 'Деление на ноль в {0}!!Division by zero in {0}'; ER_INVALID_CLASS_LABEL = 'Некорректная метка класса: {0}!!Invalid class label: {0}'; ER_STRATIFIED_LABELS_INVALID = 'Метки кластеров должны быть целыми числами!!Cluster labels must be integers'; ER_SILHOUETTE_NEEDS_2_CLUSTERS = 'Для вычисления Silhouette требуется минимум 2 кластера!!Silhouette requires at least 2 clusters'; ER_INVALID_VALUE_AT = 'Некорректное значение в {0} на позиции {1}!!Invalid value in {0} at index {1}'; ER_CH_NEEDS_2_CLUSTERS = 'Для Calinski–Harabasz требуется минимум 2 кластера!!Calinski–Harabasz requires at least 2 clusters'; ER_CH_NEEDS_N_GT_K = 'Для Calinski–Harabasz требуется n > k!!Calinski–Harabasz requires n > k'; ER_LABELS_MUST_BE_INTEGERS = 'Метки кластеров должны быть целыми числами!!Cluster labels must be integers'; ER_MAPE_ALL_ZERO_TARGET = 'Все значения целевой переменной равны нулю — MAPE не определена!!All target values are zero — MAPE is undefined'; ER_BINARY_ONLY = 'Метрика доступна только для бинарной классификации!!This metric is available only for binary classification'; ER_INVALID_PROBABILITY = 'Вероятность вне диапазона [0,1]: {0}!!Probability out of range [0,1]: {0}'; ER_ARG_OUT_OF_RANGE = 'Аргумент {0} имеет недопустимое значение: {1}!!Argument {0} is out of range: {1}'; //----------------------------- // Metrics //----------------------------- static function Metrics.MSE(yTrue, yPred: Vector): real; begin if yTrue = nil then ArgumentNullError(ER_ARG_NULL, 'yTrue'); if yPred = nil then ArgumentNullError(ER_ARG_NULL, 'yPred'); var n := yTrue.Length; if n <> yPred.Length then DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length); if n = 0 then ArgumentError(ER_EMPTY_DATA, 'MSE'); var sum := 0.0; for var i := 0 to n - 1 do begin var yt := yTrue[i]; var yp := yPred[i]; if double.IsNaN(yt) or double.IsInfinity(yt) then ArgumentError(ER_INVALID_VALUE, 'yTrue', i); if double.IsNaN(yp) or double.IsInfinity(yp) then ArgumentError(ER_INVALID_VALUE, 'yPred', i); var d := yt - yp; sum += d * d; end; Result := sum / n; end; static function Metrics.RMSE(yTrue, yPred: Vector): real; begin Result := Sqrt(MSE(yTrue, yPred)); end; static function Metrics.MAE(yTrue, yPred: Vector): real; begin if yTrue = nil then ArgumentNullError(ER_ARG_NULL, 'yTrue'); if yPred = nil then ArgumentNullError(ER_ARG_NULL, 'yPred'); var n := yTrue.Length; if n <> yPred.Length then DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length); if n = 0 then ArgumentError(ER_EMPTY_DATA, 'MAE'); var s := 0.0; for var i := 0 to n - 1 do begin var yt := yTrue[i]; var yp := yPred[i]; if double.IsNaN(yt) or double.IsInfinity(yt) then ArgumentError(ER_INVALID_VALUE, 'yTrue', i); if double.IsNaN(yp) or double.IsInfinity(yp) then ArgumentError(ER_INVALID_VALUE, 'yPred', i); s += Abs(yt - yp); end; Result := s / n; end; static function Metrics.MAPE(yTrue, yPred: Vector): real; begin if yTrue = nil then ArgumentNullError(ER_ARG_NULL, 'yTrue'); if yPred = nil then ArgumentNullError(ER_ARG_NULL, 'yPred'); if yTrue.Length <> yPred.Length then DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length); var sum := 0.0; var cnt := 0; for var i := 0 to yTrue.Length - 1 do begin var y := yTrue[i]; var yp := yPred[i]; if y = 0.0 then continue; sum += Abs((y - yp) / y); cnt += 1; end; if cnt = 0 then Error(ER_MAPE_ALL_ZERO_TARGET); Result := sum / cnt; end; static function Metrics.MedianAE(yTrue, yPred: Vector): real; begin if yTrue = nil then ArgumentNullError(ER_ARG_NULL, 'yTrue'); if yPred = nil then ArgumentNullError(ER_ARG_NULL, 'yPred'); if yTrue.Length <> yPred.Length then DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length); var n := yTrue.Length; if n = 0 then ArgumentError(ER_EMPTY_DATA, 'MedianAbsoluteError'); var errors := new real[n]; for var i := 0 to n - 1 do errors[i] := Abs(yTrue[i] - yPred[i]); // сортировка &Array.Sort(errors); if n mod 2 = 1 then Result := errors[n div 2] else Result := (errors[n div 2 - 1] + errors[n div 2]) / 2.0; end; static function Metrics.R2(yTrue, yPred: Vector): real; begin if yTrue = nil then ArgumentNullError(ER_ARG_NULL, 'yTrue'); if yPred = nil then ArgumentNullError(ER_ARG_NULL, 'yPred'); var n := yTrue.Length; if n <> yPred.Length then DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length); if n = 0 then ArgumentError(ER_EMPTY_DATA, 'R2'); var sumY := 0.0; for var i := 0 to n - 1 do begin var yt := yTrue[i]; if double.IsNaN(yt) or double.IsInfinity(yt) then ArgumentError(ER_INVALID_VALUE, 'yTrue', i); sumY += yt; end; var meanY := sumY / n; var ssRes := 0.0; var ssTot := 0.0; for var i := 0 to n - 1 do begin var yt := yTrue[i]; var yp := yPred[i]; if double.IsNaN(yp) or double.IsInfinity(yp) then ArgumentError(ER_INVALID_VALUE, 'yPred', i); var d1 := yt - yp; ssRes += d1 * d1; var d2 := yt - meanY; ssTot += d2 * d2; end; if ssTot = 0.0 then begin Result := 0.0; exit; end; Result := 1 - ssRes / ssTot; end; static function Metrics.LogLoss(yTrue, yPred: Vector): real; begin if yTrue = nil then ArgumentNullError(ER_ARG_NULL, 'yTrue'); if yPred = nil then ArgumentNullError(ER_ARG_NULL, 'yPred'); var n := yTrue.Length; if n <> yPred.Length then DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length); if n = 0 then ArgumentError(ER_EMPTY_DATA, 'LogLoss'); var eps := 1e-15; var s := 0.0; for var i := 0 to n - 1 do begin var yt := yTrue[i]; var p := yPred[i]; if double.IsNaN(yt) or double.IsInfinity(yt) then ArgumentError(ER_INVALID_VALUE, 'yTrue', i); if double.IsNaN(p) or double.IsInfinity(p) then ArgumentError(ER_INVALID_VALUE, 'yPred', i); if (yt <> 0.0) and (yt <> 1.0) then ArgumentError(ER_INVALID_CLASS_LABEL, yt); if (p < 0.0) or (p > 1.0) then ArgumentError(ER_INVALID_PROBABILITY, p); if p < eps then p := eps; if p > 1.0 - eps then p := 1.0 - eps; s += yt * Ln(p) + (1.0 - yt) * Ln(1.0 - p); end; Result := -s / n; end; static function Metrics.ROC(yTrue, yPred: Vector): (Vector, Vector); begin if yTrue = nil then ArgumentNullError(ER_ARG_NULL, 'yTrue'); if yPred = nil then ArgumentNullError(ER_ARG_NULL, 'yPred'); var n := yTrue.Length; if n <> yPred.Length then DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length); if n = 0 then ArgumentError(ER_EMPTY_DATA, 'ROC'); var totalPos := 0; var totalNeg := 0; for var i := 0 to n - 1 do begin var yt := yTrue[i]; var p := yPred[i]; if double.IsNaN(yt) or double.IsInfinity(yt) then ArgumentError(ER_INVALID_VALUE, 'yTrue', i); if double.IsNaN(p) or double.IsInfinity(p) then ArgumentError(ER_INVALID_VALUE, 'yPred', i); if (yt <> 0.0) and (yt <> 1.0) then ArgumentError(ER_INVALID_CLASS_LABEL, yt); if (p < 0.0) or (p > 1.0) then ArgumentError(ER_INVALID_PROBABILITY, p); if yt = 1.0 then totalPos += 1 else totalNeg += 1; end; if (totalPos = 0) or (totalNeg = 0) then ArgumentError(ER_DIV_BY_ZERO, 'ROC'); var idx := (0..n-1) .OrderByDescending(i -> yPred[i]) .ToArray; var tp := 0; var fp := 0; var fprList := new List; var tprList := new List; fprList.Add(0.0); tprList.Add(0.0); for var k := 0 to n - 1 do begin var i := idx[k]; if yTrue[i] = 1.0 then tp += 1 else fp += 1; var tpr := tp / totalPos; var fpr := fp / totalNeg; tprList.Add(tpr); fprList.Add(fpr); end; fprList.Add(1.0); tprList.Add(1.0); Result := (new Vector(fprList.ToArray), new Vector(tprList.ToArray)); end; static function Metrics.AUC(yTrue, yPred: Vector): real; begin if yTrue = nil then ArgumentNullError(ER_ARG_NULL, 'yTrue'); if yPred = nil then ArgumentNullError(ER_ARG_NULL, 'yPred'); var n := yTrue.Length; if n <> yPred.Length then DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length); if n = 0 then ArgumentError(ER_EMPTY_DATA, 'AUC'); var (fpr, tpr) := ROC(yTrue, yPred); if fpr.Length <> tpr.Length then DimensionError(ER_DIM_MISMATCH, fpr.Length, tpr.Length); if fpr.Length < 2 then ArgumentError(ER_EMPTY_DATA, 'AUC'); var area := 0.0; for var i := 1 to fpr.Length - 1 do begin var dx := fpr[i] - fpr[i - 1]; if double.IsNaN(dx) or double.IsInfinity(dx) then ArgumentError(ER_INVALID_VALUE, 'fpr', i); var avgY := (tpr[i] + tpr[i - 1]) / 2; if double.IsNaN(avgY) or double.IsInfinity(avgY) then ArgumentError(ER_INVALID_VALUE, 'tpr', i); area += dx * avgY; end; Result := area; end; static function Metrics.Accuracy(yTrue, yPred: Vector): real; begin if yTrue = nil then ArgumentNullError(ER_ARG_NULL, 'yTrue'); if yPred = nil then ArgumentNullError(ER_ARG_NULL, 'yPred'); var n := yTrue.Length; if n <> yPred.Length then DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length); if n = 0 then ArgumentError(ER_EMPTY_DATA, 'Accuracy'); var correct := 0; for var i := 0 to n - 1 do if Round(yTrue[i]) = Round(yPred[i]) then correct += 1; Result := correct / n; end; static function Metrics.Accuracy(yTrue, yPred: array of integer): real; begin if yTrue = nil then ArgumentNullError(ER_ARG_NULL, 'yTrue'); if yPred = nil then ArgumentNullError(ER_ARG_NULL, 'yPred'); var n := yTrue.Length; if n <> yPred.Length then DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length); if n = 0 then ArgumentError(ER_EMPTY_DATA, 'Accuracy'); var correct := 0; for var i := 0 to n - 1 do if yTrue[i] = yPred[i] then correct += 1; Result := correct / n; end; static function Metrics.Accuracy(yTrue: Vector; yPred: array of integer): real; begin if yTrue = nil then ArgumentNullError(ER_ARG_NULL, 'yTrue'); var data := yTrue.Data; var n := data.Length; if n <> yPred.Length then DimensionError(ER_DIM_MISMATCH, n, yPred.Length); if n = 0 then ArgumentError(ER_EMPTY_DATA, 'Accuracy'); var correct := 0; for var i := 0 to n - 1 do begin var yt := data[i]; // проверка: метки должны быть целыми if Abs(yt - integer(yt)) > 1e-12 then ArgumentError(ER_INVALID_CLASS_LABEL, yt); if integer(yt) = yPred[i] then correct += 1; end; Result := correct / n; end; static function Metrics.Precision(yTrue, yPred: Vector): real; begin Result := ConfusionMatrix.Create(yTrue, yPred).Precision; end; static function Metrics.Recall(yTrue, yPred: Vector): real; begin Result := ConfusionMatrix.Create(yTrue, yPred).Recall; end; static function Metrics.F1(yTrue, yPred: Vector): real; begin Result := ConfusionMatrix.Create(yTrue, yPred).F1; end; static function Metrics.Precision(yTrue, yPred: Vector; avg: Averaging): real; begin var cm := new ConfusionMatrix(yTrue, yPred); var k := cm.ClassCount; // --- Macro if avg = avMacro then begin var sum := 0.0; for var c := 0 to k - 1 do sum += cm.PrecisionForClass(c); Result := sum / k; exit; end; // --- Micro if avg = avMicro then begin var tpSum := 0; var fpSum := 0; for var c := 0 to k - 1 do begin tpSum += cm.TPForClass(c); fpSum += cm.FPForClass(c); end; if tpSum + fpSum = 0 then exit(0.0); Result := tpSum / (tpSum + fpSum); exit; end; // --- Weighted if avg = avWeighted then begin var sum := 0.0; var total := 0; for var c := 0 to k - 1 do begin var support := cm.TPForClass(c) + cm.FNForClass(c); if support = 0 then continue; sum += cm.PrecisionForClass(c) * support; total += support; end; if total = 0 then exit(0.0); Result := sum / total; exit; end; Result := 0.0; end; static function Metrics.Recall(yTrue, yPred: Vector; avg: Averaging): real; begin var cm := new ConfusionMatrix(yTrue, yPred); var k := cm.ClassCount; // --- Macro if avg = avMacro then begin var sum := 0.0; for var c := 0 to k - 1 do sum += cm.RecallForClass(c); Result := sum / k; exit; end; // --- Micro if avg = avMicro then begin var tpSum := 0; var fnSum := 0; for var c := 0 to k - 1 do begin tpSum += cm.TPForClass(c); fnSum += cm.FNForClass(c); end; if tpSum + fnSum = 0 then exit(0.0); Result := tpSum / (tpSum + fnSum); exit; end; // --- Weighted if avg = avWeighted then begin var sum := 0.0; var total := 0; for var c := 0 to k - 1 do begin var support := cm.TPForClass(c) + cm.FNForClass(c); if support = 0 then continue; sum += cm.RecallForClass(c) * support; total += support; end; if total = 0 then exit(0.0); Result := sum / total; exit; end; Result := 0.0; end; static function Metrics.F1(yTrue, yPred: Vector; avg: Averaging): real; begin var cm := new ConfusionMatrix(yTrue, yPred); var k := cm.ClassCount; // --- Macro if avg = avMacro then begin var sum := 0.0; for var c := 0 to k - 1 do sum += cm.F1ForClass(c); Result := sum / k; exit; end; // --- Micro if avg = avMicro then begin var tpSum := 0; var fpSum := 0; var fnSum := 0; for var c := 0 to k - 1 do begin tpSum += cm.TPForClass(c); fpSum += cm.FPForClass(c); fnSum += cm.FNForClass(c); end; if tpSum = 0 then exit(0.0); Result := 2 * tpSum / (2 * tpSum + fpSum + fnSum); exit; end; // --- Weighted if avg = avWeighted then begin var sum := 0.0; var total := 0; for var c := 0 to k - 1 do begin var support := cm.TPForClass(c) + cm.FNForClass(c); if support = 0 then continue; sum += cm.F1ForClass(c) * support; total += support; end; if total = 0 then exit(0.0); Result := sum / total; exit; end; Result := 0.0; end; static function Metrics.Specificity(yTrue, yPred: Vector): real; begin Result := ConfusionMatrix.Create(yTrue, yPred).Specificity; end; static function Metrics.BalancedAccuracy(yTrue, yPred: Vector): real; begin Result := ConfusionMatrix.Create(yTrue, yPred).BalancedAccuracy; end; static function Metrics.SilhouetteSamples(X: Matrix; labels: Vector): Vector; begin if X = nil then ArgumentNullError(ER_ARG_NULL, 'X'); if labels = nil then ArgumentNullError(ER_ARG_NULL, 'labels'); var n := X.RowCount; var p := X.ColCount; if n = 0 then ArgumentError(ER_EMPTY_DATA, 'SilhouetteSamples'); if labels.Length <> n then DimensionError(ER_DIM_MISMATCH, labels.Length, n); // --- собираем размеры кластеров и маппинг label -> 0..c-1 var map := new Dictionary(); var counts := new List(); var clsId := new integer[n]; for var i := 0 to n - 1 do begin var v := labels[i]; var lab := integer(v); if Abs(v - lab) > 1e-12 then ArgumentError(ER_LABELS_MUST_BE_INTEGERS); var id: integer; if map.TryGetValue(lab, id) then begin clsId[i] := id; counts[id] += 1; end else begin id := map.Count; map.Add(lab, id); clsId[i] := id; counts.Add(1); end; end; var cCount := counts.Count; if cCount < 2 then ArgumentError(ER_SILHOUETTE_NEEDS_2_CLUSTERS); // --- список уникальных кластеров, чтобы не обращаться к Dictionary в hot-path var sizes := counts.ToArray; // --- результат Result := new Vector(n); // --- рабочие массивы (переиспользуем, без аллокаций в цикле по i) var sumToCluster := new real[cCount]; for var i := 0 to n - 1 do begin // обнуляем суммы for var c := 0 to cCount - 1 do sumToCluster[c] := 0.0; // суммируем расстояния от i до всех остальных по кластерам for var t := 0 to n - 1 do if t <> i then begin var dist := 0.0; for var j := 0 to p - 1 do begin var a := X[i,j]; if double.IsNaN(a) or double.IsInfinity(a) then ArgumentError(ER_INVALID_VALUE_AT, 'X', i); var b := X[t,j]; if double.IsNaN(b) or double.IsInfinity(b) then ArgumentError(ER_INVALID_VALUE_AT, 'X', t); var d := a - b; dist += d * d; end; sumToCluster[clsId[t]] += Sqrt(dist); end; var my := clsId[i]; // a(i): среднее расстояние до своего кластера (без самой точки) var aMean := 0.0; if sizes[my] > 1 then aMean := sumToCluster[my] / (sizes[my] - 1) else aMean := 0.0; // b(i): минимальное среднее расстояние до другого кластера var bMean := double.MaxValue; for var c := 0 to cCount - 1 do if c <> my then begin if sizes[c] = 0 then continue; var mean := sumToCluster[c] / sizes[c]; if mean < bMean then bMean := mean; end; if double.IsInfinity(bMean) then bMean := 0.0; var denom := Max(aMean, bMean); if denom = 0 then Result[i] := 0.0 else Result[i] := (bMean - aMean) / denom; end; end; static function Metrics.SilhouetteScore(X: Matrix; labels: Vector): real; begin var s := SilhouetteSamples(X, labels); var n := s.Length; var sum := 0.0; for var i := 0 to n - 1 do sum += s[i]; if n = 0 then exit(0.0); Result := sum / n; end; /// Вычисляет индекс Калински–Харабаса (Calinski–Harabasz Index). /// Возвращает отношение межкластерной дисперсии к внутрикластерной дисперсии. /// Чем больше значение — тем лучше разделены кластеры. /// При нулевой внутрикластерной дисперсии возвращает +∞. /// Требует как минимум 2 кластера и n > k. /// Сложность O(n·p). static function Metrics.CalinskiHarabaszScore(X: Matrix; labels: Vector): real; begin if X = nil then ArgumentNullError(ER_ARG_NULL, 'X'); if labels = nil then ArgumentNullError(ER_ARG_NULL, 'labels'); var n := X.RowCount; var p := X.ColCount; if n = 0 then ArgumentError(ER_EMPTY_DATA, 'CalinskiHarabaszScore'); if labels.Length <> n then DimensionError(ER_DIM_MISMATCH, labels.Length, n); // --- label -> 0..k-1 + sizes var map := new Dictionary(); var counts := new List(); var clsId := new integer[n]; for var i := 0 to n - 1 do begin var v := labels[i]; var lab := integer(v); if Abs(v - lab) > 1e-12 then ArgumentError(ER_LABELS_MUST_BE_INTEGERS); var id: integer; if map.TryGetValue(lab, id) then begin clsId[i] := id; counts[id] += 1; end else begin id := map.Count; map.Add(lab, id); clsId[i] := id; counts.Add(1); end; end; var k := counts.Count; if k < 2 then ArgumentError(ER_CH_NEEDS_2_CLUSTERS); if k >= n then ArgumentError(ER_CH_NEEDS_N_GT_K); var sizes := counts.ToArray; // --- глобальное среднее var globalMean := new real[p]; for var i := 0 to n - 1 do for var j := 0 to p - 1 do begin var xij := X[i,j]; if double.IsNaN(xij) or double.IsInfinity(xij) then ArgumentError(ER_INVALID_VALUE_AT, 'X', i); globalMean[j] += xij; end; for var j := 0 to p - 1 do globalMean[j] /= n; // --- центры кластеров var centers := new Matrix(k, p); for var i := 0 to n - 1 do begin var c := clsId[i]; for var j := 0 to p - 1 do centers[c,j] += X[i,j]; end; for var c := 0 to k - 1 do for var j := 0 to p - 1 do centers[c,j] /= sizes[c]; // --- Within (W): сумма квадратов расстояний до центров кластеров var W := 0.0; for var i := 0 to n - 1 do begin var c := clsId[i]; var dist := 0.0; for var j := 0 to p - 1 do begin var d := X[i,j] - centers[c,j]; dist += d * d; end; W += dist; end; // --- Between (B): сумма n_c * ||mu_c - mu||^2 var B := 0.0; for var c := 0 to k - 1 do begin var dist := 0.0; for var j := 0 to p - 1 do begin var d := centers[c,j] - globalMean[j]; dist += d * d; end; B += sizes[c] * dist; end; if W = 0 then exit(double.PositiveInfinity); var ch := (B / (k - 1)) / (W / (n - k)); if double.IsNaN(ch) or double.IsInfinity(ch) then ArgumentError(ER_INVALID_VALUE, 'CalinskiHarabaszScore'); Result := ch; end; static function Metrics.DaviesBouldinScore(X: Matrix; labels: Vector): real; begin if X = nil then ArgumentNullError(ER_ARG_NULL, 'X'); if labels = nil then ArgumentNullError(ER_ARG_NULL, 'labels'); var n := X.RowCount; var p := X.ColCount; if labels.Length <> n then DimensionError(ER_DIM_MISMATCH, labels.Length, n); // --- label -> 0..k-1 var map := new Dictionary(); var counts := new List(); var clsId := new integer[n]; for var i := 0 to n - 1 do begin var v := labels[i]; var lab := integer(v); if Abs(v - lab) > 1e-12 then ArgumentError(ER_LABELS_MUST_BE_INTEGERS); var id: integer; if map.TryGetValue(lab, id) then begin clsId[i] := id; counts[id] += 1; end else begin id := map.Count; map.Add(lab, id); clsId[i] := id; counts.Add(1); end; end; var k := counts.Count; if k < 2 then ArgumentError(ER_CH_NEEDS_2_CLUSTERS); var sizes := counts.ToArray; // --- центры var centers := new Matrix(k, p); for var i := 0 to n - 1 do begin var c := clsId[i]; for var j := 0 to p - 1 do centers[c,j] += X[i,j]; end; for var c := 0 to k - 1 do for var j := 0 to p - 1 do centers[c,j] /= sizes[c]; // --- Si: среднее расстояние точек до центра var S := new real[k]; for var i := 0 to n - 1 do begin var c := clsId[i]; var dist := 0.0; for var j := 0 to p - 1 do begin var d := X[i,j] - centers[c,j]; dist += d * d; end; S[c] += Sqrt(dist); end; for var c := 0 to k - 1 do S[c] /= sizes[c]; // --- считаем DB var total := 0.0; for var i := 0 to k - 1 do begin var maxR := 0.0; for var j := 0 to k - 1 do if i <> j then begin var dist := 0.0; for var d := 0 to p - 1 do begin var delta := centers[i,d] - centers[j,d]; dist += delta * delta; end; dist := Sqrt(dist); if dist = 0 then continue; var Rij := (S[i] + S[j]) / dist; if Rij > maxR then maxR := Rij; end; total += maxR; end; Result := total / k; end; /// Adjusted Rand Index (ARI). /// Внешняя метрика качества кластеризации. /// Сравнивает предсказанные кластеры с истинными метками. /// /// Основана на анализе всех пар объектов: /// проверяет, совпадает ли решение алгоритма и истинная разметка /// в вопросе "находятся ли две точки в одном кластере". /// /// Корректирует обычный Rand Index на случайные совпадения. /// /// Диапазон значений: /// 1 — идеальное совпадение кластеризации и истинных меток /// 0 — случайное разбиение /// <0 — хуже случайного /// Сложность: O(n²). При больших n может быть медленным. /// Для больших выборок рекомендуется использовать альтернативную реализацию /// через таблицу сопряжённости (contingency table) с асимптотикой O(n · k) static function Metrics.AdjustedRandIndex(yTrue, yPred: Vector): real; begin if yTrue = nil then ArgumentNullError(ER_Y_NULL); if yPred = nil then ArgumentNullError(ER_Y_NULL); var n := yTrue.Length; if n <> yPred.Length then DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length); if n < 2 then begin Result := 1.0; exit; end; // --- Vector -> int[] var yTInt := new integer[n]; var yPInt := new integer[n]; for var i := 0 to n - 1 do begin yTInt[i] := Round(yTrue[i]); yPInt[i] := Round(yPred[i]); end; // --- encoding var classesTrue: array of integer; var classesPred: array of integer; var yT := EncodeLabelsInt(yTInt, classesTrue); var yP := EncodeLabelsInt(yPInt, classesPred); var kT := classesTrue.Length; var kP := classesPred.Length; // --- contingency var table := new integer[kT, kP]; for var i := 0 to n - 1 do table[yT[i], yP[i]] += 1; // --- sums var sumNij := 0.0; var sumAi := 0.0; var sumBj := 0.0; // n_ij for var i := 0 to kT - 1 do for var j := 0 to kP - 1 do begin var x := table[i,j]; if x >= 2 then sumNij += x * (x - 1) / 2.0; end; // a_i for var i := 0 to kT - 1 do begin var s := 0; for var j := 0 to kP - 1 do s += table[i,j]; if s >= 2 then sumAi += s * (s - 1) / 2.0; end; // b_j for var j := 0 to kP - 1 do begin var s := 0; for var i := 0 to kT - 1 do s += table[i,j]; if s >= 2 then sumBj += s * (s - 1) / 2.0; end; var total := n * (n - 1) / 2.0; var expected := (sumAi * sumBj) / total; var maxIndex := 0.5 * (sumAi + sumBj); var denom := maxIndex - expected; if denom = 0 then begin Result := 0.0; exit; end; Result := (sumNij - expected) / denom; end; {static function Metrics.AdjustedRandIndex(yTrue, yPred: Vector): real; begin if yTrue.Length <> yPred.Length then DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length); var n := yTrue.Length; if n < 2 then Result := 1 else begin var tp := 0; var tn := 0; var fp := 0; var fn := 0; for var i := 0 to n-2 do for var j := i+1 to n-1 do begin var sameTrue := yTrue[i] = yTrue[j]; var samePred := yPred[i] = yPred[j]; if sameTrue and samePred then tp += 1 else if (not sameTrue) and (not samePred) then tn += 1 else if (not sameTrue) and samePred then fp += 1 else fn += 1; end; var total := tp + tn + fp + fn; var ri := (tp + tn) / total; var a := tp + fp; var b := tp + fn; var c := fn + tn; var d := fp + tn; var expected := ((a*b) + (c*d)) / (total*total); if 1 - expected = 0 then Result := 0 else Result := (ri - expected) / (1 - expected); end; end;} function R(s: string; w: integer): string; begin Result := s.PadLeft(w); end; static function Metrics.ClassificationReport(yTrue, yPred: Vector): string; begin var cm := new ConfusionMatrix(yTrue, yPred); var rows := new List<(string, real, real, real, integer)>; var total := yTrue.Length; // --- собираем строки классов for var c := 0 to cm.ClassCount - 1 do begin var cls := cm.GetLabel(c).ToString; var p := cm.PrecisionForClass(c); var r := cm.RecallForClass(c); var f := cm.F1ForClass(c); var support := cm.TPForClass(c) + cm.FNForClass(c); rows.Add((cls, p, r, f, support)); end; // --- averages var pMacro := Metrics.Precision(yTrue, yPred, avMacro); var rMacro := Metrics.Recall(yTrue, yPred, avMacro); var fMacro := Metrics.F1(yTrue, yPred, avMacro); var pW := Metrics.Precision(yTrue, yPred, avWeighted); var rW := Metrics.Recall(yTrue, yPred, avWeighted); var fW := Metrics.F1(yTrue, yPred, avWeighted); // --- helper форматирования var F: real -> string := x -> x.ToString('0.00'); // --- считаем ширины колонок var wClass := 'class'.Length; var wP := 'precision'.Length; var wR := 'recall'.Length; var wF := 'f1-score'.Length; var wS := 'support'.Length; foreach var row in rows do begin wClass := Max(wClass, row.Item1.Length); wP := Max(wP, F(row.Item2).Length); wR := Max(wR, F(row.Item3).Length); wF := Max(wF, F(row.Item4).Length); wS := Max(wS, row.Item5.ToString.Length); end; wClass := Max(wClass, 'macro avg'.Length); wClass := Max(wClass, 'weighted avg'.Length); wP := Max(wP, F(pMacro).Length); wR := Max(wR, F(rMacro).Length); wF := Max(wF, F(fMacro).Length); wP := Max(wP, F(pW).Length); wR := Max(wR, F(rW).Length); wF := Max(wF, F(fW).Length); wS := Max(wS, total.ToString.Length); // --- сборка строки var sb := new System.Text.StringBuilder; // header sb.AppendLine( R('class', wClass) + ' ' + R('precision', wP) + ' ' + R('recall', wR) + ' ' + R('f1-score', wF) + ' ' + R('support', wS) ); var totalWidth := wClass + 2 + wP + 2 + wR + 2 + wF + 2 + wS; sb.AppendLine(new string('-', totalWidth)); // rows foreach var row in rows do sb.AppendLine( R(row.Item1, wClass) + ' ' + R(F(row.Item2), wP) + ' ' + R(F(row.Item3), wR) + ' ' + R(F(row.Item4), wF) + ' ' + R(row.Item5.ToString, wS) ); sb.AppendLine; // macro avg sb.AppendLine( R('macro avg', wClass) + ' ' + R(F(pMacro), wP) + ' ' + R(F(rMacro), wR) + ' ' + R(F(fMacro), wF) + ' ' + R(total.ToString, wS) ); sb.AppendLine( R('weighted avg', wClass) + ' ' + R(F(pW), wP) + ' ' + R(F(rW), wR) + ' ' + R(F(fW), wF) + ' ' + R(total.ToString, wS) ); Result := sb.ToString; end; //----------------------------- // ConfusionMatrix //----------------------------- constructor ConfusionMatrix.Create(yTrue, yPred: Vector); begin if yTrue = nil then ArgumentNullError(ER_ARG_NULL, 'yTrue'); if yPred = nil then ArgumentNullError(ER_ARG_NULL, 'yPred'); var n := yTrue.Length; if n <> yPred.Length then DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length); if n = 0 then ArgumentError(ER_EMPTY_DATA, 'ConfusionMatrix'); var yArr := yTrue.Data; var pArr := yPred.Data; // --- 1. собираем уникальные метки var setLabels := new HashSet; for var i := 0 to n - 1 do begin var yt := yArr[i]; var yp := pArr[i]; if double.IsNaN(yt) or double.IsInfinity(yt) then ArgumentError(ER_INVALID_VALUE, 'yTrue', i); if double.IsNaN(yp) or double.IsInfinity(yp) then ArgumentError(ER_INVALID_VALUE, 'yPred', i); var ytInt := integer(yt); var ypInt := integer(yp); if Abs(yt - ytInt) > 1e-12 then ArgumentError(ER_INVALID_CLASS_LABEL, yt); if Abs(yp - ypInt) > 1e-12 then ArgumentError(ER_INVALID_CLASS_LABEL, yp); setLabels.Add(ytInt); setLabels.Add(ypInt); end; // --- 2. сортируем метки var labels := setLabels.ToArray; &Array.Sort(labels); fLabels := labels; fClassCount := labels.Length; // --- 3. строим отображение label → index fLabelToIndex := new Dictionary(fClassCount); for var i := 0 to fClassCount - 1 do fLabelToIndex.Add(labels[i], i); // --- 4. создаём матрицу fMatrix := new integer[fClassCount, fClassCount]; // --- 5. заполняем for var i := 0 to n - 1 do begin var yt := integer(yArr[i]); var yp := integer(pArr[i]); var row := fLabelToIndex[yt]; var col := fLabelToIndex[yp]; fMatrix[row, col] += 1; end; end; function ConfusionMatrix.GetLabel(c: integer): integer; begin if (c < 0) or (c >= fClassCount) then ArgumentOutOfRangeError(ER_ARG_OUT_OF_RANGE, 'c', c); Result := fLabels[c]; end; function ConfusionMatrix.GetIndex(labl: integer): integer; begin if not fLabelToIndex.ContainsKey(labl) then ArgumentError(ER_INVALID_CLASS_LABEL, labl); Result := fLabelToIndex[labl]; end; function ConfusionMatrix.TPForClass(c: integer): integer; begin EnsureClassIndex(c); Result := fMatrix[c,c]; end; function ConfusionMatrix.FPForClass(c: integer): integer; begin EnsureClassIndex(c); var s := 0; for var i := 0 to fClassCount - 1 do s += fMatrix[i,c]; Result := s - fMatrix[c,c]; end; function ConfusionMatrix.FNForClass(c: integer): integer; begin EnsureClassIndex(c); var s := 0; for var j := 0 to fClassCount - 1 do s += fMatrix[c,j]; Result := s - fMatrix[c,c]; end; procedure ConfusionMatrix.EnsureBinary; begin if fClassCount <> 2 then Error(ER_BINARY_ONLY); end; function ConfusionMatrix.GetTPBinary: integer; begin EnsureBinary; Result := fMatrix[1,1]; end; function ConfusionMatrix.GetFPBinary: integer; begin EnsureBinary; Result := fMatrix[0,1]; end; function ConfusionMatrix.GetFNBinary: integer; begin EnsureBinary; Result := fMatrix[1,0]; end; function ConfusionMatrix.GetTNBinary: integer; begin EnsureBinary; Result := fMatrix[0,0]; end; procedure ConfusionMatrix.EnsureClassIndex(c: integer); begin if (c < 0) or (c >= fClassCount) then ArgumentOutOfRangeError(ER_ARG_OUT_OF_RANGE, 'c', c); end; function ConfusionMatrix.Accuracy: real; begin var total := 0; var correct := 0; for var i := 0 to fClassCount - 1 do for var j := 0 to fClassCount - 1 do begin total += fMatrix[i,j]; if i = j then correct += fMatrix[i,j]; end; if total = 0 then exit(0.0); Result := correct / total; end; function ConfusionMatrix.Precision: real; begin EnsureBinary; var tp := TP; var fp := FP; if tp + fp = 0 then exit(0.0); Result := tp / (tp + fp); end; function ConfusionMatrix.Recall: real; begin EnsureBinary; var tp := TP; var fn := FN; if tp + fn = 0 then exit(0.0); Result := tp / (tp + fn); end; function ConfusionMatrix.F1: real; begin EnsureBinary; var p := Precision; var r := Recall; if p + r = 0 then exit(0.0); Result := 2 * p * r / (p + r); end; function ConfusionMatrix.Specificity: real; begin EnsureBinary; var tn := TN; var fp := FP; if tn + fp = 0 then exit(0.0); Result := tn / (tn + fp); end; function ConfusionMatrix.BalancedAccuracy: real; begin EnsureBinary; Result := (Recall + Specificity) / 2; end; function ConfusionMatrix.PrecisionForClass(c: integer): real; begin var tp := TPForClass(c); var fp := FPForClass(c); if tp + fp = 0 then exit(0.0); Result := tp / (tp + fp); end; function ConfusionMatrix.RecallForClass(c: integer): real; begin var tp := TPForClass(c); var fn := FNForClass(c); if tp + fn = 0 then exit(0.0); Result := tp / (tp + fn); end; function ConfusionMatrix.F1ForClass(c: integer): real; begin var p := PrecisionForClass(c); var r := RecallForClass(c); if p + r = 0 then exit(0.0); Result := 2 * p * r / (p + r); end; end.