---
language:
- ru
license: mit
library_name: coreml
pipeline_tag: text-classification
base_model: cointegrated/rubert-tiny
base_model_relation: finetune
inference: false
num_parameters: 11900000
datasets:
- russian-oracle/vacancy-section-classifier-ru
tags:
- coreml
- core-ml
- text-classification
- russian
- rubert
- rubert-tiny
- bert
- ane
- apple-neural-engine
- apple-silicon
- on-device
- vacancy
- hr
- job-postings
- sequence-classification
model-index:
- name: rubert-tiny-vacancy-section-classifier-coreml
results:
- task:
type: text-classification
name: Text Classification
dataset:
name: Vacancy Section Classifier Dataset (RU) — golden-281 (human-labeled)
type: russian-oracle/vacancy-section-classifier-ru
split: test
metrics:
- type: accuracy
value: 0.765
name: Content Accuracy (4 classes, golden-281)
- type: accuracy
value: 0.687
name: 5-class Accuracy (incl. junk, golden-281)
- type: recall
value: 0.333
name: Junk Recall (golden-281)
- task:
type: text-classification
name: Text Classification
dataset:
name: Vacancy Section Classifier Dataset (RU) — in-domain test split
type: russian-oracle/vacancy-section-classifier-ru
split: test
metrics:
- type: accuracy
value: 0.893
name: Accuracy (in-domain)
- type: f1
value: 0.869
name: Macro F1 (in-domain)
- type: f1
value: 0.789
name: F1 responsibilities
- type: f1
value: 0.760
name: F1 requirements
- type: f1
value: 0.795
name: F1 terms
- type: f1
value: 0.684
name: F1 notes
- type: f1
value: 0.374
name: F1 junk
---
# rubert-tiny · Vacancy Section Classifier · CoreML
On-device CoreML (Apple Neural Engine) classifier that labels fragments of
Russian-language job postings into 5 structural sections. Built on
[`cointegrated/rubert-tiny`](https://huggingface.co/cointegrated/rubert-tiny)
(11.9M params), exported to a `float16` `.mlpackage` for Apple Silicon.
> 🇬🇧 English card below · 🇷🇺 Русская версия ниже ([перейти](#-русская-версия))
---
## 🇬🇧 English
### What it does
Given one fragment of a Russian vacancy description, the model predicts which of
5 sections it belongs to:
| id | label | meaning |
|----|----------------------|------------------------------------------------------|
| 0 | `responsibilities` | what the employee will do (задачи / обязанности) |
| 1 | `requirements` | what the candidate must have (требования / навыки) |
| 2 | `terms` | conditions of employment (условия / зарплата / ДМС) |
| 3 | `notes` | meta / "about the company" / soft boilerplate |
| 4 | `junk` | non-informative noise (routed out of structured data) |
It is the structured-extraction stage of an HH.ru vacancy-scouting pipeline,
where it replaced a heavier Qwen-embedding + cosine + rerank approach at
~1–10 ms per vacancy on Apple Silicon.
### Artifact
This repository ships the **CoreML artifact only** (no PyTorch weights):
- `section_classifier.mlpackage` — `float16`, `ComputeUnit.ALL` (ANE-eligible),
minimum deployment target macOS 13.
- `tokenizer.json`, `tokenizer_config.json` — the matching BERT WordPiece
tokenizer (vocab 29 564). **Required** — the `.mlpackage` consumes token ids,
not raw text.
#### CoreML I/O signature
| name | dtype | shape | notes |
|------------------|--------|---------|-----------------------------------|
| `input_ids` | int32 | [1, 128] | padded to `max_length=128` |
| `attention_mask` | int32 | [1, 128] | 1 = real token, 0 = pad |
| `token_type_ids` | int32 | [1, 128] | all zeros (single segment) |
| **output** `logits` | float32 | [1, 5] | un-normalized; `argmax` → class |
`max_seq_len = 128` and the label names are embedded in the model's
`user_defined_metadata`.
### Metrics
The numbers below were measured on the **source PyTorch model**. The CoreML
export was then verified at **100% argmax parity** against that source on a
held-out set of probe texts (max absolute logit difference `0.0026`, expected
for `float16`), so they carry over to this artifact.
**Headline — golden-281 (human-labeled, held-out):**
| metric | value |
|----------------------------------------------|---------------------|
| Content accuracy (4 meaningful classes) | **76.5%** (176/230) |
| Full 5-class accuracy (incl. junk routing) | **68.7%** |
| Junk recall (noise correctly routed out) | **33.3%** (17/51) |
This is the metric to trust: 281 fragments labeled by a human, never seen in
training.
In-domain test split (circular — NOT the headline)
Evaluated on the internal test split, which shares the same Claude Opus
relabeled distribution as the training data, so it overstates real-world
performance. Reported for monitoring only:
| metric | value |
|------------------|--------|
| Accuracy | 89.3% |
| Macro-F1 | 86.9% |
Per-class F1 (in-domain): responsibilities 0.789 · requirements 0.760 ·
terms 0.795 · notes 0.684 · **junk 0.374**.
### Usage (Python · coremltools)
```python
import numpy as np
import coremltools as ct
from transformers import AutoTokenizer
REPO = "russian-oracle/rubert-tiny-vacancy-section-classifier-coreml"
LABELS = ["responsibilities", "requirements", "terms", "notes", "junk"]
tok = AutoTokenizer.from_pretrained(REPO) # tokenizer.json shipped here
mlmodel = ct.models.MLModel("section_classifier.mlpackage") # hf download ... locally
text = "Опыт работы с Python от 3 лет, знание Django и PostgreSQL."
enc = tok(text, return_tensors="np", padding="max_length",
truncation=True, max_length=128)
ids = enc["input_ids"].astype(np.int32)
out = mlmodel.predict({
"input_ids": ids,
"attention_mask": enc["attention_mask"].astype(np.int32),
"token_type_ids": enc.get("token_type_ids", np.zeros_like(ids)).astype(np.int32),
})
logits = np.asarray(out["logits"]).reshape(-1)
print(LABELS[int(logits.argmax())]) # → requirements
# probabilities: softmax(logits)
```
> `coremltools` needs its native bindings, which ship only with certain CPython
> builds (a 3.12 wheel works reliably). Run prediction under such an interpreter.
**Recommended aggregation (how it is used in production):** split a full
description into sentence-level chunks (e.g. `razdel` + newline), classify each,
take the majority label per chunk; `junk` fragments are routed to an "orphans"
bucket instead of the structured output.
### Usage (Swift · sketch)
```swift
import CoreML
let model = try MLModel(contentsOf: url) // section_classifier.mlpackage (compiled)
// Provide three [1,128] MLMultiArray(.int32) inputs: input_ids, attention_mask,
// token_type_ids — produced by a BERT WordPiece tokenizer over the input text.
// Output "logits" is [1,5]; argmax over the last axis gives the class id.
```
### Training
- **Base:** `cointegrated/rubert-tiny` (BERT, 312 hidden, 3 layers, vocab 29 564).
- **Lineage:** multi-stage fine-tune — rubert-tiny → intermediate extractor →
4-class → 5-class → **5-class "rechunked"** (this model). Warm-started from the
previous 5-class checkpoint.
- **Data:** ~12–13k fragments of Russian IT vacancies, **relabeled by Claude
Opus** (silver → distilled), re-chunked with a `razdel` sentence splitter +
newline boundaries.
- **Objective:** class-weighted cross-entropy (balanced inverse-frequency) to
counter section imbalance.
- **Schedule:** 8 epochs with early stopping (patience 3, best ≈ epoch 3),
batch 32, lr 3e-5, weight decay 0.01, warmup ratio 0.1, linear decay, seed 42,
`max_length` 128, trained on Apple MPS in fp32.
- **Export:** coremltools 9.0, `compute_precision=FLOAT16`,
`compute_units=ALL`, `position_ids` baked as a constant buffer to work around a
const-fold limitation; verified at 100% argmax parity with the PyTorch source.
### Limitations & bias
- **Junk recall is low (33.3%).** The model often keeps noise rather than
dropping it; `notes` ↔ `junk` is the hardest boundary (junk F1 0.374). Add a
downstream filter if clean routing matters.
- **Domain:** trained on Russian **IT** vacancies. Other industries, other
languages, or non-vacancy text are out of distribution.
- **Granularity:** classifies a *single fragment*, not a whole posting. Use the
chunk-then-vote pattern above for full descriptions.
- **Sequence length:** fixed at 128 tokens; longer fragments are truncated.
- Labels are distilled from an LLM (Claude Opus), so they inherit its biases.
### License
**MIT** — same as the base model `cointegrated/rubert-tiny`.
### Citation
```bibtex
@misc{rubert_tiny_vacancy_section_classifier_coreml,
title = {rubert-tiny Vacancy Section Classifier (CoreML)},
author = {russian-oracle},
year = {2026},
note = {Fine-tuned from cointegrated/rubert-tiny; CoreML/ANE export},
url = {https://huggingface.co/russian-oracle/rubert-tiny-vacancy-section-classifier-coreml}
}
```
Base model:
```bibtex
@misc{dale2021rubert_tiny,
title = {rubert-tiny},
author = {Dale, David (cointegrated)},
url = {https://huggingface.co/cointegrated/rubert-tiny}
}
```
---
## 🇷🇺 Русская версия
### Что делает
По одному фрагменту русскоязычного описания вакансии модель предсказывает, к
какой из 5 структурных секций он относится:
| id | метка | смысл |
|----|----------------------|------------------------------------------------------|
| 0 | `responsibilities` | что сотрудник будет делать (задачи / обязанности) |
| 1 | `requirements` | что требуется от кандидата (требования / навыки) |
| 2 | `terms` | условия работы (зарплата / ДМС / график) |
| 3 | `notes` | мета / «о компании» / мягкий boilerplate |
| 4 | `junk` | неинформативный шум (выводится из структуры) |
Это этап структурной разметки в пайплайне скаутинга вакансий HH.ru, где модель
заменила более тяжёлую связку Qwen-эмбеддинги + косинус + reranking — при
~1–10 мс на вакансию на Apple Silicon.
### Артефакт
В репозитории — **только CoreML-артефакт** (без PyTorch-весов):
- `section_classifier.mlpackage` — `float16`, `ComputeUnit.ALL` (с поддержкой
ANE), минимальная цель развёртывания macOS 13.
- `tokenizer.json`, `tokenizer_config.json` — соответствующий BERT WordPiece
токенайзер (словарь 29 564). **Обязателен** — `.mlpackage` принимает id
токенов, а не сырой текст.
#### Сигнатура входов/выходов CoreML
| имя | тип | форма | примечание |
|------------------|--------|---------|-----------------------------------|
| `input_ids` | int32 | [1, 128] | паддинг до `max_length=128` |
| `attention_mask` | int32 | [1, 128] | 1 — реальный токен, 0 — паддинг |
| `token_type_ids` | int32 | [1, 128] | все нули (один сегмент) |
| **выход** `logits` | float32 | [1, 5] | без нормализации; `argmax` → класс |
`max_seq_len = 128` и имена классов зашиты в `user_defined_metadata` модели.
### Метрики
Цифры ниже измерены на **исходной PyTorch-модели**. CoreML-экспорт затем
проверен на **100% совпадение argmax** с этим источником на отложенном наборе
проб (макс. абс. разница логитов `0.0026`, что нормально для `float16`), поэтому
они переносятся на этот артефакт.
**Headline — golden-281 (ручная разметка, held-out):**
| метрика | значение |
|--------------------------------------------------|---------------------|
| Content-accuracy (4 содержательных класса) | **76.5%** (176/230) |
| Полная 5-class accuracy (включая роутинг junk) | **68.7%** |
| Junk recall (корректно отсеянный шум) | **33.3%** (17/51) |
Это и есть метрика, которой стоит доверять: 281 фрагмент, размеченный человеком
и не виденный при обучении.
Внутренний test-split (циркулярный — НЕ headline)
Оценка на внутреннем тестовом сплите, у которого то же Claude Opus-распределение
разметки, что и у обучающих данных, поэтому он завышает реальное качество.
Приведён только для мониторинга:
| метрика | значение |
|------------------|--------|
| Accuracy | 89.3% |
| Macro-F1 | 86.9% |
Per-class F1 (in-domain): responsibilities 0.789 · requirements 0.760 ·
terms 0.795 · notes 0.684 · **junk 0.374**.
### Использование (Python · coremltools)
```python
import numpy as np
import coremltools as ct
from transformers import AutoTokenizer
REPO = "russian-oracle/rubert-tiny-vacancy-section-classifier-coreml"
LABELS = ["responsibilities", "requirements", "terms", "notes", "junk"]
tok = AutoTokenizer.from_pretrained(REPO) # tokenizer.json в этом репо
mlmodel = ct.models.MLModel("section_classifier.mlpackage") # hf download ... локально
text = "Опыт работы с Python от 3 лет, знание Django и PostgreSQL."
enc = tok(text, return_tensors="np", padding="max_length",
truncation=True, max_length=128)
ids = enc["input_ids"].astype(np.int32)
out = mlmodel.predict({
"input_ids": ids,
"attention_mask": enc["attention_mask"].astype(np.int32),
"token_type_ids": enc.get("token_type_ids", np.zeros_like(ids)).astype(np.int32),
})
logits = np.asarray(out["logits"]).reshape(-1)
print(LABELS[int(logits.argmax())]) # → requirements
# вероятности: softmax(logits)
```
> `coremltools` требует нативных биндингов, которые есть только в части сборок
> CPython (надёжно работает wheel под 3.12). Запускайте предсказание под таким
> интерпретатором.
**Рекомендуемая агрегация (как используется в продакшене):** разбейте полное
описание на фрагменты по предложениям (например, `razdel` + переводы строк),
классифицируйте каждый, возьмите мажоритарную метку на чанк; фрагменты `junk`
отправляются в корзину «orphans», а не в структурированный вывод.
### Обучение
- **База:** `cointegrated/rubert-tiny` (BERT, 312 hidden, 3 слоя, словарь 29 564).
- **Происхождение:** многоступенчатый файн-тюн — rubert-tiny → промежуточный
extractor → 4-class → 5-class → **5-class «rechunked»** (эта модель).
Warm-start с предыдущего 5-class чекпойнта.
- **Данные:** ~12–13 тыс. фрагментов русских IT-вакансий, **переразмечены
Claude Opus** (silver → дистилляция), перечанкованы сплиттером `razdel` по
предложениям + границам строк.
- **Лосс:** взвешенная по классам кросс-энтропия (balanced inverse-frequency)
против дисбаланса секций.
- **Расписание:** 8 эпох с ранней остановкой (patience 3, лучшая ≈ эпоха 3),
batch 32, lr 3e-5, weight decay 0.01, warmup ratio 0.1, линейный спад, seed 42,
`max_length` 128, обучение на Apple MPS в fp32.
- **Экспорт:** coremltools 9.0, `compute_precision=FLOAT16`,
`compute_units=ALL`, `position_ids` зашит как константный буфер (обход
ограничения const-fold); проверено на 100% argmax-parity с PyTorch-источником.
### Ограничения и смещения
- **Низкий junk recall (33.3%).** Модель чаще оставляет шум, чем отсеивает его;
граница `notes` ↔ `junk` — самая сложная (junk F1 0.374). Если важен чистый
роутинг — добавьте downstream-фильтр.
- **Домен:** обучена на русских **IT**-вакансиях. Другие отрасли, языки или
не-вакансионный текст — вне распределения.
- **Гранулярность:** классифицирует *отдельный фрагмент*, а не вакансию целиком.
Для полных описаний используйте схему chunk-then-vote выше.
- **Длина последовательности:** фиксированные 128 токенов; длиннее — обрезается.
- Метки дистиллированы из LLM (Claude Opus) и наследуют её смещения.
### Лицензия
**MIT** — как и у базовой модели `cointegrated/rubert-tiny`.
### Цитирование
См. BibTeX в английской секции выше.