Республиканская AI-Олимпиада для школьников - Победители и Решения Отборочного этапа

Республиканская AI-Олимпиада для школьников - Победители и Решения Отборочного этапа

30 апреля 2025

DSML совместно с CPFed организовали первую Республиканскую AI-олимпиаду для школьников.

Олимпиада проходит в несколько этапов:

  1. Отборочный раунд — комбинированный пул задач из спортивного программирования и машинного обучения.
  2. Homework Kaggle Competition — настоящее Kaggle соревнование для 40 финалистов.
  3. 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 ZhunussovNIS PHMD
Nurmyrza BaizhanovБілім-Инновация лицей (Астана)
Адиль СейдазымовНИШ ФМН (Уральск)
Сұлтанбек СарыНИШ ХБН (Актау)
Ulugbek OsmanovНИШ ФМН (Тараз)
Еларыс ЕртайұлыНИШ ФМН (Астана)
Arsen KuanБілім-Инновация лицей
Timur ShakurovНСПМ
Даулет СабденовРФМШ (Алматы)
Әлемжан АхметжановНИШ ФМН (Семей)
Демеу ШерімБілім-Инновация лицей (Шымкент)
Кирилл БогомазовШкола-лицей (Лисаковск)
Арсений БерестовойШкола-лицей (Лисаковск)
Yerassyl AbilkassymHaileybury (Алматы)
Камилла ИсхаковаЛицей №134
Адилет БаратКішішыған ОМ
Жарас СейтеновШкола №10 ЖББОМ
Саят ТоккулиевШкола №58
Иномжон ТохтаровШкола №63 им. Қ. Сәтбаева
Арина ЕлетаеваГимназия им. А. Бөкейханова №1
Осман МальсаговЛицей №166
Sultan KarilovFiztex (Алматы)
Еркебулан ИманкуловЖББ №1

Решения задач отборочного этапа

Задача A. Контрастные переходы

В Astana Hub пришёл новый стартап, который разрабатывает ИИ-модуль NeuroScan — передовую систему анализа активности головного мозга. Этот модуль сканирует последовательность сигналов от нейронов и ищет области с наибольшими резкими изменениями — так называемыми контрастными переходами. Эти зоны особенно важны для понимания когнитивных всплесков, памяти и обучения.

Каждое измерение — это уровень активации нейрона. NeuroScan должен определить, какой непрерывный участок длиной демонстрирует наибольшую сумму локальных контрастов между соседними нейронами.

Формальные определения

  • Последовательность сигналов:

  • Локальный контраст между двумя соседними нейронами:

  • Суммарный локальный контраст подотрезка длины :


Цель

Найти максимальное значение среди всех подотрезков длины :


Входные данные

На вход подаётся:

  • Одно целое число — длина массива сигналов, где
  • Одно целое число — длина подотрезка, где
  • целых чисел — уровни нейронной активации, где

Выходные данные

Одно целое число — максимальный суммарный локальный контраст среди всех подотрезков длины .


💡 Решение задачи: скользящие окна

1. Обозначения

Пусть:

Тогда сумма локальных контрастов окна равна:

Искомый ответ:

То есть — максимум суммы подряд идущих элементов массива .


2. Псевдокод

Text
1. Считать 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

Python
1import 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. Алгоритм

Text
1. Считать пары (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

Python
1# 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. Псевдокод

  1. Считать
  2. Для каждого слоя:
  3. Вывести

3. Сложность

  • Время:
  • Память: (можно считать на лету)

4. Код (Python)

Python
1def 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) уже было!

Ещё до эпохи глубинного обучения инженеры решали задачи классификации, извлекая признаки вручную и обучая простые, но мощные модели — такие как логистическая регрессия. И хотя времена изменились, умение понимать структуру данных и работать без магии остаётся одним из самых ценных навыков.

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


🎯 Ваша цель

Придумать признаки, которые можно извлечь из зашумлённого изображения и подать в логистическую регрессию, чтобы классификация работала как можно лучше.

Для этого реализуйте метод:

Python
1def 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Классификация цифр без шумаscore150
2Классификация цифр с шумомscore150
3Классификация 10 двухбуквенных словscore150

Бейзлайны для оценки решений

Интересно, что задача по сути маскирует понижение размерности под задачу извлечения признаков. Для корректного скоринга мы подготовили два референсных решения:


✅ 1. MEDIAN PCA — Хорошее решение

Описание:

  • медианная фильтрация входных изображений (устраняет бимодальный и гауссов шум)
  • понижение размерности с помощью PCA до 20 признаков
  • логистическая регрессия по извлечённым признакам

Баллы по датасетам (примерно):
108 + 94 + 82 = 284

Python
1import 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

Python
1class 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-таблица с разделителем табуляция.

textcategory
предложение (абстракт)категория статьи

Категории:

  • Если категория указана (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) мультиклассификации:

Text
  limited_accuracy = max(15, min(accuracy, 90))
  score = int((limited_accuracy - 15) ** 2 / 25)

Решение TF-IDF - хорошее решение

Описание:

Код

Python
1from 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)

Всем спасибо, кто осилил этот пост до конца