Discord

Checklist OAI 2026 Final (Stage III) · Task 4

SOTA-AI Editorial

Hidden Categories

Predict the set of hierarchical e-commerce category labels of each product from its 256-dimensional collaborative-filtering embedding.

Written
3 October 2026
Released validation result
0.483 mean IoU
Estimated points
100/100
Olympiad
Polish Olympiad in Artificial Intelligence (Olimpiada Sztucznej Inteligencji), 2026 · Final (Stage III)

Measured results use the released validation sets and the evaluator logic in each notebook. They do not establish secret-test performance.

Hints

Hint 1

Inspect path lengths and category frequencies before choosing a loss or output rule.

Hint 2

Do the embeddings support local similarity? Compare cosine neighbors.

Hint 3

Use the position in each category path as a hierarchy level.

Hint 4

How should a model decide whether to stop adding deeper categories?

The solution in one line

Use similarity-weighted cosine neighbors to vote independently at each path depth, with a learned stop rule.

The editorial

Dataset analysis and EDA

There are 12,145 training products, 2,603 validation products, 256 features, and 1,095 distinct training labels. Every training row includes the root category Sports & Outdoors. The label distribution is long-tailed: 248 labels appear once, while 40 validation labels are absent from training. Paths contain 1–7 labels, with a mean length of about 4.61; most have four or five.

The embedding norms are tightly concentrated (median about 0.543; interquartile range about 0.515–0.580), so cosine neighbors are a useful similarity measure. Category paths preserve their hierarchy by list position, which makes votes by depth more appropriate than an unstructured global threshold.

Solution strategy and experiments

Normalize training and query embeddings, retrieve the 80 highest-cosine neighbors, and weight each neighbor by similarity⁴. At each category depth, accumulate votes for every label and a STOP outcome for neighbors whose path ends before that depth. Predict the strongest category when its vote exceeds the depth-specific STOP vote multiplied by [0, 1, 1, 3, 2, 1, 1].

Method Validation mean IoU
Ridge multi-label scores, top three labels 0.441
Cosine k-NN votes, global top three labels 0.472
Depth-wise cosine votes, unadjusted stop decision 0.472
Depth-wise votes with calibrated stop bias 0.483

The selected method reaches 0.483 mean IoU, above the notebook’s 0.46 full-credit threshold. The 40 validation-only categories are unseen classes; a train-vocabulary classifier cannot predict them.

Implementation and compute footprint

The YourSolution class uses NumPy only and fits solely on the training embeddings and labels. Inference requires a cosine-similarity matrix for the query batch and 80-neighbor votes. It runs on CPU and stays within the notebook’s five-minute limit for the provided sizes.

Other candidates include one-vs-rest ridge/logistic models, global label thresholds, and voting for complete paths. Ridge underperformed the depth-wise neighbor approach; complete-path voting discarded useful agreement at individual hierarchy levels.

Code

Solution code solution.py · 58 lines
import numpy as np

######################### TUTAJ JEST MIEJSCE NA TWOJE FINALNE ROZWIĄZANIE ##########################

class YourSolution:
    """Cosine-neighbor multilabel classifier that respects taxonomy depth."""

    def __init__(self):
        self.n_neighbors = 80
        self.similarity_power = 4
        # A depth-specific STOP bias, tuned on the released validation set.
        self.stop_bias = np.array([0, 1, 1, 3, 2, 1, 1], dtype=np.float32)

    def fit(self, X, y):
        X = np.asarray(X, dtype=np.float32)
        self.X_train = X / (np.linalg.norm(X, axis=1, keepdims=True) + 1e-12)
        self.paths = [list(path) for path in y]
        self.max_depth = max((len(path) for path in self.paths), default=0)
        self.classes_by_depth = []
        self.train_codes_by_depth = []

        for depth in range(self.max_depth):
            classes = sorted({path[depth] for path in self.paths if len(path) > depth})
            class_to_code = {label: index for index, label in enumerate(classes)}
            stop_code = len(classes)
            codes = np.full(len(self.paths), stop_code, dtype=np.int32)
            for row_index, path in enumerate(self.paths):
                if len(path) > depth:
                    codes[row_index] = class_to_code[path[depth]]
            self.classes_by_depth.append(classes)
            self.train_codes_by_depth.append(codes)
        return self

    def predict(self, X):
        X = np.asarray(X, dtype=np.float32)
        if X.ndim == 1:
            X = X.reshape(1, -1)
        X = X / (np.linalg.norm(X, axis=1, keepdims=True) + 1e-12)
        similarities = X @ self.X_train.T
        k = min(self.n_neighbors, len(self.X_train))
        neighbor_indices = np.argpartition(similarities, -k, axis=1)[:, -k:]
        neighbor_similarities = np.take_along_axis(similarities, neighbor_indices, axis=1)
        order = np.argsort(neighbor_similarities, axis=1)[:, ::-1]
        neighbor_indices = np.take_along_axis(neighbor_indices, order, axis=1)
        neighbor_similarities = np.take_along_axis(neighbor_similarities, order, axis=1)
        weights = np.maximum(neighbor_similarities, 0.0) ** self.similarity_power

        predictions = [[] for _ in range(len(X))]
        row_indices = np.arange(len(X))
        for depth, classes in enumerate(self.classes_by_depth):
            votes = np.zeros((len(X), len(classes) + 1), dtype=np.float32)
            train_codes = self.train_codes_by_depth[depth]
            for rank in range(k):
                codes = train_codes[neighbor_indices[:, rank]]
                votes[row_indices, codes] += weights[:, rank]

            best_codes = votes[:, :-1].argmax(axis=1)
            best_votes = votes[row_indices, best_codes]
            stop_votes = votes[:, -1]
            bias = self.stop_bias[depth] if depth < len(self.stop_bias) else 1.0
            use_category = best_votes > bias * stop_votes
            for row_index in np.flatnonzero(use_category):
                label = classes[best_codes[row_index]]
                if label not in predictions[row_index]:
                    predictions[row_index].append(label)
        return predictions

Download solution.py

Written by Gonçalo Franco; added here on 4 October 2026. Editorials are written by members of the SOTA community, not by the olympiad. The official statement and scoring win wherever an editorial differs.