一,引言
前兩章的KNN分類算法和決策樹分類算法最終都是預測出實例的確定的分類結果,但是,有時候分類器會產生錯誤結果;本章要學的朴素貝葉斯分類算法則是給出一個最優的猜測結果,同時給出猜測的概率估計值。
1 准備知識:條件概率公式
相信學過概率論的同學對於概率論絕對不會陌生,如果一時覺得生疏,可以查閱相關資料,在這里主要是想貼出條件概率的計算公式:
P(A|B)=P(A,B)/P(B)=P(B|A)*P(A)/P(B)
2 如何使用條件概率進行分類
假設這里要被分類的類別有兩類,類c1和類c2,那么我們需要計算概率p(c1|x,y)和p(c2|x,y)的大小並進行比較:
如果:p(c1|x,y)>p(c2|x,y),則(x,y)屬於類c1
p(c1|x,y)<p(c2|x,y),則(x,y)屬於類c2
我們知道p(x,y|c)的條件概率所表示的含義為:已知類別c1條件下,取到點(x,y)的概率;那么p(c1|x,y)所要表達的含義呢?顯然,我們同樣可以按照條件概率的方法來對概率含義進行描述,即在給定點(x,y)的條件下,求該點屬於類c1的概率值。那么這樣的概率該如何計算呢?顯然,我們可以利用貝葉斯准則來進行變換計算:
p(ci|x,y)=p(x,y|ci)*p(ci)/p(x,y)
利用上面的公式,我們可以計算出在給定實例點的情況下,分類計算其屬於各個類別的概率,然后比較概率值,選擇具有最大概率的那么類作為點(x,y)的預測分類結果。
以上我們知道了通過貝葉斯准則來計算屬於各個分類的概率值,那么具體而言,就是計算貝葉斯公式中的三個概率,只要得到了這三個概率值,顯然我們就能通過貝葉斯算法預測分類的結果了。因此,到了這里,我們就知道了朴樹貝葉斯算法的核心所在了。
3 朴素貝葉斯中朴素含義
"朴素"含義:本章算法全稱叫朴素貝葉斯算法,顯然除了貝葉斯准備,朴素一詞同樣重要。這就是我們要說的條件獨立性假設的概念。條件獨立性假設是指特征之間的相互獨立性假設,所謂獨立,是指的是統計意義上的獨立,即一個特征或者單詞出現的可能性與它和其他單詞相鄰沒有關系。舉個例子來說,假設單詞bacon出現在unhealthy后面是與delisious后面的概率相同。當然,我們知道其實並不正確,但這正是朴素一詞的含義。同時,朴素貝葉斯另外一個含義是,這些特征同等重要。雖然這些假設都有一定的問題,但是朴素貝葉斯的實際效果卻很好。
二,朴素貝葉斯完成文檔分類
朴素貝葉斯的一個非常重要的應用就是文檔分類。在文檔分類中,整個文檔(比如一封電子郵件)是實例,那么郵件中的單詞就可以定義為特征。說到這里,我們有兩種定義文檔特征的方法。一種是詞集模型,另外一種是詞袋模型。顧名思義,詞集模型就是對於一篇文檔中出現的每個詞,我們不考慮其出現的次數,而只考慮其在文檔中是否出現,並將此作為特征;假設我們已經得到了所有文檔中出現的詞匯列表,那么根據每個詞是否出現,就可以將文檔轉為一個與詞匯列表等長的向量。而詞袋模型,就是在詞集模型的基礎上,還要考慮單詞在文檔中出現的次數,從而考慮文檔中某些單詞出現多次所包含的信息。
好了,講了關於文檔分類的特征描述之后,我們就可以開始編代碼,實現具體的文本分類了
1 拆分文本,准備數據
要從文本中獲取特征,顯然我們需要先拆分文本,這里的文本指的是來自文本的詞條,每個詞條是字符的任意組合。詞條可以為單詞,當然也可以是URL,IP地址或者其他任意字符串。將文本按照詞條進行拆分,根據詞條是否在詞匯列表中出現,將文檔組成成詞條向量,向量的每個值為1或者0,其中1表示出現,0表示未出現。
接下來,以在線社區的留言為例。對於每一條留言進行預測分類,類別兩種,侮辱性和非侮辱性,預測完成后,根據預測結果考慮屏蔽侮辱性言論,從而不影響社區發展。
詞表到向量的轉換函數
#---------------------------從文本中構建詞條向量------------------------- #1 要從文本中獲取特征,需要先拆分文本,這里特征是指來自文本的詞條,每個詞 #條是字符的任意組合。詞條可以理解為單詞,當然也可以是非單詞詞條,比如URL #IP地址或者其他任意字符串 # 將文本拆分成詞條向量后,將每一個文本片段表示為一個詞條向量,值為1表示出現 #在文檔中,值為0表示詞條未出現 #導入numpy from numpy import * def loadDataSet(): #詞條切分后的文檔集合,列表每一行代表一個文檔 postingList=[['my','dog','has','flea',\ 'problems','help','please'], ['maybe','not','take','him',\ 'to','dog','park','stupid'], ['my','dalmation','is','so','cute', 'I','love','him'], ['stop','posting','stupid','worthless','garbage'], ['my','licks','ate','my','steak','how',\ 'to','stop','him'], ['quit','buying','worthless','dog','food','stupid']] #由人工標注的每篇文檔的類標簽 classVec=[0,1,0,1,0,1] return postingList,classVec #統計所有文檔中出現的詞條列表 def createVocabList(dataSet): #新建一個存放詞條的集合 vocabSet=set([]) #遍歷文檔集合中的每一篇文檔 for document in dataSet: #將文檔列表轉為集合的形式,保證每個詞條的唯一性 #然后與vocabSet取並集,向vocabSet中添加沒有出現 #的新的詞條 vocabSet=vocabSet|set(document) #再將集合轉化為列表,便於接下來的處理 return list(vocabSet) #根據詞條列表中的詞條是否在文檔中出現(出現1,未出現0),將文檔轉化為詞條向量 def setOfWords2Vec(vocabSet,inputSet): #新建一個長度為vocabSet的列表,並且各維度元素初始化為0 returnVec=[0]*len(vocabSet) #遍歷文檔中的每一個詞條 for word in inputSet: #如果詞條在詞條列表中出現 if word in vocabSet: #通過列表獲取當前word的索引(下標) #將詞條向量中的對應下標的項由0改為1 returnVec[vocabSet.index(word)]=1 else: print('the word: %s is not in my vocabulary! '%'word') #返回inputet轉化后的詞條向量 return returnVec
需要說明的是,上面函數creatVocabList得到的是所有文檔中出現的詞匯列表,列表中沒有重復的單詞,每個詞是唯一的。
2 由詞向量計算朴素貝葉斯用到的概率值
這里,如果我們將之前的點(x,y)換成詞條向量w(各維度的值由特征是否出現的0或1組成),在這里詞條向量的維度和詞匯表長度相同。
p(ci|w)=p(w|ci)*p(ci)/p(w)
我們將使用該公式計算文檔詞條向量屬於各個類的概率,然后比較概率的大小,從而預測出分類結果。
具體地,首先,可以通過統計各個類別的文檔數目除以總得文檔數目,計算出相應的p(ci);然后,基於條件獨立性假設,將w展開為一個個的獨立特征,那么就可以將上述公式寫為p(w|ci)=p(w0|ci)*p(w1|ci)*...p(wN|ci),這樣就很容易計算,從而極大地簡化了計算過程。
函數的偽代碼為:
計算每個類別文檔的數目
計算每個類別占總文檔數目的比例
對每一篇文檔:
對每一個類別:
如果詞條出現在文檔中->增加該詞條的計數值#統計每個類別中出現的詞條的次數
增加所有詞條的計數值#統計每個類別的文檔中出現的詞條總數
對每個類別:
將各個詞條出現的次數除以類別中出現的總詞條數目得到條件概率
返回每個類別各個詞條的條件概率和每個類別所占的比例
代碼如下:
#訓練算法,從詞向量計算概率p(w0|ci)...及p(ci) #@trainMatrix:由每篇文檔的詞條向量組成的文檔矩陣 #@trainCategory:每篇文檔的類標簽組成的向量 def trainNB0(trainMatrix,trainCategory): #獲取文檔矩陣中文檔的數目 numTrainDocs=len(trainMatrix) #獲取詞條向量的長度 numWords=len(trainMatrix[0]) #所有文檔中屬於類1所占的比例p(c=1) pAbusive=sum(trainCategory)/float(numTrainDocs) #創建一個長度為詞條向量等長的列表 p0Num=zeros(numWords);p1Num=zeros(numWords) p0Denom=0.0;p1Denom=0.0 #遍歷每一篇文檔的詞條向量 for i in range(numTrainDocs): #如果該詞條向量對應的標簽為1 if trainCategory[i]==1: #統計所有類別為1的詞條向量中各個詞條出現的次數 p1Num+=trainMatrix[i] #統計類別為1的詞條向量中出現的所有詞條的總數 #即統計類1所有文檔中出現單詞的數目 p1Denom+=sum(trainMatrix[i]) else: #統計所有類別為0的詞條向量中各個詞條出現的次數 p0Num+=trainMatrix[i] #統計類別為0的詞條向量中出現的所有詞條的總數 #即統計類0所有文檔中出現單詞的數目 p0Denom+=sum(trainMatrix[i]) #利用NumPy數組計算p(wi|c1) p1Vect=p1Num/p1Denom #為避免下溢出問題,后面會改為log() #利用NumPy數組計算p(wi|c0) p0Vect=p0Num/p0Denom #為避免下溢出問題,后面會改為log() return p0Vect,p1Vect,pAbusive
3 針對算法的部分改進
1)計算概率時,需要計算多個概率的乘積以獲得文檔屬於某個類別的概率,即計算p(w0|ci)*p(w1|ci)*...p(wN|ci),然后當其中任意一項的值為0,那么最后的乘積也為0.為降低這種影響,采用拉普拉斯平滑,在分子上添加a(一般為1),分母上添加ka(k表示類別總數),即在這里將所有詞的出現數初始化為1,並將分母初始化為2*1=2
#p0Num=ones(numWords);p1Num=ones(numWords) #p0Denom=2.0;p1Denom=2.0
2)解決下溢出問題
正如上面所述,由於有太多很小的數相乘。計算概率時,由於大部分因子都非常小,最后相乘的結果四舍五入為0,造成下溢出或者得不到准確的結果,所以,我們可以對成績取自然對數,即求解對數似然概率。這樣,可以避免下溢出或者浮點數舍入導致的錯誤。同時采用自然對數處理不會有任何損失。
#p0Vect=log(p0Num/p0Denom);p1Vect=log(p1Num/p1Denom)
下面是朴素貝葉斯分類函數的代碼:
#朴素貝葉斯分類函數 #@vec2Classify:待測試分類的詞條向量 #@p0Vec:類別0所有文檔中各個詞條出現的頻數p(wi|c0) #@p0Vec:類別1所有文檔中各個詞條出現的頻數p(wi|c1) #@pClass1:類別為1的文檔占文檔總數比例 def classifyNB(vec2Classify,p0Vec,p1Vec,pClass1): #根據朴素貝葉斯分類函數分別計算待分類文檔屬於類1和類0的概率 p1=sum(vec2Classify*p1Vec)+log(pClass1) p0=sum(vec2Classify*p0Vec)+log(1.0-pClass1) if p1>p0: return 1 else: return 0 #分類測試整體函數 def testingNB(): #由數據集獲取文檔矩陣和類標簽向量 listOPosts,listClasses=loadDataSet() #統計所有文檔中出現的詞條,存入詞條列表 myVocabList=createVocabList(listOPosts) #創建新的列表 trainMat=[] for postinDoc in listOPosts: #將每篇文檔利用words2Vec函數轉為詞條向量,存入文檔矩陣中 trainMat.append(setOfWords2Vec(myVocabList,postinDoc))\ #將文檔矩陣和類標簽向量轉為NumPy的數組形式,方便接下來的概率計算 #調用訓練函數,得到相應概率值 p0V,p1V,pAb=trainNB0(array(trainMat),array(listClasses)) #測試文檔 testEntry=['love','my','dalmation'] #將測試文檔轉為詞條向量,並轉為NumPy數組的形式 thisDoc=array(setOfWords2Vec(myVocabList,testEntry)) #利用貝葉斯分類函數對測試文檔進行分類並打印 print(testEntry,'classified as:',classifyNB(thisDoc,p0V,p1V,pAb)) #第二個測試文檔 testEntry1=['stupid','garbage'] #同樣轉為詞條向量,並轉為NumPy數組的形式 thisDoc1=array(setOfWords2Vec(myVocabList,testEntry1)) print(testEntry1,'classified as:',classifyNB(thisDoc1,p0V,p1V,pAb))
這里需要補充一點,上面也提到了關於如何選取文檔特征的方法,上面用到的是詞集模型,即對於一篇文檔,將文檔中是否出現某一詞條作為特征,即特征只能為0不出現或者1出現;然后,一篇文檔中詞條的出現次數也可能具有重要的信息,於是我們可以采用詞袋模型,在詞袋向量中每個詞可以出現多次,這樣,在將文檔轉為向量時,每當遇到一個單詞時,它會增加詞向量中的對應值
具體將文檔轉為詞袋向量的代碼為:
def bagOfWords2VecMN(vocabList,inputSet): #詞袋向量 returnVec=[0]*len(vocabList) for word in inputSet: if word in vocabList: #某詞每出現一次,次數加1 returnVec[vocabList.index(word)]+=1 return returnVec
程序運行結果:
三,實例:朴素貝葉斯的另一個應用--過濾垃圾郵件
1 切分數據
對於一個文本字符串,可以使用python的split()方法對文本進行切割,比如字符串'hello, Mr.lee.',分割結果為['hell0,','Mr.lee.'] 這樣,標點符合也會被當成詞的一部分,因為此種切割方法是基於詞與詞之間的空格作為分隔符的
此時,我們可以使用正則表達式來切分句子,其中分割符是除單詞和數字之外的其他任意字符串,即
import re
re.compile('\\W*')
這樣就得到了一系列詞組成的詞表,但是里面的空字符串還是需要去掉,此時我們可以通過字符的長度,去掉長度等於0的字符。並且,由於我們是統計某一詞是否出現,不考慮其大小寫,所有還可以將所有詞轉為小寫字符,即lower(),相應的,轉為大寫字符為upper()
此外,需要注意的是,由於是URL,因而可能會出現en和py這樣的單詞。當對URL進行切分時,會得到很多的詞,因此在實現時也會過濾掉長度小於3的詞。當然,也可以根據自己的實際需要來增加相應的文本解析函數。
2 具體代碼如下:
#貝葉斯算法實例:過濾垃圾郵件 #處理數據長字符串 #1 對長字符串進行分割,分隔符為除單詞和數字之外的任意符號串 #2 將分割后的字符串中所有的大些字母變成小寫lower(),並且只 #保留單詞長度大於3的單詞 def testParse(bigString): import re listOfTokens=re.split(r'\W*',bigString) return [tok.lower() for tok in listOPosts if len(tok)>2] def spamTest(): #新建三個列表 docList=[];classList=[];fullTest=[] #i 由1到26 for i in range(1,26): #打開並讀取指定目錄下的本文中的長字符串,並進行處理返回 wordList=testParse(open('email/spam/%d.txt' %i).read()) #將得到的字符串列表添加到docList docList.append(wordList) #將字符串列表中的元素添加到fullTest fullTest.extend(wordList) #類列表添加標簽1 classList.append(1) #打開並取得另外一個類別為0的文件,然后進行處理 wordList=testParse(open('email/ham/&d.txt' %i).read()) docList.append(wordList) fullTest.extend(wordList) classList.append(0) #將所有郵件中出現的字符串構建成字符串列表 vocabList=createVocabList(docList) #構建一個大小為50的整數列表和一個空列表 trainingSet=range(50);testSet=[] #隨機選取1~50中的10個數,作為索引,構建測試集 for i in range(10): #隨機選取1~50中的一個整型數 randIndex=int(random.uniform(0,len(trainingSet))) #將選出的數的列表索引值添加到testSet列表中 testSet.append(trainingSet[randIndex]) #從整數列表中刪除選出的數,防止下次再次選出 #同時將剩下的作為訓練集 del(trainingSet[randIndex]) #新建兩個列表 trainMat=[];trainClasses=[] #遍歷訓練集中的嗎每個字符串列表 for docIndex in trainingSet: #將字符串列表轉為詞條向量,然后添加到訓練矩陣中 trainMat.append(setOfWords2Vec(vocabList,fullTest[docIndex])) #將該郵件的類標簽存入訓練類標簽列表中 trainClasses.append(classList[docIndex]) #計算貝葉斯函數需要的概率值並返回 p0V,p1V,pSpam=trainNB0(array(trainMat),array(trainClasses)) errorCount=0 #遍歷測試集中的字符串列表 for docIndex in testSet: #同樣將測試集中的字符串列表轉為詞條向量 wordVector=setOfWords2Vec(vocabList,docList[docIndex]) #對測試集中字符串向量進行預測分類,分類結果不等於實際結果 if classifyNB(array(wordVector),p0V,p1V,pSpam)!=classList[docIndex]: errorCount+=1 print('the error rate is:',float(errorCount)/len(testSet))
代碼中,采用隨機選擇的方法從數據集中選擇訓練集,剩余的作為測試集。這種方法的好處是,可以進行多次隨機選擇,得到不同的訓練集和測試集,從而得到多次不同的錯誤率,我們可以通過多次的迭代,求取平均錯誤率,這樣就能得到更准確的錯誤率。這種方法稱為留存交叉驗證
四,實例:朴素貝葉斯從個人廣告中獲取區域傾向
在本例中,我們通過從不同的城市的RSS源中獲得的同類型廣告信息,比較兩個城市人們在廣告用詞上是否不同。如果不同,那么各自的常用詞是哪些?而從人們的用詞當中,我們能否對不同城市的人所關心的內容有所了解?如果能得到這些信息,分析過后,相信對於廣告商而言具有不小的幫助。
1 利用RSS源得到文本數據,Universal Feed Parser是Python中最常見的RSS程序庫。通過如下語句可以獲得相應的文本數據
import feedparser ny=feedparser.parse('http://newyork.craigslist.org/stp/index.rss') ny['entries']
len(ny['entries'])
2 獲取並統計相關數據:
#實例:使用朴素貝葉斯分類器從個人廣告中獲取區域傾向 #RSS源分類器及高頻詞去除函數 def calMostFreq(vocabList,fullTest): #導入操作符 import operator #創建新的字典 freqDict={} #遍歷詞條列表中的每一個詞 for token in vocabList: #將單詞/單詞出現的次數作為鍵值對存入字典 freqDict[token]=fullTest.count(token) #按照鍵值value(詞條出現的次數)對字典進行排序,由大到小 sortedFreq=sorted(freqDict.items(),keys=operator.itemgetter(1),reverse=true) #返回出現次數最多的前30個單詞 return sortedFreq[:30] def localWords(feed1,feed0): import feedparser #新建三個列表 docList=[];classList=[];fullTest=[] #獲取條目較少的RSS源的條目數 minLen=min(len(feed1['entries']),len(feed0['entries'])) #遍歷每一個條目 for i in range(minLen): #解析和處理獲取的相應數據 wordList=textParse(feed1['entries'][i]['summary']) #添加詞條列表到docList docList.append(wordList) #添加詞條元素到fullTest fullTest.extend(wordList) #類標簽列表添加類1 classList.append(1) #同上 wordList=testParse(feed0['entries'][i]['summary']) docList.append(wordList) fullTest.extend(wordList) #此時添加類標簽0 classList.append(0) #構建出現的所有詞條列表 vocabList=createVocabList(docList) #找到出現的單詞中頻率最高的30個單詞 top30Words=calMostFreq(vocabList,fullTest) #遍歷每一個高頻詞,並將其在詞條列表中移除 #這里移除高頻詞后錯誤率下降,如果繼續移除結構上的輔助詞 #錯誤率很可能會繼續下降 for pairW in top30Words: if pairW[0] in vocabList: vocabList.remove(pairW[0]) #下面內容與函數spaTest完全相同 trainingSet=range(2*minLen);testSet=[] for i in range(20): randIndex=int(random.uniform(0,len(trainingSet))) testSet.append(trainingSet[randIndex]) del(trainingSet[randIndex]) trainMat=[];trainClasses=[] for docIndex in trainingSet: trainMat.append(bagOfWords2VecMN(vocabList,docList[docIndex])) trainClasses.append(classList[docIndex]) p0V,p1V,pSpam=trainNB0(array(trainMat),array(trainClasses)) errorCount=0 for docIndex in testSet: wordVector=bagOfWords2VecMN(vocabList,docList[docIndex]) if classifyNB(array(wordVector),p0V,p1V,pSpam)!=classList[docIndex]: errorCount+=1 print('the error rate is:',float(errorCount)/len(testSet)) return vocabList,p0V,p1V
需要說明的是,這里用到了將出現次數最多的30個單詞刪除的方法,結果發現去掉了這些最常出現的高頻詞后,錯誤率大幅度上升,這表明了文本中的小部分高頻單詞占據了文本中絕大部分的用詞。產生這種情況的原因是因為語言中大部分是冗余和結果輔助性內容。所以,我們不僅可以嘗試移除高頻詞的方法,還可以進一步從某個預定詞表(停用詞表)中移除結構上的輔助詞,通過移除輔助性詞,分類錯誤率會所有下降
此外,為了得到錯誤率的精確估計,應進行多次上述實驗,從而得到錯誤率平均值。
3 對得到的數據進行分析:
得到各個用詞的概率之后,我們可以設定相應的閾值,保留大於相應閾值的用詞及其出現的概率,然后按照概率的大小進行排序;最后返回兩個地域最具表征性的詞匯,觀察和比較他們的異同。
代碼如下:
#最具表征性的詞匯顯示函數 def getTopWords(ny,sf): import operator #利用RSS源分類器獲取所有出現的詞條列表,以及每個分類中每個單詞出現的概率 vocabList,p0V,p1V=localWords(ny,sf) #創建兩個元組列表 topNY=[];topSF=[] #遍歷每個類中各個單詞的概率值 for i in range(len(p0V)): #往相應元組列表中添加概率值大於閾值的單詞及其概率值組成的二元列表 if(p0V[i]>-6.0):topSF.append((vocabList[i],p0V[i])) if(p1V[i]>-6.0):topNY.append((vocabList[i],p1V[i])) 對列表按照每個二元列表中的概率值項進行排序,排序規則由大到小 sortedSF=sorted(topSF,key=lambda pair:pair[1],reverse=true) print('SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**') #遍歷列表中的每一個二元條目列表 for item in sortedSF: #打印每個二元列表中的單詞字符串元素 print(item[0]) #解析同上 sortedNY=sorted(topNY,key=lambda pair:pair[1],reverse=true) print('SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**') for item in sortedNY: print(item[0])
盡管朴素貝葉斯的條件獨立性假設存在一定的問題,但是朴素貝葉斯算法仍然能取得比較理想的分類預測結果。
此外,朴素貝葉斯在數據較少的情況下仍然適用,雖然例子中為兩類類別的分析,但是朴素貝葉斯可以處理多分類的情況;朴素貝葉斯的一個不足的地方是,對輸入的數據有一定的要求,需要花費一定的時間進行數據的處理和解析。朴素貝葉斯中用來計算的數據為標稱型數據,我們需要將字符串特征轉化為相應的離散值,用於后續的統計和計算。