Поиск похожих инцидентов и заявок. Метрики и оптимизация

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

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

Под катом я расскажу про:


  • сбор отзывов на рекомендации
  • выработку метрик для оценки качества рекомендаций
  • построение цикла оптимизации модели
  • полученные инсайты и новую модель


Сбор отзывов

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

Отзывы было решено собирать в предельно простом формате:


  • номер инцидента, который анализируем
  • номер инцидента, который был рекомендован
  • отзыв на рекомендацию: хорошо / плохо

«Голосовалку» (маленький проект, который принимал GET запросы с параметрами, и складывал информацию в файл) разместили непосредственно в блоке рекомендаций, чтобы аналитики могли оставить свой отзыв немедленно, просто кликнув на одну из ссылок: «хорошо» или «плохо».

Дополнительно, для ретроспективного просмотра рекомендации, было сделано очень простое решение:


  • для большого куска исторических данных был запущена модель;
  • собранные рекомендации были представлены в виде нескольких standalone HTML файлов, в которых использовалась та-же «голосовалка»;
  • подготовленные файлы были розданы аналитикам для того, чтобы просмотреть результаты для 50–100 инцидентов.

Так удалось собрать данные по примерно 4000+ парам инцидент-рекомендация.


Первичный анализ отзывов

Начальные метрики были «так себе» — доля «хороших» рекомендаций, по оценке коллег, составляла всего порядка 25%.

Основные проблемы первой модели:


  1. инциденты, по «новым» проблемам получали от системы нерелевантные рекомендации; Оказалось, что в отсутствие совпадений по содержанию обращения, система подбирала близки инциденты по отделу обратившегося сотрудника.
  2. рекомендации для инцидента по одной системы попадали инциденты от других систем. Слова, использованные в обращении были похожи, но описывали проблемы других систем и были другие.

Возможными путями повышения качества рекомендаций были выбраны:


  • корректировка состава и веса атрибутов обращения, которые включены в финальный вектор
  • подбор настроек векторизации TfidfVectorizer
  • подбор расстояния «отсечки» рекомендаций


Выработка критериев качества и методики оценки

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


Что можно получить из собранных отзывов

У нас есть множество m кортежей вида: «Инцидент», «Рекомендованный инцидент», «Оценка рекомендации».


  • «Оценка рекомендации» (v) — задается бинарно: «Хорошо» | «Плохо» (1 / -1);
  • «Инцидент» и «Рекомендованный инцидент» — просто номера инцидентов. По ним можно найти инцидент в базе.

Имея такие данные, можно посчитать:


  • n_inc_total — Общее количество инцидентов, для которых есть рекомендации
  • n_inc_good — Количество инцидентов, для которых есть «хорошие» рекомендации
  • avg_inc_good — Среднее количество «хороших» рекомендаций для инцидентов
  • n_rec_total — Общее количество рекомендаций
  • n_rec_good — Общее количество «хороших» рекомендаций
  • pct_inc_good — доля инцидентов, для которых есть «хорошие» рекомендации
    pct_inc_good = n_inc_good / n_inc_total
  • pct_rec_good — общая доля «хорошие» рекомендации
    pct_rec_good = n_rec_good / n_rec_total

Эти показатели, посчитанные на основе оценок от пользователей, можно рассматривать как «базовые показатели» исходной модели. С ней мы будем сравнивать аналогичные показатели новых версий модели.

Возьмем все уникальные «инциденты» из m, и прогоним их через новую модель.

В результате получим множество m* кортежей: «Инцидент», «Рекомендованный инцидент», «Расстояние».
Здесь «расстояние» — метрика, определенная в NearestNeighbour. В нашей модели это косинусное расстояние. Значение »0» соответствует полному совпадению векторов.


Подбор «расстояния отсечки»

Дополнив набор рекомендаций m* информацией об истинной оценке v из исходного набора оценок m, получим соответствие дистанции d и истиной оценки v для данной модели.

Имея набор (d, v) можно подобрать оптимальный уровень отсечки t, что для в d <= t рекомендация будет "хорошей", а для d > t — «плохой». Подбор t можно осуществить, проводя оптимизацию простейшего бинарного классификатора v = -1 if d>t else 1 по гиперпараметру t, и используя, например, AUC ROC в качестве метрики.

# Бинарный классификатор для оптимизации
class BinarizerClassifier(Binarizer):

    def transform(self, x):
        return np.array([-1 if _x > self.threshold else 1 for _x in np.array(x, dtype=float)]).reshape(-1, 1)

    def predict_proba(self, x):
        z = self.transform(x)

        return np.array([[0 if _x > 0 else 1, 1 if _x > 0 else 0] for _x in z.ravel()])

    def predict(self, x):
        return self.transform(x)

# 
# тут другой код: 
#   - подготовка пайплайна, 
#   - получение рекомендации для m*
#   - получение пар (d,v) в z_data_for_t
# 

# подбор параметра t

b = BinarizerClassifier()

z_x = z_data_for_t[['distance']]
z_y = z_data_for_t['TYPE']

cv = GridSearchCV(b,
                      param_grid={'threshold': np.arange(0.1, 0.7, 0.01)},
                      scoring='roc_auc',
                      cv=5, iid=False,
                      n_jobs=-1)

cv.fit(z_x, z_y)

score = cv.best_score_
t = cv.best_params_['threshold']
best_b = cv.best_estimator_

Полученное значение t можно использовать для фильтрации рекомендаций.

Конечно, такой подход может все еще пропускать «плохие» рекомендации и отсекать «хорошие». Поэтому, на данном этапе мы всегда показываем «Top 5» рекомендаций, но специально помечаем те, которые считаются «хорошими», с учетом найденного t.
Альтернативный вариант: если найдена хотя бы одна «хорошая» рекомендация, то показывать только «хорошие». Иначе показывать все имеющиеся (тоже — «Top N»).


Предположение для сравнения моделей

Для обучения моделей используется один и тот-же корпус инцидентов.
Предположим, что если ранее была найдена «хорошая» рекомендация, то новая модель тоже должна найти «хорошую» рекомендацию для того же инцидента. В частности новая модель может найти те же «хорошие» рекомендации, что и старая. Однако, с новой модель мы рассчитываем, что количество «плохих» рекомендаций станет меньше.

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

Учет «хороших» рекомендаций для множества m* можно вести одним из двух способов:


  1. на основании найденного t: считать, что все рекомендации из m* с d<t — «хорошими», и учитывать их для расчета метрик
  2. на основании соответствующих истинных оценок из множества m: из рекомендаций m* отбирать только те, для которых есть истинная оценка в m, и отбрасывать остальные.

В первом случае «абсолютные» показатели (n_inc_good, n_rec_good) новой модели должны быть больше, чем для базовой модели. Во втором случае — показатели должны приближаться к показателям базовой модели.
Проблема второго способа: если новая модель лучше исходной, и она находит что-то ранее неизвестное — такая рекомендация не будет учтена в расчете.


Выбор параметров сравнения моделей

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


  • среднее количество «хороших» рекомендаций на инцидент (avg_inc_good)
  • количество инцидентов, для которых есть «хорошие» рекомендации (n_inc_good).

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

benchmark_agv_inc_good = avg_inc_good* / avg_inc_good
benchmark_n_inc_good = n_inc_good* / n_inc_good

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

composite = 2 / ( 1/benchmark_agv_inc_good + 1/benchmark_n_inc_good)


Новая модель и ее оптимизация

Для новой модели в финальный вектор, представляющий инцидент, добавим компоненты отвечающие за «область инцидента» (одна из нескольких систем, обслуживаемых нашей командой).
Информацию о подразделении и расположении сотрудника, создавшего инцидент, тоже вынесена в отдельный векторный компонент. Все компоненты имеют свой вес в финальном векторе.

p = Pipeline(
    steps=[
        ('grp', ColumnTransformer(
            transformers=[
                ('text',
                 Pipeline(steps=[
                     ('pp', CommentsTextTransformer(n_jobs=-1)),
                     ("tfidf", TfidfVectorizer(stop_words=get_stop_words(),
                                               ngram_range=(1, 3),
                                               max_features=10000,
                                               min_df=0))
                 ]),
                 ['short_description', 'comments']
                 ),
                ('area',
                 OneHotEncoder(handle_unknown='ignore'),
                 ['area']
                 ),
                ('dept',
                 OneHotEncoder(handle_unknown='ignore'),
                 ['u_impacted_department']
                 ),
                ('loc',
                 OneHotEncoder(handle_unknown='ignore'),
                 ['u_impacted_location']
                 )
            ],
            transformer_weights={'text': 1, 'area': 0.5, 'dept': 0.1, 'loc': 0.1},
            n_jobs=-1
        )),
        ('norm', Normalizer()),
        ("nn", NearestNeighborsTransformer(n_neighbors=10, metric='cosine'))
    ],
    memory=None)

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


  • параметры векторизации TF-IDF — используемые n-граммы (ngram_range), размер словаря (max_features), минимальное вождение термина (min_df)
  • вклад компонентов в финальный вектор — transformer_weights.

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


Цикл подбора параметров

Как сравнивать подбирать уровень осечки и сравнивать модели между собой уже определили. Теперь можно перейти к оптимизации через подбор гиперпараметров.

Цикл оптимизации

param_grid = {
    'grp__text__tfidf__ngram_range': [(1, 1), (1, 2), (1, 3), (2, 2)],
    'grp__text__tfidf__max_features': [5000, 10000, 20000],
    'grp__text__tfidf__min_df': [0, 0.0001, 0.0005, 0.001],
    'grp__transformer_weights': [{'text': 1, 'area': 0.5,  'dept': 0.1, 'loc': 0.1},
                                 {'text': 1, 'area': 0.75, 'dept': 0.1, 'loc': 0.1},
                                 {'text': 1, 'area': 0.5,  'dept': 0.3, 'loc': 0.3},
                                 {'text': 1, 'area': 0.75, 'dept': 0.3, 'loc': 0.3},
                                 {'text': 1, 'area': 1, 'dept': 0.1, 'loc': 0.1},
                                 {'text': 1, 'area': 1, 'dept': 0.3, 'loc': 0.3},
                                 {'text': 1, 'area': 1, 'dept': 0.5, 'loc': 0.5}],
}

for param in ParameterGrid(param_grid=param_grid):
    p.set_params(**param)
    p.fit(x)

    ...


Результаты оптимизации

В таблице приведены результаты экспериментов, в которых были достигнуты интересные результаты — top 5 лучших и худших значений по контролируемым показателям.

bil2tj8dbqexqtezwmxealh3imq.png

Ячейки с показателями в таблице помечены как:


  • темно-зеленая — лучший показатель среди всех экспериментов
  • бледно-зеленый — значение показателя входит в top-5
  • темно-красный — худший показатель среди всех экспериментов
  • бледно-красный — значение показателя входит в worst-5

Лучший композитный показатель получился у модели с параметрами:

ngram_range = (1,2)
min_df = 0.0001
max_features = 20000
transformer_weights = {'text': 1, 'area': 1, 'dept': 0.1, 'loc': 0.1}

Модель с этими параметрами показала улучшение композитного показателя по сравнению с исходной моделью 24%


Некоторые наблюдения и выводы

По результатам оптимизации:


  1. Использование триграмм (ngram_range = (1,3)), похоже, не оправдано. Они раздувают словарь и слабо повышают точность по сравнению с биграммами.


  2. Интересное поведение при построение словаря только из биграмм (ngram_range = (2,2)): «точность» рекомендаций возрастает, а количество найденных рекомендаций — падает. Прямо как баланс precision/recall в классификаторах. Аналогичное поведение наблюдается в подборе уровня отсечки t — для биграмм характерен более узкий «конус» отсечки и лучшее разделение «хороших» и «плохих» рекомендаций.


  3. Ненулевой параметр min_df, наряду с биграммами, повышает точность рекомендаций. Они начинают быть основанными на терминах, которые встречаются как минимум несколько раз. При росте параметра словарь начинает быстро сокращаться. Для маленьких выборок, как в нашем случае, наверное, понятнее будет оперировать количеством документов (целое значение min_df), чем долей документов (дробное значение min_df), содержащих термин.


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


Появились некоторые новые идеи:


  • добавить компонент «времени», чтобы среди похожих инцидентов приоритет в выдаче имели недавние инциденты.
  • посмотреть, как будет влиять введение параметра max_df — хотя при tf-idf слишком общие слова для корпуса не должны иметь существенного веса, по определению.
  • попробовать, наконец-то, другие способы векторизации содержания, например на базе word-to-vector, или на базе свертки tf-idf представления с помощью сетей.

© Habrahabr.ru