Discord

Checklist OAI 2025 Final (Stage III) · Task 3

SOTA-AI Editorial

Translation Styling

Fine-tune an English→Polish MarianMT model so that it leaves specified machine-learning terms untranslated.

Written
8 October 2026
Result
No BLEU or points measured because the Marian checkpoint was unavailable; candidate fine-tuning code is included.
Points
Not measured; see result for the limitation.
Olympiad
Polish Olympiad in Artificial Intelligence (Olimpiada Sztucznej Inteligencji), 2025 · Final (Stage III)

Hints

Hint 1

The target references already preserve most listed terms verbatim; first measure how often each term occurs in the source and target.

Hint 2

Fine-tune the supplied translation model on the provided pairs before adding more complicated prompt instructions.

Hint 3

Mark exact keyword spans in source text, use the same transform during training and evaluation, and leave the tokenizer unchanged.

Hint 4

Check sentence-level BLEU on released validation because the references do not preserve every listed phrase exactly.

The solution in one line

a train-only fine-tuning candidate is saved, but missing Marian weights block validation BLEU measurement; the hidden-test score is unknown.

The editorial

Translation Stylization

Problem, domain, and metric

  • Domain: English-to-Polish machine translation with terminology preservation.
  • Task: adapt the MarianMT model gsarti/opus-mt-tc-en-pl so selected AI/ML keywords remain in English, using the provided English/Polish training pairs and keyword lists. The evaluator calls process_example(en, keywords), generates a Polish hypothesis, and compares it with one reference.
  • Metric: the notebook averages NLTK's unsmoothed sentence_bleu over examples. The score is 0 at BLEU ≤0.82, 100 at BLEU ≥0.86, and linear in between. The test set has no Polish references available to the solution.
  • Abridged statement: train a translation model and preprocess each English input so that the target's domain terms follow the corpus style of staying untranslated.

Released data and EDA

The official task notebook and datasets provide 2,808 training and 856 validation examples. Mean sentence lengths are 92.3/96.7 characters for training English/Polish and 93.1/97.0 for validation. The training set has 1,312 distinct keyword strings; validation has 993, of which 973 appear in training. Examples have one to five keywords, most often two.

Whole-phrase, case-insensitive matching finds 95.43% of training keyword occurrences in the English source and 94.81% in the Polish reference. For validation the corresponding rates are 95.13% and 93.55%. Thus inline keyword protection has a strong signal, but exact preservation is not universal; blindly forcing every phrase could disagree with some references.

Experiments and score evidence

Experiment BLEU / points Status
Starter Marian model with no fine-tuning Not measured Marian weights are not cached locally; no starter output is saved in the official notebook
Inline keyword markers plus supervised Marian fine-tuning Not measured Candidate implementation saved; cannot load base weights in this environment

The official evaluator and score function were inspected, but there is no official/released BLEU result to cite. Network requests to Hugging Face fail through the environment proxy with HTTP tunnel 403, and the model files are absent locally. This is a model-access blocker, not a BLEU failure. No validation translation targets were used for training.

Selected candidate

translation_stylization.py provides process_example and train_translation_model. Preprocessing wraps each whole-word keyword occurrence in plain-text [KEEPTERM] markers, without changing the tokenizer. Fine-tuning uses only the official training pairs, with the original Polish translation as target, full-model AdamW at 2e-5, batch size 16, and two epochs. The same preprocessing is used during training and inference.

This is an unscored candidate, not a verified 100-point solution. The official starter already loads the specified base model/tokenizer; the function expects those objects rather than downloading or modifying the tokenizer itself.

Runtime, alternatives, and caveats

The platform allows ten minutes total for training and evaluation on GPU. Runtime was not measured because the base Marian checkpoint could not be loaded and this workspace has CPU-only PyTorch. The 2,808-example training set is small, but whether two full-model epochs fit the official budget remains unverified here.

Alternatives include fine-tuning on raw English only, using LoRA, or adding a textual instruction listing terms. These require a working base checkpoint and BLEU evaluation before they can be ranked. Validation keywords and references are available for normal scoring, but the selected training routine deliberately reads only train_dataset.

Code

Solution code solution.py · 104 lines
"""Fine-tuning and input preprocessing for Translation Stylization (2025 OAI)."""

from __future__ import annotations

import re

import torch
from torch.utils.data import DataLoader


def _split_keywords(keywords):
    if keywords is None:
        return []
    if isinstance(keywords, (list, tuple)):
        values = keywords
    else:
        values = str(keywords).split(",")
    return sorted({str(value).strip() for value in values if str(value).strip()},
                  key=len, reverse=True)


def process_example(en: str, keywords: str) -> str:
    """Mark source occurrences of technical terms for copy-preserving tuning.

    The markers are plain text (the tokenizer is left unchanged). Training uses
    this same preprocessing, with the original Polish reference as target.
    """
    text = str(en)
    terms = _split_keywords(keywords)
    if not terms:
        return text
    alternatives = "|".join(re.escape(term) for term in terms)
    pattern = re.compile(
        r"(?<!\w)(" + alternatives + r")(?!\w)", flags=re.IGNORECASE
    )
    return pattern.sub(
        lambda match: f"[KEEPTERM] {match.group(1)} [/KEEPTERM]", text
    )


def train_translation_model(
    train_dataset,
    tokenizer,
    model,
    *,
    epochs: int = 2,
    batch_size: int = 16,
    learning_rate: float = 2e-5,
    max_length: int = 192,
    device=None,
):
    """Fine-tune the supplied Marian model on training examples only.

    ``train_dataset`` is the Dataset loaded by the starter notebook, with
    English ``en``, Polish ``pl``, and comma-separated ``keywords`` columns.
    Validation translations are never used by this function.
    """
    if device is None:
        device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    if torch.cuda.is_available():
        torch.manual_seed(42)
        torch.cuda.manual_seed_all(42)

    def collate(rows):
        source_text = [
            process_example(row["en"], row.get("keywords", "")) for row in rows
        ]
        target_text = [row["pl"] for row in rows]
        source = tokenizer(
            source_text,
            padding=True,
            truncation=True,
            max_length=max_length,
            return_tensors="pt",
        )
        target = tokenizer(
            text_target=target_text,
            padding=True,
            truncation=True,
            max_length=max_length,
            return_tensors="pt",
        )
        labels = target["input_ids"]
        labels[labels == tokenizer.pad_token_id] = -100
        return {
            "input_ids": source["input_ids"],
            "attention_mask": source["attention_mask"],
            "labels": labels,
        }

    generator = torch.Generator().manual_seed(42)
    loader = DataLoader(
        train_dataset,
        batch_size=batch_size,
        shuffle=True,
        collate_fn=collate,
        num_workers=0,
        generator=generator,
    )
    model.to(device)
    model.train()
    optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate)
    old_use_cache = getattr(model.config, "use_cache", None)
    if old_use_cache is not None:
        model.config.use_cache = False

    for _ in range(epochs):
        for batch in loader:
            batch = {key: value.to(device) for key, value in batch.items()}
            result = model(**batch)
            result.loss.backward()
            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
            optimizer.step()
            optimizer.zero_grad(set_to_none=True)

    if old_use_cache is not None:
        model.config.use_cache = old_use_cache
    model.eval()
    return model

Download solution.py

Editorials are written by members of the SOTA community, not by the olympiad. The official statement and scoring win wherever an editorial differs.