Recherchez des incidents et des réclamations similaires. Mesures et optimisation

Dans un article précédent, j'ai parlé de notre moteur de recherche pour des applications similaires . Après son lancement, nous avons commencé à recevoir les premiers avis. Les analystes ont aimé et recommandé certaines recommandations, d'autres non.


Pour avancer et trouver de meilleurs modèles, il a d'abord fallu évaluer les performances du modèle actuel. Il était également nécessaire de sélectionner des critères permettant de comparer les deux modèles.


Sous la coupe, je vais parler de:


  • recueillir des commentaires sur les recommandations
  • élaboration de paramètres pour évaluer la qualité des recommandations
  • construire un cycle d'optimisation de modèle
  • reçu des idées et un nouveau modèle

Collecte de commentaires


Il serait idéal de recueillir des commentaires explicites des analystes: quelle est la pertinence de la recommandation de chacun des incidents proposés. Cela nous permettra de comprendre la situation actuelle et de continuer à améliorer le système sur la base d'indicateurs quantitatifs.


Il a été décidé de collecter les avis dans un format extrêmement simple:


  • nombre d'incidents que nous analysons
  • numéro d'incident recommandé
  • examen des recommandations: bon / mauvais

Le "vote" (un petit projet qui a accepté les demandes GET avec des paramètres et a mis les informations dans un fichier) a été placé directement dans le bloc de recommandations afin que les analystes puissent laisser leurs commentaires immédiatement en cliquant simplement sur l'un des liens: "bon" ou "mauvais".


De plus, pour un examen rétrospectif de la recommandation, une solution très simple a été faite:


  • pour une grande partie des données historiques, un modèle a été lancé;
  • Les recommandations recueillies ont été présentées sous la forme de plusieurs fichiers HTML autonomes, dans lesquels le même «vote» a été utilisé;
  • des dossiers préparés ont été remis aux analystes afin de visualiser les résultats de 50 à 100 incidents.

Il a donc été possible de collecter des données sur environ 4000+ paires de recommandations d'incidents.


Analyse de l'examen initial


Les mesures initiales étaient «moyennes» - la part des «bonnes» recommandations, selon les collègues, n'était que d'environ 25%.


Les principaux problèmes du premier modèle:


  1. les incidents concernant de «nouveaux» problèmes ont reçu des recommandations non pertinentes du système; Il s'est avéré qu'en l'absence de coïncidences dans le contenu de l'appel, le système a sélectionné des incidents proches du service de l'employé contactant.
  2. des recommandations pour un incident sur un système frappent des incidents d'autres systèmes. Les mots utilisés dans l'appel étaient similaires, mais décrivaient les problèmes des autres systèmes et étaient différents.

Les moyens possibles d'améliorer la qualité des recommandations ont été sélectionnés:


  • ajustement de la composition et du poids des attributs de traitement inclus dans le vecteur final
  • sélection des paramètres de vectorisation TfidfVectorizer
  • sélection de la distance de coupure des recommandations

Élaboration de critères de qualité et de méthodes d'évaluation


Pour rechercher une version améliorée du modèle, il est nécessaire de déterminer le principe de l'évaluation de la qualité des résultats du modèle. Cela vous permettra de comparer quantitativement les deux modèles et de choisir le meilleur.


Que peut-on obtenir des avis recueillis


Nous avons plusieurs m tuples de la forme: "Incident", "Incident recommandé", "Evaluation de la recommandation".


  • "Note de recommandation" ( v ) - est défini en binaire: "Bon" | Pauvre (1 / -1);
  • "Incident" et "Incident recommandé" sont simplement des numéros d'incident. Sur eux, vous pouvez trouver l'incident dans la base de données.

Ayant de telles données, vous pouvez calculer:


  • n_inc_total - Le nombre total d'incidents pour lesquels il existe des recommandations
  • n_inc_good - Le nombre d'incidents pour lesquels il existe de «bonnes» recommandations
  • avg_inc_good - Le nombre moyen de «bonnes» recommandations pour les incidents
  • n_rec_total - Nombre total de recommandations
  • n_rec_good - Le nombre total de «bonnes» recommandations
  • pct_inc_good - part des incidents pour lesquels il existe de "bonnes" recommandations
    pct_inc_good = n_inc_good / n_inc_total
  • pct_rec_good - part totale des "bonnes" recommandations
    pct_rec_good = n_rec_good / n_rec_total

Ces indicateurs, calculés sur la base des estimations des utilisateurs, peuvent être considérés comme des «indicateurs de base» du modèle d'origine. Avec lui, nous comparerons des indicateurs similaires de nouvelles versions du modèle.


Prenez tous les «incidents» uniques de m et faites-les passer par le nouveau modèle.


En conséquence, nous obtenons de nombreux m * tuples: "Incident", "Incident recommandé", "Distance".
Ici, "distance" est la métrique définie dans NearestNeighbors. Dans notre modèle, c'est la distance cosinus. La valeur "0" correspond à la coïncidence complète des vecteurs.


Sélection de la "distance de coupure"


En complétant l'ensemble de recommandations m * avec des informations sur l'estimation vraie de v à partir de l'ensemble initial d'estimations de m , nous obtenons la correspondance entre la distance d et l'estimation vraie de v pour ce modèle.


Ayant l'ensemble ( d , v ), il est possible de choisir le niveau de coupure optimal t , qui pour d <= t la recommandation sera "bonne", et pour d> t - "mauvaise". La sélection de t peut être effectuée en optimisant le classificateur binaire le plus simple v = -1 if d>t else 1 rapport à l'hyperparamètre t, et en utilisant, par exemple, AUC ROC comme métrique.


 #     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_ 

La valeur t obtenue peut être utilisée pour filtrer les recommandations.


Bien sûr, cette approche peut encore ignorer les «mauvaises» recommandations et couper les «bonnes». Par conséquent, à ce stade, nous affichons toujours les recommandations du "Top 5", mais nous marquons spécialement celles qui sont considérées comme "bonnes", en tenant compte du t trouvé.
Alternative: si au moins une «bonne» recommandation est trouvée, ne montrer que «bonne». Sinon, affichez tous les disponibles (également - "Top N").


Hypothèse de comparaison des modèles


Pour les modèles de formation, le même cas d'incident est utilisé.
Supposons que si une «bonne» recommandation a déjà été trouvée, le nouveau modèle devrait également trouver une «bonne» recommandation pour le même incident. En particulier, le nouveau modèle peut trouver les mêmes «bonnes» recommandations que l'ancien. Cependant, avec le nouveau modèle, nous nous attendons à ce que le nombre de «mauvaises» recommandations diminue.


Ensuite, en considérant les mêmes indicateurs pour les recommandations m * du nouveau modèle, ils peuvent être comparés aux indicateurs correspondants pour m . Sur la base de la comparaison, vous pouvez choisir le meilleur modèle.


Il existe deux manières de prendre en compte les «bonnes» recommandations pour l'ensemble m * :


  1. sur la base du t trouvé: considérer que toutes les recommandations de m * avec d < t sont «bonnes» et les prendre en compte pour le calcul des métriques
  2. sur la base des estimations vraies correspondantes de l'ensemble m : dans les recommandations m *, sélectionnez uniquement celles pour lesquelles il existe une estimation vraie en m et jetez le reste.

Dans le premier cas, les indicateurs "absolus" ( n_inc_good , n_rec_good ) du nouveau modèle doivent être supérieurs à ceux du modèle de base. Dans le second cas, les indicateurs doivent s'approcher des indicateurs du modèle de base.
Le problème de la seconde méthode: si le nouveau modèle est meilleur que l'original, et qu'il trouve quelque chose de précédemment inconnu, une telle recommandation ne sera pas prise en compte dans le calcul.


Sélectionnez les options de comparaison de modèles


Lors du choix d'un nouveau modèle, je souhaite que les indicateurs s'améliorent par rapport au modèle existant:


  • nombre moyen de «bonnes» recommandations par incident ( avg_inc_good )
  • nombre d'incidents pour lesquels il existe de «bonnes» recommandations ( n_inc_good ).

Pour la comparaison avec le modèle d'origine, nous utiliserons les relations de ces paramètres du nouveau modèle et de l'original. Ainsi, si le rapport du paramètre du nouveau modèle à l'ancien est supérieur à 1, le nouveau modèle est meilleur.


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

Pour simplifier la sélection, il est préférable d'utiliser un seul paramètre. Nous prenons la moyenne harmonique des indicateurs relatifs individuels et nous l'utilisons comme seul critère de qualité composite pour le nouveau modèle.


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

Nouveau modèle et son optimisation


Pour le nouveau modèle, dans le vecteur final représentant l'incident, nous ajoutons les composants responsables de la "zone d'incident" (l'un des nombreux systèmes desservis par notre équipe).
Les informations sur l'unité et l'emplacement de l'employé qui a créé l'incident sont également placées dans un composant vectoriel distinct. Tous les composants ont leur poids dans le vecteur final.


 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) 

Les hyperparamètres du modèle devraient affecter les cibles du modèle. Dans l'architecture du modèle sélectionné, nous considérerons comme hyperparamètres:


  • Paramètres de vectorisation TF-IDF - n-grammes utilisés (ngram_range), taille du dictionnaire (max_features), durée minimale de conduite (min_df)
  • contribution des composantes au vecteur final - transformer_weights.

Les valeurs initiales des hyperparamètres de vectorisation de texte sont tirées du modèle précédent. Les poids initiaux des composants sont sélectionnés sur la base d'un jugement d'expert.


Cycle de sélection des paramètres


Comment comparer, sélectionner le niveau de raté et comparer les modèles entre eux ont déjà été déterminés. Nous pouvons maintenant procéder à l'optimisation par la sélection d'hyperparamètres.


Cycle d'optimisation


 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) ... 

Résultats d'optimisation


Le tableau montre les résultats d'expériences dans lesquelles des résultats intéressants ont été obtenus - les 5 meilleures et les pires valeurs pour les indicateurs contrôlés.



Les cellules avec des indicateurs dans le tableau sont marquées comme suit:


  • le vert foncé est le meilleur indicateur parmi toutes les expériences
  • vert pâle - la valeur de l'indicateur est dans le top-5
  • rouge foncé - le pire indicateur de toutes les expériences
  • rouge pâle - l'indicateur est dans le pire-5

Le meilleur indicateur composite a été obtenu pour un modèle avec paramètres:


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

Un modèle avec ces paramètres a montré une amélioration de l'indicateur composite par rapport au modèle d'origine 24%


Quelques observations et conclusions


Selon les résultats d'optimisation:


  1. L'utilisation de trigrammes ( ngram_range = (1,3) ) ne semble pas justifiée. Ils gonflent le dictionnaire et augmentent légèrement la précision par rapport aux bigrammes.


  2. Un comportement intéressant lors de la construction d'un dictionnaire utilisant uniquement des bigrammes ( ngram_range = (2,2) ): la "précision" des recommandations augmente et le nombre de recommandations trouvées diminue. Tout comme un équilibre précision / rappel dans les classificateurs. Un comportement similaire est observé dans la sélection du niveau de coupure t - pour les bigrammes, un «cône» de coupure plus étroit et une meilleure séparation des «bonnes» et des «mauvaises» recommandations sont caractéristiques.


  3. Le paramètre non nul min_df, avec les bigrammes, augmente la précision des recommandations. Ils commencent à être basés sur des termes qui se produisent au moins plusieurs fois. À mesure que le paramètre augmente, le dictionnaire commence à rétrécir rapidement. Pour les petits échantillons, comme dans notre cas, il sera probablement plus compréhensible d'opérer avec le nombre de documents (valeur entière min_df) que la fraction de documents (valeur fractionnaire min_df) contenant le terme.


  4. De bons résultats sont obtenus lorsque l'attribut incident responsable de la "région" est inclus dans le vecteur final avec une pondération égale ou proche de la composante texte. Des valeurs faibles entraînent une augmentation de la proportion de «mauvaises» recommandations en raison de la découverte de mots similaires dans des documents provenant d'autres domaines. Mais les signes de l'emplacement du client n'affectent pas si bien les résultats des recommandations dans notre cas.



De nouvelles idées ont émergé:


  • ajouter une composante «temps» afin que les incidents récents aient priorité sur les incidents similaires.
  • voyez comment l'introduction du paramètre max_df influencera - bien qu'avec tf-idf des mots trop généraux pour le corpus ne devraient pas avoir un poids significatif, par définition.
  • essayer enfin d'autres façons de vectoriser le contenu, par exemple, en se basant sur le mot à vecteur, ou en se basant sur la convolution de vues tf-idf à l'aide de réseaux.

Source: https://habr.com/ru/post/fr472772/


All Articles