Довольно часто встречается задача классификации текстов — например, определение тональности (выражает ли текст позитивное мнение или отрицательное о чем-либо), или разнесения текста по тематикам. На Хабре уже есть хорошие статьи с введением в данный вопрос.

Сегодня я хочу поговорить о проблеме классификации отдельных предложений. Решение этой задачи позволяет делать много интересного, например, выделять положительные и отрицательные моменты из длинных текстов, определять тональность твитов, является компонентом многих систем отвечающих на естественно-языковые вопросы (классификация типа вопроса), помогает сегментировать веб-страницы на смысловые блоки и многое другое. Однако, классификация отдельных предложений значительно сложнее классификации больших блоков текста — в одном предложении значительно меньше полезных признаков, и велико влияние порядка слов. Например: «как положено фильму ужасов, этот фильм был ну очень жутким» — содержит негативные слова («ужас», «жуткий»), но выражает положительное мнение о фильме, «все было ужасно красиво», или даже «отличный фильм, ничего не скажешь, только зря деньги потратили».

Традиционно это затруднение стараются решать с помощью предварительной обработки текста и ручного выделения признаков. В предварительную обработку могут входить как относительно простые техники (учет отрицания путем приклеивания частицы «не» к следующим словам), там и более сложные наборы правил переключения тональности, а также построение дерева зависимостей, а в ручные признаки — словари положительных и отрицательных слов, место слова в предложении, и другие — насколько хватит фантазии. Ясно, что сей процесс нудный, требующий много сторонних функций (например, нужен парсер для грамматического разбора предложения, словари), и не всегда результативный. Например, если авторы предложений делали много грамматических ошибок, то парсер, строящий дерево зависимостей, начинает сильно путаться, и качество работы всей системы резко снижается.

Но дело даже и не в этом — а в том, что все это делать лень. Я глубоко убежден, что системы машинного обучения должны работать end-to-end – загрузили обучающие данные — получили работающую модель. Пусть даже качество упадет на пару процентов, но трудозатраты снизятся на порядок, и откроется путь для большого числа новых и полезных приложений (например, вот я нашел статью об интересном применении классификатора текстов ).

Теория: Но короче, от слов к делу. Чтобы предложение можно было подать на вход нейронной сети, надо решить несколько проблем. Во-первых, необходимо преобразовать слова в цифры. Первое желание, которое возникает — сопоставить каждому слову из словаря свое число. Скажем (Абрикос — 1, Аппарат — 2, …. Яблоко — 53845). Но делать так нельзя, потому что таким образом мы неявно предполагаем, что абрикос гораздо больше похож на аппарат, чем на яблоко. Второй вариант — закодировать слова длинным вектором, в котором нужному слову соответствует 1, а всем остальным — 0 (Абрикос — 1 0 0 …, Аппарат — 0 1 0 0 …, … Яблоко — … 0 0 0 1). Здесь все слова равноудалены и не похожи друг на друга. Этот подход гораздо лучше и в ряде случаев работает хорошо (если есть достаточно много примеров).

Но если набор примеров маленький, то весьма вероятно, что какие-то слова (например, «абрикос») в нем будут отсутствовать, и в результате встретив такие слова в реальных примерах, алгоритм не будет знать, что с ними делать. Поэтому оптимально кодировать слова такими векторами, чтобы похожие по смыслу слова оказывались близко друг к другу — а далекие, соответственно — далеко. Есть несколько алгоритмов, которые «читают» большие объемы текстов, и на основании этого создают такие вектора (самый известный, но не всегда самый лучший — word2vec). Подробности — тема для отдельного разговора, пока для понимания достаточно знать, что такие способы есть, они берут на вход длинные массивы текстов и выдают вектора фиксированной длины, соответствующие каждому слову.

Получив вектора слов, мы сталкиваемся с задачей номер два — как представить цельное предложение для нейронной сети. Дело в том, что обычные нейронные сети прямого распространения (feed-forward) должны иметь входные данные фиксированной длины (см картинку). Объяснять, как работает классическая искусственная нейронная сеть прямого распространения здесь не буду — на эту тему уже написано достаточно статей см, например. Для полноты картины вставлю только рисунок.



Так вот, наша проблема в том, что все предложения содержат разное число слов. Самый простой выход — сложить все вектора, получив таким образом результирующий вектор предложения. Приведя все такие вектора к единичной длине, получаем пригодные входные данные. Такое представление часто называется «neural bag of words” (NBoW) — «нейронная сумка слов», поскольку порядок слов в нем теряется. Плюсом данного алгоритма является крайняя простота реализации (имея под рукой вектора слов и любую библиотеку с реализацией нейронных сетей или другого классификатора, можно сделать рабочий вариант за 10 — 20 мин). При этом результаты иногда превосходят другие более сложные алгоритмы, оставаясь, правда, далеко от максимально возможных (сие, впрочем, зависит от задачи — например, при классификации текстов на отзывы о товарах/описания товаров/прочее, NBoW у нас показал 92% точности на тестовой выборке, против 86% алгоритма использующего логистическую регрессию и тщательно подобранный вручную словарь).

Однако, мы уже говорили о том, что для классификации предложений важен порядок слов. Чтобы сохранить порядок слов, вектор предложения можно образовать, соединив все вектора слов «голова к хвосту», в один длинный вектор, если конечно придумать, как передать нейронной сети входные данные разной длины.

Маленькое отступление — внимательный читатель должен возмутиться — я обещал классификатор предложений без предварительной обработки, но поделить предложение на слова и преобразовать в вектор — это уже предварительная обработка. Поэтому я должен был написать «с минимальной предварительной обработкой». Но это звучит не так хорошо, и к тому же по сравнению с обычными методами, такая обработка, в общем-то, и не считается. На сегодня есть методы, которые позволяют работать непосредственно с буквами, позволяя избежать деления предложения на слова и преобразования в вектора, но это опять отдельная тема.

Вернемся к проблеме разной длины входных данных. У нее есть разные решения, но мы пока рассмотрим одно — а именно сверточный фильтр. Идея простая — мы берем один нейрон и подаем на вход два (или более) слова (см. рис 2). Потом мы сдвигаем вход на одно слово и повторяем операцию. На выходе мы имеем представление предложения, которое в два (или в n) раз меньше оригинального. При этом таких фильтров обычно создается несколько (от 10 до 100). Далее операцию можно повторить, поставив над первым слоем, второй такой же, использующий входные значения первого пока все предложение не будет свернуто, либо, на определенном этапе выбрать максимальное значение активации нейрона (так называемый слой объединения — pooling layer). За счет этого, последний слой нейронов получает представление фиксированной длины, и уже он предсказывает нужную категорию предложения.



Главный результат — нейронная сеть получает возможность строить иерархические модели, формируя в каждом следующем слое более абстрактные представления предложения. Существует ряд модификаций, которые отличаются деталями архитектуры, при этом некоторые из них не только получают лучшие из известных результатов в классификации предложений на стандартных задачах, но и могут быть обучены для перевода с одного языка на другой.

Вообще такие сети похожи на те, которые успешно используются в распознавании изображений, что опять же приятно (одна архитектура на все задачи). Правда совсем одна не получается — у сетей обрабатывающих текст, все-таки есть свои особенности.

Опыты: Для опытов реализацию нейронной сети нам пришлось написать самостоятельно. Мы не хотели этого делать, но вышло так, что это проще, чем применить некоторые готовые библиотеки, которые к тому же сложно потом перенести в конечное приложение. Ну и полезно в плане самообразования. Тем более что данная реализация нам нужна не сама по себе (хотя и это полезно), а как часть большой системы.

В нашей тестовой реализации простой сверточной сети три слоя, — один сверточный слой, один слой объединения, и верхний полностью соединенный слой (как на первом рисунке), который выдает собственно классификацию. Все это следует примерно описанию системы из работы Kim et al, 2014 – там же есть и иллюстрация, которую я не буду копировать сюда, чтобы не думать про авторские права лишний раз.

В качестве объекта тестирования взяли стандартный набор положительных и отрицательных предложений о кинофильмах на английском языке. Вот что получилось:

Алгоритм Точность классификации
NBoW 68%
Сверточная сеть, 8 фильтров 74.3%
Сверточная сеть, 16 фильтров 77.8%


В целом, получилось достаточно неплохо. Лучший опубликованный результат на этих данных для таких сетей сейчас составляет 83% с 100 фильтрами (см. статью выше), а лучший результат с помощью ручного подбора признаков — 77.3%.

Комментарии (21)


  1. ffriend
    25.04.2015 14:50

    Не совсем понятно, в чём здесь принципиальное отличие от n-грамм? По сути, и там и там в качестве признаков просто берутся не отдельные слова, а пары, тройки и т.д. слов.


    1. Durham Автор
      25.04.2015 17:27

      Как известно, чем длиннее n-грамма, тем реже она встречается. Распределенное представление слов позволят обобщить скажем «хороший фильм» на отсутствующие в обучающей выборке «отличное кино», «фильм замечательный» и т.п. Во-вторых, многослойные модели могут восстанавливать порядок следования между разными n-граммами, вплоть до того, что могут выучивать процедуры, аналогичные грамматическому разбору предложения, что позволяет системе понимать дальние зависимости между словами


      1. ffriend
        25.04.2015 21:28

        Распределенное представление слов позволят обобщить скажем «хороший фильм» на отсутствующие в обучающей выборке «отличное кино», «фильм замечательный» и т.п.

        Так насколько я понимаю, представление здесь как раз «плотное», т.е. из предложения «мама мыла раму» мы суммарно можем получить [«мама», «мыла», «раму», «мама_мыла», «мыла_раму», «мама_мыла_раму»], но не «мама_раму», правильно?


        1. mephistopheies
          26.04.2015 00:11

          в примере "мама мыла раму" если ширина окна свертки 2, но на первом слое действительно не будет ни одного фильтра который ловит «мама — раму», но добавление второго скрытого слоя позволит отловить пару «мама мыла» + «мыла раму», и такой нейрон вполне будет детектировать факт того, что мама и рама связаны

          вот в этой статье авторы исследуют вопрос о том что же выучивают нейроны от слоя к слою и получают такую вот визуалицию

          большая картинка
          image


          1. ffriend
            26.04.2015 01:01

            Картинка как раз показывает стандартные свёрточные сети для изображений. Суть таких сетей в том, что вместо оценки распределения на всём изображении (а для картинки в 100x100 пикселей это 10к случайных переменных/входных нейронов) берутся небольшие участки, скажем, 11x11 пикселей (121 переменная). Такой фильтр обучить гораздо проще, а пиксели, которые находятся рядом, с гораздо большей вероятностью окажутся связанными между собой (читать как «несущими повторяющиеся паттерны»), чем те, которые находятся в разных углах картинки.

            Свёрточные сети для текста в рамках одного слоя работают так же — обучают фильтры на «локальных участках» текста, как то «мама мыла» и «мыла раму». Но наверх поднимаются уже не конкретные слова, а признаки, отражающую всю пару, т.е. собственно биграмму. Алгоритм видит слово «мала мыла» — сигнал 1, видит что-то другое — сигнал 0. Как тогда может получится «мама — раму» — непонятно.

            Могу предположить, что признаки по аналогии с визуальными фильтрами «агрегируются», т.е. если алгоритм видит «мама протирала», то наверх поднимает сигнал `w1 * 1 + w2 * 0`, т.е. продвигает вперёд первое слово (с соответствующим весом), но игнорирует второе. Тогда получается частичное совпадение с паттерном, и при некотором стечении информации модель может выучить связь между «мама» и «раму». Но связь между «хороший фильм» и «отличное кино» всё равно остаётся чем-то странным.

            Я понимаю, что что-то в этом есть, но в описании, честно говоря, много пробелов. Так что, видимо, всё-таки придётся читать оригинальную статью :D


            1. ServPonomarev
              26.04.2015 09:42
              +1

              Тут всё просто. По факту, мы имеем окошко в одно слово справа. При сдвиге на одну позицию получается окошко на одно слово слева. Итого — такое вот комбинированное окно на одно на три слова (целевое и слева/справа). По этому окну уже можно векторизовать слова по контексту. А поскольку «хороший» и «отличный» по контексту подобны, то и реагировать на них следующий слой ИНС будет. В этом — кардинальное отличие от биграм.

              Смотрите:

              есть куча примеров — отличный банк, хороший банк, отличный спектакль, хороший спектакль, но в обучающей выборке нет ни одного отличного фильма, только хорошие. Биграммы, не имея ни одного отличного фильма, ничего не сделают — ноль он и есть ноль. А векторизация ИНС покажет, что отличный и хороший — взаимозаменяемые слова в обучающих контекстах, поэтому будет хорошо реагировать на отличный фильм (который ни разу в выборке не фигурировал)


              1. ffriend
                26.04.2015 10:39

                Да, про частичное совпадение я понял, спасибо. Но что касается фраз «хороший фильм» и «отличное кино», то между ними нет ни одного общего слова, ни слева, ни справа. Тогда непонятно, почему «хороший фильм» может быть обобщён до «отличное кино», но не до «ужасное кино» или даже «банк булочка».


                1. ServPonomarev
                  26.04.2015 11:32
                  +1

                  Вот степень близости слова хороший, по контексту:

                  Введите слово: хороший

                  0.725336 отличный
                  0.706231 лучший
                  0.695592 лудший
                  0.675475 недорогой
                  0.672351 лучщий
                  0.669878 лутший
                  0.666732 лучьший
                  0.663715 лучшый
                  0.660743 плохой
                  0.654255 луший
                  0.652924 нормальный
                  0.652379 клевый
                  0.64917 качественный
                  0.646531 лутьший
                  0.646344 надежный
                  0.634561 лчший
                  0.632026 красивый
                  0.621539 обычный

                  Вот слово кино:

                  Введите слово: кино

                  0.688375 кинофильм
                  0.670716 фильм
                  0.670262 кино2014
                  0.655078 фильмов
                  0.636523 фильмы
                  0.627678 фильми
                  0.626337 кинофильмы
                  0.62412 филм
                  0.61639 сериал

                  Итого, хороший фильм = 0.72*0.67 = 0.48 от отличного кино.

                  Это если в лоб. Но есть способ оценивать слова не по отдельности, а в комбинации. Тогда:

                  Слово______________хороший фильм______плохой фильм
                  отличное кино _______0.511512____________0.41835
                  ужасное кино________0.499768____________0.486588
                  банк булочка________-0.0707078___________-0.0777618

                  Самостоятельно поэкспериментировать с векторными репрезентациями можно тут servponomarev.livejournal.com/7667.html


                  1. ffriend
                    26.04.2015 15:44

                    Так, стоп. В ЖЖ вы говорите про word2vec, который основан на skip-граммах и векторном пространстве слов. Сравнение близости слов, насколько я помню, там делается через косинусное расстоение. Это всё круто и понятно, но каким образом оно связано со свёрточной сетью из данной статьи?


                    1. Durham Автор
                      26.04.2015 15:49

                      Как написано в статье, вектора слов являются входными данными для рассматриваемой реализации сверточной сети. Соотвественно, сеть пользуется всем возможностями данного представления — т.е. фильтр выучивает не строго вектор слова «фильм», а может реагировать на слова пропорционально их близости в векторном пространстве


                      1. ffriend
                        26.04.2015 20:56

                        А, т.е. когда вы говорите «вектора слов», вы подразумеваете вектора слов в сжатом пространстве «концепций»? Т.е. сначала каждое слово прогоняется через алгоритм сжатия (какой-нибудь PCA или автокодировщик), а потом уже подаётся на вход свёрточной сети? Тогда да, согласен, входные термы будут обобщаться. Но тогда получается, что под свёртку попадают не пары «мама мыла» и «мыла раму», а как раз эти абстрактные концепции, т.е.:

                        conv(vector("мама"), vector("мыла"))
                        conv(vector("мыла"), vector("раму"))
                        


                        Правильно я понял?


                        1. Durham Автор
                          26.04.2015 21:41
                          +1

                          Тогда уж скорее так:

                          conv([vector(«мама»),vector(«мыла»),vector(«раму»)],w,'valid') где w — вектор весов (прошу прощения, если подзабыл синтаксис matlab'а)

                          т.е. рама с мылом непосредственно не свертывается. И тут еще один момент не отражен — свертка идет с шагом, равным (обычно) длине вектора, представляющего одно слово. В остальном — верно


                          1. ffriend
                            26.04.2015 21:54

                            Вот теперь всё стало на место, спасибо :)


            1. mephistopheies
              26.04.2015 12:06

              image

              The figure shows two layers of a CNN. Layer m-1 contains four feature maps. Hidden layer m contains two feature maps (h^0 and h^1). Pixels (neuron outputs) in h^0 and h^1 (outlined as blue and red squares) are computed from pixels of layer (m-1) which fall within their 2x2 receptive field in the layer below (shown as colored rectangles). Notice how the receptive field spans all four input feature maps. The weights W^0 and W^1 of h^0 and h^1 are thus 3D weight tensors. The leading dimension indexes the input feature maps, while the other two refer to the pixel coordinates.

              вот гляньте сюда — следующий слой аггрегирует изображения полученные прогоном оригинала через каждый фильтр из банка, таким образом следующий вполне может «схватить», или скажем сконструировать такую фичу как «мама — раму» которая активируется при условии что две предыдущие «мама мыла» и «мыла раму» тоже активированы


              1. ffriend
                26.04.2015 20:57

                Да, это как раз то, что я выше назвал неполным совпадением с паттерном. Спасибо за ответ!


  1. Tiendil
    25.04.2015 23:24

    Сравнивали точность классификации с какими-нибудь простыми статистическими методами?


    1. Durham Автор
      26.04.2015 12:32

      По литературным данным в задачах классификации предложений, в среднем сверточные нейронные сети работают существенно лучше, чем например, Naive Bayes, или SVM с n-граммами. На том же наборе данных MR (предложения из отзывах о фильмах), простые методы показывают порядка 60-70% точности. В задачах классификации длинных текстов ситуация не так однозначна.

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


      1. ffriend
        26.04.2015 21:15

        Всё зависит от ситуации. Как-то, когда это ещё не было модно, мы делали сентиментный анализ твитов. После апробирования 15+ моделей (правда, без свёрточных сетей — тогда они для текста ещё активно не использовались) лучший результат показал обычный наивный Байес с корнями слов и POS-тегами, дав что-то порядка 87%. Т.е. все остальные модели даже с теми же признаками давали от силы 80%, а байесовский классификатор из коробки обошёл их сразу почти на 10%. Насколько я понял, такой метод выстрелил за счёт слабой структуры текста в твитах: предложения сокращаются, слова перемешиваются с хеш-тегами и ссылками. Так или иначе, всегда полезно сначала попробовать простые модели, а потом уже иди в направлении решения конкретных проблем.


  1. andy1618
    26.04.2015 02:28
    +1

    Спасибо, интересно!
    Было бы здорово ещё посмотреть глазами на 2-3 примера ошибок классификатора, чтобы понять масштаб бедствия.
    А то, порою, люди такие отзывы пишут, что и нейронная сеть человека не разберётся, позитив это или негатив :)


    1. Durham Автор
      26.04.2015 12:26
      +1

      На английской выборке предложений (отзывы о фильмах)

      положительные, ошибочно отнесенные к отрицательным

      charming and witty, it's also somewhat clumsy
      [allen] manages to breathe life into this somewhat tired premise
      .

      Отрицательные, отнесенные к положительным
      yo, it's the days of our lives meets electric boogaloo
      do we really need another film that praises female self-sacrifice?


      Из опытов с русскими предложениями (отзывы о телефонах, три класса — положительный, отрицательный, нейтральный)

      Отрицательные, отнесенные к нейтральным:

      «Такое чувство, как будто забивается оперативная память, или тому подобное»

      Положительные, отнесенные к нейтральным:
      «Жесткий диск 4 гигабайта, наличие стандартного разъема 3,5 для наушников»
      «Недостатков у этого телефона нет»


      но это пока на стадии предварительных опытов


      1. andy1618
        26.04.2015 12:43

        Да, во всех случаях человек бы смог разобраться с тональностью, и это хорошо — значит, есть потенциал для улучшения системы.