{"id":14270,"url":"\/distributions\/14270\/click?bit=1&hash=a51bb85a950ab21cdf691932d23b81e76bd428323f3fda8d1e62b0843a9e5699","title":"\u041b\u044b\u0436\u0438, \u043c\u0443\u0437\u044b\u043a\u0430 \u0438 \u0410\u043b\u044c\u0444\u0430-\u0411\u0430\u043d\u043a \u2014 \u043d\u0430 \u043e\u0434\u043d\u043e\u0439 \u0433\u043e\u0440\u0435","buttonText":"\u041d\u0430 \u043a\u0430\u043a\u043e\u0439?","imageUuid":"f84aced9-2f9d-5a50-9157-8e37d6ce1060"}

Как Machine Learning помогает при аудите качества клиентского сервиса

Можно ли за короткое время и без больших трудозатрат проанализировать обращения клиентов и выявить причины возникновения негативных отзывов? В этой статье хотим рассказать, как с помощью инструментов ML нам удалось решить эту задачу.

В своей работе мы столкнулись с необходимостью оценить качество клиентского сервиса. Перед нами стояла задача проанализировать обращения клиентов и выявить причины возникновения негативных отзывов по постпродажному обслуживанию по продуктам страхования.

На обработку обращений тратится большое количество времени. Мы поставили себе задачу — уменьшить временные затраты на проверку с помощью инструментов машинного обучения.

Не секрет, что от качества входных данных зависит качество самой модели, поэтому независимо от выбора алгоритма первоочередной задачей является предобработка имеющейся информации. Поскольку задача относится к задачам, связанным с обработкой естественного языка, то и набор преобразований был выбран можно сказать классическим для данной области.

На первом этапе, с помощью регулярных выражений из обращений клиентов были удалены служебные поля, а также символы, не относящиеся к символам русского алфавита:

def cl_text(text): c = text.lower() c = re.sub(r'crm[^\n]+', '', c) c = re.sub(r'документ:\s*\d{2}\s?\d{2}\s?\d{6}\s*', '', c) c = re.sub(r'дул:\s*\d{2}\s?\d{2}\s?\d{6}\s*', '', c) c = re.sub(r'дата рождения( застрахованного лица)?:\s*\d{2}\.?\d{2}\.?\d{4}\s*', '', c) c = re.sub(r'дата начала действия:\s*\d{2}\.?\d{2}\.?\d{4}\s*', '', c) c = re.sub(r'дата окончания действия:\s*\d{2}\.?\d{2}\.?\d{4}\s*', '', c) c = re.sub(r'дата выдачи:\s*\d{2}\.?\d{2}\.?\d{4}\s*', '', c) c = re.sub(r'дата выдачи:[\S\W]\w*', '', c) c = re.sub(r'\n+', ' ', c) c = re.sub(r'\s+', ' ', c) c = re.sub(r"[A-Za-z!#$%&'()*+,./:;<=>?@[\]^_`{|}~—\"\-]+", ' ', c) return c.strip()

Вторым этапом стало приведение слов в обращениях в нормальную форму, попутно удаляя слова, которые ничего не значат в контексте русского языка и нашего домена. Данные стоп-слова частично позаимствованы из библиотеки NLTK и перечислены в массиве stopwords:

import pymorphy2 import nltk morph = pymorphy2.MorphAnalyzer() stopwords = nltk.corpus.stopwords.words('russian') stopwords.extend(['сообщение','документ','номер','запрос','страхование','страховой']) def lemmatize(text): text = re.sub(r"\d+", '', text.lower()) #удаление цифр из текста for token in text.split(): token = token.strip() token = morph.normal_forms(token)[0].replace('ё', 'е') if token and token not in stopwords: tokens.append(token) if len(tokens) > 2: ' '.join(tokens) return None

После этих нехитрых действий обращения приняли следующий вид:

Далее, для того, чтобы объединить похожие жалобы в группы необходимо было перейти от словесного представления жалоб к векторно-числовому. Очень часто для этой цели используют OneHotEncoding или TF-IDF. И хотя эти способы получения эмбеддингов распространены и показывают неплохие результаты в некоторых задачах, все же, у них есть серьезный недостаток – данные подходы основаны на частотных характеристиках корпуса и не учитывают семантику текста. Это означает, что, несмотря на одну и ту же смысловую нагрузку, векторы предложений «сожалеем за доставленные неудобства» и «просим прощение за возникшие трудности» не будут иметь ничего общего друг с другом, т.к. фразы состоят из разных слов.

Ввиду доступности и неплохой скорости работы нами было решено использовать модель Universal Sentence Embedder, обученной для многих языков, в числе которых и русский. Данная модель способна перевести предложения в векторное пространство с сохранением семантического расстояния между ними. Такой подход открывает перед нами возможность по оценке близости текстов по смыслу.

import tensorflow as tf import tensorflow_hub as hub import tensorflow_text model = hub.load(r'/UniverseSentenseEmbeddings/USEv3') embedding = model(‘предложение для перевода в вектор’)

Как видим, для использования данной модели достаточно написать буквально 5 строк кода. Взглянем на результат работы модели, сравнив косинусное расстояние между полученными векторами от тестовых фраз:

input1, input2 = ['большая собака'], ['крупный пёс', 'большая кошка', 'маленькая собака', 'маленькая кошка', 'старая картина'] emb1, emb2 = model(input1), model(input2) results_cosine = pairwise.cosine_similarity(emb1, emb2).tolist()[0] for i, res in enumerate(results_cosine): print('"{}" <> "{}", cos_sim={:.3f}'.format(input1[0],input2[i],results_cosine[i]))

Результат получается достаточно интересным:

"большая собака" <> "крупный пёс", cos_sim = 0.860 "большая собака" <> "большая кошка", cos_sim = 0.769 "большая собака" <> "маленькая собака", cos_sim = 0.748 "большая собака" <> "маленькая кошка", cos_sim = 0.559 "большая собака" <> "старая картина", cos_sim = 0.192

Итак, модель вполне жизнеспособна и судя по всему достаточно неплохо выявляет семантику предложений, поэтому полученные с ее помощью вектора теперь можно кластеризовать для выявления тематик.

В качестве алгоритма кластеризации были использованы 4 метода: dbscan, агломеративная, kMeans и MiniBatchKMeans. В последствии мы остановились на результате работы агломеративной кластеризации, т.к., по нашему мнению, именно этот метод наиболее адекватно разделял наш набор данных на тематические подгруппы:

from sklearn.cluster import AgglomerativeClustering num_clusters = 5 agglo1 = AgglomerativeClustering(n_clusters=num_clusters, affinity='euclidean') #cosine, l1, l2, manhattan get_ipython().magic('time answer = agglo1.fit_predict(sent_embs)')

С помощью вышеописанного подхода были получены 5 кластеров, однако нам предстояло еще выяснить, за какую тему отвечает каждая из групп. Для этого был использован простой подход – для каждого кластера были подсчитаны все входящие слова и ТОП-10 из них были представлены в качестве основной сути:

cl = {} for cluster, data in tqdm(report.groupby('AGGLOM'), desc=method): arr = ' '.join(data['НФ'].values).split() arr_morph = [] for k in arr: arr_morph.append(morph.parse(k)[0].normal_form) cl[method+'_'+str(cluster)] = Counter([x.replace('ё', 'е') for x in arr_morph if x not in stopwords]).most_common(10)

После некоторых дополнений списка стоп-слов получились следующие результаты:

Нулевой и второй кластер фактически не содержали обращений, связанных с недовольством клиентов, что позволило уделить больше внимание именно запросам из оставшихся трех проблемных кластеров.

Данный метод помог нам сократить время и трудозатраты, автоматизировать ряд рутинных задач, уменьшить размер выборки для ручного анализа.

В результате нам удалось сопоставить информацию из разных БД, выявить отклонения и направить рекомендации по улучшению действующих процессов.

0
5 комментариев
Sergei Zotov
После этих нехитрых действий обращения приняли следующий вид

Вот здесь видно, что так же пропало и имя с отчеством. Это из-за чего произошло? Из-за stopwords NLTK? Вроде в регулярках не вижу ничего подобного

Ответить
Развернуть ветку
NTA
Автор

Регулярное выражение re.sub(r’crm[^\n]+’, ’’, c) в данном случае убрало строчку с именем и отчеством. Кроме того, нужно понимать, что в оригинальном коде функция cl_text имела куда больше строк кода, в статье её усеченная версия, чтобы не перегружать статью.

Ответить
Развернуть ветку
Sergei Zotov

спасибо, круто!

Ответить
Развернуть ветку
Grapsus

ML значительно упрощает задачи. Главное, правильно составить алгоритмы. 

Ответить
Развернуть ветку
NTA
Автор

Dasha, да, согласны! Очень упрощает!

Ответить
Развернуть ветку
2 комментария
Раскрывать всегда