Книга «Обработка естественного языка в действии»
Время на прочтение
Привет, Хаброжители! Мы издали практическое руководство по обработке и генерации текстов на естественном языке. Книга снабжена всеми инструментами и методиками, необходимыми для создания прикладных NLP-систем с целью обеспечения работы виртуального помощника (чат-бота), спам-фильтра, программы — модератора форума, анализатора тональностей, программы построения баз знаний, интеллектуального анализатора текста на естественном языке или практически любого другого NLP-приложения, какое только можно себе представить.
Книга ориентирована на Python-разработчиков среднего и высокого уровня. Значительная часть книги будет полезна и тем читателям, которые уже умеют проектировать и разрабатывать сложные системы, поскольку в ней содержатся многочисленные примеры рекомендуемых решений и раскрываются возможности самых современных алгоритмов NLP. Хотя знание объектно-ориентированного программирования на Python может помочь создавать лучшие системы, для использования приводимой в этой книге информации оно не обязательно.
Что вы найдете в книге
Относящиеся к части I главы описывают логику работы с естественным языком и преобразование его в числа для поиска и вычислений. Подобные базовые навыки работы со словами обладают дополнительным достоинством в виде таких удивительно полезных приложений, как информационный поиск и анализ тональностей. Освоив азы, вы узнаете, что с помощью очень простых арифметических операций, многократно повторяемых в цикле, можно решить весьма важные задачи, например фильтрации спама. Спам-фильтры, подобные тем, которые мы будем создавать в главах 2–4, спасли всемирную систему электронной почты от анархии и застоя. Вы узнаете, как создать фильтр спама с более чем 90%-ной точностью с помощью технологии 1990-х годов — просто вычисляя количества слов и средние значения этих количеств.
Вся эта математика со словами может показаться скучной, но она весьма увлекательна. Очень скоро вы научитесь создавать алгоритмы, умеющие принимать решения относительно естественного языка не хуже, а то и лучше, чем вы сами (и уж точно намного быстрее). Вероятно, при этом вы впервые в жизни сумеете по-настоящему оценить, насколько слова отражают и вообще делают возможным ваше мышление. Надеемся, представления слов и мыслей в многомерном векторном пространстве закружат ваш мозг в рекуррентных циклах самопознания.
Кульминация обучения наступит примерно к середине книги. Центральным моментом этой книги в части II будет исследование вами сложной паутины вычислений и взаимодействия между нейронными сетями. Сетевой эффект взаимодействия в паутине «мышления» маленьких логических блоков обеспечивает возможность решения машинами задач, к которым лишь очень умные люди пытались подступиться в прошлом: таких задач, как вопросы аналогии, автоматическое реферирование текста и перевод с одного естественного языка на другой.
Мы расскажем вам не только о векторах слов, но и о многом, многом другом. Вы научитесь визуализировать слова, документы и предложения в облаке взаимосвязанных понятий, распространяющемся далеко за пределы простого и понятного трехмерного пространства. Вы начнете представлять себе документы и слова в виде описания персонажа в игре «Подземелья и драконы» с множеством случайно выбранных характеристик и способностей, развивающихся и растущих с течением времени, но только в наших головах.
Умение ценить эту межсубъектную реальность слов и их смыслов будет фундаментом завершающей книгу части III, из которой вы узнаете, как создавать машины, способные общаться и отвечать на вопросы не хуже людей.
Нейронные сети с обратной связью
В главе 7 продемонстрированы возможности анализа фрагмента или целого предложения с помощью сверточной нейронной сети, отслеживание соседних слов в предложении путем наложения на них фильтра разделяемых весов (выполнения свертки). Встречающиеся группами слова можно также обнаруживать в связке. Сеть также устойчива к небольшим смещениям позиций этих слов. В то же время встречающиеся по соседству понятия могут существенно влиять на сеть. Но если нужно охватить взглядом большую картину происходящего, учесть взаимосвязи за более длительный промежуток времени, окно, охватывающее больше 3–4 токенов из предложения? Как ввести в сеть понятие произошедших ранее событий? Память?
Для каждого тренировочного примера (или батча неупорядоченных примеров) и выходного сигнала (или пакета выходных сигналов) нейронной сети прямого распространения веса нейронной сети необходимо откорректировать для отдельных нейронов на основе метода обратного распространения ошибки. Это мы уже демонстрировали. Но результаты этапа обучения для следующего примера в основном не зависят от порядка входных данных. Сверточные нейронные сети стремятся захватить эти отношения порядка за счет захвата локальных взаимосвязей, но существует и другой способ.
В сверточной нейронной сети каждый тренировочный пример передается сети в виде сгруппированного набора токенов слов. Векторы слов сгруппированы в матрицу в форме (длина вектора слова × число слов в примере), как показано на рис. 8.1.

Но эту последовательность векторов слов можно столь же легко передать и обычной нейронной сети прямого распространения из главы 5 (рис. 8.2), правда?

Безусловно, это вполне работоспособная модель. При подобном способе передачи входных данных нейронная сеть прямого распространения сможет реагировать на совместные вхождения токенов, что нам и нужно. Но она будет при этом реагировать на все совместные вхождения одинаково, независимо от того, разделяет ли их длинный текст, или они находятся рядом друг с другом. Кроме того, нейронные сети прямого распространения, как и CNN, плохо умеют обрабатывать документы переменной длины. Они не способны обработать текст в конце документа, если он выходит за пределы ширины сети.
Лучше всего нейронные сети прямого распространения проявляют себя в моделировании взаимосвязи выборки данных в целом с соответствующей ей меткой. Слова в начале и в конце предложения ровно так же влияют на выходной сигнал, как и посередине, невзирая на то что вряд ли они семантически связаны друг с другом.
Подобная однородность (равномерность влияния) явно может вызывать проблемы в случае, например, токенов резкого отрицания и модификаторов (прилагательных и наречий), таких как «нет» или «хороший». В нейронной сети прямого распространения выражающие отрицание слова влияют на смысл всех слов в предложении, даже сильно удаленных от места, на которое они должны влиять на самом деле.
Одномерные свертки — способ решения проблем с этими взаимосвязями между токенами путем анализа нескольких слов через окна. Обсуждавшиеся в главе 7 слои субдискретизации специально предназначены для учета небольших изменений порядка слов. В этой главе мы рассмотрим другой подход, благодаря которому сможем сделать первый шаг к понятию памяти нейронной сети. Вместо того чтобы разбирать язык как большую порцию данных, мы начнем рассматривать его последовательное формирование, токен за токеном, с ходом времени.
Запоминание в нейронных сетях
Конечно, слова в предложении редко бывают совершенно независимыми друг от друга; их вхождения влияют или подвергаются влиянию вхождений других слов в документе. Например: The stolen car sped into the arena и The clown car sped into the arena.
У вас могут возникнуть совершенно различные впечатления от этих двух предложений, когда вы дочитаете до конца. Конструкция фразы в них одинакова: прилагательное, существительное, глагол и предложный оборот. Но замена прилагательного в них радикальным образом меняет суть происходящего с точки зрения читателя.
Как смоделировать подобную взаимосвязь? Как понять, что arena и даже sped могут иметь немного разные коннотации, если перед ними в предложении есть прилагательное, не являющееся прямым определением ни одного из них?
Если бы существовал способ запоминать произошедшее моментом ранее (особенно помнить на шаге t + 1 произошедшее на шаге t), можно было бы выявлять закономерности, возникающие при появлении определенных токенов в связанных с другими токенами последовательности закономерностях. Рекуррентные нейронные сети (RNN) как раз делают возможным запоминание нейронной сетью прошлых слов последовательности.
Как вы видите на рис. 8.3, отдельный рекуррентный нейрон из скрытого слоя добавляет в сеть рекуррентный цикл для «повторного использования» выходного сигнала скрытого слоя для момента t. Выходной сигнал для момента t прибавляется к следующему входному сигналу для момента t + 1. В результате обработки сетью этого нового входного сигнала на временном шаге t + 1 получается выходной сигнал скрытого слоя для момента t + 1. Этот выходной сигнал для момента времени t + 1 далее повторно используется сетью и включается во входной сигнал на временном шаге t + 2 и т. д.
Хотя идея воздействия на состояние сквозь время выглядит немного запутанной, основная концепция проста. Результаты каждого сигнала на входе обычной нейронной сети прямого распространения на временном шаге t используются в качестве дополнительного входного сигнала вместе со следующим фрагментом подаваемых на вход сети данных на временном шаге t + 1. Сеть получает информацию не только о происходящем сейчас, но и о происходившем ранее.
В этой и следующей главах большая часть нашего обсуждения происходит на языке временных шагов. Это вовсе не то же самое, что отдельные примеры данных. Речь идет о разбиении одного примера данных на меньшие порции, отражающие изменения во времени. Этот отдельный пример данных все равно представляет собой фрагмент текста, скажем короткий отзыв о фильме или твит. Как и ранее, мы токенизируем предложение. Но вместо того, чтобы отправлять токены в сеть все сразу, мы передаем их по одному. Эта схема отличается от передачи нескольких новых примеров документов. Токены при этом остаются частью одного примера данных, которому соответствует одна метка.
t можно считать индексом последовательности токенов. Так, t = 0 — первый токен в документе, а t + 1 — следующий. Токены в порядке следования их в документе служат входными сигналами на каждом из временных (токенных) шагов. Причем токены не обязательно должны быть словами, отдельные символы также допустимы. Подача примера данных в сеть разбивается на подшаги — ввод в сеть отдельных токенов.
На протяжении всей этой книги мы будем обозначать текущий временной шаг t, а следующий временной шаг — t + 1.
Рекуррентную нейронную сеть можно визуализировать так, как показано на рис. 8.3: круги соответствуют целым слоям нейронной сети прямого распространения, состоящим из одного или нескольких нейронов. Выходной сигнал скрытого слоя выдается сетью как обычно, но затем поступает обратно в качестве своего же (скрытого слоя) входного сигнала вместе с обычными входными данными следующего временного шага. На схеме этот цикл обратной связи изображен в виде дуги, ведущей из выхода слоя обратно на вход.
Более простой (и чаще используемый) способ иллюстрации этого процесса — с использованием развертывания сети. Рисунок 8.4 демонстрирует сеть «вверх ногами» с двумя развертками временной переменной (t) — слоями для шагов t + 1 и t + 2.

Каждому из временных шагов соответствует развернутая версия той же нейронной сети в виде столбца нейронов. Это все равно, что смотреть сценарий или отдельные видеокадры нейронной сети в каждый момент времени. Сеть справа представляет собой будущую версию сети слева. Выходной сигнал скрытого слоя в момент времени (t) подается снова на вход скрытого слоя вместе с входными данными для следующего временного шага (t + 1) справа. И еще раз. На схеме показаны две итерации этого развертывания, всего три столбца нейронов для t = 0, t = 1 и t = 2.
Все вертикальные маршруты на этой схеме полностью аналогичны, в них показаны одни и те же нейроны. Они отражают одну и ту же нейронную сеть в разные моменты времени. Такое наглядное представление удобно при демонстрации движения информации по сети в прямом и обратном направлении во время обратного распространения ошибки. Но помните, глядя на эти три развернутые сети: они представляют собой различные моментальные снимки одной и той же сети с тем же набором весов.
Изучим внимательнее исходное представление рекуррентной нейронной сети до ее развертывания и покажем взаимосвязи входных сигналов и весов. Отдельные слои этой RNN выглядят так, как показано на рис. 8.5 и 8.6.

У всех нейронов скрытого состояния есть по набору весов, применяемых к каждому из элементов каждого из входных векторов, как в обычной сети прямого распространения. Но в этой схеме появился и дополнительный набор обучаемых весов, которые применяются к выходным сигналам скрытых нейронов из предыдущего временного шага. Сеть путем обучения подбирает подходящие веса (важность) предыдущих событий при вводе последовательности токен за токеном.
У первого входного сигнала в последовательности нет «прошлого», так что скрытое состояние на шаге t = 0 получает нулевой входной сигнал от себя же с шага t – 1. Можно также для «заполнения» начального значения состояния сначала передать в сеть взаимосвязанные, но отдельные примеры данных, один за другим. Итоговый выходной сигнал каждого примера используется во входном сигнале t = 0 следующего примера данных. В посвященном сохранению состояния разделе в конце данной главы мы расскажем вам, как сохранять больше информации из набора данных с помощью альтернативных методик заполнения.
Вернемся к данным: представьте, что у вас есть набор документов, каждый из которых представляет собой маркированный пример. И вместо того, чтобы для каждого выборочного примера передавать набор векторов слов целиком в сверточную нейронную сеть, как в предыдущей главе (рис. 8.7), мы передаем пример данных в RNN по одному токену (рис. 8.8).

Мы передаем вектор слов для первого токена и получаем выходной сигнал нашей рекуррентной нейронной сети. Затем передаем второй токен, а вместе с ним — выходной сигнал от первого! После этого передаем третий токен вместе с выходным сигналом от второго! И так далее. Теперь в нашей нейронной сети существуют понятия «до» и «после», причины и следствия, некое, пусть и расплывчатое, представление о времени (см. рис. 8.8).

Теперь наша сеть уже кое-что запоминает! Ну, в известной мере. Осталось выяснить еще несколько вещей. Во-первых, как может происходить обратное распространение ошибки в подобной структуре?
Обратное распространение ошибки во времени
Во всех обсуждавшихся выше сетях существовала искомая метка (целевая переменная), и RNN не исключение. Но у нас отсутствует понятие метки для каждого токена, а есть только одна метка для всех токенов каждого из примеров текста. У нас имеются только метки для примеров документов.
Мы говорим о токенах как входных данных для сети на каждом из временных шагов, но рекуррентные нейронные сети могут так же работать с любыми данными временных рядов. Токены могут быть любыми, дискретными или непрерывными: показания метеостанции, ноты, символы в предложении и т. п.
Здесь мы сначала сравниваем выходной сигнал сети на последнем временном шаге с меткой. Именно это мы и будем (пока что) называть ошибкой, а именно ошибку наша сеть и пытается минимизировать. Но есть небольшое отличие от предыдущих глав. Заданный пример данных разбивается на меньшие части, подаваемые в нейронную сеть последовательно. Однако вместо того, чтобы непосредственно использовать получаемый для каждого из этих «подпримеров» выходной сигнал, мы отправляем его обратно в сеть.
Нас интересует пока что только итоговый выходной сигнал. Каждый из токенов последовательности подается в сеть, и на основе выходного сигнала последнего временного шага (токена) вычисляются потери (рис. 8.9).

Необходимо определить при наличии ошибки для заданного примера, какие веса обновить и насколько. В главе 5 мы рассказали, как производить обратное распространение ошибки по обычной сети. И мы знаем, что величина корректировки веса зависит от его (этого веса) вклада в ошибку. Мы можем подавать по токену из выборочной последовательности на вход сети, вычисляя на основе ее выходного сигнала ошибку для предыдущего временного шага. Тут-то идея обратного распространения ошибки во времени, похоже, все и запутывает.
Впрочем, можно просто рассматривать это как процесс с привязкой по времени. На каждом временном шаге токены, начиная с первого при t = 0, подаются по одному на вход расположенного впереди скрытого нейрона — следующий столбец на рис. 8.9. При этом сеть развертывается, раскрывая следующий столбец сети, уже готовый для получения очередного токена в последовательности. Скрытые нейроны развертываются по одному, подобно музыкальной шкатулке или механическому пианино. В конце концов, когда в сеть будут поданы все элементы примеров, развертывать больше будет нечего и мы получим на выходе итоговую метку для интересующей нас целевой переменной, которую можно использовать для вычисления ошибки и корректировки весов. Мы только что прошли весь путь по графу вычислений для этой развернутой сети (unrolled net).
Пока что мы считаем входные данные в целом статическими. Можно проследить по всему графу, какой входной сигнал поступает в какой нейрон. А раз мы знаем, как срабатывает какой нейрон, то можем распространить ошибку обратно по цепочке, по тому же пути, точно так же, как и в случае обычной нейронной сети прямого распространения.
Для обратного распространения ошибки на предыдущий слой мы воспользуемся цепным правилом. Вместо предыдущего слоя мы распространим ошибку на тот же слой в прошлом, как если бы все развернутые варианты сети были различны (рис. 8.10). Математика расчетов при этом не меняется.

Ошибка с последнего шага распространяется обратно. Вычисляется градиент более раннего временного шага относительно более нового. После вычисления всех отдельных градиентов по токенам, вплоть до шага t = 0 для данного примера, изменения агрегируются и применяются к одному набору весов.
Когда что обновлять
Мы превратили нашу странную RNN в нечто похожее на обычную нейронную сеть прямого распространения, так что обновление весов не должно вызвать затруднений. Впрочем, есть один нюанс. Хитрость в том, что веса обновляются вовсе не в другой ветке нейронной сети. Каждая ветка представляет собой ту же сеть в другие моменты времени. Веса для каждого временного шага одни и те же (см. рис. 8.10).
Простое решение этой проблемы — вычисление поправок для весов на каждом из временных шагов с отсрочкой обновления. В сети прямого распространения все обновления весов вычисляются сразу после вычисления всех градиентов для конкретного входного сигнала. И здесь точно так же, но обновления откладываются, пока мы не доберемся до начального (нулевого) временного шага для конкретного входного примера данных.
В основе расчета градиента должны лежать значения весов, при которых они внесли данный вклад в ошибку. Вот и самая ошеломительная часть: вес на временном шаге t внес некий вклад в ошибку. И тот же вес получает другой входной сигнал на временном шаге t + 1, а значит, вносит уже другой вклад в ошибку.
Можно вычислять различные изменения весов на каждом из временных шагов, суммировать их и затем применять сгруппированные изменения к весам скрытого слоя в качестве последнего шага этапа обучения.
Во всех этих примерах передается отдельный тренировочный пример данных для прямого прохода, а затем производится обратное распространение ошибки. Как и в любой нейронной сети, этот прямой проход по сети можно производить или после каждого тренировочного примера, или в виде батчей. Оказывается, что у пакетного подхода есть и другие преимущества, помимо быстродействия. Но пока что мы будем рассматривать эти процессы с точки зрения отдельных примеров данных, отдельных предложений или документов.
Настоящее волшебство. При обратном распространении ошибки во времени может производиться корректировка отдельного веса в одну сторону на временном шаге t (в зависимости от его реакции на входной сигнал на временном шаге t), а затем в другую сторону на временном шаге t – 1 (в соответствии с тем, как он отреагировал на входной сигнал на временном шаге t – 1) для одного примера данных! Помните, что нейронные сети в целом основаны на минимизации функции потерь вне зависимости от сложности промежуточных шагов. В совокупности сеть оптимизирует эту сложную функцию. Поскольку обновление веса применяется для примера данных однократно, то сеть (если она вообще сходится, конечно) в итоге останавливается на наиболее оптимальном в этом смысле весе для конкретного входного сигнала и конкретного нейрона.
Результаты предыдущих шагов все же важны
Иногда оказывается важна вся последовательность значений, генерируемая на всех промежуточных временных шагах. В главе 9 мы приведем примеры ситуаций, в которых выходной сигнал конкретного временного шага t ничуть не менее важен, чем выходной сигнал последнего временного шага. На рис. 8.11 приведен способ сбора данных об ошибке для любого временного шага и обратного ее распространения для корректировки всех весов сети.

Этот процесс напоминает обычное обратное распространение ошибки во времени для n временных шагов. В данном случае мы распространяем обратно ошибку из нескольких источников одновременно. Но, как и в первом примере, корректировки весов носят аддитивный характер. Ошибка распространяется с последнего временного шага в начале до первого с суммированием изменений каждого из весов. Затем то же самое происходит с ошибкой, вычисленной на предпоследнем временном шаге с суммированием всех изменений вплоть до t = 0. Этот процесс повторяется, пока мы не дойдем до нулевого временного шага с обратным распространением ошибки для него так, как будто он единственный. Затем суммарные изменения применяются все сразу к соответствующему скрытому слою.
На рис. 8.12 видно, как ошибка распространяется от каждого выходного сигнала обратно аж до t = 0, затем агрегируется перед итоговой корректировкой весов. Это основная идея данного раздела. Как и в случае обычной нейронной сети прямого распространения, веса обновляются только после вычисления предлагаемого изменения весов для всего шага обратного распространения ошибки для данного входного сигнала (или набора входных сигналов). В случае RNN обратное распространение ошибки включает обновления вплоть до момента времени t = 0.
Обновление весов ранее внесло бы искажения в расчеты градиентов при обратных распространениях ошибок в более ранние моменты времени. Как вы помните, градиенты вычисляются относительно конкретного веса. Если обновить этот вес слишком рано, скажем на временном шаге t, то при вычислении градиента на временном шаге t – 1 значение веса (напомним, что это та же позиция веса в сети) изменится. И при вычислении градиента на основе входного сигнала с временного шага t – 1 расчеты окажутся искаженными. Фактически при этом вес будет штрафоваться (или вознаграждаться) за то, в чем «не виноват»!

Об авторах
Хобсон Лейн (Hobson Lane) обладает 20-летним опытом создания автономных систем, принимающих важные решения в интересах людей. В компании Talentpair Хобсон обучал машины читать и понимать резюме менее предубежденно, чем большинство специалистов по подбору персонала. В Aira он помогал в создании их первого чат-бота, предназначенного для толкования окружающего мира незрячим. Хобсон — страстный поклонник открытости ИИ и ориентированности его на благо общества. Он вносит активный вклад в такие проекты с открытым исходным кодом, как Keras, scikit-learn, PyBrain, PUGNLP и ChatterBot. Сейчас он занимается открытыми научными исследованиями и образовательными проектами для Total Good, включая создание виртуального помощника с открытым исходным кодом. Он опубликовал многочисленные статьи, выступал к лекциями на AIAA, PyCon, PAIS и IEEE и получил несколько патентов в области робототехники и автоматизации.
Ханнес Макс Хапке (Hannes Max Hapke) — инженер-электротехник, ставший инженером в области машинного обучения. В средней школе он увлекся нейронными сетями, когда изучал способы вычислений нейронных сетей на микроконтроллерах. Позднее, в колледже, он применял принципы нейронных сетей к эффективному управлению электростанциями на возобновляемых источниках энергии. Ханнес обожает автоматизировать разработку программного обеспечения и конвейеров машинного обучения. Он соавтор моделей глубокого обучения и конвейеров машинного обучения для сфер подбора персонала, энергетики и здравоохранения. Ханнес выступал с презентациями на тему машинного обучения на разнообразных конференциях, включая OSCON, Open Source Bridge и Hack University.
Коул Ховард (Cole Howard) — специалист по машинному обучению, специалист-практик, занимающийся NLP, и писатель. Вечный искатель закономерностей, он нашел себя в мире искусственных нейронных сетей. В числе его разработок — масштабные рекомендательные системы для торговли через Интернет и передовые нейронные сети для систем машинного интеллекта сверхвысокой размерности (глубокие нейронные сети), занимающие первые места на конкурсах Kaggle. Он выступал с докладами на тему сверточных нейронных сетей, рекуррентных нейронных сетей и их роли в обработке естественного языка на конференциях Open Source Bridge и Hack University.
Об иллюстрации на обложке
Рисунок на обложке называется «Женщина из Краньска-Гора, Словения». Эта иллюстрация взята из недавнего переиздания книги Бальтазара Аке (Balthasar Hacquet) Images and Descriptions of Southwestern and Eastern Wends, Illyrians and Slavs («Изображения и описания юго-западных и восточных венедов, иллирийцев и славян»), опубликованного Этнографическим музеем в Сплите (Хорватия) в 2008 году. Аке (1739–1815) — австрийский врач и ученый, потративший долгие годы на изучение флоры, геологии и этносов Юлийских Альп — горного хребта, простирающегося от северо-восточной Италии до Словении и названного в честь Юлия Цезаря. Нарисованные вручную иллюстрации сопровождают многие опубликованные Аке научные статьи и книги.
Широкое разнообразие рисунков в публикациях Аке наглядно говорит об уникальности и своеобразии восточных Альп всего 200 лет назад. В то время манера одеваться однозначно различала жителей деревень, расположенных всего в нескольких милях друг от друга, а членов разных социальных групп и профессий можно было легко определить по их одежде. Стили одежды с тех пор изменились, и столь богатое разнообразие различных регионов угасло. Зачастую непросто отличить даже жителя одного континента от жителя другого, а обитатели живописных городков и деревушек в Словенских Альпах не так уж сильно не похожи на жителей других частей Словении или остальной части Европы.
Для Хаброжителей скидка 25% по купону — NLP
По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Привет, Хаброжители! Python и spaCy помогут вам быстро и легко создавать NLP-приложения: чат-боты, сценарии для сокращения текста или инструменты принятия заказов. Вы научитесь использовать spaCy для интеллектуального анализа текста, определять синтаксические связи между словами, идентифицировать части речи, а также определять категории для имен собственных. Ваши приложения даже смогут поддерживать беседу, создавая собственные вопросы на основе разговора.
Прочитав эту книгу, вы можете сами расширить приведенные в ней сценарии, чтобы обрабатывать разнообразные варианты ввода и создавать приложения профессионального уровня.
Использование меток синтаксических зависимостей при обработке текста
Как вы узнали из раздела «Выделение и генерация текста с помощью тегов частей речи» на с. 81, теги частей речи — весьма мощный инструмент для интеллектуальной обработки текста. Но на практике для этого может понадобиться больше информации о токенах предложения.
Например, часто необходимо знать, чем является личное местоимение в предложении: подлежащим или дополнением. Иногда это несложно определить. Личные местоимения I, he, she, they и we практически всегда выступают в роли подлежащего. При использовании в качестве дополнения I превращается в me, как в предложении A postman brought me a letter.
Но с некоторыми другими личными местоимениями, например you или it, которые выглядят одинаково и в роли подлежащего и в роли дополнения, не всегда все очевидно. Рассмотрим два предложения: I know you. You know me. В первом предложении you является прямым дополнением глагола know. Во втором же you является подлежащим.
Попробуем решить эту задачу с помощью меток синтаксических зависимостей и тегов частей речи, после чего, опять же с помощью меток синтаксических зависимостей, создадим усовершенствованную версию нашего чат-бота, отвечающего на вопросы.
Различаем подлежащие и дополнения
Чтобы определить программным образом, чем в заданном предложении являются такие местоимения, как you или it, необходимо посмотреть на присвоенную им метку зависимости. Теги частей речи в сочетании с метками зависимостей позволяют получить гораздо больше информации о роли токена в предложении.
Вернемся к предложению из предыдущего примера и взглянем на результаты разбора зависимостей в нем:
Для токенов предложения были извлечены теги частей речи, метки зависимостей и их описание:
I PRON PRP nsubj nominal subject
can VERB MD aux auxiliary
promise VERB VB ROOT None
it PRON PRP nsubj nominal subject
is VERB VBZ ccomp clausal complement
worth ADJ JJ acomp adjectival complement
your ADJ PRP$ poss possession modifier
time NOUN NN npadvmod noun phrase as adverbial modifier
. PUNCT . punct punctuation
Второй и третий столбцы содержат теги общих и уточненных частей речи соответственно. Четвертый столбец содержит метки зависимостей, а пятый — описания этих меток.
Сочетание тегов частей речи с метками зависимостей демонстрирует более ясную, чем теги частей речи или метки зависимости по отдельности, картину грамматической роли каждого из токенов в предложении. В данном примере тег части речи VBZ, присвоенный токену is, означает глагол третьего лица единственного числа настоящего времени, в то время как присвоенная тому же токену метка зависимости ccomp указывает, что is — это клаузальное дополнение (зависимое придаточное предложение с внутренним подлежащим). Здесь is представляет собой клаузальное дополнение глагола promise с внутренним подлежащим it.
Чтобы определить роль you в I know you. You know me, взглянем на следующий список тегов частей речи и меток зависимостей, присвоенных токенам:
I PRON PRP nsubj nominal subject
know VERB VBP ROOT None
you PRON PRP dobj direct object
. PUNCT . punct punctuation
You PRON PRP nsubj nominal subject
know VERB VBP ROOT None
me PRON PRP dobj direct object
. PUNCT . Punct punctuation
В обоих случаях токену you присвоены одни и те же теги частей речи: PRON и PRP (общий и уточненный соответственно). Но метки зависимости в этих двух случаях различны: dobj в первом предложении и nsubj — во втором.
Выясняем, какой вопрос должен задать чат-бот
Иногда для извлечения необходимой информации приходится обходить дерево зависимостей предложения. Рассмотрим следующий диалог между чат-ботом и пользователем:
Чат-бот способен продолжать разговор, просто задавая вопросы. Но обратите внимание, что в выяснении того, какой вопрос ему следует задать, ключевую роль играет наличие/отсутствие прилагательного-модификатора.
В английском языке существует два основных типа вопросов: вопросы типа «да/нет» и информационные вопросы. Возможных ответов на вопросы типа «да/нет» (наподобие сгенерированного в примере из подраздела «Преобразование утвердительных высказываний в вопросительные» на с. 85) может быть только два: да или нет. Чтобы сформулировать подобный вопрос, необходимо поставить вспомогательный модальный глагол перед подлежащим, а смысловой глагол — после подлежащего. Например: Could you modify it?
Информационные вопросы предполагают развернутый ответ, а не только да/нет. Они начинаются с вопросительного слова, например с what, where, when, why или how. Далее процесс формирования информационного вопроса не отличается от процесса с вопросом типа «да/нет». Например: What do you think about it?
В первом случае в предыдущем примере с apple чат-бот задает вопрос типа «да/нет». Во втором случае, когда пользователь добавляет к слову apple модификатор green, чат-бот формулирует информационный вопрос.
Краткая сводка этого подхода приведена на рис. 4.1.
Следующий сценарий анализирует введенное предложение, выбирая, какой вид вопроса задать, после чего формирует соответствующий вопрос. Код этого сценария мы рассмотрим по частям в различных разделах, но программу целиком я рекомендую сохранить в одном файле с названием question.py.

Начнем с импорта модуля sys, который позволяет получить предложение в виде аргумента для дальнейшей обработки:
import spacy
import sys
Это шаг вперед по сравнению с предыдущими сценариями, в которых мы жестко «зашивали» анализируемое предложение. Теперь пользователи могут подавать на вход собственные предложения.
Далее опишем функцию для распознавания и извлечения произвольного именного фрагмента — прямого дополнения из входного документа. Например, если вы ввели документ, содержащий предложение I want a green apple., то будет возвращен фрагмент a green apple:
Проходим в цикле по токенам введенного предложения 1 и, проверяя теги зависимостей на равенство dobj 2, ищем такой токен, который выступал бы в роли прямого дополнения. В предложении I want a green apple. прямым дополнением является существительное apple. После обнаружения прямого дополнения необходимо определить элементы, являющиеся для него синтаксически дочерними 3, поскольку именно из них состоит фрагмент, на основе которого будет определяться тип задаваемого вопроса. В целях отладки полезно вывести на экран дочерние элементы этого прямого дополнения 4.
Для выделения нужного фрагмента производим срез объекта Doc, вычисляя начальный и конечный индексы. Начальный индекс равен индексу найденного прямого дополнения минус число его синтаксических дочерних элементов: как вы, возможно, догадались, он представляет собой индекс крайнего слева дочернего элемента. Конечный индекс равен индексу прямого дополнения плюс один, так что последним включаемым в искомый фрагмент токеном и является это прямое дополнение 5.
Проще говоря, реализованный в сценарии алгоритм предполагает, что у прямого дополнения есть только левосторонние дочерние элементы. В действительности это не всегда так. Например, в предложении I want to touch a wall painted green. необходимо проверять и левосторонние, и правосторонние дочерние элементы прямого дополнения wall. Кроме того, поскольку green не является прямым дочерним элементом wall, необходимо обойти дерево зависимостей, чтобы определить, является ли green модификатором wall.
Следующая функция просматривает фрагмент и определяет, какой тип вопроса должен задать чат-бот:
Сначала задаем начальное значение переменной question_type равным ‘yesno’, что соответствует вопросу типа «да/нет» 1. Далее в переданном в функцию chunk ищем токен с тегом amod, который означает прилагательное-модификатор 2. Если таковое находится, меняем значение переменной question_type на ‘info’, соответствующее информационному типу вопроса 3.
Определив, какой тип вопроса нам нужен, генерируем в следующей функции вопрос на основе входного предложения:

В серии циклов for превращаем входное утверждение в вопрос, производя инверсию и замену личных местоимений. Для формирования вопроса перед личным местоимением добавляем глагол do, поскольку в утверждении отсутствует вспомогательный модальный глагол. ( Напомню, что такой алгоритм годится лишь для определенных предложений; в более полной реализации необходимо программным образом определять, какой подход к обработке использовать.)
Если значение переменной question_type равно ‘info’, добавляем слово why в начало вопроса 1. Если значение переменной question_type равно ‘yesno’ 2, вставляем прилагательное для модификации прямого дополнения в вопросе. В данном примере ради простоты мы жестко «зашили» прилагательное в код, выбрав для этого прилагательное red 3, которое в некоторых предложениях будет выглядеть странно. Например, можно сказать Do you want a red orange?, но никак не Do you want a red idea?. В более совершенной реализации такого чат-бота необходимо определить программным образом подходящее прилагательное для модификации прямого дополнения. Этот вопрос будет рассмотрен в главе 6.
Обратите внимание: используемый алгоритм предполагает, что входное предложение оканчивается знаком препинания, например. или!
После описания всех функций посмотрим на основной блок сценария:

Прежде всего проверяем, передал ли пользователь предложение в виде аргумента командной строки 1. Если да, то применяем к аргументу конвейер spaCy, создавая экземпляр объекта Doc 2.
Далее передаем этот doc в функцию find_chunk, которая должна вернуть содержащий прямое дополнение именной фрагмент, например a green apple, для дальнейшей обработки 3. Если же во входном предложении такого именного фрагмента нет 4, мы получим сообщение The sentence does not contain a direct object.
Затем передаем только что выделенный фрагмент в функцию determine_question_type, которая, проанализировав его структуру, определяет, какой вопрос задать 5.
Наконец, передаем входное предложение и определенный нами тип вопроса в функцию generate_question, генерирующую соответствующий вопрос и возвращающую его в виде строки 6.
Выводимый сценарием результат зависит от введенного предложения. Вот несколько возможных вариантов:
Если в качестве аргумента командной строки передать предложение, содержащее прилагательное-модификатор (например, green для прямого дополнения наподобие apple), сценарий должен сгенерировать информационный вопрос 1.
Если предложение содержит прямое дополнение без прилагательного-модификатора, сценарий должен ответить вопросом типа «да/нет» 2.
Если же ввести предложение без прямого дополнения, сценарий должен сразу это заметить и предложить повторный ввод 3.
Наконец, если вы забыли передать предложение, сценарий также должен вернуть соответствующее сообщение 4.
Как отмечалось ранее, сценарий, о котором шла речь в предыдущем разделе, годится не для всех предложений. Для формирования вопроса он добавляет глагол do, что подходит только для предложений без вспомогательного модального глагола.
Попробуйте расширить функциональность этого сценария для работы с утверждениями, содержащими вспомогательные модальные глаголы. Например, получив на входе утверждение I might want a green apple., сценарий должен вернуть Why might you want a green one?. Подробнее о том, как преобразовать в вопрос утвердительное высказывание, содержащее вспомогательный модальный глагол, вы прочитали в разделе «Преобразование утвердительных высказываний в вопросительные» на с. 85.
Для Хаброжителей скидка 25% по купону — spaCy
