導讀:
分類問題是機器學習應用中的常見問題,而二分類問題是其中的典型,例如垃圾郵件的識別。本文基於UCI機器學習數據庫中的銀行營銷數據集,從對數據集進行探索,數據預處理和特徵工程,到學習模型的評估與選擇,較爲完整的展現瞭解決分類問題的大體流程。文中包含了一些常見問題的處理方式,例如缺失值的處理、非數值屬性如何編碼、如何使用過抽樣和欠抽樣的方法解決分類問題中正負樣本不均衡的問題等等。html
做者:llhthinkergit
歡迎轉載,請保留原文連接:http://www.cnblogs.com/llhthinker/p/7101572.htmlgithub
本次實驗選取UCI機器學習庫中的銀行營銷數據集(Bank Marketing Data Set: http://archive.ics.uci.edu/ml/datasets/Bank+Marketing )[Moro et al., 2014]. 。這些數據與葡萄牙銀行機構的直接營銷活動有關。這些直接營銷活動是以電話爲基礎的。一般來講,銀行機構的客服人員至少須要聯繫一次客戶來得知客戶是否將認購銀行的產品(按期存款)。所以,與該數據集對應的任務是分類任務,而分類目標是預測客戶是(yes)否(no)認購按期存款(變量y)。算法
數據集包含四個csv文件:數據庫
1) bank-additional-full.csv: 包含全部的樣例(41188個)和全部的特徵輸入(20個),根據時間排序(從2008年5月到2010年9月);網絡
2) bank-additional.csv: 從1)中隨機選出10%的樣例(4119個);app
3) bank-full.csv: 包含全部的樣例(41188個)和17個特徵輸入,根據時間排序。(該數據集是更老的版本,特徵輸入較少);dom
4) bank.csv: 從3)中隨機選出10%的樣例4119個)。機器學習
提供小的數據集(bank-additional.csv和bank.csv)是爲了可以快速測試一些計算代價較大的機器學習算法(例如SVM)。本次實驗將選取較新的數據集,即包含20個特徵量的1)和2)。ide
數據集的輸入變量是20個特徵量,分爲數值變量(numeric)和分類(categorical)變量。具體描述見數據集網站http://archive.ics.uci.edu/ml/datasets/Bank+Marketing。
輸出變量爲y,即客戶是否已經認購按期存款(binary: "yes", "no")。
首先載入數據,
而後使用info()函數和describe()函數查看數據集的基本信息。
從2.2節給出的數據集基本信息能夠看出,數值型變量(int64和float64)沒有缺失。非數值型變量可能存在unknown值。使用以下代碼查看字符型變量unknown值的個數。
缺失值處理一般有以下的方法:
def fill_unknown(data, bin_attrs, cate_attrs, numeric_attrs): # fill_attrs = ['education', 'default', 'housing', 'loan'] fill_attrs = [] for i in bin_attrs+cate_attrs: if data[data[i] == 'unknown']['y'].count() < 500: # delete col containing unknown data = data[data[i] != 'unknown'] else: fill_attrs.append(i) data = encode_cate_attrs(data, cate_attrs) data = encode_bin_attrs(data, bin_attrs) data = trans_num_attrs(data, numeric_attrs) data['y'] = data['y'].map({'no': 0, 'yes': 1}).astype(int) for i in fill_attrs: test_data = data[data[i] == 'unknown'] testX = test_data.drop(fill_attrs, axis=1) train_data = data[data[i] != 'unknown'] trainY = train_data[i] trainX = train_data.drop(fill_attrs, axis=1) test_data[i] = train_predict_unknown(trainX, trainY, testX) data = pd.concat([train_data, test_data]) return data
爲了能使分類變量參與模型計算,咱們須要將分類變量數值化,也就是編碼。分類變量又能夠分爲二項分類變量、有序分類變量和無序分類變量。不一樣種類的分類變量編碼方式也有區別。
根據上文的輸入變量描述,能夠認爲變量default 、housing 和loan 爲二分類變量,對其進行0,1編碼。代碼以下:
def encode_bin_attrs(data, bin_attrs): for i in bin_attrs: data.loc[data[i] == 'no', i] = 0 data.loc[data[i] == 'yes', i] = 1 return data
3.2.2 有序分類變量編碼
根據上文的輸入變量描述,能夠認爲變量education是有序分類變量,影響大小排序爲"illiterate", "basic.4y", "basic.6y", "basic.9y", "high.school", "professional.course", "university.degree", 變量影響由小到大的順序編碼爲一、二、三、...,。代碼以下:
def encode_edu_attrs(data): values = ["illiterate", "basic.4y", "basic.6y", "basic.9y", "high.school", "professional.course", "university.degree"] levels = range(1,len(values)+1) dict_levels = dict(zip(values, levels)) for v in values: data.loc[data['education'] == v, 'education'] = dict_levels[v] return data
根據上文的輸入變量描述,能夠認爲變量job,marital,contact,month,day_of_week爲無序分類變量。須要說明的是,雖然變量month和day_of_week從時間角度是有序的,可是對於目標變量而言是無序的。對於無序分類變量,能夠利用啞變量(dummy variables)進行編碼。通常的,n個分類須要設置n-1個啞變量。例如,變量marital分爲divorced、married、single,使用兩個啞變量V1和V2來編碼。
marital |
V1 |
V2 |
divorced |
0 |
0 |
married |
1 |
0 |
single |
0 |
1 |
Python的pandas包提供生成啞變量的函數,故代碼以下:
def encode_cate_attrs(data, cate_attrs): data = encode_edu_attrs(data) cate_attrs.remove('education') for i in cate_attrs: dummies_df = pd.get_dummies(data[i]) dummies_df = dummies_df.rename(columns=lambda x: i+'_'+str(x)) data = pd.concat([data,dummies_df],axis=1) data = data.drop(i, axis=1) return data
將連續型特徵離散化的一個好處是能夠有效地克服數據中隱藏的缺陷: 使模型結果更加穩定。例如,數據中的極端值是影響模型效果的一個重要因素。極端值致使模型參數太高或太低,或致使模型被虛假現象"迷惑",把原來不存在的關係做爲重要模式來學習。而離散化,尤爲是等距離散,能夠有效地減弱極端值和異常值的影響。
經過觀察2.2節的原始數據集的統計信息,能夠看出變量duration的最大值爲4918,而75%分位數爲319,遠小於最大值,並且該變量的標準差爲259,相對也比較大。所以對變量duration進行離散化。具體地,使用pandas.qcut()函數來離散化連續數據,它使用分位數對數據進行劃分(分箱: bining),能夠獲得大小基本相等的箱子(bin),以區間形式表示。而後使用pandas.factorize()函數將區間轉爲數值。
data[bining_attr] = pd.qcut(data[bining_attr], bining_num)
data[bining_attr] = pd.factorize(data[bining_attr])[0]+1
因爲不一樣變量經常使用不一樣的度量單位,從數值上看它們相差很大,容易使基於距離度量的學習模型更容易受數值較大的變量影響。數據規範化就是將數據壓縮到一個範圍內,從而使得全部變量的單位影響一致。
for i in numeric_attrs: scaler = preprocessing.StandardScaler() data[i] = scaler.fit_transform(data[i])
因爲須要訓練模型預測unknown值,預處理過程的時間代價比較大。所以將預處理後的數據持久化,保存到文件中,以後的學習模型直接讀取文件數據進行訓練預測,無須再預處理。
def preprocess_data(): input_data_path = "../data/bank-additional/bank-additional-full.csv" processed_data_path = '../processed_data/bank-additional-full.csv' print("Loading data...") data = pd.read_csv(input_data_path, sep=';') print("Preprocessing data...") numeric_attrs = ['age', 'duration', 'campaign', 'pdays', 'previous', 'emp.var.rate', 'cons.price.idx', 'cons.conf.idx', 'euribor3m', 'nr.employed',] bin_attrs = ['default', 'housing', 'loan'] cate_attrs = ['poutcome', 'education', 'job', 'marital', 'contact', 'month','day_of_week'] data = shuffle(data) data = fill_unknown(data, bin_attrs, cate_attrs, numeric_attrs) data.to_csv(processed_data_path, index=False)
須要注意的是,因爲原始數據是有序的(以時間爲序),讀取原始數據後,須要將其隨機打亂,變成無序數據集。這裏使用sklearn.utils包中的shuffle()函數進行打亂。
一些狀況下原始數據維度很是高,維度越高,數據在每一個特徵維度上的分佈就越稀疏,這對機器學習算法基本都是災難性(維度災難)。當咱們又沒有辦法挑選出有效的特徵時,須要使用PCA等算法來下降數據維度,使得數據能夠用於統計學習的算法。可是,若是可以挑選出少而精的特徵了,那麼PCA等降維算法沒有很大必要。在本次實驗中,數據集中的特徵已經比較有表明性並且並不過多,因此應該不須要降維(實驗證實降維確實沒有幫助)。關於降維的介紹能夠參考以前寫的這個博客。
總之,數據預處理對於訓練機器學習算法很是重要,正所謂「garbage in, garbage out」。
首先,須要將處理好的數據集劃分爲3部分,分別是訓練集(train set)、交叉檢驗集(Cross validation set)和測試集(test set)。(另見博客學習模型的評估和選擇)。訓練集是用於訓練模型。交叉檢驗集用來進行模型的選擇,包括選擇不一樣的模型或者同一模型的不一樣參數,即選擇在交叉檢驗集上的測試結果最優的模型。測試集用於檢測最終選擇的最優模型的質量。一般,能夠按照6:2:2的比例劃分,代碼以下:
def split_data(data): data_len = data['y'].count() split1 = int(data_len*0.6) split2 = int(data_len*0.8) train_data = data[:split1] cv_data = data[split1:split2] test_data = data[split2:] return train_data, cv_data, test_data
對導入的數據集按以下方式進行簡單統計能夠發現,正樣本(y=1)的數量遠小於負樣本(y=0)的數量,近似等於負樣本數量的1/8。
在分類模型中,這種數據不平衡問題會使得學習模型傾向於把樣本分爲多數類,可是,咱們經常更關心少數類的預測狀況。在本次分類問題中,分類目標是預測客戶是(yes:1)否(no:0)認購按期存款(變量y)。顯然,咱們更關心有哪些客戶認購按期存款。爲減弱數據不均衡問題帶來的不利影響,在數據層面有兩種較簡單的方法:過抽樣和欠抽樣。
在本次實驗中,採用Smote算法[Chawla et al., 2002]增長新的樣本進行過抽樣;採用隨機地去掉一些多數類樣本的方法進行欠抽樣。Smote算法的基本思想是對於少數類中每個樣本x,以歐氏距離爲標準計算它到少數類樣本集中全部樣本的距離,獲得其k近鄰。而後根據樣本不平衡比例設置一個採樣比例以肯定採樣倍率N,對於每個少數類樣本x,從其k近鄰中隨機選擇若干個樣本,構建新的樣本。針對本實驗的數據,爲防止新生成的數據噪聲過大,新的樣本只有數值型變量真正是新生成的,其餘變量和原樣本一致。重採樣的代碼以下:
def resample_train_data(train_data, n, frac): numeric_attrs = ['age', 'duration', 'campaign', 'pdays', 'previous', 'emp.var.rate', 'cons.price.idx', 'cons.conf.idx', 'euribor3m', 'nr.employed',] #numeric_attrs = train_data.drop('y',axis=1).columns pos_train_data_original = train_data[train_data['y'] == 1] pos_train_data = train_data[train_data['y'] == 1] new_count = n * pos_train_data['y'].count() neg_train_data = train_data[train_data['y'] == 0].sample(frac=frac) train_list = [] if n != 0: pos_train_X = pos_train_data[numeric_attrs] pos_train_X2 = pd.concat([pos_train_data.drop(numeric_attrs, axis=1)] * n) pos_train_X2.index = range(new_count) s = smote.Smote(pos_train_X.values, N=n, k=3) pos_train_X = s.over_sampling() pos_train_X = pd.DataFrame(pos_train_X, columns=numeric_attrs, index=range(new_count)) pos_train_data = pd.concat([pos_train_X, pos_train_X2], axis=1) pos_train_data = pd.DataFrame(pos_train_data, columns=pos_train_data_original.columns) train_list = [pos_train_data, neg_train_data, pos_train_data_original] else: train_list = [neg_train_data, pos_train_data_original] print("Size of positive train data: {} * {}".format(pos_train_data_original['y'].count(), n+1)) print("Size of negative train data: {} * {}".format(neg_train_data['y'].count(), frac)) train_data = pd.concat(train_list, axis=0) return shuffle(train_data)
經常使用的分類模型包括感知機,SVM,樸素貝葉斯,決策樹,logistic迴歸,隨機森林等等。本次實驗選擇logistic迴歸和隨機森林在訓練集上進行訓練,在交叉檢驗集上進行評估,隨機森林的表現更優,因此最終選擇隨機森林模型在測試集上進行測試。
對於不一樣的任務,評價一個模型的優劣可能不一樣。正如4.2節中所言,實驗選取的數據集是不平衡的,數據集中負樣本0值佔數據集總比例高達88.7%,若是咱們的模型"預測"全部的目標變量值都爲0,那麼準確度(Accuracy)應該在88.7%左右。可是,顯然,這種"預測"沒有意義。因此,咱們更傾向於可以預測出正樣本(y=1)的模型。所以,實驗中將正樣本的f1-score做爲評價模型優劣的標準(也能夠用其餘相似的評價指標如AUC)。訓練與評估的代碼以下:
def train_evaluate(train_data, test_data, classifier, n=1, frac=1.0, threshold = 0.5): train_data = resample_train_data(train_data, n, frac) train_X = train_data.drop('y',axis=1) train_y = train_data['y'] test_X = test_data.drop('y', axis=1) test_y = test_data['y'] classifier = classifier.fit(train_X, train_y) prodict_prob_y = classifier.predict_proba(test_X)[:,1] report = classification_report(test_y, prodict_prob_y > threshold, target_names = ['no', 'yes']) prodict_y = (prodict_prob_y > threshold).astype(int) accuracy = np.mean(test_y.values == prodict_y) print("Accuracy: {}".format(accuracy)) print(report) fpr, tpr, thresholds = metrics.roc_curve(test_y, prodict_prob_y) precision, recall, thresholds = metrics.precision_recall_curve(test_y, prodict_prob_y) test_auc = metrics.auc(fpr, tpr) plot_pr(test_auc, precision, recall, "yes") return prodict_y
利用訓練評估函數能夠進行模型的選擇,分別選擇Logistic迴歸模型和隨機森林模型,並對其分別調整各自參數的取值,最終選擇f1-score最高的隨機森林模型。具體地,當將n_estimators設置爲400,對正樣本進行7倍的過抽樣(n=7),不對負樣本進行負抽樣(frac=1.0),正樣本分類的閾值爲0.40(threshold),即當預測某樣本屬於正樣本的機率大於0.4時,就將該樣本分類爲正樣本。
forest = RandomForestClassifier(n_estimators=400, oob_score=True)
prodict_y = train_evaluate(train_data, test_data, forest, n=7, frac=1, threshold=0.40)
該模型在交叉檢驗集上的評估結果以下:
precision-recall曲線以下:
最後,將該模型應用於測試集,測試結果以下:
precision-recall曲線以下:
還能夠考慮如下幾個方面以提升F1得分:
完整代碼:見github
參考資料:
[Moro et al., 2014] S. Moro, P. Cortez and P. Rita. A Data-Driven Approach to Predict the Success of Bank Telemarketing. Decision Support Systems, Elsevier, 62:22-31, June 2014
[Chawla et al., 2002] N. V. Chawla, L. O. Hall, K. W. Bowyer, and W. P. Kegelmeyer. SMOTE: SMOTE: Synthetic Minority Over-sampling Technique. Journal of Artificial Intelligence Research, 16:321–357, 2002.
http://blog.csdn.net/dream2009gd/article/details/35569343