AIQ | 简单的文本相似性检测与抄袭判断



转载请注明 AIQ - 最专业的机器学习大数据社区  http://www.6aiq.com

AIQ 机器学习大数据 知乎专栏 点击关注

前言

本文旨在记录本萌新在做练手项目总结的心得体会,主要针对初学者,介绍的概念和技术会比较基础,从而提供一些解决实际问题的思路(不必拘泥与其中使用到的概念和算法,在细节上完全可以做得更好,用其它更先进更前沿的技术替代),同时会重点介绍我认为比较需要注意的技术细节。

注意

  1. 本文的样例数据恕不能分享,如有需要请自己动手爬取。

  2. 基本的操作在这里不作讨论,如有需要请自己查阅相关文档。

  3. 相关概念:TF-IDF朴素贝叶斯 (naive bayes)k-means 聚类

问题描述

假如现在你是国内某新闻社的工作人员,现在发现其它媒体抄袭你平台的文章,现在你接到一个任务,需要把其它媒体怀疑抄袭的文章找出来,并与原文对比定位抄袭的地方。

解决流程

1. 数据清洗

我们首先读取数据命名为news的 dataframe,数据字段大概如下
ca76996f6ca94344bcccdb373d6b2619.png
我们需要根据 content 字段来训练模型,因此查看 content 字段为 NaN 的样本,经查看不是很多,因此可以直接去掉。

#show nans in the dataset
news[news.content.isna()].head(5)
#drop the nans
news=news.dropna(subset=['content'])

然后定义一个简单的函数(使用 jieba 分词)准备对 content 进行分词,在分词前去掉一些符号和中文标点,分词后过滤掉一些停用词,其中punctuation包含所有中文标点,stopwords是一个列表包含了一些停用词(百度搜索可以下载,你也可以根据需要编辑)。在此我只是展示一种可行的处理方法,如果觉得有提升空间你大可不必这样做,或许你可以用 pos of tag 根据词性过滤你想要的词汇,或者需要 pharse detection 甚至用 word2vec 来表征。

def split_text(text):return ''.join([w for w in list(jieba.cut(re.sub('\s|[%s]'% (punctuation),'',text)))if w not in stopwords])

测试下函数大概是这样的效果:

split_text(news.iloc[1].content)
#out:
骁龙 835 唯一 Windows10 桌面 平台 认证 ARM 处理器 高通 强调 不会 只 考虑 性能 屏蔽掉 小 核心 相反 正 联手 微软 找到 一种 适合 桌面 平台 兼顾 性能 功耗 完美 方案 报道 微软 已经 拿到 一些 源码 Windows10 更好 理解 big little 架构 资料 显示 骁龙 835 一款 集成 CPUGPU 基带 蓝牙 Wi Fi SoC 传统 Wintel 方案 节省 至少 30% PCB 空间 按计划 今年 Q4 华硕 惠普 联想 首发 骁龙 835Win10 电脑 预计 均 二合一 形态 产品 当然 高通 骁龙 未来 也许 见到 三星 Exynos 联发科 华为 麒麟 小米 澎湃 进入 Windows10 桌面 平台

现在可以把函数应用到整列 content 字段上面啦!在这里展示使用 pandas 的方法,在完整代码示例我使用了比较 pythontic 的方法。

news['content_split'] = news['content'].apply(split_text)

类似地,我们可以使用相似的方法制造标签 (比如我现在假设新闻来源包含新华两个字为正例)

news['is_xinhua'] = np.where(news['source'].str.contains('新华'), 1, 0)

到此, 我们的数据清洗工作就完成啦!:D

2. 数据预处理

要运用机器学习算法,我们必须把文本转化成算法可理解的形式,现在我们需要使用 sklearn 构造TF-IDF矩阵来表征文本,TF-IDF 是表征文本简单有效的方式,如果你不知道这是什么请戳链接。

tfidfVectorizer = TfidfVectorizer(encoding='gb18030',min_df=0.015)
tfidf = tfidfVectorizer.fit_transform(news['content_split'])

在创建TfidfVectorizer时候注意指定 encoding 参数(默认是 utf-8),在这里 min_df=0.015 表示创建词库时忽略文档频率低于设置阈值的词汇,这样设置是因为我的机器不能计算太多的 feature,如果计算资源充足可以设置 max_features=30000 这样会取词频排列在前 30000 的词汇作为 feature(tfidf 矩阵的列),这样模型效果会更加好。

3. 训练预测模型

训练模型之前我们需要把数据分为训练集 (70%) 和测试集(30%)。

#split the data
lable = news['is_xinhua'].values
X_train, X_test, y_train, y_test = train_test_split(tfidf.toarray(),label,test_size = 0.3, random_state=42)

现在可以用朴素贝叶斯训练模型啦!

clf = MultinomialNB()
clf.fit(X=X_train,y=y_train)

现在,怎么知道我们的模型拟合得好不好呢?可以应用交叉验证 (cross-validation)输出你关注的衡量指标,在这里我选择了 precision,recall,accuracy,f1 这些指标进行 3 折 (3-folds) 交叉验证(实际上你需要根据关注问题的不同选择不同的衡量指标,如果你不知道这些指标,请务必查阅相关资料。),并且和测试集的表现进行对比。

scores=cross_validate(clf,X_train,y_train,scoring=('precision','recall','accuracy','f1',cv=3,return_train_score=True)
print(scores)
#out:
'''{'fit_time': array([0.51344204, 0.43621135, 0.40280986]),
 'score_time': array([0.15626907, 0.15601063, 0.14357495]),
 'test_precision': array([0.9599404 , 0.96233543, 0.96181975]),
 'train_precision': array([0.96242476, 0.96172716, 0.96269257]),
 'test_recall': array([0.91072205, 0.91409308, 0.90811222]),
 'train_recall': array([0.91286973, 0.91129295, 0.91055894]),
 'test_accuracy': array([0.88475361, 0.88981883, 0.88415715]),
 'train_accuracy': array([0.88883419, 0.88684308, 0.88706462]),
 'test_f1': array([0.93468374, 0.93759411, 0.9341947 ]),
 'train_f1': array([0.93699249, 0.93583104, 0.9359003 ])}'''

 y_predict = clf.predict(X_test)

 def show_test_reslt(y_true,y_pred):
 print('accuracy:',accuracy_score(y_true,y_pred))
 print('precison:',precision_score(y_true,y_pred))
 print('recall:',recall_score(y_true,y_pred))
 print('f1_score:',f1_score(y_true,y_pred))

show_test_reslt(y_test,y_predict)
#out:
'''
accuracy: 0.8904162040050542
precison: 0.9624150339864055
recall: 0.9148612694792855
f1_score: 0.9380358534684333
'''

首先看 cv 的结果,3 折的衡量指标差别都不大比较稳定,而且测试集和 cv 的结果也非常相近,说明模型拟合效果尚可,在这个数据中若用更多的 features,accuracy 可接近 1。

到此,我们已经建立了一个给定文本,预测来源是否某新闻平台的模型,下面我们就可以定位抄袭文章了。

4. 定位抄袭文章

到了这步,我们可以根据模型预测的结果来对全量文本(或者新加入的文本,使用时你可能需要封装一个 pipline,这里不作演示)进行预测,对于那些预测为正类但是实际上为负类的文本,说明了他们的文本与你平台写作风格有相似之处才被错判,这些文本很可能就系抄袭文本或原文引用,首先把这部分“候选者”拿出来。

prediction = clf.predict(tfidf.toarray())

labels = np.array(label)

compare_news_index = pd.DataFrame({'prediction':prediction,'labels':labels})

copy_news_index=compare_news_index[(compare_news_index['prediction'] == 1) & (compare_news_index['labels'] == 0)].index

xinhuashe_news_index=compare_news_index[(compare_news_index['labels'] == 1)].index

现在我们必须把这些疑似抄袭的文本和原文进行对比,拿出相似度较高的文本进一步分析,但是如果使用蛮力搜索算法复杂度相当高,仅仅是两重嵌套循环就已经是 O(n^2),这种做法效率太低。

因此我们需要一种更高效的搜索相似文本的方法,在这里我使用 k-means 聚类(当然还有更好的方法,你可以改进)。首先对所有文本进行 k-means 聚类,我们就可以得到一个 id-cluster 的字典,根据这个字典创建 cluster-id 字典,这样给定一个特定文本我就可以知道这个文本属于哪个 cluster,再用它和 cluster 中的其它文本做对比,找出最相似的 top n 个文本再分析,这样做大大减少了搜索范围。

normalizer = Normalizer()
scaled_array = normalizer.fit_transform(tfidf.toarray())

kmeans = KMeans(n_clusters=25,random_state=42,n_jobs=-1)
k_labels = kmeans.fit_predict(scaled_array)

id_class = {index:class_ for index,class_ in enumerate(k_labels)}

class_id = defaultdict(set)
for index,class_ in id_class.items():
 if index in xinhuashe_news_index.tolist():
 class_id[class_].add(index)

看这里),这样聚类时就是用余弦距离衡量相似度。

还有一点要谈的就是 k-means 中心数量 (n_clusters) 的选择,在这里我选择简单地聚为 25 类。实际上你可以根据你对数据的了解,比如说你知道你的数据中大概包含体育,军事,娱乐这几类的新闻,你就可以根据经验选择中心数量,当然前提是你对数据非常熟悉。还有一种方法就是根据一些指标例如 SSE,silhouette 等等这些指标观察 elbow 值选取中心数量,这里有详细例子

现在我们就可以应用聚类的结果搜索相似文本

def find_similar_text(cpindex,top=10):
 dist_dict={i:cosine_similarity(tfidf[cpindex],tfidf[i]) for i in class_id[id_class[cpindex]]}
 return sorted(dist_dict.items(),key=lambda x:x[1][0],reverse=True)[:top]

print(copy_news_index.tolist())

#random choice a candidate to show some results
fst=find_similar_text(3352)
print(fst)
#out:
'''
 id , cosine_similarity 
[(3134, array([[0.96849349]])),
 (63511, array([[0.94619604]])),
 (29441, array([[0.94281928]])),
 (3218, array([[0.87620818]])),
 (980, array([[0.87535143]])),
 (29615, array([[0.86922775]])),
 (29888, array([[0.86194742]])),
 (64046, array([[0.85277668]])),
 (29777, array([[0.84882241]])),
 (64758, array([[0.73406445]]))]
'''

找出相似文本后,更仔细地,你可以根据某些特征(特定的长度,特定的分隔符)分割文本的句子,或者在这里我简单以“。”分割文本,分别计算相似文本句子间的edit distance后排序定位具体相似的地方。

def find_similar_sentence(candidate,raw):
 similist = []
 cl = candidate.strip().split('。')
 ra = raw.strip().split('。')
 for c in cl:
 for r in ra:
 similist.append([c,r,editdistance.eval(c,r)])
 sort=sorted(similist,key=lambda x:x[2])
 for c,r,ed in sort:
 if c!='' and r!='':
 print('怀疑抄袭句:{0}\n相似原句:{1}\neditdistance:{2}\n'.format(c,r,ed))

find_similar_sentence(news.iloc[3352].content,news.iloc[3134].content)

总结

本文主要提供了一个解决实际问题的思路框架,把一个实际的抄袭检测问题分解成一个文本分类问题和一个相似文本搜索问题,结合机器学习的思路解决实际问题的思路值得参考。

同时本文很多部分只采取了简单的方法,受到启发的同学欢迎不断优化,我的进一步优化理念和心得体会将会持续更新。

完整示例代码戳这里

致谢

感谢你耐心阅读完我的文章,不足之处欢迎批评指正,希望和你共同交流进步。

感谢我的指导老师高老师,还有积极讨论解决问题的同学朋友们!

作者:重炮手东方未明
链接:https://juejin.im/post/5af01dc06fb9a07abc29d6dd
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


更多高质资源 尽在AIQ 机器学习大数据 知乎专栏 点击关注

转载请注明 AIQ - 最专业的机器学习大数据社区  http://www.6aiq.com