“看来这已经发生了?” 搜索类似的事件和索赔

每个花了一定时间支持系统的人都对déjàvu感觉很熟悉,因为他们收到了新的应用程序:“就是这样,它已经被整理出来了,但我不记得是多么的精确”。 您可以花时间,深入研究以前的应用程序,然后尝试找到类似的应用程序。 这将有所帮助:可以更快地解决事件,甚至有可能发现根本原因并一劳永逸地解决问题。


刚加入团队的“年轻”员工头脑中没有这样的故事。 他们很可能不知道类似的事件发生在例如六个月到一年前。 隔壁房间的同事决定了那件事。


最有可能的是,“年轻”员工不会在事件数据库中查找类似内容,而是会从头解决问题。 花更多的时间,积累经验,下一次会更快。 也许他们会立即在新应用程序的流中忘记它。 下次一切都会再次发生。


我们已经在使用ML模型对事件进行分类 。 为了帮助我们的团队更有效地处理应用程序,我们创建了另一个ML模型来准备“先前已关闭的类似事件”列表。 详细信息-下切。


我们需要什么?


对于每个传入事件,有必要在历史记录中查找“类似”的封闭事件。 “相似性”的定义应该在事件开始时就出现,最好在支持人员开始分析之前。


为了比较事件,有必要在联系时使用用户提供的信息:简要说明,详细说明(如果有),用户记录的任何属性。


该团队支持4组系统。 我要用于搜索相似事件的事件总数约为10,000。


首要决定


目前没有有关事件“相似性”的经过验证的信息。 因此,目前必须推迟培训暹罗网络的最新选择。
首先想到的是由上诉内容组成的“单词袋”的简单组合。


在这种情况下,事件处理过程如下:


  1. 突出显示必要的文本片段
  2. 文字预处理/清洁
  3. TF-IDF矢量化
  4. 找到你最近的邻居

显然,使用所描述的方法,相似性将基于字典的比较:在两个不同的事件中使用相同的单词或n-gram将被视为“相似性”。


当然,这是一种相当简化的方法。 但是请记住,如果用相似的词来描述问题,那么我们会评估用户点击的文字-最有可能的事件是相似的。 除了文本外,您还可以添加用户部门的名称,希望不同组织中相同部门的用户会遇到类似的问题。


突出显示必要的文本片段


我们以最简单的方式从service-now.com获取事件数据-通过以编程方式运行自定义报告,并以CSV文件的形式检索其结果。


在这种情况下,作为事件的一部分,在支持人员和用户之间交换的消息数据将以一个大文本字段的形式返回,并包含整个通信历史记录。


必须使用正则表达式“截断”来自此类字段的有关首次调用的信息。


  • 所有消息均由特征线<when>-<who>分隔。
  • 消息通常以正式签名结尾,特别是如果通过电子邮件提出上诉。 该信息在重要单词列表中明显为“ fonil”,因此也必须删除签名。

原来是这样的:


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 

预处理事件文本


为了提高分类的质量,对上诉文本进行了预处理。


使用事件描述中的一组正则表达式,找到了特征片段:日期,服务器名称,产品代码,IP地址,网址,不正确的名称形式等。 此类片段已替换为相应的概念标记。


最后,使用词干将单词变成一种通用形式。 这使我们摆脱了动词的复数形式和结尾。 众所周知的snowballstemmer被用作茎梗。


所有处理过程都组合到一个转换类中,可以在不同的过程中使用。


顺便说一下,事实证明(当然是通过实验), stemmer.stemWord()方法不是线程安全的。 因此,如果您尝试在管道中实现并行文本处理(例如,使用joblib Prallel / delayed),则必须使用锁保护对词干分析器常规实例的访问。


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

向量化


矢量化是由标准TfidfVectorizer使用以下设置进行的:


  • max_features = 10000
  • ngram =(1,3)-试图捕获稳定的组合和语义ngram
  • max_df / min_df默认为左
  • stop_words英语单词的标准列表,以及其自己的其他单词集。 例如,一些用户提到分析人员的姓名,并且专有名称经常成为重要的属性。

TfidfVectorizer本身默认情况下会进行L2归一化,因此入射向量已准备好测量它们之间的余弦距离。


搜索类似事件


该过程的主要任务是返回最近的N个邻居的列表。 sklearn.neighbors.NearestNeighbors类非常适合于此。 一个问题是它不能实现transform方法,否则就不能在pipeline使用它。


因此,有必要使它基于Transformer ,然后才将其置于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 

加工过程


综上所述,我们得到了一个紧凑的过程:


 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) 

训练后,可以使用picklepipeline保存到文件中,并用于处理传入的事件。
我们将与模型一起保存必要的入射场-以便稍后在模型运行时在输出中使用它们。


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

首次申请结果


同事们对引入“提示”系统的反应通常是非常积极的。 重复发生的事件开始得到更快的解决,我们开始着手进行故障排除。


但是,不能指望无监督学习系统会带来奇迹。 同事抱怨说,有时系统提供了完全不相关的链接。 有时甚至很难理解这些建议的来源。


显然,改进模型的领域很大。 某些缺陷可以解决,包括或排除事件的某些属性。 部分-通过为当前事件和“建议”之间的距离选择适当的截止级别。 可以考虑其他向量化方法。


但是主要的问题是缺乏推荐的质量指标。 如果是这样,就不可能理解“什么是好,什么是坏,什么是坏”,并在此基础上建立模型的比较。


我们无法访问http日志,因为该服务系统可远程工作(SaaS)。 我们进行了用户调查-但仅是定性的。 有必要进行定量评估,并在其基础上建立清晰的质量指标。


但是在下一部分中将有更多关于...

Source: https://habr.com/ru/post/zh-CN472210/


All Articles