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

前言

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

注意

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

问题描述

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

解决流程

1. 数据清洗

我们首先读取数据命名为news的dataframe,数据字段大概如下

idauthorsourcecontentfeaturetitleurl
89617NaN快科技此外,自本周(6月12日)起,除小米手机6等15款机型外,其余机型已暂停更新发布(含开发版/{"type":"科技","site":"cnbeta","commentNum":"37"...小米MIUI 9首批机型曝光:共计15款http://www.cnbeta.com/articles/tech/623597.htm
89616NaN快科技骁龙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的样本,经查看不是很多,因此可以直接去掉。

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

在这里需要注意的是,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)
复制代码

总结

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

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

完整示例代码戳这里

致谢

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

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

转载于:https://juejin.im/post/5af01dc06fb9a07abc29d6dd

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值