引子
想象一个场景:现在有近九万条新闻(如下图所示),其中一部分新闻来自于新华社,还有一部分新闻来自其他媒体,如何判断其他媒体是不是抄袭了新华社的新闻呢?

其中关键在于衡量文章的特征,以及两篇文章特征的相似关系,而在文本数据中常见的特征就是关键词。因此我们可以采取以下办法
- 分词:按照一定规则进行提取、筛选所有文章的关键词组成特征列表
- 编码:每篇文章按特征列表转化成向量,如特征单词出现为1,否则为0
- 聚类:一般都是同类型文章间抄袭,因此按编码后的特征向量聚类
- 计算:计算同一簇内非新华社文章与新华社文章的相似度
- 筛选:筛选出相似度高的文章,即为存在抄袭的文章
提取文章中的关键词常用的方法是:tfidf(item frequency–inverse document frequency)
- tf:词频,显然词频越大,越有可能是文章的关键词
- idf:计算公式(sklearn中)为$log(\frac{文档数+1}{包含该单词的文档数+1})+1$,显然包含该单词的文档数越少,即该单词不太常见,则idf值越大
- tfidf:计算公式tf*idf。显然tfidf越大,筛选出的越是不太常见但重要的词语。
实践
导入数据1
2
3
4
5
6
7import pandas as pd
# 导入新闻数据和停用词
with open('chinese_stopwords.txt', 'r', encoding='utf-8') as file:
stopwords = [line[:-1] for line in file.readlines()]
news = pd.read_csv('sqlResult.csv', encoding='gb18030')
# 处理缺失值
news = news.dropna(subset=['content'])
对所有新闻分词,然后筛选掉无意义的停用词,用corpus存放所有分词结果。由于数据量较大,运行一次可能需要二十分钟,可以用pickle将运行结果保存下来,下次就不用再跑一次了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22import jieba
# 分词
def split_text(text):
text = text.replace(' ', '') # 去除所有空格
text = text.replace('\n', '') # 去除所有换行符
text = text.replace('\r', '') # 去除所有回车符
text2 = jieba.lcut(text.strip())
print(text2)
result = ' '.join([w for w in text2 if w not in stopwords])
return result
print(news.iloc[0].content)
print('\n', split_text(news.iloc[0].content))
# 得到语料库corpus
import pickle, os
if not os.path.exists('corpus.pkl'):
corpus = list(map(split_text, [str(item) for item in news.content]))
with open('corpus.pkl', 'wb') as file:
pickle.dump(corpus, file)
else:
# 调用上次的处理结果
with open('corpus.pkl', 'rb') as file:
corpus = pickle.load(file)
sklearn中有现成的tfidf的接口可以调用,min_df代表最小词频,这样tfidf中存放了所有新闻的特征向量。1
2
3
4
5
6
7
8from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer, TfidfVectorizer
# 处理预料库,提取每条新闻的特征单词
countvectorizer = CountVectorizer(encoding='gb18030', min_df=0.02) # 提取所有文档中词频大于min_df的特征单词, countvectorizer.get_feature_names()
countvector = countvectorizer.fit_transform(corpus) # 返回各特征单词在每个文档中出现次数的矩阵
tfidftransformer = TfidfTransformer()
tfidf = tfidftransformer.fit_transform(countvector)
# tfidfvectorizer = TfidfVectorizer(min_df=0.3) # 上述语句可用以下两句代替,效果相同且更为简便
# tfidf = tfidfvectorizer.fit_transform(corpus)
接下来根据每条新闻的tfidf向量进行聚类。聚类分析也比较耗时,因此将运行结果保存。1
2
3
4
5
6
7
8
9
10
11
12
13
14# 对全量文档按tfidf聚类
from sklearn.preprocessing import Normalizer
from sklearn.cluster import KMeans
# 保存到文件
if not os.path.exists('cluster.pkl'):
normalizer = Normalizer()
scaled = normalizer.fit_transform(tfidf.toarray()) # 归一化
kmeans = KMeans(n_clusters=25) # 这里将新闻分成25类
k_labels = kmeans.fit_predict(scaled)
with open('cluster.pkl', 'wb') as file:
pickle.dump(k_labels, file)
else:
with open('cluster.pkl', 'rb') as file:
k_labels = pickle.load(file)
接下来就可以在簇内计算非新华社新闻和新华社新闻的相似度了。不过在此之前,我们可以使用贝叶斯分类器以每条新闻的tfidf向量为特征,以是否为新华社的文章作为标签,来构造模型。将预测为”新华社”新闻,但实际非”新华社”新闻挑选出来,作为”疑似抄袭”的对象,这样可以进一步缩减之后相似度计算量,当然这一步非必须,我们后面会对比不使用分类器的结果。1
2
3
4
5
6
7
8
9
10
11
12
13
14# 筛选嫌疑文章
# 准备训练集
from sklearn.model_selection import train_test_split
label = list(map(lambda source: 1 if '新华社' in str(source) else 0, news.source))
X_train, X_test, y_train, y_test = train_test_split(tfidf.toarray(), label, test_size=0.3)
# 准备分类模型
from sklearn.naive_bayes import MultinomialNB
clf = MultinomialNB() # 假设先验分布为多项式分布
clf.fit(X_train, y_train)
# 筛选出预测是新华社,但实际不是(疑似抄袭的)文章
prediction = clf.predict(tfidf.toarray())
compare = pd.DataFrame({'prediction':prediction, 'label':label})
copy = compare[(compare['prediction']==1) & (compare['label']==0)]
xinhua = compare[compare['label']==1].index
这样一来,所有”疑似抄袭”的新闻都放在了copy中了。接下来就是计算每条“疑似抄袭”新闻与所有”新华社”新闻之间的相似度了,这里选定阈值为0.9,如果余弦相似度超过0.9,则认为该”疑似抄袭”新闻确实抄袭了。调用分类器进行筛选后的计算结果放入results中,不调用分类器直接从全量非新华社新闻中计算的结果放入all_results中,方便进行对比。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35# 查找相似文档:与嫌疑文章同类,且相似度 >= 0.9
from sklearn.metrics.pairwise import cosine_similarity
def find_similar_text(cpindex, top=10):
simibase=0.9
simiindex = [] # 新华社中相似文章
similarity = [] # 相似文章的相似度
# 在新华社发布的同类文章中查找
for i in xinhua:
if k_labels[i]==k_labels[cpindex]:
similar = cosine_similarity(tfidf[cpindex], tfidf[i])[0, 0]
if similar >= simibase:
simiindex.append(i)
similarity.append(similar)
return simiindex, similarity
# 计算抄袭文章索引、所属类别、新华社相似文章索引以及相似度
if not os.path.exists('all_results_simi0.9.pkl'):
all_results = [] # 全量中筛选结果
results = [] # 嫌疑中筛选结果
for i, value in enumerate(label):
if value==0: # 所有非新华社的新闻
simiindex, similarity = find_similar_text(i)
if len(simiindex) > 0: # 列表非空,即该文章与新华社某些文章相似度大于0.9
all_results.append([i, k_labels[i], simiindex, similarity])
if i in copy.index: # 嫌疑新闻
results.append([i, k_labels[i], simiindex, similarity])
with open('all_results_simi0.9.pkl', 'wb') as file:
pickle.dump(all_results, file)
with open('results_simi0.9.pkl', 'wb') as file:
pickle.dump(results, file)
else:
with open('all_results_simi0.9.pkl', 'rb') as file:
all_results = pickle.load(file)
with open('results_simi0.9.pkl', 'wb') as file:
results = pickle.load(file)1
2
3
4
5
6
7
8
9
10
11# 最后输出
print('分类前:', len(all_results), '条抄袭新闻', '\t分类后:', len(results), '条抄袭新闻')
print('='*100)
for item in all_results:
print('文章编号:', item[0])
print('类型:', item[1])
print('出处:', news.iloc[item[0]].source)
print('新华社相似文章编号:', item[2])
print('相似度:', item[3])
print('分类后:有') if item in results else print('分类后:无')
print('-'*100)
实验结果
由于设置的阈值比较高,因此筛选出的”抄袭文章”较少

从中我们发现文章编号78和文章编号312相似度高达0.97,文章内容也是肉眼可的抄袭(文章内容如下),但却并没有被分类器判断为”疑似抄袭”,由此可见,此次分类器根据tfidf向量鉴别”新华社”特征并不准确。

扩展
手动计算idf
1 | import math |