russian-oracle commited on
Commit
94adbdf
·
verified ·
1 Parent(s): 6b6d102

Add CoreML vacancy section classifier (mlpackage + tokenizer + bilingual card)

Browse files
README.md ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language:
3
+ - ru
4
+ license: mit
5
+ library_name: coreml
6
+ pipeline_tag: text-classification
7
+ base_model: cointegrated/rubert-tiny
8
+ base_model_relation: finetune
9
+ inference: false
10
+ tags:
11
+ - coreml
12
+ - core-ml
13
+ - text-classification
14
+ - russian
15
+ - rubert
16
+ - rubert-tiny
17
+ - bert
18
+ - ane
19
+ - apple-neural-engine
20
+ - apple-silicon
21
+ - on-device
22
+ - vacancy
23
+ - hr
24
+ - job-postings
25
+ - sequence-classification
26
+ ---
27
+
28
+ # rubert-tiny · Vacancy Section Classifier · CoreML
29
+
30
+ On-device CoreML (Apple Neural Engine) classifier that labels fragments of
31
+ Russian-language job postings into 5 structural sections. Built on
32
+ [`cointegrated/rubert-tiny`](https://huggingface.co/cointegrated/rubert-tiny)
33
+ (11.9M params), exported to a `float16` `.mlpackage` for Apple Silicon.
34
+
35
+ > 🇬🇧 English card below · 🇷🇺 Русская версия ниже ([перейти](#-русская-версия))
36
+
37
+ ---
38
+
39
+ ## 🇬🇧 English
40
+
41
+ ### What it does
42
+
43
+ Given one fragment of a Russian vacancy description, the model predicts which of
44
+ 5 sections it belongs to:
45
+
46
+ | id | label | meaning |
47
+ |----|----------------------|------------------------------------------------------|
48
+ | 0 | `responsibilities` | what the employee will do (задачи / обязанности) |
49
+ | 1 | `requirements` | what the candidate must have (требования / навыки) |
50
+ | 2 | `terms` | conditions of employment (условия / зарплата / ДМС) |
51
+ | 3 | `notes` | meta / "about the company" / soft boilerplate |
52
+ | 4 | `junk` | non-informative noise (routed out of structured data) |
53
+
54
+ It is the structured-extraction stage of an HH.ru vacancy-scouting pipeline,
55
+ where it replaced a heavier Qwen-embedding + cosine + rerank approach at
56
+ ~1–10 ms per vacancy on Apple Silicon.
57
+
58
+ ### Artifact
59
+
60
+ This repository ships the **CoreML artifact only** (no PyTorch weights):
61
+
62
+ - `section_classifier.mlpackage` — `float16`, `ComputeUnit.ALL` (ANE-eligible),
63
+ minimum deployment target macOS 13.
64
+ - `tokenizer.json`, `tokenizer_config.json` — the matching BERT WordPiece
65
+ tokenizer (vocab 29 564). **Required** — the `.mlpackage` consumes token ids,
66
+ not raw text.
67
+
68
+ #### CoreML I/O signature
69
+
70
+ | name | dtype | shape | notes |
71
+ |------------------|--------|---------|-----------------------------------|
72
+ | `input_ids` | int32 | [1, 128] | padded to `max_length=128` |
73
+ | `attention_mask` | int32 | [1, 128] | 1 = real token, 0 = pad |
74
+ | `token_type_ids` | int32 | [1, 128] | all zeros (single segment) |
75
+ | **output** `logits` | float32 | [1, 5] | un-normalized; `argmax` → class |
76
+
77
+ `max_seq_len = 128` and the label names are embedded in the model's
78
+ `user_defined_metadata`.
79
+
80
+ ### Metrics
81
+
82
+ The numbers below were measured on the **source PyTorch model**. The CoreML
83
+ export was then verified at **100% argmax parity** against that source on a
84
+ held-out set of probe texts (max absolute logit difference `0.0026`, expected
85
+ for `float16`), so they carry over to this artifact.
86
+
87
+ **Headline — golden-281 (human-labeled, held-out):**
88
+
89
+ | metric | value |
90
+ |----------------------------------------------|---------------------|
91
+ | Content accuracy (4 meaningful classes) | **76.5%** (176/230) |
92
+ | Full 5-class accuracy (incl. junk routing) | **68.7%** |
93
+ | Junk recall (noise correctly routed out) | **33.3%** (17/51) |
94
+
95
+ This is the metric to trust: 281 fragments labeled by a human, never seen in
96
+ training.
97
+
98
+ <details>
99
+ <summary>In-domain test split (circular — NOT the headline)</summary>
100
+
101
+ Evaluated on the internal test split, which shares the same Claude Opus
102
+ relabeled distribution as the training data, so it overstates real-world
103
+ performance. Reported for monitoring only:
104
+
105
+ | metric | value |
106
+ |------------------|--------|
107
+ | Accuracy | 89.3% |
108
+ | Macro-F1 | 86.9% |
109
+
110
+ Per-class F1 (in-domain): responsibilities 0.789 · requirements 0.760 ·
111
+ terms 0.795 · notes 0.684 · **junk 0.374**.
112
+ </details>
113
+
114
+ ### Usage (Python · coremltools)
115
+
116
+ ```python
117
+ import numpy as np
118
+ import coremltools as ct
119
+ from transformers import AutoTokenizer
120
+
121
+ REPO = "russian-oracle/rubert-tiny-vacancy-section-classifier-coreml"
122
+ LABELS = ["responsibilities", "requirements", "terms", "notes", "junk"]
123
+
124
+ tok = AutoTokenizer.from_pretrained(REPO) # tokenizer.json shipped here
125
+ mlmodel = ct.models.MLModel("section_classifier.mlpackage") # hf download ... locally
126
+
127
+ text = "Опыт работы с Python от 3 лет, знание Django и PostgreSQL."
128
+ enc = tok(text, return_tensors="np", padding="max_length",
129
+ truncation=True, max_length=128)
130
+ ids = enc["input_ids"].astype(np.int32)
131
+ out = mlmodel.predict({
132
+ "input_ids": ids,
133
+ "attention_mask": enc["attention_mask"].astype(np.int32),
134
+ "token_type_ids": enc.get("token_type_ids", np.zeros_like(ids)).astype(np.int32),
135
+ })
136
+ logits = np.asarray(out["logits"]).reshape(-1)
137
+ print(LABELS[int(logits.argmax())]) # → requirements
138
+ # probabilities: softmax(logits)
139
+ ```
140
+
141
+ > `coremltools` needs its native bindings, which ship only with certain CPython
142
+ > builds (a 3.12 wheel works reliably). Run prediction under such an interpreter.
143
+
144
+ **Recommended aggregation (how it is used in production):** split a full
145
+ description into sentence-level chunks (e.g. `razdel` + newline), classify each,
146
+ take the majority label per chunk; `junk` fragments are routed to an "orphans"
147
+ bucket instead of the structured output.
148
+
149
+ ### Usage (Swift · sketch)
150
+
151
+ ```swift
152
+ import CoreML
153
+
154
+ let model = try MLModel(contentsOf: url) // section_classifier.mlpackage (compiled)
155
+ // Provide three [1,128] MLMultiArray(.int32) inputs: input_ids, attention_mask,
156
+ // token_type_ids — produced by a BERT WordPiece tokenizer over the input text.
157
+ // Output "logits" is [1,5]; argmax over the last axis gives the class id.
158
+ ```
159
+
160
+ ### Training
161
+
162
+ - **Base:** `cointegrated/rubert-tiny` (BERT, 312 hidden, 3 layers, vocab 29 564).
163
+ - **Lineage:** multi-stage fine-tune — rubert-tiny → intermediate extractor →
164
+ 4-class → 5-class → **5-class "rechunked"** (this model). Warm-started from the
165
+ previous 5-class checkpoint.
166
+ - **Data:** ~12–13k fragments of Russian IT vacancies, **relabeled by Claude
167
+ Opus** (silver → distilled), re-chunked with a `razdel` sentence splitter +
168
+ newline boundaries.
169
+ - **Objective:** class-weighted cross-entropy (balanced inverse-frequency) to
170
+ counter section imbalance.
171
+ - **Schedule:** 8 epochs with early stopping (patience 3, best ≈ epoch 3),
172
+ batch 32, lr 3e-5, weight decay 0.01, warmup ratio 0.1, linear decay, seed 42,
173
+ `max_length` 128, trained on Apple MPS in fp32.
174
+ - **Export:** coremltools 9.0, `compute_precision=FLOAT16`,
175
+ `compute_units=ALL`, `position_ids` baked as a constant buffer to work around a
176
+ const-fold limitation; verified at 100% argmax parity with the PyTorch source.
177
+
178
+ ### Limitations & bias
179
+
180
+ - **Junk recall is low (33.3%).** The model often keeps noise rather than
181
+ dropping it; `notes` ↔ `junk` is the hardest boundary (junk F1 0.374). Add a
182
+ downstream filter if clean routing matters.
183
+ - **Domain:** trained on Russian **IT** vacancies. Other industries, other
184
+ languages, or non-vacancy text are out of distribution.
185
+ - **Granularity:** classifies a *single fragment*, not a whole posting. Use the
186
+ chunk-then-vote pattern above for full descriptions.
187
+ - **Sequence length:** fixed at 128 tokens; longer fragments are truncated.
188
+ - Labels are distilled from an LLM (Claude Opus), so they inherit its biases.
189
+
190
+ ### License
191
+
192
+ **MIT** — same as the base model `cointegrated/rubert-tiny`.
193
+
194
+ ### Citation
195
+
196
+ ```bibtex
197
+ @misc{rubert_tiny_vacancy_section_classifier_coreml,
198
+ title = {rubert-tiny Vacancy Section Classifier (CoreML)},
199
+ author = {russian-oracle},
200
+ year = {2026},
201
+ note = {Fine-tuned from cointegrated/rubert-tiny; CoreML/ANE export},
202
+ url = {https://huggingface.co/russian-oracle/rubert-tiny-vacancy-section-classifier-coreml}
203
+ }
204
+ ```
205
+
206
+ Base model:
207
+
208
+ ```bibtex
209
+ @misc{dale2021rubert_tiny,
210
+ title = {rubert-tiny},
211
+ author = {Dale, David (cointegrated)},
212
+ url = {https://huggingface.co/cointegrated/rubert-tiny}
213
+ }
214
+ ```
215
+
216
+ ---
217
+
218
+ ## 🇷🇺 Русская версия
219
+
220
+ ### Что делает
221
+
222
+ По одному фрагменту русскоязычного описания вакансии модель предсказывает, к
223
+ какой из 5 структурных секций он относится:
224
+
225
+ | id | метка | смысл |
226
+ |----|----------------------|------------------------------------------------------|
227
+ | 0 | `responsibilities` | что сотрудник будет делать (задачи / обязанности) |
228
+ | 1 | `requirements` | что требуется от кандидата (требования / навыки) |
229
+ | 2 | `terms` | условия работы (зарплата / ДМС / график) |
230
+ | 3 | `notes` | мета / «о компании» / мягкий boilerplate |
231
+ | 4 | `junk` | неинформативный шум (выводится из структуры) |
232
+
233
+ Это этап структурной разметки в пайплайне скаутинга вакансий HH.ru, где модель
234
+ заменила более тяжёлую связку Qwen-эмбеддинги + косинус + reranking — при
235
+ ~1–10 мс на вакансию на Apple Silicon.
236
+
237
+ ### Артефакт
238
+
239
+ В репозитории — **только CoreML-артефакт** (без PyTorch-весов):
240
+
241
+ - `section_classifier.mlpackage` — `float16`, `ComputeUnit.ALL` (с поддержкой
242
+ ANE), минимальная цель развёртывания macOS 13.
243
+ - `tokenizer.json`, `tokenizer_config.json` — соответствующий BERT WordPiece
244
+ токенайзер (словарь 29 564). **Обязателен** — `.mlpackage` принимает id
245
+ токенов, а не сырой текст.
246
+
247
+ #### Сигнатура входов/выходов CoreML
248
+
249
+ | имя | тип | форма | примечание |
250
+ |------------------|--------|---------|-----------------------------------|
251
+ | `input_ids` | int32 | [1, 128] | паддинг до `max_length=128` |
252
+ | `attention_mask` | int32 | [1, 128] | 1 — реальный токен, 0 — паддинг |
253
+ | `token_type_ids` | int32 | [1, 128] | все нули (один сегмент) |
254
+ | **выход** `logits` | float32 | [1, 5] | без нормализации; `argmax` → класс |
255
+
256
+ `max_seq_len = 128` и имена классов зашиты в `user_defined_metadata` модели.
257
+
258
+ ### Метрики
259
+
260
+ Цифры ниже измерены на **исходной PyTorch-модели**. CoreML-экспорт затем
261
+ проверен на **100% совпадение argmax** с этим источником на отложенном наборе
262
+ проб (макс. абс. разница логитов `0.0026`, что нормально для `float16`), поэтому
263
+ они переносятся на этот артефакт.
264
+
265
+ **Headline — golden-281 (ручная разметка, held-out):**
266
+
267
+ | метрика | значение |
268
+ |--------------------------------------------------|---------------------|
269
+ | Content-accuracy (4 содержательных класса) | **76.5%** (176/230) |
270
+ | Полная 5-class accuracy (включая роутинг junk) | **68.7%** |
271
+ | Junk recall (корректно отсеянный шум) | **33.3%** (17/51) |
272
+
273
+ Это и есть метрика, которой стоит доверять: 281 фрагмент, размеченный человеком
274
+ и не виденный при обучении.
275
+
276
+ <details>
277
+ <summary>Внутренний test-split (циркулярный — НЕ headline)</summary>
278
+
279
+ Оценка на внутреннем тестовом сплите, у которого то же Claude Opus-распределение
280
+ разметки, что и у обучающих данных, поэтому он завышает реальное качество.
281
+ Приведён только для мониторинга:
282
+
283
+ | метрика | значение |
284
+ |------------------|--------|
285
+ | Accuracy | 89.3% |
286
+ | Macro-F1 | 86.9% |
287
+
288
+ Per-class F1 (in-domain): responsibilities 0.789 · requirements 0.760 ·
289
+ terms 0.795 · notes 0.684 · **junk 0.374**.
290
+ </details>
291
+
292
+ ### Использование (Python · coremltools)
293
+
294
+ ```python
295
+ import numpy as np
296
+ import coremltools as ct
297
+ from transformers import AutoTokenizer
298
+
299
+ REPO = "russian-oracle/rubert-tiny-vacancy-section-classifier-coreml"
300
+ LABELS = ["responsibilities", "requirements", "terms", "notes", "junk"]
301
+
302
+ tok = AutoTokenizer.from_pretrained(REPO) # tokenizer.json в этом репо
303
+ mlmodel = ct.models.MLModel("section_classifier.mlpackage") # hf download ... локально
304
+
305
+ text = "Опыт работы с Python от 3 лет, знание Django и PostgreSQL."
306
+ enc = tok(text, return_tensors="np", padding="max_length",
307
+ truncation=True, max_length=128)
308
+ ids = enc["input_ids"].astype(np.int32)
309
+ out = mlmodel.predict({
310
+ "input_ids": ids,
311
+ "attention_mask": enc["attention_mask"].astype(np.int32),
312
+ "token_type_ids": enc.get("token_type_ids", np.zeros_like(ids)).astype(np.int32),
313
+ })
314
+ logits = np.asarray(out["logits"]).reshape(-1)
315
+ print(LABELS[int(logits.argmax())]) # → requirements
316
+ # вероятности: softmax(logits)
317
+ ```
318
+
319
+ > `coremltools` требует нативных биндингов, которые есть только в части сборок
320
+ > CPython (надёжно работает wheel под 3.12). Запускайте предсказание под таким
321
+ > интерпретатором.
322
+
323
+ **Рекомендуемая агрегация (как используется в продакшене):** разбейте полное
324
+ описание на фрагменты по предложениям (например, `razdel` + переводы строк),
325
+ классифицируйте каждый, возьмите мажоритарную метку на чанк; фрагменты `junk`
326
+ отправляются в корзину «orphans», а не в структурированный вывод.
327
+
328
+ ### Обучение
329
+
330
+ - **База:** `cointegrated/rubert-tiny` (BERT, 312 hidden, 3 слоя, словарь 29 564).
331
+ - **Происхождение:** многоступенчатый файн-тюн — rubert-tiny → промежуточный
332
+ extractor → 4-class → 5-class → **5-class «rechunked»** (эта модель).
333
+ Warm-start с предыдущего 5-class чекпойнта.
334
+ - **Данные:** ~12–13 тыс. фрагментов русских IT-вакансий, **переразмечены
335
+ Claude Opus** (silver → дистилляция), перечанкованы сплиттером `razdel` по
336
+ предложениям + границам строк.
337
+ - **Лосс:** взвешенная по классам кросс-энтропия (balanced inverse-frequency)
338
+ против дисбаланса секций.
339
+ - **Расписание:** 8 эпох с ранней ос��ановкой (patience 3, лучшая ≈ эпоха 3),
340
+ batch 32, lr 3e-5, weight decay 0.01, warmup ratio 0.1, линейный спад, seed 42,
341
+ `max_length` 128, обучение на Apple MPS в fp32.
342
+ - **Экспорт:** coremltools 9.0, `compute_precision=FLOAT16`,
343
+ `compute_units=ALL`, `position_ids` зашит как константный буфер (обход
344
+ ограничения const-fold); проверено на 100% argmax-parity с PyTorch-источником.
345
+
346
+ ### Ограничения и смещения
347
+
348
+ - **Низкий junk recall (33.3%).** Модель чаще оставляет шум, чем отсеивает его;
349
+ граница `notes` ↔ `junk` — самая сложная (junk F1 0.374). Если важен чистый
350
+ роутинг — добавьте downstream-фильтр.
351
+ - **Домен:** обучена на русских **IT**-вакансиях. Другие отрасли, языки или
352
+ не-вакансионный текст — вне распределения.
353
+ - **Гранулярность:** классифицирует *отдельный фрагмент*, а не вакансию целиком.
354
+ Для полных описаний используйте схему chunk-then-vote выше.
355
+ - **Длина последовательности:** фиксированные 128 токенов; длиннее — обрезается.
356
+ - Метки дистиллированы из LLM (Claude Opus) и наследуют её смещения.
357
+
358
+ ### Лицензия
359
+
360
+ **MIT** — как и у базовой модели `cointegrated/rubert-tiny`.
361
+
362
+ ### Цитирование
363
+
364
+ См. BibTeX в английской секции выше.
section_classifier.mlpackage/Data/com.apple.CoreML/model.mlmodel ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e150127ec4c530d1453f50c6b609e7f58d1143632a54dbb3f307918f1b9436ce
3
+ size 40872
section_classifier.mlpackage/Data/com.apple.CoreML/weights/weight.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d3b134748113e168e281da6ea9784184e9f55d174061570d2365798a05190f41
3
+ size 23336048
section_classifier.mlpackage/Manifest.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "fileFormatVersion": "1.0.0",
3
+ "itemInfoEntries": {
4
+ "059072FA-F4AC-4B94-A284-8A828ED6C0D2": {
5
+ "author": "com.apple.CoreML",
6
+ "description": "CoreML Model Specification",
7
+ "name": "model.mlmodel",
8
+ "path": "com.apple.CoreML/model.mlmodel"
9
+ },
10
+ "E6CAA176-ED54-4F45-B849-90F22B9627EB": {
11
+ "author": "com.apple.CoreML",
12
+ "description": "CoreML Model Weights",
13
+ "name": "weights",
14
+ "path": "com.apple.CoreML/weights"
15
+ }
16
+ },
17
+ "rootModelIdentifier": "059072FA-F4AC-4B94-A284-8A828ED6C0D2"
18
+ }
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "backend": "tokenizers",
3
+ "cls_token": "[CLS]",
4
+ "do_basic_tokenize": true,
5
+ "do_lower_case": false,
6
+ "is_local": true,
7
+ "local_files_only": false,
8
+ "mask_token": "[MASK]",
9
+ "max_length": 512,
10
+ "model_max_length": 512,
11
+ "never_split": null,
12
+ "pad_to_multiple_of": null,
13
+ "pad_token": "[PAD]",
14
+ "pad_token_type_id": 0,
15
+ "padding_side": "right",
16
+ "sep_token": "[SEP]",
17
+ "stride": 0,
18
+ "strip_accents": null,
19
+ "tokenize_chinese_chars": true,
20
+ "tokenizer_class": "BertTokenizer",
21
+ "truncation_side": "right",
22
+ "truncation_strategy": "longest_first",
23
+ "unk_token": "[UNK]"
24
+ }