Принимайте участие в зарплатном опросе! Уже собрано почти 8 000 анкет.
×Закрыть

Применяем машинное обучение для сбора обратной связи от пользователей

Меня зовут Александр Белобородов, я .NET Developer в Community Management Department в Plarium. Наша команда разрабатывает инструменты для оптимизации работы агентов поддержки и комьюнити-менеджеров, а также инструменты вовлечения пользователей вне игры. Хочу поделиться нашим опытом использования машинного обучения для сбора обратной связи от игроков.

Зачем это нужно

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

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

Ежедневно игроки оставляют от 250 до 3 500 комментариев. Проанализировать их вручную и составить объективную картину отношения пользователей к игре достаточно затратно по времени, поэтому мы решили автоматизировать этот процесс. Эта функциональность стала частью большого проекта по оптимизации работы в социальных сетях.

В итоге мы разработали инструмент, который выполняет следующие функции:

  • агрегирует все профили в один веб-интерфейс;
  • считает статистику личных сообщений;
  • считает статистику публикаций и комментариев;
  • подсчитывает соотношение положительных, нейтральных и негативных комментариев;
  • считает количество лайков и репостов;
  • визуализирует статистику перехода по коротким ссылкам в игру, а также статистику охвата.

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

Немного теории

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

Больше о методе можно почитать по ссылкам, приведенным в конце статьи.

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

  • «Один против всех»: обучается N классификаторов, где N — количество классов.
    Классификатор с самым высоким значением функции выхода
    присваивает новый объект к определенному классу. Здесь идет сопоставление типа «самолёт / всё, что не самолёт», «дом / всё, что не дом» и т. д.
  • «Один против одного»: обучается N классификаторов, только теперь объект присваивается к тому классу, к которому его отнесло большинство классификаторов. Стратегия напоминает то, как проводятся соревнования в футбольной лиге: команды играют между собой, и та, которая побеждает максимальное количество раз, становится победителем.

Преимущества использования метода опорных векторов:

  • точность классификации;
  • хорошо справляется с небольшими наборами данных;
  • хорошая обучающая способность: метод практически не нуждается в предварительной настройке, если не считать необходимость подбора функции ядра.

Недостатки метода:

  • неустойчив к шуму в исходных данных;
  • не работает с вероятностями;
  • не содержит формализованных алгоритмов выбора ядра;
  • при попытке использования в мультиклассовой задаче качество и скорость работы падают;
  • не подходит для больших объемов данных. Если обучающая выборка содержит шумовые выбросы, они будут существенным образом учтены при построении разделяющей гиперплоскости.

Области применения метода опорных векторов:

  • распознавание изображений;
  • спам-фильтры;
  • категоризация текста;
  • распознавание рукописного текста.

Метод опорных векторов в решении наших задач

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

Готовую реализацию метода мы взяли из библиотеки libsvm.net.

В результате обучения получается готовая к распознаванию модель и словарь.

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

Адаптируем библиотеку libsvm.net под задачу классификации текста

Сама библиотека libsvm.net является только реализацией метода опорных векторов. Чтобы с ее помощью классифицировать текстовые данные, необходимо написать надстройку над этой библиотекой, которая будет превращать текст в вектор признаков.

Перед тем как обучать модель, необходимо очистить входную строку от «шумных» слов. Для этого мы разработали класс StringProcessor. Суть его в том, что он содержит два метода — Normalize и GetWords.

Normalize заменяет переносы строк на пробелы и убирает фрагменты строки, которые попадают под шаблоны из списка игнорируемых регулярных выражений. Это сделано для того, чтобы легко отфильтровать управляющие конструкции в соцсетях, такие как упоминания, начинающиеся с @ на Facebook. Метод GetWords возвращает из исходной строки набор слов, одновременно убирая стоп-слова.

public class StringProcessor : IStringProcessor
  {
    private readonly SvmModelSettings _settings;

    public StringProcessor(SvmModelSettings settings)
    {
      if (settings == null) throw new ArgumentNullException("settings");
      _settings = settings;
    }

    public string Normalize(string text)
    {
      var str = text.Replace('\n', ' ');
      return _settings.IgnoredPatterns.Aggregate(str,
        (current, pattern) => Regex.Replace(current, pattern, "", RegexOptions.IgnoreCase));
    }

    public IEnumerable<string> GetWords(string text)
    {
      return
        text.Split(_settings.Delimiters, StringSplitOptions.RemoveEmptyEntries)
          .Select(w => w.ToLower())
          .Where(w => !_settings.IgnoredWords.Contains(w));
    }
  }

Основные модели классификатора выглядят так:

  public enum Emotion
  {
    PositiveOrNeutral = 1,
    Negative = -1
  }

  public class ClassifiedItem //Классифицированный образец
  {
    public Emotion Emotion { get; set; } //Тональность образца
    public string Text { get; set; } //Текст
  }

Класс SvmModelBuilder. Умеет тренировать модель, а также извлекать тренированную модель из файла.

public class SvmModelBuilder //Класс предназначен для создания модели
  {
    private readonly IStringProcessor _stringProcessor;

    public SvmModelBuilder(IStringProcessor stringProcessor)
    {
      _stringProcessor = stringProcessor;
    }

    public virtual SvmTrainedModel Train(IEnumerable<ClassifiedItem> items)
    {
      if (!items.Any())
        throw new InvalidOperationException("No data to train the model");

      var emotionArr = new List<double>();
      var vocabularySet = new HashSet<string>();
      var linewords = new List<string[]>();

      foreach (var classifiedItem in items) //строим словарь слов из полного входного набора
      {
        var words = GetWords(classifiedItem.Text).ToArray();
        vocabularySet.UnionWith(words);
        linewords.Add(words);
        emotionArr.Add((double)classifiedItem.Emotion);
      }

      var vocabulary = new Dictionary<string, int>(vocabularySet.Count);
      var sorted = vocabularySet.OrderBy(w => w).ToArray();

      //сортируем слова в словаре и проставляем индексы
      // чтобы потом исходную строку можно было превратить в вектор признаков
      for (var i = 0; i < sorted.Length; i++)
      {
        vocabulary.Add(sorted[i], i);
      }

      var problem = CreateProblem(linewords, emotionArr, vocabulary);

      //получаем модель при помощи классов библиотеки libsvm.net
      var model = new C_SVC(problem, KernelHelper.LinearKernel(), 1);
      
      //возвращаем модель, готовую к классификации
      return new SvmTrainedModel(model, vocabulary, _stringProcessor);
    }

    private static svm_problem CreateProblem(IReadOnlyCollection<string[]> lines, List<double> emotionArr, IReadOnlyDictionary<string, int> vocabulary)
    {
      return new svm_problem()
      {
        l = lines.Count, //общее количество классифицируемых комментариев

        //превращает строки в вектора признаков
        x = lines.Select(line => NodeUtils.CreateNode(line, vocabulary).ToArray()).ToArray(),

        y = emotionArr.ToArray() //вектор оценок комментариев
      };
    }

    //возвращает список слов из строки, очищенные от “шума”
    protected virtual IEnumerable<string> GetWords(string text)
    {
      var normalized = _stringProcessor.Normalize(text);
      return _stringProcessor.GetWords(normalized);
    }

    //тут извлечение модели из файла…
    //...
  }

Класс SvmTrainedModel, представляющий обученную модель. Основное его назначение в том, чтобы выполнять классификацию входной строки.

public class SvmTrainedModel
  {
    private readonly SVM _model;
    private readonly IReadOnlyDictionary<string, int> _vocabulary;
    private readonly IStringProcessor _stringProcessor;

    public SvmTrainedModel(SVM model, IReadOnlyDictionary<string, int> vocabulary, IStringProcessor stringProcessor)
    {
      if (model == null) throw new ArgumentNullException("model");
      if (vocabulary == null) throw new ArgumentNullException("vocabulary");
      if (stringProcessor == null) throw new ArgumentNullException("stringProcessor");
      _model = model;
      _vocabulary = vocabulary;
      _stringProcessor = stringProcessor;
    }

    public Emotion Classify(string text) //выполняет классификацию строки
    {
      return (Emotion)Model.Predict(NodeUtils.CreateNode(GetWords(text).ToArray(), Vocabulary).ToArray());
    }

    //возвращает список слов из строки, очищенные от “шума”
    protected virtual IEnumerable<string> GetWords(string text)
    {
      var normalized = StringProcessor.Normalize(text);
      return StringProcessor.GetWords(normalized);
    }

   //тут методы для сохранения модели и словаря в файл
   //...
  }

Класс NodeUtils. Его задача — превращать массив слов в вектор признаков, используя словарь.

public static class NodeUtils
  {
    public static IEnumerable<svm_node> CreateNode(string[] words, IReadOnlyDictionary<string, int> vocabulary)
    {
      var uniqueWords = new HashSet<string>(words);
      foreach (var uniqueWord in uniqueWords)
      {
        int i;

        //пропускаем слова, которых нет в словаре
        //т.к. мы не сможем проставить для них индекс
        if (!vocabulary.TryGetValue(uniqueWord, out i)) 
          continue;

        //считаем количество вхождений слова в текущую строку (комментарий)
        var occuranceCount = words.Count(w => string.Equals(w, uniqueWord, StringComparison.InvariantCultureIgnoreCase));

       //сохраняем индекс слова в словаре и количество его вхождений
       // в данной строке (комментарии)
        yield return new svm_node() 
        {
          index = i + 1,
          value = occuranceCount
        };
      }
    }
  }

Вот как всё вместе выглядит в нашем проекте:

В этом и есть суть классификатора на основе библиотеки libsvm.net. Остается написать обертки в виде сервисов, которые уже будут специфичны для конкретного проекта.

Проверка классификатора на реальных данных

Для проверки работы метода на реальном примере мы отобрали 5 сообществ и извлекли 1 200 комментариев из каждого. После этого комьюнити менеджеры разметили их тональность, поставив «1» положительным и нейтральным комментариям и «-1» — негативным.

Классификатор обучали на 800 комментариях каждого сообщества по отдельности.

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

После обучения классификатора мы провели оценку качества: сверили оставшиеся 400 комментариев с каждого сообщества на совпадение оценок комьюнити-менеджеров с оценками, которые поставил наш классификатор. В результате сопоставления мы получили от 5% до 15% отличий. Результат совпадений в 85% нас устроил.

Способы совершенствования классификатора

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

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

Кроме того, некоторые комментарии тяжело оценить без контекста, например, сарказм. Даже комьюнити-менеджерам иногда трудно определить, положительный ли, нейтральный или негативный комментарий без знания контекста.

Выводы

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

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


В заключение приведу полезные ссылки:

SVM Tutorial: Classify text in C# — статья-вдохновитель. Содержит пошаговую инструкцию, как использовать библиотеку libsvm.net в проекте на .NET.

Теория от ИНТУИТа — методы классификации и прогнозирования. Метод опорных векторов. Метод «ближайшего соседа». Байесовская классификация.

Классификация данных методом опорных векторов — описывает метод опорных векторов, показывает, как работает ядро.

Топ-10 data mining-алгоритмов простым языком — описание и сравнение разных методов машинного обучения.

К. В. Воронцов. Лекции по SVM — лекции по методу опорных векторов для тех, кто хочет разобраться подробнее.

В чем суть метода опорных векторов простым словами? — принцип работы классификатора объясняется ну очень простыми словами. Рекомендую новичкам.

Классификация документов методом опорных векторов — пример разработки классификатора на основе SVM.

Спасибо за внимание!

LinkedIn

18 комментариев

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

Интересная статья,
Вы визуализировали опорные векторы, выбранные путем обучения?
И сопоставьте их со словарем, чтобы увидеть, что на самом деле выбранная гиперплоскость аргументирована.
Возможно, вы решите сбалансировать это по-другому.
Что касается больших данных, я полагаю, что LSTM является правильным выбором.

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

1. Возможно имеет смысл включить пунктуацию в BoW — !, ?, :), etc которые могут быть полезны для сентимент анализа
2. Тоже самое для стоп слов типа I, their, him, etc которые помогут идентифицировать Subjective текст.
3. Двух этапная классификация, первый этап классифицирует комментарии на Subjective и Objective типы. В первом содержат сентименты — то что вы оцениваете, во втором факты и цифры без эмоциональной окраски, которые можно отфильтровать.
4. ngramm модель для построения BoW
5. SVM не лучший алгоритм для sentiment analysis, возможно будет интересен анализ алгоритмов для подобных задач
www.linkedin.com/...​t-analysis-muktabh-mayank

Спасибо за рекомендации и ссылку на статью. А чем именно пунктуация может помочь анализировать позитивный/негативный контекст при использовании SVM?

Смайлики, знаки пунктуации служат индикаторами эмоциональной окраски текста, например «?», «???», «?!» скорее всего означают негативный комментарий.

Я согласен, что «?», «???», «?!» усиливают эмоциональную окраску, но при этом не определяют её ни в положительную, ни в отрицательную сторону

Надо построить две модели с пунктуацией и без. А потом сравните, какая работает лучше в Вашем случае.

Выдвигаете предположение, пробуете, смотрите на результат — основной подход в машинном обучении ;)

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

Спасибо за статью. Возникло несколько вопросов:

  • Насколько сбалансированная выборка (соотношение классов 1/-1) для обучение и оценки модели?
  • Качество модели Вы оценивали с помощью точности (Accuracy). Лучше смотреть на Precision и Recall
  • .

Что стоило бы еще попробовать:

  • Tf-Idf вместо bag of words
  • Использовать кросс-валидацию для подбора гиперпараметров модели

Спасибо за рекомендации. Оценка сбалансированности не проводилась, т.к. сама по себе выборка была небольшая. Нам нужно было проверить, работает ли SVM в принципе для нашей задачи. Мы планируем увеличение исходной выборки на этапе обучения. Можете подробнее рассказать о том, что вы имеете ввиду под использованием кросс-валидации?

Кросс-валидация заслуживает отдельной статьи :)

Но если вкратце — Вы хотите протестировать разные ядра для SVM. Для этого Вы тренировочную выборку разбиваете на, например, 3 подвыборки. Дальше в цикле для каждого ядра берете две из трех подвыборок для обучения, а третью для проверки модели. Три результата для каждого ядра усредняете и выбираете ядро с лучшим результатом. Если схематически:

train_set = [sub_set, sub_set, sub_set]

for kernel in kernels:
     train | train | test
     train | test  | train
     test  | train | train

Я бы Вам настоятельно порекомендовал проверить Вашу модель на других метриках (Precision и Recall).

Классический пример — есть 100 отзывов. 80 — положительные, 20 — отрицательные. Для модели, которая пометит все отзывы, как положительные:
Accuracy = 0.8
Precision = 0.64
Recall = 0.8
f1-score = 0.71

Все залежить, що для них гірше:
1. промаркувати багато позитивних відгуків як негативні
2. промаркувати багато негативних відгуків як позитивні
3. зробити багато помилкових маркувань, незалежно від тональності

Решение задачи бинарной классификации заключается в поиске некой линейной функции

Не обов’язково лінійної. Ви можете застосувати нелінійне ядро і побачите, як покращиться результат.

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

Традиційно використовують кластерінг.

SVM не выдает вероятностные показатели классификации, что затрудняет
его использование в алгоритмах принятия решений.

Він видає відстань до гіперплощини, яка і є аналогією ймовірності.

SVM плохо справляется с «шумом», необходимо составлять словарь стоп-слов.

Потрібно гратися з нелінійними ядрами SVM та іншими параметрами, їх там багато. Зверніть увагу на Radial Basis Function (RBF) ядро та парамтри до нього scikit-learn.org/...​vm-plot-rbf-parameters-py

Шуми не настільки важливі, коли будете мати на порядки більше даних для тренувань, ніж зараз. Stop-words, stemming — навряд чи сильно вплинуть на точність.
Спробуйте погратися в word2vec+нейронка.

Дякую за рекомендації. Щодо підбору функції ядра йде мова у способах удосконалення класифікатора. Звісно, можно використовувати кластерінг у мультикласових задачах, у статті наведено приклад, як використовувати той же SVM, коли треба розрізняти декілька категорій

Спасибо, очень интересная статья. Я правильно понял, что метод определяет соотношение положительных и отрицательных комментариев, то есть, если первый пользователь резко негативно отреагировал, например, на очередное обновление, а пятеро с ним согласились и поддержали, то соотношение негатива к позитиву будет 1:5? :)

Это проблема оценки без контекста, о которой я писал. Если один говорит всё плохо, и ещё 5 с ним соглашаются типа «да, клёво всё расписал, дружище!», то 1 будет трактоваться как негативный, а 5 других как позитивные

Подписаться на комментарии