
Республиканская AI-Олимпиада для школьников - Победители и Решения Отборочного этапа
DSML совместно с CPFed организовали первую Республиканскую AI-олимпиаду для школьников.
Олимпиада проходит в несколько этапов:
- Отборочный раунд — комбинированный пул задач из спортивного программирования и машинного обучения.
- Homework Kaggle Competition — настоящее Kaggle соревнование для 40 финалистов.
- Offline Final — два тура, где участники за 2 дня решают 4 задачи в стиле Kaggle по ML, CV и NLP.
Со стороны Сообщества организацией занимались:
Имя | Описание |
---|---|
Ален Баев | Организатор олимпиад по математике и спортивному программированию, призёр IMO, ML Engineer @ Higgsfield AI |
Ануар Аймолдин | DSML KZ Community Founder, Kaggle Top 14, призёр APMO |
Нурдаулет Аханов | Призёр международных олимпиад по математике и информатике, SWE @ Google Taiwan |
Данил Орел | NLP ресерчер, KazNLP контрибьютор, студент MBZUAI |
Результаты отборочного раунда AI-Олимпиады
I. Победители (600+ баллов)
Место | Имя | Школа |
---|---|---|
1 | Бекетов Даужан | Лицей №165 (Алматы) |
2 | Агзам Шамсадинов | РФМШ (Алматы) |
3 | Шерияздан Алихан | НИШ (Павлодар) |
II. 500+ баллов
Имя | Школа |
---|---|
Дулат Сәлімжан | НИШ ФМБ (Шымкент) |
Әлім Дәулетбек | НИШ ФМН (Астана) |
Арсен Ғалымжан | НИШ ХБН (Петропавловск) |
Ахмет Сулейменов | ОМПЛИ (Экибастуз) |
III. 400+ баллов
Имя | Школа |
---|---|
Сұлтанби Исатай | НИШ ХБН (Караганда) |
Сұңғат Серікбай | Білім-Инновация лицей (Тараз) |
Әбу Қанабек | РФМШ (Алматы) |
Жанибек Касымкан | НИШ ФМН (Талдыкорган) |
Арнелла Төлеген | НИШ ХБН (Караганда) |
Талим Аушахман | РФМШ (Алматы) |
Nurbatyr Tolegen | НИШ (Петропавловск) |
Batyr Yerdenov | НИШ ХБН (Караганда) |
Юсуф Ситдиков | ОМПЛИ (Экибастуз) |
Әлем Аменов | Білім-Инновация лицей |
Олжас Мусалимов | НИШ ФМН |
Остальные Финалисты
Имя | Школа |
---|---|
Dauren Zhunussov | NIS PHMD |
Nurmyrza Baizhanov | Білім-Инновация лицей (Астана) |
Адиль Сейдазымов | НИШ ФМН (Уральск) |
Сұлтанбек Сары | НИШ ХБН (Актау) |
Ulugbek Osmanov | НИШ ФМН (Тараз) |
Еларыс Ертайұлы | НИШ ФМН (Астана) |
Arsen Kuan | Білім-Инновация лицей |
Timur Shakurov | НСПМ |
Даулет Сабденов | РФМШ (Алматы) |
Әлемжан Ахметжанов | НИШ ФМН (Семей) |
Демеу Шерім | Білім-Инновация лицей (Шымкент) |
Кирилл Богомазов | Школа-лицей (Лисаковск) |
Арсений Берестовой | Школа-лицей (Лисаковск) |
Yerassyl Abilkassym | Haileybury (Алматы) |
Камилла Исхакова | Лицей №134 |
Адилет Барат | Кішішыған ОМ |
Жарас Сейтенов | Школа №10 ЖББОМ |
Саят Токкулиев | Школа №58 |
Иномжон Тохтаров | Школа №63 им. Қ. Сәтбаева |
Арина Елетаева | Гимназия им. А. Бөкейханова №1 |
Осман Мальсагов | Лицей №166 |
Sultan Karilov | Fiztex (Алматы) |
Еркебулан Иманкулов | ЖББ №1 |
Решения задач отборочного этапа
Задача A. Контрастные переходы
В Astana Hub пришёл новый стартап, который разрабатывает ИИ-модуль NeuroScan — передовую систему анализа активности головного мозга. Этот модуль сканирует последовательность сигналов от нейронов и ищет области с наибольшими резкими изменениями — так называемыми контрастными переходами. Эти зоны особенно важны для понимания когнитивных всплесков, памяти и обучения.
Каждое измерение — это уровень активации нейрона. NeuroScan должен определить, какой непрерывный участок длиной демонстрирует наибольшую сумму локальных контрастов между соседними нейронами.
Формальные определения
-
Последовательность сигналов:
-
Локальный контраст между двумя соседними нейронами:
-
Суммарный локальный контраст подотрезка длины :
Цель
Найти максимальное значение среди всех подотрезков длины :
Входные данные
На вход подаётся:
- Одно целое число — длина массива сигналов, где
- Одно целое число — длина подотрезка, где
- целых чисел — уровни нейронной активации, где
Выходные данные
Одно целое число — максимальный суммарный локальный контраст среди всех подотрезков длины .
💡 Решение задачи: скользящие окна
1. Обозначения
Пусть:
Тогда сумма локальных контрастов окна равна:
Искомый ответ:
То есть — максимум суммы подряд идущих элементов массива .
2. Псевдокод
Text1. Считать N, K и массив a[0…N−1]. 2. Если K == 1: Контрастов нет (нет пар соседей) → ответ 0 3. Построить массив d[0…N−2], где: d[i] = (a[i] - a[i+1])² 4. Вычислить начальную сумму: s = sum(d[0…K−2]) // контрасты первого окна 5. Завести переменную max_s = s 6. Для pos = 1 … N-K: s = s - d[pos-1] // убираем старый (левый) контраст + d[pos + K - 2] // добавляем новый (правый) контраст max_s = max(max_s, s) 7. Вывести max_s
3. Python code
Python1import sys 2def max_local_contrast(a, K): 3 N = len(a) 4 if K == 1:# нет пар 5 return 0 6 7 # 1. первые K-1 контрастов 8 cur = sum((a[i] - a[i + 1]) ** 2 for i in range(K - 1)) 9 best = cur 10 11 # 2. скользящее окно 12 for left in range(1, N - K + 1): 13 cur -= (a[left - 1] - a[left]) ** 2 # ушёл слева 14 cur += (a[left + K - 2] - a[left + K - 1]) ** 2 # добавился справа 15 if cur > best: 16 best = cur 17 return best
3. Сложность
-
Время:
— одно проход по массиву , одно скользящее окно по -
Память:
— можно не хранить весь , а пересчитывать нужные элементы на лету
Задача B. Binary Classifier
Абдикожа собрал фотографий друзей, для каждой из которых он отметил, улыбается ли человек (метка ) или нет (). Затем он запустил бинарный классификатор, который выдал вероятность того, что человек на фото улыбается.
После этого он выбрал некоторый порог и оставил только те фотографии, для которых . Он заметил, что при изменении порога точность (precision) может уменьшаться — то есть модель становится менее уверенной. Абдикожа решил найти все такие значения порога , в которых функция убывает.
Определения
- True Positive (TP) — число отобранных фото (), где
- False Positive (FP) — число отобранных фото (), где
- Precision:
Решение
Идея
- Точность меняется только в точках
- Сортируем пары по убыванию
- Поддерживаем TP, FP и считаем точность до и после включения новой фотографии
- Если — сохраняем
3. Алгоритм
Text1. Считать пары (Y_i, P_i) 2. Отсортировать по убыванию P_i 3. Инициализировать: tp = 0, fp = 0, prev = -1 4. Для каждой (Y_i, P_i): - precision_before = tp / (tp + fp) - если Y_i == 1 → tp += 1, иначе → fp += 1 - precision_after = tp / (tp + fp) - если after < before → добавить P_i в ответ - prev = after
Сложность
- Время: O(N log N)
- Память: O(N)
Python code
Python1# Read integer N 2N = int(input()) 3 4# Read the N pairs (label_i, proba_i) 5pairs = [] 6for _ in range(N): 7 values = input() 8 values = values.split() 9 label, proba = int(values[0]), float(values[1]) 10 pairs.append((label, proba)) 11 12# Sort pairs by proba_i ascending 13pairs.sort(key=lambda x: x[1]) 14 15# Find left_index - first i where label_i = 1 16left_index = -1 17for i in range(N): 18 if pairs[i][0] == 1: 19 left_index = i 20 break 21 22# Find right_index - last i where label_i = 0 23right_index = -1 24for i in range(N - 1, -1, -1): 25 if pairs[i][0] == 0: 26 right_index = i 27 break 28 29# Print all proba_i where label_i == 1 30found = False 31for i in range(left_index, right_index + 1): 32 if pairs[i][0] == 1: 33 print(pairs[i][1]) 34 found = True 35 36if not found: 37 print(0)
Задача С - CPFED Net
В CPFED задумались, можно ли построить искусственный интеллект для помощи системе прокторинга. Для этого разрабатывается нейросеть CPFED Net, построенная по принципу многослойной линейной регрессии.
Каждый слой содержит нейронов, всего слоёв — . Все нейроны внутри одного слоя получают один и тот же вход — суммарное значение с предыдущего слоя (на первом слое — это просто входной сигнал ).
Используется техника Dropout — случайное "отключение" нейронов:
- с вероятностью нейрон выдаёт значение ;
- с вероятностью нейрон выдаёт:
Dropout применяется независимо для каждого нейрона.
Финальный результат сети — сумма выходов всех нейронов последнего слоя.
Ваша задача
Вычислите математическое ожидание финального результата нейросети.
Что такое математическое ожидание?
Это среднее значение выходной величины, если эксперимент (отключение нейронов) повторяется бесконечное число раз.
Если нейрон работает с вероятностью , его ожидаемый вклад:
Формат ввода
В первой строке четыре значения:
- — входной сигнал (вещественное число)
- — число слоёв
- — число нейронов в каждом слое
- — вероятность отключения нейрона
Далее строк — по одной на каждый нейрон, сначала нейронов первого слоя, затем второго и т.д.
Каждая строка содержит два целых числа и — параметры линейной регрессии нейрона.
Формат вывода
Одно вещественное число — математическое ожидание финального выхода сети.
Ответ считается корректным, если абсолютная или относительная погрешность не превышает .
Решение
Для каждого слоя :
База:
Искомое значение:
2. Псевдокод
- Считать
- Для каждого слоя:
- Вывести
3. Сложность
- Время:
- Память: (можно считать на лету)
4. Код (Python)
Python1def main(): 2 # Read input 3 parts = input().split() 4 x = float(parts[0]) 5 k = int(parts[1]) 6 n = int(parts[2]) 7 p = float(parts[3]) 8 9 a = [] 10 b = [] 11 for _ in range(k): 12 layer_a = [] 13 layer_b = [] 14 for _ in range(n): 15 coef_a, coef_b = map(int, input().split()) 16 layer_a.append(coef_a) 17 layer_b.append(coef_b) 18 a.append(layer_a) 19 b.append(layer_b) 20 21 prev_layer = x 22 for layer in range(k): 23 sum_val = 0 24 for i in range(n): 25 sum_val += a[layer][i] * prev_layer + b[layer][i] 26 prev_layer = sum_val * (1 - p) 27 28 print(f"{prev_layer:.6f}")
D. DSML Feature Engineering
Эта задача вдохновлена эпохой, когда нейросетей ещё не было — а сообщество ML-специалистов Казахстана DSML KZ (www.dsml.kz) уже было!
Ещё до эпохи глубинного обучения инженеры решали задачи классификации, извлекая признаки вручную и обучая простые, но мощные модели — такие как логистическая регрессия. И хотя времена изменились, умение понимать структуру данных и работать без магии остаётся одним из самых ценных навыков.
В рамках этой задачи мы предлагаем вам погрузиться в атмосферу "олдскульного" машинного обучения — чтобы вы почувствовали, каково это: вытащить максимум из данных без единого слоя нейросети, опираясь только на здравый смысл, математику и немного изобретательности.
🎯 Ваша цель
Придумать признаки, которые можно извлечь из зашумлённого изображения и подать в логистическую регрессию, чтобы классификация работала как можно лучше.
Для этого реализуйте метод:
Python1def extract_features(images): 2 # images: N x 28 x 28 3 features_list = [] 4 for image in images: 5 features = [image.max(), image.std(), image.mean()] 6 features_list.append(features) 7 return features_list
- images — список из изображений
- Каждое изображение — NumPy-массив размера , значения от 0 до 255
- Функция должна вернуть список из векторов признаков
- Каждый вектор должен содержать не более 20 признаков
⚠ Проблема
Ваши враги сильно испортили изображения:
- Бимодальный шум: часть пикселей заменена на значения, близкие к 0 или 255
(шум моделируется через бета-распределение) - Гауссов шум: к каждому пикселю добавлена случайная величина из
⏱ Ограничения
- Время на весь пайплайн (извлечение признаков, обучение и предсказание): не более 10 секунд
- Разрешённые библиотеки: numpy, scikit-learn, scipy, pandas
- Запрещены: нейросети, трансформеры, внешние эмбеддинги, предобученные модели
🧪 Система оценивания
Будет 3 набора тестов:
№ | Описание | Метрика | Баллы макс |
---|---|---|---|
1 | Классификация цифр без шума | score | 150 |
2 | Классификация цифр с шумом | score | 150 |
3 | Классификация 10 двухбуквенных слов | score | 150 |
Бейзлайны для оценки решений
Интересно, что задача по сути маскирует понижение размерности под задачу извлечения признаков. Для корректного скоринга мы подготовили два референсных решения:
✅ 1. MEDIAN PCA — Хорошее решение
Описание:
- медианная фильтрация входных изображений (устраняет бимодальный и гауссов шум)
- понижение размерности с помощью PCA до 20 признаков
- логистическая регрессия по извлечённым признакам
Баллы по датасетам (примерно):
108 + 94 + 82 = 284
Python1import numpy as np 2from scipy.ndimage import median_filter 3from sklearn.decomposition import PCA 4from sklearn.linear_model import LogisticRegression 5from sklearn.metrics import accuracy_score 6from sklearn.model_selection import train_test_split 7 8# --- Медианная фильтрация изображений --- 9def denoise_with_median(X, size=2): 10 X_denoised = np.zeros_like(X) 11 for i in range(X.shape[0]): 12 img = X[i].reshape(28, 28) 13 filtered = median_filter(img, size=size) 14 X_denoised[i] = filtered.flatten() 15 return X_denoised 16 17# --- Экстрактор признаков: медианная фильтрация + PCA --- 18class PCADenoizedFeatureExtractor: 19 def __init__(self, pca_components: int): 20 self.pca_components = pca_components 21 self.pca = PCA(n_components=self.pca_components) 22 23 def fit(self, X): 24 self.pca.fit(X / 255.0) 25 26 def transform(self, X): 27 denoized = denoise_with_median(X, size=2) 28 return self.pca.transform(denoized / 255.0) 29 30# --- Основной пайплайн решения --- 31def denoize_pca_solution(X_train, X_test, y_train): 32 feature_extractor = PCADenoizedFeatureExtractor(pca_components=20) 33 feature_extractor.fit(X_train) 34 train_features = feature_extractor.transform(X_train) 35 test_features = feature_extractor.transform(X_test) 36 37 model = LogisticRegression(multi_class="multinomial", solver="lbfgs", max_iter=5000) 38 model.fit(train_features, y_train) 39 y_pred = model.predict(test_features) 40 return y_pred 41 42def main(X, y): 43 """Для краткости пропустим часть с загрузкой данных""" 44 45 # Разделение на train/test 46 X_train, X_test, y_train, y_test = train_test_split( 47 X, y, test_size=0.5, random_state=42 48 ) 49 50 # Предсказание и метрика 51 y_pred = denoize_pca_solution(X_train, X_test, y_train) 52 acc = accuracy_score(y_test, y_pred) 53 print(f"Denoize PCA solution accuracy: {acc:.4f}")
🧱 2. RESHAPE — доступное решение без знаний ML
Описание:
- уменьшение размеров изображения с помощью бикубической интерполяции
- ресайз до и развёртка в вектор длины 20
- логистическая регрессия по этим признакам
Баллы по датасетам (примерно):
44 + 16 + 42 = 102
Python1class ResizeFeatureExtractor: 2 def __init__(self, n_rows: int, n_cols: int): 3 self.n_rows = n_rows 4 self.n_cols = n_cols 5 self.original_size = (28, 28) 6 7 def fit(self, X, y=None): 8 return self 9 10 def transform(self, X): 11 n_samples = X.shape[0] 12 resized = np.zeros((n_samples, self.n_rows * self.n_cols), dtype=np.float32) 13 14 zoom_factors = ( 15 self.n_rows / self.original_size[0], 16 self.n_cols / self.original_size[1], 17 ) 18 19 for i in range(n_samples): 20 img = X[i].reshape(self.original_size) 21 img_resized = zoom(img, zoom_factors, order=3) # bi-cubic interpolation 22 resized[i] = img_resized.flatten() 23 24 return resized 25 26def resize_solution(X_train, X_test, y_train): 27 # Обучаем 28 resize_feature_extractor = ResizeFeatureExtractor(5, 4) 29 resize_feature_extractor.fit(X_train) 30 train_features = resize_feature_extractor.transform(denoise_with_median(X_train)) 31 test_features = resize_feature_extractor.transform(denoise_with_median(X_test)) 32 33 model = LogisticRegression(multi_class="multinomial", solver="lbfgs", max_iter=5000) 34 model.fit(train_features, y_train) 35 y_pred = model.predict(test_features) 36 return y_pred 37 38# --- Main function for this solution --- 39def main(X, y): 40 """Для краткости пропустим часть с загрузкой данных""" 41 X_train, X_test, y_train, y_test = train_test_split( 42 X, y, test_size=0.5, random_state=42 43 ) 44 y_pred_resize = resize_solution(X_train, X_test, y_train) 45 46 acc_resize = accuracy_score(y_test, y_pred_resize) 47 print(f"Resize solution: {acc_resize:.4f}")
Тематическая классификация научных абстрактов (arXiv)
В научном мире ежедневно публикуются тысячи статей на arXiv.org — одном из крупнейших репозиториев препринтов по математике, физике, компьютерным наукам, экономике и другим дисциплинам.
Каждая статья содержит абстракт — краткое резюме, в котором изложены цели, методы и основные идеи исследования. Как правило, уже по одному предложению из абстракта можно понять, к какой области относится статья.
🎯 Цель
Построить модель, которая по отдельному предложению из абстракта будет определять тематику статьи.
🧩 Что нужно реализовать
В отличие от предыдущего задания, где вы разрабатывали feature extractor для компьютерного зрения, здесь вы будете отправлять полный пайплайн, включающий:
- препроцессинг текста;
- извлечение признаков;
- обучение модели;
- предсказание тематики для новых данных.
⏱ Ограничения
- Время на весь пайплайн (извлечение признаков, обучение и предсказание): не более 10 секунд
- Разрешённые библиотеки: numpy, scikit-learn, scipy, pandas
- Запрещены: нейросети, трансформеры, внешние эмбеддинги, предобученные модели
🔓 Что разрешено
- Любой вариант классической предобработки текста:
- Любая модель из sklearn, подходящая под задачу
🗂 Формат ввода
Датасет — это tsv-таблица с разделителем табуляция.
text | category |
---|---|
предложение (абстракт) | категория статьи |
Категории:
- Если категория указана (3000 примеров) — используйте как обучающую выборку
- Если категория = UNKNOWN (1000 примеров) — это тест, требуется предсказание
Пример:
Text| text | category | |--------------------------------------------------------|----------| | Biomolecules often have some bond lengths. | Biology | | Contextuality is well known as a vital resource. | Physics | | Protein-specific large language models. | UNKNOWN | | In many applications Protein LLMs. | UNKNOWN |
📈 Формула итоговых баллов
Результат зависит от точности (accuracy) мультиклассификации:
Textlimited_accuracy = max(15, min(accuracy, 90)) score = int((limited_accuracy - 15) ** 2 / 25)
Решение TF-IDF - хорошее решение
Описание:
- векторизовать используя tfidf (https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html) vectorizer
- подать в логистическую регрессию
- Баллы по датасетам - 144 + 139 = 283
Код
Python1from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer 2from sklearn.linear_model import LogisticRegression 3from sklearn.metrics import accuracy_score, classification_report 4 5from nltk.stem.snowball import SnowballStemmer 6import re 7 8data = pd.read_csv("arxiv_data/arxiv_dataset.csv", index_col=0) 9 10X_train = data.loc[:2999, 'sampled_sentence'] 11X_test = data.loc[3000:, 'sampled_sentence'] 12y_train = data.loc[:2999, 'paper_section'] 13 14# Инициализируем стеммер 15stemmer = SnowballStemmer("english") 16 17# Кастомная токенизация + стемминг 18def tokenize_and_stem(text): 19 # Базовая нормализация текста 20 text = text.lower() 21 text = re.sub(r'\d+', '', text) 22 text = re.sub(r'[^\w\s]', '', text) 23 tokens = text.strip().split() 24 return [stemmer.stem(token) for token in tokens if len(token) > 2] 25 26vectorizer = TfidfVectorizer( 27 tokenizer=tokenize_and_stem, 28 ngram_range=(1, 2), # unigrams + bigrams 29 stop_words='english', # убираем частые слова 30 max_df=0.06, # убираем супервстречающиеся слова 31) 32 33X_train_vec = vectorizer.fit_transform(X_train) # sparse matrix 34X_test_vec = vectorizer.transform(X_test) 35 36model = LogisticRegression(max_iter=1000) 37model.fit(X_train_vec, y_train) 38 39y_pred = model.predict(X_test_vec)
Всем спасибо, кто осилил этот пост до конца