Найти в Дзене
Один Rust не п...Rust

Валидация json с Rust ML

🎯 https://github.com/nicktretyakov/json_validate Чтение данных → Извлечение признаков → Нормализация → Обучение моделей → Валидация → Обнаружение аномалий struct ComplexData {
id: u64,
name: String,
features: Vec<f64>, // Основные числовые признаки
labels: Option<Vec<String>>, // Метки (могут отсутствовать)
nested: HashMap<String, NestedData>, // Вложенные данные
timestamp: String,
} struct NestedData {
value: f64, // Числовое значение
category: String, // Категориальная информация
sub_features: Vec<f64>, // Дополнительные признаки
} fn read_json_file("train.jsonl") -> Result<Vec<ComplexData>> fn extract_features(data: &[ComplexData]) -> (Vec<Vec<f64>>, Vec<usize>) Что делает: Пример преобразования: Исходные данные:
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] fn normalize_features(features: &[Vec<f64>]) -> Vec<Vec<f64>> Алгоритм: Зачем нужно: Пр
Оглавление

🎯 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>>

Алгоритм:

  1. Для каждого признака вычисляет:
    Среднее значение: mean = sum(values) / count
    Стандартное отклонение: std = sqrt(variance)
  2. Нормализует по формуле: (value - mean) / std
  3. Если 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, // Количество соседей
}

Как работает предсказание:

  1. Для каждой новой точки вычисляет расстояния до всех обучающих точек
  2. Находит k ближайших соседей
  3. Выбирает метку, которая чаще всего встречается среди соседей

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

Пример:
Допустим, у вас есть три города на карте: Москва, Сочи, Санкт-Петербург. Вы хотите добавить новый город — Казань. Вы измеряете расстояние от Казани до Москвы, Сочи и Санкт-Петербурга. Вы выбираете заранее число 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
}

Как обучается:

  1. Перебирает все признаки и возможные пороги
  2. Для каждого варианта вычисляет примесь Джини

Gini = 1 - ∑(pᵢ)² // где pᵢ - доля класса i

  1. Выбирает разделение с минимальной примесью
  2. Определяет метки для левой и правой ветвей по 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>

Процесс:

  1. Обучает KNN и дерево решений на тренировочных данных
  2. Получает предсказания от обеих моделей для валидационных данных
  3. Энсамблирование: точка считается валидной, если:
    Обе модели согласны в предсказании
    Предсказанная метка ≠ 0 (метка 0 считается "невалидной")

Программа обучает две модели машинного обучения: KNN и дерево решений.

  • KNN (k-ближайших соседей) — это метод, который ищет в данных похожие объекты. Например, если вы хотите предсказать, понравится ли человеку фильм, KNN посмотрит, какие фильмы понравились людям с похожими вкусами.
  • Дерево решений — это схема, которая задаёт вопросы типа "да/нет", чтобы прийти к ответу. Например, чтобы решить, идти ли на прогулку, дерево может спросить: "Солнечно? → Да → Идём. Нет → Сидим дома".

Пример:
Допустим, у нас есть данные о фруктах: цвет, вес, форма. Мы обучаем модели на этих данных, чтобы они научились отличать яблоки от апельсинов.

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

  • Валидационные данные — это как контрольная работа: мы смотрим, насколько хорошо модели справились с задачей.

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

Энсамблирование — это когда несколько моделей работают вместе, чтобы принять решение.

  • Точка (объект) — это один пример из данных (например, один фрукт).
  • Валидная точка — это та, которую модели признали правильной по своим критериям.

Условия:

  • Обе модели согласны в предсказании — если KNN и дерево решений сказали одно и то же (например, "это яблоко").
  • Предсказанная метка ≠ 0 — если метка не равна нулю (например, 0 — "невалидный", 1 — "яблоко", 2 — "апельсин").

Пример:
Если обе модели сказали, что фрукт — яблоко (метка 1), то это валидное предсказание. Если одна сказала "яблоко", а другая — "апельсин", или если метка 0 — то точка невалидная.

  1. Обучаем KNN и дерево решений на этих данных.
  2. Проверяем на новых фруктах: если обе модели сказали "яблоко" (метка 1), то предсказание валидное.
  3. Если одна модель сказала "яблоко", а другая — "апельсин", или если метка 0 — то предсказание невалидное.

🚨 Обнаружение аномалий

fn detect_anomalies(features: &[Vec<f64>], threshold_percentile: f64) -> Vec<bool>

Алгоритм:

  1. Для каждой точки находит расстояние до её ближайшего соседа
  2. Сортирует все расстояния
  3. Выбирает порог как 95-й перцентиль (95% точек ближе этого расстояния)
  4. Точки с расстоянием > порога считаются аномалиями

Логика: Аномальные точки обычно находятся далеко от других точек в 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. Вывод результатов
// ...
}