Извлечение полей из IT-вакансий: Qwen3-1.7B, LoRA
Модель читает текст вакансии на русском (портал, сайт или пост в Telegram) и отвечает одним JSON-объектом: роль, грейд, навыки, зарплата из текста, формат работы, города, требуемый опыт, тип занятости, язык текста. Не найдено в тексте, значит null или пустой список: модель обучена не угадывать.
Часть проекта ru-it-jobs-radar: ежедневный сбор IT-вакансий России из «Работы в России» (trudvsem), Хабр Карьеры, SuperJob и публичных Telegram-каналов, без hh.ru. Корпус проекта размечает AWQ-версия этих весов (4 бита, в этом репозитории её нет, качество в таблице ниже), по её полям строятся витрины и сайт с рынком. Разметка постов Telegram ещё идёт. Попробовать на своём тексте можно в Space.
Файлы
| Файл | Что | МиБ |
|---|---|---|
model.safetensors и конфиги в корне | веса после слияния LoRA с базой, bf16 | 3 282 |
adapter/ | LoRA-адаптер для Qwen/Qwen3-1.7B (PEFT) | 66 |
gguf/qwen3_1p7b_r16_clean-Q8_0.gguf | llama.cpp, 8 бит | 1 749 |
gguf/qwen3_1p7b_r16_clean-Q4_K_M.gguf | llama.cpp, 4 бита, на нём работает Space | 1 056 |
dataset_meta.json | системный промпт, длина входа, хэши обучающих данных |
GGUF собраны llama.cpp b11052 из слитых весов без imatrix.
Схема ответа
{"role": "data_scientist", "grade": "middle", "skills": ["Python", "SQL", "PyTorch"],
"salary": {"from": 250000, "to": 350000, "currency": "RUB", "gross_net": "net", "period": "month"},
"work_format": "hybrid", "cities": ["Москва"], "experience_years": {"min": 3, "max": null},
"employment_type": "labor_contract", "lang": "ru"}
Роли: ml_engineer, data_scientist, mlops, data_engineer, data_analyst, business_analyst, system_analyst, backend, frontend, fullstack, mobile, software_engineer, developer_1c, embedded, devops, qa, sysadmin, support, security, designer, manager, other_it, non_it. Полное описание полей и правила разметки: docs/annotation_guide.md.
Как запускать
Промпт должен быть ровно таким, как при обучении: системный промпт из dataset_meta.json, шаблон чата Qwen3 с выключенным thinking, вход обрезан до первых 1 536 токенов. Декодирование жадное, до 512 новых токенов.
import json
from huggingface_hub import hf_hub_download
from transformers import AutoModelForCausalLM, AutoTokenizer
repo = "alekseevpavel04/ru-it-vacancy-extractor-qwen3-1.7b"
tok = AutoTokenizer.from_pretrained(repo)
model = AutoModelForCausalLM.from_pretrained(repo, dtype="auto")
meta = json.load(open(hf_hub_download(repo, "dataset_meta.json"), encoding="utf-8"))
text = "Python-разработчик в команду данных. Москва, гибрид. Опыт от 3 лет, SQL, Airflow. 250-300 тыс. руб."
ids = tok(text, add_special_tokens=False, return_offsets_mapping=True)
if len(ids.input_ids) > meta["max_input_tokens"]:
text = text[: ids.offset_mapping[meta["max_input_tokens"] - 1][1]]
messages = [{"role": "system", "content": meta["system_prompt"]}, {"role": "user", "content": text}]
prompt = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True, enable_thinking=False)
inputs = tok(prompt, return_tensors="pt", add_special_tokens=False)
out = model.generate(**inputs, max_new_tokens=512, do_sample=False)
print(tok.decode(out[0, inputs.input_ids.shape[1] :], skip_special_tokens=True))
С llama.cpp: llama-server -m qwen3_1p7b_r16_clean-Q4_K_M.gguf, промпт собирается так же и отправляется текстом в /completion с temperature: 0.
Как обучена
- Опубликована модель по рецепту первой итерации SFT (E9 в docs/EXPERIMENTS.md), обученная заново на серебряной разметке, пересобранной после чистки персональных данных в корпусе 12.10: ни в одном обучающем входе нет имён, которые чистка теперь удаляет. Все числа карточки относятся к этой модели. Модель того же рецепта на прежней разметке не публикуется, её числа в журнале экспериментов.
- База
Qwen/Qwen3-1.7B, QLoRA: ранг 16, все линейные слои внимания и MLP, 2 эпохи, одна RTX 3070 на 8 ГБ, 3 773 с обучения. - 10 866 обучающих примеров из 11 427 размеченных автоматически (серебряная разметка): 5 719 вакансий «Работы в России» и Хабр Карьеры и 5 147 постов Telegram.
- Разметка порталов: правила и регулярки по тексту и структурным полям источника. Разметка Telegram: значения, в которых согласились две модели-учителя (Qwen3-8B и YandexGPT-5-Lite-8B), иначе правила. Состав выбран на dev, где каждая из четырёх заранее заданных схем сравнивалась с правилами.
- Вакансии и посты эталона и их дубли в обучение не попали.
Качество
Эталон: вакансии и посты, предразмеченные LLM в два независимых прохода по одной инструкции с третьим проходом по расхождениям и проверенные мной вручную целиком. Ниже dev-выборки, на которых подбиралось всё; test считается один раз в конце. Macro F1 по полям схемы, в скобках 95% бутстрап-интервал.
dev: 165 вакансий «Работы в России» и Хабр Карьеры.
| Система | Macro F1 | Валидный JSON |
|---|---|---|
| Эта модель, bf16 | 0,899 [0,878; 0,918] | 165 из 165 |
| Эта модель, GGUF Q8_0 | 0,912 [0,890; 0,932] | 165 из 165 |
| Эта модель, GGUF Q4_K_M | 0,904 [0,881; 0,925] | 165 из 165 |
| Эта модель, AWQ с грамматикой JSON-схемы (vLLM, ею размечен корпус) | 0,903 [0,881; 0,924] | 165 из 165 |
| Правила и регулярки | 0,900 [0,878; 0,918] | |
| Учитель Qwen3-8B Q4_K_M | 0,822 [0,785; 0,855] | 164 из 165 |
| Учитель YandexGPT-5-Lite-8B Q4_K_M | 0,826 [0,795; 0,853] | 165 из 165 |
| Qwen3-1.7B без дообучения, тот же промпт | 0,134 [0,087; 0,183] | 32 из 165 |
| Qwen3-4B без дообучения, тот же промпт | 0,437 [0,398; 0,476] | 123 из 165 |
tg_dev: 54 поста Telegram.
| Система | Macro F1 | Валидный JSON |
|---|---|---|
| Эта модель, bf16 | 0,832 [0,760; 0,879] | 54 из 54 |
| Эта модель, GGUF Q4_K_M | 0,847 [0,787; 0,891] | 54 из 54 |
| Эта модель, AWQ с грамматикой JSON-схемы (vLLM, ею размечен корпус) | 0,857 [0,786; 0,902] | 54 из 54 |
| Правила и регулярки | 0,653 [0,582; 0,706] | |
| Учитель Qwen3-8B Q4_K_M | 0,818 [0,745; 0,867] | 54 из 54 |
| Qwen3-1.7B без дообучения, тот же промпт | 0,214 [0,119; 0,293] | 14 из 54 |
| Qwen3-4B без дообучения, тот же промпт | 0,484 [0,410; 0,550] | 44 из 54 |
Test, один запуск после всех решений: 440 вакансий «Работы в России» и Хабр Карьеры и 130 постов Telegram. Ни одно решение по этим числам не принималось.
| Система | test, macro F1 | test, валидный JSON | tg_test, macro F1 | tg_test, валидный JSON |
|---|---|---|---|---|
| Эта модель, bf16 | 0,896 [0,882; 0,909] | 439 из 440 | 0,842 [0,810; 0,867] | 130 из 130 |
| Эта модель, GGUF F16 (в репозитории нет, точка отсчёта для квантизации) | 0,882 [0,867; 0,897] | 439 из 440 | 0,847 [0,815; 0,873] | 130 из 130 |
| Эта модель, GGUF Q8_0 | 0,881 [0,866; 0,895] | 440 из 440 | 0,846 [0,815; 0,872] | 130 из 130 |
| Эта модель, GGUF Q4_K_M | 0,883 [0,867; 0,899] | 438 из 440 | 0,852 [0,822; 0,878] | 130 из 130 |
| Эта модель, AWQ с грамматикой JSON-схемы (vLLM, ею размечен корпус) | 0,876 [0,860; 0,891] | 440 из 440 | 0,854 [0,822; 0,879] | 130 из 130 |
| Правила и регулярки | 0,890 [0,874; 0,903] | 0,647 [0,613; 0,677] | ||
| Учитель Qwen3-8B Q4_K_M | 0,825 [0,806; 0,841] | 440 из 440 | 0,791 [0,752; 0,825] | 130 из 130 |
| Учитель YandexGPT-5-Lite-8B Q4_K_M | 0,805 [0,786; 0,821] | 440 из 440 | 0,793 [0,755; 0,824] | 130 из 130 |
| Qwen3-1.7B без дообучения, тот же промпт | 0,108 [0,081; 0,136] | 65 из 440 | 0,184 [0,129; 0,237] | 32 из 130 |
| Qwen3-4B тем же рецептом SFT, на разметке до чистки 12.10 (не публикуется) | 0,895 [0,881; 0,907] | 440 из 440 | 0,834 [0,801; 0,861] | 130 из 130 |
Qwen3-4B без дообучения на test не запускалась: без остановки по схеме она отвечала по 31 с на запись при нехватке видеопамяти, около 3 часов на test. Базовые модели сравнены на dev, в таблице выше.
Разности на тех же записях (95% парный бутстрап):
| Разность macro F1 | test | tg_test |
|---|---|---|
| bf16 минус правила | +0,006 [-0,004; +0,017] | +0,194 [+0,164; +0,225] |
| bf16 минус учитель Qwen3-8B | +0,071 [+0,053; +0,090] | +0,051 [+0,022; +0,080] |
| bf16 минус учитель YandexGPT-5-Lite-8B | +0,091 [+0,072; +0,111] | +0,049 [+0,025; +0,073] |
| bf16 минус 4B того же рецепта | +0,001 [-0,007; +0,010] | +0,007 [-0,009; +0,024] |
| GGUF F16 минус bf16 | -0,014 [-0,026; -0,002] | +0,006 [-0,013; +0,028] |
| GGUF Q8_0 минус GGUF F16 | -0,001 [-0,005; +0,005] | -0,001 [-0,005; +0,002] |
| GGUF Q4_K_M минус GGUF F16 | +0,001 [-0,005; +0,006] | +0,005 [-0,008; +0,017] |
| AWQ со схемой минус GGUF F16 | -0,006 [-0,015; +0,004] | +0,006 [-0,010; +0,022] |
Как читать. На вакансиях порталов модель на уровне правил: разность с правилами на test в пределах интервала, включающего ноль. Для порталов её обучающая разметка и есть правила: там у вакансии есть структурные поля, и правила их читают надёжно. Выигрыш модели на свободном тексте постов Telegram, где правила слабы. Учителя на 8B ниже ученика на обеих тестовых выборках. Переход с bf16 в transformers на GGUF F16 в llama.cpp дал небольшой спад на test порталов, интервал ноль не включает; на tg_test разницы не видно. Квантизация от F16 до Q8_0, Q4_K_M и AWQ меняет macro F1 в пределах интервалов, включающих ноль. 4B того же рецепта не лучше 1.7B ни на одной выборке, поэтому опубликована 1.7B: она меньше и работает на CPU. Базовые модели без дообучения с тем же промптом часто отвечают не JSON по схеме, и дообучение здесь даёт основной прирост.
Ограничения
- Эталон предразмечен LLM и проверен вручную одним человеком. Dev маленький, интервалы широкие. На dev варианты квантизации отличались от bf16 в пределах интервалов, на test порталов GGUF ниже bf16 значимо, хотя и немного.
- Модель видела вакансии и посты с одной вакансией. Резюме, статьи и посты с несколькими вакансиями в обучение не входили.
- Дообучение по предпочтениям (DPO) на 716 парах пробовали, но на карте 8 ГБ оно не уместилось в предел видеопамяти ни в одном из пробных запусков. Опубликованная модель обучена только SFT, и финальный тест считается без DPO.
- Модель знает только то, что написано во входе. Зарплата без валюты остаётся без валюты, gross или net не угадывается.
Данные и лицензия
Лицензия смешанная, подробно в LICENSE.md. Веса производные от Qwen/Qwen3-1.7B (Apache 2.0). Часть обучающей разметки (посты Telegram) взята из ответов YandexGPT-5-Lite-8B-instruct, а его лицензионное соглашение считает модели, обученные на его выходных данных, производными произведениями. Поэтому модель распространяется и на условиях соглашения YandexGPT-5-Lite-8B, его копия лежит в licenses/. Код проекта под MIT.
YandexGPT-5-Lite-8B распространяется на условиях Лицензионного соглашения YandexGPT-5-Lite-8B. Copyright (c) 2025, ООО «ЯНДЕКС». Все права защищены.
Тексты обучающих примеров не публикуются: открытые данные «Работы в России» лежат в датасете проекта, тексты Хабр Карьеры и Telegram остаются только в локальном хранилище проекта.