11 KiB
Последняя Надежда Эпсилон-9: Протокол «К-Резонанс»
Ограничение по времени: 3.0 с Ограничение по памяти: 512 МБ
Год 2142. Межзвездная колония на планете Эпсилон-9 столкнулась с невидимой смертью. Внешне безобидная флора планеты скрывает фатальную угрозу: питательный, спасающий от голода «Звездный Корень» и парализующая нервную систему «Теневая Лоза» визуально абсолютно неразличимы. Их выдают только биоэлектрические поля, которые сканируют наши харвестеры.
До вчерашнего дня наш Центральный ИИ «Тензор-Оракул» успешно классифицировал растения. Но аномальная вспышка на местной звезде сожгла серверные стойки. Библиотеки машинного обучения расплавились в цифровом небытии. Остался только аварийный терминал жизнеобеспечения. Он принимает исключительно базовый, первобытный диалект языка Python. Использовать оператор import категорически запрещено — модулей просто не существует. Голод неминуем. Склады забиты смертельно опасным неклассифицированным урожаем.
Командор колонии Лира врывается в ваш инженерный отсек. В её руках дымящийся кристалл памяти.
— Инженер! — кричит она, смахивая пепел с терминала. — Я вытащила из мертвых архивов чертежи древнего алгоритма K-Ближайших Соседей. Мы должны воссоздать его с нуля, иначе завтра колония вымрет. И нам нужна строгая архитектура, чтобы система не рухнула. Будем писать объектно-ориентированно. Слушай меня внимательно, я зачитаю спецификацию протокола прямо с поврежденных секторов, а ты пиши код!
Лира подключает кристалл к вашему монитору, и аварийное освещение окрашивает отсек в красный цвет.
— Разделим логику выживания на модули, — диктует Командор, глядя на бегущие строки. — Для начала создай класс DataProcessor, он будет отвечать за очистку. Харвестеры присылают нам сплошной текстовый мусор. В этом классе тебе нужно написать первый метод parse_sensor_data(self, record). Он должен принимать сырую строку, где числа и класс разделены запятыми, и возвращать кортеж. Первым элементом в нем пойдет список чисел с плавающей точкой — это наши сырые признаки. Вторым — строка класса растения.
Она нервно стучит по экрану:
— Данные с сенсоров скачут! Чтобы алгоритм не сошел с ума от разницы в масштабах измерений, нам нужна калибровка. В этот же класс добавь метод calculate_statistics(self, dataset). Прогони через него размеченные данные и вычисли для каждого признака среднее арифметическое и стандартное отклонение. Верни это как список словарей с ключами "mean" и "std". И помни, корень придется извлекать возведением в степень 0.5, у нас нет математических модулей!
— Как только получим статистическую базу, — продолжает Лира, — пиши там же метод normalize_features(self, features, stats). Каждый сырой признак растения нужно очистить от шума, вычтя среднее и разделив на стандартное отклонение. Этот метод должен брать список признаков и словарь статистики, а возвращать новый список идеальных, нормализованных чисел.
Свет в отсеке мигает. Система охлаждения воет на пределе возможностей.
— Теперь самое главное — архитектура ядра! Создай класс KNNClassifier. При инициализации через __init__(self, k) он должен принимать и сохранять внутри себя количество соседей k. Затем заложи в него геометрию выживания — метод euclidean_distance(self, vector_a, vector_b). Он должен брать два нормализованных списка чисел и вычислять дистанцию между ними в многомерном пространстве. Просто верни одно число с плавающей точкой.
Лира выводит на экран голограмму базы данных:
— Систему нужно обучить. Напиши метод fit(self, train_features, train_labels), который просто сохранит нормализованную обучающую выборку и её классы внутри объекта классификатора. А чтобы найти сородичей, добавь метод find_k_nearest(self, test_point). Он должен прочесать сохраненную базу и найти k самых близких соседей для неизвестного растения test_point. От тебя требуется вернуть только список их классов-строк.
— Но соседи могут расходиться во мнениях, — Лира хмурится. — Нам нужна логика большинства. Метод majority_vote(self, neighbors) в этом же классе примет этот список и вернет тот класс, который встречается чаще всего. Если будет ничья — система должна выжить, просто возвращай любой из лидирующих классов. Это и будет наш приговор.
Она тяжело вздыхает, опираясь на стол:
— Оберни этот поиск и голосование в единый боевой интерфейс predict_class(self, test_point). Ему мы будем скармливать одно неизвестное нормализованное растение, чтобы он сам вызвал поиск соседей, провел голосование и сразу выдал итоговый класс-строку.
— Но прежде чем отправить харвестеры в поля и кормить людей, — голос Лиры дрожит, — мы обязаны проверить алгоритм на архивах. Создай отдельный класс AccuracyCalculator. В нем будет всего один метод calculate(self, predictions, true_labels). Сравни списки наших предсказаний с истинными классами на тестовом полигоне и верни долю правильных ответов от 0.0 до 1.0. Цена каждой ошибки — чья-то жизнь.
Лира кладет руку вам на плечо. В её глазах отражается код аварийного терминала:
— И наконец, Инженер... Великий Синтез. Вне классов напиши финальную функцию run_protocol(train_raw_lines, test_raw_lines, k). Она должна объединить весь пайплайн воедино. Создай экземпляры DataProcessor, KNNClassifier и AccuracyCalculator. Сначала распарси обе выборки. Затем вычисли статистику строго и только по обучающей базе, чтобы не было утечки данных из будущего. Потом нормализуй обе выборки. Скорми обучающие данные в метод fit. Пройдись циклом и сделай предсказания для каждого растения в тесте. В самом конце функция должна рассчитать точность и вернуть словарь с двумя ключами: "accuracy" с итоговой точностью и "predictions" со списком всех предсказанных классов.
Терминал призывно мигает пустым курсором. Система охлаждения реактора издает последний предупреждающий гудок. Жизнь Эпсилон-9 зависит от чистоты твоей архитектуры. Приступай.
Блок тестирования: Симуляция запуска на архивных данных
Лира выводит на соседний голографический экран небольшую выборку исторических данных. — Инженер, мы не можем рисковать. Прежде чем подключать твой код напрямую к харвестерам, запусти симуляцию на этом крошечном наборе. Если твой «Синтез» работает верно, мы спасены.
Требования к проверке:
- Запишите в код следующие архивные данные:
train_raw = [
"0.5, 1.2, Звездный Корень",
"0.6, 1.3, Звездный Корень",
"5.1, 8.2, Теневая Лоза",
"4.9, 8.1, Теневая Лоза"
]
test_raw = [
"0.55, 1.25, Звездный Корень",
"5.0, 8.15, Теневая Лоза"
]