Ashybalka/xlm-roberta-taxonomy-seniority-de
XLM-RoBERTa Job Taxonomy — Seniority (German)
   
Fine-tuned xlm-roberta-base for classifying German job listings by seniority level into 5 grades: Trainee, Junior, Mid, Senior, Lead+.
Part of the JobBlast taxonomy model family — a system for automated classification of German-language job postings. This model is orthogonal to the industry classifier: it determines the experience level required for a position, independent of the field. Combine it with xlm-roberta-taxonomy-main-de to get both what kind of role and what level a vacancy targets.
Test Metrics
Evaluated on a held-out test set of 20,55 German job listings.
Per-class results
The close gap between F1 macro (0.939) and F1 weighted (0.946) indicates balanced performance across grades despite the underlying class imbalance — rare grades (Junior, Lead+) are not sacrificed for accuracy on the dominant Mid class.
Label Schema
Note on Lead+
The original LLM-labeled dataset distinguished Lead and Principal as separate grades. Because Principal had only ~200 examples — too few to train a reliable transformer classifier — the two classes were merged into Lead+ for training. If you need to recover Principal specifically, apply a post-processing regex on the job title after this model's prediction (e.g. r"\b(Principal|Staff|Chief|VP)\b").
Repository Structure
Ashybalka/xlm-roberta-taxonomy-seniority-de/
├── config.json # shared — label map, model config
├── tokenizer.json # shared — fast tokenizer
├── tokenizer_config.json # shared
├── sentencepiece.bpe.model # shared
├── special_tokens_map.json # shared
├── test_metrics.json # evaluation results
├── classification_report.txt # full per-class report
│
├── pytorch/
│ ├── config.json # needed for from_pretrained(subfolder=)
│ └── model.safetensors # GPU inference / fine-tuning (~1.1 GB)
│
├── onnx/
│ └── model.onnx # CPU fp32 inference (~1.1 GB)
│
└── onnx-int8/
└── model_quantized.onnx # CPU INT8 quantized (~280 MB, 3-4× smaller)Usage
Input Format
[Job Title] [SEP] [Job Description]text = "Senior DevOps Engineer [SEP] Mindestens 5 Jahre Erfahrung mit " \
"Kubernetes, Terraform und CI/CD Pipelines. Teamführung von Vorteil."PyTorch (GPU / fine-tuning)
from transformers import pipeline
clf = pipeline(
"text-classification",
model="Ashybalka/xlm-roberta-taxonomy-seniority-de",
subfolder="pytorch",
device=0, # GPU; -1 for CPU
)
result = clf("Senior DevOps Engineer [SEP] 5+ Jahre Erfahrung mit Kubernetes und Terraform.")
print(result)
# [{'label': 'Senior', 'score': 0.9645}]ONNX fp32 (CPU inference)
from optimum.onnxruntime import ORTModelForSequenceClassification
from transformers import AutoTokenizer, pipeline
model = ORTModelForSequenceClassification.from_pretrained(
"Ashybalka/xlm-roberta-taxonomy-seniority-de",
subfolder="onnx"
)
tokenizer = AutoTokenizer.from_pretrained("Ashybalka/xlm-roberta-taxonomy-seniority-de")
clf = pipeline("text-classification", model=model, tokenizer=tokenizer)
result = clf("Werkstudent Marketing [SEP] Unterstützung bei Social-Media-Kampagnen, 15-20 Stunden pro Woche.")
print(result)
# [{'label': 'Trainee', 'score': 0.9821}]ONNX INT8 (CPU, lightweight)
from optimum.onnxruntime import ORTModelForSequenceClassification
from transformers import AutoTokenizer, pipeline
model = ORTModelForSequenceClassification.from_pretrained(
"Ashybalka/xlm-roberta-taxonomy-seniority-de",
subfolder="onnx-int8"
)
tokenizer = AutoTokenizer.from_pretrained("Ashybalka/xlm-roberta-taxonomy-seniority-de")
clf = pipeline("text-classification", model=model, tokenizer=tokenizer)Direct ONNX Runtime (no transformers)
For production FastAPI services or environments without transformers:
import json
import numpy as np
import onnxruntime as ort
from huggingface_hub import snapshot_download
from tokenizers import Tokenizer
# download model
path = snapshot_download(
"Ashybalka/xlm-roberta-taxonomy-seniority-de",
allow_patterns=["onnx/model.onnx", "tokenizer.json", "config.json"]
)
# load
session = ort.InferenceSession(f"{path}/onnx/model.onnx",
providers=["CPUExecutionProvider"])
tokenizer = Tokenizer.from_file(f"{path}/tokenizer.json")
tokenizer.enable_truncation(max_length=510)
tokenizer.no_padding()
with open(f"{path}/config.json") as f:
labels = [v for _, v in sorted(json.load(f)["id2label"].items(), key=lambda x: int(x[0]))]
vocab = tokenizer.get_vocab()
bos, eos = vocab["<s>"], vocab["</s>"]
def classify_seniority(title: str, description: str) -> dict:
text = f"{title} [SEP] {description}"
encoding = tokenizer.encode(text, add_special_tokens=False)
ids = [bos] + encoding.ids + [eos]
mask = [1] * len(ids)
logits = session.run(None, {
"input_ids": np.array([ids], dtype=np.int64),
"attention_mask": np.array([mask], dtype=np.int64),
})[0][0]
exp = np.exp(logits - logits.max())
probs = exp / exp.sum()
idx = int(np.argmax(probs))
return {"grade": labels[idx], "confidence": round(float(probs[idx]), 4)}
print(classify_seniority(
"Lead Software Architect",
"Verantwortung für die Architektur unserer Microservices-Plattform, Führung eines 8-köpfigen Teams."
))
# {'grade': 'Lead+', 'confidence': 0.9456}Training Details
Class distribution in training data
The 16.5× imbalance ratio between Mid and Junior is handled through balanced class weights at training time.
Limitations
- Optimized for German job listings only — performance on other languages is not validated
Junioris the weakest class (F1 0.893) due to limited training data (747 samples) and a genuinely ambiguous Junior↔Mid boundary in German job ads, where "1-3 Jahre Erfahrung" often overlaps with the Mid grade definition- Slight upward bias on grade boundaries: when uncertain between
MidandSenior(orSeniorandLead+), the model tends to predict the higher grade — visible as higher recall than precision forSeniorandLead+ Lead+includes what was originally labeled asPrincipal. For applications that need to distinguish Principal/Staff/Chief-level roles specifically, apply title-based post-processing on top of the model output- The model classifies the seniority required by the job ad, not the actual seniority of any candidate
- Inputs longer than 512 tokens are truncated — title and the first paragraph of the description carry most of the seniority signal anyway
- The model is trained on German job listings and has learned to prioritize legally-defined seniority markers (Werkstudent, Immatrikulationsbescheinigung, Praktikum, Ausbildung) above explicit title keywords. For listings where the title and description conflict, the model will follow the strongest unambiguous signal, which usually means description-level legal markers override title-level grade keywords. This matches German HR practice but may differ from how other markets handle title-based classification.
Related Models
A typical JobBlast pipeline: the main model assigns the top-level category, IT-tagged listings are passed to the it model for fine-grained IT-role classification, and the seniority model runs in parallel on every listing to attach an experience grade — giving a (category, sub_category, seniority) triple per vacancy.
