Todo mundo que passou um certo tempo apoiando os sistemas está familiarizado com o sentimento de déjà vu quando recebeu um novo aplicativo: "era assim, foi resolvido, mas não me lembro exatamente". Você pode gastar tempo, mergulhar nos aplicativos anteriores e tentar encontrar aplicativos semelhantes. Isso ajudará: o incidente será encerrado mais rapidamente ou talvez seja possível detectar a causa raiz e encerrar o problema de uma vez por todas.
Os funcionários “jovens” que acabaram de ingressar na equipe não têm essa história em suas cabeças. Provavelmente, eles não sabem que um incidente semelhante, por exemplo, ocorreu seis meses a um ano atrás. E o colega da sala ao lado decidiu esse incidente.
Muito provavelmente, os funcionários "jovens" não procurarão algo semelhante no banco de dados de incidentes, mas resolverão problemas do zero. Gaste mais tempo, ganhe experiência e da próxima vez lidará mais rápido. Ou talvez eles o esqueçam imediatamente sob o fluxo de novos aplicativos. E da próxima vez tudo vai acontecer novamente.
Já estamos usando modelos de ML para classificar incidentes . Para ajudar nossa equipe a processar aplicativos com mais eficiência, criamos outro modelo de ML para preparar uma lista de "incidentes similares anteriormente fechados". Detalhes - sob o corte.
Do que precisamos?
Para cada incidente recebido, é necessário encontrar incidentes fechados "semelhantes" na história. A definição de “similaridade” deve ocorrer no início do incidente, de preferência antes que a equipe de suporte inicie a análise.
Para comparar incidentes, é necessário usar as informações fornecidas pelo usuário ao entrar em contato: uma breve descrição, uma descrição detalhada (se houver), quaisquer atributos do registro do usuário.
A equipe suporta 4 grupos de sistemas. O número total de incidentes que quero usar para procurar outros semelhantes é de cerca de 10 mil.
Primeira decisão
Não há informações verificadas sobre a "similaridade" de incidentes disponíveis. Portanto, as opções de ponta para o treinamento de redes siamesas terão que ser adiadas por enquanto.
A primeira coisa que vem à mente é um simples agrupamento de um "pacote de palavras" composto pelo conteúdo dos recursos.
Nesse caso, o processo de manipulação de incidentes é o seguinte:
- Destacando os fragmentos de texto necessários
- Pré-processamento / limpeza de texto
- Vetorização TF-IDF
- Encontre o seu vizinho mais próximo
É claro que, com a abordagem descrita, a similaridade será baseada em uma comparação de dicionários: o uso das mesmas palavras ou n-grama em dois incidentes diferentes será considerado "similaridade".
Obviamente, essa é uma abordagem bastante simplificada. Mas lembrando que avaliamos os textos das ocorrências dos usuários, se o problema for descrito em palavras semelhantes - provavelmente os incidentes são semelhantes. Além do texto, você pode adicionar o nome do departamento do usuário, esperando que os usuários dos mesmos departamentos em diferentes organizações tenham problemas semelhantes.
Destacando os fragmentos de texto necessários
Os dados de incidentes que obtemos do service-now.com da maneira mais simples - iniciando programaticamente relatórios de usuários e recebendo seus resultados na forma de arquivos CSV.
Os dados das mensagens trocadas entre o suporte e os usuários como parte do incidente são retornados nesse caso na forma de um grande campo de texto, com todo o histórico da correspondência.
As informações sobre a primeira chamada desse campo precisavam ser "cortadas" por expressões regulares.
- Todas as mensagens são separadas por uma linha de característica <when> - <who>.
- As mensagens geralmente terminam com assinaturas formais, especialmente se a apelação foi feita por email. Esta informação está visivelmente "fonil" na lista de palavras significativas, portanto a assinatura também teve que ser excluída.
Aconteceu algo como isto:
def get_first_message(messages): res = "" if len(messages) > 0:
Pré-processamento de textos de incidentes
Para melhorar a qualidade da classificação, o texto da apelação é pré-processado.
Usando um conjunto de expressões regulares nas descrições de incidentes, foram encontrados fragmentos de características: datas, nomes de servidores, códigos de produtos, endereços IP, endereços da Web, formas incorretas de nomes etc. Esses fragmentos foram substituídos pelos tokens de conceito correspondentes.
No final, stamming foi usado para trazer as palavras a uma forma comum. Isso nos permitiu livrar-nos das formas plurais e dos finais dos verbos. O conhecido snowballstemmer
foi usado como um stemmer.
Todos os processos de processamento são combinados em uma classe transformadora, que pode ser usada em diferentes processos.
A propósito, descobriu-se (experimentalmente, é claro) que o método stemmer.stemWord()
não é seguro para threads. Portanto, se você tentar implementar o processamento de texto paralelo no pipeline, por exemplo, usando o joblib
Prallel / delayed, o acesso à instância geral do stemmer deverá ser protegido com bloqueios.
__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')
Vetorização
A vetorização é realizada pelo TfidfVectorizer
padrão com as seguintes configurações:
max_features
= 10000ngram
= (1,3) - na tentativa de capturar combinações estáveis e conectivos semânticosmax_df
/ min_df
- deixado por padrãostop_words
- uma lista padrão de palavras em inglês, além de seu próprio conjunto adicional de palavras. Por exemplo, alguns usuários mencionaram nomes de analistas e, muitas vezes, nomes próprios se tornaram atributos significativos.
TfidfVectorizer
próprio TfidfVectorizer
faz a normalização de L2 por padrão, para que vetores de incidentes estejam prontos para medir a distância de cosseno entre eles.
Pesquise incidentes semelhantes
A principal tarefa do processo é retornar uma lista dos N vizinhos mais próximos. A classe sklearn.neighbors.NearestNeighbors
é bastante adequada para isso. Um problema é que ele não implementa o método de transform
, sem o qual não pode ser usado no pipeline
.
Portanto, era necessário torná-lo baseado no Transformer
, que só o colocou na última etapa do 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
Processo de processamento
Juntando tudo, temos um processo 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)
Após o treinamento, o pipeline
pode ser salvo em um arquivo usando pickle
e usado para lidar com incidentes de entrada.
Juntamente com o modelo, salvaremos os campos de incidente necessários - para usá-los posteriormente na saída quando o modelo estiver em execução.
Resultados da primeira aplicação
A reação dos colegas à introdução de um sistema de "dicas" foi geralmente muito positiva. Incidentes recorrentes começaram a ser resolvidos mais rapidamente, começamos a trabalhar na solução de problemas.
No entanto, não se poderia esperar um milagre do sistema de aprendizado não supervisionado. Os colegas reclamaram que, às vezes, o sistema oferece links completamente irrelevantes. Às vezes até era difícil entender de onde vêm essas recomendações.
Ficou claro que o campo para melhorar o modelo é enorme. Algumas das deficiências podem ser resolvidas, incluindo ou excluindo alguns atributos do incidente. Parte - selecionando um nível de corte adequado para a distância entre o incidente atual e a "recomendação". Outros métodos de vetorização podem ser considerados.
Mas o principal problema foi a falta de métricas de qualidade para recomendações. E, nesse caso, era impossível entender "o que é bom e o que é ruim e quanto é", e construir uma comparação de modelos sobre isso.
Não tivemos acesso aos logs http, porque o sistema de serviço funciona remotamente (SaaS). Realizamos pesquisas com usuários - mas apenas qualitativamente. Era necessário proceder a avaliações quantitativas e construir com base em métricas claras de qualidade.
Mas mais sobre isso na próxima parte ...