"Parece que esto ya sucedió?" Busque incidentes y reclamos similares

Todos los que pasaron un cierto tiempo apoyando los sistemas están familiarizados con la sensación de déjà vu cuando recibieron una nueva aplicación: "fue así, se resolvió, pero no recuerdo exactamente cómo". Puede pasar tiempo, profundizar en aplicaciones anteriores e intentar encontrar otras similares. Esto ayudará: el incidente se cerrará más rápido, o incluso será posible detectar la causa raíz y cerrar el problema de una vez por todas.


Los empleados "jóvenes" que acaban de unirse al equipo no tienen esa historia en sus cabezas. Lo más probable es que no sepan que un incidente similar, por ejemplo, ocurrió hace seis meses a un año. Y el colega de la habitación contigua decidió ese incidente.


Lo más probable es que los empleados "jóvenes" no busquen algo similar en la base de datos de incidentes, pero resolverán los problemas desde cero. Pase más tiempo, gane experiencia y la próxima vez será más rápido. O tal vez lo olvidarán de inmediato bajo la corriente de nuevas aplicaciones. Y la próxima vez todo volverá a suceder.


Ya estamos utilizando modelos ML para clasificar incidentes . Para ayudar a nuestro equipo a procesar las aplicaciones de manera más eficiente, hemos creado otro modelo de ML para preparar una lista de "incidentes similares previamente cerrados". Detalles - debajo del corte.


Que necesitamos


Para cada incidente entrante, es necesario encontrar incidentes cerrados "similares" en el historial. La definición de "similitud" debe aparecer al comienzo del incidente, preferiblemente antes de que el personal de apoyo haya comenzado el análisis.


Para comparar incidentes, es necesario utilizar la información proporcionada por el usuario al contactar: ​​una breve descripción, una descripción detallada (si la hay), cualquier atributo del registro del usuario.


El equipo admite 4 grupos de sistemas. El número total de incidentes que quiero usar para buscar otros similares es de aproximadamente 10 mil.


Primera decisión


No hay información verificada sobre la "similitud" de incidentes disponibles. Por lo tanto, las opciones de vanguardia para entrenar redes siamesas tendrán que posponerse por ahora.
Lo primero que viene a la mente es una simple agrupación de una "bolsa de palabras" compuesta por el contenido de las apelaciones.


En este caso, el proceso de manejo de incidentes es el siguiente:


  1. Destacando los fragmentos de texto necesarios
  2. Preprocesamiento / limpieza de texto
  3. Vectorización TF-IDF
  4. Encuentra tu vecino más cercano

Está claro que con el enfoque descrito, la similitud se basará en una comparación de diccionarios: el uso de las mismas palabras o n-gramas en dos incidentes diferentes se considerará como "similitud".


Por supuesto, este es un enfoque bastante simplificado. Pero recordando que evaluamos los textos de visitas de usuarios, si el problema se describe con palabras similares, lo más probable es que los incidentes sean similares. Además del texto, puede agregar el nombre del departamento del usuario, esperando que los usuarios de los mismos departamentos en diferentes organizaciones tengan problemas similares.


Destacando los fragmentos de texto necesarios


Datos de incidentes que obtenemos de service-now.com de la manera más simple: ejecutando informes personalizados mediante programación y recuperando sus resultados en forma de archivos CSV.


Los datos sobre mensajes intercambiados entre el soporte y los usuarios como parte del incidente se devuelven en este caso en forma de un campo de texto grande, con el historial completo de la correspondencia.


La información sobre la primera llamada de dicho campo tuvo que ser "cortada" por expresiones regulares.


  • Todos los mensajes están separados por una línea característica <when> - <who>.
  • Los mensajes a menudo terminan con firmas formales, especialmente si la apelación se realizó por correo electrónico. Esta información es notablemente "fonil" en la lista de palabras significativas, por lo que la firma también tuvo que ser eliminada.

Resultó algo como esto:


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 

Preprocesamiento de textos de incidentes


Para mejorar la calidad de la clasificación, el texto de la apelación se procesa previamente.


Utilizando un conjunto de expresiones regulares en las descripciones de incidentes, se encontraron fragmentos característicos: fechas, nombres de servidores, códigos de productos, direcciones IP, direcciones web, formas incorrectas de nombres, etc. Dichos fragmentos fueron reemplazados con los correspondientes tokens de concepto.


Al final, el tartamudeo se usó para llevar las palabras a una forma común. Esto nos permitió deshacernos de las formas y terminaciones plurales de los verbos. El conocido snowballstemmer se utilizó como stemmer.


Todos los procesos de procesamiento se combinan en una clase de transformación, que se puede utilizar en diferentes procesos.


Por cierto, resultó (experimentalmente, por supuesto) que el método stemmer.stemWord() no es seguro para subprocesos. Por lo tanto, si intenta implementar el procesamiento de texto paralelo dentro de la tubería, por ejemplo, utilizando joblib Prallel / delay, el acceso a la instancia general del stemmer debe protegerse con bloqueos.


 __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),) 

Vectorización


La vectorización se realiza mediante el TfidfVectorizer estándar con la siguiente configuración:


  • max_features = 10000
  • ngram = (1,3) - en un intento de atrapar combinaciones estables y conectivas semánticas
  • max_df / min_df : izquierda por defecto
  • stop_words : una lista estándar de palabras en inglés, más su propio conjunto adicional de palabras. Por ejemplo, algunos usuarios mencionaron nombres de analistas, y con frecuencia los nombres propios se convirtieron en atributos significativos.

TfidfVectorizer propio TfidfVectorizer realiza la normalización L2 por defecto, por lo que los vectores incidentes están listos para medir la distancia del coseno entre ellos.


Buscar incidentes similares


La tarea principal del proceso es devolver una lista de los N vecinos más cercanos. La clase sklearn.neighbors.NearestNeighbors es bastante adecuada para esto. Un problema es que no implementa el método de transform , sin el cual no se puede usar en la pipeline .


Por lo tanto, era necesario hacerlo basado en Transformer , que solo entonces lo colocó en el último paso de la 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 

Proceso de procesamiento


En conjunto, obtenemos un proceso compacto:


 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) 

Después del entrenamiento, la pipeline se puede guardar en un archivo usando pickle y usar para manejar incidentes entrantes.
Junto con el modelo, guardaremos los campos de incidentes necesarios, para luego usarlos en la salida cuando el modelo se esté ejecutando.


 # 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') 

Resultados de la primera aplicación


La reacción de los colegas a la introducción de un sistema de "pistas" fue generalmente muy positiva. Los incidentes recurrentes comenzaron a resolverse más rápido, comenzamos a trabajar en la solución de problemas.


Sin embargo, uno no podría esperar un milagro del sistema de aprendizaje no supervisado. Los colegas se quejaron de que a veces el sistema ofrece enlaces completamente irrelevantes. A veces incluso era difícil entender de dónde provienen esas recomendaciones.


Estaba claro que el campo para mejorar el modelo es enorme. Algunas de las deficiencias pueden resolverse, incluyendo o excluyendo algunos atributos del incidente. Parte: seleccionando un nivel de corte adecuado para la distancia entre el incidente actual y la "recomendación". Se pueden considerar otros métodos de vectorización.


Pero el problema principal era la falta de métricas de calidad para las recomendaciones. Y si es así, era imposible entender "qué es bueno y qué es malo, y cuánto es", y construir una comparación de modelos sobre esto.


No teníamos acceso a los registros http, porque el sistema de servicio funciona de forma remota (SaaS). Realizamos encuestas de usuarios, pero solo de forma cualitativa. Era necesario proceder a evaluaciones cuantitativas y construir sobre su base parámetros de calidad claros.


Pero más sobre eso en la siguiente parte ...

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


All Articles