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