Всем привет! Меня зовут Максим. Я NLP‑инженер в red_mad_robot и авторTelegram‑канала Максим Максимов // IT, AI. Сегодня я расскажу о том, как решать задачу NER на практике. Теории будет по минимуму — вместо неё разберёмся, как решать задачу руками: подходы, ресурсы, код на Python.
Сегодня в меню:
- Что такое NER
- Понимание целей и задач
- Работа с данными
- Моделирование
- Создание сервиса на основе модели
Давайте начинать!
Что такое NER
Начнем с определения.
Named Entity Recognition (NER) — задача распознавания именованных сущностей. Это задача из области обработки естественного языка (NLP), суть которой — находить и классифицировать именованные сущности в тексте. Примерами сущностей могут быть: локации, ФИО человека, даты, различные персональные данные (ИНН, паспорт,...).
Эта NLP‑задача имеет множество практических применений. Например:
- Обнаружение персональных данных в тексте (автоматическое скрытие имён и адресов в документах перед их публикацией (обезличивание по 152-ФЗ));
- Поиск ключевых навыков в резюме, по которым система поймёт, подходите ли вы под вакансию;
- Извлечение номера заказа и города доставки в чат‑ботах поддержки для быстрой обработки обращений;
Отлично, мы с вами кратко разобрались, что такое NER и с чем его едят. Давайте теперь на реальном примере пройдём основные этапы решения этой задачи.
Понимание целей и задач
Прежде чем ворошить данные и писать код, стоит разобраться в задаче: понять домен, формат данных и ожидания заказчика. Это сэкономит время на всех последующих этапах.
Представим: к нам пришёл клиент с просьбой разработать модуль для HR‑системы, который поможет рекрутерам находить наиболее релевантных кандидатов по входящим резюме и добавлять их в базу.
Пообщавшись с рекрутерами, мы поняли, что наиболее релевантными считаются резюме, в которых указаны нужные навыки и ожидаемая зарплата.
После обсуждений в команде решили пробовать решать задачу через NER.
Пообщавшись с рекрутерами, мы выяснили, что на вход будут поступать резюме кандидатов в форматах PDF и DOC.
Далее необходимо задать нашим заказчикам несколько вопросов, которые позволят нам лучше решить задачу.
Вопрос про домен.Из начального описания это понятно — это будут текстовые описания резюме, которые содержат информацию о профессиональном опыте кандидата. В них будет содержаться информация о человеке (ФИО, место проживания), о том, где человек работал, какие задачи выполнял, с какими технологиями работал, ожидаемая должность, ожидаемая ЗП.
Вопрос про язык.Пообщавшись с клиентом, мы узнали, что резюме будут приходить только на русском языке.
Также мы уточнили, в каком формате им нужны выходные данные из сервиса. Заказчик сказал, что им требуется следующая информация о потенциальном кандидате: имя, фамилия, email, номер, список навыков, ожидаемая ЗП.
Итак, у нас достаточно информации, чтобы зафиксировать, с чем мы работаем:
Резюме кандидатов (профессиональный опыт, навыки, контакты)
Формат входных данных
Извлекаемые сущности
Имя, Фамилия, Email, Телефон, Навыки, Ожидаемая ЗП
Вопросов по задаче еще можно задать много: объем обрабатываемых данных, какие интеграции должны быть у модуля и так далее. Здесь мы сосредоточились на тех вещах, которые могут потребоваться для решения задачи NER.
Формализуем задачу. На вход поступает файл резюме в формате PDF или DOC. Из него мы извлекаем текст, а затем с помощью NER‑модели находим в нём нужные сущности: имя, фамилию, email, телефон, навыки и ожидаемую заработную плату. В конечном итоге модуль отдает найденные сущности на дальнейшую обработку.
Работа с данными
Далее мы переходим к работе с данными. Это самый важный этап при решении задачи NER (да и в целом любой NLP-задачи). Всегда стоит помнить фразу: «Garbage in — garbage out».
На прошлом шаге мы рассмотрели задачу и поняли, что нам требуется. Обратим внимание на те сущности, которые нам необходимо извлечь: имя, фамилия, email, телефон, навыки, ожидаемая ЗП. Именно эти сущности и будет находить модель в тексте.
Важно зафиксировать точные определения для каждой сущности, чтобы в дальнейшем однозначно размечать их в тексте.
Личное имя кандидата
Фамилия кандидата
Адрес электронной почты
Номер телефона (в любом формате)
Профессиональные навыки, технологии, инструменты
Ожидаемая ЗП
Желаемый уровень заработной платы (включая сумму и валюту)
Как вы могли заметить, мы добавили столбец «Тег». Эти теги мы зададим для NER‑модели, чтобы она возвращала их для найденных сущностей в тексте.
Переходим к конкретному формату данных, с которыми будет работать модель. Существуют различные форматы разметки:
Мы будем использовать схемуBIO, так как в резюме навыки часто перечисляются подряд без разделителей — например, «Python SQL Docker» — и тег B‑SKILL на каждом слове позволяет модели понять, что это три отдельных навыка, а не один (как с IO форматом).
Поиск готового датасета
Первым шагом попробуем найти подходящий датасет для нашей задачи в открытых источниках. Для этого можно использовать несколько платформ.
Начнём сHugging Face. Для тех, кто не знает: Hugging Face — это популярная платформа в области ИИ, которая содержит большое количество open‑source‑моделей и датасетов для различных задач.
Попробуем найти здесь датасет для нашей задачи.
Все датасеты можно увидеть во вкладке Datasets. Настроим немного фильтры для нашей задачи. Мы решаем через NER — значит, в фильтре Task следует выбрать «Token Classification». Далее, так как наши резюме будут на русском — выставим русский язык во вкладке «Languages». Таким образом, мы получим датасеты на русском языке для задачи NER. Далее я попытался через поиск найти датасеты по резюме, но таких не оказалось.
Если же убрать фильтр на язык, всего останется 16 датасетов по запросу resume
Далее я попробовал сделать DeepResearch через Claude, чтобы он также попробовал поискать датасеты на Hugging Face. Ничего на русском языке он не нашёл. Но всё же я заприметил один интересныйдатасет.
Это датасет, в котором содержатся 5000 резюме на английском, а также размеченные навыки (SKILLS) для каждого из них. Решил приберечь его, чтобы попробовать перевести часть на русский для своей задачи.
Далее я провёл те же поиски на:
- Kaggle
- Zenodo
- Google Dataset Search
Есть ещё много различных площадок с данными. Это одни из самых популярных. В итоге конкретно для своей задачи я так и не нашёл датасета — только схожий на английском, который упоминал выше. В целом, так чаще всего и происходит. Чем специфичнее задача, тем меньше вероятность, что будут найдены данные. Но всегда есть шанс найти схожий датасет, который можно будет модернизировать или переиспользовать.
Далее рассмотрим вариант подготовки данных, когда мы их размечаем вручную.
Разметка в Label Studio
В качестве инструмента разметки можно использоватьLabel Studio.
Это популярная open‑source‑платформа для разметки данных. Она позволяет размечать данные для большого количества задач:
Она же включает средства для разметки NER‑датасетов — что нам и подходит.
Давайте рассмотрим процесс разметки.
В связи с некоторыми нюансами по работе с реальными резюме (персональные данные) я решил продемонстрировать сам подход разметки. Эти резюме в примерах я сгенерировал или взял из открытых источников (писал выше). Главная цель — показать, как вы можете работать с разметкой, когда у вас будут реальные данные.
Давайте я покажу, как настроить работу Label Studio для NER.
Первым делом необходимо её установить. Сделать это можно через pip. Продемонстрирую этот процесс на Windows. У вас должен быть установлен Python.
Для этого создайте новую виртуальную среду. Откройте командную строку и введите команду
Активируйте среду
Далее установим Label Studio командой
После установки запустить Label Studio можно командой
Запустится сервис Label Studio. Доступ к нему можно получить перейдя по ссылкеhttp://localhost:8080/. Откроется страница авторизации:
Нажмите на Sign Up, создайте аккаунт, а после авторизуйтесь.
Вы попадете на главную страницу Label Studio.
Далее создадим проект для нашей задачи. Нажмите на «Create Project»
После этого вы можете ввести имя проекта и описание (опционально)
Далее необходимо загрузить данные. Я столкнулся со следующими проблемами:
- Label Studio не предоставляет встроенного парсера текста из PDF — поэтому необходимо самим вытащить текст оттуда.
- Если загружать в Label Studio.txt‑файлы, то он не может их прочитать, а показывает просто ссылку.
Решил я эти проблемы тем, что написал простенький парсер текста из PDF, а после перевёл их в определённый формат JSON, который принимает Label Studio и хорошо считывает.
Скидываю вам скрипт для этого:
Перед запуском, поместите все ваши pdf файлы в папку, а после запустите его командой
После этого создастся JSON, который загружается в Label Studio на странице «Data Import»
Далее определим формат данных. Для этого перейдите в «Labeling Setup». В ней выберете область «Natural Language Processing», а после задачу «Named Entity Recognition».
Далее необходимо добавить теги для разметки. Наши теги: NAME, SURNAME, EMAIL, PHONE, SKILL, SALARY. Чтобы теги добавились в проект — введите их в поле «Add label names» каждый с новой строки и нажмите «Add».
После добавления для удобства можно добавить цвета для каждого тега. Обычно я выставляю различные цвета, чтобы они не смешивались при разметке и их можно было быстро отличить, и вам советую.
После этого начинается «самое интересное» — разметка. Чтобы разметить в тексте нужные теги — необходимо выбрать нужные тег (кликнуть на него), а после выделить нужный фрагмент в тексте под этот тег. Для ускорения я использую клавиатуру — нажимая «1», «2»,... — можно переключаться между тегами.
Таким образом, шаг за шагом происходит разметка всех текстовых примеров по нужным тегам.
После каждой итерации разметки (а лучше раз в минуту) не забывайте нажимать «Submit», чтобы сохранить разметку.
Далее. После разметки датасета необходимо его выгрузить.
Как обсуждали выше, нам необходимы данные в формате BIO. В Label Studio нет встроенного функционала для выгрузки данных в таком формате.
Мы бы могли заранее выбрать теги в таком формате и размечать:
Мне показалось это неудобным и замедляющим разметку. Поэтому я пришёл к следующему решению — оставить теги в Label Studio как есть и выгружать датасет в доступном формате, скриптом переведя его в формат BIO.
Чтобы выгрузить данные, нужно на странице проекта нажать кнопку "Export", а после выбрать нужный формат. Я выберу JSON.
После этого датасет можно форматировать в BIO при помощи скрипта:
Это можно сделать командой
Таким образом мы получим Excel табличку с разметкой в формате:
- text: «Алексей Смирнов знает Python зп 150000»
- bio_tags: ['B‑NAME', 'B‑SURNAME', 'O', 'B‑SKILL', 'O', 'B‑SALARY']
Давайте сделаем небольшой промежуточный итог — этот подход с ручной разметкой является самым ресурсозатратным, но в то же время самым качественным подходом к подготовке данных. Выше я продемонстрировал, как это сделать при помощи Label Studio. Далее перейдём к подходу создания синтетических данных.
Генерация данных
В этом разделе мы рассмотрим подходы к генерации данных для нашей задачи.
Первый способ, который я использовал — это перевод найденных ранее датасетов с английского на русский язык и последующий прогон этих данных через LLM для формирования разметки.
Я взял небольшое количество резюме издатасетана английском языке, перевёл их на русский язык. Так как мы решали задачу для русского языка, помимо перевода самого текста заменяли иностранные имена и фамилии на русские. При переводе возникали и другие потери — email и телефоны иногда коверкались, название некоторых технологий тоже переводились (питон). Поэтому после перевода требовалась дополнительная валидация (тоже LLM). Далее из переведённых резюме я извлёк нужные мне сущности (NAME, SURNAME, EMAIL, PHONE, SKILL, SALARY). После этого на основе найденных сущностей я формировал BIO‑разметку в формате: [«word», «word», …] — [«O», «B‑...», …]
Для перевода и извлечения сущностей я использовал gpt-4o‑mini по API. В целом, модель показала хорошее качество извлечения и не допускала много ошибок. Кстати, насчёт ошибок — возникали моменты, когда LLM не размечала какие‑то теги или размечала лишнего. Например, организации добавлялись в SKILL (Oracle как работодатель, а не как продукт), человеческие языки размечались как навыки (Английский, Хинди), в токены попадали markdown‑артефакты. Также была проблема — одна и та же технология (например Java) в одном документе помечалась как SKILL, а в другом оставалась без разметки. Здесь я решал проблему частичным ручным просмотром данных и их исправлением, а также просил Claude проанализировать датасет и исправить ошибки.
В целом генерация NER‑датасета — непростая задача для LLM. На моей практике при создании датасета такого рода модель часто ошибается, пропускает какие‑то метки или делает неверную разметку. Решал я это ручным просмотром и исправлением разметки с помощью той же LLM. Помимо этого, использовал regex для доразметки пропущенных EMAIL и PHONE — LLM их иногда просто не замечала, а регулярное выражение ловит 100% (если не сильно коверкаются эти значения). Также важным шагом было размечать навыки (SKILL) везде в тексте (и в описании опыта, и в секции навыков), а не только в секции «Навыки» — иначе модель учится не «что такое скилл», а «что стоит после слова Навыки».
Вторую часть данных я подготовил используя другой подход — просил LLM сгенерировать текст для резюме с нужными мне тегами (NAME, SURNAME, EMAIL, PHONE, SKILL, SALARY), которые я сформировал для неё заранее.
А именно — были сформированы различные роли (DevOps, QA, Data Scientist…) и под каждую роль были сформированы свои SKILLS. Для остальных тегов я использовал библиотекуFaker— библиотека которая упрощает создание фиктивных персональных данных (почта, номер, имя..). Для структурного разнообразия я подготовил 6 разных шаблонов резюме (классическое, минималистичное, навыки в центре внимания, профиль‑ориентированное и др.) — чтобы модель не переобучалась на одну структуру документа.
В конечном итоге я сформировал 114 документов и разметку для каждого. Для production‑обучения этого объёма недостаточно — на практике речь идёт о тысячах размеченных примеров, — но для демонстрации этого хватит. Распределение тегов было следующее:
Видим сильный дисбаланс — тег SKILL сильно преобладает над другими. Но в целом, для резюме это нормально, потому что резюме в основном состоит из описания навыков человека.
Закончу эту часть напоминанием, что я лишь демонстрирую подходы, которые можно использовать для создания такого рода датасетов. Здесь я хотел бы выделить то, что если всё же вы генерируете данные, то уделяйте достаточно времени проверке их качества. Например, для этого я делал себе небольшой сервис, в котором просматривал часть разметки после генерации и подправлял в местах, где были ошибки. Таким образом, датасет становился качественнее.
Моделирование
Далее перейдём к обучению модели. Для решения задачи NER наиболее популярной архитектурой является BERT. Я решил в качестве эксперимента обучить различные BERT‑модели и сравнить их точность.
На всякий случай сделаю ремарку: здесь я описываю основные этапы обучения в упрощённом виде. На практике, как правило, требуется более тщательная настройка гиперпараметров, подготовки данных и выбора метрик.
Для обучения можно использовать бесплатные сервисы вроде Kaggle или Google Colab, либо арендовать GPU воблаке. Для нашего эксперимента был выбран второй вариант. В качестве GPU взял RTX 3090 на 24 Гб VRAM.
Я решил сравнить следующие модели для решения нашей задачи:
- google‑bert/bert‑base‑multilingual‑cased
- xlm‑roberta‑base
- xlm‑roberta‑large
- ai‑forever/ruBert‑base
- ai‑forever/ruRoberta‑large
- distilbert/distilbert‑base‑multilingual‑cased
Давайте шаг за шагом пройдемся по коду обучения.
Сначала импортируем необходимые библиотеки
Из основного — обучение я буду проводить при помощи библиотеки transformers, метрики будут браться из seqeval.
Далее выгрузим наши данные из excel в удобный формат
После этого производим маппинг тегов.
Это необходимо для того, чтобы перевести наши теги в числовой вид — который понимает нейросеть (tag2id), а также для того, чтобы перевести предсказания модели в понятные нам теги (id2tag).
После этого производится чанкование. Это нужно потому, что иногда резюме может содержать большое количество текста, которое просто не поместится в контекстное окно BERT.
Далее разбиваем датасет на обучающую и валидационную выборки.
Переводим наши данные в Dataset формат
Определяем модели, которые будем обучать
После определю вспомогательные функции:
Эта функция производит выравнивание между исходной токенизацией в датасете и токенизатором модели, потому что токенизация датасета может совершенно не совпадать с тем, как токенизирует модель.
Функция для подсчёта метрик. В качестве метрики мы будем отслеживать F1. Для этого используем функцию f1_score из библиотеки seqeval. Если вы хотите подробнее узнать о метриках NER — загляните в мою предыдущуюстатью.
Функция, которая возвращает более подробную информацию по метрикам (classification report).
Далее код цикла обучения
Основные этапы обучения:
- В цикле перебираем каждую выбранную предобученную модель.
- Для каждой модели токенизируем данные и производим выравнивание.
- Задаём настройки обучения: размер батча, количество эпох, скорость обучения, валидация после каждой эпохи, папка для сохранения результатов,...
- Обучаем модель через стандартный Trainer из Hugging Face.
- После обучения считаем F1-меру.
- Освобождаем память GPU, чтобы следующая модель не упала.
После обучения я получил следующий результат.
Таким образом, на моём датасете модель ruRoBERTa‑large показала наилучший результат. На втором месте — bert‑base‑multilingual‑cased. Я решил выбрать bert‑base‑multilingual‑cased в качестве модели для извлечения сущностей из резюме. Почему, спросите вы? Есть же модель, которая показала себя лучше.
Дело в том, что mBERT (bert‑base‑multilingual‑cased) содержит 178M параметров — это вдвое меньше, чем у ruRoBERTa‑large (355M). При этом разница в F1 составила всего ~0.01. Меньшее количество параметров означает более быстрый инференс и меньшие требования к GPU‑памяти (а можно даже на CPU). В нашем случае такой trade‑off между качеством и скоростью оправдан.
Создание сервиса на основе модели
И последний шаг — это создание сервиса поверх нашей модели. Для этого я решил обернуть её в FastAPI, что позволит предоставить API модели и использовать её на различных платформах клиента. Для инференса будет использоваться библиотека transformers.
Давайте пошагово разберем код FastAPI сервера с нашей моделью.
Импортируем нужные библиотеки
Определяем логику токенизации текста, аналогична той, что была в обучающей выборке.
Далее загружаем и инициализируем нашу обученную модель.
Код инференса модели. На вход подается текст, он токенизируется. После происходит чанкинг текста (это нужно потому что BERT имеет размер контекста 512 токенов). Далее происходит сам инференс по фрагментам и сбор найденных тегов в выходные данные.
Здесь мы определяем логику сервиса. Имеет схему входных данных, а также выходных данных для ответа сервера на запрос.
Таким образом, запустив этот сервер командой
Можно обращаться на этот сервер POST запросом с текстом резюме и в ответ получать нужные теги из резюме.
Заключение
Мы с вами шаг за шагом прошлись по этапам решения задачи NER. Я опускал много нюансов, которые могут возникать в процессе. Здесь я делал упор на практическую составляющую, чтобы вы поняли, какие верхнеуровневые шаги необходимо пройти — от получения задачи и формирования данных до обучения модели и написания сервиса.
Спасибо за внимание!
Подписывайтесь намой Telegram‑канал, в котором я также рассказываю интересные вещи об IT и AI технологиях.