特徵真的越多越好嗎?從特徵工程角度看「garbage in,garbage out」

  • 2019 年 10 月 24 日
  • 筆記

1. 從樸素貝葉斯在醫療診斷中的迷思說起

這個模型最早被應用於醫療診斷,其中,類變量的不同值用於表示患者可能患的不同疾病。證據變量用於表示不同癥狀、化驗結果等。在簡單的疾病診斷上,樸素貝葉斯模型確實發揮了很好的作用,甚至比人類專家的診斷結果都要好。但是在更深度的應用中,醫生髮現,對於更複雜(由多種致病原因和癥狀共同表現)的疾病,模型表現的並不好。

數據科學家經過分析認為,出現這種現象的原因在於:模型做了集中通常並不真實的強假設,例如:

  • 一個患者至多可能患一種疾病
  • 在已知患者的疾病條件下,不同癥狀的出現與否,不同化驗結果,之間是互相獨立的

這種模型可用於醫學診斷是因為少量可解釋的參數易於由專家獲得,早期的機器輔助醫療診斷系統正式建立在這一技術之上。

但是,之後更加深入的實踐表明,構建這種模型的強假設降低了模型診斷的準確性,尤其是“過度計算”某些特定的證據,該模型很容易過高估計某些方面特徵的影響

例如,“高血壓”和“肥胖症”是心臟病的兩個硬指標,但是,這兩個癥狀之間相關度很高,高血壓一般就伴隨着肥胖症。在使用樸素貝葉斯公式計算的時候,由於乘法項的緣故,關於這方面的證據因子就會被重複計算,如下式:

P(心臟病 | 高血壓,肥胖症) = P(高血壓 | 心臟病) * P(高血壓 | 肥胖症) / P(高血壓,肥胖症)

由於“高血壓”和“肥胖症”之間存在較強相關性的緣故,我們可以很容易想像,分子乘積增加的比率是大於分母聯合分佈增加的比率的。因此,當分子項繼續增加的時候,最終的後驗概率就會不斷增大。但是因為新增的特徵項並沒有提供新的信息,後驗概率的這種增大變化反而降低了模型的預測性能。

實際上,在實踐中人們發現,樸素貝葉斯模型的診斷性能會隨着特徵的增加而降低,這種降低常常歸因於違背了強條件獨立性假設

筆者將這種現象稱之為“過度特徵化(over-featuring)”,這是工程中常見的一種現象,過度特徵化如果無法得到有效規避,會顯著降低模型的泛化和預測性能。在這篇文章中,我們通過實驗和分析來論證這個說法。

 

2. 用鳶尾花分類例子討論特徵工程問題

0x1:數據集觀察

Iris 鳶尾花數據集是一個經典數據集,在統計學習和機器學習領域都經常被用作示例。
 
數據集內包含 3 類共 150 條記錄,每類各 50 個數據,每條記錄都有 4 項特徵:
  • 花萼長度
  • 花萼寬度
  • 花瓣長度
  • 花瓣寬度

可以通過這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的特徵維度,篩選掉低相關度的無用特徵。