《Machine Learning in Action》—— hao朋友,快來玩啊淤堵,決策樹呦

《Machine Learning in Action》—— hao朋友寝衫,快來玩啊,決策樹呦

在上篇文章中拐邪,《Machine Learning in Action》—— Taoye給你講講決策樹到底是支什么“鬼”主要講述了決策樹的理論內(nèi)容慰毅,介紹了什么決策樹,以及生成決策樹時所需要優(yōu)先選取的三種決策標(biāo)準(zhǔn)扎阶。有學(xué)習(xí)的過SVM汹胃,或閱讀過Taoye之前寫的幾篇SVM內(nèi)容的文章可以發(fā)現(xiàn),決策樹相對于SVM來講要簡單很多乘陪,沒有太多且復(fù)雜的公式推導(dǎo)统台。

我們在把之前的內(nèi)容稍微回顧下:

  • 屬性特征的信息增益越高,按道理來講應(yīng)當(dāng)被優(yōu)先選取啡邑,常用于ID3算法
  • 屬性特征的增益率越高贱勃,按道理來講應(yīng)當(dāng)被優(yōu)先選取,常用與C4.5算法
  • 屬性特征的尼基指數(shù)低,按道理來講應(yīng)當(dāng)被優(yōu)先選取贵扰,常用于CART算法

有了前面的內(nèi)容做基礎(chǔ)仇穗,閱讀本篇文章就會很輕松了。本篇主要講述以下幾部分的內(nèi)容:

  • 基于ID3算法手動構(gòu)建決策樹戚绕,并通過Matplotlib進(jìn)行可視化
  • 基于已經(jīng)構(gòu)建好的決策樹進(jìn)行分類預(yù)測
  • 構(gòu)建好的決策樹模型應(yīng)當(dāng)如何保存和讀任谱?
  • 通過鳶尾花(Iris)數(shù)據(jù)集舞丛,使用Sklearn構(gòu)建決策樹耘子,

一、基于ID3算法手動構(gòu)建決策樹球切,并通過Matplotlib進(jìn)行可視化

構(gòu)建決策樹的算法有好幾種谷誓,比如像ID3、C4.5吨凑、CART之類的捍歪,限于篇幅、時間和精力的關(guān)系鸵钝,本篇文章主要采用ID3算法來進(jìn)行構(gòu)建糙臼,使用到的決策標(biāo)準(zhǔn)(指標(biāo))是上篇文章中所提到的信息增益。關(guān)于C4.5和CART算法構(gòu)建決策樹恩商,有興趣的讀者可以參考上期中的增益率和尼基指數(shù)的內(nèi)容变逃。

本次構(gòu)建決策樹所需要用到的數(shù)據(jù)集仍然是李航——《統(tǒng)計學(xué)習(xí)方法》中的貸款數(shù)據(jù),這里再次把數(shù)據(jù)集放出來瞅瞅:

image

前面也有提到痕届,ID3算法主要是基于信息增益作為選取屬性特征的準(zhǔn)則韧献,在上期我們也計算過各個屬性特征所對應(yīng)的信息增益值,如下:

\begin{aligned} & Gain(D, 年齡) = 0.083 \\ & Gain(D, 工作)=0.324 \\ & Gain(D, 房子)=0.420 \\ & Gain(D, 信用)=0.363 \end{aligned}

而根據(jù)ID3算法過程研叫,我們可以知道锤窑,需要優(yōu)先選取信息增益最大的屬性進(jìn)行決策,即房子嚷炉,也就是說將房子作為決策樹的根節(jié)點(diǎn)渊啰。由于房子這一個屬性特征有兩個取值,所以引申出兩個子節(jié)點(diǎn)申屹,一個對應(yīng)“有房子”绘证,另一個對應(yīng)“無房子”,而“有房子”的六個樣本的類別都是允許放款哗讥,也就是有同一類樣本點(diǎn)嚷那,所以它理應(yīng)成為一個葉子節(jié)點(diǎn),且節(jié)點(diǎn)的類不妨標(biāo)記為“允許”杆煞。

這樣一來魏宽,我們的根節(jié)點(diǎn)以及其中的一個葉子節(jié)點(diǎn)就確定了腐泻。接下來,我們需要將“無房子”所對應(yīng)的樣本集再次選取一個新的屬性特征進(jìn)行決策队询。注意:此次做決策的數(shù)據(jù)樣本總體就不再是初始數(shù)據(jù)了派桩,而是“無房子”所對應(yīng)的所有樣本,這一點(diǎn)需要格外注意蚌斩。

我們在“無房子的”的數(shù)據(jù)樣本中再次計算其他屬性所對應(yīng)的信息增益铆惑,我們不妨講此次的數(shù)據(jù)樣本集合記為D_1,計算結(jié)果如下:

\begin{aligned} & Gain(D_1, 年齡) = 0.251 \\ & Gain(D_1, 工作)=0.918 \\ & Gain(D_1, 信用)=0.474 \end{aligned}

與上同樣分析送膳,可以發(fā)現(xiàn)此時工作所對應(yīng)的信息增益值最高员魏,也就是說第二個優(yōu)先選取的屬性為“工作”。而且肠缨,我們可以發(fā)現(xiàn)逆趋,在數(shù)據(jù)樣本集D_2中,總共有9個晒奕,其中允許放款的有三個,拒絕的有6個名斟,且結(jié)果標(biāo)簽與工作值剛好完全都對應(yīng)上了脑慧,也就是說有工作的都允許放款了,沒工作的都拒絕放款了砰盐。所以闷袒,在第二個屬性特征選取完成之后,此時產(chǎn)生了倆個葉子節(jié)點(diǎn)岩梳,節(jié)點(diǎn)結(jié)果與是否有工作對應(yīng)囊骤。

通過如上分析,我們就得到了此次基于ID3算法所構(gòu)建出的決策樹冀值,決策樹如下:

<img src="https://gitee.com/tianxingjian123/my-images-repository/raw/master/img/jueceshu_7_.png" width="300">

接下來我們通過代碼來生成這顆決策樹也物,對于樹形結(jié)構(gòu)的數(shù)據(jù),我們可以通過字典或者說是json類型來進(jìn)行保存列疗。比如上圖中的決策樹滑蚯,我們可以通過如下結(jié)果來進(jìn)行表示:

{"房子": {
    "1": "Y",
    "0":{"工作": {
        "1": "Y",
        "0": "N"
    }}
}}

上述表示的數(shù)據(jù)格式我們一般稱其為Json,這個在前后端抵栈、爬蟲告材,亦或是在其他各種領(lǐng)域中都是接觸的非常多的。另外古劲,我們可以發(fā)現(xiàn)在決策樹生成的過程中斥赋,在一個屬性特征選取完成之后,需要經(jīng)過同樣的操作再次選取一個屬性特征产艾,其實(shí)就相當(dāng)于一個周期疤剑,換句話講正好滿足了遞歸的特性滑绒,只是我們的數(shù)據(jù)總體發(fā)生了變化而已。既然我們明確了保存樹形結(jié)構(gòu)數(shù)據(jù)所需要的類型骚露,下面我們通過代碼來實(shí)現(xiàn):

此次的代碼相較于上篇文章中計算信息增益的變化主要有三個地方:

  • 由于我們最終需要生成屬性的具體內(nèi)容蹬挤,而非僅僅索引,所以我們在創(chuàng)建數(shù)據(jù)的時候棘幸,除了返回數(shù)據(jù)本身之外焰扳,還需要返回對應(yīng)的屬性,修改establish_data方法如下所示:
"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:創(chuàng)建訓(xùn)數(shù)據(jù)集
"""
def establish_data():
    data = [[0, 0, 0, 0, 'N'],         # 樣本數(shù)據(jù)集相關(guān)信息误续,前面幾項代表屬性特征吨悍,最后一項表示是否放款
            [0, 0, 0, 1, 'N'],
            [0, 1, 0, 1, 'Y'],
            [0, 1, 1, 0, 'Y'],
            [0, 0, 0, 0, 'N'],
            [1, 0, 0, 0, 'N'],
            [1, 0, 0, 1, 'N'],
            [1, 1, 1, 1, 'Y'],
            [1, 0, 1, 2, 'Y'],
            [1, 0, 1, 2, 'Y'],
            [2, 0, 1, 2, 'Y'],
            [2, 0, 1, 1, 'Y'],
            [2, 1, 0, 1, 'Y'],
            [2, 1, 0, 2, 'Y'],
            [2, 0, 0, 0, 'N']]
    labels = ["年紀(jì)", "工作", "房子", "信用"]
    return np.array(data), labels
  • 在上篇文章中,我們的handle_data方法僅僅是找出對應(yīng)屬性特征值的樣本蹋嵌,比如找出所有年紀(jì)為青年的樣本數(shù)據(jù)集育瓜。而要想構(gòu)建決策樹,在第一次選取了屬性之后栽烂,應(yīng)當(dāng)將該屬性從數(shù)據(jù)集中移除躏仇,所以修改handle_data如下:
"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:找出對應(yīng)屬性特征值的樣本,比如找出所有年紀(jì)為青年的樣本數(shù)據(jù)集
"""
def handle_data(data, axis, value):
    result_data = list()
    for item in data:
        if item[axis] == value:
            reduced_data = item[: axis].tolist()
            reduced_data.extend(item[axis + 1:])
            result_data.append(reduced_data)
    return result_data
  • 第三個就是需要一個創(chuàng)建決策樹的方法establish_decision_tree腺办,具體代碼如下焰手,其中在選取完成一個屬性之后需要遞歸調(diào)用本身來選取第二個屬性
"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:創(chuàng)建決策樹
"""
def establish_decision_tree(data, labels, feat_labels):
    cat_list = [item[-1] for item in data]
    if (cat_list.count(cat_list[0]) == len(cat_list)): return cat_list[0]   # 數(shù)據(jù)集中的類別只有一種
    best_feature_index = calc_information_gain(data)    # 通過信息增益優(yōu)先選取最好的屬性特征
    best_label = labels[best_feature_index]   # 屬性特征對應(yīng)的標(biāo)簽內(nèi)容
    # feat_labels表示已選取的屬性;新建一個決策樹節(jié)點(diǎn)怀喉;將屬性標(biāo)簽列表中刪除已選取的屬性
    feat_labels.append(best_label); decision_tree = {best_label: dict()}; del(labels[best_feature_index])
    feature_values = [item[best_feature_index] for item in data]
    unique_values = set(feature_values)      # 獲取最優(yōu)屬性對應(yīng)值的set集合
    for value in unique_values:
        sub_label = labels[:]
        decision_tree[best_label][value] = establish_decision_tree(np.array(handle_data(data, best_feature_index, value)), sub_label, feat_labels)
    return decision_tree

該部分的完整代碼如下所示:

import numpy as np
import pandas as pd

np.__version__
pd.__version__

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:創(chuàng)建訓(xùn)數(shù)據(jù)集
"""
def establish_data():
    data = [[0, 0, 0, 0, 'N'],         # 樣本數(shù)據(jù)集相關(guān)信息书妻,前面幾項代表屬性特征,最后一項表示是否放款
            [0, 0, 0, 1, 'N'],
            [0, 1, 0, 1, 'Y'],
            [0, 1, 1, 0, 'Y'],
            [0, 0, 0, 0, 'N'],
            [1, 0, 0, 0, 'N'],
            [1, 0, 0, 1, 'N'],
            [1, 1, 1, 1, 'Y'],
            [1, 0, 1, 2, 'Y'],
            [1, 0, 1, 2, 'Y'],
            [2, 0, 1, 2, 'Y'],
            [2, 0, 1, 1, 'Y'],
            [2, 1, 0, 1, 'Y'],
            [2, 1, 0, 2, 'Y'],
            [2, 0, 0, 0, 'N']]
    labels = ["年紀(jì)", "工作", "房子", "信用"]
    return np.array(data), labels

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:計算信息熵
"""
def calc_information_entropy(data):
    data_number, _ = data.shape
    information_entropy = 0
    for item in pd.DataFrame(data).groupby(_ - 1):
        proportion = item[1].shape[0] / data_number
        information_entropy += - proportion * np.log2(proportion)
    return information_entropy

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:找出對應(yīng)屬性特征值的樣本躬拢,比如找出所有年紀(jì)為青年的樣本數(shù)據(jù)集
"""
def handle_data(data, axis, value):
    result_data = list()
    for item in data:
        if item[axis] == value:
            reduced_data = item[: axis].tolist()
            reduced_data.extend(item[axis + 1:])
            result_data.append(reduced_data)
    return result_data

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:計算最大的信息增益躲履,并輸出其所對應(yīng)的特征索引
"""
def calc_information_gain(data):
    feature_number = data.shape[1] - 1                    # 屬性特征的數(shù)量
    base_entropy = calc_information_entropy(data)                 # 計算總體數(shù)據(jù)集的信息熵
    max_information_gain, best_feature = 0.0, -1                 # 初始化最大信息增益和對應(yīng)的特征索引
    for index in range(feature_number):
        feat_list = [item[index] for item in data]
        feat_set = set(feat_list)
        new_entropy = 0.0
        for set_item in feat_set:                         # 計算屬性特征劃分后的信息增益
            sub_data = handle_data(data, index, set_item)
            proportion = len(sub_data) / float(data.shape[0])           # 計算子集的比例
            new_entropy += proportion * calc_information_entropy(np.array(sub_data))
        temp_information_gain = base_entropy - new_entropy                     # 計算信息增益
        print("第%d個屬性特征所對應(yīng)的的增益為%.3f" % (index + 1, temp_information_gain))            # 輸出每個特征的信息增益
        if (temp_information_gain > max_information_gain):
            max_information_gain, best_feature = temp_information_gain, index       # 更新信息增益,確定的最大的信息增益對應(yīng)的索引
    return best_feature

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:創(chuàng)建決策樹
"""
def establish_decision_tree(data, labels, feat_labels):
    cat_list = [item[-1] for item in data]
    if (cat_list.count(cat_list[0]) == len(cat_list)): return cat_list[0]   # 數(shù)據(jù)集中的類別只有一種
    best_feature_index = calc_information_gain(data)    # 通過信息增益優(yōu)先選取最好的屬性特征
    best_label = labels[best_feature_index]   # 屬性特征對應(yīng)的標(biāo)簽內(nèi)容
    # feat_labels表示已選取的屬性聊闯;新建一個決策樹節(jié)點(diǎn)工猜;將屬性標(biāo)簽列表中刪除已選取的屬性
    feat_labels.append(best_label); decision_tree = {best_label: dict()}; del(labels[best_feature_index])
    feature_values = [item[best_feature_index] for item in data]
    unique_values = set(feature_values)      # 獲取最優(yōu)屬性對應(yīng)值的set集合
    for value in unique_values:
        sub_label = labels[:]
        decision_tree[best_label][value] = establish_decision_tree(np.array(handle_data(data, best_feature_index, value)), sub_label, feat_labels)
    return decision_tree

if __name__ == "__main__":
    data, labels = establish_data()
    print(establish_decision_tree(data, labels, list()))

運(yùn)行結(jié)果:

{'房子': {'1': 'Y', '0': {'工作': {'1': 'Y', '0': 'N'}}}}

可見,代碼運(yùn)行的結(jié)果與我們手動創(chuàng)建的決策樹如出一轍馅袁,完美域慷,哈哈哈~~~

可是,如上決策樹的顯示未免有點(diǎn)太不親民了汗销,生成的決策樹比較簡單那還好點(diǎn)犹褒,假如我們生成的決策樹比較復(fù)雜,那通過Json格式的數(shù)據(jù)來輸出決策樹就有點(diǎn)懵了弛针。

對此叠骑,我們需要將決策樹進(jìn)行可視化,可視化主要使用到了Matplotlib包削茁。關(guān)于Matplotlib的使用大家可以參考文檔及其他資料宙枷,Taoye后期也會整理出一篇自己常用的接口掉房。

import numpy as np
import pandas as pd

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:創(chuàng)建訓(xùn)數(shù)據(jù)集
"""
def establish_data():
    data = [[0, 0, 0, 0, 'N'],         # 樣本數(shù)據(jù)集相關(guān)信息,前面幾項代表屬性特征慰丛,最后一項表示是否放款
            [0, 0, 0, 1, 'N'],
            [0, 1, 0, 1, 'Y'],
            [0, 1, 1, 0, 'Y'],
            [0, 0, 0, 0, 'N'],
            [1, 0, 0, 0, 'N'],
            [1, 0, 0, 1, 'N'],
            [1, 1, 1, 1, 'Y'],
            [1, 0, 1, 2, 'Y'],
            [1, 0, 1, 2, 'Y'],
            [2, 0, 1, 2, 'Y'],
            [2, 0, 1, 1, 'Y'],
            [2, 1, 0, 1, 'Y'],
            [2, 1, 0, 2, 'Y'],
            [2, 0, 0, 0, 'N']]
    labels = ["年紀(jì)", "工作", "房子", "信用"]
    return np.array(data), labels

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:計算信息熵
"""
def calc_information_entropy(data):
    data_number, _ = data.shape
    information_entropy = 0
    for item in pd.DataFrame(data).groupby(_ - 1):
        proportion = item[1].shape[0] / data_number
        information_entropy += - proportion * np.log2(proportion)
    return information_entropy

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:找出對應(yīng)屬性特征值的樣本卓囚,比如找出所有年紀(jì)為青年的樣本數(shù)據(jù)集
"""
def handle_data(data, axis, value):
    result_data = list()
    for item in data:
        if item[axis] == value:
            reduced_data = item[: axis].tolist()
            reduced_data.extend(item[axis + 1:])
            result_data.append(reduced_data)
    return result_data

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:計算最大的信息增益,并輸出其所對應(yīng)的特征索引
"""
def calc_information_gain(data):
    feature_number = data.shape[1] - 1                    # 屬性特征的數(shù)量
    base_entropy = calc_information_entropy(data)                 # 計算總體數(shù)據(jù)集的信息熵
    max_information_gain, best_feature = 0.0, -1                 # 初始化最大信息增益和對應(yīng)的特征索引
    for index in range(feature_number):
        feat_list = [item[index] for item in data]
        feat_set = set(feat_list)
        new_entropy = 0.0
        for set_item in feat_set:                         # 計算屬性特征劃分后的信息增益
            sub_data = handle_data(data, index, set_item)
            proportion = len(sub_data) / float(data.shape[0])           # 計算子集的比例
            new_entropy += proportion * calc_information_entropy(np.array(sub_data))
        temp_information_gain = base_entropy - new_entropy                     # 計算信息增益
        print("第%d個屬性特征所對應(yīng)的的增益為%.3f" % (index + 1, temp_information_gain))            # 輸出每個特征的信息增益
        if (temp_information_gain > max_information_gain):
            max_information_gain, best_feature = temp_information_gain, index       # 更新信息增益诅病,確定的最大的信息增益對應(yīng)的索引
    return best_feature

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:創(chuàng)建決策樹
"""
def establish_decision_tree(data, labels, feat_labels):
    cat_list = [item[-1] for item in data]
    if (cat_list.count(cat_list[0]) == len(cat_list)): return cat_list[0]   # 數(shù)據(jù)集中的類別只有一種
    best_feature_index = calc_information_gain(data)    # 通過信息增益優(yōu)先選取最好的屬性特征
    best_label = labels[best_feature_index]   # 屬性特征對應(yīng)的標(biāo)簽內(nèi)容
    # feat_labels表示已選取的屬性哪亿;新建一個決策樹節(jié)點(diǎn);將屬性標(biāo)簽列表中刪除已選取的屬性
    feat_labels.append(best_label); decision_tree = {best_label: dict()}; del(labels[best_feature_index])
    feature_values = [item[best_feature_index] for item in data]
    unique_values = set(feature_values)      # 獲取最優(yōu)屬性對應(yīng)值的set集合
    for value in unique_values:
        sub_label = labels[:]
        decision_tree[best_label][value] = establish_decision_tree(np.array(handle_data(data, best_feature_index, value)), sub_label, feat_labels)
    return decision_tree
"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:統(tǒng)計決策樹當(dāng)中的葉子節(jié)點(diǎn)數(shù)目贤笆,以及決策樹的深度
"""
def get_leaf_number_and_tree_depth(decision_tree):
    leaf_number, first_key, tree_depth = 0, next(iter(decision_tree)), 0; second_dict = decision_tree[first_key]
    for key in second_dict.keys():
        if type(second_dict.get(key)).__name__ == "dict":
            temp_number, temp_depth = get_leaf_number_and_tree_depth(second_dict[key])
            leaf_number, curr_depth = leaf_number + temp_number, 1 + temp_depth
        else: leaf_number += 1; curr_depth = 1
        if curr_depth > tree_depth: tree_depth = curr_depth
    return leaf_number, tree_depth

from matplotlib.font_manager import FontProperties

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:繪制節(jié)點(diǎn)
"""
def plot_node(node_text, center_pt, parent_pt, node_type):
    arrow_args = dict(arrowstyle = "<-")
    font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=14)    # 設(shè)置字體
    create_plot.ax1.annotate(node_text, xy=parent_pt,  xycoords='axes fraction',
                            xytext=center_pt, textcoords='axes fraction',
                            va="center", ha="center", bbox=node_type, arrowprops=arrow_args, FontProperties=font)

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:標(biāo)注有向邊的值
"""
def tag_text(cntr_pt, parent_pt, node_text):
    x_mid = (parent_pt[0] - cntr_pt[0]) / 2.0 + cntr_pt[0]
    y_mid = (parent_pt[1] - cntr_pt[1]) / 2.0 + cntr_pt[1]
    create_plot.ax1.text(x_mid, y_mid, node_text, va="center", ha="center", rotation=30)
"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:繪制決策樹
"""
def plot_tree(decision_tree, parent_pt, node_text):
    decision_node = dict(boxstyle="sawtooth", fc="0.8")
    leaf_node = dict(boxstyle = "round4", fc = "0.8")
    leaf_number, tree_depth = get_leaf_number_and_tree_depth(decision_tree)
    first_key = next(iter(decision_tree))
    cntr_pt = (plot_tree.xOff + (1.0 + float(leaf_number)) / 2.0 / plot_tree.totalW, plot_tree.yOff)
    tag_text(cntr_pt, parent_pt, node_text); plot_node(first_key, cntr_pt, parent_pt, decision_node)
    second_dict = decision_tree[first_key]
    plot_tree.yOff = plot_tree.yOff - 1.0 / plot_tree.totalD
    for key in second_dict.keys():
        if type(second_dict[key]).__name__ == 'dict': plot_tree(second_dict[key], cntr_pt, str(key))
        else:
            plot_tree.xOff = plot_tree.xOff + 1.0 / plot_tree.totalW
            plot_node(second_dict[key], (plot_tree.xOff, plot_tree.yOff), cntr_pt, leaf_node)
            tag_text((plot_tree.xOff, plot_tree.yOff), cntr_pt, str(key))
    plot_tree.yOff = plot_tree.yOff + 1.0 / plot_tree.totalD

from matplotlib import pyplot as plt
"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:創(chuàng)建決策樹
"""
def create_plot(in_tree):
    fig = plt.figure(1, facecolor = "white")
    fig.clf()
    axprops = dict(xticks = [], yticks = [])
    create_plot.ax1 = plt.subplot(111, frameon = False, **axprops)
    leaf_number, tree_depth = get_leaf_number_and_tree_depth(in_tree)
    plot_tree.totalW, plot_tree.totalD = float(leaf_number), float(tree_depth)
    plot_tree.xOff = -0.5 / plot_tree.totalW; plot_tree.yOff = 1.0
    plot_tree(in_tree, (0.5,1.0), '')
    plt.show()
    
if __name__ == "__main__":
    data, labels = establish_data()
    decision_tree = establish_decision_tree(data, labels, list())
    print(decision_tree)
    print("決策樹的葉子節(jié)點(diǎn)數(shù)和深度:", get_leaf_number_and_tree_depth(decision_tree))
    create_plot(decision_tree)

手動可視化決策樹的結(jié)果如下所示:

image

實(shí)話實(shí)說蝇棉,通過Matplotlib手動對決策樹進(jìn)行可視化,對于之前沒什么經(jīng)驗的碼手來講確實(shí)有點(diǎn)不友好芥永。上述代碼能看懂就行篡殷,沒必要死揪著不放,后面的話會介紹通過Graphviz來繪制決策樹埋涧。這里對上方的代碼做個簡短的說明:

  • get_leaf_number_and_tree_depth主要用于統(tǒng)計決策樹當(dāng)中的葉子節(jié)點(diǎn)數(shù)目板辽,以及決策樹的深度。選取key對應(yīng)的value棘催,判斷value是否為一個字典類型戳气,否的話說明是一個葉子節(jié)點(diǎn),是的話說明非葉子節(jié)點(diǎn)巧鸭,不同情況做不同處理
  • plot_node方法用于繪制節(jié)點(diǎn),這里設(shè)置了font類型是在windows下的麻捻,如果是linux則需要額外設(shè)置
  • tag_text用于標(biāo)注有向邊的屬性值纲仍,在這里主要是用1和0來進(jìn)行標(biāo)注,1代表對屬性的肯定贸毕,0表示否定
  • plot_tree遍歷繪制決策樹郑叠,在這里需要調(diào)用前面所定義的幾個方法

二、基于已經(jīng)構(gòu)建好的決策樹進(jìn)行分類預(yù)測

依靠訓(xùn)練數(shù)據(jù)構(gòu)造了決策樹之后明棍,我們既可以通過該決策樹模型應(yīng)用于實(shí)際數(shù)據(jù)來進(jìn)行分類乡革。在對數(shù)據(jù)進(jìn)行分類時,需要使用決策樹以及用于構(gòu)造決策樹的標(biāo)簽向量摊腋;然后沸版,程序比較測試數(shù)據(jù)與決策樹上的數(shù)值,遞歸執(zhí)行該過程直到進(jìn)入到葉子節(jié)點(diǎn)兴蒸;最后將測試數(shù)據(jù)定義為葉子節(jié)點(diǎn)所屬的類型视粮。——《機(jī)器學(xué)習(xí)實(shí)戰(zhàn)》

在已經(jīng)獲取到?jīng)Q策樹模型的前提下對測試數(shù)據(jù)進(jìn)行分類還是挺好理解的

對此橙凳,我們定義一個classify方法來進(jìn)行分類:

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:通過決策樹模型對測試數(shù)據(jù)進(jìn)行分類
""" 
def classify(decision_tree, best_feature_labels, test_data):
    first_node = next(iter(decision_tree))
    second_dict = decision_tree[first_node]
    feat_index = best_feature_labels.index(first_node)
    for key in second_dict.keys():
        if int(test_data[feat_index]) == int(key):
            if type(second_dict[key]).__name__ == "dict":   # 為字典說明還沒到葉子節(jié)點(diǎn)
                result_label = classify(second_dict[key], best_feature_labels, test_data)
            else: result_label = second_dict[key]
    return result_label

我們分別對四個數(shù)據(jù)樣本進(jìn)行測試蕾殴,樣本分別是(有房子笑撞,沒工作),(有房子钓觉,有工作)茴肥,(沒房子,沒工作)荡灾,(沒房子瓤狐,有工作),使用列表表示分別是[1, 0], [1, 1], [0, 0], [0, 1]卧晓,運(yùn)行結(jié)果如下:

image

可見芬首,四組數(shù)據(jù)都能夠分類成功。

三逼裆、構(gòu)建好的決策樹模型應(yīng)當(dāng)如何保存和讀扔羯浴?

構(gòu)建好決策樹之后胜宇,我們要想保存該模型就可以通過pickle模塊來進(jìn)行耀怜。

保存好模型之后,下次使用該模型就不需要的再次訓(xùn)練了桐愉,只需要加載模型即可财破。保存和加載模型的示例代碼如下(挺簡單的,就不多說了):

import pickle
with open("DecisionTreeModel.txt", "wb") as f:
    pickle.dump(decision_tree, f)           # 保存決策樹模型

f = open("DecisionTreeModel.txt", "rb")
decision_tree = pickle.load(f)             # 加載決策樹模型

四从诲、通過鳶尾花(Iris)數(shù)據(jù)集左痢,使用Sklearn構(gòu)建決策樹

現(xiàn)在我們通過sklearn來實(shí)現(xiàn)一個小案例,數(shù)據(jù)集采用的是機(jī)器學(xué)習(xí)中比較常用的鳶尾花(Iris)數(shù)據(jù)集系洛。更多其他關(guān)于決策樹分類的案例俊性,大家可以去sklearn.tree.DecisionTreeClassifier的文檔中學(xué)習(xí):https://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html

image

在sklearn中實(shí)現(xiàn)決策樹分類主要用到的接口是sklearn.tree.DecisionTreeClassifier,這個主要是通過數(shù)據(jù)樣本集構(gòu)建一個決策樹模型描扯。此外萝究,如果我們要想將決策樹可視化哩都,還需要使用到export_graphviz冤馏。當(dāng)然了内列,在sklearn.tree`下還有其他接口可供大家調(diào)用,這里不做的過多介紹了恩够,讀者可自行學(xué)習(xí)卒落。

關(guān)于sklearn.tree.DecisionTreeClassifier的使用,可參考:https://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html玫鸟。其中內(nèi)置了很多的參數(shù)导绷,這里主要記錄8個參數(shù),也方便后期回顧屎飘,其他的有機(jī)會用到再查找資料:

  • criterion:屬性選取的標(biāo)準(zhǔn)妥曲,默認(rèn)采用的是gini贾费,也可以自行選擇entropygini是基尼值檐盟,entropy是信息熵褂萧,這兩個我們在上篇文章中已經(jīng)講到過了
  • splitter:特征劃分節(jié)點(diǎn)的選擇標(biāo)準(zhǔn),默認(rèn)是best葵萎,可以設(shè)置為random导犹。默認(rèn)的"best"適合樣本量不大的時候,而如果樣本數(shù)據(jù)量非常大羡忘,此時決策樹構(gòu)建推薦"random"谎痢。
  • max_depth:決策樹最大深度,默認(rèn)是None卷雕。需要注意一點(diǎn)的是节猿,該深度是不包含根節(jié)點(diǎn)的。一般來說漫雕,數(shù)據(jù)少或者特征少的時候可以不管這個值滨嘱。如果模型樣本量多,特征也多的情況下浸间,推薦限制這個最大深度太雨,具體的取值取決于數(shù)據(jù)的分布。
  • max_features:劃分時考慮的最大特征數(shù)魁蒜,默認(rèn)是None囊扳。一般來說,如果樣本特征數(shù)不多兜看,比如小于50宪拥,我們用默認(rèn)的"None"就可以了,如果特征數(shù)非常多铣减,我們可以靈活使用其他取值來控制劃分時考慮的最大特征數(shù),以控制決策樹的生成時間脚作。用到的時候查看下文檔即可葫哗。
  • min_samples_split:內(nèi)部節(jié)點(diǎn)再劃分所需最小樣本數(shù),默認(rèn)為2球涛。意思就是說劣针,比如我們某個屬性對應(yīng)樣本數(shù)目小于min_samples_split,即使它滿足優(yōu)先選取條件亿扁,依然會被剔除掉捺典。
  • min_samples_leaf:葉子節(jié)點(diǎn)最少樣本數(shù),默認(rèn)是1从祝。這個值限制了葉子節(jié)點(diǎn)最少的樣本數(shù)襟己,如果某葉子節(jié)點(diǎn)數(shù)目小于樣本數(shù)引谜,則會和兄弟節(jié)點(diǎn)一起被剪枝。葉結(jié)點(diǎn)需要最少的樣本數(shù)擎浴,也就是最后到葉結(jié)點(diǎn)员咽,需要多少個樣本才能算一個葉結(jié)點(diǎn)。如果設(shè)置為1贮预,哪怕這個類別只有1個樣本贝室,決策樹也會構(gòu)建出來。
  • max_leaf_nodes:最大葉子節(jié)點(diǎn)數(shù)仿吞,默認(rèn)是None滑频。通過限制最大葉子節(jié)點(diǎn)數(shù),可以防止過擬合唤冈。如果加了限制峡迷,算法會建立在最大葉子節(jié)點(diǎn)數(shù)內(nèi)最優(yōu)的決策樹。如果特征不多务傲,可以不考慮這個值凉当,但是如果特征分成多的話,可以加以限制售葡,具體的值可以通過交叉驗證得到看杭。
  • random_state:隨機(jī)數(shù)種子,默認(rèn)是None挟伙。如果沒有設(shè)置隨機(jī)數(shù)楼雹,隨機(jī)出來的數(shù)與當(dāng)前系統(tǒng)時間有關(guān),每個時刻都是不同的尖阔。如果設(shè)置了隨機(jī)數(shù)種子贮缅,那么相同隨機(jī)數(shù)種子,不同時刻產(chǎn)生的隨機(jī)數(shù)也是相同的介却。

此外谴供,在DecisionTreeClassifier下也有很多方法可供調(diào)用,詳情可參考文檔進(jìn)行使用齿坷,如下:

image

接下來桂肌,我們就用sklearn來對鳶尾花數(shù)據(jù)集進(jìn)行分類吧。參考資料:https://scikit-learn.org/stable/auto_examples/tree/plot_iris_dtc.html#sphx-glr-auto-examples-tree-plot-iris-dtc-py

構(gòu)建決策樹本身的代碼并不難永淌,主要在于可視化崎场,其中涉及到了Matplotlib的不少操作,以增強(qiáng)可視化效果遂蛀,完整代碼如下:

import numpy as np
import matplotlib.pyplot as plt

from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier, plot_tree

class IrisDecisionTree:
    """
        Explain:屬性的初始化
        Parameters:
            n_classes: 鳶尾花的類別數(shù)
            plot_colors: 不同類別花的顏色
            plot_step: meshgrid網(wǎng)格的步長
    """
    def __init__(self, n_classes, plot_colors, plot_step):
        self.n_classes = n_classes
        self.plot_colors = plot_colors
        self.plot_step = plot_step
    
    """
        Explain: 通過load_iris構(gòu)建數(shù)據(jù)集
    """
    def establish_data(self):
        iris_info = load_iris()
        return iris_info.data, iris_info.target, iris_info.feature_names, iris_info.target_names
    
    """
        Explain:分類的可視化
    """
    def show_result(self, x_data, y_label, feature_names, target_names):
        # 選取兩個屬性來構(gòu)建決策樹谭跨,以方便可視化,其中列表內(nèi)部元素代表屬性對應(yīng)的索引
        for index, pair in enumerate([[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3]]):
            sub_x_data, sub_y_label = x_data[:, pair], y_label
            clf = DecisionTreeClassifier().fit(sub_x_data, sub_y_label)   # 選取兩個屬性構(gòu)建決策樹
            plt.subplot(2, 3, index + 1)
            x_min, x_max = sub_x_data[:, 0].min() - 1, sub_x_data[:, 0].max() + 1   # 第一個屬性
            y_min, y_max = sub_x_data[:, 1].min() - 1, sub_x_data[:, 1].max() + 1   # 第二個屬性
            xx, yy = np.meshgrid(np.arange(x_min, x_max, self.plot_step), np.arange(y_min, y_max, self.plot_step))
            plt.tight_layout(h_pad=0.5, w_pad=0.5, pad=2.5)
            Z = clf.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)    # 預(yù)測meshgrid內(nèi)部每個元素的分類
            cs = plt.contourf(xx, yy, Z, cmap = plt.cm.RdYlBu)   # 繪制帶有顏色的網(wǎng)格圖
            plt.xlabel(feature_names[pair[0]]); plt.ylabel(feature_names[pair[1]])     # 標(biāo)注坐標(biāo)軸標(biāo)簽
            for i, color in zip(range(self.n_classes), self.plot_colors):
                idx = np.where(sub_y_label == i)
                plt.scatter(sub_x_data[idx, 0], sub_x_data[idx, 1], c=color, label=target_names[i],
                            cmap=plt.cm.RdYlBu, edgecolor='black', s=15)    # 繪制數(shù)據(jù)樣本集的散點(diǎn)圖
        
        from matplotlib.font_manager import FontProperties
        font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=14)    # 定義中文字體
        plt.suptitle("通過決策樹對鳶尾花數(shù)據(jù)進(jìn)行可視化", fontproperties=font)
        plt.legend(loc='lower right', borderpad=0, handletextpad=0)
        plt.axis("tight")
        plt.figure()
        clf = DecisionTreeClassifier().fit(x_data, y_label)     # 針對鳶尾花數(shù)據(jù)集的多重屬性來構(gòu)建決策樹
        plot_tree(clf, filled=True)
        plt.show()
    
if __name__ == "__main__":
    iris_decision_tree = IrisDecisionTree(3, "ryb", 0.02)
    x_data, y_label, feature_names, target_names = iris_decision_tree.establish_data()
    iris_decision_tree.show_result(x_data, y_label, feature_names, target_names)

運(yùn)行結(jié)果如下所示:

image

通過可視化結(jié)果,我們可以發(fā)現(xiàn)主要有兩個結(jié)果螃宙,我們分別對其進(jìn)行說明下:于鳶尾花數(shù)據(jù)集來講蛮瞄,總共有四種屬性特征以及三種標(biāo)簽結(jié)果。為了方便可視化污呼,第一張圖只選取了兩個屬性來構(gòu)建決策樹裕坊,四種屬性,選兩個燕酷,很簡單籍凝,學(xué)過排列組合的都應(yīng)該知道有C_4^2=6種可能,所第一張圖中的每張子圖分別對應(yīng)一種可能苗缩。且顏色不同代表不同的分類饵蒂,假如數(shù)據(jù)集顏色與網(wǎng)格內(nèi)部顏色一致,則說明分類正確酱讶。因此退盯,從直觀上來看,選取sepal length和petal length這兩種屬性構(gòu)建的決策樹相對較好泻肯。而第二張圖是針對數(shù)據(jù)集中的所有屬性特征來構(gòu)建決策樹渊迁。具體的結(jié)果可自行運(yùn)行上方代碼查看(由于設(shè)置了font字體,所以上方代碼需在windows下運(yùn)行)

我們可以發(fā)現(xiàn)灶挟,上面代碼可視化決策樹的時候采用的是sklearn.tree.plot_tree琉朽,前面我們在講解通過Matplotlib繪制決策樹的時候也有說到,使用graphviz亦可可視化決策樹稚铣,下面我們不妨來看看吧箱叁!

graphviz不能采用pip進(jìn)行安裝,采用anaconda安裝的時候也會很慢惕医,甚至多次嘗試都可能安裝失敗耕漱,前幾天幫同學(xué)安裝就出現(xiàn)這種情況(windows下是這樣的,linux環(huán)境下會很方便)抬伺,所以這里我們采用直接通過whl文件來安裝螟够。

建議:對于使用Python有過一段時間的Pyer來講,都會經(jīng)常安裝一些第三方模塊峡钓,有些可以直接通過pip或者conda完美的解決齐鲤,而有些在安裝的過程中會遇到各種不明所以的錯誤。所以椒楣,對于在安裝過程中遇到錯誤的讀者不妨嘗試通過whl文件進(jìn)行安裝,whl目標(biāo)地址:https://www.lfd.uci.edu/~gohlke/pythonlibs/#wordcloud牡肉,其中整合了各種Python模塊的whl文件捧灰。

打開上述url地址 --> ctrl + f搜索graphviz --> 下載需要的graphviz安裝文件

image

在本地目標(biāo)路徑中執(zhí)行安裝即可:

pip install graphviz?0.15?py3?none?any.whl

此外對于windows來將 ,還需要前往官網(wǎng)安裝graphviz的exe文件,然后將bin目錄添加到環(huán)境變量即可毛俏。exe文件的下載地址:https://graphviz.org/download/

如果是Linux用戶炭庙,那就比較方便了,直接通過命令安裝即可:

$ sudo apt install graphviz         # Ubuntu
$ sudo apt install graphviz         # Debian

至此煌寇,Graphviz就已經(jīng)安裝好了焕蹄。我們通過它來實(shí)現(xiàn)決策樹的可視化吧,在IrisDecisionTree下添加如下show_result_by_graphviz方法:

"""
    Explain:通過graphviz實(shí)現(xiàn)決策樹的可視化
"""
def show_result_by_graphviz(self, x_data, y_label):
    clf = DecisionTreeClassifier().fit(x_data, y_label)
    iris_dot_data = tree.export_graphviz(clf, out_file=None, 
                                         feature_names=iris.feature_names,  
                                         class_names=iris.target_names,  
                                         filled=True, rounded=True,  
                                         special_characters=True) 
    import graphviz
    graph = graphviz.Source(iris_dot_data); graph.render("iris")

運(yùn)行之后會在當(dāng)前目錄下生成一個pdf文件阀溶,其中就是可視化之后的決策樹腻脏。注意:以上只是實(shí)現(xiàn)簡單的鳶尾花的決策樹分類,讀者可通過調(diào)解DecisionTreeClassifier的參數(shù)構(gòu)建不同的決策樹银锻,以此來判別各個決策樹的優(yōu)劣永品。

以上就是本篇全部內(nèi)容了,決策樹的相關(guān)內(nèi)容暫時就更新到這了击纬,其他內(nèi)容像過擬合鼎姐、剪枝等等以后有時間再更新,下期的機(jī)器學(xué)習(xí)系列文章就是肝SVM的非線性模型了更振。

就不嘮嗑了~~~

我是Taoye炕桨,愛專研,愛分享肯腕,熱衷于各種技術(shù)献宫,學(xué)習(xí)之余喜歡下象棋、聽音樂乎芳、聊動漫遵蚜,希望借此一畝三分地記錄自己的成長過程以及生活點(diǎn)滴,也希望能結(jié)實(shí)更多志同道合的圈內(nèi)朋友奈惑,更多內(nèi)容歡迎來訪微信公主號:玩世不恭的Coder吭净。

參考資料:

<font style="font-size:13px;opacity:0.7">[1] 《機(jī)器學(xué)習(xí)實(shí)戰(zhàn)》:Peter Harrington 人民郵電出版社</font>
<font style="font-size:13px;opacity:0.7">[2] 《統(tǒng)計學(xué)習(xí)方法》:李航 第二版 清華大學(xué)出版社</font>
<font style="font-size:13px;opacity:0.7">[3] 《機(jī)器學(xué)習(xí)》:周志華 清華大學(xué)出版社</font>
<font style="font-size:13px;opacity:0.7">[4] Python Extension Packages:https://www.lfd.uci.edu/~gohlke/pythonlibs/#wordcloud</font>
<font style="font-size:13px;opacity:0.7">[5] sklearn.tree.DecisionTreeClassifier:https://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html</font>
<font style="font-size:13px;opacity:0.7">[6] Graphviz官網(wǎng):https://graphviz.org/</font>

推薦閱讀

《Machine Learning in Action》—— Taoye給你講講決策樹到底是支什么“鬼”
《Machine Learning in Action》—— 剖析支持向量機(jī),優(yōu)化SMO
《Machine Learning in Action》—— 剖析支持向量機(jī)肴甸,單手狂撕線性SVM
print( "Hello寂殉,NumPy!" )
干啥啥不行原在,吃飯第一名
Taoye滲透到一家黑平臺總部友扰,背后的真相細(xì)思極恐
《大話數(shù)據(jù)庫》-SQL語句執(zhí)行時,底層究竟做了什么小動作庶柿?
那些年村怪,我們玩過的Git,真香
基于Ubuntu+Python+Tensorflow+Jupyter notebook搭建深度學(xué)習(xí)環(huán)境
網(wǎng)絡(luò)爬蟲之頁面花式解析
手握手帶你了解Docker容器技術(shù)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末浮庐,一起剝皮案震驚了整個濱河市甚负,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖梭域,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件斑举,死亡現(xiàn)場離奇詭異,居然都是意外死亡病涨,警方通過查閱死者的電腦和手機(jī)富玷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來既穆,“玉大人赎懦,你說我怎么就攤上這事⊙ィ” “怎么了铲敛?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長会钝。 經(jīng)常有香客問我伐蒋,道長,這世上最難降的妖魔是什么迁酸? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任先鱼,我火速辦了婚禮,結(jié)果婚禮上奸鬓,老公的妹妹穿的比我還像新娘焙畔。我一直安慰自己,他們只是感情好串远,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布宏多。 她就那樣靜靜地躺著,像睡著了一般澡罚。 火紅的嫁衣襯著肌膚如雪伸但。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天留搔,我揣著相機(jī)與錄音更胖,去河邊找鬼。 笑死隔显,一個胖子當(dāng)著我的面吹牛却妨,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播括眠,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼彪标,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了掷豺?” 一聲冷哼從身側(cè)響起捞烟,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤账锹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后坷襟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡生年,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年婴程,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片抱婉。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡档叔,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蒸绩,到底是詐尸還是另有隱情衙四,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布患亿,位于F島的核電站传蹈,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏步藕。R本人自食惡果不足惜惦界,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望咙冗。 院中可真熱鬧沾歪,春花似錦、人聲如沸雾消。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽立润。三九已至狂窑,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間范删,已是汗流浹背蕾域。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留到旦,地道東北人旨巷。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像添忘,于是被迫代替她去往敵國和親采呐。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容