Применяем машинное обучение для сбора обратной связи от пользователей
Меня зовут Александр Белобородов, я .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.
Спасибо за внимание!