"Il semble que cela soit déjà arrivé?" Rechercher des incidents et des réclamations similaires

Tous ceux qui ont passé un certain temps à soutenir les systèmes connaissent le sentiment de déjà vu lorsqu'ils ont reçu une nouvelle application: «c'était comme ça, ça a été réglé, mais je ne me souviens pas exactement comment». Vous pouvez passer du temps, explorer les applications précédentes et essayer de trouver des applications similaires. Cela vous aidera: l'incident sera fermé plus rapidement, ou il peut même être possible de détecter la cause première et de résoudre le problème une fois pour toutes.


Les «jeunes» employés qui viennent de rejoindre l'équipe n'ont pas une telle histoire en tête. Très probablement, ils ne savent pas qu'un incident similaire, par exemple, s'est produit il y a six mois à un an. Et le collègue de la pièce voisine a décidé de cet incident.


Très probablement, les "jeunes" employés ne chercheront pas quelque chose de similaire dans la base de données des incidents, mais ils résoudront les problèmes à partir de zéro. Passez plus de temps, gagnez de l'expérience et la prochaine fois, vous vous en sortirez plus rapidement. Ou peut-être qu'ils l'oublieront immédiatement sous le flux de nouvelles applications. Et la prochaine fois, tout se reproduira.


Nous utilisons déjà des modèles ML pour classer les incidents . Pour aider notre équipe à traiter les demandes plus efficacement, nous avons créé un autre modèle ML pour préparer une liste des «incidents similaires précédemment fermés». Détails - sous la coupe.


De quoi avons-nous besoin?


Pour chaque incident entrant, il est nécessaire de trouver des incidents fermés «similaires» dans l'historique. La définition de «similitude» devrait se produire au tout début de l'incident, de préférence avant que le personnel de soutien n'ait commencé l'analyse.


Pour comparer les incidents, il est nécessaire d'utiliser les informations fournies par l'utilisateur lors du contact: une brève description, une description détaillée (le cas échéant), tout attribut du dossier de l'utilisateur.


L'équipe prend en charge 4 groupes de systèmes. Le nombre total d'incidents que je souhaite utiliser pour rechercher des incidents similaires est d'environ 10 000.


Première décision


Il n'y a aucune information vérifiée sur la "similitude" des incidents en cours. Les options de pointe pour la formation des réseaux siamois devront donc être reportées pour l'instant.
La première chose qui me vient à l'esprit est un simple regroupement d'un «sac de mots» composé du contenu des appels.


Dans ce cas, le processus de traitement des incidents est le suivant:


  1. Surligner les fragments de texte nécessaires
  2. Prétraitement / nettoyage du texte
  3. Vectorisation TF-IDF
  4. Trouvez votre voisin le plus proche

Il est clair qu'avec l'approche décrite, la similitude sera basée sur une comparaison des dictionnaires: l'utilisation des mêmes mots ou n-gramme dans deux incidents différents sera considérée comme une «similitude».


Bien sûr, c'est une approche assez simplifiée. Mais en se souvenant que nous évaluons les textes des hits des utilisateurs, si le problème est décrit dans des mots similaires - les incidents sont probablement similaires. En plus du texte, vous pouvez ajouter le nom du service de l'utilisateur, en attendant que les utilisateurs des mêmes services dans différentes organisations auront des problèmes similaires.


Surligner les fragments de texte nécessaires


Les données sur les incidents que nous obtenons de service-now.com de la manière la plus simple - en exécutant des rapports personnalisés par programme et en récupérant leurs résultats sous forme de fichiers CSV.


Les données sur les messages échangés entre l'assistance et les utilisateurs dans le cadre de l'incident sont renvoyées dans ce cas sous la forme d'un grand champ de texte, avec l'historique complet de la correspondance.


Les informations sur le premier appel à partir d'un tel champ devaient être "découpées" par des expressions régulières.


  • Tous les messages sont séparés par une ligne caractéristique <when> - <who>.
  • Les messages se terminent souvent par des signatures officielles, surtout si l'appel a été fait par courrier électronique. Cette information est sensiblement "fonil" dans la liste des mots significatifs, donc la signature a également dû être supprimée.

Il s'est avéré quelque chose comme ça:


def get_first_message(messages): res = "" if len(messages) > 0: # take the first message spl = re.split("\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2} - ((\w+((\s|-)\w+)?,(\s\w+)+)|\w{9}|guest)\s\(\w+\s\w+\)\n", messages.lower()) res = spl[-1] # cut off "mail footer" with finalization statements res = re.split("(best|kind)(\s)+regard(s)+", res)[0] # cut off "mail footer" with embedded pictures res = re.split("\[cid:", res)[0] # cut off "mail footer" with phone prefix res = re.split("\+(\d(\s|-)?){7}", res)[0] return res 

Prétraitement des textes des incidents


Pour améliorer la qualité du classement, le texte de l'appel est prétraité.


À l'aide d'un ensemble d'expressions régulières dans les descriptions des incidents, des fragments caractéristiques ont été trouvés: dates, noms de serveurs, codes de produit, adresses IP, adresses Web, formes de noms incorrectes, etc. Ces fragments ont été remplacés par les jetons de concept correspondants.


En fin de compte, le stamming a été utilisé pour amener les mots à une forme commune. Cela nous a permis de nous débarrasser des formes plurielles et des terminaisons des verbes. Le célèbre snowballstemmer été utilisé comme stemmer.


Tous les processus de traitement sont combinés en une seule classe de transformation, qui peut être utilisée dans différents processus.


Par ailleurs, il s'est avéré (expérimentalement, bien sûr) que la méthode stemmer.stemWord() n'est pas sûre pour les threads. Par conséquent, si vous essayez d'implémenter un traitement de texte parallèle dans le pipeline, par exemple, en utilisant joblib Prallel / retardé, l'accès à l'instance générale du stemmer doit être protégé par des verrous.


 __replacements = [ ('(\d{1,3}\.){3}\d{1,3}', 'IPV4'), ('(?<=\W)((\d{2}[-\/ \.]?){2}(19|20)\d{2})|(19|20)\d{2}([-\/ \.]?\d{2}){2}(?=\W)', 'YYYYMMDD'), ('(?<=\W)(19|20)\d{2}(?=\W)', 'YYYY'), ('(?<=\W)(0|1)?\d\s?(am|pm)(?=\W)', 'HOUR'), ('http[s]?:\/\/(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', 'SOMEURL') #      ] __stemmer_lock = threading.Lock() __stemmer = snowballstemmer.stemmer('english') def stem_string(text: str): def stem_words(word_list): with __stemmer_lock: res = __stemmer.stemWords(word_list) return res return " ".join(stem_words(text.split())) def clean_text(text: str): res = text for p in __replacements: res = re.sub(p[0], '#'+p[1]+'#', res) return res def process_record(record): txt = "" for t in record: t = "" if t == np.nan else t txt += " " + get_first_message(str(t)) return stem_string(clean_text(txt.lower())) class CommentsTextTransformer(BaseEstimator, TransformerMixin): _n_jobs = 1 def __init__(self, n_jobs=1): self._n_jobs = n_jobs def fit(self, X, y=None): return self def transform(self, X, y=None): features = Parallel(n_jobs=self._n_jobs)( delayed(process_record)(rec) for i, rec in enumerate(X.values) ) return np.array(features, dtype=object).reshape(len(X),) 

Vectorisation


La vectorisation est effectuée par le TfidfVectorizer standard avec les paramètres suivants:


  • max_features = 10000
  • ngram = (1,3) - pour tenter d'attraper des combinaisons stables et des connecteurs sémantiques
  • max_df / min_df - laissé par défaut
  • stop_words - une liste standard de mots anglais, plus son propre ensemble supplémentaire de mots. Par exemple, certains utilisateurs ont mentionné des noms d'analystes et, bien souvent, les noms propres sont devenus des attributs importants.

TfidfVectorizer lui-même effectue la normalisation L2 par défaut, donc les vecteurs incidents sont prêts à mesurer la distance cosinus entre eux.


Rechercher des incidents similaires


La tâche principale du processus est de renvoyer une liste des N voisins les plus proches. La classe sklearn.neighbors.NearestNeighbors est tout à fait appropriée pour cela. Un problème est qu'il n'implémente pas la méthode de transform , sans laquelle il ne peut pas être utilisé dans le pipeline .


Par conséquent, il était nécessaire de le faire basé sur Transformer , qui n'a alors mis que la dernière étape du pipeline :


 class NearestNeighborsTransformer(NearestNeighbors, TransformerMixin): def __init__(self, n_neighbors=5, radius=1.0, algorithm='auto', leaf_size=30, metric='minkowski', p=2, metric_params=None, n_jobs=None, **kwargs): super(NearestNeighbors, self).__init__(n_neighbors=n_neighbors, radius=radius, algorithm=algorithm, leaf_size=leaf_size, metric=metric, p=p, metric_params=metric_params, n_jobs=n_jobs) def transform(self, X, y=None): res = self.kneighbors(X, self.n_neighbors, return_distance=True) return res 

Processus de traitement


En mettant tout cela ensemble, nous obtenons un processus compact:


 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)) ]), ['short_description', 'comments', 'u_impacted_department'] ) ] )), ("nn", NearestNeighborsTransformer(n_neighbors=10, metric='cosine')) ], memory=None) 

Après la formation, le pipeline peut être enregistré dans un fichier à l'aide de pickle et utilisé pour gérer les incidents entrants.
Avec le modèle, nous enregistrerons les champs d'incident nécessaires - afin de les utiliser ultérieurement dans la sortie lorsque le modèle est en cours d'exécution.


 # inc_data - pandas.Dataframe,     # ref_data - pandas.Dataframe,    . #     .    # inc_data["recommendations_json"] = "" #   . # column_list -  ,          nn_dist, nn_refs = p.transform(inc_data[column_list]) for idx, refs in enumerate(nn_refs): nn_data = ref_data.iloc[refs][['number', 'short_description']].copy() nn_data['distance'] = nn_dist[idx] inc_data.iloc[idx]["recommendations_json"] = nn_data.to_json(orient='records') #     , .     -. inc_data[['number', 'short_description', 'recommendations_json']].to_json(out_file_name, orient='records') 

Résultats de la première application


La réaction des collègues à l'introduction d'un système de "conseils" a été généralement très positive. Les incidents récurrents ont commencé à être résolus plus rapidement, nous avons commencé à travailler sur le dépannage.


Cependant, on ne pouvait pas s'attendre à un miracle du système d'apprentissage non supervisé. Des collègues se sont plaints du fait que le système offre parfois des liens complètement non pertinents. Parfois, il était même difficile de comprendre d'où venaient ces recommandations.


Il était clair que le champ pour améliorer le modèle est immense. Certains des défauts peuvent être résolus, en incluant ou en excluant certains attributs de l'incident. Partie - en sélectionnant un niveau de coupure adéquat pour la distance entre l'incident actuel et la «recommandation». D'autres méthodes de vectorisation peuvent être envisagées.


Mais le principal problème était le manque de paramètres de qualité pour les recommandations. Et si c'est le cas, il était impossible de comprendre "ce qui est bon et ce qui est mauvais, et combien", et de construire une comparaison des modèles à ce sujet.


Nous n'avions pas accès aux journaux http, car le système de service fonctionne à distance (SaaS). Nous avons mené des enquêtes auprès des utilisateurs - mais uniquement de manière qualitative. Il était nécessaire de procéder à des évaluations quantitatives et de s'appuyer sur des critères de qualité clairs.


Mais plus à ce sujet dans la partie suivante ...

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


All Articles