Как мне манипулировать деревьями синтаксического анализа?

Я играл с деревьями синтаксического анализа естественного языка и манипулировал ими по-разному. Я использовал Стэнфордские инструменты Tregex и Tsurgeon, но код беспорядок и плохо вписывается в мою среду, в основном Python (эти инструменты Java и не идеальны для настройки). Я хотел бы иметь набор инструментов, который позволил бы легко взломать, когда мне нужно больше функциональности. Существуют ли какие-либо другие инструменты, которые хорошо подходят для сопоставления с образцом на деревьях и последующего манипулирования этими совпавшими ветвями?

Например, я хотел бы взять в качестве входных данных следующее дерево:

(ROOT
  (S
    (NP
      (NP (NNP Bank))
      (PP (IN of)
        (NP (NNP America))))
    (VP (VBD used)
      (S
        (VP (TO to)
          (VP (VB be)
            (VP (VBN called)
              (NP
                (NP (NNP Bank))
                (PP (IN of)
                  (NP (NNP Italy)))))))))))

и (это упрощенный пример):

  1. Найдите любой узел с меткой NP, у которого есть первый потомок с меткой NP и некоторый потомок с именем «Банк», а второй потомок с меткой PP.
  2. Если это соответствует, то возьмите всех дочерних элементов узла PP и переместите их в конец совпадающих дочерних элементов NP.

Например, возьмем эту часть дерева:

(NP
  (NP (NNP Bank))
  (PP (IN of)
    (NP (NNP America))))

и превратить его в это:

(NP
  (NP (NNP Bank) (IN of) (NP (NNP America))))

Поскольку мои входные деревья представляют собой S-выражения, я рассматривал возможность использования Лиспа (встроенного в мою программу на Python), но я так долго писал на Лиспе что-то значимое, что понятия не имею, с чего даже начать.

Что было бы хорошим способом описать шаблоны? Как правильно описать манипуляции? Как лучше подумать об этой проблеме?


person guidoism    schedule 12.09.2010    source источник


Ответы (3)


Это типичный случай использования Лиспа. Вам понадобится функция, которая сопоставляет другую функцию с деревом.

Вот пример процедурного сопоставления с использованием Common Lisp. В Лиспе есть сопоставители, которые работают со структурами списков, которые можно было бы использовать вместо них. Использование средства сопоставления списков упростило бы пример (см. мой другой ответ для примера использования средства сопоставления с образцом).

Код:

(defun node-children (node)
  (rest node))

(defun node-name (node)
  (second node))

(defun node-type (node)
  (first node))


(defun treemap (tree matcher transformer)
  (cond ((null tree) nil)
        ((consp tree)
         (if (funcall matcher tree)
             (funcall transformer tree)
           (cons (node-type tree)
                 (mapcar (lambda (child)
                           (treemap child matcher transformer))
                         (node-children tree)))))
        (t tree))))

Пример:

(defvar *tree*
  '(ROOT
    (S
     (NP
      (NP (NNP Bank))
      (PP (IN of)
          (NP (NNP America))))
     (VP (VBD used)
         (S
          (VP (TO to)
              (VP (VB be)
                  (VP (VBN called)
                      (NP
                       (NP (NNP Bank))
                       (PP (IN of)
                           (NP (NNP Italy))))))))))))



(defun example ()
  (pprint
   (treemap *tree*
            (lambda (node)
              (and (= (length (node-children node)) 2)
                   (eq (node-type (first (node-children node))) 'np)
                   (some (lambda (node)
                           (eq (node-name node) 'bank))
                         (children (first (node-children node))))
                   (eq (first (second (node-children node))) 'pp)))
            (lambda (node)
              (list (node-type node)
                    (append (first (node-children node))
                            (node-children (second (node-children node)))))))))

Запуск примера:

CL-USER 75 > (example)

(ROOT
 (S
  (NP
   (NP (NNP BANK) (IN OF) (NP (NNP AMERICA))))
  (VP
   (VBD USED)
   (S
    (VP
     (TO TO)
     (VP
      (VB BE)
      (VP
       (VBN CALLED)
       (NP
        (NP
         (NNP BANK)
         (IN OF)
         (NP (NNP ITALY)))))))))))
person Rainer Joswig    schedule 12.09.2010

Красота в глазах смотрящего. Но вы никогда не говорите насколько код Tregex или Tsurgeon беспорядок. Это больше похоже на то, что вы не можете иметь дело с Java или большей абстракцией, и поэтому вы ищете что-то конкретное, написанное на Python.

Нет ничего плохого в написании от руки функций сопоставления деревьев и преобразования. Действительно, мы всегда так делали. Но после первых нескольких сотен казалось, что должен быть лучший способ, и поэтому мы перешли к использованию предметно-ориентированных языков Tregex и Turgeon. Обычно это считается похвальным стилем программирования. См. в Википедии. Это хорошо определенные языки с точной спецификацией синтаксиса и т. д. Вот ваш пример их использования.

Tree t = Tree.valueOf("(ROOT (S (NP (NP (NNP Bank)) (PP (IN of) (NP (NNP America)))) (VP (VBD used) (S (VP (TO to) (VP (VB be) (VP (VBN called) (NP (NP (NNP Bank)) (PP (IN of) (NP (NNP Italy)))))))))))");
TregexPattern pat = TregexPattern.compile("NP <1 (NP << Bank) <2 PP=remove");
TsurgeonPattern surgery = Tsurgeon.parseOperation("excise remove remove");
Tsurgeon.processPattern(pat, surgery, t).pennPrint();

Обратите внимание, что код Java на самом деле короче, чем код Lisp, именно из-за использования предметно-ориентированного языка. Трудно понять, как это может быть проще: укажите шаблон, укажите операцию, примените.

Но если вы предпочитаете писать вручную методы, которые сопоставляют шаблоны с деревьями и превращают их в другие деревья в Python, то вы можете пойти и сделать это.

person Christopher Manning    schedule 18.09.2010
comment
Есть ли какая-либо документация по использованию регулярного выражения дерева SP? Или javadocs пока единственная документация? - person sholsapp; 19.09.2010
comment
А, здравствуйте, профессор Мэннинг, извините, что критикую вашу работу без указания конкретных причин. Я хочу взломать код, но я нахожу 100 000 строк немного пугающими для простого добавления небольшого уровня абстракции. Я очень благодарен за существование Tregex/Turgeon. Мне просто любопытно, можно ли DSL, делающий что-то подобное, написать более кратко. По-прежнему существует проблема взаимодействия Java‹-›Python, которую я решил неудовлетворительным образом, но она работает (отчасти). - person guidoism; 19.09.2010
comment
@gnucom: я признаю, что документация могла бы быть лучше/более обширной. Но есть еще пара ресурсов. Слайды ppt nlp.stanford.edu/software/tregex/ являются лучшими место, где можно познакомиться с шаблонами tregex. В приложении с графическим интерфейсом есть полезные экраны справки. Подробнее см.: nlp.stanford.edu/software/tregex-faq.shtml. #б . - person Christopher Manning; 20.09.2010
comment
@guidoism: Кстати, это была не моя работа. DSL и большая часть кода были написаны Роджером Леви и Галеном Эндрю. Я уверен, что DSL можно было бы написать более кратко на других языках, таких как Scala или Clojure, которые больше подходят для этого. Java совсем не лаконична и не особенно ориентирована на написание DSL. Но я думаю, что ваша главная мысль заключается в том, что если вы просто хотите добавить несколько особых случаев и не очень хорошо знакомы с инструментами и кодом, на самом деле проще сделать это с помощью древовидных функций, чем с DSL, где вам нужно изменить токенизатор и парсер и т. д. - person Christopher Manning; 20.09.2010

Вот вторая версия на Common Lisp. На этот раз я использую сопоставитель шаблонов.

Я использую функцию, которая сопоставляет шаблон с данными Lisp. PMATCH:MATCH — это расширенная версия средства сопоставления с образцом из книги Winston/Horn, Lisp, 3-е издание. Доступны аналогичные функции сопоставления с образцом.

Данные такие же, как в моем другом ответе.

Функция сопоставления дерева изменена, чтобы использовать сопоставление с образцом. Функция PMATCH:MATCH возвращает T или соответствующий список привязок, если совпадение прошло успешно. Он возвращает NIL, если совпадение не удалось. PMATCH:INSTANTIATE-PATTERN принимает шаблон и набор привязок. Он возвращает новую структуру списка, в которой переменные шаблона заменены привязками.

(defun treemapp (tree pattern transformer)
  (cond ((null tree) nil)
        ((consp tree)
         (let ((bindings (pmatch:match pattern tree)))
           (if bindings
               (pmatch:instantiate-pattern transformer bindings)
             (cons (node-type tree)
                   (mapcar (lambda (child)
                             (treemapp child pattern transformer))
                           (node-children tree))))))
        (t tree)))

В примере используются шаблоны Now.

Шаблон представляет собой структуру списка. #?symbol соответствует одному элементу и создает привязку для символа. #$symbol соответствует списку элементов и создает привязку для символа.

Преобразователь — это шаблон, который будет создан с помощью привязок.

(defun example1 ()
  (pprint (treemapp *tree*
                    '(NP (NP (#?type bank)) (PP #$children))
                    '(NP (NP (#?type bank) #$children)))))

Запуск этого кода возвращает тот же результат, что и в моем другом ответе.

person Rainer Joswig    schedule 18.09.2010
comment
OK, приложение treemapp можно адаптировать для использования библиотеки optima lib (github.com/m2ym/optima), но это все еще содержат ограничение, преобразование выполняется только в первом совпадении дерева. - person Alexandre Rademaker; 17.11.2016