Назад в блог

Методы извлечения навыков из резюме и вакансий

Разбираем конвейер автоматического извлечения навыков: NER на RuBERT, сопоставление шаблонов через spaCy PhraseMatcher и нормализация через векторные представления SentenceTransformers.

Авторы: slavb18

Методы извлечения навыков из резюме и вакансий

Автоматическое извлечение навыков (skills extraction) - ключевая задача в системах подбора персонала, рекомендации вакансий и анализа рынка труда. На вход поступает неструктурированный текст: раздел "Требования" вакансии или блок "Опыт/Навыки" резюме. На выходе ожидается нормализованный список компетенций, пригодный для поиска, сравнения и аналитики.

В этой статье рассматривается конвейер, реализованный в iskillmatching, который объединяет три взаимодополняющих подхода:

  1. NER на основе LLM - нейросетевое распознавание именованных сущностей.
  2. Сопоставление шаблонов через spaCy - поиск по заранее составленному словарю навыков.
  3. Нормализация через векторные представления - приведение извлечённых вариантов к каноническим формам с помощью семантического сходства.

1. NER на основе LLM (нейросетевое распознавание сущностей)

Что такое NER

Named Entity Recognition (NER) - задача последовательной классификации, при которой каждому токену текста присваивается метка: является ли он частью именованной сущности (например, "технология", "навык", "организация") или нет. Традиционно NER решался с помощью CRF и правил, однако современные трансформерные LLM (Large Language Models) достигают значительно более высокого качества за счёт контекстного понимания текста.

Используемая модель

В ner_utils.py применяется пайплайн HuggingFace Transformers:

from transformers import pipeline

def get_ner_extractor(model_name="dondosss/rubert-finetuned-ner"):
return pipeline(
"token-classification",
model=model_name,
aggregation_strategy="simple"
)

Модель dondosss/rubert-finetuned-ner - это дообученная (fine-tuned) версия RuBERT на задаче NER для русскоязычных текстов. RuBERT - российский аналог BERT, предобученный на большом корпусе русского языка. Дообучение на размеченных текстах резюме и вакансий позволяет модели распознавать технологии, фреймворки и инструменты как именованные сущности.

Параметр aggregation_strategy="simple" обеспечивает автоматическое объединение субтокенов (BPE-фрагментов) в целые слова - без него результат содержал бы артефакты вроде ##Script вместо JavaScript.

Процесс извлечения

def extract_ner_skills(texts, ner_extractor):
results = ner_extractor(texts)
piped_skills = []
for ents in results:
skills = list(set([e["word"].lower() for e in ents]))
piped_skills.append("|".join(skills))
return piped_skills

Модель обрабатывает батч текстов и возвращает для каждого список сущностей. Результат сериализуется в строку с разделителем | для удобного хранения в CSV. Дубликаты устраняются через set.

Достоинства и ограничения

Достоинства Ограничения
Находит навыки без словаря, "из контекста" Может галлюцинировать (ложные сущности)
Справляется с опечатками и морфологией Зависит от качества дообучающей выборки
Работает с аббревиатурами и составными терминами Высокие требования к памяти GPU/CPU

2. Сопоставление шаблонов через spaCy (PhraseMatcher)

Идея подхода

В отличие от NER, который "угадывает" навыки из контекста, PhraseMatcher работает по принципу словаря: задаётся список эталонных фраз, и система ищет их вхождения в тексте. Это делает метод детерминированным - он не ошибается, если навык есть в словаре, и не придумывает несуществующих сущностей.

Реализация в spaCy

from spacy.matcher import PhraseMatcher

def get_spacy_matcher(nlp, skills_list):
matcher = PhraseMatcher(nlp.vocab, attr="LOWER")
patterns = [nlp.make_doc(skill) for skill in skills_list]
matcher.add("SKILL", patterns)
return matcher

Ключевой момент: attr="LOWER" означает регистронезависимое сравнение. nlp.make_doc создаёт spaCy-документ из строки - это быстрее, чем полный парсинг, так как не требует запуска всего NLP-конвейера.

Сам поиск работает за линейное время от длины текста (O(n)), поскольку под капотом spaCy использует оптимизированный хэш-матч по токенам.

Батчевая обработка через nlp.pipe

def extract_spacy_skills(texts, nlp, matcher):
piped_skills = []
for doc in nlp.pipe(texts):
matches = matcher(doc)
seen_skills = set()
extracted_skills = []
for match_id, start, end in matches:
span = doc[start:end]
skill_name = span.text
if skill_name.lower() not in seen_skills:
extracted_skills.append(skill_name)
seen_skills.add(skill_name.lower())
piped_skills.append("|".join(extracted_skills))
return piped_skills

nlp.pipe обрабатывает тексты потоком, что существенно эффективнее цикла с nlp(text), - тексты передаются батчами, токенизация векторизована.

Источник словаря навыков

Словарь загружается из CSV-файла или извлекается как fallback из столбцов skills/stack входных данных:

def extract_fallback_skills(df):
fallback_skills = set()
for col in ["skills", "stack"]:
if col in df.columns:
series = df[col].fillna("").astype(str)
for val in series:
parts = [p.strip().lower() for p in val.split("|") if p.strip()]
fallback_skills.update(parts)
return sorted(list(fallback_skills))

Достоинства и ограничения

Достоинства Ограничения
Высокая точность (precision) при хорошем словаре Не найдёт навык, которого нет в словаре
Детерминированность и воспроизводимость Требует поддержки словаря
Быстрая работа даже на CPU Плохо справляется с вариантами написания

3. Объединение результатов и очистка

Оба метода дают пересекающиеся, но дополняющие друг друга результаты. В clean_utils.py они объединяются и очищаются:

def combine_and_clean(ner_piped, spacy_piped):
ner_set = set(ner_piped.split("|")) if ner_piped else set()
spacy_set = set(spacy_piped.split("|")) if spacy_piped else set()
combined_set = ner_set.union(spacy_set)
cleaned_skills = []
seen = set()
for s in combined_set:
cleaned = clean_skill(s)
if cleaned and cleaned not in seen:
cleaned_skills.append(cleaned)
seen.add(cleaned)
return "|".join(sorted(cleaned_skills))

Функция clean_skill решает несколько задач:

  • Удаление BERT-артефактов: субтокены вида ##ScriptScript.
  • Удаление шума на границах: лишние пробелы, дефисы, скобки, кавычки.
  • Фильтрация стоп-слов: слова "знание", "опыт", "уровень", "разработка" и т.д. не являются навыками и исключаются.

Список стоп-слов (SKILL_STOP_WORDS) специфичен для русскоязычных текстов резюме и вакансий и насчитывает более 50 типичных "пустышек".


4. Нормализация через векторные представления

Проблема, которую решает нормализация

После объединения NER и spaCy-результатов мы получаем "сырые" навыки: "python 3", "python3", "Python", "питон" - это одна и та же технология, но разные строки. Без нормализации они будут учитываться раздельно, что делает невозможным корректный поиск и агрегацию.

Семантические векторы (Sentence Embeddings)

В normalize_utils.py используется библиотека SentenceTransformers:

from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity

class SkillNormalizer:
def __init__(self, skills_data, model_name, threshold=0.6):
self.model = SentenceTransformer(model_name)
self.threshold = threshold
self.skill_embeddings = self.model.encode(
self.skills,
normalize_embeddings=True,
batch_size=128
)

Модель anass1209/resume-job-matcher-all-MiniLM-L6-v2 - это дообученная версия all-MiniLM-L6-v2, адаптированная специально для задачи сопоставления резюме и вакансий. Она преобразует строку навыка в вектор размерностью 384, кодирующий семантический смысл.

Флаг normalize_embeddings=True гарантирует, что все векторы имеют единичную длину. В этом случае косинусное сходство совпадает со скалярным произведением, что ускоряет вычисления.

Алгоритм нормализации

def normalize_batch(self, piped_skills_batch):
for piped_str in piped_skills_batch:
current_skills = [s.strip() for s in piped_str.split("|") if s.strip()]
embeddings = self.model.encode(current_skills, normalize_embeddings=True)
sims = cosine_similarity(embeddings, self.skill_embeddings)
normalized_set = set()
for row in sims:
best_idx = np.argmax(row)
if row[best_idx] >= self.threshold:
normalized_set.add(self.skills[best_idx])
results.append("|".join(sorted(normalized_set)))

Для каждого извлечённого навыка алгоритм:

  1. Строит его эмбеддинг.
  2. Вычисляет косинусное сходство с эмбеддингами всех эталонных навыков.
  3. Выбирает наиболее похожий эталон (argmax).
  4. Принимает его, если сходство превышает порог (threshold=0.8 в extract.py).

Если порог не достигнут, навык отбрасывается - это защита от ложных обобщений (например, чтобы слово "работа" не нормализовалось к "Java"). Класс SkillNormalizer принимает порог как параметр конструктора (по умолчанию 0.6), а в точке вызова в extract.py он переопределяется до 0.8 для получения более строгих совпадений в продакшн-сценарии.

Визуализация работы нормализации

Сырой навык → Эмбеддинг → Ближайший эталон → Сходство
─────────────────────────────────────────────────────────────────────────
"python3" → [0.12, ...] → "Python" → 0.97 ✓
"react.js" → [0.34, ...] → "React" → 0.91 ✓
"разработка на С++" → [0.56, ...] → "C++" → 0.82 ✓
"опыт работы" → [0.78, ...] → "Работа" → 0.51 ✗ (ниже порога)

Достоинства и ограничения

Достоинства Ограничения
Не зависит от точного совпадения строк Требует предобученной модели (RAM/диск)
Обрабатывает опечатки и синонимы Скорость зависит от размера словаря эталонов
Работает для многоязычных текстов Порог подбирается эмпирически

5. Общая архитектура конвейера

┌─────────────┐
Текст вакансии/резюме │ extract.py │
─────────────────────► │ │
└──────┬──────┘
│
┌─────────────────┼─────────────────┐
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ NER (LLM) │ │ spaCy Matcher │
│ RuBERT NER │ │ (PhraseMatcher) │
└──────┬───────┘ └────────┬─────────┘
│ │
└──────────────┬─────────────────────┘
▼
┌─────────────────┐
│ combine_and_clean│
│ (stop words, │
│ dedup, clean) │
└────────┬────────┘
▼
┌─────────────────┐
│ SkillNormalizer │
│ (SentenceTransf.│
│ cosine sim.) │
└────────┬────────┘
▼
Нормализованный список навыков

Каждый шаг сохраняется в отдельный столбец CSV (ner, spacy, combined, normalized), что позволяет отлаживать конвейер и анализировать вклад каждого метода.


6. Сравнение подходов

Критерий NER (LLM) spaCy Matcher Векторная нормализация
Полнота (recall) Высокая Средняя (ограничена словарём) -
Точность (precision) Средняя Высокая Высокая
Скорость Медленно (GPU желателен) Быстро (CPU) Средне
Поддержка Нужно дообучение Нужен актуальный словарь Нужен словарь эталонов
Мультиязычность Зависит от модели Зависит от словаря Зависит от модели

7. Запуск

# Установка зависимостей (spaCy модель для токенизации)
uv run python -m spacy download en_core_web_lg

# Запуск с внешним словарём навыков
uv run -m iskillmatching.extract -i input.csv -o output.csv -s skills.csv -l 500

# Запуск без словаря (навыки извлекаются из столбцов skills/stack)
uv run -m iskillmatching.extract -i input.csv -o output.csv

Входной CSV должен содержать столбец requirement_ru с текстами на русском языке. Результаты записываются в столбцы ner, spacy, combined, normalized.


Материалы

Можно попробовать обучение собственной модели NER на основе NER-Web-App-TensorFlowJS или настройку prompt к LLM через DSPy.

  1. Сравнение различных вариантов NER: arxiv.org/pdf/2407.19816, модель из статьи: dondosss/rubert-finetuned-ner
  2. Модель NER на tensorflow: NER-Web-App-TensorFlowJS
  3. SentenceTransformer based on all-mpnet-base-v2: TechWolf/JobBERT-v2
  4. SentenceTransformer based on all-MiniLM-L6-v2: anass1209/resume-job-matcher-all-MiniLM-L6-v2
  5. Ещё один SentenceTransformer based on all-MiniLM-L6-v2: hetbhagatji09/Job-resume-ner-match-model
  6. Сервис для запуска моделей: michaelfeil/infinity
  7. NER на LLM с DSPy: MarcusElwin/ner-dspy

Исходный код конвейера: iconicompany/iskillmatching


📚 Читайте также

iconicompany

агрегатор аутстаффинга ·
проектная работа для ИТ-специалистов

заказчикам Платформа

Вход для заказчиков — отдельная страница, не витрина для специалистов.

примечания к спецификации
  • из проекта imatching берётся только тема кабинета — функциональность кабинета не переносится
  • цвета — из темы кабинета imatching
  • шрифты — из темы кабинета imatching: Bricolage Grotesque · Public Sans · JetBrains Mono
  • регистр — «воздух публичного сайта», а не плотность кабинета
  • тип продукта — landing, публичный сайт
  • не админка
  • не мобильное приложение
  • ориентир по типу продукта — skillstaff.ru, и только по типу
  • бренд и оформление skillstaff.ru не копируются
  • темы различаются: собственный продукт (эта витрина) и движок (страница «Платформа»)
  • собственный продукт — ИТ-аутстаффинг; витрина сделана для него
  • тендерная история на главную не выносится — она живёт на странице «Платформа»

© 2026 iconicompany

отклик — ссылкой на hh-резюме · без регистрации