萌新Learning-简单的文本类似性检测与抄袭判断

前言

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

注意

  1. 本文的样例数据恕不能分享,若有须要请本身动手爬取。
  2. 基本的操做在这里不做讨论,若有须要请本身查阅相关文档。
  3. 相关概念:TF-IDF朴素贝叶斯(naive bayes)k-means聚类

问题描述

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

解决流程

1. 数据清洗

咱们首先读取数据命名为news的dataframe,数据字段大概以下git

id author source content feature title url
89617 NaN 快科技 此外,自本周(6月12日)起,除小米手机6等15款机型外,其他机型已暂停更新发布(含开发版/ {"type":"科技","site":"cnbeta","commentNum":"37"... 小米MIUI 9首批机型曝光:共计15款 http://www.cnbeta.com/articles/tech/623597.htm
89616 NaN 快科技 骁龙835做为惟一经过Windows 10桌面平台认证的ARM处理器,高通强调,不会由于只考.. {"type":"科技","site":"cnbeta","commentNum":"15"... 骁龙835在Windows 10上的性能表现有望改善 http://www.cnbeta.com/articles/tech/623599.htm
89613 胡淑丽_MN7479 深圳大件事 (原标题:44岁女子跑深圳约会网友被拒,暴雨中裸身奔走……)\r\n@深圳交警微博称:昨日清.. {"type":"新闻","site":"网易热门","commentNum":"978",.. 44岁女子约网友被拒暴雨中裸奔 交警为其披衣相随 http://news.163.com/17/0618/00/CN617P3Q0001875...

咱们须要根据content字段来训练模型,所以查看content字段为NaN的样本,经查看不是不少,所以能够直接去掉。github

#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的方法。app

news['content_split'] = news['content'].apply(split_text)
复制代码

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

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

到此,咱们的数据清洗工做就完成啦!:Ddom

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)
复制代码

在这里须要注意的是,sklearn中的k-means算法只支持根据欧氏距离计算类似度,在文本与文本的类似度比较中咱们通常使用余弦距离,在使用k-means以前咱们须要把tfidf矩阵normalize成单位长度(unit norm),由于这样作以后欧氏距离和余弦距离线性相关(为何?看这里),这样聚类时就是用余弦距离衡量类似度。

还有一点要谈的就是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)
复制代码

总结

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

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

完整示例代码戳这里

致谢

感谢你耐心阅读完个人文章,不足之处欢迎批评指正,但愿和你共同交流进步。

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

相关文章
相关标签/搜索