Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Можно ли за короткое время и без больших трудозатрат проанализировать обращения клиентов и выявить причины возникновения негативных отзывов? В этой статье хотим рассказать, как с помощью инструментов 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
После этих нехитрых действий обращения приняли следующий вид:
До обработки | После обработки |
'CRM+XX.XX.XXXX XXXXXXXXXXXXX К***ВА НАТАЛЬЯ ГЕОРГИЕВНА\nДата рождения застрахованного лица: XX.XX.XXXX\nу клиента на ХХ.ХХ.ХХХХ В ЛК она видит ДИД [СУММА] руб, клиента интересует почему ДИд не выплачивают, клиент просит пояснить когда ДИд ей будет выплачен, документы все направлены. просьба предоставить разъяснения\nТип задачи: Проведение экспертизы\nДУЛ: XX XX XXXXXX' | 'лк видеть дид интересовать дид выплачивать просить пояснить дид выплатить документ направить просьба предоставить разъяснение' |
Далее, для того, чтобы объединить похожие жалобы в группы необходимо было перейти от словесного представления жалоб к векторно-числовому. Очень часто для этой цели используют 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)
После некоторых дополнений списка стоп-слов получились следующие результаты:
| ТОП10 слов | ТЕМА |
AGGLOM_0 | [('справка', 1548), ('предоставление', 786), ('фнс', 565), ('взнос', 552), ('заявление', 494), ('подготовить', 427), ('уплатить', 371), ('информация', 73), ('вмср', 45), ('необходимый', 40)] | предоставление справок |
AGGLOM_1 | [('заявление', 2984), ('информация', 2627), ('полис', 2205), ('выплата', 2144), ('платеж', 1932), ('лк', 1931), ('справка', 1688), ('отображаться', 1653), ('направить', 1571), ('личный', 1460)] | обращения связанные с наступлением страховых случаев, их оплаты, а также отображением платежей в личном кабинете |
AGGLOM_2 | [('полис', 3807), ('оформить', 540), ('оплата', 443), ('заявление', 370), ('платеж', 351), ('оплатить', 312), ('заемщик', 290), ('вложение', 275), ('информация', 272), ('дело', 264)] | запросы информации о оформлении и оплате страховых продуктов |
AGGLOM_3 | [('лпу', 1100), ('застраховать', 683), ('выписка', 660), ('действие', 440), ('учреждение', 428), ('больница', 329), ('диагноз', 315), ('мед', 303), ('врач', 292), ('медицинский', 287)] | вопросы связанные с взаимодействием с ЛПУ, мед. учреждениями и врачебным персоналом |
AGGLOM_4 | [('расторжение', 459), ('возврат', 459), ('оис', 386), ('найти', 383), ('дс', 266), ('отображаться', 196), ('полис', 184), ('дсж', 142), ('заявление', 5), ('защита', 5)] | Расторжение договора и возврат денежных средств |
Нулевой и второй кластер фактически не содержали обращений, связанных с недовольством клиентов, что позволило уделить больше внимание именно запросам из оставшихся трех проблемных кластеров.
Данный метод помог нам сократить время и трудозатраты, автоматизировать ряд рутинных задач, уменьшить размер выборки для ручного анализа.
В результате нам удалось сопоставить информацию из разных БД, выявить отклонения и направить рекомендации по улучшению действующих процессов.