1.前言
此实验由我和老兰和老李共同完成。代码稍后放到git上,现我将其整理如下:
2.思路图 + 代码(包含提取特征之外的所有词)
废话不多说,上代码:
#-*- coding: UTF-8 -*-
import bayesRecode
from numpy import *
import os
import pickle
def testingNB():
myVocabDict = bayesRecode.createVocabList()
listOPosts = bayesRecode.loadDataSet()
length = len(myVocabDict)
pAb = 1 / float(10)
dictP = {}
dictNum = {}
dictDenom = {}
jishu = 0
for postinDoc in listOPosts:
jishu += 1
print(jishu)
listMat = []
trainMat = bayesRecode.setOfWords2Vec(myVocabDict, postinDoc)
print(sum(trainMat))
type = postinDoc[0]
if dictNum.__contains__(type):
dictNum[type] = dictNum[type] + trainMat
dictDenom[type] += sum(trainMat)
else:
dictNum[type] = ones(length)
dictDenom[type] = 2.0
for trainType in dictNum:
numArray = dictNum[trainType]
total = dictDenom[trainType]
p = log(numArray/total)
dictP[trainType] = p
#
# with open("F:/课程/数据挖掘/mr.liyou/modelTrain1000.txt", 'wb') as f:
# pickle.dump(dictP, f)#pickle:python持久化存储,存到文件里
print("end train")
parentDir = os.listdir("E:/study/1.研一的课/数据挖掘实验/finalTest")
allTotal = 0
allCount = 0
for sonDir in parentDir:
type = ""
with open("E:/study/1.研一的课/数据挖掘实验/finalTest/" + sonDir) as f:
line = f.readline()
count = 0
total = 1
type = sonDir
calCount = 0
while line:
calCount += 1
if calCount > 50000:
break
splitFile = line.split(" ")
classifiction = splitFile[0]
thisDoc = array(bayesRecode.setOfWords2Vec(myVocabDict, splitFile))
predict = bayesRecode.classifyNB(thisDoc, dictP, pAb)
if classifiction == predict:
count += 1
total += 1
allTotal += 1
allCount += 1
else:
total += 1
allTotal += 1
line = f.readline()
print(count / float(total))
print(type)
print("total perception is %f" % (allCount / float(allTotal)))
testingNB()
# 最终相当于训练出10个大数组,里面存着概率
# 可以把每个训练好的模型存到文档里面,可以单独训练哪一类的模型
#test()
上面那段代码中引用的bayesRecode类:
#-*- coding: UTF-8 -*-
from numpy import *
import os
def loadDataSet():
postingList=[]
# parentDir = os.listdir("D:/1佩王的文件/机器学习-文本分类/test")
parentDir = os.listdir("E:/study/1.研一的课/数据挖掘实验/finalTest")
for sonDir in parentDir:
with open("E:/study/1.研一的课/数据挖掘实验/finalTest/"+sonDir, 'r') as f:
line = f.readline()
while line:
split = line.split(" ")
postingList.append(0)
postingList[len(postingList)-1] = split
line = f.readline()
return postingList
def createVocabList():
vocabDict = {} # create empty set
listFiles = os.listdir("E:/study/1.研一的课/数据挖掘实验/tfidfallresult20171130/1000")
cursor = 0
for sonListFile in listFiles:
with open("E:/study/1.研一的课/数据挖掘实验/tfidfallresult20171130/1000/"+sonListFile, 'r') as f:
line = f.readline()
line = f.readline()
while line:
# todo 把从line中读取的一行的 /n 符号去掉
# todo 把每个类别的标识号去掉
# 按步骤检查,每个步骤是否出错
# 每次编完一个函数之后,进行测试,不能实现所有功能,在进行测试。
# 记得写注释
word = line.split("\n")
if not vocabDict.__contains__(word[0]):
vocabDict[word[0]] = cursor
cursor += 1
line = f.readline()
if vocabDict.__contains__("\n"):
vocabDict.pop("\n")
if vocabDict.__contains__(""):
vocabDict.pop("")
print(vocabDict)
return vocabDict
def setOfWords2Vec(vocabDict, inputSet):
returnVec = [0] * len(vocabDict) #print([0] * 4005555)
for word in inputSet:
#if vocabDict.__contains__(word+"\n"):
#returnVec[vocabDict[word+"\n"]] += 1
if vocabDict.__contains__(word):
returnVec[vocabDict[word]] = returnVec[vocabDict[word]] + 1
return returnVec
def classifyNB(vec2Classify, dictP, pClass1):#返回最大概率的分类
maxP = sum(vec2Classify * dictP["mil"]) + log(pClass1)
predictType = "mil"
for type in dictP:
p = sum(vec2Classify * dictP[type]) + log(pClass1)
if p > maxP:
maxP = p
predictType = type
return predictType
3.数据获取
3.1 获取到的数据集的大小
共11类新闻,每类10万新闻,共110万数据。但其中有一个滚动新闻类的噪声很大,就没有用这个类。
训练集:测试集 = 1:1。训练集有10个类,每个类5万篇。测试集有10个类,每个类5万篇。
3.2编写爬虫从中国新闻网爬取新闻
爬取规则:
利用日期编写URL,比如http://www.chinanews.com/scroll-news/cj/2017/1129/news.shtml,其中的“cj”是指财经新闻,2017/1129是指2017年11月29日。
获取到财经类每天的新闻的URL后,使用爬虫挨个访问这些URL,返回是的一篇具体新闻的html代码,然后从这些代码中找到我们需要的新闻,保存到文件中。
技术路线:
爬虫使用Python+Scrapy爬虫框架。
中国新闻网没有反扒机制,不用设置动态代理,所以方便爬取。
3.3下载搜狐的历史新闻数据后解析
数据格式为:
<doc>
<url>页面URL</url>
<docno>页面ID</docno>
<contenttitle>页面标题</contenttitle>
<content>页面内容</content>
</doc>
4.字典处理
4.1分词
本次实验使用了中科院的分词库Nlpir进行分词
首先对Nlpir定义并初始化接口的静态变量,由以下代码完成:
随后,对原文数据和语料进行分词,原文数据以及语料基本是按行存储于txt文件中,故将txt中的各行按顺序输入到程序中进行分词处理,再将结果输出是一项更为合理的选择:
此处采用了Nlpir分词工具的1模式进行分词,1模式分词的优势是能够直接在分词结果给各个词标注上词性,便于后续取名词操作。
4.2去停用词和取名词
首先,根据上文的分词结果进行名词抽取,代码如下:
Nlpir提供了一个非常优秀的标注词性功能,他不仅标注有名词,还能区分出人名,地名等信息,如图所示。因此,在代码中便利的抽取了分词结果为普通名词的词,并去掉了所有只包含单一字符的名词,我们认为单一字符的名词并不包含任何意义,并且会影响分类的准确度,故去除了单一字符的名词。
将抽取出的名词与停用词表进行比对,并去除停用词,代码如下:
4.3降维
在本次实验中采取了TF-IDF进行降维,TF-IDF代码如下:
TF指的是词频(Term Frequency,Term表示词),标准的TF公式如下图所示:
以上式子中 是该词在文件 中的出现次数,而分母则是在文件 中所有字词的出现次数之和。
在我们设计的tf中,我们将一个类别看做是一片文档,因此对一个类别内所有的词进行了词频统计,将一个词在这个类别中出现的总次数除以这个类别中所有词的总个数,得到的比值为我们所需要的tf值。
IDF指的是逆文档频率( inverse document frequency),标准的IDF公式如下:
但我们将一个类别看做是一片文档,这是因为我们的数据量是100万,我们想在短时间内得到处理结果。因此我们直接将总类别数10类除以包含该词的类别数量,得到的比值再取对数,得到了IDF的值
最后将得到的TF值与IDF值相乘,得到TF-IDF值,即为该词在该类别中的权重。将权重值按大小进行排列,并且每个类别选取权重最高的1000个词作为字典进行输出。如下图所示。
5.特殊处理
5.1改进加权与IDF去0
1. 由于我们将每个类别看做是一片文档,导致每个词tf的值很低,基本处于〖10〗(-4)-〖10〗(-7)这个数量级,而idf的值为log10(x)x∈[0,10]的范围内,即idf取值属于0-1,,数量级基本为0.1,因此,idf的值难以对tf值造成影响,为此,经过研究,我们决定扩大tf的取值权重,我们将tf的值统一乘以〖10〗^5,扩大了tf取值,让idf能够更好的作用于tf,让输出的字典更符合能体现该类文档的内容。
2. 由于把每个类别看做一篇文档,因此我们的idf值常常会出现取0的情况,例如“系列”这个词在产品类别中出现的频率很高,达到了0.1的词频,但是由于其他类别中都出现过“系列”一词,导致系列一次的IDF值归零,使得“系列”一次在产品类中的tf-idf值为0,与其他类别相同,从而被从字典中排出。我们认为这样的排除是十分不合理的,并且测试下来发现这个排除最终影响了文档分类准确率。因此,我们选择对idf进行去0操作,具体实现就是将idf中的|D|加上1,成为|D|+1,这样由于分子上的值比所有文档数大了1,即使在所有文档中都出现过该词,该词的tf-idf值也不会归0,事实证明,这项处理有效的增加了本次实验文档分类的正确性。
5.2 初始化每个类的词向量时
初始化训模型的某个类的词向量时,初始化为1,以保证获取概率时不会出现0.
5.3训练模型存储到文件中
训练模型(即每一类的关键特征向量)存储到文件中,在预测测试集时,直接从文件中读出训练模型,省去了再一次训练的时间。
6.使用自编贝叶斯分类器
6.1零概率处理
加一平滑
6.2 程序大体算法介绍
6.2.1创建词典
读取文件中降维好的每类的特征(即关键词)。使用Python的dict(即key-value)来存储关键词特征,key是词,value是词的序号。
vocabDict = {}
for line in file.readlines(): # 每行是一个关键词(也就是一个词向量)
if line.strip() not in vocabDict:
vocabDict[line.strip()] = count # key:词,value:词的序号
count += 1
return vocabDict
6.2.2加载训练集中分好词的新闻数据
从文件中读取分好词的新闻数据,存放到一个list集合中,每篇新闻作为一个list中的元素。
for sonDir in parentDir:
with open("E:/study /数据挖掘实验/test/" + sonDir, 'r') as f:
line = f.readline() # 去掉首行
while line:
split = line.split(" ")
postingList.append(0) # 先用0占一下位置,然后用split这个列表把0覆盖掉
postingList[len(postingList) - 1] = split
line = f.readline()
return postingList
6.2.3统计训练集中每类新闻中关键词特征出现的次数,将训练完的模型结果存到文件中
对每一篇新闻,使用词袋模型进行关键词特征的出现次数的统计:
def bagOfWords2Vec(vocabDict, inputSet): # 返回文档向量
returnVec = [0] * len(vocabDict) #print([0] * 4005555)
for word in inputSet:
if word in vocabDict: # 若词汇表dict中有这个key的话
returnVec[vocabDict[word]] += 1
return returnVec
统计完一篇新闻后,将生成的这篇新闻的关键词向量,加到这一类的关键词向量上,同时将这篇新闻的总词数加到这类新闻的总词数上:
trainMat = bagOfWords2Vec(myVocabDict,postingDoc)
type = postingDoc[0]#每篇新闻的首个元素是新闻类别,如auto
if dictNum.__contains__(type):
dictNum[type] += trainMat#矩阵
dictDenom[type] += sum(trainMat)
else:#如果没有则初始化
dictNum[type] = ones(length)
dictDenom[type] = 2.0
统计完所有的训练集中的新闻后,将生成的矩阵存储到文件中,这样就不用每次都重复训练了:
with open("E:/study/1.研一的课/数据挖掘实验/modelTrain1000.txt", 'wb',encoding='gbk') as f:
pickle.dump(dictP, f)#pickle:python持久化存储,存到文件里
训练完的模型是一个这样的10*特征数量的矩阵:
6.2.4手写贝叶斯对测试集上的新闻进行预测,输出准确率召回率等
利用上一步训练好的模型,手写贝叶斯对测试集上的新闻进行预测。
贝叶斯的公式是:
P(A|W1,…,Wn) = P(W1,…,Wn |A) * P(A) / P(W1,…,Wn)
因为每条新闻的P(W1,…,Wn)是相同的,P(A)是很容易确定的(如本实验是1/10),那么我们来求棘手的P(W1,…,Wn |A)。假设每个特征之间相互独立,则P(W1,…,Wn |A)变为:
P(W1,…,Wn |A) = P(W1|A) * … * P(Wn|A)
利用求对数的方法log()把乘法转换成加法:
Log P(W1,…,Wn |A) = log P(W1|A) + … + log P(Wn|A)
而P(Wn|A) = Count(A类Wn词出现次数) / Count(A类总次数)
求出每篇新闻属于每一类的概率P1~P10,然后找出最大概率的类别,那么分类器就认定这篇新闻就是这个类别的!
准确率:准确率是预测正确的新闻的数量,除以总的训练集新闻的数量。
召回率:某一类的召回率Recall:又称为True Positive Rate,等于TP / (TP+FN),即该类中预测正确的除以该类中正确和错误的和(这个和就是该类的新闻数量)
F测度:F-measure = 2 * 召回率 * 准确率/ (召回率+准确率);这就是传统上通常说的F1 measure
6.3 程序运行结果
6.4 混淆矩阵
7.性能
◎ 最高的正确率= 98.9%
◎ 最高的召回率= 98.1%
◎ 最低的正确率= 70.2%
◎ 最低的召回率= 69.7%
◎ 平均正确率= 84.6%
◎ 平均召回率= 85%
◎ F测度 = 0.847
◎ 训练时间= 679s
◎ 测试时间= 462s