機器學習實戰-Scikit決策樹分類算法

機器學習實戰-決策樹分類算法

鑑於目前Python最流行的機器學習庫Scikit集成了很多成型的算法,本博客就使用其中的決策樹對Kdd99數據集進行分類處理,完整代碼位於Github(https://github.com/PENGZhaoqing/kdd99-scikit)

Kdd99數據集

KDD Cup是由SIGKDD贊助的數據挖掘分析的比賽,每年舉行一次,Kdd99是1999年比賽的關於入侵檢測的數據集,不過現在學術界已經拋棄了這個數據集,若要測試自己算法的有效性還是使用其他數據集吧,我們只爲初窺兩種分類算法

任務目的是檢測網絡連接,區分正常和非正常的連接。非正常的連接大概有四大類: DOS、R2L、 U2R和probing,每一類下還有若干小類別

  • 值得注意的是,訓練集和測試集中的數據概率分佈不同,測試集比訓練集多出了14種攻擊小類別,官方給的解釋是:這能夠很好的模擬真實情況,而且新的攻擊類別一般爲已知攻擊類別的變形。因此算法模型需要能很好的抓住每種攻擊類別的feature,數據集都能在官網上獲得

1. 數據預處理

1.1 原始文件預處理

原始訓練集和測試集的分類結果都是攻擊的特定類,共有24+14=38種 ,而在這裏,我們不考慮那麼細的分類,只把所有的連接分到四個攻擊的大類即可,因此我們根據官網提供的映射關係,將type屬性降爲5類,用數字0,1,2,3,4替換,分別代表四種攻擊的大類和正常情況

  • 執行以下代碼能將位於raw目錄下的測試集和訓練集預處理到data目錄下
from io import open

category = {}
category["back."] = 2
category["buffer_overflow."] = 3
category["ftp_write."] = 4
category["guess_passwd."] = 4
category["imap."] = 4
category["ipsweep."] = 1
category["land."] = 2
category["loadmodule."] = 3
category["multihop."] = 4
category["neptune."] = 2
category["nmap."] = 1
category["perl."] = 3
category["phf."] = 4
category["pod."] = 2
category["portsweep."] = 1
category["rootkit."] = 3
category["satan."] = 1
category["smurf."] = 2
category["spy."] = 4
category["teardrop."] = 2
category["warezclient."] = 4
category["warezmaster."] = 4

category["apache2."] = 2
category["back."] = 2
category["buffer_overflow."] = 3
category["ftp_write."] = 4
category["guess_passwd."] = 4
category["httptunnel."] = 4
category["httptunnel."] = 3
category["imap."] = 4
category["ipsweep."] = 1
category["land."] = 2
category["loadmodule."] = 3
category["mailbomb."] = 2
category["mscan."] = 1
category["multihop."] = 4
category["named."] = 4
category["neptune."] = 2
category["nmap."] = 1
category["perl."] = 3
category["phf."] = 4
category["pod."] = 2
category["portsweep."] = 1
category["processtable."] = 2
category["ps."] = 3
category["rootkit."] = 3
category["saint."] = 1
category["satan."] = 1
category["sendmail."] = 4
category["smurf."] = 2
category["snmpgetattack."] = 4
category["snmpguess."] = 4
category["sqlattack."] = 3
category["teardrop."] = 2
category["udpstorm."] = 2
category["warezmaster."] = 2
category["worm."] = 4
category["xlock."] = 4
category["xsnoop."] = 4
category["xterm."] = 3
category["normal."] = 0

attr_list =['duration', 'protocol_type','service', 'flag', 'src_bytes', 'dst_bytes', 'land','wrong_fragment', 'urgent','hot', 'num_failed_logins', 'logged_in', 'num_compromised', 'root_shell', 'su_attempted', 'num_root', 'num_file_creations', 'num_shells', 'num_access_files', 'num_outbound_cmds', 'is_host_login', 'is_guest_login', 'count', 'srv_count', 'serror_rate', 'srv_serror_rate', 'rerror_rate', 'srv_rerror_rate', 'same_srv_rate', 'diff_srv_rate', 'srv_diff_host_rate', 'dst_host_count', 'dst_host_srv_count', 'dst_host_same_srv_rate', 'dst_host_diff_srv_rate', 'dst_host_same_src_port_rate', 'dst_host_srv_diff_host_rate', 'dst_host_serror_rate', 'dst_host_srv_serror_rate', 'dst_host_rerror_rate', 'dst_host_srv_rerror_rate', 'type']

def transform_type(input_file, output_file):
    with open(output_file, "w") as text_file:
        with open(input_file) as f:
            lines = f.readlines()
            for line in lines:
                columns = line.split(',')
                for raw_type in category:
                    flag = False
                    if raw_type == columns[-1].replace("\n", ""):
                        str = ','.join(columns[0:attr_list.index('type')])
                        text_file.write("%s,%d\n" % (str, category[raw_type]))
                        flag = True
                        break
                if not flag:
                    text_file.write(line)
                    print(line)

transform_type("raw/kddcup.data_10_percent.txt", "data/kddcup.data_10_percent.txt")
transform_type("raw/corrected.txt", "data/corrected.txt")

1.2 數據儲存

爲了後面方便讀取、排序等操作,降低運行時間,我們把data目錄下的文件數據讀取並寫入Mongodb數據庫

  • 讀寫的過程中,注意將有些變量轉化int和float後再存儲,同時爲training_set表的type屬性增加索引
# -------------import data------------------
from pymongo import MongoClient

client = MongoClient('localhost', 27017)
db = client.test

def isfloat(value):
    try:
        float(value)
        return True
    except ValueError:
        return False

db.training_data.delete_many({})
db.training_data.create_index("training_set.type")
with open("data/kddcup.data_10_percent.txt") as f:
    lines = f.readlines()
    for line in lines:
        columns = line.split(',')
        dic = {}
        for attr in attr_list:
            element = columns[attr_list.index(attr)]
            if element.isdigit():
                element = int(element)
            elif isfloat(element):
                element = float(element)
            dic[attr] = element
        db.training_data.insert_one({"training_set": dic})

db.test_data.delete_many({})
with open("data/corrected.txt") as f:
    lines = f.readlines()
    for line in lines:
        columns = line.split(',')
        dic = {}
        for attr in attr_list:
            element = columns[attr_list.index(attr)]
            if element.isdigit():
                element = int(element)
            elif isfloat(element):
                element = float(element)
            dic[attr] = element
        db.test_data.insert_one({"test_set": dic})

2. Scikit決策樹分類

2.1. 加載數據

我們從MongoDb中取出所有的訓練集和測試集,由於決策樹對於有序的輸入能夠加速訓練,因此我們使用sort方法對訓練集中的type屬性進行排序,然後訓練集和測試集所有的輸入儲存在dataset裏,將所有的輸出目標儲存在datatarget,而且爲區分訓練集和測試集,我們用T_len記錄訓練集的大小

  • 注意:從MongoDb數據庫讀出的字符串爲unicode,因此需將其重新編碼爲ascii
# -------------Fetching data------------------
from pymongo import MongoClient
import pymongo
import numpy as np

client = MongoClient('localhost', 27017)
db = client.test

training_cursor = db.training_data.find({"training_set.src_bytes": {"$gt": 1000000}})
test_cursor = db.test_data.find({"test_set.src_bytes": {"$gt": 1000000}})

cursor = training_cursor.sort('training_set.type', pymongo.ASCENDING)
dataset = []
datatarget = []
for document in cursor:
    tmp = []
    for attr in attr_list:
        if attr is not 'type':
            try:
                tmp.append(document['training_set'][attr].encode('ascii'))
            except:
                tmp.append(document['training_set'][attr])
    dataset.append(tmp)
    dataTarget.append(int(document['training_set']['type']))

training_len = len(dataset)
for document in test_cursor:
    tmp = []
    for attr in attr_list:
        if attr is not 'type':
            try:
                tmp.append(document['test_set'][attr].encode('ascii'))
            except:
                tmp.append(document['test_set'][attr])
    dataset.append(tmp)
    dataTarget.append(int(document['test_set']['type']))

dataset = np.array(dataset)
datatarget = np.array(datatarget)
T_len = training_len

2.2. 類別變量重編碼

對於類別變量(Categorical Variable),例如:男和女、high和low等,這種字符串變量一般是不能直接輸入到算法模型中的,需要重編碼爲數字1,2,3,4等或者是二進制bitmap。

但是對於1,2,3,4這樣的有順序大小的標量,有些算法理解會出2大於1,即男大於女,而事實上男和女只代表是男或女,並沒有先後和大小關係,所以一般使用二進制編碼:男用0 1表示 、女用1 0表示。而這樣就增加了變量的個數,從一維變量sex增加到了兩維變量sex=male和sex=female,因此,若類別變量的種類很多,最後編碼後的變量維度會極大的增加,不利於計算,被稱爲維度災難

而對於決策樹算法來說,利用的是信息純度來分類,因此編碼爲1,2,3不會產生影響。所以我們將dataset中的三個類別變量(protocol_type, service, flag)重新編碼爲數字集合

# -------------Categorical variable encoding------------------
from sklearn import preprocessing

le_1 = preprocessing.LabelEncoder()
le_2 = preprocessing.LabelEncoder()
le_3 = preprocessing.LabelEncoder()

le_1.fit(np.unique(dataset[:, 1]))
le_2.fit(np.unique(dataset[:, 2]))
le_3.fit(np.unique(dataset[:, 3]))

dataset[:, 1] = le_1.transform(dataset[:, 1])
dataset[:, 2] = le_2.transform(dataset[:, 2])
dataset[:, 3] = le_3.transform(dataset[:, 3])

2.3. 特徵選取(feature selection)

一般的數據集的變量之間會出現一下幾種情況:

  1. 變量之間相關性太強,互相依賴嚴重,會導致有冗餘變量
  2. 變量中的數據極其稀疏,很大部分都是0或者無意義
  3. 變量維度太高,經過重編碼後變量維度達到了幾百個

以上三種情況都很不利對模型的訓練,情況2種能通過設置閾值來過濾一些稀疏的變量;情況1和3能通過主成分分析(PCA)提取主要因子進行降維,除此之外,還有屬性子集選擇方法:通過刪除不相關或者冗餘的屬性減少數據量,有逐步向前選擇、逐步向後選擇和決策樹歸納三種

經過前面類別變量的重編碼,dataset變量一共有42個維度,高維度的數據集在決策樹中的訓練很容易出現過擬合的情況,因此我們需要對變量進行篩選,也就是feature selection,一組好的feature能直接影響算法的精度,而Scikit已經提供了基於樹的屬性子集選擇方法,我們在這裏直接把數據輸入ExtraTreesClassifier訓練,然後用SelectFromModel提取訓練的模型,最後使用transform方法篩選出重要的特徵

  • 由於測試集的變量也需要篩選,但是篩選原則應該與訓練集一致,不然訓練集和測試集的input會不一樣,因此我們將這組feature的索引存在fea_index數組中
# -------------feature selection------------------
from sklearn.ensemble import ExtraTreesClassifier
from sklearn.feature_selection import SelectFromModel

data_set = dataset[0:(T_len - 1)]
data_target = datatarget[0:(T_len - 1)]

clf = ExtraTreesClassifier()
clf = clf.fit(data_set, data_target)
print clf.feature_importances_

model = SelectFromModel(clf, prefit=True)
feature_set = model.transform(data_set)

fea_index = []
for A_col in np.arange(data_set.shape[1]):
    for B_col in np.arange(feature_set.shape[1]):
        if (data_set[:, A_col] == feature_set[:, B_col]).all():
            fea_index.append(A_col)

Output:

{'count': '8', 'srv_serror_rate': '0.0', 'srv_count': '8', 'same_srv_rate': '1.0', 'dst_host_same_src_port_rate': '0.11', 'dst_host_srv_rerror_rate': '0.0', 'dst_host_srv_count': '9', 'dst_host_count': '9', 'logged_in': '1', 'protocol_type': '1'}

我們將fea_index對應的變量名和其中的某行數據打印出來,可以看出最後篩選出的feature:一共有11個,分別爲countsrv_serror_ratesrv_countsame_srv_ratedst_host_same_src_port_ratedst_host_srv_rerror_ratedst_host_srv_rerror_ratedst_host_srv_countdst_host_countlogged_inprotocol_type


2.4. 交叉驗證(一)

在訓練模型前,我們將訓練集的十分之一用來做交叉驗證,剩下數據用來做訓練,因此將訓練集分爲了兩部分:

  1. training_set、training_target:用來訓練模型
  2. test_set、test_target:用來交叉驗證
# -------------Cross Validation Split----------------
from sklearn import tree
import random
import pydotplus
from sklearn.externals import joblib

test_index = random.sample(range(0, len(feature_set) - 1), int(len(data_target) * 0.1))
training_index = list(set(range(0, len(feature_set) - 1)) - set(test_index))

training_set = feature_set[training_index]
training_target = data_target[training_index]

test_set = feature_set[test_index]
test_target = data_target[test_index]

Output:

training_set: (444617, 11)
training_target: (444617,)
test_set: (49401, 11)
test_target: (49401,)

這裏能可以看出訓練集有444617行,用於交叉驗證的訓練集集有49401行,兩者的變量都爲11維度


2.5. 訓練決策樹模型

決策樹是一種非參數的監督學習算法,常用的有ID3、C4.5和CART,ID3和C4.5基於信息熵entropy,CART基於gini不純度,而Scikit提供了一種優化版本的CART算法,封裝在DecisionTreeClassifier裏,有一些主要的參數:

  • criterion:節點分裂的準則,gini or entropy
  • max_depth:樹的最大深度,這個值過大會導致算法對訓練集的過擬合,而過小的值會妨礙算法對數據的學習,初始值推薦爲5,然後慢慢增大,觀察樹的形狀
  • min_samples_split:需要被分裂成子節點的最小樣本數,當樣本數小於這個值時,就直接標記爲葉節點而不用繼續生成子節點,值越大,樹的枝越少,達到一定的剪枝效果
  • class_weight:當輸入的樣本集類別數量相差很大時,樹最後的形狀會傾向朝大數據樣本的方向生成,導致樹的不平衡,因此可以設置各個類別的權重,數據量少的類權重大,數據量大的類權重小,能夠有效地將樹平衡

通過反覆嘗試,我們找到一組比較好的訓練參數:用信息entropy來分裂各個節點;min_samples_split=30;讓所有訓練樣本充分平衡,即輸入的各類元組數相等

# -------------Training Tree------------------
clf = tree.DecisionTreeClassifier(criterion="entropy", min_samples_split=30, class_weight="balanced")
clf = clf.fit(training_set, training_target)

class_names = np.unique([str(i) for i in training_target])
feature_names = [attr_list[i] for i in fea_index]

dot_data = tree.export_graphviz(clf, out_file=None,
                                feature_names=feature_names,
                                class_names=class_names,
                                filled=True, rounded=True,
                                special_characters=True)

graph = pydotplus.graph_from_dot_data(dot_data)
graph.write_pdf("output/tree-vis.pdf")
joblib.dump(clf, 'output/CART.pkl')
  • 訓練完模型後,我們可以把模型進行持久化在本地文件裏,下次就可以直接從文件讀取模型進行預測
  • 我們還可以對訓練的樹模型進行可視化,導出爲pdf文件,如:

這裏寫圖片描述

2.6. 交叉驗證(二)

我們對訓練集的十分之一(test_set)進行預測,獲得的預測值trained_target與實際值test_target進行對比,輸出混淆矩陣(confusion matrix)和分類的結果統計,用來驗證訓練出的模型對於有相同的概率分佈的數據集的有效性

# -------------Prediction of cross validation--------------
from sklearn.metrics import classification_report
from sklearn.metrics import confusion_matrix

clf = joblib.load('output/CART.pkl')
trained_target = clf.predict(test_set)

print confusion_matrix(test_target, trained_target, labels=[0, 1, 2, 3, 4])
print classification_report(test_target, trained_target)

Output:

1.混淆矩陣:

[ 9558    26    11    17    50]
 [    5   418     2     0     0]
 [  176    19 38992     3     4]
 [    0     0     0     7     1]
 [    4     0     0     6   102]]

混淆矩陣(confusion matrix)給出了分類器分類的最終結果,行表示實際的類,而列表示預測的類。矩陣對角線上的數字表明預測出來的與實際結果一樣的元組數,而對角線以外數字表示,預測的與實際結果不一致的元祖個數,如:176表示:有176個元組實際上屬於類別2(第2行),而卻被預測到了類別0(第0列)

2.模型精度:

             precision    recall  f1-score   support

          0       0.98      0.99      0.99      9662
          1       0.90      0.98      0.94       425
          2       1.00      0.99      1.00     39194
          3       0.21      0.88      0.34         8
          4       0.65      0.91      0.76       112

avg / total       0.99      0.99      0.99     49401

精度(precision)可以看作是精確性的度量(即標記爲正類的元祖實際爲正類所佔的百分比),而召回率(recall)是完全性的度量(即正元組標記爲正的百分比),而f1-score表示精度和召回率的調和均值,它能夠度量精度和召回率的組合(以相同的權重),support是這類元組的個數

可以看出各個類的數量比例差距很大,此交叉驗證輸入的屬於類別3的元組數只有3個


2.6. 測試

最後我們來測試訓練的模型是否能夠很好的預測測試數據,我們利用T_len從dataset中取出測試輸入test_data_set 和test_data_target,然後使用fea_index同樣將測試輸入提取子屬性(特徵),最終比較預測值test_trained_target和實際值test_data_target

# -------------Prediction of test dataset------------------
test_data_set = dataset[T_len:len(dataset)]
test_data_target = datatarget[T_len:len(dataset)]
test_feature_set = test_data_set[:, fea_index]

clf = joblib.load('output/CART.pkl')
test_trained_target = clf.predict(test_feature_set)

print confusion_matrix(test_data_target, test_trained_target, labels=[0, 1, 2, 3, 4])
print classification_report(test_data_target, test_trained_target)

Output:

1.混淆矩陣:

[[ 6294    38    15    10    11]
 [    5   800     4     0     0]
 [  191    20 41508     1     0]
 [    0     0     0     3     0]
 [ 1076     5     0    16     3]]

從混淆矩陣我們可以看出,對於類0、類1和類2,分類器還是能正常識別。對於類別3,只有3個元組被正常歸類;而對於類別4,也只有3個元組被正常歸類,但是有1076個元組實際上屬於類別4卻被分到了類別0

2.模型精度:

             precision    recall  f1-score   support

          0       0.83      0.99      0.90      6368
          1       0.93      0.99      0.96       809
          2       1.00      0.99      1.00     41720
          3       0.10      1.00      0.18         3
          4       0.21      0.00      0.01      1100

avg / total       0.96      0.97      0.96     50000

經過混淆矩陣的粗略分析,我們算出各個類的精度、召回率、F度量來描述分類器對每個類的分類情況,可以看出來,訓練出的決策樹模型對前三類有較好的效果

而對於類別3:

  • precision=0.1表示: 所有預測出來爲類3的元組中,預測正確的佔0.1,也就是真實也爲類3的佔0.1
  • recall=1表示:所有的真實爲類3的元組全部被正確分類到類3

3.總結

對比交叉驗證的模型精度和最後測試的的模型精度,我們可以看出訓練出來的決策樹模型能夠很好的預測具有相同概率分佈的數據集(十分之一的訓練集),而對於測試集,由於其概率分佈不同,對於類3和類4的分類效果並不是很好

總結一下對於決策樹的訓練的收穫:

  1. 輸入數據格式要正確轉換,對於類別變量要用數字編碼或者是二進制編碼(one-hot-encoding),取決於算法的要求
  2. 原始數據集需要選取合適的特徵,儘量選取主要的、非冗餘的屬性,遇到高維的數據要降維處理,最好用各種特徵選取的方法來嘗試,選一個效果最佳
  3. 決策樹訓練時深度從小到大,觀察樹的形狀,必要的時候需要設置樹的最大深度以防止出現模型對訓練數據的過擬合;對於樣本類別數量差異較大的要先設置各類的權重,不然小類別的數據很容易在樹的生成的過程中就被大類別的數據湮沒;訓練完的樹最好能進行後剪枝,將重複的枝剪掉,Scikit集成的決策樹還沒有支持這種剪枝功能,但是可以通過設置min_samples_split在生成樹的過程中進行一定的剪枝

所有的代碼位於https://github.com/PENGZhaoqing/kdd99-scikit,裏面除了Scikit決策樹算法的使用,對比了Scikit的多層感知機對這個數據集的處理,由於篇幅就不再講了,感興趣的可以自己去試試

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章