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

Меня зовут Александр Белобородов, я .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.

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

Похожие статьи:
Digma продолжает расширять линейку электронных книг и представляет новую серию, состоящую из трех читалок: Digma T645, Digma T635, Digma R655. Устройства...
Японская компания Sony представила новые модели своей весенней линейки Handycam 2014, которые обладают такими характеристиками, как система...
Прочитав на DOU очередную статью на тему «Как я из фокусника стал айтишником в свои 89», решил и я рассказать о том, как сменил...
Компания Samsung Electronics объявила о том, что, начиная с октября 2015 года, пассажиры столичного метро – обладатели одной из...
Оператор мобильной связи МТС объявил консолидированные финансовые и операционные результаты за четвертый квартал и...
Яндекс.Метрика