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

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

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

Похожие статьи:
Багато українських айтівців регулярно допомагають українській армії грошима: хтось донатить на ЗСУ 50% або й 80% зарплати, іншим уже...
Японский оператор мобильной связи NTT DOCOMO представил новую зимнюю коллекцию мобильных терминалов и планшетов для своих абонентов,...
Меня зовут Максим, и я тестировщик. С интересом слежу за событиями в мире тестирования и IT. Собираю самое полезное...
Длительность курса: 120 академических часов (3 месяца): 3 занятия по 3 часа в неделю. График занятий: понедельник,...
В выпуске: что нового в C# 7.0, с чего начинать в .NET Core 1.1, анонс .NET Framework 4.7, как организовать unit тесты с Autofixture...
Яндекс.Метрика