Обработка естественного языка — это не только нейронные сети, а данные — это не только строки, числа и перечисления. Область работы с данными простирается намного дальше. К старту флагманского курса по Data Science представляем вашему вниманию перевод из блога разработчиков графовой базы данных neo4j о том, как при помощи SpaCy и Selenium извлечь из книги граф взаимоотношений героев. Подробности и код, как всегда, под катом. Скорее всего, вы уже видели созданный Эндрю Бевериджем граф Game of Thrones. Эндрю построил граф взаимодействий книжных персонажей. Если два персонажа появляются на некотором расстоянии друг от друга в тексте, мы можем предположить, что в книге они как-то связаны или взаимодействуют. Я решил создать подобный проект, но выбрать популярную книгу, не имеющую известной мне стратегии извлечения данных из сети. Так родился проект по извлечению графа персонажей книги “Гарри Поттер и философский камень”. Я провёл много экспериментов, чтобы решить, как лучше поступить. Попробовал большинство моделей распознавания именованных сущностей с открытым исходным кодом, сравнивая их работу, но ни одна из них не была достаточно хороша. К счастью, на странице фэндома Гарри Поттера есть список персонажей первой книги. Нам известна глава первого появления каждого героя, это поможет разделить персонажей. Чтобы найти все упоминания персонажа, воспользуемся основанным на правилах матчером SpaCy. Останется определить метрику совместных вхождений и сохранить результаты в Neo4j. Порог совпадений возьмём из проекта графа "Игры престолов": когда два персонажа появляются в пределах 14 слов друг от друга, будем считать, что они взаимодействовали, и сохраним количество этих взаимодействий как вес отношений. Парсим страницу фэндома Гарри Поттера. Предварительно обрабатываем текст книги, решая проблему разрешения совместных ссылок. Распознаём сущности сопоставлением на основе правил SpaCy. Делаем выводы об отношениях персонажей. Сохраняем результаты в графовой базе данных Neo4j. На случай если вы хотите повторить шаги, я подготовил блокнот Google Colab. Работать будем с Selenium, соберём данные персонажей книги "Гарри Поттер и философский камень", вот список их упоминаний. Благодаря коду выше мы получили список персонажей с информацией об их первом появлении в книге. У каждого персонажа есть веб-страница с подробной информацией о нём. На странице Гермионы Грейнджер вы увидите структурированную таблицу с дополнительной информацией. Чтобы извлечь сущности из книги и обогатить граф знаний, воспользуемся разделом таблицы с псевдонимами и добавим детали, такие как дом и происхождение по крови. Для ясности я показываю не весь код, а также я добавил некоторые исключения для псевдонимов. Например, у Гарри есть такой псевдоним: Грегори Гойл (под маскировкой Polyjuice). Проигнорируем псевдонимы под маскировкой Polyjuice. А ещё, кажется, Гарри сказал Стэнли Шанпайку, что он Невилл Лонгботтом, это мы тоже пропустим. Прежде чем продолжить извлечение именованных сущностей, сохраним собранную информацию в Neo4j. Наши атрибуты: имя; адрес страницы; псевдонимы; национальность; кровное происхождение; пол; биологический вид. Взаимоотношения: дом; сообщество, которому предан персонаж; семья. Сохраняем информацию о персонажах в Neo4j Похоже, Гермиона также известна как Маленькая мисс Совершенство и предана Обществу содействия благосостоянию эльфов. К сожалению, данные не разделены по времени, поэтому Фред Уизли уже сейчас оказывается шурином Маленькой мисс Совершенство. Прежде всего, мы должны получить текст книги. Я нашёл на GitHub репозиторий с текстами первых четырёх книг о Гарри Поттере. К данным не прилагается лицензия, поэтому предположу, что вправе использовать данные в образовательных целях с чистой совестью: Мы должны быть внимательны и передать функции ссылку на необработанное текстовое содержимое, и это должно сработать. Когда я впервые извлекал сущности, то забыл предварительно провести разрешение кореферентности, то есть определить относящиеся к одному и тому же объекту лингвистические выражения. Посмотрите мой пост о конвейере извлечения информации, в нём показан работающий пример. Я искал модели разрешения кореферентности с открытым исходным кодом, но, насколько я знаю, их всего две. Первая — NeuralCoref, она работает поверх SpaCy, вторая — AllenNLP. Ранее я работал с NeuralCoref и решил посмотреть, как работает AllenNLP. К сожалению, в Colab 16 ГБ ОЗУ и память быстро закончилась, когда я вводил всю главу в AllenNLP. Затем я нарезал главу на предложения, но это работало очень медленно, возможно, из-за фреймворка BERT, поэтому я по умолчанию использую NeuralCoref, который легко справляется с целой главой и работает быстрее. Код подготовки текста: Я хотел использовать модель распознавания именованных сущностей, пробовал модели SpaCy, HuggingFace, Flair и даже Stanford NLP, но ни одна из них не работала достаточно хорошо, поэтому вместо обучения модели я воспользовался шаблонами сопоставления на основе правил в SpaCy. Мы уже знаем, каких персонажей ищем. Остаётся найти способ точнее сопоставить их. Определим шаблоны для каждого персонажа. Сначала как искомый шаблон добавляем полное имя, затем разбиваем имя пробелом и создаём шаблон из каждого слова. Пример: определяя шаблоны для Альбуса Дамблдора, получим три различных шаблона, которые могут представлять данного персонажа: полное имя: Альбус Дамблдор; имя: Альбус; фамилия: Дамблдор. Есть и кое-какие исключения: я определил список стоп-слов, которых не должно быть в шаблоне персонажа. Например, в книге присутствует персонаж Смотритель зоопарка [Keeper of the Zoo]. Интуитивно понятно, что не следует определять "of" или "the" как шаблоны сопоставления сущности. Хочется, чтобы все отдельные слова были чувствительны к регистру. Это делается, чтобы все слова black не распознавались как ссылка на Сириуса Блэка. Будем считать, что речь идёт о Сириусе Блэке, только если Black написано заглавными. Решение не идеально: Black может быть написано заглавными из-за того, что слово находится в начале предложения, но такого решения достаточно. Особый случай — Рональд Уизли, который в тексте в основном Рон. Не отделяются друг от друга сущности "секретарь Вернона Дурсли" и "филин Драко Малфоя". При таком подходе возникает две проблемы. Первая — в тексте "Альбус Дамблдор — хороший волшебник" найдётся три совпадения, поскольку в шаблоне есть как полное имя, так и его части. Решение: мы будем отдавать приоритет сущностям длиннее. Если в одном и том же месте есть совпадение из нескольких слов, а также другое совпадение из одного слова, то приоритет отдадим совпадению с несколькими словами. Результаты работы шаблона сопоставления с приоритезацией. Моя реализация приоритизации сущностей с более длинными словами очень проста. Во-первых, она проверяет, существует ли среди нескольких сущностей сущность длиннее остальных. Затем проверяется, не длиннее ли все существующие сущности в той же позиции, чем текущий результат. Наконец, если ещё нет существующих сущностей, добавляется новый результат. Интересна последняя инструкция "else", ведь иногда одной сущности соответствует несколько персонажей: "Уизли, иди сюда!". В книге есть несколько Уизли. Проблема возникает, когда на человека ссылаются по его фамилии, а персонажей с такой фамилией много. Чтобы разграничить такие сущности, мы должны придумать общее решение: Эта функция длиннее. Мы начинаем с определения требующих разделения сущностей. Я написал логику, которая устраняет неоднозначность с мистером и миссис Дурсли. Если перед термином "Дурсли" присутствует "мистер", то это ссылка на Вернона, а если "миссис" — на Петунию. И вот обобщённое решение: алгоритм присваивает ссылку ближайшему соседу. Теперь предположим, что невозможно выбрать между Гарри Поттером, Джеймсом Поттером и Лили Поттер. Тогда алгоритм определяет ближайшую в тексте сущность из трёх и присваивает текущему элементу её значение. Есть исключения, когда полное или обычное имя не упоминается в пределах одной главы, так что как последнее средство я жёстко закодировал варианты. Самое трудное позади. Сделать вывод об отношениях персонажей очень просто. Сначала нужно определить порог расстояния взаимодействия (или связи) между двумя персонажами. Как уже упоминалось, мы будем использовать тот же порог расстояния, что и при извлечении графа "Игры престолов". Если два характера встречаются на расстоянии 14 слов, предполагается, что они должны были взаимодействовать. Чтобы не искажать результаты, я объединил сущности. О чём я говорю? Есть два предложения: "У Гарри был хороший день. После обеда он пошёл поговорить с Дамблдором". Здесь будет определено три сущности: "Гарри", "Он" как ссылка на Гарри и "Дамблдор". Решая задачу в лоб, мы могли бы сделать вывод о двух взаимодействиях между Гарри и Дамблдором, поскольку два упоминания "Гарри" близки к упоминанию "Дамблдора". Такого хочется избежать, поэтому я объединил сущности в последовательности, где на одного персонажа ссылаются как на одну сущность. Посчитаем взаимодействия пар персонажей: Мы извлекли граф взаимодействий между персонажами, и осталось только сохранить результаты в графовой базе данных. Запрос на импорт очень прост, поскольку мы имеем дело с сетью моночастиц. Если вы используете подготовленный мной блокнот Colab, для хранения результатов проще всего создать бесплатные Neo4j Sandbox или инстанс базы данных Aura: Визуализируем результаты, чтобы изучить их: На первый взгляд, результаты выглядят классно. В центре сети — Гарри Поттер, книга в основном написана о Гарри. Видно, что мне следовало бы добавить несколько стоп-слов в шаблон матчера. Век живи — век учись.  Код проекта. Песочница алгоритмов на графах. В любой области IT учиться нужно каждый день, а наши курсы готовят к профессиям в сфере информационных технологий не только через правильный баланс теории и практики. Мы прививаем мышление, которое приведёт вас к нужному результату, поэтому, если вам интересно работать с естественным языком и другими данными, приходите на наши курсы по Data Science, по аналитике данных или на курс по глубокому и машинному обучению, где вы научитесь извлекать из данных пользу и решать проблемы бизнеса. Также вы можете узнать, как начать карьеру или прокачаться в других направлениях: Data Science и Machine Learning Профессия Data Scientist Профессия Data Analyst Курс «Математика для Data Science» Курс «Математика и Machine Learning для Data Science» Курс по Data Engineering Курс «Machine Learning и Deep Learning» Курс по Machine Learning Python, веб-разработка Профессия Fullstack-разработчик на Python Курс «Python для веб-разработки» Профессия Frontend-разработчик Профессия Веб-разработчик Мобильная разработка Профессия iOS-разработчик Профессия Android-разработчик Java и C# Профессия Java-разработчик Профессия QA-инженер на JAVA Профессия C#-разработчик Профессия Разработчик игр на Unity От основ — в глубину Курс «Алгоритмы и структуры данных» Профессия C++ разработчик Профессия Этичный хакер А также: Курс по DevOps