Python DataScience CookBook -- Learning Notes (III)

Bagging | Boosting | Random Forest | Perceptron | Gradient Descent

Posted by Paradise on March 2, 2021

教材介绍:https://book.douban.com/subject/26630181/

第八章 集成方法

集成学习(Ensemble Learning)的概念:不仅仅通过个人,而是通过集体智慧来做出决策。准确来说,就是生成大量模型,用它们来进行预测。从多个相近的模型输出的结果会比仅仅从一个模型的到的结果更好。集成的模型可以是不同类别的,比如同时利用神经网络模型和贝叶斯模型。本章只讨论集成同类的模型。

挂袋法,每个模型只使用训练集的一部分,以减少过拟合。挂袋法很容易实现并行化,可以同时处理的不同的训练集样本。挂袋法对如线性回归之类的线性预测器无效。

提升法,产生一个逐步复杂的模型序列,基于上一个模型的错误,训练新的模型。每次训练的模型被赋予一个权重,并按权重得出最终的预测值。可见提升法是按顺序执行的,无法并行化。提升法常常用一些弱分类器,如单层决策树。

(1)装袋法

集成方法属于基于评委学习方法一族。挂袋法也叫引导聚集,只有在潜在的模型能产生不同变化时才有效。也就是要产生有轻微变化的多种模型。

使用自举方法在模型上产生变化,自举就是在数据集中随机采样一定的观测,不管是否有替换。在挂袋法中,通过自举产生 m 个数据集,然后对每一个建立一个模型,最终使用所有模型的输出来产生最终的预测。对于回归问题就是取均值,对于分类问题使用投票方法。

挂袋法适用于不稳定的、对变化敏感的模型,例如决策树,尤其未剪枝的。而不适合 KNN 等稳定的分类器。应用于 KNN 时需要使用随机子空间方法(例如,在集成的每个模型里随机选择特征属性的子集)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from sklearn.neighbors import KNeighborsClassifier
from sklearn.ensemble import BaggingClassifier

# generate a dataset for classification
X, y = make_classification(n_samples=500, n_features=30, flip_y=0.03,
    n_informative=18, n_redundant=3, n_repeated=3, random_state=1)
# 一共30个特征,18个关键特征,3个多余的特征,3个重复的特征,flip_y使分类更困难

# split data
x_train, x_temp, y_train, y_temp = \
            train_test_split(X, y, test_size=0.3, random_state=1)
x_dev, x_test, y_dev, y_test = \
            train_test_split(x_temp, y_temp, test_size=0.3, random_state=1)

# generate a KNN classifier, then report
knn = KNeighborsClassifier()
knn.fit(x_train, y_train)
y_pred_knn = knn.predict(x_train)
print(classification_report(y_train, y_pred_knn))

# generate a Bagging classifier on KNN, then report
bagging = BaggingClassifier(KNeighborsClassifier(), n_estimators=100,
                            max_samples=1.0, max_features=0.7,
                            bootstrap=True, bootstrap_features=True,
                            random_state=1)
# estimator自举时选用100%的样本,选取70%的特征属性(随机空间方法)
# 样本与特征的采样都是有放回(default=False)
bagging.fit(x_train, y_train)
y_pred_bagging = bagging.predict(x_train)
print(classification_report(y_train, y_pred_bagging))
# 可以看到bagging比knn的准确率明显提高

# 打印出前 3 个模型所选取的特征属性
print("\nSampled attributes in top 3 estimators: \n" + '='*66)
for i, features in enumerate(bagging.estimators_features_[0:3]):
    print(f"estimator_{i+1:d}: ", features)

# 在验证集上对比knn和bagging
print("\nSingle KNN: \n" + '='*66)
print(classification_report(y_dev, knn.predict(x_dev)))
print("\nBagging KNN: \n" + '='*66)
print(classification_report(y_dev, bagging.predict(x_dev)))

(2)提升法

a) Boosting 原理步骤:

对于二分类问题,分类器的输入可以表达为:$X = [x_1, x_2, …, x_N]\ and\ Y = [0, 1]$。 分类器的任务就是找到一个可以近似的函数:$Y = F(X)$。

分类器的错误率定义为:$error\ rate = \frac{1}{N} * \sum_i{[instance\ |\ y_i \neq{F(x_i)}]}$

假设构建一个弱分类器(错误率仅好于随机猜测),然后通过提升法构建一系列弱分类器用在经过微调的数据集上,每个分类器使用的数据只做了很小的调整。最后结束于第 $M$ 个分类器:$F_1(X), F_2(X), …, F_M(X)$

最后把各个分类器生成的预测集成起来,进行加权投票:$F_{final}(X) = sign( \sum_i(\alpha_i * F_i(X)) )$,其中 $\alpha_i$ 为模型 $F_i(\cdot)$ 的权重。

b) 模型权重计算原理步骤:

首先修改错误率公式:$error\ rate = \sum_i( w_i * |y_i - \hat{y_i}| ) / \sum_i( w_i )$

对于 $N$ 个实例,每个实例的权重为 $1/N$ ,$w_i$ 表示模型的初始权重 $w_i = n / N$ ,$n$ 为采样数目。

根据调整的 $error_rate$ 计算每个模型的 $\alpha$: $\alpha_i = \frac{1}{2} * log( (1 - error\ rate + \epsilon) / (error\ rate + \epsilon) )$ ,其中 $\epsilon$ 是一个微小的值。

最终计算每个模型的输出权重:$w_i = w_i * exp( \alpha_i * |y_i - \hat{y_i}| )$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, zero_one_loss
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import AdaBoostClassifier

# generate dataset for classification
X, y = make_classification(n_samples=500, n_features=30, flip_y=0.03,
                           n_informative=18, n_redundant=3, n_repeated=3,
                           random_state=1)
# split dataset
x_train, x_temp, y_train, y_temp = \
            train_test_split(X, y, test_size=0.3, random_state=1)
x_dev, x_test, y_dev, y_test = \
            train_test_split(x_temp, y_temp, test_size=0.3, random_state=1)

# build decision tree classifier and report
dtc = DecisionTreeClassifier()
dtc.fit(x_train, y_train)
y_pred_dtc = dtc.predict(x_train)
print("\nSingle Model Accuracy on training data:\n" + '='*66)
print(classification_report(y_train, y_pred_dtc))
print("Fraction of Misclassification: {:0.2f}%".format(
                                    zero_one_loss(y_train, y_pred_dtc)*100))

# build boosting classifier and report
boosting = AdaBoostClassifier(
            DecisionTreeClassifier(max_depth=1, min_samples_leaf=1),
            n_estimators=85,
            algorithm='SAMME',
            random_state=1)
# 决策树只有树桩,叶子结点最小样本数为1
# SAMME:AdaBoost的增强版:
#   Stage wise Additive Modeling using Multi-class Exponential loss function

boosting.fit(x_train, y_train)
y_pred_boost = boosting.predict(x_train)
print("\nBoosting Model Accuracy on training data:\n" + '='*66)
print(classification_report(y_train, y_pred_boost))
print("Fraction of Misclassification: {:0.2f}%".format(
                                    zero_one_loss(y_train, y_pred_boost)*100))

# boosting model 参数
print("\nEstimator Weights and Error on Boosting Models: " + '=' * 66)
for i, weight in enumerate(boosting.estimator_weights_):
    e = boosting.estimator_errors_[i]
    print(f"estimator_{i + 1:d}: \tweight = {weight:0.4f}\t\terror = {e:0.4f}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
'''绘制模型数量与错误率的关系,同时对比single decision tree 模型'''
error_rates = []
error_rates_dev = []
dtc_error_rates = []
dtc_error_rates_dev = []
numbers = [i for i in range(20, 120, 10)]

for e in numbers:
    dtc = DecisionTreeClassifier()
    dtc.fit(x_train, y_train)
    boosting = AdaBoostClassifier(
            DecisionTreeClassifier(max_depth=1,min_samples_leaf=1),
            n_estimators=int(e), algorithm='SAMME', random_state=1 )
    boosting.fit(x_train, y_train)
    y_pred = boosting.predict(x_train)
    y_dev_pred = boosting.predict(x_dev)
    y_pred_dtc = dtc.predict(x_train)
    y_dev_pred_dtc = dtc.predict(x_dev)
    error_rates.append(zero_one_loss(y_train, y_pred))
    error_rates_dev.append(zero_one_loss(y_dev, y_dev_pred))
    dtc_error_rates.append(zero_one_loss(y_train, y_pred_dtc))
    dtc_error_rates_dev.append(zero_one_loss(y_dev, y_dev_pred_dtc))

plt.figure(figsize=(6, 6.75), dpi=80)
plt.title("Number of Estimators VS. Mis-classification Rate\n" +
                "--compared with single decision tree model")
plt.plot(numbers, error_rates, label="Train--AdaBoost")
plt.plot(numbers, error_rates_dev, label="Dev--AdaBoost")
plt.plot(numbers, dtc_error_rates, label="Train--DTC")
plt.plot(numbers, dtc_error_rates_dev, label="Dev--DTC")
plt.xlabel('Number of Estimators'); plt.ylabel('Mis-classification Rate')
plt.legend(loc='upper right')
plt.show()
''' 结果显示:
1.boosting模型测试集错误率明显低于弱分类器dtc;但是
2.模型数量越大训练集误差越小,但是测试集误差只能有限减小,最后震荡在一个范围。
'''

(3)梯度提升

  • 提升法:用一种渐进的,阶段改良的方式,从一系列若分类器适配出一个增强模型。具体就是通过错误率调整实例的权重,在下一模型改进不足之处。
  • 梯度提升法就是采用梯度而不是权重来鉴别缺陷。以下是一个简单回归问题的梯度提升步骤。
    • 给定预测变量 $X$ 和响应变量 $Y$:$X = [x_1, x_2, …, x_N]\ and\ Y = [y_1, y_2, …, y_N]$
    • 先从简单模型开始,例如直接使用均值预测所有值:$\hat{y_i} = \sum_i^N(y_i) / N$
    • 得到残差:$R_i = y_i - \hat{y_i}$
    • 下一个分类在以下数据上训练:$[(x_1, R_1), (x_2, R_2), …, (x_N, R_N)]$
    • 进行迭代达到所需准确率
  • 为何叫做梯度提升:$F(x_i) - y_i$ 就代表梯度,即该点的一阶导数,正好是负的残差;亦即在简单回归问题中,梯度提升等同于残差缩减
  • 梯度提升是一种框架而不仅仅是一种具体的算法,任何可微函数都可以应用到框架中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from sklearn.datasets import load_boston
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import PolynomialFeatures
from sklearn.ensemble import GradientBoostingRegressor

def view_model(model):
    print("\nTraining Scores: \n" + '='*66)
    for i, score in enumerate(model.train_score_):
        print(f"Estimator_{i+1:d} Score:\t{score:0.3f}")
    print("\nFeature Importance: \n" + '='*66)
    for i, score in enumerate(model.feature_importances_):
        print(f"Feature_{i+1} Importance:\t{score:0.3f}")
    plt.figure(figsize=(6, 6.75), dpi=80)
    plt.plot(range(1, model.estimators_.shape[0]+1), model.train_score_)
    plt.xlabel("Model Sequence"); plt.ylabel("Model Score")
    plt.show()

# load boston dataset
X, y = load_boston()['data'], load_boston()['target']
# split dataset
x_train, x_temp, y_train, y_temp = \
            train_test_split(X, y, test_size=0.3, random_state=1)
x_dev, x_test, y_dev, y_test = \
            train_test_split(x_temp, y_temp, test_size=0.3, random_state=1)

# generate polynomial features
polyfeat = PolynomialFeatures(2, interaction_only=True)
polyfeat.fit(x_train)
for name in ['train', 'dev', 'test']:
    exec(f"x_{name}_poly = polyfeat.transform(x_{name})")

# build gradient boosting model and report
poly_gbr = GradientBoostingRegressor(n_estimators=500, verbose=10,
            subsample=0.7, learning_rate=0.15, max_depth=3, random_state=1)
# 当verbose>1,将每个模型或树构建时的进展情况打印出来;
# 采样率subsample指定模型采样70%的训练集数据;
# 学习率learning_rate,是一个缩减参数,控制每个模型的贡献;

poly_gbr.fit(x_train_poly, y_train)
y_pred = poly_gbr.predict(x_train_poly)
print("\nModel Performance in Training Set: \n" + '='*66)
view_model(poly_gbr)
print("\nMSE:\t{:0.2f}".format(mean_squared_error(y_train, y_pred)))

# 在验证集上测试结果
y_dev_pred = poly_gbr.predict(x_dev_poly)
print("\nModel Performance in Dev Set: \n" + '='*66)
print("\nMSE:\t{:0.2f}".format(mean_squared_error(y_dev, y_dev_pred)))

第九章 生长树

上述基于树算法具备对抗噪声的健壮性和解决各类问题的广泛能力,能在没有数据整理的情况下获得很好的结果,可作为黑箱工具。

主要优点:挂袋法有天然的并行性;决策树算法在每一层将数据划分,实现隐式的特征选择;几乎不需要对数据进行预处理,不同度量、缺失值或者异常值,以及非线性问题等,对其几乎没有影响。

主要难题:为了避免过拟合进行剪枝的难度;大型树容易导致低偏差和高方差。

(1)随机森林

  • 随机森林也是一种挂袋法,基本思路是利用大量的噪声评估器,用平均法处理噪声,以减小最终结果的方差。
  • 随机森林构建的是相互之间没有关联的树。具体方法是,在进行结点划分时,不选择所有的属性,而是随机选择一个属性子集。
  • 构建随机森林:$LOOP\ for\ 1\ to\ T$
    • 随机选择 $m$ 个属性
    • 采用预定义的 $criterion$,选择一个最佳属性作为划分变量
    • 将数据集划分为两个部分
    • 返回划分的数据集,分别在两个部分迭代上述过程
    • 最终获得 $T$ 棵树
  • 提升法使用弱分类器进行强化,实现较低的方差(较高的验证集准确率);而在随机森林中,构建最大深度的树,但是引入了高方差。随后通过大量树进行投票,解决高方差问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from operator import itemgetter
from sklearn.datasets import make_classification
from sklearn.metrics import classification_report, accuracy_score
from sklearn.model_selection import train_test_split, RandomizedSearchCV
from sklearn.ensemble import RandomForestClassifier

# generate dataset for classification
X, y = make_classification(n_samples=500, n_features=30, flip_y=0.03,
                           n_informative=18, n_redundant=3, n_repeated=3,
                           random_state=1)
# split dataset
x_train, x_temp, y_train, y_temp = \
            train_test_split(X, y, test_size=0.3, random_state=1)
x_dev, x_test, y_dev, y_test = \
            train_test_split(x_temp, y_temp, test_size=0.3, random_state=1)

# build forest and report
rfc = RandomForestClassifier(n_estimators=100)
rfc.fit(x_train, y_train)
y_pred = rfc.predict(x_train)
y_dev_pred = rfc.predict(x_dev)
train_score = accuracy_score(y_train, y_pred)
dev_score = accuracy_score(y_dev, y_dev_pred)
print(f"Training Accuracy: {train_score:0.2f}\tDev Accuracy: {dev_score:0.2f}")
# 结果方差较大,训练集准确率100%,测试集只有83%

# search parameters
param_dict = {
    "n_estimators": np.random.randint(75, 200, 20),
    "criterion": ['gini', 'entropy'],   #基尼系数与熵判据
    "max_features": [int(np.sqrt(X.shape[1]))*i for i in [1,2,3]] + \
                    [int(np.sqrt(X.shape[1]))+10]   #每个结点选取的特征数
    }
grid = RandomizedSearchCV(estimator=RandomForestClassifier(),
                          param_distributions=param_dict,
                          n_iter=20, random_state=1, n_jobs=-1, cv=5)
grid.fit(X, y)

# view parameters of grid searching
print("\n模型参数:\n" + '='*66)
for score in grid.cv_results_['params']:
    print(score)
print(classification_report(y_dev, grid.predict(x_dev)))
# 当对grid使用predict实例方法时,隐式调用了最优模型
best_model = grid.best_estimator_
print(best_model)

(2)超随机树

  • 超随机树(Extra Trees)或称为极限随机树(Extremely Randomized Trees)与随机森林主要有两点不同:
    • 不使用自举法,而是使用完整的数据集
    • 给定结点随机选择属性数量 $K$,它随机选择割点,不考虑目标变量
  • 优势:更好地降低方差,可以在未知数据集上取得很好的效果;并且计算复杂度相对较低
  • 构建超随机树:$LOOP\ for\ 1\ to\ T$
    • 随机选择 $m$ 个属性
    • 随机选取一个属性作为划分变量(忽略任何标准,完全随机)
    • … 接下来与随机森林一致
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import numpy as np
from sklearn.datasets import make_classification
from sklearn.metrics import classification_report, accuracy_score
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.model_selection import RandomizedSearchCV
from sklearn.ensemble import ExtraTreesClassifier

# generate dataset for classification
X, y = make_classification(n_samples=500, n_features=30, flip_y=0.03,
                           n_informative=18, n_redundant=3, n_repeated=3,
                           random_state=1)
# split dataset
x_train, x_temp, y_train, y_temp = \
            train_test_split(X, y, test_size=0.3, random_state=1)
x_dev, x_test, y_dev, y_test = \
            train_test_split(x_temp, y_temp, test_size=0.3, random_state=1)

# build forest and report
etc = ExtraTreesClassifier(n_estimators=100, random_state=1)
etc.fit(x_train, y_train)
y_pred = etc.predict(x_train)
y_dev_pred = etc.predict(x_dev)
print(f"Training Accuracy: {accuracy_score(y_train, y_pred):0.2f}")
print(f"Dev Accuracy: {accuracy_score(y_dev, y_dev_pred):0.2f}")
print("\nCross Validated Score: \n" + '='*66)
print(cross_val_score(etc, x_dev, y_dev, cv=5))

# search parameters (与上一节一样流程,只estimator由随机森林变成extra_trees)
param_dict = {
    "n_estimators": np.random.randint(75, 200, 20),
    "criterion": ['gini', 'entropy'],   #理论来说不需要判断准则
    "max_features": [int(np.sqrt(X.shape[1]))*i for i in [1,2,3]] + \
                    [int(np.sqrt(X.shape[1]))+10]   #每个结点选取的特征数
    }
grid = RandomizedSearchCV(estimator=ExtraTreesClassifier(),
                          param_distributions=param_dict,
                          n_iter=20, random_state=1, n_jobs=-1, cv=5)
grid.fit(X, y)
print("\n模型参数:\n" + '='*66)
for score in grid.cv_results_['params']:
    print(score)
print(classification_report(y_dev, grid.predict(x_dev)))
# 当对grid使用predict实例方法时,隐式调用了最优模型

best_model = grid.best_estimator_
print(best_model)
# 在验证集的准确率达到100%

(3)旋转森林

  • 随机森林和挂袋法使用大量的树实现精确的预测,而旋转森林的思路是使用少得的集成数量。
  • 构建旋转森林步骤:$LOOP\ for\ 1\ to\ T$
    • 将训练集的属性划分为大小相等的 $K$ 个不重叠子集
    • 对每个子集,自举 75% 的样本,在样本上执行:
      • 在 $K$ 个数据集的第 $i$ 个子集中进行 PCA,保留主成分。对每个特征 $j$, 主成分标为 $a_{ij}$
      • 保留以上所有子集的主成分
    • 创建 $n*n$ 的旋转矩阵,$n$ 是特征总数
    • 将上述的主成分放进旋转矩阵,这些成分匹配特征在初始训练数据集中的位置
    • 通过矩阵乘法将训练集投影到旋转矩阵上
    • 用投影的数据构建一颗决策树,并保存树和旋转矩阵
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
"""python 中还没有包含旋转矩阵算法的库(2016-12)需要自己编写"""
import numpy as np
from sklearn.datasets import make_classification
from sklearn.metrics import classification_report
from sklearn.model_selection import train_test_split
from sklearn.decomposition import PCA
from sklearn.tree import DecisionTreeClassifier

# generate dataset for classification
X, y = make_classification(n_samples=500, n_features=50, flip_y=0.03,
                           n_informative=30, n_redundant=5, n_repeated=5,
                           random_state=1)
# split dataset
x_train, x_temp, y_train, y_temp = \
            train_test_split(X, y, test_size=0.3, random_state=1)
x_dev, x_test, y_dev, y_test = \
            train_test_split(x_temp, y_temp, test_size=0.3, random_state=1)

def random_subset(iterable_obj, k):
    '''get random subset from iterable object, using in next function'''
    subsets = []
    iteration = 0
    limit = len(iterable_obj) / k  #先储存limit,iterable_obj的长度会动态变化
    # 洗牌:
    np.random.shuffle(iterable_obj)
    # 获取k个子集:
    while iteration < limit:
        if k <= len(iterable_obj):
            subset = k
        else:
            subset = len(iterable_obj)
        subsets.append(iterable_obj[:subset])
        del iterable_obj[:subset]    #删除该切片,使子集不相交
        iteration += 1
    return subsets

# ========================== 生成旋转森林的主循环 ===========================

T = 25    #树的数量T
K = 5    #特征子集长度K
models = []    #保存每一棵树
r_matrices = []    #保存与树对应的旋转矩阵
feature_subsets = []    #保存随机选取的特征子集

for i in range(T):
    # 训练集自举,随机选取75%的子集:
    x, _, _, _ = train_test_split(x_train, y_train, test_size=0.25)
    # 在数据集上随机选取特征(生成用于选集的index):
    feature_idx = [idx for idx in range(x.shape[1])]
    random_k_subset = random_subset(feature_idx, K)
    feature_subsets.append(random_k_subset)

    # 生成旋转矩阵:
    rotation_matrix = np.zeros((x.shape[1],x.shape[1]), dtype=float)  #初始化
    for subset in random_k_subset:
        # 对每个特征子集执行PCA,并将所有components存放进旋转矩阵的对应位置:
        # 注意到这个循环只填满一个旋转矩阵,因为每一个subset之间是互斥的
        pca = PCA()
        x_subset = x[:, subset]
        pca.fit(x_subset)
        for ii in range(len(pca.components_)):
            for jj in range(len(pca.components_)):
                rotation_matrix[subset[ii],subset[jj]] = pca.components_[ii,jj]
    r_matrices.append(rotation_matrix)

    # 将输入数据投影到旋转矩阵
    x_transformed = x_train.dot(rotation_matrix)
    # 使用转换的数据构建决策树并保存
    dtc = DecisionTreeClassifier()
    dtc.fit(x_transformed, y_train)
    models.append(dtc)

# =========================== 评估旋转森林模型 =============================
def predict_models(x, models=models, r_matrices=r_matrices):
    '''采用全部模型的预测值进行投票产生最终预测'''
    # 获取每个模型的预测值
    y_predictions = []
    for i, model in enumerate(models):
        x_trans = x.dot(r_matrices[i])
        y_pred = model.predict(x_trans)
        y_predictions.append(y_pred)
    # 结合所有模型的预测得出最终预测
    prediction_matrix = np.matrix(y_predictions)
    final_predictions = []
    for i in range(x.shape[0]):
        predictions = np.ravel(prediction_matrix[:,i])    #降维,扁平化
        nonzero_idx = np.nonzero(predictions)[0]    #返回非零的坐标(元组里面)
        is_one = len(nonzero_idx) > len(models)/2    #投票,过半数为1则返回True
        final_predictions.append(is_one)
    return np.array(final_predictions)

# 分别在训练集和验证集上评估
y_pred = predict_models(x_train)
y_dev_pred = predict_models(x_dev)
print(classification_report(y_train, y_pred))
print(classification_report(y_dev, y_dev_pred))

# 跑了几次,发现效果不好,方差较大。

第十章 大规模机器学习 – 在线学习

本章的关注点是大规模机器学习以及适合处理大规模问题的算法。前几章的所有模型的数据集都是可以完全加载进计算机的内存的。当数据集数目庞大,需要构建一个框架,可以根据部分数据进行判断,并随着新数据的输入而持续提高。

随机梯度下降(Stochastic Gradient Descent)就是一个这样的框架。许多线性方法,如逻辑斯蒂回归、线性回归、线性 SVM,以及非线性的核方法等都可用于 SGD 框架。

(1)用感知器作为在线学习算法

  • 感知器(Perceptron):处理大规模学习问题,建模时一次只使用数据集的一部分
  • 具体算法:
    • 将模型权重用一个小的随机数初始化
    • 用输入数据 $X$ 的均值进行去中心化
    • 在每个步骤 $t$ 中(或称为纪元):
      • 随机选择记录中的一个实例进行预测
      • 比较预测标签和真实标签的误差
      • 如果预测错误则更新权重
  • 如何更新权重?
    • 假定在一个纪元中,输入 X 为:$X_i = [x_1, x_2, …, x_m],\ i=1, 2, …, n$
    • Y 的集合为:$Y = [1, -1]$
    • 则权重定义为:$W = [w_1, w_2, …, w_m]$
    • 每条记录得出的预测值为:$\hat{y_i} = sign(w_i * x_i)$
    • 权重的更新公式为:$w_t + 1 = w_t + y_i * x_i$
    • 增加学习速率参数 $\alpha$:$w_t + 1 = w_t + \alpha * y_i * x_i$
      • $\alpha$ 取值一般为 $[0.1, 0.4]$
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import numpy as np
from sklearn.datasets import make_classification
from sklearn.metrics import classification_report
from sklearn.preprocessing import scale, PolynomialFeatures

def get_data(batch_size, kernel='linear'):
    '''生成多组数据模拟大型数据和数据流,隐式返回一个生成器generator'''
    b_size = 0
    while b_size < batch_size:
        X, y = make_classification(n_samples=1000, n_features=30, flip_y=0.03,
                                   n_informative=24, n_redundant=3,n_repeated=3,
                                   random_state=1)
        # 添加非线性核选项:
        if kernel == 'linear':
            X = scale(X, with_mean=True, with_std=True)    #特征数据中心化
        elif kernel == 'polynomial':
            polyfeat = PolynomialFeatures(degree=2)
            X = polyfeat.fit_transform(X)
        y[y<1] = -1    #把y标签由{0, 1}改为{-1, +1}
        b_size += 1
        yield X, y

def build_perceptron(x, y, weights, epochs, alpha=0.5):
    ''' 构建一个感知器模型,用于对于多组数据进行循环建模,模拟现实的数据流。
        weights: 初始权重矩阵;
        epochs: 纪元数,即更新权重的次数;
        alpha: 学习速率。
    '''
    for i in range(epochs):
        # 搅乱数据集:
        shuffled_idx = [i for i in range(len(y))]
        np.random.shuffle(shuffled_idx)    #这个函数是就地更改的
        x_train = x[shuffled_idx, :].reshape(x.shape)
        y_train = np.ravel(y[shuffled_idx])

        # 一次构建一个实例的权重:
        for idx in range(len(y)):
            prediction = np.sign(np.sum(x_train[idx,:] * weights))
            if prediction != y_train[idx]:
                weights = weights + alpha * (y_train[idx] * x_train[idx, :])
    # 所有纪元更新完之后返回权重
    return weights

# 生成 10 组数据流
data = get_data(10, 'linear')    #data = get_data(10, 'polynomial')
# 使用生成器的__next__()方法获取下一组数据
X, y = data.__next__()

# 初始化权重矩阵
weights = np.zeros(X.shape[1])

# 模拟收到10组数据集,进行建模学习
for i in range(10):
    weights = build_perceptron(X, y, weights, 100, 0.5)
    y_pred = np.sign(np.sum(X * weights, axis=1))
    print('='*66+f"\nModel Performance After Receiving Dataset Batch {i+1}:\n")
    print(classification_report(y, y_pred))
    if i != 9:
        X, y = data.__next__()

(2)用梯度下降解决回归问题

标准的回归结构中,有一系列实例:$X = [x_1, x_2, …, x_n]$, 每个实例有 $m$ 个属性(特征):$x_i = [x_{i1}, x_{i2}, …, x_{im}]$。 回归算法的任务是找到一个 $X$ 到 $Y$ 映射:$Y = F(X)$ ,由一个权重向量来进行参数化: $Y = F(X, W) = X\cdot{W} + b$ ,于是回归问题就变成寻找最优权重的问题。采用损失函数进行优化,对 $n$ 个实例的数据集,全局损失函数形式为:$\frac{1}{n} \sum_i^n{ L\lbrace{f(x_i, w), y_i}\rbrace }$。

随机梯度下降(Stochastic Gradient Descent, SGD)是一种优化技术,可用于最小化损失函数。首先要找出 $L(\cdot)$ 的梯度,也就是损失函数对权重 $w$ 的偏导

和批量梯度下降等其他技术不同,SGD 每次只操作一个实例:

  • 对每个纪元 $t$ ,搅乱数据集
  • 选择一个实例 $x_i$ 及其对应的响应变量 $y$
  • 计算损失函数及其对于 $w$ 的偏导 $\nabla{w}$(倒三角符号,表示矢量求偏导)
  • 更新权重值

更新权重值的公式为:

$w_t + 1 = w_t - \nabla{w} L(\hat{y_i}, y_i)$

式中权重和梯度的方向相反,这样迫使权重向量降序排列,以减小目标函数。引入学习率后表示为:

$w_t + 1 = w_t - \eta * (\nabla{w} L(\hat{y_i}, y_i))$

在 SGD 的基础上加上正则化,类似岭回归增加 L2 范数正则项和学习率,权重更新公式表示为:

$w_t + 1 = w_t - \eta * (\nabla{w} L(\hat{y_i}, y_i)) + \alpha * (\nabla{w}R(W))$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from sklearn.datasets import make_regression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_absolute_error, mean_squared_error
from sklearn.linear_model import SGDRegressor


# generate dataset for regression
X, y = make_regression(n_samples=1000, n_features=30, random_state=1)
# split dataset
x_train, x_temp, y_train, y_temp = \
            train_test_split(X, y, test_size=0.3, random_state=1)
x_dev, x_test, y_dev, y_test = \
            train_test_split(x_temp, y_temp, test_size=0.3, random_state=1)


# 建模 & 预测
sgdr = SGDRegressor(n_iter=10, shuffle=True, loss='squared_loss',
                    learning_rate='constant', eta0=0.01, fit_intercept=True,
                    penalty='none')
# 上述大部分参数是默认的,写出来看一下而已
# learning_rate指定学习速率的类型,eta0指定学习速率的值
# penalty指定缩减类型(Lr缩减),本例无需进行缩减
sgdr.fit(x_train, y_train)
y_pred = sgdr.predict(x_train)
y_dev_pred = sgdr.predict(x_dev)

# 查看模型参数
print(f"\nModel Intercept: {sgdr.intercept_}")
for i, coef in enumerate(sgdr.coef_):
    print(f"Coefficient_{i+1}: {coef:0.3f}")
# 打印训练集和验证的MSE和MAE
print('\nTrain set: ========>>')
print(f"Mean absolute error: {mean_absolute_error(y_train, y_pred):0.2f}")
print(f"Mean squared error: {mean_squared_error(y_train, y_pred):0.2f}")
print('\nDev set: ========>>')
print(f"Mean absolute error: {mean_absolute_error(y_dev, y_dev_pred):0.2f}")
print(f"Mean squared error: {mean_squared_error(y_dev, y_dev_pred):0.2f}")

# 使用 L2 正则化建模:通过penalty参数指定正则项
regularized_sgdr = SGDRegressor(n_iter=10, learning_rate='constant',
                            penalty='l2', alpha=0.01)    #alpha:正则项缩放
regularized_sgdr.fit(x_train, y_train)
# 查看模型参数
print("\nRegularized Model Coefficients: \n" + '='*66)
for i, coef in enumerate(regularized_sgdr.coef_):
    print(f"Coef_{i+1:d}:\t{coef:0.3f}")
print(f"Model Intercept:\t{regularized_sgdr.intercept_}")

(3)用梯度下降解决分类问题

分类问题除响应变量不同,其他结构与回归问题类似。由于响应变量的性质,需要不一样的损失函数。对于二分类问题,应用逻辑斯蒂回归函数获取预测值:

$F(w, x_i) = 1 / (1 + e^{-x_i * w^T})$

上式被称为 $sigmoid$ 函数。当 $x_i$ 为很大的正数,函数值趋向 1,反之趋向 0。于是可以定义如下的 $log$ 损失函数:

$L(w, x_i) = -y_i * log(F(w, x_i)) - (1-y_i) * log(1 - F(w, x_i))$

将上式代入 SGD 的权重更新公式,即得到分类问题的 SGD。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.linear_model import SGDClassifier


# generate dataset for classification
X, y = make_classification(n_samples=1000, n_features=30, flip_y=0.03,
                           n_informative=18, n_redundant=3, n_repeated=3,
                           random_state=1)
# split dataset
x_train, x_temp, y_train, y_temp = \
            train_test_split(X, y, test_size=0.3, random_state=1)
x_dev, x_test, y_dev, y_test = \
            train_test_split(x_temp, y_temp, test_size=0.3, random_state=1)


# build model and predict
sgdc = SGDClassifier(n_iter=50, loss='log',    #使用log损失函数
                     learning_rate='constant', eta0=1e-4, penalty='none')
sgdc.fit(x_train, y_train)
y_pred = sgdc.predict(x_train)
y_dev_pred = sgdc.predict(x_dev)

# evaluate model and show score
print("Training Accuracy: {0:0.2f}\tDev Accuracy: {1:0.2f}".format(\
        accuracy_score(y_train, y_pred), accuracy_score(y_dev, y_dev_pred)))

END