一、环境以及数据,项目地址
- 环境:
Python3.8 - 入门理论部分:
- 数据:
网盘:
pan.baidu.com/s/1tknw4W7A…
提取码:1024
- 项目地址
gitee地址为:gitee.com/huadeng863/…
此项目为2021年软件杯赛题,挂个赛题链接www.cnsoftbei.com/plus/view.p… 可以查看具体题目要求。
- 可以运行体验一下,一般一个py文件要运行5-6分钟,可以对照文章截图理解一下自然语言处理的实际操作。
- 为了方便立即将文章分为几个部分来讲解。
第一步:划分训练集和测试集(choosed.py)
数据已经全部放入data文件夹中,data文件夹下有九大类新闻文本,
每个新闻分类中是他的所有新闻样例,一条新闻单独一个txt文件,文件均有序号标注。
通过对所有数据样本进行统计:
可以发现样本成不均匀分布,为了保证训练模型中不会偏向某个分类,因此要保证不同分类的训练样本数目一致,所以,最后随机从每个分类中取5000条。并最终以8:2划分训练集和测试集。
实现代码及注释如下:
import os
import random
import shutil
path='./data/'
cate_list = os.listdir(path) # 获取未分词语料库中所有类别
for mydir in cate_list:
train_dir = './train_corpus/' + mydir + "/" # 拼出训练集存贮的对应目录路径如:train_corpus/体育/
if not os.path.exists(train_dir): # 是否存在分词目录,如果没有则创建该目录
os.makedirs(train_dir)
test_dir = './test_corpus/' + mydir+'/' # 拼出测试存贮的对应目录路径如:test_corpus/体育/
if not os.path.exists(test_dir):
os.makedirs(test_dir)
class_path=path+mydir+'/' # 生成当前类别的目录 如 data/体育/
file_list=os.listdir(class_path) # 生成分类下文本名称列表
length=len(file_list)
print(mydir,length)
res = random.sample(range(1, length+1), 5000) #在所有文件中随机选取五千个生成索引列表,乱序
train=res[0:4000]#训练集占80%,4000条
test=res[4000:5000]#测试集占20%,1000条
#print(len(train),len(test))
for file_path in file_list:
fullname = class_path + file_path # 拼出文件名全路径如:data/体育/21.txt
#print(file_path)
x=int(file_path.split('.')[0].split('_')[1])#将文件名中的序号取出
if x in train:
shutil.copyfile(fullname, train_dir+file_path)#将前面的文件复制到后面的目录中
if x in test:
shutil.copyfile(fullname, test_dir + file_path)
执行choosed.py文件划分前:
划分后将data文件分成两部分(训练集和测试集):
第二步:语料分词(corpus_segment.py)
之前已经介绍过分词的jieba库,所以直接上手就可以了。
实现代码及注释如下:
import os
import jieba
from Tools import savefile, readfile
def corpus_segment(corpus_path, seg_path):
catelist = os.listdir(corpus_path) # 获取corpus_path下的所有子目录
#print(catelist)
'''
其中子目录的名字就是类别名,例如:
train_corpus/体育/21.txt中,'train_corpus/'是corpus_path,'体育'是catelist中的一个成员
'''
# 获取每个目录(类别)下所有的文件
for mydir in catelist:
class_path = corpus_path + mydir + "/" # 拼出分类子目录的路径如:train_corpus/体育/
seg_dir = seg_path + mydir + "/" # 拼出分类分词目录的路径如:train_seg/体育/
if not os.path.exists(seg_dir): # 是否存在分词目录
os.makedirs(seg_dir) #如果没有则创建该目录
file_list = os.listdir(class_path) # 获取未分词语料库中某一类别中的所有文本
for file_path in file_list: # 遍历类别目录下的所有文件
fullname = class_path + file_path # 拼出文件名全路径如:train_corpus/art/21.txt
content = readfile(fullname) # 读取文件内容
'''此时,content里面存贮的是原文本的所有字符,例如多余的空格、空行、回车等等,
接下来,我们需要把这些无关痛痒的字符统统去掉,变成只有标点符号做间隔的紧凑的文本内容
为了保证大量文本的读取速度,Tools封装的readfiles为rb方法,也就是二进制的读写,所以需要转成'utf-8'编码
'''
content = content.replace('rn'.encode('utf-8'), ''.encode('utf-8')).strip() # 删除换行
content = content.replace(' '.encode('utf-8'), ''.encode('utf-8')).strip() # 删除空行、多余的空格
content_seg =jieba.cut(content) # 为文件内容分词
savefile(seg_dir + file_path, ' '.join(content_seg).encode('utf-8')) # 将处理后的文件保存到分词后语料目录
if __name__ == "__main__":
seg_path = "./train_corpus_seg/"#分词之后的语料库储存路径
corpus_path = "./train_corpus/"#需要分词的语料库路径
corpus_segment(corpus_path, seg_path)
print("训练语料分词结束!!!")
seg_path = "./test_corpus_seg/" # 分词之后的语料库储存路径
corpus_path = "./test_corpus/" # 需要分词的语料库路径
corpus_segment(corpus_path, seg_path)
print("测试语料分词结束!!!")
执行corpus_segment.py.py文件分词前:
分词后(将训练预料和测试语料分词后放入seg文件夹中):
第三步:bunch化操作(corpus2Bunch.py)
Bunch本质上的数据类型是dict字典类型,在这里为了方便处理将每个文本的分类类别,文件名路径,文件内容(已分词)转换成一个个的字典类型方便后续处理。
实现代码及注释如下:
import os
import pickle
from sklearn.datasets._base import Bunch
from Tools import readfile
def corpus2Bunch(wordbag_path, seg_path):
catelist = os.listdir(seg_path) # 获取seg_path下的所有子目录,也就是分类信息
# 创建一个Bunch实例
bunch = Bunch(target_name=[], label=[], filenames=[], contents=[])
bunch.target_name.extend(catelist)
'''
extend(addlist)是python list中的函数,意思是用新的list(addlist)去扩充
原来的list
'''
# 获取每个目录下所有的文件
for mydir in catelist:
class_path = seg_path + mydir + "/" # 拼出分类子目录的路径
file_list = os.listdir(class_path) # 获取class_path下的所有文件
for file_path in file_list: # 遍历类别目录下文件
fullname = class_path + file_path # 拼出文件名全路径
bunch.label.append(mydir)
bunch.filenames.append(fullname)
bunch.contents.append(readfile(fullname)) # 读取文件内容
'''append(element)是python list中的函数,意思是向原来的list中添加element,注意与extend()函数的区别'''
# 将bunch存储到wordbag_path路径中
with open(wordbag_path, "wb") as file_obj:
pickle.dump(bunch, file_obj)
print("构建文本对象结束!!!")
if __name__ == "__main__":
# 对训练集进行Bunch化操作:
wordbag_path = "train_word_bag/train_set.dat" # Bunch存储路径
if not os.path.exists("train_word_bag"): # 是否存在分词目录,如果没有则创建该目录
os.makedirs("train_word_bag")
seg_path = "train_corpus_seg/" # 分词后分类语料库路径
corpus2Bunch(wordbag_path, seg_path)
# 对测试集进行Bunch化操作:
wordbag_path = "test_word_bag/test_set.dat" # Bunch存储路径
if not os.path.exists("test_word_bag"): # 是否存在分词目录,如果没有则创建该目录
os.makedirs("test_word_bag")
seg_path = "test_corpus_seg/" # 分词后分类语料库路径
corpus2Bunch(wordbag_path, seg_path)
执行corpus2Bunch.py,bunch前:
bunch化后多出两个词袋文件夹,里面存放的是所有训练集、测试集集成好得一个dat文件,方便了后续的处理、
第四步:构建TF-IDF向量空间(TFIDF_space.py)
这一步包括了去停用词,特征工程和特征选择。这部分的理论在上一篇文章做了简单的介绍,而且这个方法已经在python的sklearn中封装好了方法。
实现代码及注释如下:
from sklearn.datasets._base import Bunch
from sklearn.feature_extraction.text import TfidfVectorizer
from Tools import readfile, readbunchobj, writebunchobj
def vector_space(stopword_path, bunch_path, space_path, train_tfidf_path=None):
stpwrdlst = readfile(stopword_path).splitlines()
bunch = readbunchobj(bunch_path)
tfidfspace = Bunch(target_name=bunch.target_name, label=bunch.label, filenames=bunch.filenames, tdm=[],
vocabulary={})
if train_tfidf_path is not None:
trainbunch = readbunchobj(train_tfidf_path)
tfidfspace.vocabulary = trainbunch.vocabulary
vectorizer = TfidfVectorizer(stop_words=stpwrdlst, sublinear_tf=True, max_df=0.5,
vocabulary=trainbunch.vocabulary)
'''
#.stop_words=list类型,直接过滤指定的停用词。
# sublinear_tf:,计算tf值采用亚线性策略。比如,我们以前算tf是词频,现在用1+log(tf)来充当词频。
#max_df,过滤出现在超过max_df=0.5比例的句子中的词语,当他在全文档出现的频次过多>50%时我们认为他太过常见而不具备代表性
# .vocabulary: dict类型,只使用特定的词汇,为了避免在测试集中出现训练集中没有出现的词汇而造成困扰所以一般会用这个,但是如果训练集足够大可以不用
'''
tfidfspace.tdm = vectorizer.fit_transform(bunch.contents)
else:
vectorizer = TfidfVectorizer(stop_words=stpwrdlst, sublinear_tf=True, max_df=0.5)
'''
#.stop_words=list类型,直接过滤指定的停用词。
# sublinear_tf:,计算tf值采用亚线性策略。比如,我们以前算tf是词频,现在用1+log(tf)来充当词频。
# max_df,过滤出现在超过max_df=0.5比例的句子中的词语,当他在全文档出现的频次过多>50%时我们认为他太过常见而不具备代表性
'''
tfidfspace.tdm = vectorizer.fit_transform(bunch.contents)
tfidfspace.vocabulary = vectorizer.vocabulary_
writebunchobj(space_path, tfidfspace)
print("if-idf词向量空间实例创建成功!!!")
if __name__ == '__main__':
stopword_path = "hit_stopwords.txt"
bunch_path = "train_word_bag/train_set.dat"
space_path = "train_word_bag/tfdifspace.dat"
vector_space(stopword_path, bunch_path, space_path)
bunch_path = "test_word_bag/test_set.dat"
space_path = "test_word_bag/testspace.dat"
train_tfidf_path = "train_word_bag/tfdifspace.dat"
vector_space(stopword_path, bunch_path, space_path, train_tfidf_path)
执行后会在对应词袋文件中生成TF-IDF向量空间实例,嫌麻烦就不展示了。
第五步:训练模型
在sklearn中已经封装好的库直接调用就可以了。
from sklearn.linear_model import LogisticRegression
from sklearn.naive_bayes import MultinomialNB # 导入多项式贝叶斯算法
from sklearn import metrics
from Tools import readbunchobj
import os
# 导入训练集
trainpath = "train_word_bag/tfdifspace.dat"
train_set = readbunchobj(trainpath)
# 导入测试集
testpath = "test_word_bag/testspace.dat"
test_set = readbunchobj(testpath)
# 训练分类器:输入词袋向量和分类标签,alpha:0.001 alpha越小,迭代次数越多,精度越高
clf = MultinomialNB(alpha=0.001).fit(train_set.tdm, train_set.label)
#clf = LogisticRegression(C=1000.0).fit(train_set.tdm, train_set.label)
print("训练完毕!!!")
# 预测分类结果
predicted = clf.predict(test_set.tdm)
print("预测完毕!!!")
# 计算分类精度:
def metrics_result(actual, predict):
print('精度:{0:.3f}'.format(metrics.precision_score(actual, predict, average='weighted')))
print('召回:{0:0.3f}'.format(metrics.recall_score(actual, predict, average='weighted')))
print('f1-score:{0:.3f}'.format(metrics.f1_score(actual, predict, average='weighted')))
metrics_result(test_set.label, predicted)
之前理论介绍的是朴素贝叶斯模型,但是走到这一步发现朴素贝叶斯和TF-IDF的相适性并不好,至少逻辑回归比他更合适。
但是都走到这一步我是不想换模型的。先试一下贝叶斯的效果。
再看看逻辑回归的效果:
贝叶斯模型在精确度上全面溃败。但是你实际运行会发现贝叶斯运行的比逻辑回归要快很多,虽然精确度才是王道。
个人总结
这只是文本分类入门的基础的机器学习模型,但是这种机器学习模型解释性更强,能够更好地让我们理解文本分类的基本原理和运作流程。
事实上,目前深度学习的神经网络模型在分类,预测等任务上的表现效果和机器学习模型表现差距也不是很大。
至少在kaggle这类比赛中,树模型和神经网络的表现可以说是平分秋色。
但是昂,文本分类的向量转换决定了深度学习才是这类任务的最终归宿。