樸素貝葉斯
1 介紹
樸素貝葉斯是一種構建分類器的簡單方法。該分類器模型會給問題實例分配用特徵值表示的類標籤,類標籤取自有限集合。它不是訓練這種分類器的單一算法,而是一系列基於相同原理的算法:所有樸素貝葉斯分類器都假定樣本每個特徵與其他特徵都不相關。 舉個例子,如果一種水果其具有紅,圓,直徑大概3英寸等特徵,該水果可以被判定爲是蘋果。儘管這些特徵相互依賴或者有些特徵由其他特徵決定,然而樸素貝葉斯分類器認爲這些屬性在判定該水果是否爲蘋果的概率分佈上獨立的。
對於某些類型的概率模型,在有監督學習的樣本集中能獲取得非常好的分類效果。在許多實際應用中,樸素貝葉斯模型參數估計使用最大似然估計方法;換言之,在不用貝葉斯概率或者任何貝葉斯模型的情況下,樸素貝葉斯模型也能奏效。
儘管是帶着這些樸素思想和過於簡單化的假設,但樸素貝葉斯分類器在很多複雜的現實情形中仍能夠取得相當好的效果。儘管如此,有論文證明更新的方法(如提升樹和隨機森林)的性能超過了貝葉斯分類器。
樸素貝葉斯分類器的一個優勢在於只需要根據少量的訓練數據估計出必要的參數(變量的均值和方差)。由於變量獨立假設,只需要估計各個變量,而不需要確定整個協方差矩陣。
1.1 樸素貝葉斯的優缺點
-
優點:學習和預測的效率高,且易於實現;在數據較少的情況下仍然有效,可以處理多分類問題。
-
缺點:分類效果不一定很高,特徵獨立性假設會是樸素貝葉斯變得簡單,但是會犧牲一定的分類準確率。
2 樸素貝葉斯概率模型
理論上,概率模型分類器是一個條件概率模型。
獨立的類別變量C
有若干類別,條件依賴於若干特徵變量F_1,F_2,...,F_n
。但問題在於如果特徵數量n
較大或者每個特徵能取大量值時,基於概率模型列出概率表變得不現實。所以我們修改這個模型使之變得可行。
貝葉斯定理有以下式子:
實際中,我們只關心分式中的分子部分,因爲分母不依賴於C
而且特徵F_i
的值是給定的,於是分母可以認爲是一個常數。這樣分子就等價於聯合分佈模型。
重複使用鏈式法則,可將該式寫成條件概率的形式,如下所示:
現在“樸素”的條件獨立假設開始發揮作用:假設每個特徵F_i
對於其他特徵F_j
是條件獨立的。這就意味着
所以聯合分佈模型可以表達爲
這意味着上述假設下,類變量C
的條件分佈可以表達爲:
其中Z
是一個只依賴與F_1,...,F_n
等的縮放因子,當特徵變量的值已知時是一個常數。
從概率模型中構造分類器
討論至此爲止我們導出了獨立分佈特徵模型,也就是樸素貝葉斯概率模型。樸素貝葉斯分類器包括了這種模型和相應的決策規則。一個普通的規則就是選出最有可能的那個:這就是大家熟知的最大後驗概率(MAP
)決策準則。相應的分類器便是如下定義的公式:
3 參數估計
所有的模型參數都可以通過訓練集的相關頻率來估計。常用方法是概率的最大似然估計。類的先驗概率P(C)
可以通過假設各類等概率來計算(先驗概率
= 1 / (類的數量))
,或者通過訓練集的各類樣本出現的次數來估計(A類先驗概率=(A類樣本的數量)/(樣本總數))
。
對於類條件概率P(X|c)
來說,直接根據樣本出現的頻率來估計會很困難。在現實應用中樣本空間的取值往往遠遠大於訓練樣本數,也就是說,很多樣本取值在訓練集中根本沒有出現,直接使用頻率來估計P(x|c)
不可行,因爲"未被觀察到"和"出現概率爲零"是不同的。
爲了估計特徵的分佈參數,我們要先假設訓練集數據滿足某種分佈或者非參數模型。
這種假設稱爲樸素貝葉斯分類器的事件模型(event model
)。對於離散的特徵數據(例如文本分類中使用的特徵),多元分佈和伯努利分佈比較流行。
3.1 高斯樸素貝葉斯
如果要處理的是連續數據,一種通常的假設是這些連續數值服從高斯分佈。例如,假設訓練集中有一個連續屬性x
。我們首先對數據根據類別分類,然後計算每個類別中x
的均值和方差。令mu_c
表示爲x
在c
類上的均值,令sigma^2_c
爲x
在c
類上的方差。在給定類中某個值的概率 P(x=v|c)
,可以通過將v
表示爲均值爲mu_c
,方差爲sigma^2_c
的正態分佈計算出來。
處理連續數值問題的另一種常用的技術是通過離散化連續數值的方法。通常,當訓練樣本數量較少或者是精確的分佈已知時,通過概率分佈的方法是一種更好的選擇。 在大量樣本的情形下離散化的方法表現更優,因爲大量的樣本可以學習到數據的分佈。由於樸素貝葉斯是一種典型的用到大量樣本的方法(越大計算量的模型可以產生越高的分類精確度),所以樸素貝葉斯方法都用到離散化方法,而不是概率分佈估計的方法。
3.2 多元樸素貝葉斯
在多元事件模型中,樣本(特徵向量)表示特定事件發生的次數。用p_i
表示事件i
發生的概率。特徵向量X=(x_1,x_2,...,x_n)
是一個histogram
,其中x_i
表示事件i
在特定的對象中被觀察到的次數。事件模型通常用於文本分類。相應的x_i
表示詞i
在單個文檔中出現的次數。 X
的似然函數如下所示:
當用對數空間表達時,多元樸素貝葉斯分類器變成了線性分類器。
如果一個給定的類和特徵值在訓練集中沒有一起出現過,那麼基於頻率的估計下該概率將爲0。這將是一個問題。因爲與其他概率相乘時將會把其他概率的信息統統去除。所以常常要求要對每個小類樣本的概率估計進行修正,以保證不會出現有爲0的概率出現。常用到的平滑就是加1平滑(也稱拉普拉斯平滑)。
根據參考文獻【2】,我們以文本分類的訓練和測試爲例子來介紹多元樸素貝葉斯的訓練和測試過程。如下圖所示。
這裏的CondProb[t][c]
即上文中的P(x|C)
。T_ct
表示類別爲c
的文檔中t
出現的次數。+1
就是平滑手段。
3.3 伯努利樸素貝葉斯
在多變量伯努利事件模型中,特徵是獨立的二值變量。和多元模型一樣,這個模型在文本分類中也非常流行。它的似然函數如下所示。
其中p_ki
表示類別C_k
生成term
w_i
的概率。這個模型通常用於短文本分類。
根據參考文獻【2】,我們以文本分類的訓練和測試爲例子來介紹多元樸素貝葉斯的訓練和測試過程。如下圖所示。
4 源碼分析
MLlib
中實現了多元樸素貝葉斯和伯努利樸素貝葉斯。下面先看看樸素貝葉斯的使用實例。
4.1 實例
import org.apache.spark.mllib.classification.{NaiveBayes, NaiveBayesModel}
import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.mllib.regression.LabeledPoint
//讀取並處理數據
val data = sc.textFile("data/mllib/sample_naive_bayes_data.txt")
val parsedData = data.map { line =>
val parts = line.split(',')
LabeledPoint(parts(0).toDouble, Vectors.dense(parts(1).split(' ').map(_.toDouble)))
}
// 切分數據爲訓練數據和測試數據
val splits = parsedData.randomSplit(Array(0.6, 0.4), seed = 11L)
val training = splits(0)
val test = splits(1)
//訓練模型
val model = NaiveBayes.train(training, lambda = 1.0, modelType = "multinomial")
//測試數據
val predictionAndLabel = test.map(p => (model.predict(p.features), p.label))
val accuracy = 1.0 * predictionAndLabel.filter(x => x._1 == x._2).count() / test.count()
4.2 訓練模型
從上文的原理分析我們可以知道,樸素貝葉斯模型的訓練過程就是獲取概率p(C)
和p(F|C)
的過程。根據MLlib
的源碼,我們可以將訓練過程分爲兩步。
第一步是聚合計算每個標籤對應的term
的頻率,第二步是迭代計算p(C)
和p(F|C)
。
- 1 計算每個標籤對應的
term
的頻率
val aggregated = data.map(p => (p.label, p.features)).combineByKey[(Long, DenseVector)](
createCombiner = (v: Vector) => {
if (modelType == Bernoulli) {
requireZeroOneBernoulliValues(v)
} else {
requireNonnegativeValues(v)
}
(1L, v.copy.toDense)
},
mergeValue = (c: (Long, DenseVector), v: Vector) => {
requireNonnegativeValues(v)
//c._2 = v*1 + c._2
BLAS.axpy(1.0, v, c._2)
(c._1 + 1L, c._2)
},
mergeCombiners = (c1: (Long, DenseVector), c2: (Long, DenseVector)) => {
BLAS.axpy(1.0, c2._2, c1._2)
(c1._1 + c2._1, c1._2)
}
//根據標籤進行排序
).collect().sortBy(_._1)
這裏我們需要先了解createCombiner
函數的作用。createCombiner
的作用是將原RDD
中的Vector
類型轉換爲(long,Vector)
類型。
如果modelType
爲Bernoulli
,那麼v
中包含的值只能爲0或者1。如果modelType
爲multinomial
,那麼v
中包含的值必須大於0。
//值非負
val requireNonnegativeValues: Vector => Unit = (v: Vector) => {
val values = v match {
case sv: SparseVector => sv.values
case dv: DenseVector => dv.values
}
if (!values.forall(_ >= 0.0)) {
throw new SparkException(s"Naive Bayes requires nonnegative feature values but found $v.")
}
}
//值爲0或者1
val requireZeroOneBernoulliValues: Vector => Unit = (v: Vector) => {
val values = v match {
case sv: SparseVector => sv.values
case dv: DenseVector => dv.values
}
if (!values.forall(v => v == 0.0 || v == 1.0)) {
throw new SparkException(
s"Bernoulli naive Bayes requires 0 or 1 feature values but found $v.")
}
}
mergeValue
函數的作用是將新來的Vector
累加到已有向量中,並更新詞率。mergeCombiners
則是合併不同分區的(long,Vector)
數據。
通過這個函數,我們就找到了每個標籤對應的詞頻率,並得到了標籤對應的所有文檔的累加向量。
- 2 迭代計算
p(C)
和p(F|C)
//標籤數
val numLabels = aggregated.length
//文檔數
var numDocuments = 0L
aggregated.foreach { case (_, (n, _)) =>
numDocuments += n
}
//特徵維數
val numFeatures = aggregated.head match { case (_, (_, v)) => v.size }
val labels = new Array[Double](numLabels)
//表示logP(C)
val pi = new Array[Double](numLabels)
//表示logP(F|C)
val theta = Array.fill(numLabels)(new Array[Double](numFeatures))
val piLogDenom = math.log(numDocuments + numLabels * lambda)
var i = 0
aggregated.foreach { case (label, (n, sumTermFreqs)) =>
labels(i) = label
//訓練步驟的第5步
pi(i) = math.log(n + lambda) - piLogDenom
val thetaLogDenom = modelType match {
case Multinomial => math.log(sumTermFreqs.values.sum + numFeatures * lambda)
case Bernoulli => math.log(n + 2.0 * lambda)
case _ =>
// This should never happen.
throw new UnknownError(s"Invalid modelType: $modelType.")
}
//訓練步驟的第6步
var j = 0
while (j < numFeatures) {
theta(i)(j) = math.log(sumTermFreqs(j) + lambda) - thetaLogDenom
j += 1
}
i += 1
}
這段代碼計算上文提到的p(C)
和p(F|C)
。這裏的lambda
表示平滑因子,一般情況下,我們將它設置爲1。代碼中,p(c_i)=log
(n+lambda)/(numDocs+numLabels*lambda)
,這對應上文訓練過程的第5步prior(c)=N_c/N
。
根據modelType
類型的不同,p(F|C)
的實現則不同。當modelType
爲Multinomial
時,P(F|C)=T_ct/sum(T_ct)
,這裏sum(T_ct)=sumTermFreqs.values.sum
+ numFeatures * lambda
。這對應多元樸素貝葉斯訓練過程的第10步。 當modelType
爲Bernoulli
時,P(F|C)=(N_ct+lambda)/(N_c+2*lambda)
。這對應伯努利貝葉斯訓練算法的第8行。
需要注意的是,代碼中的所有計算都是取對數計算的。
4.3 預測數據
override def predict(testData: Vector): Double = {
modelType match {
case Multinomial =>
labels(multinomialCalculation(testData).argmax)
case Bernoulli =>
labels(bernoulliCalculation(testData).argmax)
}
}
預測也是根據modelType
的不同作不同的處理。當modelType
爲Multinomial
時,調用multinomialCalculation
函數。
private def multinomialCalculation(testData: Vector) = {
val prob = thetaMatrix.multiply(testData)
BLAS.axpy(1.0, piVector, prob)
prob
}
這裏的thetaMatrix
和piVector
即上文中訓練得到的P(F|C)
和P(C)
,根據P(C|F)=P(F|C)*P(C)
即可以得到預測數據歸屬於某類別的概率。
注意,這些概率都是基於對數結果計算的。
當modelType
爲Bernoulli
時,實現代碼略有不同。
private def bernoulliCalculation(testData: Vector) = {
testData.foreachActive((_, value) =>
if (value != 0.0 && value != 1.0) {
throw new SparkException(
s"Bernoulli naive Bayes requires 0 or 1 feature values but found $testData.")
}
)
val prob = thetaMinusNegTheta.get.multiply(testData)
BLAS.axpy(1.0, piVector, prob)
BLAS.axpy(1.0, negThetaSum.get, prob)
prob
}
當詞在訓練數據中出現與否處理的過程不同。伯努利模型測試過程中,如果詞存在,需要計算log(condprob)
,否在需要計算log(1-condprob)
,condprob
爲P(f|c)=exp(theta)
。所以預先計算log(1-exp(theta))
以及它的和可以應用到預測過程。這裏thetaMatrix
表示logP(F|C)
,negTheta
代表log(1-exp(theta))=log(1-condprob)
,thetaMinusNegTheta
代表log(theta
- log(1-exp(theta)))
。
private val (thetaMinusNegTheta, negThetaSum) = modelType match {
case Multinomial => (None, None)
case Bernoulli =>
val negTheta = thetaMatrix.map(value => math.log(1.0 - math.exp(value)))
val ones = new DenseVector(Array.fill(thetaMatrix.numCols){1.0})
val thetaMinusNegTheta = thetaMatrix.map { value =>
value - math.log(1.0 - math.exp(value))
}
(Option(thetaMinusNegTheta), Option(negTheta.multiply(ones)))
case _ =>
// This should never happen.
throw new UnknownError(s"Invalid modelType: $modelType.")
}
這裏math.exp(value)
將對數概率恢復成真實的概率。
參考文獻
【1】樸素貝葉斯分類器
【2】Naive Bayes text classification
轉自:https://github.com/endymecy/spark-ml-source-analysis/blob/master/%E5%88%86%E7%B1%BB%E5%92%8C%E5%9B%9E%E5%BD%92/%E6%9C%B4%E7%B4%A0%E8%B4%9D%E5%8F%B6%E6%96%AF/nb.md