1. 從朴素貝葉斯在醫療診斷中的迷思說起
這個模型最早被應用於醫療診斷,其中,類變量的不同值用於表示患者可能患的不同疾病。證據變量用於表示不同症狀、化驗結果等。在簡單的疾病診斷上,朴素貝葉斯模型確實發揮了很好的作用,甚至比人類專家的診斷結果都要好。但是在更深度的應用中,醫生發現,對於更復雜(由多種致病原因和症狀共同表現)的疾病,模型表現的並不好。
數據科學家經過分析認為,出現這種現象的原因在於:模型做了集中通常並不真實的強假設,例如:
- 一個患者至多可能患一種疾病
- 在已知患者的疾病條件下,不同症狀的出現與否,不同化驗結果,之間是互相獨立的
這種模型可用於醫學診斷是因為少量可解釋的參數易於由專家獲得,早期的機器輔助醫療診斷系統正式建立在這一技術之上。
但是,之后更加深入的實踐表明,構建這種模型的強假設降低了模型診斷的准確性,尤其是“過度計算”某些特定的證據,該模型很容易過高估計某些方面特征的影響。
例如,“高血壓”和“肥胖症”是心臟病的兩個硬指標,但是,這兩個症狀之間相關度很高,高血壓一般就伴隨着肥胖症。在使用朴素貝葉斯公式計算的時候,由於乘法項的緣故,關於這方面的證據因子就會被重復計算,如下式:
P(心臟病 | 高血壓,肥胖症) = P(高血壓 | 心臟病) * P(高血壓 | 肥胖症) / P(高血壓,肥胖症)
由於“高血壓”和“肥胖症”之間存在較強相關性的緣故,我們可以很容易想象,分子乘積增加的比率是大於分母聯合分布增加的比率的。因此,當分子項繼續增加的時候,最終的后驗概率就會不斷增大。但是因為新增的特征項並沒有提供新的信息,后驗概率的這種增大變化反而降低了模型的預測性能。
實際上,在實踐中人們發現,朴素貝葉斯模型的診斷性能會隨着特征的增加而降低,這種降低常常歸因於違背了強條件獨立性假設。
筆者將這種現象稱之為“過度特征化(over-featuring)”,這是工程中常見的一種現象,過度特征化如果無法得到有效規避,會顯著降低模型的泛化和預測性能。在這篇文章中,我們通過實驗和分析來論證這個說法。
2. 用鳶尾花分類例子討論特征工程問題
0x1:數據集觀察

- 花萼長度
- 花萼寬度
- 花瓣長度
- 花瓣寬度
可以通過這4個特征預測鳶尾花卉屬於(iris-setosa, iris-versicolour, iris-virginica)中的哪一品種。
0x2:欠特征化(under-featuring)
我們先來討論欠特征化(under-featuring)的情況,我們的數據集中有4個維度的特征,並且這4個特征和目標target的相關度都是很高的,換句話說這4個特征都是富含信息量的特征:
# -*- coding: utf-8 -*- from sklearn.naive_bayes import GaussianNB import numpy as np from sklearn.datasets import load_iris from sklearn.metrics import accuracy_score from sklearn.metrics import confusion_matrix import numpy from sklearn.utils import shuffleif __name__ == '__main__': # naive Bayes muNB = GaussianNB() # load data iris = load_iris() print "np.shape(iris.data): ", np.shape(iris.data) # feature vec X_train = iris.data[:int(len(iris.data)*0.8)] X_test = iris.data[int(len(iris.data)*0.8):] # label Y_train = iris.target[:int(len(iris.data)*0.8)] Y_test = iris.target[int(len(iris.data)*0.8):] # shuffle X_train, Y_train = shuffle(X_train, Y_train) X_test, Y_test = shuffle(X_test, Y_test) # load origin feature X_train_vec = X_train[:, :4] X_test_vec = X_test[:, :4] print "Pearson Relevance X[0]: ", numpy.corrcoef(np.array([i[0] for i in X_train_vec[:, 0:1]]), Y_train)[0, 1] print "Pearson Relevance X[1]: ", numpy.corrcoef(np.array([i[0] for i in X_train_vec[:, 1:2]]), Y_train)[0, 1] print "Pearson Relevance X[2]: ", numpy.corrcoef(np.array([i[0] for i in X_train_vec[:, 2:3]]), Y_train)[0, 1] print "Pearson Relevance X[3]: ", numpy.corrcoef(np.array([i[0] for i in X_train_vec[:, 3:4]]), Y_train)[0, 1]
4個特征的皮爾森相關度都超過了0.5
現在我們分別嘗試只使用1個、2個、3個、4個特征情況下,訓練得到的朴素貝葉斯模型的泛化和預測性能:
# -*- coding: utf-8 -*- from sklearn.naive_bayes import GaussianNB import numpy as np from sklearn.datasets import load_iris from sklearn.metrics import accuracy_score from sklearn.metrics import confusion_matrix import numpy from sklearn.utils import shuffle def model_tain_and_test(feature_cn): # load origin feature X_train_vec = X_train[:, :feature_cn] X_test_vec = X_test[:, :feature_cn] # train model muNB.fit(X_train_vec, Y_train) # predidct the test data y_predict = muNB.predict(X_test_vec) print "feature_cn: ", feature_cn print 'accuracy is: {0}'.format(accuracy_score(Y_test, y_predict)) print 'error is: {0}'.format(confusion_matrix(Y_test, y_predict)) print ' ' if __name__ == '__main__': # naive Bayes muNB = GaussianNB() # load data iris = load_iris() print "np.shape(iris.data): ", np.shape(iris.data) # feature vec X_train = iris.data[:int(len(iris.data)*0.8)] X_test = iris.data[int(len(iris.data)*0.8):] # label Y_train = iris.target[:int(len(iris.data)*0.8)] Y_test = iris.target[int(len(iris.data)*0.8):] # shuffle X_train, Y_train = shuffle(X_train, Y_train) X_test, Y_test = shuffle(X_test, Y_test) # train and test the generalization and prediction model_tain_and_test(1) model_tain_and_test(2) model_tain_and_test(3) model_tain_and_test(4)
可以看到,只使用1個特征的時候,在測試集上的預測精確度只有33.3%,隨着特征數的增加,測試集上的預測精確度逐漸增加。
用貝葉斯網的角度來看朴素貝葉斯模型,有如下結構圖,
Xi節點這里相當於特征,網絡中每個Xi節點的增加,都會改變對Class結果的概率推理,Xi越多,推理的准確度也就越高。
從信息論的角度也很好理解,我們可以將P(Class | Xi)看成是條件熵的信息傳遞過程,我們提供的信息越多,原則上,對Class的不確定性就會越低。
至此,我們得出如下結論:
特征工程過程中需要特別關注描述完整性問題(description integrity problem),特征維度沒有完整的情況下,提供再多的數據對模型效果都沒有實質的幫助。樣本集的概率完整性要從“特征完整性”和“數據完整性”兩個方面保證,它們二者歸根結底還是信息完整性的本質問題。
0x3:過特征化(over-featuring)
現在我們在原始的4個特征維度上,繼續增加新的無用特征,即那種和目標target相關度很低的特征。
# -*- coding: utf-8 -*- from sklearn.naive_bayes import GaussianNB import numpy as np from sklearn.datasets import load_iris from sklearn.metrics import accuracy_score from sklearn.metrics import confusion_matrix import numpy from sklearn.utils import shuffle import random def feature_expend(feature_vec): # colum_1 * colum_2 feature_vec = np.hstack((feature_vec, np.array([[i] for i in np.multiply(feature_vec[:, 0], feature_vec[:, 1])]))) # random from colum_1 feature_vec = np.hstack((feature_vec, np.array([[random.uniform(.0, i)] for i in feature_vec[:, 0]]))) feature_vec = np.hstack((feature_vec, np.array([[random.uniform(.0, i)] for i in feature_vec[:, 0]]))) feature_vec = np.hstack((feature_vec, np.array([[random.uniform(.0, i)] for i in feature_vec[:, 0]]))) feature_vec = np.hstack((feature_vec, np.array([[random.uniform(.0, i)] for i in feature_vec[:, 0]]))) # random from colum_2 feature_vec = np.hstack((feature_vec, np.array([[random.uniform(.0, i)] for i in feature_vec[:, 1]]))) feature_vec = np.hstack((feature_vec, np.array([[random.uniform(.0, i)] for i in feature_vec[:, 1]]))) feature_vec = np.hstack((feature_vec, np.array([[random.uniform(.0, i)] for i in feature_vec[:, 1]]))) feature_vec = np.hstack((feature_vec, np.array([[random.uniform(.0, i)] for i in feature_vec[:, 1]]))) return feature_vec def model_tain_and_test(X_train, X_test, Y_train, Y_test, feature_cn): # load origin feature X_train_vec = X_train[:, :feature_cn] X_test_vec = X_test[:, :feature_cn] # train model muNB.fit(X_train_vec, Y_train) # predidct the test data y_predict = muNB.predict(X_test_vec) print "feature_cn: ", feature_cn print 'accuracy is: {0}'.format(accuracy_score(Y_test, y_predict)) print 'error is: {0}'.format(confusion_matrix(Y_test, y_predict)) print ' ' if __name__ == '__main__': # naive Bayes muNB = GaussianNB() # load data iris = load_iris() print "np.shape(iris.data): ", np.shape(iris.data) # feature vec X_train = iris.data[:int(len(iris.data)*0.8)] X_test = iris.data[int(len(iris.data)*0.8):] # label Y_train = iris.target[:int(len(iris.data)*0.8)] Y_test = iris.target[int(len(iris.data)*0.8):] # shuffle X_train, Y_train = shuffle(X_train, Y_train) X_test, Y_test = shuffle(X_test, Y_test) # expend feature X_train = feature_expend(X_train) X_test = feature_expend(X_test) print "X_test: ", X_test # show Pearson Relevance for i in range(len(X_train[0])): print "Pearson Relevance X[{0}]: ".format(i), numpy.corrcoef(np.array([i[0] for i in X_train[:, i:i+1]]), Y_train)[0, 1] model_tain_and_test(X_train, X_test, Y_train, Y_test, len(X_train[0]))
我們用random函數模擬了一個無用的新特征,可以看到,無用的特征對模型不但沒有幫助,反而降低了模型的性能。
至此,我們得出如下結論:
特征不是越多越多,機器學習不是洗衣機,一股腦將所有特征都丟進去,然后雙手合十,指望着模型能施展魔法,自動篩選出有用的好特征,當然,dropout/正則化這些手段確實有助於提高模型性能,它們的工作本質也是通過去除一些特征,從而緩解垃圾特征對模型帶來的影響。
當然,未來也許會發展出autoFeature的工程技術,但是作為數據科學工作者,我們自己必須要理解特征工程的意義。
0x4:特征加工對模型性能的影響
所謂的“特征加工”,具體來說就是對原始的特征進行線性變換(拉伸和旋轉),得到新的特征,例如:
- X_i * X_j
- X_i ^ 2
- X_i / X_j
- X_i + X_j
- X_i - X_j
本質上來說,我們可以將深度神經網絡的隱層看做是一種特征加工操作,稍有不同的是,深度神經網絡中激活函數充當了非線性扭曲的作用,不過其本質思想還是不變的。
那接下來問題是,特征加工對模型的性能有沒有影響呢?
准確的回答是,特征加工對模型的影響取決於新增特征的相關度,以及壞特征在所有特征中的占比。
我們來通過幾個實驗解釋上面這句話,下面筆者先通過模擬出幾個典型場景,最終給出總結結論:
1. 新增的特征和目標target相關度很低,同時該壞特征的占比還很高
# -*- coding: utf-8 -*- from sklearn.naive_bayes import GaussianNB import numpy as np from sklearn.datasets import load_iris from sklearn.metrics import accuracy_score from sklearn.metrics import confusion_matrix import numpy from sklearn.utils import shuffle def feature_expend(feature_vec): # colum_1 * colum_2 feature_vec = np.hstack((feature_vec, np.array([[i] for i in np.multiply(feature_vec[:, 0], feature_vec[:, 1])]))) # colum_1 / colum_2 # feature_vec = np.hstack((feature_vec, np.array([[i] for i in np.divide(feature_vec[:, 0], feature_vec[:, 1])]))) # colum_3 * colum_4 # feature_vec = np.hstack((feature_vec, np.array([[i] for i in np.multiply(feature_vec[:, 2], feature_vec[:, 3])]))) # colum_4 * colum_1 # feature_vec = np.hstack((feature_vec, np.array([[i] for i in np.multiply(feature_vec[:, 3], feature_vec[:, 0])]))) # colum_1 ^ 2 # feature_vec = np.hstack((feature_vec, np.array([[i] for i in np.multiply(feature_vec[:, 0], feature_vec[:, 0])]))) # colum_2 ^ 2 # feature_vec = np.hstack((feature_vec, np.array([[i] for i in np.multiply(feature_vec[:, 1], feature_vec[:, 1])]))) # colum_3 ^ 2 # feature_vec = np.hstack((feature_vec, np.array([[i] for i in np.multiply(feature_vec[:, 2], feature_vec[:, 2])]))) # colum_4 ^ 2 # feature_vec = np.hstack((feature_vec, np.array([[i] for i in np.multiply(feature_vec[:, 3], feature_vec[:, 3])]))) return feature_vec def model_tain_and_test(X_train, X_test, Y_train, Y_test, feature_cn): # load origin feature X_train_vec = X_train[:, :feature_cn] X_test_vec = X_test[:, :feature_cn] # train model muNB.fit(X_train_vec, Y_train) # predidct the test data y_predict = muNB.predict(X_test_vec) print "feature_cn: ", feature_cn print 'accuracy is: {0}'.format(accuracy_score(Y_test, y_predict)) print 'error is: {0}'.format(confusion_matrix(Y_test, y_predict)) print ' ' if __name__ == '__main__': # naive Bayes muNB = GaussianNB() # load data iris = load_iris() print "np.shape(iris.data): ", np.shape(iris.data) # feature vec X_train = iris.data[:int(len(iris.data)*0.8)] X_test = iris.data[int(len(iris.data)*0.8):] # label Y_train = iris.target[:int(len(iris.data)*0.8)] Y_test = iris.target[int(len(iris.data)*0.8):] # shuffle X_train, Y_train = shuffle(X_train, Y_train) X_test, Y_test = shuffle(X_test, Y_test) # expend feature X_train = feature_expend(X_train) X_test = feature_expend(X_test) print "X_test: ", X_test # show Pearson Relevance for i in range(len(X_train[0])): print "Pearson Relevance X[{0}]: ".format(i), numpy.corrcoef(np.array([i[0] for i in X_train[:, i:i+1]]), Y_train)[0, 1] model_tain_and_test(X_train, X_test, Y_train, Y_test, len(X_train[0])-1) model_tain_and_test(X_train, X_test, Y_train, Y_test, len(X_train[0]))
上面代碼中,我們新增了一個“colum_1 * colum_2”特征維度,並且打印了該特征的皮爾森相關度,相關度只有0.15,這是一個很差的特征。同時該壞特征占了總特征的1/5比例,是一個不低的比例。
因此在這種情況下,模型的檢出效果受到了影響,下降了。原因之前也解釋過,壞的特征因為累乘效應,影響了最終的概率值。
2. 新增的一批特征中,出現了少量的壞特征,即壞特征占比很低
# -*- coding: utf-8 -*- from sklearn.naive_bayes import GaussianNB import numpy as np from sklearn.datasets import load_iris from sklearn.metrics import accuracy_score from sklearn.metrics import confusion_matrix import numpy from sklearn.utils import shuffle def feature_expend(feature_vec): # colum_1 * colum_2 feature_vec = np.hstack((feature_vec, np.array([[i] for i in np.multiply(feature_vec[:, 0], feature_vec[:, 1])]))) # colum_1 / colum_2 feature_vec = np.hstack((feature_vec, np.array([[i] for i in np.divide(feature_vec[:, 0], feature_vec[:, 1])]))) # colum_3 * colum_4 feature_vec = np.hstack((feature_vec, np.array([[i] for i in np.multiply(feature_vec[:, 2], feature_vec[:, 3])]))) # colum_4 * colum_1 feature_vec = np.hstack((feature_vec, np.array([[i] for i in np.multiply(feature_vec[:, 3], feature_vec[:, 0])]))) # colum_1 ^ 2 feature_vec = np.hstack((feature_vec, np.array([[i] for i in np.multiply(feature_vec[:, 0], feature_vec[:, 0])]))) # colum_2 ^ 2 feature_vec = np.hstack((feature_vec, np.array([[i] for i in np.multiply(feature_vec[:, 1], feature_vec[:, 1])]))) # colum_3 ^ 2 feature_vec = np.hstack((feature_vec, np.array([[i] for i in np.multiply(feature_vec[:, 2], feature_vec[:, 2])]))) # colum_4 ^ 2 feature_vec = np.hstack((feature_vec, np.array([[i] for i in np.multiply(feature_vec[:, 3], feature_vec[:, 3])]))) return feature_vec def model_tain_and_test(X_train, X_test, Y_train, Y_test, feature_cn): # load origin feature X_train_vec = X_train[:, :feature_cn] X_test_vec = X_test[:, :feature_cn] # train model muNB.fit(X_train_vec, Y_train) # predidct the test data y_predict = muNB.predict(X_test_vec) print "feature_cn: ", feature_cn print 'accuracy is: {0}'.format(accuracy_score(Y_test, y_predict)) print 'error is: {0}'.format(confusion_matrix(Y_test, y_predict)) print ' ' if __name__ == '__main__': # naive Bayes muNB = GaussianNB() # load data iris = load_iris() print "np.shape(iris.data): ", np.shape(iris.data) # feature vec X_train = iris.data[:int(len(iris.data)*0.8)] X_test = iris.data[int(len(iris.data)*0.8):] # label Y_train = iris.target[:int(len(iris.data)*0.8)] Y_test = iris.target[int(len(iris.data)*0.8):] # shuffle X_train, Y_train = shuffle(X_train, Y_train) X_test, Y_test = shuffle(X_test, Y_test) # expend feature X_train = feature_expend(X_train) X_test = feature_expend(X_test) print "X_test: ", X_test # show Pearson Relevance for i in range(len(X_train[0])): print "Pearson Relevance X[{0}]: ".format(i), numpy.corrcoef(np.array([i[0] for i in X_train[:, i:i+1]]), Y_train)[0, 1] model_tain_and_test(X_train, X_test, Y_train, Y_test, len(X_train[0]))
在這個場景中,“colum_1 * colum_2”這個壞特征依然存在,但和上一個場景不同的是,除了這個壞特征之外,新增的特征都是好特征(相關度都很高)。
根據簡單的乘積因子原理可以很容易理解,這個壞特征對最終概率數值的影響會被“稀釋”,從而降低了對模型性能的影響。
至此,我們得出如下結論:
深度神經網絡的隱層結構大規模增加了特征的數量。本質上,深度神經網絡通過矩陣線性變換和非線性激活函數得到海量的特征維度的組合。我們可以想象到,其中一定有好特征(相關度高),也一定會有壞特征(相關度低)。
但是有一定我們可以確定,好特征的出現概率肯定是遠遠大於壞特征的,因為所有的特征都是從輸入層的好特征衍生而來的(遺傳進化思想)。那么當新增特征數量足夠多的時候,從概率上就可以證明,好特征的影響就會遠遠大於壞特征,從而消解掉了壞特征對模型性能的影響。這就是為什么深度神經網絡的適應度很強的原因之一。
用一句通俗的話來說就是:如果你有牛刀,殺雞為啥不用牛刀?用牛刀殺雞的好處在於,不管來的是雞還是牛,都能自適應地保證能殺掉。
3. 不同機器學習模型中,過特征化的結構基礎
冗余特征和過特征化現象在機器學習模型中並不罕見,在不同的模型中有不同的表現形式,例如:
- 朴素貝葉斯:累乘效應
- 多項式回歸:累加效應
- 深度神經網絡:累加、累乘效應
4. 一些工程實踐指導原則
這里列舉一些筆者在工程實踐中總結出的一些指導性原則:
- 將benchmark作為基本操作,在探索復雜模型之前采用決策樹、簡單邏輯回歸、朴素貝葉斯這樣的基礎模型進行基准測試。目的是獲取目標問題真實難度的大致判斷。
- 在項目開始的時候,盡量先選用小的模型,一般來說,模型復雜度越低,泛化性能越好。
- 將正則化作是機器學習訓練中的標准配置,例如剪枝、dropout、正則參數懲罰等,目的是為了將決策權重集中到真正有價值的特征上。深度神經網絡dropout的作用,一定程度上可以理解為,去除相關性較低的特征在最終決策中的權重,避免“低相關度特征累乘現象”導致的誤差效應。
- 重視特征工程環節,對候選的特定進行相關性分析,優先選出相關度大於0.25的特征維度,篩選掉低相關度的無用特征。