Застосовуємо машинне навчання для збору зворотного зв'язку від користувачів

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

Навіщо це потрібно

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

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

Щодня гравці залишають від 250 до 3 500 коментарів. Проаналізувати їх та скласти об'єктивну картину ставлення користувачів до гри досить затратно по часу, тому ми вирішили автоматизувати цей процес. Ця функціональність стала частиною великого проекту з оптимізації роботи в соціальних мережах.

У підсумку ми розробили інструмент, який виконує наступні функції:

Щоб навчити нашу систему відрізняти позитивні і негативні коментарі, ми використовували алгоритми машинного навчання, а конкретно — метод опорних векторів. Він досить простий у застосуванні і добре справляється з завданнями бінарної класифікації.

Трохи теорії

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

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

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

Переваги використання методу опорних векторів:

Недоліки методу:

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

Метод опорних векторів у вирішенні наших завдань

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

Готову реалізацію методу ми взяли з бібліотеки 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('
', ' ');
 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 Tutorial: Classify text in C# — стаття-натхненник. Містить покрокову інструкцію, як використовувати бібліотеку libsvm.net у проекті .NET.

Теорія від Інтуїта — методи класифікації та прогнозування. Метод опорних векторів. Метод «найближчого сусіда». Байєсова класифікація.

Класифікація даних методом опорних векторів — описує метод опорних векторів, показує, як працює ядро.

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

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

У чому суть методу опорних векторів простими словами? — принцип роботи класифікатора пояснюється ну дуже простими словами. Рекомендую новачкам.

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

Спасибі за увагу!

Опубліковано: 17/07/18 @ 07:00
Розділ Безпека

Рекомендуємо:

Go дайджест #4: WebAssembly and Go, Go 1.11 Beta 1, GraphQL, Apple Metal API and Go
Що почитати: огляд Telegram-каналів українських IT-фахівців
Поради сеньйорів: як прокачати знання junior C++
В ІТ без диплома: історії Technical Architect, Front-end Dev, Product Manager та інших
Centers of Excellence – майбутнє аутсорсингу?