От неструктурированного текста к структурированным графам знаний
Делиться

До появления LLM-библиотек существовала SpaCy, которая фактически являлась основной библиотекой для обработки естественного языка как для начинающих, так и для опытных пользователей. Она позволяла легко освоить NLP, даже если вы не были экспертом в глубоком обучении. Однако с появлением ChatGPT и других LLM-библиотек она, похоже, отошла на второй план.
Хотя модели LLM, такие как Claude или Gemini, могут автоматически выполнять множество задач в области НЛП, не всегда хочется идти на кулачный бой с ракетницей. GliNER возглавляет возвращение более компактных, специализированных моделей для классических методов НЛП, таких как извлечение сущностей и связей. Она достаточно легкая, чтобы работать на процессоре, и в то же время достаточно мощная, чтобы вокруг нее сформировалось процветающее сообщество.
Выпущенный в начале этого года GliNER2 представляет собой значительный шаг вперед. Если оригинальный GliNER был сосредоточен на распознавании сущностей (породив различные ответвления, такие как GLiREL для отношений и GLiClass для классификации), то GliNER2 объединяет распознавание именованных сущностей , классификацию текста , извлечение отношений и извлечение структурированных данных в единую структуру.
Ключевое изменение в GliNER2 заключается в использовании подхода, основанного на схеме , который позволяет декларативно определять требования к извлечению данных и выполнять несколько задач в рамках одного вызова функции вывода. Несмотря на расширенные возможности, модель остается эффективной с точки зрения использования ресурсов процессора, что делает ее идеальным решением для преобразования неструктурированного текста в чистые данные без дополнительных затрат, связанных с использованием больших языковых моделей.
Как энтузиаст графов знаний в Neo4j, я был особенно заинтересован в недавно добавленной функции извлечения структурированных данных с помощью метода extract_json. Хотя извлечение сущностей и отношений само по себе ценно, возможность определить схему и извлечь структурированный JSON непосредственно из текста — это то, что меня действительно вдохновляет. Это идеально подходит для обработки данных в графах знаний, где структурированный и согласованный результат имеет важное значение.

В этой статье мы оценим возможности GliNER2, в частности модели fastino/gliner2-large-v1 , и сосредоточимся на том, насколько хорошо она может помочь нам создавать чистые, структурированные графы знаний.
Код доступен на GitHub.
выбор набора данных
Мы не проводим здесь формальных тестов, а лишь быстро проверяем, на что способен GliNER2. Вот наш тестовый текст, взятый со страницы Ады Лавлейс в Википедии:
Августа Ада Кинг, графиня Лавлейс (10 декабря 1815 — 27 ноября 1852), также известная как Ада Лавлейс, была английским математиком и писательницей, известной прежде всего своей работой над предложенным Чарльзом Бэббиджем механическим универсальным компьютером — аналитической машиной. Она первой признала, что у этой машины есть применение за пределами чистых вычислений. Лавлейс часто считают первым программистом. Лавлейс была единственной законной дочерью поэта лорда Байрона и реформатора Анны Изабеллы Милбанк. Все её сводные братья и сёстры, другие дети лорда Байрона, родились вне брака от других женщин. Лорд Байрон расстался со своей женой через месяц после рождения Ады и навсегда покинул Англию. Он умер в Греции во время Греческой войны за независимость, когда ей было восемь лет. Леди Байрон беспокоилась о воспитании дочери и поощряла интерес Лавлейс к математике и логике, чтобы предотвратить развитие у неё предполагаемого безумия отца. Несмотря на это, Лавлейс сохранила интерес к своему отцу, назвав одного сына Байроном, а другого — Гордоном, в честь второго имени отца. По её просьбе Лавлейс была похоронена рядом с отцом. Хотя в детстве она часто болела, Лавлейс усердно продолжала учёбу. В 1835 году она вышла замуж за Уильяма Кинга. Кинг был бароном и в 1838 году получил титул виконта Окхема и 1-го графа Лавлейса. Фамилия Лавлейс была выбрана потому, что Ада происходила из прекратившего своё существование рода баронов Лавлейс. Таким образом, титул, присвоенный её мужу, сделал Аду графиней Лавлейс.
Это внушительный объем текста, состоящий из 322 токенов. Давайте начнем.
Извлечение сущностей
Начнём с извлечения сущностей. По своей сути, извлечение сущностей — это процесс автоматического определения и классификации ключевых сущностей в тексте , таких как люди, места, организации или технические понятия . GliNER1 уже хорошо справлялся с этой задачей, но GliNER2 идёт ещё дальше, позволяя добавлять описания к типам сущностей, что даёт более точный контроль над тем, что будет извлечено.
entities = extractor.extract_entities( text, { «Person»: «Имена людей, включая дворянские титулы.»», «Location»: «Страны, города или географические места.»», «Invention»: «Машины, устройства или технологические изобретения.»», «Event»: «Исторические события, войны или конфликты.» } )
Результаты следующие:

Предоставление пользовательских описаний для каждого типа сущностей помогает устранить неоднозначность и повысить точность извлечения. Это особенно полезно для широких категорий, таких как «Событие», где сама по себе модель может не знать, следует ли включать войны, церемонии или личные достижения. Добавление исторических событий, войн или конфликтов уточняет предполагаемый охват.
Извлечение связей
Извлечение отношений позволяет определить связи между парами сущностей в тексте. Например, в предложении «Стив Джобс основал Apple» модель извлечения отношений определит связь «Основал» между сущностями «Стив Джобс» и «Apple» .
В GLiNER2 вы определяете только те типы отношений, которые хотите извлечь, поскольку нельзя ограничить, какие типы сущностей разрешены в качестве начала или конца каждого отношения. Это упрощает интерфейс, но может потребовать постобработки для фильтрации нежелательных пар.
Здесь я провел простой эксперимент, добавив определения как псевдонима, так и отношения same_as.
relations = extractor.extract_relations( text, { «parent_of»: «Человек является родителем другого человека», «married_to»: «Человек состоит в браке с другим человеком», «worked_on»: «Человек внес вклад в изобретение или работал над ним», «invented»: «Человек создал или предложил изобретение», «alias»: «Сущность является псевдонимом, прозвищем, титулом или альтернативной ссылкой на другую сущность», «same_as»: «Сущность является псевдонимом, прозвищем, титулом или альтернативной ссылкой на другую сущность» } )
Результаты следующие:

В результате извлечения были правильно определены ключевые связи: лорд Байрон и Анна Изабелла Милбанк как родители Ады, её брак с Уильямом Кингом, Бэббидж как изобретатель аналитической машины и работа Ады над ней. Примечательно, что модель определила Августу Аду Кинг как псевдоним Ады Лавлейс, но связь same_as не была обнаружена, несмотря на идентичное описание. Выбор, по-видимому, не случаен, поскольку модель всегда заполняет псевдоним, но никогда связь same_as. Это подчеркивает, насколько чувствительно извлечение связей к именованию меток, а не только к описаниям.
Удобно, что GLiNER2 позволяет комбинировать несколько типов извлечения в одном вызове, так что вы можете получить типы сущностей наряду с типами отношений за один проход. Однако операции независимы: извлечение сущностей не фильтрует и не ограничивает, какие сущности будут отображаться в извлечении отношений, и наоборот. Рассматривайте это как параллельное выполнение обоих извлечений, а не как конвейер.
schema = (extractor.create_schema() .entities({ «Person»: «Имена людей, включая дворянские титулы.»», «Location»: «Страны, города или географические места.»», «Invention»: «Машины, устройства или технологические изобретения.»», «Event»: «Исторические события, войны или конфликты.» }) .relations({ «parent_of»: «Человек является родителем другого человека», «married_to»: «Человек состоит в браке с другим человеком», «worked_on»: «Человек внес вклад в изобретение или работал над ним», «invented»: «Человек создал или предложил изобретение», «alias»: «Сущность является псевдонимом, прозвищем, титулом или альтернативной ссылкой на другую сущность» }) ) results = extractor.extract(text, schema)
Результаты следующие:

Объединенное извлечение теперь дает нам типы сущностей, которые различаются цветом. Однако несколько узлов кажутся изолированными (Греция, Англия, Греческая война за независимость), поскольку не каждая извлеченная сущность участвует в обнаруженной связи.
Извлечение структурированного JSON
Пожалуй, наиболее мощной функцией является извлечение структурированных данных с помощью extract_json. Это имитирует функциональность структурированного вывода в таких LLM-системах, как ChatGPT или Gemini, но работает исключительно на процессоре. В отличие от извлечения сущностей и связей, это позволяет определять произвольные поля и извлекать их в структурированные записи. Синтаксис соответствует шаблону field_name::type::description, где type — это строка или список.
results = extractor.extract_json( text, { «person»: [ «name::str», «gender::str::male or female», «alias::str::brief summary of included information about the person», «description::str», «birth_date::str», «death_date::str», «parent_of::str», «married_to::str» ] } )
Здесь мы экспериментируем с некоторым пересечением: alias, parent_of и married_to также можно моделировать как отношения. Стоит изучить, какой подход лучше подходит для вашего случая. Интересным дополнением является поле описания, которое немного расширяет границы: оно ближе к генерации сводки, чем к чистому извлечению.
Результаты следующие:
{ «person»: [ { «name»: «Augusta Ada King», «gender»: null, «alias»: «Ada Lovelace», «description»: «English mathematician and writer», «birth_date»: «10 December 1815», «death_date»: «27 November 1852», «parent_of»: «Ada Lovelace», «married_to»: «William King» }, { «name»: «Charles Babbage», «gender»: null, «alias»: null, «description»: null, «birth_date»: null, «death_date»: null, «parent_of»: «Ada Lovelace», «married_to»: null }, { «name»: «Lord Byron», «gender»: null, «alias»: null, «description»: «reformer», «birth_date»: null, «death_date»: null, «parent_of»: «Ada Lovelace», «married_to»: null }, { «name»: «Anne Isabella Milbanke», «gender»: null, «alias»: null, «description»: «reformer», «birth_date»: null, «death_date»: null, «parent_of»: «Ada Lovelace», «married_to»: null }, { «name»: «William King», «gender»: null, «alias»: null, «description»: null, «birth_date»: null, «death_date»: null, «parent_of»: «Ada Lovelace», «married_to»: null } ] }
Результаты выявляют некоторые ограничения. Все поля, относящиеся к полу, имеют значение null, хотя Ада явно названа дочерью, модель не делает вывод о том, что она женского пола. Поле описания содержит только поверхностные фразы («английский математик и писатель», «реформатор»), а не содержательные резюме, что не подходит для рабочих процессов, подобных GraphRAG от Microsoft, которые полагаются на более подробные описания сущностей. Также имеются явные ошибки: Чарльз Бэббидж и Уильям Кинг ошибочно отмечены как parent_of Ады, а лорд Байрон обозначен как реформатор (это Анна Изабелла). Эти ошибки с parent_of не возникали при извлечении отношений, поэтому, возможно, это лучший метод в данном случае. В целом, результаты показывают, что модель отлично справляется с извлечением, но испытывает трудности с рассуждениями или выводом, что, вероятно, является компромиссом из-за ее компактного размера.
Кроме того, все атрибуты являются необязательными, что логично и упрощает задачу. Однако следует быть осторожным, поскольку иногда атрибут имени может быть равен null, что делает запись недействительной. Наконец, мы могли бы использовать что-то вроде PyDantic для проверки результатов и преобразования их в соответствующие типы, такие как числа с плавающей запятой или даты, а также для обработки недействительных результатов.
Построение графов знаний
Поскольку GLiNER2 позволяет извлекать несколько типов данных за один проход, мы можем объединить все вышеперечисленные методы для построения графа знаний. Вместо запуска отдельных конвейеров для извлечения сущностей, отношений и структурированных данных, единое определение схемы обрабатывает все три типа данных. Это упрощает переход от необработанного текста к богатому, взаимосвязанному представлению.
schema = (extractor.create_schema() .entities({ «Person»: «Имена людей, включая дворянские титулы.»», «Location»: «Страны, города или географические места.»», «Invention»: «Машины, устройства или технологические изобретения.»», «Event»: «Исторические события, войны или конфликты.» }) .relations({ «parent_of»: «Человек является родителем другого человека», «married_to»: «Человек состоит в браке с другим человеком», «worked_on»: «Человек внес вклад в изобретение или работал над ним», «invented»: «Человек создал или предложил изобретение», }) .structure(«person») .field(«name», dtype=»str») .field(«alias», dtype=»str») .field(«description», dtype=»str») .field(«birth_date», dtype=»str») ) results = extractor.extract(text, schema)
Способ сопоставления этих выходных данных с вашим графом (узлами, связями, свойствами) зависит от вашей модели данных. В этом примере мы используем следующую модель данных:

Обратите внимание, что мы включаем в граф также исходный фрагмент текста, что позволяет нам извлекать и ссылаться на исходный материал при запросе к графу, обеспечивая более точные и отслеживаемые результаты. Импорт Cypher выглядит следующим образом:
import_cypher_query = «»» // Создание узла Chunk из текста CREATE (c:Chunk {text: $text}) // Создание узлов Person со свойствами WITH c CALL (c) { UNWIND $data.person AS p WITH p WHERE p.name IS NOT NULL MERGE (n:__Entity__ {name: p.name}) SET n.description = p.description, n.birth_date = p.birth_date MERGE (c)-[:MENTIONS]->(n) WITH p, n WHERE p.alias IS NOT NULL MERGE (m:__Entity__ {name: p.alias}) MERGE (n)-[:ALIAS_OF]->(m) } // Динамическое создание узлов сущностей с базовой меткой __Entity__ + динамической меткой CALL (c) { UNWIND keys($data.entities) AS label UNWIND $data.entities[label] AS entityName MERGE (n:__Entity__ {name: entityName}) SET n:$(label) MERGE (c)-[:MENTIONS]->(n) } // Динамическое создание связей CALL (c) { UNWIND keys($data.relation_extraction) AS relType UNWIND $data.relation_extraction[relType] AS rel MATCH (a:__Entity__ {name: rel[0]}) MATCH (b:__Entity__ {name: rel[1]}) MERGE (a)-[:$(toUpper(relType))]->(b) } RETURN distinct 'import completed' AS result «»»
Запрос Cypher берет результаты из выходных данных GliNER2 и сохраняет их в Neo4j. Мы также могли бы включить встраивания для фрагментов текста, сущностей и так далее.
Краткое содержание
GliNER2 — это шаг в правильном направлении для извлечения структурированных данных. С появлением LLM-моделей легко использовать ChatGPT или Claude всякий раз, когда нужно извлечь информацию из текста, но это часто излишне. Запуск модели с миллиардом параметров для извлечения нескольких сущностей и связей кажется нерациональным, когда эту задачу могут выполнить более мелкие специализированные инструменты на процессоре.
GliNER2 объединяет распознавание именованных сущностей, извлечение связей и вывод структурированного JSON в единую структуру. Он хорошо подходит для таких задач, как построение графов знаний, где требуется согласованное извлечение на основе схемы, а не генерация произвольного шаблона.
Хотя у этой модели есть свои ограничения, она лучше всего подходит для прямого извлечения информации, а не для вывода или рассуждений, и результаты могут быть непоследовательными. Однако прогресс от оригинальной GliNER1 до GliNER2 обнадеживает, и, будем надеяться, мы увидим дальнейшее развитие в этой области. Во многих случаях модель, ориентированная на извлечение информации, превосходит модель LLM, которая делает гораздо больше, чем необходимо.
Код доступен на GitHub.
Источник: towardsdatascience.com



























