利用TF-IDF的机器学习方法对搜狗新闻数据进行文本分类

这篇文章分为两个部分,第一部分是叙述TF-IDF的计算过程,第二部分是基于sklearn利用TF-IDF建立多种机器学习模型进行文本分类。其中文本分类使用的数据集来自搜狗实验室提供的新闻数据,使用的是其中完整版648MB的数据。

TF-IDF的计算过程

TF-IDF(Term Frequency - Inverse Document Frequency)即词频-逆向文本频率,是一种用于信息检索和文本挖掘的常用加权技术。假设一个语料库包含多个文件,TF-IDF则用于评估一字词对于一个语料库中的其中一份文件的重要程度。字词的重要性会随着它在文件中出现的次数成正比增加,但同时也会随着它在语料库中出现的频率成反比下降。

词频(TF)

词频(term frequency)指的是某一个给定的词语在该文件中出现的频率。对于在某一文件里的词语$t_i$来说,它的重要性可表示为:

其中,$n_{i,j}$是该词在文件$d_j$中的出现次数,而分母是文件$d_j$中所有字词的出现次数总和。

逆向文本频率(IDF)

逆向文件频率(inverse document frequency)是一个词语普遍重要性的度量。某一特定词语的idf,可以由总文件数目除以包含该词语之文件的数目,再将得到的商取以10为底的对数得到:

其中,$|D|$是语料库中的文件总数,$|j:t_i \in d_j|$是包含词语$t_i$的文件数目。如果没有一个文件包含词语$t_i$,那么导致分母为零,所以通常使用$|j:t_i \in d_j|+1$。

词频-逆向文本频率(TF-IDF)

TF-IDF即将TF和IDF相乘。下面首先用个简单的例子介绍一下TF-IDF的计算过程。

假设给定一个包含4个文件的语料库:

1
2
3
4
5
6
7
8
9
from collections import Counter
import numpy as np

corpus = [
'this is the first document',
'this is the second second document',
'and the third one',
'is this the first document'
]

将其分词后加入数组corpus_list中:

1
2
3
4
5
# 分词
corpus_list = []
for i in range(len(corpus)):
corpus_list.append(corpus[i].split(' '))
print(corpus_list)

然后对每个文件中的单词统计其在文件中的出现次数:

1
2
3
4
5
# 统计词频
count_list = []
for i in range(len(corpus_list)):
count_list.append(Counter(corpus_list[i]))
print(count_list)

计算语料库中每个文件中每个单词的TF-IDF:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 计算TF-IDF
def TFIDF(word, count, count_list):
# 计算tf
tf = count[word] / sum(count.values())
# 计算idf
num_contain = sum(1 for count in count_list if word in count)
idf = np.log10(len(count_list) / (num_contain+1))
# 计算tf-idf
return tf*idf


for i, count in enumerate(count_list):
# 创建字典,存储每个单词的TF-IDF
score_dic = {word: TFIDF(word, count, count_list) for word in count}
print(score_dic)

结果如下:

基于sklearn利用TF-IDF进行文本分类

数据清洗与准备

所谓数据分析,真的是一大半时间全部花在了数据的预处理上。将搜狗实验室的原始新闻数据下载下来后会有一个SogouCS.reduced文件夹,里面有128个txt文件,包含着所有新闻数据。

数据格式为:

1
2
3
4
5
6
7
8
9
10
11
<doc>

<url>页面URL</url>

<docno>页面ID</docno>

<contenttitle>页面标题</contenttitle>

<content>页面内容</content>

</doc>

我们要做的第一步就是将这些数据从txt文件中提取出来,这里用强大的正则表达式来提取。要注意的是,下载下来的数据是用gbk进行编码的,记得设置一下decode的方式是gbk。由于数据中也没有给类别数据,不过好在url中的三级域名就是其对应的类别。因此新闻的类别可以从url中提取,这里使用的正则表达式 re.compile(r’http://(.*?).sohu.com‘, re.S) 来匹配。

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
import re
import os
import jieba.posseg as pseg
from collections import Counter

# 定义正则表达式
patternURL = re.compile(r'<url>(.*?)</url>', re.S)
patternCtt = re.compile(r'<content>(.*?)</content>', re.S)
contents_total = []
classes_total = []

for file in os.listdir("./SogouCS.reduced"):
# 设置路径打开文件
file_path = os.path.join("./SogouCS.reduced", file)
text = open(file_path, 'rb').read().decode("gbk", 'ignore')

# 正则匹配出url和content
urls = patternURL.findall(text)
contents = patternCtt.findall(text)
classes = []
for i in range(urls.__len__()):
patternClass = re.compile(r'http://(.*?).sohu.com', re.S)
classes.append(patternClass.findall(urls[i])[0])

# 得到所有contents和classes
contents_total = contents_total + contents
classes_total = classes_total + classes

看看这些新闻的种类共有多少以及每个种类有多少篇新闻:

可以看到,有些奇奇怪怪的种类,不知道什么意思,这里将他们删除,同时删除字数小于100个字的新闻,提高分类的准确度:

1
2
3
4
5
6
# 去除几个不需要的种类,同时删除字数小于100字的新闻
for i in range(contents_total.__len__())[::-1]:
if (len(contents_total[i]) < 100 or classes_total[i] == '2008' or classes_total[i] == 'auto'
or classes_total[i] == 'cul' or classes_total[i] == 'mil.news' or classes_total[i] == 'career'):
contents_total.pop(i)
classes_total.pop(i)

删除之后,再看一下,发现每类新闻的数量不太均衡,于是从每个类别中抽取2000条数据:

1
2
3
4
5
6
7
8
9
# 每一类提取2000个新闻
X = []
y = []
d = {"business":0, "health":0, "house":0, "it":0, "learning":0, "news":0, "sports":0, "travel":0, "women":0, "yule":0}
for i in range(len(classes_total))[::-1]:
if (d[classes_total[i]] < 2000):
d[classes_total[i]] += 1
X.append(contents_total[i])
y.append(classes_total[i])

至此,初步的数据准备就完成了,得到两个一维数组X和y,X中包含所有的新闻,每条新闻是一个字符串,同样,y中包含所有新闻对应的类别。

中文分词

如果是英文语料,那就十分简单了,直接用空格进行分词就可以。但中文嘛……有点困难,不过好在有大神开发了中文分词工具,这里使用的是jieba分词工具,这是一个相当不错的中文分词工具了,推荐使用!

1
2
3
4
5
6
7
8
9
# 对所有语料进行分词
X_fenci = []
for line in X:
words = pseg.cut(line)
line0 = []
for w in words:
if 'x' != w.flag:
line0.append(w.word)
X_fenci.append(' '.join(line0))

分词的计算量有点大,感觉可慢了,如果数据量实在太大的话可以考虑多进程分词。将分词好的语料保存:

1
2
3
4
5
6
7
# 将分词好的语料保存
file_X_fenci = open("X_fenci.txt", 'w')
file_y = open("y.txt", 'w')
file_X_fenci.write('\n'.join(X_fenci))
file_y.write('\n'.join(y))
file_X_fenci.close()
file_y.close()

TF-IDF特征提取

第一步是将刚才分词好的数据导入,然后使用sklearn的接口提取文本的tf-idf特征,将文本转换为文档-词项矩阵。然后划分训练集和测试集,这里将前15000条数据划分为训练集,后5000条数据划分为测试集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def tf_idf(contents):
# 提取文本特征tf-idf
vectorizer = CountVectorizer(min_df=1e-5)
transformer = TfidfTransformer()
tfidf = transformer.fit_transform(vectorizer.fit_transform(contents))
return tfidf


# 导入数据
X_fenci = open("X_fenci.txt", 'rb').read().decode("gbk", "ignore").split('\n')
y = open("y.txt", 'rb').read().decode("gbk", "ignore").split('\n')

# 提取tf-idf特征以及数据划分
X_ifidf = tf_idf(X_fenci)
X_train, y_train = X_ifidf[:15000], y[:15000]
X_test, y_test = X_ifidf[15000:], y[15000:]

模型建立与测试

朴素贝叶斯模型

1
2
3
4
5
6
7
8
9
10
11
"""朴素贝叶斯模型"""
NB_model = MultinomialNB(alpha=0.01)
NB_model.fit(X_train, y_train)

preds = NB_model.predict(X_test)

num = 0
for i, pred in enumerate(preds):
if pred == y_test[i]:
num += 1
print("precision_score:" + str(float(num)/len(preds)))

朴素贝叶斯模型很简单,但同时也很有效,precision_score:0.7842

逻辑回归模型

1
2
3
4
5
6
7
8
9
10
11
"""逻辑回归模型"""
lr_model = LogisticRegressionCV(solver='newton-cg', multi_class='multinomial', cv=10, n_jobs=-1)
lr_model.fit(X_train, y_train)

preds = lr_model.predict(X_test)

num = 0
for i, pred in enumerate(preds):
if pred == y_test[i]:
num += 1
print("precision_score:" + str(float(num)/len(preds)))

选用十折交叉验证,训练时间有点长,precision_score:0.8412

线性支持向量机模型

1
2
3
4
5
6
7
8
9
10
11
"""线性支持向量机模型"""
svm_model = SVC(kernel="linear")
svm_model.fit(X_train, y_train)

preds = svm_model .predict(X_test)

num = 0
for i, pred in enumerate(preds):
if pred == y_test[i]:
num += 1
print("precision_score:" + str(float(num)/len(preds)))

precision_score:0.7958

K-近邻模型

1
2
3
4
5
6
7
8
9
10
11
12
"""K-近邻模型"""
for x in range(1, 15):
knn_model = KNeighborsClassifier(n_neighbors=x)
knn_model.fit(X_train, y_train)

preds = knn_model.predict(X_test)

num = 0
for i, pred in enumerate(preds):
if pred == y_test[i]:
num += 1
print("precision_score:" + str(float(num) / len(preds)))

在K-近邻模型中,经测试,当k取值为8时,模型在测试集上的表现最好,precision_score:0.734。

总结

尽管在这个样本集上,逻辑回归模型的表现最好,各个模型的表现差距也不是很明显。但机器学习方法五花八门,具体用什么方法还是得取决于具体的问题。这里的数据量有限,只是将每类的新闻抽取两千条,整个数据集也才两万条新闻,当数据量上升,模型的训练时间也是必须要考虑的问题,参数优化也是必不可少的。而且仅用precision_score作为单一的评价标准还是太简陋了,之后有时间的话再加入LSTM、CNN等深度学习模型以及更多的机器学习评价标准进行进一步的模型评估吧。

参考

https://zh.wikipedia.org/wiki/Tf-idf
https://blog.csdn.net/Shuang_Mo/article/details/81916214
https://blog.csdn.net/Techmonster/article/details/74905668
https://zhuanlan.zhihu.com/p/26729228
https://www.libinx.com/2018/text-classification-classic-ml-by-sklearn/
https://blog.csdn.net/pnnngchg/article/details/86601168
https://blog.csdn.net/sadfassd/article/details/80568321
https://www.libinx.com/2018/text-classification-cnn-by-tensorflow/