本文介绍一下使用朴素贝叶斯算法来做文本分类任务。
数据集是搜狗新闻数据集“corpus_6_4000”,它包含六大类新闻,每类新闻4000篇,每篇新闻长度在几百到几千字不等。六类新闻分别是’Auto’, ‘Culture’, ‘Economy’, ‘Medicine’, ‘Military’, ‘Sports’。今天的任务就是使用监督学习算法(朴素贝叶斯)来实现文本自动分类问题。话不多说,让我们开干吧!
1、数据探索分析和预处理
首先看看数据集是什么样的
file_dir = './corpus_6_4000'
file_list = os.listdir(file_dir)
file_list[:5]
"""['Auto_0.txt', 'Auto_1.txt', 'Auto_10.txt', 'Auto_100.txt', 'Auto_1000.txt']"""
看一下文件名称,从名称可以看出文件名称就是新闻的类别(标签),因此我们需要把这些标签提取出来,让他们和文本组成一一对应的列表,如[[doc1,label1],[doc2,label2],…]形式。
stop_word = []
with open('stopwords_cn.txt','r') as f:
for word in f.readlines():
stop_word.append(word.strip())
data_set = []
for file in file_list:
doc_label = []
file_path = file_dir + '/' + file
with open(file_path) as f:
data = f.read()
data = re.sub('[a-zA-Z0-9]+','',data.strip())
data = jieba.cut(data)
datas = [word for word in data if word not in stop_word and len(word)>1]
doc_label.append(datas)
label = file.split('_')[0]
doc_label.append(label)
data_set.append(doc_label)
stop_word为加载的停用词列表,data_set存储新闻文档,doc_label存储标签。在这里我们将文档中的数字字母都去掉了,而且分词之后,把单个的字也去掉了;因为我觉得这些字对文档的特征表达意义不大。
接下来将数据集的顺序随机打乱,并且保存下来,供之后使用。
random.shuffle(data_set)
pickle.dump(data_set,open('./data/data_set.pkl','wb'))
df = pd.DataFrame(data_set,columns=['data','label'])
df['label'] = df['label'].map(map_list)
data = df['data'].tolist()
label = df['label'].tolist()
train_data = data[:16800]
test_data = data[16800:]
train_label = label[:16800]
test_label = label[16800:]
pickle.dump(train_data,open('./data/train_data.pkl','wb'))
pickle.dump(test_data,open('./data/test_data.pkl','wb'))
pickle.dump(train_label,open('./data/train_label.pkl','wb'))
pickle.dump(test_label,open('./data/test_label.pkl','wb'))
我将24000篇新闻的80%用于训练模型,20%用于测试。
2、特征工程
老话说的好,机器学习中,特征的好坏决定了你的模型性能的上限,而算法的好坏决定你能否逼近这个上限。因此,特征工程非常重要。那么怎么对文本进行构造特征?由于计算机是不能对字符串进行计算的,因此我们需要将文本进行数字化,也就向量化,我们把每一篇新闻用一个向量表示,那么怎样用向量表示呢?
第一个思路是使用关键词词频,也就是哪些词出现的次数多,我就把他们作为关键词,然后构造向量空间模型。经过对训练数据集的统计发现:
[(‘中国’, 43812),
(‘一个’, 29356),
(‘市场’, 23137),
(‘汽车’, 22275),
(‘没有’, 20719),
(‘已经’, 17960),
(‘发展’, 17408),
(‘进行’, 16574),
(‘目前’, 15792),
(‘公司’, 15480),
(‘问题’, 15072),
(‘表示’, 14901),
(‘记者’, 14688),
(‘文化’, 14311),
(‘可能’, 13462),
(‘工作’, 13163),
(‘国家’, 13156),
(‘北京’, 12984),
(‘网易’, 12710),
(‘认为’, 11984),
(‘.%’, 11888),
(‘美国’, 11732),
(‘比赛’, 11615),
(‘经济’, 11422),
(‘成为’, 11016),
(‘企业’, 10861),
(‘方面’, 10685),
(‘车型’, 10465),
(‘现在’, 10403),
(‘医院’, 10308)]
我们看到有相当多高词频的词根本是对分类没有任何意义的,比如”中国“,”一个“,”目前“等,如果把词频最高的一些词作为特征去构造特征,这样数据集中将有很大的噪声,并且无法分类,我选取了词频最高的5000个词,10000个词,15000个词分别作了实验,准确率都只有16%左右。因此很明显,选用词频构造特征完全不行。
于是我们把目光转向TF-IDF,TF-IDF指词频和逆文档频率。词频计算公式如下:
公式里除以文章总词数是为了消除不同文章的长短不同所带来的影响。词频也可以如下计算:
逆文档频率计算公式如下:
IF-IDF就是将两者相乘
TF-IDF的具体意思,以及它为什么能做表示文章特征的原因,参看这篇文章。
计算TF-IDF可以使用sklearn库里面的函数进行计算,但是今天,我自己动手实现了一下,也让大家能更好的理解TF-IDF。
def make_idf_vocab(train_data):
if os.path.exists('./data/idf.pkl'):
idf = pickle.load(open('./data/idf.pkl','rb'))
vocab = pickle.load(open('./data/vocab.pkl','rb'))
else:
word_to_doc = {}
idf = {}
total_doc_num = float(len(train_data))
for doc in train_data:
for word in set(doc):
if word not in word_to_doc.keys():
word_to_doc[word] = 1
else:
word_to_doc[word] += 1
for word in word_to_doc.keys():
if word_to_doc[word] > 10:
idf[word] = np.log(total_doc_num/(word_to_doc[word]+1))
sort_idf = sorted(idf.items(),key=lambda x:x[1])
vocab = [x[0] for x in sort_idf]
pickle.dump(idf,open('./data/idf.pkl','wb'))
pickle.dump(vocab,open('./data/vocab.pkl','wb'))
return idf,vocab
word_to_doc字典存储每一个词在多少篇文章出现,vocab存储经过idf值从大到小排序后的词的列表。本文中我将那些只在10篇及以下的新闻中出现的那些词排除掉了。
接下来实现计算文档词频的函数。
def cal_term_freq(doc):
term_freq = {}
for word in doc:
if word not in term_freq.keys():
term_freq[word] = 1
else:
term_freq[word] += 1
for word in term_freq.keys():
term_freq[word] = term_freq[word]/float(len(doc))
return term_freq
计算单个文档的词频,完全按照以上公式来的。
接下来实现构造文档特征的函数。
def make_doc_feature(vocab,idf,doc,topN):
doc_feature = [0.]*topN
vocab = vocab[:topN]
tf = cal_term_freq(doc)
for word in doc:
if word in vocab:
index = vocab.index(word)
doc_feature[index] = tf[word]*idf[word]
return doc_feature
topN确定构造多少维的特征向量,维数越高,包含的信息也越多,但是噪声也会越多,而且会增加计算难度。
将训练数据集矩阵转换成tfidf权重矩阵。
def make_tfidf(train_data,vocab,idf,topN):
tfidf_data = []
for doc in train_data:
doc_feature = make_doc_feature(vocab,idf,doc,topN)
tfidf_data.append(doc_feature)
return tfidf_data
到这里,特征工程完成了!
3、训练模型
我先后使用了多项式朴素贝叶斯算法和K近邻算法来进行分类,发现朴素贝爷效果更好;这里使用多项式贝叶斯的原因是,我们的tfidf特征值是0-1之间的实数,是连续的,而伯努利贝叶斯适合离散型的特征。
train_data = pickle.load(open('./data/train_data.pkl','rb'))
train_label = pickle.load(open('./data/train_label.pkl','rb'))
idf,vocab = make_idf_vocab(train_data)
tfidf_data = make_tfidf(train_data,vocab,idf,6000)
train_x = np.array(tfidf_data[:13500])
train_y = np.array(train_label[:13500])
val_x = np.array(tfidf_data[13500:])
val_y = np.array(train_label[13500:])
我使用13500个文本作为训练集,剩下的3300个文本作为验证集。另外,我topN取6000,也就是我选6000个idf值排前的特征词来构文本造特征向量。
在验证集上准确率达到94%,非常不错,所以我就没有调参,打算直接用这个模型去测试集上试试。
另外使用KNN的结果如下,只有59%的准确率,差很多。
朴素贝叶斯算法在测试集上的结果如下:
和验证集上的结果差不多。
所以可以看到做文本分类时,采用TF-IDF作为文本的特征效果是非常不错的。另外我们也可以采用互信息作为特征。。很晚了,不说了,睡觉去了。。。