Weka проект для задачи распознавания тональности (сентимента)

  • Tutorial
Это перевод моей публикации на английском языке.

Интернет полон статьями, заметками, блогами и успешными историями применения машинного обучения (machine learning, ML) для решения практических задач. Кто-то использует его для пользы и просто поднять настроение, как эта картинка:

image

Правда, человеку, не являющемуся экспертом в этих областях, подчас не так просто подобраться к существующему инструментарию. Есть, безусловно, хорошие и относительно быстрые пути к практическому машинному обучению, например, Python-библиотека scikit. Кстати, этот проект содержит код, написанный в команде SkyNet (автору довелось быть её лидирующим участником) и иллюстрирующий простоту взаимодействия с библиотекой. Если вы Java разработчик, есть пара хороших инструментов: Weka и Apache Mahout. Обе библиотеки универсальны с точки зрения применимости к конкретной задаче: от рекомендательных систем до классификации текстов. Существует инструментарий и более заточенный под текстовое машинное обучение: Mallet и набор библиотек Stanford. Есть и менее известные библиотеки, как Java-ML.

В этом посте мы сфокусируемся на библиотеке Weka и сделаем проект-заготовку или проект-шаблон для текстового машинного обучения на конкретном примере: задача распознавания тональности или сентимента (sentiment analysis, sentiment detection). Несмотря на всё это, проект полностью рабочий и даже под commercial-friendly лицензией (сама Weka под GPL 3.0), т.е. при большом желании вы можете даже применить код в своих проектах.

Из всего набора в целом подходящих для выбранной задачи алгоритмов Weka мы воспользуемся алгоритмом Multinomial Naive Bayes. В этом посте я почти всегда привожу те же ссылки, что и в английской версии. Но так как перевод задача творческая, позволю себе привести ссылку по теме на отечественный ресурс по машинному обучению.

По моему мнению и опыту взаимодействия с инструментарием машинного обучения, обычно программист находится в поисках решения трёх задач при использовании той или иной ML-библиотеки: настройка алгоритма, тренировка алгоритма и I/O, т.е. сохранение на диск и загрузка с диска натренированной модели в память. Помимо перечисленных сугубо практических аспектов из теоретических, пожалуй, наиболее важным является оценка качества модели. Мы коснёмся этого тоже.

Итак, по порядку.

Настройка алгоритма классификации


Начнём с задачи распознавания тональности на три класса.

    public class ThreeWayMNBTrainer {  
        private NaiveBayesMultinomialText classifier;  
        private String modelFile;  
        private Instances dataRaw;  
      
        public ThreeWayMNBTrainer(String outputModel) {  
            // create the classifier  
            classifier = new NaiveBayesMultinomialText();  
            // filename for outputting the trained model  
            modelFile = outputModel;  
      
            // listing class labels  
            ArrayList<attribute> atts = new ArrayList<attribute>(2);  
            ArrayList<string> classVal = new ArrayList<string>();  
            classVal.add(SentimentClass.ThreeWayClazz.NEGATIVE.name());  
            classVal.add(SentimentClass.ThreeWayClazz.POSITIVE.name());  
            atts.add(new Attribute("content",(ArrayList<string>)null));  
            atts.add(new Attribute("@@class@@",classVal));  
            // create the instances data structure  
            dataRaw = new Instances("TrainingInstances",atts,10);  
        }  
      
    }  

В приведённом коде происходит следующее:

  • Создаётся объект класса алгоритма классификации (мы любим каламбуры)
  • Приводится список меток целевых классов: NEGATIVE и POSITIVE
  • Создаётся структура данных для хранения пар (объект, метка класса)


Похожим образом, но с бо́льшим количеством выходных меток, создаётся классификатор на 5 классов:

    public class FiveWayMNBTrainer {  
        private NaiveBayesMultinomialText classifier;  
        private String modelFile;  
        private Instances dataRaw;  
      
        public FiveWayMNBTrainer(String outputModel) {  
            classifier = new NaiveBayesMultinomialText();  
            classifier.setLowercaseTokens(true);  
            classifier.setUseWordFrequencies(true);  
      
            modelFile = outputModel;  
      
            ArrayList<Attribute> atts = new ArrayList<Attribute>(2);  
            ArrayList<String> classVal = new ArrayList<String>();  
            classVal.add(SentimentClass.FiveWayClazz.NEGATIVE.name());  
            classVal.add(SentimentClass.FiveWayClazz.SOMEWHAT_NEGATIVE.name());  
            classVal.add(SentimentClass.FiveWayClazz.NEUTRAL.name());  
            classVal.add(SentimentClass.FiveWayClazz.SOMEWHAT_POSITIVE.name());  
            classVal.add(SentimentClass.FiveWayClazz.POSITIVE.name());  
            atts.add(new Attribute("content",(ArrayList<String>)null));  
            atts.add(new Attribute("@@class@@",classVal));  
      
            dataRaw = new Instances("TrainingInstances",atts,10);  
        }  
    }  


Тренировка классификатора


Тренировка алгоритма классификации или классификатора заключается в сообщении алгоритму примеров (объект, метка), помещённых в пару (x,y). Объект описывается некоторыми признаками, по набору (или вектору) которых можно качественно отличать объект одного класса от объекта другого класса. Скажем, в задаче классификации объектов-фруктов, к примеру на два класса: апельсины и яблоки, такими признаками могли бы быть: размер, цвет, наличие пупырышек, наличие хвостика. В контексте задачи распознавания тональности вектор признаков может состоять из слов (unigrams) либо пар слов (bigrams). А метками будут названия (либо порядковые номера) классов тональности: NEGATIVE, NEUTRAL или POSITIVE. На основе примеров мы ожидаем, что алгоритм сможет обучиться и обобщиться до уровня предсказания неизвестной метки y' по вектору признаков x'.

Реализуем метод добавления пары (x,y) для классификации тональности на три класса. Будем полагать, что вектором признаков является список слов.

public void addTrainingInstance(SentimentClass.ThreeWayClazz threeWayClazz, String[] words) {  
        double[] instanceValue = new double[dataRaw.numAttributes()];  
        instanceValue[0] = dataRaw.attribute(0).addStringValue(Join.join(" ", words));  
        instanceValue[1] = threeWayClazz.ordinal();  
        dataRaw.add(new DenseInstance(1.0, instanceValue));  
        dataRaw.setClassIndex(1);  
    } 


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

Собственно, тренировка классификатора реализуется в одну строку:

    public void trainModel() throws Exception {  
            classifier.buildClassifier(dataRaw);  
        }  


Просто!

I/O (сохранение и загрузка модели)


Вполне распространённым сценарием в области машинного обучения является тренировка модели классификатора в памяти и последующее распознавание / классификация новых объектов. Однако для работы в составе некоторого продукта модель должна поставляться на диске и загружаться в память. Сохранение на диск и загрузка с диска в память натренированной модели в Weka достигается очень просто благодаря тому, что классы алгоритмов классификации реализуют среди множества прочих интерфейс Serializable.

Сохранение натренированной модели:

    public void saveModel() throws Exception {  
            weka.core.SerializationHelper.write(modelFile, classifier);  
        }  


Загрузка натренированной модели:

    public void loadModel(String _modelFile) throws Exception {  
            NaiveBayesMultinomialText classifier = (NaiveBayesMultinomialText) weka.core.SerializationHelper.read(_modelFile);  
            this.classifier = classifier;  
        }  


После загрузки модели с диска займёмся классификацией текстов. Для трёх-классового предсказания реализуем такой метод:

    public SentimentClass.ThreeWayClazz classify(String sentence) throws Exception {  
            double[] instanceValue = new double[dataRaw.numAttributes()];  
            instanceValue[0] = dataRaw.attribute(0).addStringValue(sentence);  
      
            Instance toClassify = new DenseInstance(1.0, instanceValue);  
            dataRaw.setClassIndex(1);  
            toClassify.setDataset(dataRaw);  
      
            double prediction = this.classifier.classifyInstance(toClassify);  
      
            double distribution[] = this.classifier.distributionForInstance(toClassify);  
            if (distribution[0] != distribution[1])
                return SentimentClass.ThreeWayClazz.values()[(int)prediction];  
            else  
                return SentimentClass.ThreeWayClazz.NEUTRAL;  
        }  


Обратите внимание на строку if (distribution[0] != distribution[1]). Как вы помните, мы определили список меток классов для данного случая как: {NEGATIVE, POSITIVE}. Поэтому в принципе наш классификатор должен быть как минимум бинарным. Но! В случае если распределение вероятностей двух данных меток одинаково (по 50%), можно совершенно уверенно полагать, что мы имеем дело с нейтральным классом. Таким образом, мы получаем классификатор на три класса.

Если классификатор построен верно, то следующий юнит-тест должен отработать верно:

    @org.junit.Test  
        public void testArbitraryTextPositive() throws Exception {  
            threeWayMnbTrainer.loadModel(modelFile);  
            Assert.assertEquals(SentimentClass.ThreeWayClazz.POSITIVE, threeWayMnbTrainer.classify("I like this weather"));  
        }  


Для полноты реализуем класс-оболочку, который строит и тренирует классификатор, сохраняет модель на диск и тестирует модель на качество:

    public class ThreeWayMNBTrainerRunner {  
        public static void main(String[] args) throws Exception {  
            KaggleCSVReader kaggleCSVReader = new KaggleCSVReader();  
            kaggleCSVReader.readKaggleCSV("kaggle/train.tsv");  
            KaggleCSVReader.CSVInstanceThreeWay csvInstanceThreeWay;  
      
            String outputModel = "models/three-way-sentiment-mnb.model";  
      
            ThreeWayMNBTrainer threeWayMNBTrainer = new ThreeWayMNBTrainer(outputModel);  
      
            System.out.println("Adding training instances");  
            int addedNum = 0;  
            while ((csvInstanceThreeWay = kaggleCSVReader.next()) != null) {  
                if (csvInstanceThreeWay.isValidInstance) {  
                    threeWayMNBTrainer.addTrainingInstance(csvInstanceThreeWay.sentiment, csvInstanceThreeWay.phrase.split("\\s+"));  
                    addedNum++;  
                }  
            }  
      
            kaggleCSVReader.close();  
      
            System.out.println("Added " + addedNum + " instances");  
      
            System.out.println("Training and saving Model");  
            threeWayMNBTrainer.trainModel();  
            threeWayMNBTrainer.saveModel();  
      
            System.out.println("Testing model");  
            threeWayMNBTrainer.testModel();  
        }  
    }  


Качество модели


Как вы уже догадались, тестирование качества модели тоже довольно просто реализуется с Weka. Вычисление качественных характеристик модели необходимо, например, для того, чтобы проверить, переобучилась ли или недоучилась наша модель. С недоученностью модели интуитивно понятно: мы не нашли оптимального количества признаков классифицируемых объектов, и модель получилась слишком простой. Переобучение означает, что модель слишком подстроилась под примеры, т.е. она не обобщается на реальный мир, являясь излишне сложной.

Существуют разные способы тестирования модели. Один из таких способов заключается в выделении тестовой выборки из тренировочного набора (скажем, одну треть) и прогоне через кросс-валидацию. Т.е. на каждой новой итерации мы берём новую треть тренировочного набора в качестве тестовой выборки и вычисляем уместные для решаемой задачи параметры качества, например, точность / полноту / аккуратность и т.д. В конце такого прогона вычисляем среднее по всем итерациям. Это будет амортизированным качеством модели. Т.е., на практике оно может быть ниже, чем по полному тренировочному набору данных, но ближе к качеству в реальной жизни.

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

    public void testModel() throws Exception {  
            Evaluation eTest = new Evaluation(dataRaw);  
            eTest.evaluateModel(classifier, dataRaw);  
            String strSummary = eTest.toSummaryString();  
            System.out.println(strSummary);  
        }  


Данный метод выводит следующие стастистики:

Correctly Classified Instances       28625               83.3455 %
Incorrectly Classified Instances      5720               16.6545 %
Kappa statistic                          0.4643
Mean absolute error                      0.2354
Root mean squared error                  0.3555
Relative absolute error                 71.991  %
Root relative squared error             87.9228 %
Coverage of cases (0.95 level)          97.7697 %
Mean rel. region size (0.95 level)      83.3426 %
Total Number of Instances            34345     


Таким образом, аккуратность модели по всему тренировочному набору 83,35%. Полный проект с кодом можно найти на моём github. Код использует данные с kaggle. Поэтому если вы решите использовать код (либо даже посоревноваться на конкурсе) вам понадобится принять условия участия и скачать данные. Задача реализации полного кода для классификации тональности на 5 классов остаётся читателю. Успехов!
  • +6
  • 10.4k
  • 9
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 9

    0
    Спасибо за статью.
    Какая производительность этого решения?
      0
      Пожалуйста.

      После Вашей просьбы я сделал случайную выборку из imdb (positive + negative тексты) и получил такую производительность (после разогрева JVM тем же тестом):

      Testing on 1000 samples, 23626 words, 122403 characters...
      Time 82 ms.
      Speed 1492.719512195122 chars/ms
      Speed 288.1219512195122 words/ms
      


        +1
        Я бы не советовал использовать такую систему для серьёзых, тем боле коммерческих приложений.
        Во-первых, анализ тональности, построенный на машинном обучении, очень зависим от предметной сферы, в которой он был натренирован (так называемая domain-dependency). Более того, даже без смены предметной сферы модели тональности очень быстро устаревают и через, скажем, месяц ваша модель начнёт «чудить». Общая ошибка — верить результатам n-fold cross validation. Да, на том же корпусе результаты будут вполне приемлимые (ок 80%), но к реальной жизни это, увы, никакого отношения не имеет.
        Во-вторых, самая большая ошибка сообщать результат по всем трём классам сразу. Обычно нейтральный класс очень многочисленный и самый простой «классификатор», который всё относит к этому классу, легко набирает и 90%. Если у вас корпус сбалансирован по трём классам, то это, скорее всего, очень далеко от жизни — крайне редко мне попадались такие предметные области, где все три класса распределенны одинаково. Как правило, нейтральные высказывания заметно более частотны. Либо наоборот — есть «ругательные» темы, где негатив зашкаливает, а есть «хвалебные» темы, где «солнце, радость, пазитифф»))
        Есть, конечно, шанс, что на очень большом корпусе можно обучить систему чему-то полезному, но рзметка такого корпуса обойдётся влетит в копеечку (мягко говоря). Так что если хотите серьёзный анализ тональности, забудьте про машинное обучение. По крайней мере в её классической форме, педставленной здесь уважаемым автором.
          0
          Я бы не советовал использовать такую систему для серьёзых, тем боле коммерческих приложений.

          Это всего лишь тьюториал по взаимодействию с библиотекой машинного обучения на примере анализа тональности. Kick-start, после которого заинтересовавшиеся пойдут делать более глубокое исследование и ставить эксперименты. А насчёт серьёзных-несерьёзных, я позволю себе поспорить (см. результаты ниже).

          Во-первых, анализ тональности, построенный на машинном обучении, очень зависим от предметной сферы, в которой он был натренирован (так называемая domain-dependency). Более того, даже без смены предметной сферы модели тональности очень быстро устаревают и через, скажем, месяц ваша модель начнёт «чудить».

          У rule-based систем такая же зависимость и устаревание ;) Вопрос ещё в том, как тренировка происходит. Например, в одной из систем я делал фичи на синтаксических шаблонах и они были более устойчивыми, потому что слова too good или not that bad так быстро не устаревают.

          Общая ошибка — верить результатам n-fold cross validation. Да, на том же корпусе результаты будут вполне приемлимые (ок 80%), но к реальной жизни это, увы, никакого отношения не имеет.

          По-хорошему, нужно выделять test корпус. Но за неимением оного на kaggle n-fold очень даже подходит. Это всего лишь инструмент, позволяющий определить приблизительное качество на данный момент. Ну в конце концов, можно и глазками качество смотреть, но увы, это не масштабируется.

          Во-вторых, самая большая ошибка сообщать результат по всем трём классам сразу. Обычно нейтральный класс очень многочисленный и самый простой «классификатор», который всё относит к этому классу, легко набирает и 90%. Если у вас корпус сбалансирован по трём классам, то это, скорее всего, очень далеко от жизни — крайне редко мне попадались такие предметные области, где все три класса распределенны одинаково. Как правило, нейтральные высказывания заметно более частотны. Либо наоборот — есть «ругательные» темы, где негатив зашкаливает, а есть «хвалебные» темы, где «солнце, радость, пазитифф»))


          Хороший верный комментарий. Но ещё раз повторюсь: цель данной статьи не показать прорывной метод для анализа тональности, а продемонстировать работу с библиотекой, ввести маломальскую терминологию и показать отправную точку. Классический такой тьюториал. Если кто-то из уважаемых читателей после этой статьи напишет хороший классификатор, хорошо работающий для их текстов, можно считать задачу выполненной :)

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

          Added 34345 instances
          Of which 27273 positive instances, 7072 negative instances
          


          Я сбалансировал распределение:

          Added 14144 instances
          Of which 7072 positive instances, 7072 negative instances
          


          и получил ещё лучше качество:

          Correctly Classified Instances       11976               84.6719 %
          Incorrectly Classified Instances      2168               15.3281 %
          Kappa statistic                          0.6934
          Mean absolute error                      0.2109
          Root mean squared error                  0.3347
          Relative absolute error                 42.1894 %
          Root relative squared error             66.9461 %
          Coverage of cases (0.95 level)          98.1123 %
          Mean rel. region size (0.95 level)      78.7719 %
          Total Number of Instances            14144
          


          0
          Я понимаю, что тьюториал, понимаю, что kick-start)) Проблема лишь в том, что он, по моему скромному мнению, он не в ту сторону kick делает)
          Правила, конечно, тоже могут устареть, но их гораздо легче контроллировать)
            0
            Ох, тут тоже буду спорить. Я давно за соединение методов rule-based и машинного обучения, а не за борьбу лагерей.

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

            В машинном обучении можно эффективно (см. научные публикации) использовать, например MTurk и cross-валидацией между аннотаторами добиться отличного корпуса, близкого к экспертной работе. Растущий качественный корпус даёт всё больший охват предметной области. Отсюда растущее качество, контролируемое «извне».
            — Конец философствования. —
              0
              По поводу MTurk есть большие сомнения. Были жалобы на качество разметки (именно по тональности — уж слишком разные у людей представления о том, что такое «хорошо» и что такое «плохо»). Кроме того, помню корпуса, по которым было где-то только около 20% совпадений между аннонтаторами. Создание корпусов — тот ещё геморрой… А для систем анализа тональности на ML этот геморрой не пройдёт никогда. Это я на своём опыте чётко понял.
              Я тоже за мир и дружбу между лагерями, но пока не очень представляю, как их «скрестить». В этой связи было бы интересно почитать про опыты с паттернами в машинном обучении. Правда, их тоже нужно делать гибкими (не регексах, например), и много. Так может получится, что просто сделать правилаполучится дешевле.
              Про золотые глаза и касту посвящнных я с не совсем согласен. Эта проблема решается довольно просто — созданием «человеческого» интерфейса у редактора правил. Задаче вполне решаемая и разовая. И да, тестовый корпус нужен. А когда он не нужен, если речь идёт об автоматической обработке текста?
              Кстати, как машинное обучение решает названные проблемы? Для поиска ошибок глаз нужен такой же «золотой»… Только понять, как их исправить сможет не всякий, даже не всякий автор такой системы. Потому что копаться в дампе модели и смотреть, какие веса каких фич сыграли свою гадкую роль в дикой ошибке класификации, — занятие не для слабонервных)
              И про растущий корпус не согласен)) Расти он, конечно, будет, только лучше он станет (если станет) ой как не скоро… Либо он должен расти пропорционально количеству обрабатываемых данных. А это ОЧЕНЬ дорого.
                0
                Представление «хорошо» и «плохо» решается произвольными пересечениями между независимыми аннотаторами. То, что попало в пересечение, наш корпус.

                Честно говоря, мне ещё не доводилось видеть лингво-интерфейсы не для лингвистов (опять же, эксперты), которые успешно работают. А сделать нелингвистами корпус как раз можно.

                Дебаггинг и там, и там несколько ужасен и приводит в трепет: а не подпортил ли я другой важный кейс?

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

                1) задачи компьютерной лингвистики интересные и очень непростые. Но не такие далёкие: на расстоянии вытянутой руки;
                2) 100% качественных методов ещё не изобрели, как и сам искусственный интеллект. Но приемлемые методы есть, а иначе как объяснить IBM Watson, вложения Facebook/Google/… в deep learning и то, почему в поиске без лингвистике не обойтись?

                Самое главное — пробудить интерес к этой области. Что, надеюсь, хотя бы отчасти, делает эта публикация.
                  0
                  Создание корпуса просто на произвольных пересечениях, конечно, хорошо, но беда в том, что часто аннотаторы, мягко говоря, плевать хотели на задание и описание классов) Найти качественного ответственного аннотатора — тот ещё квест)) Я таких практически любил и готов был на них жениться, лишь бы куда-нибудь не ушли))
                  И да, за пост спасибо. Написано чётко и ясно. Моё несогласие с рядом тезисов вполне может быть субъективным)
                  Ждём новых публикаций на эту тему.

          Only users with full accounts can post comments. Log in, please.