Время прочтения: 5 мин.

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

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

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

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

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)]Расторжение договора и возврат денежных средств

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

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

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