【讀書筆記】特徵工程不再難
本篇文章為 特徵工程不再難:資料科學新手也能輕鬆搞定!( Feature Engineering Made Easy. By Sinan Ozdemir, Divya Susarla ) 之個人讀書筆記。
書中有提供程式碼,請見此 Github Repo,但應該是 Python 2,我下面的程式碼都會改為 Python 3 版本。
特徵工程的評估步驟
- 先得到機器學習模型的 baseline performance
- 應用特徵工程
- 對於每一種特徵工程,獲取一個效能指標並和 baseline 相比較
- 如果效能的改進大於某個臨界值 (User defined),則認為這種特徵工程是有益的,並將其部屬到機器學習模型中
- 效能的改變通常是以百分比(%)為計 (如果 baseline performance 是從 40% 準確率變成 76% 準確率的話,改進就是 76-40 / 40 = 90%
評估監督式學習演算法?迴歸通常用 MSE,分類通常用 Accuracy 或是 AUC
評估非監督式學習演算法?主要用 輪廓係數( silhouette coefficient ) 或是用統計檢定的相關係數、t-test、卡方檢定(Chi-squared tests)以及其他方法來評估並量化原始資料以及轉換後的資料的結果
特徵工程的技巧有哪些
- 特徵理解 : 學習如何辨識定量(數值型)和定性資料(分類型)
- 特徵改進 : 清洗和填補缺失值
- 特徵選擇 : 透過統計方法選擇一部分特徵以減少資料雜訊
- 特徵建構 : 建構新的特徵,探索特徵之間的互動
- 特徵轉換 : 提取資料中的隱藏結構,利用數學方法轉換資料集、增強效果
- 特徵學習 : 以深度學習來對資料進行學習,以此來更加地瞭解資料
特徵理解
資料結構分為結構化和非結構化
- 結構化資料指的是可明確將觀察值(Row)和特徵(Column)分開的資料
- 非結構化資料指的是不遵守標準結構 (表格) 的資料
通常判斷資料的第一個問題是,資料是定量還是定性的?
事實上,資料可以同時是定量和定性的,為了更明確的去區分開來,通常會分為四個等級
- 定類等級 (nominal level) : 第一個等級,結構最弱,屬於定性資料,比如血型的A、B、O、AB,動物物種和人名,通常可以畫出眾數(mode)以及長條圖(bar plot)、圓餅圖來統計
- 定序等級 (ordinal level) : 繼承了定類的屬性同時可以用來排序,屬於定性資料,比如考試的成績A, B, C~F,可算出頻率、眾數、中位數、百分位數,不僅可畫出長條和圓餅圖,也可以畫出莖葉圖、盒鬚圖
- 定距等級 (interval level) : 屬於定量資料,不僅可以排序,值之間的差異也有意義,也能夠做加減計算,比如溫度。除了頻率、眾數、中位數以外也能算出 mean 和 std,能畫出上面提到的圖以及直方圖 (Histogram)、散佈圖(scatter plot)、折線圖(line plot),也能將差值做為新的特徵
- 定比等級 (ratio level) : 最高等級,擁有最高程度的控制以及數學運算能力,有 0 的概念且能做乘除運算,比如金錢、重量。可以算出 mean 和 std,且最高和最低之間的比值有意義。
實作上可用 Pandas 的 .describe() 來讓他自動判斷定量或是定性
特徵改進
這邊只提定量資料的填補缺失值以及刪除有害資料跟資料的Normalization / Standardization、建構新特徵和轉換資料維度
填補缺失值
通常會補0、unknown 或是 ?
以 pima 資料集為例
# 檢查是否有缺失值
pima.isnull().sum()
# 對 serum_insulin 這行的缺失值從 0 改為 None
pima['serum_insulin'] = pima['serum_insulin'].map(lambda x:x if x != 0) else None)
# 也可用 for loop 來改掉多個
columns = ['serum_insulin', 'bmi', 'plasma_glucose_concentration']
for col in columns:
pima[col].replace([0], [None], inplace=True)
# 刪除有害的 col, 直接 drop 掉 NaN 值, 若改為 dropna(0) 則是把 NaN 用0填補
pima_dropped = pima.dropna()
# 檢查刪掉了多少列
num_rows_lost = round(100*(pima.shape[0] - pima_dropped.shape[0]) / float(pima.shape[0]))
print("retained {}% of rows".format(num_rows_lost))
# 檢查刪除 NaN 前後的差別
# label值的差別
pima['onset_diabetes'].value.counts(normalize=True)
pima_dropped['onset_diabetes'].value.counts(normalize=True)
# 每一行平均值的差別
pima.mean()
pima_dropped.mean()
# 平均值變化百分比
(pima_dropped.mean() - pima.mean()) / pima.mean()
# 用長條圖視覺化變化百分比
ax = ((pima_dropped.mean() - pima.mean()) / pima.mean()
plot(kind='bar', title='% change in average column values')
ax.set_ylabel('% change')
另一個常用的手法是利用該行的平均值或是中位數去填補缺失值
pima['plasma_glucose_concentration'].fillna(pima['plasma_glucose_concentration'].mean(), inplace=True)
但這樣一次只能補一行 (每行平均值不同)因此也可利用 scikit-learn 的 Imputer 來一次做完
from sklearn.impute import SimpleImputer
imputer = SimpleImputer(missing_values=np.nan,, strategy='mean')
pima_imputed = imputer.fit_transform(pima) #這會輸出 Numpy array
type(pima_imputed)
pima_imputed = pd.DataFrame(pima_imputed, columns=pima_column_names) # 轉回 DataFrame
pima_imputed.head()
pima_imputed.isnull().sum()
需要注意的是不能在分割資料集之前就做填補,應該要讓訓練資料集與測試資料集都以訓練資料的平均值去填補他們,否則模型就會在訓練之前就事先知道所有資料的平均值是甚麼了,這樣是作弊,對你的模型一點幫助都沒有甚至可能使其學習得更差
# 不正確的流程
from sklearn.model_selection import train_test_split
X = pima[['serum_insulin']].copy()
y = pima['onset_diabetes'].copy()
X_train, X_test, y_train, y_test = train_test_split(X, y, random-state=64)
X.isnull().sum()
entire_data_set_mean = X.mean() # 所有資料集的平均
X = X.fillna(entire_data_set_mean)
print(entire_data_set_mean)
X_train, X_test, y_train, y_test = train_test_split(X, y, random-state=64)
# 正確的流程
from sklearn.model_selection import train_test_split
X = pima[['serum_insulin']].copy()
y = pima['onset_diabetes'].copy()
X_train, X_test, y_train, y_test = train_test_split(X, y, random-state=64)
X.isnull().sum()
training_mean = X_train.mean() # 訓練資料集的平均
print(training_mean)
X_train = X_train.fillna(training_mean) # 用訓練資料集的平均去填補
X_test = X_test.fillna(training_mean) # 一樣用訓練資料集的平均去填補
也可以利用 Pipeline 將整個流程簡化
from sklearn.pipeline import Pipeline
from sklearn.model_selection import GridSearchCV
from sklearn.neighbors import KNeighborsClassifier
knn_params = {'classify_n_neighbors' : [1, 2, 3, 4, 5, 6, 7]}
knn = KNeighborsClassifier()
mean_impute = Pipeline([('imputer', SimpleImputer(strategy='mean')), ('classifier', knn)])
X = pima.drop('onset_diabetes', axis=1)
y = pima['onset_diabetes']
grid = GridSearchCV(mean_impute, knn_params)
grid.fit(X,y)
print(grid.best_score_, grid.best_params_)
如果要換成用中位數填補的話就把 mean 都改成 median 即可
標準化和常態化 (Standardization and Normalization)
有些模型會受到資料尺度不同而影響,比說舒張壓介於24到122之間,但年齡是21歲到81歲之,此時模型就較難最佳化。因此通常會做標準化或常態化來緩解這個問題。
常用的有 z-score 、 min-max scale 和 row normalization 三種方法
- z-score 是將所有資料都減去其平均值並除以標準差得到 z-score
- min-max 是將所有資料都 mapping 到你所設定的範圍上,通常是 0~1 或是 -1~1
- row normalization 則是讓每一個 row 都有一樣的 範數(Norm),比如 L2-Norm
標準化
from sklearn.preprocessing import Normalizer, StandardScaler, MinMaxScaler
# 算出 plasma_glucose_concentration 的平均值和標準差
pima['plasma_glucose_concentration'].mean(), pima['plasma_glucose_concentration'].std()
# 畫出直方圖
ax = pima['plasma_glucose_concentration'].hist()
ax.set_title('Distribution of plasma_glucose_concentration')
# z-score 標準化
glucose_z_score_standardized = StandardScaler().fit_transform(pima[['plasma_glucose_concentration']])
# 做完之後平均值會等於0,而標準差會等於1
glucose_z_score_standardized.mean(), glucose_z_score_standardized.std()
# 畫出做完 z-score 後的直方圖
ax = pd.Series(glucose_z_score_standardized.reshape(-1,)).hist()
ax.set_title('Distribution of plasma_glucose_concentration after Z Score Scaling')
# MinMax 標準化, 預設是 mapping 到 0~1之間
glucose_min_max_standardized = MinMaxScaler().fit_transform(pima[['plasma_glucose_concentration']])
glucose_min_max_standardized.mean(), glucose_min_max_standardized.std()
# 畫出做完 min_max 後的直方圖
ax = pd.Series(glucose_min_max_standardized.reshape(-1,)).hist()
ax.set_title('Distribution of plasma_glucose_concentration after Min Max Scaling')
# 如果要一次對所有的資料都做標準化並畫出來
scale = StandardScaler() # instantiate a z-scaler object
pima_imputed_mean_scaled = pd.DataFrame(scale.fit_transform(pima_imputed_mean), columns=pima_column_names)
pima_imputed_mean_scaled.hist(figsize=(15, 15), sharex=True)
常態化
from sklearn.preprocessing import Normalizer
normalize = Normalizer()
pima_normalized = pd.DataFrame(normalize.fit_transform(pima_imputed), columns=pima_column_names)
# 做完常態化之後所有 row 的範數會都是1,其平均當然也會是 1
print(np.sqrt((pima_normalized**2).sum(axis=1)).mean())
# pipeline 版本
knn_params = {'imputer__strategy': ['mean', 'median'], 'classify__n_neighbors':[1, 2, 3, 4, 5, 6, 7]}
mean_impute_normalize = Pipeline([('imputer', Imputer()), ('normalize', Normalizer()), ('classify', knn)])
X = pima.drop('onset_diabetes', axis=1)
y = pima['onset_diabetes']
grid = GridSearchCV(mean_impute_normalize, knn_params)
grid.fit(X, y)
print(grid.best_score_, grid.best_params_)
受資料尺度而影響的機器學習演算法比如
- KNN、K-Means : 因為依賴於歐幾里得距離
- Logistic Regression、SVM、神經網路 : 如果利用梯度下降來學習的話就會受影響
- PCA : 特徵向量會偏向尺度較大的 Column
特徵建構
包含檢查資料、填補分類特徵、編碼分類特徵、擴展數值特徵
檢查資料是指把資料印出來,填補分類特徵是用 isnull().sum() 來檢查是否有缺失值,並且用自定義的 Imputer 來填補
編碼分類特徵是指把資料變成 dummy variable,並且記得要避免 dummy variable trap
- 比如 female 和 male 都可以編碼成 0 或是 1,這時候就要利用 pd.get_dummies 寫死
至於定序的特徵 (有順序性關係的,比如喜歡、有些人喜歡以及不喜歡可以編碼成 2, 1, 0)
則是可以用 mapping 的方式編碼,或是就在讀取的時候定義好即可,如下
X = pd.DataFrame({'city':['tokyo', None, 'london', 'seattle', 'san francisco', 'tokyo'], 'boolean':['yes', 'no', None, 'no', 'no', 'yes'], 'ordinal_column':['somewhat like', 'like', 'somewhat like', 'like', 'somewhat like', 'dislike'], 'quantitative_column':[1, 11, -.5, 10, None, 20]})
最後是對連續型特徵分箱,可以利用 pd.cut 達成擴展數值特徵這段是在講要結合特徵,這部分可以先透過觀察資料來達成,比如下圖就表示只要猜7就可以達到51%的正確率
另一個例子是產生多項式特徵,有degree,多項式的指數、interaction_only,是否只產生互相影響的特徵的布林,以及inclue_bias,是否產生指數為0特徵的布林這三個重要參數可以調整
特徵選擇
包含特徵選擇、特徵改進基於模型的特徵選擇則是有決策樹之類的,比如我曾經寫給同學看的鐵達尼號特徵排序repo,另外也可以用線性回歸、SVM去選擇特徵,可以利用 sklearn 的 SelectFromModel 來選
特徵轉換
Summary of machine learning pipeline : EDA -> Feature Understanding -> Feature improvement -> Feature Comstruction -> Feature Selection
這部分主要是在講降維,比如上面有提過的 PCA 以及 中心化和縮放 (也就是StandardScaler)對 PCA 的影響,書中有做一個實驗是比較有中心化跟沒中心化的 PCA 差別,結論是有一點的差別,可以都做看看並且測試
另外一個有提到的就是 LDA,它有5個步驟
- 計算每個類別的平均值
- 計算類別內跟類別之間的散布矩陣
- 計算特徵值跟特徵向量
- 降冪排序特徵值,保留前 k 個特徵向量
- 使用前幾個特徵向量將資料投影到新空間
特徵學習
這是本書的最後一章,特別的是這邊的主題是RBM,受限波爾茲曼機。
實際上這是真正意義上的神經網路起源模型,因為神經網路是從 Hinton 的深度信念網路(Deep Belief Network, DBN) 而來,而 DBN 就是把 RBM 疊起來而成的網路
留言
張貼留言