File size: 18,115 Bytes
94adbdf
 
 
 
 
 
 
 
 
047624c
ca58d44
 
94adbdf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8bab2b6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94adbdf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
---
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.

<details>
<summary>In-domain test split (circular — NOT the headline)</summary>

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**.
</details>

### 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 фрагмент, размеченный человеком
и не виденный при обучении.

<details>
<summary>Внутренний test-split (циркулярный — НЕ headline)</summary>

Оценка на внутреннем тестовом сплите, у которого то же 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**.
</details>

### Использование (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 в английской секции выше.