🎯 https://github.com/nicktretyakov/json_validate
💡 Для чего нужна данная статья?
- Достичь нужного качества данных в json
- Классифицировать и структурировать данные
🏗️ Общая архитектура
Чтение данных → Извлечение признаков → Нормализация → Обучение моделей → Валидация → Обнаружение аномалий
📊 Структуры данных
ComplexData
struct ComplexData {
id: u64,
name: String,
features: Vec<f64>, // Основные числовые признаки
labels: Option<Vec<String>>, // Метки (могут отсутствовать)
nested: HashMap<String, NestedData>, // Вложенные данные
timestamp: String,
}
NestedData
struct NestedData {
value: f64, // Числовое значение
category: String, // Категориальная информация
sub_features: Vec<f64>, // Дополнительные признаки
}
🔄 Процесс обработки данных
1. Чтение JSON файлов
fn read_json_file("train.jsonl") -> Result<Vec<ComplexData>>
- Читает файлы построчно (JSON Lines формат)
- Каждая строка парсится в структуру ComplexData
- Возвращает вектор объектов
2. Извлечение признаков
fn extract_features(data: &[ComplexData]) -> (Vec<Vec<f64>>, Vec<usize>)
Что делает:
- Объединяет все числовые данные в один вектор признаков:
Основные features
sub_features из вложенных данных
value из вложенных данных - Создает метки: хеширует текстовые labels в числа 0-9
Пример преобразования:
Исходные данные:
features: [1.0, 2.0], nested.value: 3.0, nested.sub_features: [0.1, 0.2]
Результат:
[1.0, 2.0, 3.0, 0.1, 0.2]
3. Нормализация
fn normalize_features(features: &[Vec<f64>]) -> Vec<Vec<f64>>
Алгоритм:
- Для каждого признака вычисляет:
Среднее значение: mean = sum(values) / count
Стандартное отклонение: std = sqrt(variance) - Нормализует по формуле: (value - mean) / std
- Если std = 0 (все значения одинаковы), использует std = 1.0
Зачем нужно: Приводит все признаки к одинаковому масштабу, что улучшает работу алгоритмов ML. Возьмём данные: 160, 170, 180, 170, 160.
Среднее: 168
Отклонения от среднего:
(160-168)² = 64
(170-168)² = 4
(180-168)² = 144
(170-168)² = 4
(160-168)² = 64
Сумма квадратов: 64 + 4 + 144 + 4 + 64 = 280
Дispерсия: 280 / 5 = 56
Стандартное отклонение: √56 ≈ 7,48 см
Нормализация — это преобразование каждого значения так, чтобы среднее стало 0, а стандартное отклонение — 1. Это помогает сравнивать данные с разными масштабами.
Пример:
Возьмём первое значение: 160
(160 - 168) / 7,48 ≈ -8 / 7,48 ≈ -1,07
Это значит, что 160 см на 1,07 стандартных отклонения ниже среднего. Если все значения одинаковые, то стандартное отклонение = 0. Делить на 0 нельзя, поэтому используют 1.0, чтобы не было ошибки.
Пример:
Допустим, у всех рост 170 см.
Среднее: 170
Стандартное отклонение: 0
Чтобы нормализовать, используют std = 1.0:
(170 - 170) / 1.0 = 0
🤖 Машинное обучение
Модель 1: K-Ближайших Соседей (KNN)
struct SimpleKNN {
training_features: Vec<Vec<f64>>, // Обучающие данные
training_labels: Vec<usize>, // Метки обучающих данных
k: usize, // Количество соседей
}
Как работает предсказание:
- Для каждой новой точки вычисляет расстояния до всех обучающих точек
- Находит k ближайших соседей
- Выбирает метку, которая чаще всего встречается среди соседей
Представьте, что у вас есть карта с отмеченными городами (это «обучающие точки»). У вас появляется новый город, и вы хотите понять, к какой группе он относится (например, к северным или южным городам). Для этого вы измеряете расстояние от нового города до всех уже известных.
Пример:
Допустим, у вас есть три города на карте: Москва, Сочи, Санкт-Петербург. Вы хотите добавить новый город — Казань. Вы измеряете расстояние от Казани до Москвы, Сочи и Санкт-Петербурга. Вы выбираете заранее число k (например, 3). Затем смотрите, какие k городов из известных находятся ближе всего к новому городу.
Пример:
Если k=2, то для Казани ближайшими могут оказаться Москва и Санкт-Петербург. Каждый известный город имеет «метку» — например, «северный» или «южный». Вы смотрите, какая метка чаще встречается среди ближайших соседей, и присваиваете её новому городу.
Пример:
Если Москва и Санкт-Петербург — «северные», а Сочи — «южный», то Казань скорее всего тоже будет «северной», потому что среди двух ближайших соседей оба — северные.
Расстояние Евклида:
fn euclidean_distance(a: &[f64], b: &[f64]) -> f64 {
sqrt(∑(aᵢ - bᵢ)²)
}
Это способ измерить расстояние между двумя точками по прямой. Если у вас две точки с координатами (x1, y1) и (x2, y2), то расстояние между ними вычисляется по формуле:
Формула:
√((x1 - x2)² + (y1 - y2)²)
Пример:
Если Москва — (0, 0), а Казань — (3, 4), то расстояние между ними:
√((3-0)² + (4-0)²) = √(9 + 16) = √25 = 5
Модель 2: Дерево решений
struct SimpleDecisionTree {
threshold: f64, // Пороговое значение
feature_index: usize, // Индекс признака для разделения
left_label: usize, // Метка для значений ≤ threshold
right_label: usize, // Метка для значений > threshold
}
Как обучается:
- Перебирает все признаки и возможные пороги
- Для каждого варианта вычисляет примесь Джини
Gini = 1 - ∑(pᵢ)² // где pᵢ - доля класса i
- Выбирает разделение с минимальной примесью
- Определяет метки для левой и правой ветвей по majority vote
Алгоритм (например, дерево решений) смотрит на все характеристики (признаки) объектов и пытается найти лучший способ разделить их на группы.
Пример:
Допустим, у нас есть данные о людях: их возраст и доход. Алгоритм будет проверять, как лучше разделить людей по возрасту (например, до 30 лет и после 30 лет) или по доходу (например, до 50 000 и после 50 000), чтобы группы получились как можно более однородными.
Примесь Джини (Gini impurity) — это мера того, насколько разнородна группа. Чем меньше примесь, тем однороднее группа.
Формула:
Gini = 1 - ∑(pᵢ)²
где pᵢ — доля объектов класса i в группе.
Пример:
Допустим, в группе из 10 человек 8 здоровых и 2 больных.
Доля здоровых (p₁) = 8/10 = 0.8
Доля больных (p₂) = 2/10 = 0.2
Gini = 1 - (0.8² + 0.2²) = 1 - (0.64 + 0.04) = 0.32
Если бы в группе были только здоровые (10/10), то Gini = 1 - 1² = 0 — идеальная однородность.
Выбирает разделение с минимальной примесью
Алгоритм выбирает такой признак и порог, при котором группы получаются максимально однородными (то есть примесь Джини минимальна).
Пример:
Если при делении по возрасту Gini = 0.3, а при делении по доходу Gini = 0.1, то алгоритм выберет деление по доходу.
Определяет метки для левой и правой ветвей по majority vote
После разделения алгоритм присваивает каждой группе (ветви) метку того класса, который в ней преобладает.
Пример:
Если в левой ветви 7 здоровых и 3 больных, то метка будет «здоровый».
Если в правой ветви 2 здоровых и 8 больных, то метка будет «больной».
Представьте, что вы сортируете яблоки и апельсины по весу и цвету. Алгоритм попробует разные варианты (например, «вес больше 150 г» или «цвет красный»), выберет тот, при котором в каждой коробке будет как можно больше одинаковых фруктов, и подпишет коробки по большинству.
🎯 Валидация данных
fn validate_data(train_features, train_labels, val_features) -> Vec<bool>
Процесс:
- Обучает KNN и дерево решений на тренировочных данных
- Получает предсказания от обеих моделей для валидационных данных
- Энсамблирование: точка считается валидной, если:
Обе модели согласны в предсказании
Предсказанная метка ≠ 0 (метка 0 считается "невалидной")
Программа обучает две модели машинного обучения: KNN и дерево решений.
- KNN (k-ближайших соседей) — это метод, который ищет в данных похожие объекты. Например, если вы хотите предсказать, понравится ли человеку фильм, KNN посмотрит, какие фильмы понравились людям с похожими вкусами.
- Дерево решений — это схема, которая задаёт вопросы типа "да/нет", чтобы прийти к ответу. Например, чтобы решить, идти ли на прогулку, дерево может спросить: "Солнечно? → Да → Идём. Нет → Сидим дома".
Пример:
Допустим, у нас есть данные о фруктах: цвет, вес, форма. Мы обучаем модели на этих данных, чтобы они научились отличать яблоки от апельсинов.
После обучения модели проверяются на новых данных (валидационных), которые не использовались при обучении.
- Валидационные данные — это как контрольная работа: мы смотрим, насколько хорошо модели справились с задачей.
Пример:
Если мы обучили модели отличать яблоки от апельсинов, то на валидационных данных проверяем, правильно ли они определяют новые фрукты.
Энсамблирование — это когда несколько моделей работают вместе, чтобы принять решение.
- Точка (объект) — это один пример из данных (например, один фрукт).
- Валидная точка — это та, которую модели признали правильной по своим критериям.
Условия:
- Обе модели согласны в предсказании — если KNN и дерево решений сказали одно и то же (например, "это яблоко").
- Предсказанная метка ≠ 0 — если метка не равна нулю (например, 0 — "невалидный", 1 — "яблоко", 2 — "апельсин").
Пример:
Если обе модели сказали, что фрукт — яблоко (метка 1), то это валидное предсказание. Если одна сказала "яблоко", а другая — "апельсин", или если метка 0 — то точка невалидная.
- Обучаем KNN и дерево решений на этих данных.
- Проверяем на новых фруктах: если обе модели сказали "яблоко" (метка 1), то предсказание валидное.
- Если одна модель сказала "яблоко", а другая — "апельсин", или если метка 0 — то предсказание невалидное.
🚨 Обнаружение аномалий
fn detect_anomalies(features: &[Vec<f64>], threshold_percentile: f64) -> Vec<bool>
Алгоритм:
- Для каждой точки находит расстояние до её ближайшего соседа
- Сортирует все расстояния
- Выбирает порог как 95-й перцентиль (95% точек ближе этого расстояния)
- Точки с расстоянием > порога считаются аномалиями
Логика: Аномальные точки обычно находятся далеко от других точек в feature space.
Каждый объект (например, пользователь, транзакция, изображение) можно описать набором характеристик — их называют признаками (features). Например, если у нас есть данные о людях, то признаками могут быть: рост, вес, возраст. Каждый человек в этом случае — точка в многомерном пространстве (feature space).
Пример:
Представьте, что у вас есть таблица с данными о фруктах:
- Яблоко: вес = 150 г, цвет = красный, сахар = 10 г
- Банан: вес = 200 г, цвет = жёлтый, сахар = 15 г
- Апельсин: вес = 130 г, цвет = оранжевый, сахар = 8 г
Каждый фрукт — точка в трёхмерном пространстве (вес, цвет, сахар).
Для каждой точки считают, насколько она далеко находится от самой близкой к ней другой точки. Это называется расстояние до ближайшего соседа.
Пример:
Если у вас на плоскости три точки: A, B, C. Расстояние от A до B — 2 см, от A до C — 5 см. Тогда ближайший сосед для A — это B, а расстояние — 2 см.
Все посчитанные расстояния выстраивают по порядку — от самого маленького до самого большого.
Пример:
Допустим, у нас расстояния: 1, 3, 2, 5, 4. После сортировки: 1, 2, 3, 4, 5.
Перцентиль — это значение, ниже которого находится определённый процент данных. 95-й перцентиль означает, что 95% всех расстояний меньше этого значения, а 5% — больше.
Пример:
Если у нас 100 расстояний, отсортированных по возрастанию, то 95-е по счёту значение и будет 95-м перцентилем. Все расстояния больше этого значения считаются редкими, необычными.
Точки, расстояние до ближайшего соседа у которых больше порога (95-й перцентиль), считаются аномалиями. Это значит, что они сильно отличаются от остальных.
Пример:
Представьте, что у вас данные о доходах людей. Большинство зарабатывает от 30 до 100 тысяч рублей, а один человек — 1 миллион. Этот человек — аномалия, потому что его доход сильно выбивается из общей картины.
Аномальные точки часто указывают на что-то необычное: мошенничество, ошибки в данных, редкие события. Например, если в данных о банковских транзакциях одна операция сильно отличается от остальных, это может быть попытка кражи.
📈 Основной пайплайн
fn main() -> Result<(), ValidationError> {
// 1. Чтение данных
let train_data = read_json_file("train.jsonl")?;
let validation_data = read_json_file("validate.jsonl")?;
// 2. Извлечение признаков
let (train_features, train_labels) = extract_features(&train_data);
let (val_features, _) = extract_features(&validation_data);
// 3. Нормализация
let normalized_train_features = normalize_features(&train_features);
let normalized_val_features = normalize_features(&val_features);
// 4. Валидация
let validations = validate_data(&normalized_train_features, &train_labels, &normalized_val_features);
// 5. Обнаружение аномалий
let anomalies = detect_anomalies(&normalized_val_features, 0.95);
// 6. Вывод результатов
// ...
}