Discord

Checklist Bulgaria selection 2026 IOAI Team Selection · Task 3

Are You Surprised? (Facial Emotion Recognition)

Bulgarian title: Изненадани ли сте?

Classify 48x48 greyscale face images into seven emotions without pre-trained models.

  • Vision
  • Multi-class image classification
  • Bulgarian original · English translation

The task

The images are greyscale human faces labelled with one of seven emotions: angry, disgusted, fearful, happy, neutral, sad and surprised. The contestant must build an algorithm that recognises the emotion, improving on a provided Random Forest baseline on flattened pixels.

Neural networks, classical machine learning, data augmentation and ensembles are allowed. Manual labelling of test images, use of test labels, searching for the test images online, models that send images to external services, additional labelled emotion datasets and pre-trained models are forbidden.

Abridged and translated by SOTA from the official Bulgarian materials. The official statement has the exact rules, and it wins wherever this summary differs.

In English

This task was published in Bulgarian. SOTA translated its 2 files into English on 17 September 2026. Only the words changed in the notebooks: markdown, code comments, messages and printed output. The code, file names and paths are the original's, so a translated notebook runs with the original data.

Read the task notebook in English 284 words and 9 code cells

Are You Surprised?

Task

You are given black-and-white images of human faces, of size 48x48, classified into 7 categories according to the emotion of the person in the photo: angry (angry), disgusted (disgusted), fearful (fearful), happy (happy), neutral (neutral), sad (sad) and surprised (surprised).

Some examples from the training data:

Your task is to build an algorithm that can recognise these emotions.

Allowed:

  • neural networks
  • classical machine learning algorithms
  • data augmentation techniques (data augmentation)
  • ensemble methods

Forbidden:

  • manually labelling test images
  • using test labels
  • searching for the test images on the internet
  • models that send images to external services
  • using additional datasets labelled with emotions
  • pre-trained models
Evaluation metric

The metric for automatic evaluation is F1 Macro. It computes the arithmetic mean of the F1 score for each class separately, treating all classes as completely equal, regardless of their size or count.

Submission file

The submission file has two columns - image_id and label. Example structure:

image_id,label
028710.png,sad
028711.png,sad
028712.png,sad
028713.png,sad
028714.png,sad

Baseline solution

Importing libraries

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.svm import SVC
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import f1_score
import time
import joblib
import os
import zipfile

Loading the data

def decompress_zip(file_path, extract_to):
    with zipfile.ZipFile(file_path, 'r') as zip_ref:
        zip_ref.extractall(extract_to)
# Loading the prepared data
data = np.load('../data/processed/fer2013_processed.npz', allow_pickle=True)

X_train = data['X_train']
X_val = data['X_val']
X_test = data['X_test']
y_train = data['y_train']
y_val = data['y_val']
y_test = data['y_test']
class_weights_array = data['class_weights']
EMOTIONS = list(data['emotions'])

print(f"X_train shape: {X_train.shape}")
print(f"X_val shape: {X_val.shape}")
print(f"X_test shape: {X_test.shape}")
print(f"\nEmotions: {EMOTIONS}")

Flattening the images

Models such as Random Forest expect a 1D vector as input, not a 2D image.

Transformation: (N, 48, 48)(N, 2304)

Each pixel becomes a separate feature (48 × 48 = 2304 features).

# Flatten: (N, 48, 48) -> (N, 2304)
X_train_flat = X_train.reshape(X_train.shape[0], -1)
X_val_flat = X_val.reshape(X_val.shape[0], -1)
X_test_flat = X_test.reshape(X_test.shape[0], -1)

print(f"X_train: {X_train.shape} -> {X_train_flat.shape}")
print(f"X_val: {X_val.shape} -> {X_val_flat.shape}")
print(f"X_test: {X_test.shape} -> {X_test_flat.shape}")
print(f"\nEach image is a vector with {X_train_flat.shape[1]} features")

4. Training a Random Forest model

print("Training a Random Forest model...")
print()

start_time = time.time()

rf_model = RandomForestClassifier(
    n_estimators=100,
    class_weight='balanced',
    random_state=42,
    n_jobs=-1,
    verbose=1
)

rf_model.fit(X_train_flat, y_train)

rf_train_time = time.time() - start_time
print(f"\nTraining time: {rf_train_time:.2f} seconds")
# Predictions on the validation set
print("Predicting on the validation set...")
y_val_pred_rf = rf_model.predict(X_val_flat)

# Metrics
rf_val_accuracy = accuracy_score(y_val, y_val_pred_rf)
rf_val_f1 = f1_score(y_val, y_val_pred_rf, average='weighted')

print(f"\nRandom Forest Validation Results:")
print(f"  Accuracy: {rf_val_accuracy:.4f} ({rf_val_accuracy*100:.2f}%)")
print(f"  F1-Score (weighted): {rf_val_f1:.4f}")

Task

Improve the given baseline model or implement another model. Using a pre-trained model is not allowed.

# Write your solution here

Final evaluation on the Test Set

We use the test set only once - for the final evaluation!

print(f"Final evaluation of {best_model_name} on the TEST set:")
print("=" * 50)

# Predictions on the test set
y_test_pred = best_model.predict(X_test_flat)

# Metrics
test_accuracy = accuracy_score(y_test, y_test_pred)
test_f1 = f1_score(y_test, y_test_pred, average='weighted')

print(f"Test Accuracy: {test_accuracy:.4f} ({test_accuracy*100:.2f}%)")
print(f"Test F1-Score (weighted): {test_f1:.4f}")
print()
print("Classification Report:")
print(classification_report(y_test, y_test_pred, target_names=EMOTIONS))

10. Saving the model

# Creating a directory for models
os.makedirs('../models', exist_ok=True)

# Saving the two models
joblib.dump(rf_model, '../models/rf_baseline.joblib')

print("The models have been saved:")

Translated by SOTA. The Bulgarian original is the official version and wins wherever the two differ. The original folder names the solution '03_Best_Solution' and does not name its author; it reads the data from the Kaggle competition task-emotion-detection-cv. The baseline in the task notebook loads ../data/processed/fer2013_processed.npz, which is not in the original folder (it has train.zip and test.zip), and its saved outputs come from an earlier run. The title and axis labels inside the solution's saved class-distribution plot are still in Bulgarian. If you organise this olympiad and would like the translation removed, email [email protected] and we will take it down.

At a glance

You get
train.zip with the training images; test.zip (in the repository it holds only sample_submission.csv); the baseline notebook.
You submit
A submission CSV with columns image_id and label (emotion name).
Scoring
Macro F1.
Rules
  • No pre-trained models or additional labelled data.
  • No manual labelling or use of test labels.
Format
Bulgarian IOAI 2026 team selection (after the National Competition); Day 2, Task 3. Dates are not published in the repository.

Details

Year
2026
Round
IOAI Team Selection · Task 3
Language
Bulgarian; English translation by SOTA
License
Not stated by the source