机器学习本科课程 实验3 决策树处理分类任务

实验3.1 决策树处理分类任务

  1. 使用sklearn.tree.DecisionTreeClassifier完成肿瘤分类(breast-cancer)
  2. 计算最大深度为10时,十折交叉验证的精度(accuracy),查准率(precision),查全率(recall),F1值
  3. 绘制最大深度从1到10的决策树十折交叉验证精度的变化图

1. 读取数据

import numpy as np
import pandas as pd
data = pd.read_csv('breast-cancer.csv')
print(data.shape)
data.head()
data = data.values 
data_x = data[:,2:-1]
data_y = data[:,1:2]
data_y = np.reshape(data_y,(-1))


print(data_x.shape)
print(data_y.shape)

2. 导入模型

from sklearn.model_selection import cross_val_predict
from sklearn.metrics import accuracy_score
from sklearn.metrics import precision_score
from sklearn.metrics import recall_score
from sklearn.metrics import f1_score
from sklearn.tree import DecisionTreeClassifier

3. 训练与预测

计算最大深度为10的决策树,在使用数据data_x,标记data_y下,十折交叉验证的精度,查准率,查全率和F1值

model = DecisionTreeClassifier(max_depth = 10) # 参数max_depth决定了决策树的最大深度


prediction = cross_val_predict(model,data_x,data_y,cv = 10)
 
acc1 = accuracy_score(data_y,prediction)
precision1 = precision_score(data_y,prediction,average="macro")
recall1 = recall_score(data_y,prediction,average="macro")
f1 = f1_score(data_y,prediction,average="macro")

print("决策树在data_测试集上的四项指标")
print("精度:",acc1)
print("查准率:",precision1)
print("查全率:",recall1)
print("f1值:",f1)

4. 改变最大深度,绘制决策树的精度变换图

绘制最大深度从1到10,决策树十折交叉验证精度的变化图

import matplotlib.pyplot as plt
%matplotlib inline

y = []

for i in range(10):
    model = DecisionTreeClassifier(max_depth = i + 1)
    prediction = cross_val_predict(model,data_x,data_y,cv=10)
    y.append(prediction)
    
x = np.linspace(1,10,10)
test = [accuracy_score(data_y, val) for val in y]

plt.figure()
plt.plot(x,test,'-')
plt.title("DecisionTree's accuracy_score changes with the max_depth")
plt.xlabel("max_depth")    
plt.ylabel("accuracy_score")

5. 通过调整参数,得到一个泛化能力最好的模型

查看决策树文档,通过调整决策树的参数,得到一个最好的模型
http://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html#sklearn.tree.DecisionTreeClassifier
并在下方给出参数的设定与其泛化性能指标
机器学习本科课程 实验3 决策树处理分类任务_第1张图片

criterion:用于衡量特征选择质量的准则。可以是"gini"(基尼系数)或"entropy"(信息增益)。
max_depth:决策树的最大深度。控制树的复杂度和过拟合的风险。
min_samples_split:拆分内部节点所需的最小样本数。
min_samples_leaf:叶子节点所需的最小样本数。
max_features:寻找最佳分割时要考虑的特征数量。
random_state:控制随机性的种子值。

使用的GridSearchCV,它存在的意义就是自动调参,只要把参数输进去,就能给出最优化的结果和参数。
但是这个方法适合于小数据集,一旦数据的量级上去了,很难得出结果。

from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import StratifiedKFold
modelfit = DecisionTreeClassifier(max_depth = 10)
param_grid = {'criterion':['gini','entropy'],'max_depth':[10,11,12],
                  'min_samples_leaf':[1,2,3,4,5],'max_features':[1,2,3,4,5],'min_samples_split':[2,3,4,5]}

grid = GridSearchCV(modelfit,param_grid,cv = 10)

grid.fit(data_x,data_y)

best = grid.best_params_  #最优分类器
print(best)
best_decision_tree_classifier = DecisionTreeClassifier(max_depth = best['max_depth'],max_features=best['max_features'],
                                                       min_samples_leaf = best['min_samples_leaf'],min_samples_split = best['min_samples_split'])
# your code
prediction11 = cross_val_predict(model, data_x, data_y, cv=10)
acc11 = accuracy_score(data_y, prediction)
precision11 = precision_score(data_y, prediction, average="macro")
recall11 = recall_score(data_y, prediction, average="macro")
f1_11 = f1_score(data_y, prediction, average="macro")
print("-------------------")
print("精度:", acc11)
print("查准率:", precision11)
print("查全率:", recall11)
print("f1值:", f1_11)
双击此处填写优化后的决策树参数设置与性能指标的结果

参数设置:

划分标准-基尼系数; 最大深度-10; 最大特征数-5; 叶子节点最少样本数-5; 内部节点再划分所需最小样本数-3;

性能指标得分:

  • 精度: 0.9104

  • 查准率: 0.9033

  • 查全率: 0.9056

  • f1值: 0.9044

实验3.2决策树处理回归任务

  1. 使用sklearn.tree.DecisionTreeRegressor完成kaggle房价预测问题
  2. 计算最大深度为10的决策树,训练集上十折交叉验证的MAE和RMSE
  3. 绘制最大深度从1到30,决策树在训练集和测试集上MAE的变化曲线
  4. 选择一个合理的树的最大深度,并给出理由

1. 读取数据

import pandas as pd
data = pd.read_csv('train.csv')
# 丢弃有缺失值的特征(列)
data.dropna(axis = 1, inplace = True)

# 只保留整数的特征
data = data[[col for col in data.dtypes.index if data.dtypes[col] == 'int64']]

2. 数据集划分

70%做训练集,30%做测试集

from sklearn.utils import shuffle
data_shuffled = shuffle(data, random_state = 32)
split_line = int(len(data_shuffled) * 0.7)
training_data = data_shuffled[:split_line]
testing_data = data_shuffled[split_line:]

3. 导入模型

from sklearn.model_selection import cross_val_predict
from sklearn.metrics import mean_absolute_error
from sklearn.metrics import mean_squared_error
from sklearn.tree import DecisionTreeRegressor
import numpy as np

4. 选取特征和标记

features = data.columns.tolist()
target = 'SalePrice'
features.remove(target)

5. 训练与预测

请你在下面计算树的最大深度为10时,使用训练集全量特征训练的决策树的十折交叉验证的MAE和RMSE

# YOUR CODE HERE
# training_data[features]
# training_data[target]
model12 = DecisionTreeRegressor(max_depth = 10)
model12.fit(training_data[features], training_data[target])
predictions12 = model12.predict(testing_data[features])

mae12 = mean_absolute_error(testing_data[target], predictions12)
mse12 = mean_squared_error(testing_data[target], predictions12)
rmse12 = np.sqrt(mse12)

print("Mean Absolute Error:", mae12)
print("Mean Squared Error:", mse12)
print("Root Mean Squared Error:", rmse12)

6. 改变最大深度,绘制决策树的精度变换图

绘制最大深度从1到30,决策树训练集和测试集MAE的变化图

import matplotlib.pyplot as plt
%matplotlib inline
plt.style.use("fivethirtyeight")
# YOUR CODE HERE
y12 = []

for i in range(30):
    model12 =  DecisionTreeRegressor(max_depth = i+1)
    model12.fit(training_data[features], training_data[target])
    predictions12 = model12.predict(testing_data[features])
    
    mae12 = mean_absolute_error(testing_data[target], predictions12)
    mse12 = mean_squared_error(testing_data[target], predictions12)
    rmse12 = np.sqrt(mse12)
    y12.append(mae12)

    print('----------------------')
    print("max_depth: ", i+1)
    print("Mean Absolute Error:", mae12)
    print("Mean Squared Error:", mse12)
    print("Root Mean Squared Error:", rmse12)

x12 = np.linspace(1, 30, 30)

plt.figure()
plt.plot(x12, y12, '-')
plt.title("DecisionTree's MAE changes with the max_depth")
plt.xlabel("max_depth")
plt.ylabel("MAE")

7. 请你选择一个合理的树的最大深度,并给出理由

请你选择一个合理的树的最大深度,并给出理由

根据走势图, 我认为选择最大深度为6比较合适, 当最大深度到达6附近, MAE接近全局最小值, 而当最大深度增大时, 对模型的性能没有明显的增益, 甚至增大一定程度后会造成MAE值的波动, 因此在效率和精度的双重考虑下选择最大深度为6

实验3.3实现决策树

使用LendingClub Safe Loans数据集:

  1. 实现信息增益、信息增益率、基尼指数三种划分标准
  2. 使用给定的训练集完成三种决策树的训练过程
  3. 计算三种决策树在最大深度为10时在训练集和测试集上的精度,查准率,查全率,F1值
  4. 画出决策树(选作)

在这部分,我们会实现一个很简单的二叉决策树

1. 读取数据

# 导入类库
import pandas as pd
import numpy as np
import json
# 导入数据
loans = pd.read_csv('lending-club-data.csv', low_memory=False)

数据中有两列是我们想预测的指标,一项是safe_loans,一项是bad_loans,分别表示正例和负例,我们对其进行处理,将正例的safe_loans设为1,负例设为-1,删除bad_loans这列

# 对数据进行预处理,将safe_loans作为标记
loans['safe_loans'] = loans['bad_loans'].apply(lambda x : +1 if x==0 else -1)
del loans['bad_loans']

我们只使用grade, term, home_ownership, emp_length这四列作为特征,safe_loans作为标记,只保留loans中的这五列

features = ['grade',              # grade of the loan
            'term',               # the term of the loan
            'home_ownership',     # home_ownership status: own, mortgage or rent
            'emp_length',         # number of years of employment
           ]
target = 'safe_loans'
loans = loans[features + [target]]

2. 划分训练集和测试集

from sklearn.utils import shuffle
loans = shuffle(loans, random_state = 34)

split_line = int(len(loans) * 0.6)
train_data = loans.iloc[: split_line]
test_data = loans.iloc[split_line:]

3. 特征预处理

可以看到所有的特征都是离散类型的特征,需要对数据进行预处理,使用one-hot编码对其进行处理。
one-hot编码的思想就是将离散特征变成向量,假设特征 A A A有三种取值 { a , b , c } \{a, b, c\} {a,b,c},这三种取值等价,如果我们使用1,2,3三个数字表示这三种取值,那么在计算时就会产生偏差,有一些涉及距离度量的算法会认为,2和1离得近,3和1离得远,但这三个值应该是等价的,这种表示方法会造成模型在判断上出现偏差。解决方案就是使用一个三维向量表示他们,用 [ 1 , 0 , 0 ] [1, 0, 0] [1,0,0]表示a, [ 0 , 1 , 0 ] [0, 1, 0] [0,1,0]表示b, [ 0 , 0 , 1 ] [0, 0, 1] [0,0,1]表示c,这样三个向量之间的距离就都是相等的了,任意两个向量在欧式空间的距离都是 2 \sqrt{2} 2 。这就是one-hot编码是思想。
pandas中使用get_dummies生成one-hot向量

def one_hot_encoding(data, features_categorical):
    '''
    Parameter
    ----------
    data: pd.DataFrame
    
    features_categorical: list(str)
    '''
    
    # 对所有的离散特征遍历
    for cat in features_categorical:
        
        # 对这列进行one-hot编码,前缀为这个变量名
        one_encoding = pd.get_dummies(data[cat], prefix = cat)
        
        # 将生成的one-hot编码与之前的dataframe拼接起来
        data = pd.concat([data, one_encoding],axis=1)
        
        # 删除掉原始的这列离散特征
        del data[cat]
    
    return data

首先对训练集生成one-hot向量,然后对测试集生成one-hot向量,这里需要注意的是,如果训练集中,特征 A A A的取值为 { a , b , c } \{a, b, c\} {a,b,c},这样我们生成的特征就有三列,分别为 A _ a A\_a A_a, A _ b A\_b A_b, A _ c A\_c A_c,然后我们使用这个训练集训练模型,模型就就会考虑这三个特征,在测试集中如果有一个样本的特征 A A A的值为 d d d,那它的 A _ a A\_a A_a A _ b A\_b A_b A _ c A\_c A_c就都为0,我们不去考虑 A _ d A\_d A_d,因为这个特征在训练模型的时候是不存在的。

train_data = one_hot_encoding(train_data, features)
train_data.head()
one_hot_features = train_data.columns.tolist()
one_hot_features.remove(target)
one_hot_features

接下来是对测试集进行one_hot编码,但只要保留出现在one_hot_features中的特征即可

test_data_tmp = one_hot_encoding(test_data, features)
# 创建一个空的DataFrame
test_data = pd.DataFrame(columns = train_data.columns)
for feature in train_data.columns:
    # 如果训练集中当前特征在test_data_tmp中出现了,将其复制到test_data中
    if feature in test_data_tmp.columns:
        test_data[feature] = test_data_tmp[feature].copy()
    else:
        # 否则就用全为0的列去替代
        test_data[feature] = np.zeros(test_data_tmp.shape[0], dtype = 'uint8')
        
test_data.head()

处理完后,所有的特征都是0和1,标记是1和-1,以上就是数据预处理流程

4. 实现3种特征划分准则

决策树中有很多常用的特征划分方法,比如信息增益、信息增益率、基尼指数

我们需要实现一个函数,它的作用是,给定决策树的某个结点内的所有样本的标记,让它计算出对应划分指标的值是多少

接下来我们会实现上述三种划分指标

这里我们约定,将所有特征取值为0的样本,划分到左子树,特征取值为1的样本,划分到右子树

4.1 信息增益

信息熵:
E n t ( D ) = − ∑ k = 1 ∣ Y ∣ p k l o g 2 p k \mathrm{Ent}(D) = - \sum^{\vert \mathcal{Y} \vert}_{k = 1} p_k \mathrm{log}_2 p_k Ent(D)=k=1Ypklog2pk

信息增益:
G a i n ( D , a ) = E n t ( D ) − ∑ v = 1 V ∣ D v ∣ ∣ D ∣ E n t ( D v ) \mathrm{Gain}(D, a) = \mathrm{Ent}(D) - \sum^{V}_{v=1} \frac{\vert D^v \vert}{\vert D \vert} \mathrm{Ent}(D^v) Gain(D,a)=Ent(D)v=1VDDvEnt(Dv)

计算信息熵时约定:若 p = 0 p = 0 p=0,则 p log ⁡ 2 p = 0 p \log_2p = 0 plog2p=0

def information_entropy(labels_in_node):
    '''
    求当前结点的信息熵
    
    Parameter
    ----------
    labels_in_node: np.ndarray, 如[-1, 1, -1, 1, 1]
    
    Returns
    ----------
    float: information entropy
    '''
    # 统计样本总个数
    num_of_samples = labels_in_node.shape[0]
    
    if num_of_samples == 0:
        return 0
    
    # 统计出标记为1的个数
    num_of_positive = len(labels_in_node[labels_in_node == 1])
    
    # 统计出标记为-1的个数
    num_of_negative = len(labels_in_node[labels_in_node == -1])
    
    # 统计正例的概率
    prob_positive = num_of_positive / num_of_samples
    
    # 统计负例的概率
    prob_negative = num_of_negative / num_of_samples
    
    if prob_positive == 0:
        positive_part = 0
    else:
        positive_part = prob_positive * np.log2(prob_positive)
    
    if prob_negative == 0:
        negative_part = 0
    else:
        negative_part = prob_negative * np.log2(prob_negative)
    
    return - ( positive_part + negative_part )

下面是6个测试样例

# 信息熵测试样例1
example_labels = np.array([-1, -1, 1, 1, 1])
print(information_entropy(example_labels)) # 0.97095

# 信息熵测试样例2
example_labels = np.array([-1, -1, 1, 1, 1, 1, 1])
print(information_entropy(example_labels)) # 0.86312
    
# 信息熵测试样例3
example_labels = np.array([-1, -1, -1, -1, -1, 1, 1])
print(information_entropy(example_labels)) # 0.86312

# 信息熵测试样例4
example_labels = np.array([-1] * 9 + [1] * 8)
print(information_entropy(example_labels)) # 0.99750

# 信息熵测试样例5
example_labels = np.array([1] * 8)
print(information_entropy(example_labels)) # 0

# 信息熵测试样例6
example_labels = np.array([])
print(information_entropy(example_labels)) # 0

接下来完成计算所有特征的信息增益的函数
需要填写三个部分

def compute_information_gains(data, features, target, annotate = False):
    '''
    计算所有特征的信息增益
    
    Parameter
    ----------
        data: pd.DataFrame,传入的样本,带有特征和标记的dataframe
        
        features: list(str),特征名组成的list
        
        target: str, 标记(label)的名字
        
        annotate, boolean,是否打印所有特征的信息增益值,默认为False
        
    Returns
    ----------
        information_gains: dict, key: str, 特征名
                                 value: float,信息增益
    '''
    
    # 我们将每个特征划分的信息增益值存储在一个dict中
    # 键是特征名,值是信息增益值
    information_gains = dict()
    
    # 对所有的特征进行遍历,使用信息增益对每个特征进行计算
    for feature in features:
        
        # 左子树保证所有的样本的这个特征取值为0
        left_split_target = data[data[feature] == 0][target]
        
        # 右子树保证所有的样本的这个特征取值为1
        right_split_target =  data[data[feature] == 1][target]
            
        # 计算左子树的信息熵
        left_entropy = information_entropy(left_split_target)
        
        # 计算左子树的权重
        left_weight = len(left_split_target) / (len(left_split_target) + len(right_split_target))

        # 计算右子树的信息熵
        right_entropy = information_entropy(right_split_target)
        
        # 计算右子树的权重
        right_weight = len(right_split_target) / (len(left_split_target) + len(right_split_target))
        
        # 计算当前结点的信息熵
        current_entropy = information_entropy(data[target])
            
        # 计算使用当前特征划分的信息增益
        gain = current_entropy - (left_weight * left_entropy + right_weight * right_entropy)

        
        # 将特征名与增益值以键值对的形式存储在information_gains中
        information_gains[feature] = gain
        
        if annotate:
            print(" ", feature, gain)
            
    return information_gains
# 信息增益测试样例1
print(compute_information_gains(train_data, one_hot_features, target)['grade_A']) # 0.01759

# 信息增益测试样例2
print(compute_information_gains(train_data, one_hot_features, target)['term_ 60 months']) # 0.01429

# 信息增益测试样例3
print(compute_information_gains(train_data, one_hot_features, target)['grade_B']) # 0.00370

4.2 信息增益率

信息增益率:

G a i n _ r a t i o ( D , a ) = G a i n ( D , a ) I V ( a ) \mathrm{Gain\_ratio}(D, a) = \frac{\mathrm{Gain}(D, a)}{\mathrm{IV}(a)} Gain_ratio(D,a)=IV(a)Gain(D,a)

其中

I V ( a ) = − ∑ v = 1 V ∣ D v ∣ ∣ D ∣ log ⁡ 2 ∣ D v ∣ ∣ D ∣ \mathrm{IV}(a) = - \sum^V_{v=1} \frac{\vert D^v \vert}{\vert D \vert} \log_2 \frac{\vert D^v \vert}{\vert D \vert} IV(a)=v=1VDDvlog2DDv
完成计算所有特征信息增益率的函数
这里要完成五个部分

def compute_information_gain_ratios(data, features, target, annotate = False):
    '''
    计算所有特征的信息增益率并保存起来
    
    Parameter
    ----------
    data: pd.DataFrame, 带有特征和标记的数据
    
    features: list(str),特征名组成的list
    
    target: str, 特征的名字
    
    annotate: boolean, default False,是否打印注释
    
    Returns
    ----------
    gain_ratios: dict, key: str, 特征名
                       value: float,信息增益率
    '''
    
    gain_ratios = dict()
    
    # 对所有的特征进行遍历,使用当前的划分方法对每个特征进行计算
    for feature in features:
        
        # 左子树保证所有的样本的这个特征取值为0
        left_split_target = data[data[feature] == 0][target]
        
        # 右子树保证所有的样本的这个特征取值为1
        right_split_target =  data[data[feature] == 1][target]
            
        # 计算左子树的信息熵
        left_entropy = information_entropy(left_split_target)
        
        # 计算左子树的权重
        left_weight = len(left_split_target) / (len(left_split_target) + len(right_split_target))

        # 计算右子树的信息熵
        right_entropy = information_entropy(right_split_target)
        
        # 计算右子树的权重
        right_weight = len(right_split_target) / (len(left_split_target) + len(right_split_target))
        
        # 计算当前结点的信息熵
        current_entropy = information_entropy(data[target])
        
        # 计算当前结点的信息增益
        
        gain = current_entropy - (left_weight * left_entropy + right_weight * right_entropy)
        
        # 计算IV公式中,当前特征为0的值
        if left_weight == 0:
            left_IV = 0
        else:
            left_IV = - (left_weight * np.log2(left_weight))
        
        # 计算IV公式中,当前特征为1的值
        if right_weight == 0:
            right_IV = 0
        else:
            right_IV = - (right_weight * np.log2(right_weight))
        
        # IV 等于所有子树IV之和的相反数
        IV = - (left_IV + right_IV)
            
        # 计算使用当前特征划分的信息增益率
        # 这里为了防止IV是0,导致除法得到np.inf(无穷),在分母加了一个很小的小数
        gain_ratio = gain / (IV + np.finfo(np.longdouble).eps)
        
        # 信息增益率的存储
        gain_ratios[feature] = gain_ratio
        
        if annotate:
            print(" ", feature, gain_ratio)
            
    return gain_ratios
# 信息增益率测试样例1
print(compute_information_gain_ratios(train_data, one_hot_features, target)['grade_A']) # 0.02573

# 信息增益率测试样例2
print(compute_information_gain_ratios(train_data, one_hot_features, target)['grade_B']) # 0.00417

# 信息增益率测试样例3
print(compute_information_gain_ratios(train_data, one_hot_features, target)['term_ 60 months']) # 0.01970

4.3 基尼指数

数据集 D D D的基尼值:

G i n i ( D ) = ∑ k = 1 ∣ Y ∣ ∑ k ′ ≠ k p k p k ′ = 1 − ∑ k = 1 ∣ Y ∣ p k 2 . \begin{aligned} \mathrm{Gini}(D) & = \sum^{\vert \mathcal{Y} \vert}_{k=1} \sum_{k' \neq k} p_k p_{k'}\\ & = 1 - \sum^{\vert \mathcal{Y} \vert}_{k=1} p^2_k. \end{aligned} Gini(D)=k=1Yk=kpkpk=1k=1Ypk2.

属性 a a a的基尼指数:

G i n i _ i n d e x ( D , a ) = ∑ v = 1 V ∣ D v ∣ ∣ D ∣ G i n i ( D v ) \mathrm{Gini\_index}(D, a) = \sum^V_{v = 1} \frac{\vert D^v \vert}{\vert D \vert} \mathrm{Gini}(D^v) Gini_index(D,a)=v=1VDDvGini(Dv)
完成数据集基尼值的计算
这里需要填写三部分

def gini(labels_in_node):
    '''
    计算一个结点内样本的基尼指数
    
    Paramters
    ----------
    label_in_data: np.ndarray, 样本的标记,如[-1, -1, 1, 1, 1]
    
    Returns
    ---------
    gini: float,基尼指数
    '''
    
    # 统计样本总个数
    num_of_samples = labels_in_node.shape[0]
    
    if num_of_samples == 0:
        return 0
    
    # 统计出1的个数
    num_of_positive = len(labels_in_node[labels_in_node == 1])
    
    # 统计出-1的个数
    num_of_negative = len(labels_in_node[labels_in_node == -1])
    
    # 统计正例的概率
    prob_positive = num_of_positive / num_of_samples
    
    # 统计负例的概率
    prob_negative = num_of_negative / num_of_samples
    
    # 计算基尼值
    gini = 1 - (prob_positive ** 2 + prob_negative ** 2)
    
    return gini
# 基尼值测试样例1
example_labels = np.array([-1, -1, 1, 1, 1])
print(gini(example_labels)) # 0.48

# 基尼值测试样例2
example_labels = np.array([-1, -1, 1, 1, 1, 1, 1])
print(gini(example_labels)) # 0.40816
    
# 基尼值测试样例3
example_labels = np.array([-1, -1, -1, -1, -1, 1, 1])
print(gini(example_labels)) # 0.40816

# 基尼值测试样例4
example_labels = np.array([-1] * 9 + [1] * 8)
print(gini(example_labels)) # 0.49827

# 基尼值测试样例5
example_labels = np.array([1] * 8)
print(gini(example_labels)) # 0

# 基尼值测试样例6
example_labels = np.array([])
print(gini(example_labels)) # 0

然后计算所有特征的基尼指数
这里需要填写三部分

def compute_gini_indices(data, features, target, annotate = False):
    '''
    计算使用各个特征进行划分时,各特征的基尼指数
    
    Parameter
    ----------
    data: pd.DataFrame, 带有特征和标记的数据
    
    features: list(str),特征名组成的list
    
    target: str, 特征的名字
    
    annotate: boolean, default False,是否打印注释
    
    Returns
    ----------
    gini_indices: dict, key: str, 特征名
                       value: float,基尼指数
    '''
    
    gini_indices = dict()
    # 对所有的特征进行遍历,使用当前的划分方法对每个特征进行计算
    for feature in features:
        # 左子树保证所有的样本的这个特征取值为0
        left_split_target = data[data[feature] == 0][target]
        
        # 右子树保证所有的样本的这个特征取值为1
        right_split_target =  data[data[feature] == 1][target]
            
        # 计算左子树的基尼值
        left_gini = gini(left_split_target.values)
        
        # 计算左子树的权重
        left_weight = len(left_split_target) / (len(left_split_target) + len(right_split_target))

        # 计算右子树的基尼值
        right_gini = gini(right_split_target.values)
        
        # 计算右子树的权重
        right_weight = len(right_split_target) / (len(left_split_target) + len(right_split_target))
        
        # 计算当前结点的基尼指数
        gini_index = left_weight * left_gini + right_weight * right_gini
        
        # 存储
        gini_indices[feature] = gini_index
        
        if annotate:
            print(" ", feature, gini_index)
            
    return gini_indices
# 基尼指数测试样例1
print(compute_gini_indices(train_data, one_hot_features, target)['grade_A']) # 0.30095

# 基尼指数测试样例2
print(compute_gini_indices(train_data, one_hot_features, target)['grade_B']) # 0.30568

# 基尼指数测试样例3
print(compute_gini_indices(train_data, one_hot_features, target)['term_ 36 months']) # 0.30055

5. 完成最优特征的选择

到此,我们完成了三种划分策略的实现,接下来就是完成获取最优特征的函数
这里需要填写三个部分

def best_splitting_feature(data, features, target, criterion = 'gini', annotate = False):
    '''
    给定划分方法和数据,找到最优的划分特征
    
    Parameters
    ----------
    data: pd.DataFrame, 带有特征和标记的数据
    
    features: list(str),特征名组成的list
    
    target: str, 特征的名字
    
    criterion: str, 使用哪种指标,三种选项: 'information_gain', 'gain_ratio', 'gini'
    
    annotate: boolean, default False,是否打印注释
    
    Returns
    ----------
    best_feature: str, 最佳的划分特征的名字
    
    '''
    if criterion == 'information_gain':
        if annotate:
            print('using information gain')
        
        # 得到当前所有特征的信息增益
        information_gains = compute_information_gains(data, features, target, annotate)
    
        # information_gains是一个dict类型的对象,我们要找值最大的那个元素的键是谁
        # 根据这些特征和他们的信息增益,找到最佳的划分特征
        # YOUR CODE HERE
        best_feature = max(information_gains, key=information_gains.get)
        return best_feature

    elif criterion == 'gain_ratio':
        if annotate:
            print('using information gain ratio')
        
        # 得到当前所有特征的信息增益率
        gain_ratios = compute_information_gain_ratios(data, features, target, annotate)
    
        # 根据这些特征和他们的信息增益率,找到最佳的划分特征
        # YOUR CODE HERE
        best_feature = max(gain_ratios,key=gain_ratios.get)

        return best_feature
    
    elif criterion == 'gini':
        if annotate:
            print('using gini')
        
        # 得到当前所有特征的基尼指数
        gini_indices = compute_gini_indices(data, features, target, annotate)
        
        # 根据这些特征和他们的基尼指数,找到最佳的划分特征
        # YOUR CODE HERE
        best_feature = min(gini_indices, key=gini_indices.get)
        return best_feature
    else:
        raise Exception("传入的criterion不合规!", criterion)

6. 判断结点内样本的类别是否为同一类

def intermediate_node_num_mistakes(labels_in_node):
    '''
    求树的结点中,样本数少的那个类的样本有多少,比如输入是[1, 1, -1, -1, 1],返回2
    
    Parameter
    ----------
    labels_in_node: np.ndarray, pd.Series
    
    Returns
    ----------
    int:个数
    
    '''
    # 如果传入的array为空,返回0
    if len(labels_in_node) == 0:
        return 0
    
    # 统计1的个数
    # YOUR CODE HERE
    num_of_one = np.sum(labels_in_node == 1)
    
    # 统计-1的个数
    # YOUR CODE HERE
    num_of_minus_one = np.sum(labels_in_node == -1)
    
    return num_of_one if num_of_minus_one > num_of_one else num_of_minus_one
# 测试样例1
print(intermediate_node_num_mistakes(np.array([1, 1, -1, -1, -1]))) # 2

# 测试样例2
print(intermediate_node_num_mistakes(np.array([]))) # 0

# 测试样例3
print(intermediate_node_num_mistakes(np.array([1]))) # 0

7. 创建叶子结点

def create_leaf(target_values):
    '''
    计算出当前叶子结点的标记是什么,并且将叶子结点信息保存在一个dict中
    
    Parameter:
    ----------
    target_values: pd.Series, 当前叶子结点内样本的标记

    Returns:
    ----------
    leaf: dict,表示一个叶结点,
            leaf['splitting_features'], None,叶结点不需要划分特征
            leaf['left'], None,叶结点没有左子树
            leaf['right'], None,叶结点没有右子树
            leaf['is_leaf'], True, 是否是叶子结点
            leaf['prediction'], int, 表示该叶子结点的预测值
    '''
    # 创建叶子结点
    leaf = {'splitting_feature' : None,
            'left' : None,
            'right' : None,
            'is_leaf': True}
   
    # 数结点内-1和+1的个数
    num_ones = len(target_values[target_values == +1])
    num_minus_ones = len(target_values[target_values == -1])    

    # 叶子结点的标记使用少数服从多数的原则,为样本数多的那类的标记,保存在 leaf['prediction']
    if num_ones > num_minus_ones:
        leaf['prediction'] = 1
    else:
        leaf['prediction'] = -1

    # 返回叶子结点
    return leaf

8. 递归地创建决策树

递归的创建决策树
递归算法终止的三个条件:

  1. 如果结点内所有的样本的标记都相同,该结点就不需要再继续划分,直接做叶子结点即可
  2. 如果结点所有的特征都已经在之前使用过了,在当前结点无剩余特征可供划分样本,该结点直接做叶子结点
  3. 如果当前结点的深度已经达到了我们限制的树的最大深度,直接做叶子结点
def decision_tree_create(data, features, target, criterion = 'gini', current_depth = 0, max_depth = 10, annotate = False):
    '''
    Parameter:
    ----------
    data: pd.DataFrame, 数据

    features: iterable, 特征组成的可迭代对象,比如一个list

    target: str, 标记的名字

    criterion: 'str', 特征划分方法,只支持三种:'information_gain', 'gain_ratio', 'gini'

    current_depth: int, 当前深度,递归的时候需要记录

    max_depth: int, 树的最大深度,我们设定的树的最大深度,达到最大深度需要终止递归

    Returns:
    ----------
    dict, dict['is_leaf']          : False, 当前顶点不是叶子结点
          dict['prediction']       : None, 不是叶子结点就没有预测值
          dict['splitting_feature']: splitting_feature, 当前结点是使用哪个特征进行划分的
          dict['left']             : dict
          dict['right']            : dict
    '''
    
    if criterion not in ['information_gain', 'gain_ratio', 'gini']:
        raise Exception("传入的criterion不合规!", criterion)
    
    # 复制一份特征,存储起来,每使用一个特征进行划分,我们就删除一个
    remaining_features = features[:]
    
    # 取出标记值
    target_values = data[target]
    if annotate:
        print("-" * 50)
        print("Subtree, depth = %s (%s data points)." % (current_depth, len(target_values)))

    # 终止条件1
    # 如果当前结点内所有样本同属一类,即这个结点中,各类别样本数最小的那个等于0
    # 使用前面写的intermediate_node_num_mistakes来完成这个判断
    # YOUR CODE HERE
    if intermediate_node_num_mistakes(target_values) == 0:
        if annotate:
            print("Stopping condition 1 reached.")
        return create_leaf(target_values)   # 创建叶子结点
    
    # 终止条件2
    # 如果已经没有剩余的特征可供分割,即remaining_features为空
    
    # YOUR CODE HERE
    if not remaining_features:
        if annotate:
            print("Stopping condition 2 reached.")
        return create_leaf(target_values)   # 创建叶子结点
    
    # 终止条件3
    # 如果已经到达了我们要求的最大深度,即当前深度达到了最大深度
    
    # YOUR CODE HERE
    if current_depth == max_depth:
        if annotate:
            print("Reached maximum depth. Stopping for now.")
        return create_leaf(target_values)   # 创建叶子结点

    # 找到最优划分特征
    # 使用best_splitting_feature这个函数
    
    # YOUR CODE HERE
    splitting_feature = best_splitting_feature(data, remaining_features, target, criterion, annotate)
    # 使用我们找到的最优特征将数据划分成两份
    # 左子树的数据
    left_split = data[data[splitting_feature] == 0]
    
    # 右子树的数据
    # YOUR CODE HERE
    right_split = data[data[splitting_feature] == 1]
    # 现在已经完成划分,我们要从剩余特征中删除掉当前这个特征
    remaining_features.remove(splitting_feature)
    
    # 打印当前划分使用的特征,打印左子树样本个数,右子树样本个数
    if annotate:
        print("Split on feature %s. (%s, %s)" % (\
                          splitting_feature, len(left_split), len(right_split)))
        
    # 如果使用当前的特征,将所有的样本都划分到一棵子树中,那么就直接将这棵子树变成叶子结点
    # 判断左子树是不是“完美”的
    if len(left_split) == len(data):
        if annotate:
            print("Creating leaf node.")
        return create_leaf(left_split[target])
    
    # 判断右子树是不是“完美”的
    if len(right_split) == len(data):
        if annotate:
            print("Creating right node.")
        return create_leaf(right_split[target])
        
    # 递归地创建左子树
    left_tree = decision_tree_create(left_split, remaining_features, target, criterion, current_depth + 1, max_depth, annotate)
    
    # 递归地创建右子树
    right_tree = decision_tree_create(right_split, remaining_features, target, criterion, current_depth + 1, max_depth, annotate)                         # YOUR CODE HERE

    # 返回树的非叶子结点
    return {'is_leaf'          : False, 
            'prediction'       : None,
            'splitting_feature': splitting_feature,
            'left'             : left_tree, 
            'right'            : right_tree}

训练一个模型

my_decision_tree = decision_tree_create(train_data, one_hot_features, target, 'gini', max_depth = 6, annotate = False)

9. 预测

接下来我们需要完成预测函数

def classify(tree, x, annotate = False):
    '''
    递归的进行预测,一次只能预测一个样本
    
    Parameters
    ----------
    tree: dict
    
    x: pd.Series,待预测的样本
    
    annotate: boolean, 是否显示注释
    
    Returns
    ----------
    返回预测的标记
    '''
    if tree['is_leaf']:
        if annotate:
            print ("At leaf, predicting %s" % tree['prediction'])
        return tree['prediction']
    else:
        split_feature_value = x[tree['splitting_feature']]
        if annotate:
             print ("Split on %s = %s" % (tree['splitting_feature'], split_feature_value))
        if split_feature_value == 0:
            return classify(tree['left'], x, annotate)
        else:
            return classify(tree['right'], x, annotate)

我们取测试集第一个样本来测试

test_sample = test_data.iloc[0]
print(test_sample)
print('True class: %s ' % (test_sample['safe_loans']))
print('Predicted class: %s ' % classify(my_decision_tree, test_sample))

打印出使用决策树判断的过程

classify(my_decision_tree, test_sample, annotate=True)

10. 在测试集上对我们的模型进行评估

from sklearn.metrics import accuracy_score
from sklearn.metrics import precision_score
from sklearn.metrics import recall_score
from sklearn.metrics import f1_score

先来编写一个批量预测的函数,传入的是整个测试集那样的pd.DataFrame,这个函数返回一个np.ndarray,存储模型的预测结果
这里需要填写一个部分

def predict(tree, data):
    '''
    按行遍历data,对每个样本进行预测,将值存在prediction中,最后返回np.ndarray
    
    Parameter
    ----------
    tree: dict, 模型
    
    data: pd.DataFrame, 数据
    
    Returns
    ----------
    predictions:np.ndarray, 模型对这些样本的预测结果
    '''
    predictions = np.zeros(len(data)) # 长度和data一样
    
    # YOUR CODE HERE
    for i in range(len(data)):
        predictions[i] = classify(tree,data.iloc[i])
    
    return predictions

11. 请你计算使用不同评价指标得到模型的四项指标的值,填写在下方表格内

树的最大深度为6

# YOUR CODE HERE
criteria = ['information_gain', 'gain_ratio', 'gini']
for c in criteria:
    print(f'Using criterion: {c}')
    my_decision_tree=decision_tree_create(train_data, one_hot_features, target, c, max_depth=6, annotate=False)
    predictions = predict(my_decision_tree, test_data)
    acc33 = accuracy_score(test_data[target], predictions)
    precision33 = precision_score(test_data[target], predictions, average='macro')
    recall33 = recall_score(test_data[target], predictions, average='macro')
    f133 = f1_score(test_data[target], predictions, average='macro')
    print(f'Accuracy: {acc33}\nPrecision: {precision33}\nRecall: {recall33}\nF1 Score: {f133}\n')
    

实验3.4随机森林的应用——鸢尾花分类

加载sklearn中的鸢尾花数据集,选取前两个特征作为分类依据

运用Accuracy, Precision, Recall, F1四个指标进行评测

可视化分类结果

import sklearn
import numpy as np

1.导入数据

from sklearn.datasets import load_iris

feat,label = load_iris(return_X_y=True)
data = load_iris()
feat_names = data['feature_names']
label_names = data['target_names']
print(feat_names)
print(label_names)

选取前两个特征

feat = feat[:,:2]
feat.shape

2.导入模型

from sklearn.ensemble import RandomForestClassifier

3.模型训练

# YOUR CODE HERE
from sklearn.model_selection import train_test_split
# 拆分训练集, 测试集
feat_train, feat_test, label_train, label_test = train_test_split(feat, label, test_size=0.2, random_state=42)
# 创建随机森林分类器
rf_classifier34 = RandomForestClassifier(n_estimators=100, random_state=42)
# 拟合模型
rf_classifier34.fit(feat_train, label_train)
# 进行预测
label_pred = rf_classifier34.predict(feat_test)

4.评价指标的计算

from sklearn.metrics import accuracy_score
from sklearn.metrics import precision_score
from sklearn.metrics import recall_score
from sklearn.metrics import f1_score
# YOUR CODE HERE
acc34 = accuracy_score(label_test, label_pred)
precision34 = precision_score(label_test, label_pred, average='macro')
recall34 = recall_score(label_test, label_pred, average='macro')
f134 = f1_score(label_test, label_pred, average='macro')
print(f'Accuracy: {acc34}\nPrecision: {precision34}\nRecall: {recall34}\nF1 Score: {f134}\n')

5.可视化分类结果

import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap

# 创建一个网格,用于绘制决策边界
x_min, x_max = feat[:, 0].min() - 1, feat[:, 0].max() + 1
y_min, y_max = feat[:, 1].min() - 1, feat[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.01), np.arange(y_min, y_max, 0.01))

# 预测网格上的点的类别
Z = rf_classifier34.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)

cmap_background = ListedColormap(['#FFAAAA', '#AAAAFF', '#AAFFAA'])
cmap_points = ListedColormap(['#FF0000', '#0000FF', '#00FF00'])

# 绘制决策边界
plt.contourf(xx, yy, Z, cmap=cmap_background, alpha=0.3)

# 绘制数据点
plt.scatter(feat_test[:, 0], feat_test[:, 1], c=label_test, cmap=cmap_points, edgecolors='k', marker='o', s=80)

# 设置图形属性
plt.title('Random Forest Classifier - Iris Dataset')
plt.xlabel(feat_names[0])
plt.ylabel(feat_names[1])

# 显示图例
legend_labels = [f'{label_names[i]} ({i})' for i in range(len(label_names))]
plt.legend(legend_labels)

# 显示图形
plt.show()

实验3.5自行实现AdaBoost并完成肿瘤分类

加载sklearn中的肿瘤归类数据集

自行选择基学习器(可以使用Scikit-learn现成的分类器)自己实现,使用不同的基学习器实现2种以上的AdaBoost

运用Accuracy, Precision, Recall, F1四个指标进行对比评测,随机选择70%作为训练集,30%作为测试集,把结果绘制成表格

与Scikit-learn 的AdaBoostClassifier得到的结果进行对比(基学习器和你自己实现的AdaBoost相同)

import sklearn
import numpy as np

导入数据

from sklearn.datasets import load_breast_cancer

feat,label = load_breast_cancer(return_X_y=True)

feat.shape

划分数据集:70%训练集,30%测试集 (随机种子固定为32)

from sklearn.model_selection import train_test_split
trainX, testX, trainY, testY = train_test_split(feat, label, test_size = 0.3, random_state = 32)
trainX.shape

实现AdaBoost

import copy
# YOUR CODE HERE
class MyAdaBoostClassifier:
    def __init__(self, base_estimator, n_estimators=50, learning_rate=1.0, n_classes=2):
        self.base_estimator = base_estimator
        self.n_estimators = n_estimators 
        self.lr = learning_rate
        self.R = n_classes
        self.estimators = []
        self.alphas = []  # model_weight
        for m in range(n_estimators):
            self.estimators.append(copy.deepcopy(base_estimator))

    def fit(self, X, y):
        sample_weight = np.ones(len(X)) / len(X)  # 初始化样本权重为 1/N
        for i in range(self.n_estimators):
            model = self.estimators[i]
            model.fit(X, y, sample_weight)  # 训练弱学习器
            y_pred = model.predict(X)
            error = np.sum(sample_weight* (y_pred != y))  
            alpha = self.lr * (np.log((1-error)/error) + np.log(self.R-1))  # 权重系数
            sample_weight *= np.exp(alpha*(y_pred!=y))  # 更新迭代样本权重
            sample_weight /= np.sum(sample_weight)  # 样本权重归一化
            self.alphas.append(alpha)
        return self   
   
    def predict(self, X):  # 假定类别映射成 0,1,...
        y_pred = [] 
        for i in range(self.n_estimators):
            y_pred.append(self.estimators[i].predict_proba(X) )
        # 将预测类别概率与训练权重乘积作为集成预测类别概率    
        y_pred = np.average(np.asarray(y_pred), weights=np.array(self.alphas), axis=0 ) 
        y_pred = y_pred/np.array(self.alphas).sum()
        y_pred = np.argmax(y_pred, axis=1)
        return y_pred

基分类器选择决策树

from sklearn.tree import DecisionTreeClassifier
# YOUR CODE HERE
base_tree = DecisionTreeClassifier(max_depth=1)  # You can customize the base estimator
Mymodel1 = MyAdaBoostClassifier(base_estimator=base_tree, n_estimators=50, learning_rate=1.0)
Mymodel1.fit(trainX, trainY)

基分类器选择对数几率回归

from sklearn.linear_model import LogisticRegression
# YOUR CODE HERE
base_lr = LogisticRegression() 
Mymodel2 = MyAdaBoostClassifier(base_estimator=base_lr, n_estimators=50, learning_rate=1.0)
Mymodel2.fit(trainX, trainY)

评价指标的计算

from sklearn.metrics import accuracy_score
from sklearn.metrics import precision_score
from sklearn.metrics import recall_score
from sklearn.metrics import f1_score
prediction1 = Mymodel1.predict(testX)
prediction2 = Mymodel2.predict(testX)
# YOUR CODE HERE
acc351 = accuracy_score(testY, prediction1)
precision351 = precision_score(testY, prediction1)
recall351 = recall_score(testY, prediction1)
f1351 = f1_score(testY, prediction1)
print(f'Accuracy: {acc351}\nPrecision: {precision351}\nRecall: {recall351}\nF1 Score: {f1351}\n')
acc352 = accuracy_score(testY, prediction2)
precision352 = precision_score(testY, prediction2)
recall352 = recall_score(testY, prediction2)
f1352 = f1_score(testY, prediction2)
print(f'Accuracy: {acc352}\nPrecision: {precision352}\nRecall: {recall352}\nF1 Score: {f1352}\n')

调用sklearn的模型

from sklearn.ensemble import AdaBoostClassifier
# YOUR CODE HERE
# 决策树作为基
base_classifier = DecisionTreeClassifier(max_depth=1)  
ada_boost_model = AdaBoostClassifier(base_classifier, n_estimators=50, learning_rate=1.0)
ada_boost_model.fit(trainX, trainY)

# 预测
predictions353 = ada_boost_model.predict(testX)

# 计算评估指标
acc353 = accuracy_score(testY, predictions353)
precision353 = precision_score(testY, predictions353)
recall353 = recall_score(testY, predictions353)
f1353 = f1_score(testY, predictions353)
print(f'Accuracy: {acc353}\nPrecision: {precision353}\nRecall: {recall353}\nF1 Score: {f1353}\n')

你可能感兴趣的:(机器学习,本科课程,机器学习,决策树,分类)