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