Checklist OAI 2025 Stage I · Task 5
SOTA-AI Editorial
Hidden Subsequences
Learn a small neural regressor that predicts the value of a binary string, defined as the sum of weights of hidden patterns occurring in it as subsequences.
Hints
Hint 1
Verify that targets take a finite set of values before choosing regression or classification.
Hint 2
A subsequence depends on ordered bits across the whole input, not only adjacent pairs.
Hint 3
Use all recurrent time-step states or attention so early and middle patterns remain visible to the output layer.
Hint 4
Map class predictions back to the eight legal target values.
The solution in one line
An 8-class, 49,512-parameter LSTM scores MSE 1.0468 (98/100) on organizer-released validation; Google Drive blocking prevents local pursuit of 100 points.
The editorial
Poland 2025 Stage I — Ukryte Podciągi (Hidden Subsequences)
Domain: Binary sequence modeling, hidden-pattern regression/classification
Metric: Mean squared error. The score is round(max(100*(64-MSE)/64, 0)); a validation MSE of about 0.32 or lower is needed for a rounded 100.
Official sources: SOTA checklist · starter notebook · organizer worked solution
Abridged statement
Given a fixed-length binary sequence, predict the sum of the weights for three hidden binary subsequences that occur in it. A subsequence preserves order but need not be contiguous. The submission must be a learnable PyTorch model with fewer than 50,000 parameters, trained for no more than 4,000 iterations, and must run within four minutes on GPU.
Dataset analysis and EDA
The organizer notebook reports 511,999 training and 511,999 validation rows. Each input is a 24-bit binary vector with no missing values. Targets take exactly eight values: [-19, -8, 0, 11, 28, 39, 47, 58]. Train and validation target means are 22.67 and 22.70, standard deviations are both 24.24, and the mean input bit is 0.50 in both splits. The notebook notes that the number of ones alone has no simple linear relation to the target. Data are downloaded from Google Drive and are not in GitHub or the local mirrors; access failed here with HTTP 403: CONNECT tunnel failed.
Experiments
- A last-state LSTM regressor with 42,113 parameters reaches validation MSE 56.3366 (12/100 points).
- Adding attention over all LSTM outputs uses 42,177 parameters and reduces MSE to 7.2511 (89/100 points).
- Regression outputs from the attention model are all between the eight valid target values rather than exactly equal to them, motivating classification.
- The selected eight-class LSTM uses all 24 hidden states, maps classes back to the eight target values, has 49,512 parameters, and is trained for the allowed 4,000 iterations. Its validation MSE is 1.0468, corresponding to 98/100 points.
- The notebook also discusses 1-D CNN regression and GRU/LSTM pooling as alternatives, but supplies no measured results for them.
The organizer's example MLP and the LSTM v1 use the final sequence state; v2 adds attention; v3 changes the output to classification. No additional local experiments were possible without the CSVs; the shared PyTorch runtime is available on CPU, but the task expects a GPU run.
Selected solution and validation evidence
The standalone script in hidden_subsequences.py reworks the selected ClfLSTM and its 4,000-step training loop. It derives the class list from training targets and uses the validation CSV only for final MSE scoring. The organizer's notebook records validation MSE 1.0468 and 98/100 estimated points. This is organizer-reported, not locally reproduced. The CSV download requests are blocked by the environment's Google Drive 403. PyTorch is available locally, but no GPU is available for the four-minute GPU limit; the inaccessible data prevent local pursuit of the remaining two points or alternative validation. No hidden labels or secret-test claims are made.
Compute and runtime
The task permits at most 4,000 training iterations, 50,000 parameters, and four minutes on GPU. The selected model has 49,512 parameters and uses 128-example minibatches. A precise end-to-end wall-clock time is not recorded in the executed notebook output.
Alternatives
The measured alternatives are a final-state LSTM regressor and an attention LSTM regressor. The model notebook discusses 1-D convolutions for local motifs and mean/max pooling over recurrent states, but gives no validation measurements for those ideas. Recovering the exact subsequence logic is an attractive diagnostic, but the competition explicitly requires a learnable model and the official solution stays within that requirement.
Code
Solution code
"""Train-only LSTM classifier for Poland 2025 Stage I: hidden subsequences.
The architecture and optimization follow the organizer's strongest notebook
variant (8-class, 2-layer LSTM using every time-step output). It has 49,512
trainable parameters and accepts at most 4,000 minibatch updates. The script
does not download data and never uses validation targets during training.
Usage:
python hidden_subsequences.py train_dataset.csv val_dataset.csv
CSV rows contain 24 binary inputs followed by the target value. PyTorch is the
only non-standard dependency. The official task expects a GPU; CPU execution
is supported for smoke checks but may exceed the competition time limit.
"""
from __future__ import annotations
import argparse
import random
from pathlib import Path
import numpy as np
import pandas as pd
import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset
SEQUENCE_LENGTH = 24
MAX_STEPS = 4000
DEFAULT_BATCH_SIZE = 128
def seed_everything(seed: int = 12345) -> None:
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
def read_csv_split(path: str | Path) -> tuple[torch.Tensor, torch.Tensor]:
"""Load 24 binary columns and one target column from an official CSV."""
values = pd.read_csv(path).to_numpy(dtype=np.float32)
if values.ndim != 2 or values.shape[1] != SEQUENCE_LENGTH + 1:
raise ValueError(
f"expected {SEQUENCE_LENGTH} input columns plus one target, got {values.shape}"
)
x = torch.from_numpy(values[:, :-1])
y = torch.from_numpy(values[:, -1])
if not torch.isin(x, torch.tensor([0.0, 1.0])).all():
raise ValueError("input sequence must contain only 0 and 1")
return x, y
class HiddenSubsequenceLSTM(nn.Module):
"""Organizer v3 classifier; forward returns values in the task's target set."""
def __init__(self, possible_values: torch.Tensor | np.ndarray | list[float]):
super().__init__()
values = torch.as_tensor(possible_values, dtype=torch.float32).flatten().sort().values
if values.numel() == 0:
raise ValueError("possible_values cannot be empty")
self.register_buffer("possible_values", values)
self.lstm = nn.LSTM(input_size=1, hidden_size=56, num_layers=2, batch_first=True)
self.head = nn.Linear(56 * SEQUENCE_LENGTH, values.numel())
def logits(self, x: torch.Tensor) -> torch.Tensor:
x = x.float().unsqueeze(-1)
recurrent, _ = self.lstm(x)
return self.head(recurrent.reshape(x.shape[0], -1))
def encode_targets(self, y: torch.Tensor) -> torch.Tensor:
values = self.possible_values.to(y.device)
class_ids = torch.searchsorted(values, y.float().reshape(-1))
if (class_ids >= values.numel()).any() or not torch.equal(
values[class_ids], y.float().reshape(-1)
):
raise ValueError("target value was not present in the training target set")
return class_ids
def forward(self, x: torch.Tensor) -> torch.Tensor:
class_ids = self.logits(x).argmax(dim=1)
return self.possible_values[class_ids]
def train_model(
train_csv: str | Path,
*,
steps: int = MAX_STEPS,
batch_size: int = DEFAULT_BATCH_SIZE,
learning_rate: float = 0.01,
seed: int = 12345,
device: str | torch.device | None = None,
) -> tuple[HiddenSubsequenceLSTM, torch.device]:
"""Fit only on the supplied training CSV for no more than 4,000 steps."""
if not 1 <= steps <= MAX_STEPS:
raise ValueError(f"steps must be between 1 and {MAX_STEPS}")
seed_everything(seed)
x_train, y_train = read_csv_split(train_csv)
train_loader = DataLoader(
TensorDataset(x_train, y_train), batch_size=batch_size, shuffle=True
)
if len(train_loader) == 0:
raise ValueError("training CSV contains no examples")
target_values = torch.unique(y_train, sorted=True)
model = HiddenSubsequenceLSTM(target_values)
target_device = torch.device(
device if device is not None else ("cuda" if torch.cuda.is_available() else "cpu")
)
model.to(target_device)
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
criterion = nn.CrossEntropyLoss()
batches = iter(train_loader)
model.train()
for _ in range(steps):
try:
x_batch, y_batch = next(batches)
except StopIteration:
batches = iter(train_loader)
x_batch, y_batch = next(batches)
x_batch = x_batch.to(target_device)
y_batch = y_batch.to(target_device)
class_targets = model.encode_targets(y_batch)
optimizer.zero_grad(set_to_none=True)
loss = criterion(model.logits(x_batch), class_targets)
loss.backward()
optimizer.step()
return model.eval(), target_device
@torch.no_grad()
def evaluate_mse(
model: HiddenSubsequenceLSTM,
csv_path: str | Path,
device: str | torch.device,
batch_size: int = DEFAULT_BATCH_SIZE,
) -> float:
"""Score a released split after fitting; its targets never enter training."""
x, y = read_csv_split(csv_path)
loader = DataLoader(TensorDataset(x, y), batch_size=batch_size, shuffle=False)
batch_mses = []
model.eval()
for x_batch, y_batch in loader:
predictions = model(x_batch.to(device)).cpu()
batch_mses.append(float(torch.mean((predictions - y_batch) ** 2)))
return float(np.mean(batch_mses))
def estimate_points(mse: float) -> int:
return int(round(max(100.0 * (64.0 - mse) / 64.0, 0.0)))
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("train_csv", help="released training CSV")
parser.add_argument("valid_csv", help="released validation CSV; used only for scoring")
parser.add_argument("--steps", type=int, default=MAX_STEPS)
parser.add_argument("--batch-size", type=int, default=DEFAULT_BATCH_SIZE)
parser.add_argument("--device", default=None, help="defaults to CUDA when available")
args = parser.parse_args()
model, device = train_model(
args.train_csv, steps=args.steps, batch_size=args.batch_size, device=args.device
)
count = sum(parameter.numel() for parameter in model.parameters() if parameter.requires_grad)
mse = evaluate_mse(model, args.valid_csv, device, batch_size=args.batch_size)
print(f"trainable parameters: {count}")
print(f"validation MSE: {mse:.4f}")
print(f"estimated score: {estimate_points(mse)}/100")
if __name__ == "__main__":
main()
Editorials are written by members of the SOTA community, not by the olympiad. The official statement and scoring win wherever an editorial differs.