# 7장 앙상블 학습과 랜덤 포레스트

## 핵심 주제

예측기의 모음을 **앙상블**(ensemble),
앙상블을 이용한 예측기 학습을 **앙상블 학습**(ensemble learning),
앙상블 학습 알고리즘을 **앙상블 기법**(ensemble method) 등으로 부른다.

앙상블 기법을 적용하는 다양한 모델, 기법, 알고리즘 등을 살펴본다.

1. 다수결 분류기
1. 배깅/페이스팅
    1. 랜덤 패치와 랜덤 서브스페이스
1. 랜덤 포레스트
1. 부스팅
    1. 에이다부스트
    1. 그레이디언트 부스팅
1. 스태킹

## 파이썬 기본 설정

* 파이선 버전 3.5 이상
* 사이킷런 버전 0.20 이상
* 모듈 불러오기
    * numpy
    * matplotlib
    * matplotlib.pyplot
* 그림 저장 위치 지정

```python
# 파이썬 ≥3.5 필수
import sys
assert sys.version_info >= (3, 5)

# 사이킷런 ≥0.20 필수
import sklearn
assert sklearn.__version__ >= "0.20"

# 공통 모듈 임포트
import numpy as np
import os

# 노트북 실행 결과를 동일하게 유지하기 위해
np.random.seed(42)

# 깔끔한 그래프 출력을 위해
%matplotlib inline
import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.rc('axes', labelsize=14)
mpl.rc('xtick', labelsize=12)
mpl.rc('ytick', labelsize=12)

# 그림을 저장할 위치
PROJECT_ROOT_DIR = "."
CHAPTER_ID = "ensembles"
IMAGES_PATH = os.path.join(PROJECT_ROOT_DIR, "images", CHAPTER_ID)
os.makedirs(IMAGES_PATH, exist_ok=True)

def save_fig(fig_id, tight_layout=True, fig_extension="png", resolution=300):
    path = os.path.join(IMAGES_PATH, fig_id + "." + fig_extension)
    print("그림 저장:", fig_id)
    if tight_layout:
        plt.tight_layout()
    plt.savefig(path, format=fig_extension, dpi=resolution)
```

## 핵심 1: 다수결 분류기

**다수결 분류기**(voting classifier)의 기본 개념은
동일한 훈련 세트에 대해 여러 종류의 분류기를 학습시킨 후 각 분류기의 예측을 모아
다수결 방식으로 예측값을 결정하는 것이다.
이와 같이 작동하는 분류기를 **직접 투표**(hard voting) 분류기라 부른다.

반면에 사용되는 분류기가 모두 클래스에 포함될 확률을 계산하는 `predict_proba()` 메서드를
지원한다면 사용된 분류기의 확률 예측을 평균 내어 가장 높은 확률의 클래스를 선택할 수 있다.
이렇게 작동하는 분류기를 **간접 투표**(soft voting) 분류기라 부른다.
확류리 높은 투표에 비중을 더 두기 때문에 직접 투표 방식보다 좀 더 좋은 성능을 발휘한다.

아래 그림은 다수결 분류기의 기본적인 작동과정을 보여준다.

<그림출처> [mlxtend: EnsembleVoteClassifier](http://rasbt.github.io/mlxtend/user_guide/classifier/EnsembleVoteClassifier/)

다수결 분류기를 사용하면 하나의 성능이 좋은 분류기를 사용하는 것보다 일반적으로
정확한 예측을 한다.
단, 다수결 분류기에 포함된 분류기들 사이의 독립성이 전제되어야 한다.
독립성이 제대로 보장되지 못하면 다수결 분류기의 성능이 성능이 낮아질 수도 있다.
독립성이 어느 정도 이상 보장된 분류기를 이용하여 앙상블 학습을 하면 보다 좋은
성능의 분류기가 만들어지는 이유를 확률적으로 설명할 수 있다.

앞면이 나올 확률이 51%로 약간 편향된 동전을 이용하여 동전 던지기 실험을 해보자.

* 동전을 1,000번 던지면 동전의 앞면이 나오는 비율이 50% 이상일 확률은 74.7% 이다.
* 동전을 10,000번 던지면 동전의 앞면이 나오는 비율이 50% 이상일 확률은 97.8% 이다.

**참고**: 위 확률은 이항분포의 누적분포함수를 이용하여 계산할 수 있다.

```
from scipy.stats import binom
1-binom.cdf(499, 1000, 0.51)     # 0.747
1-binom.cdf(4999, 10000, 0.51)   # 0.978
```

아래 코드는 동전 10,000번 던지기를 10번 시행하는 것을
모의실험으로 보여준다.
앞서 언급한 확률대로라면 10,000번 던지기를 100번 하면 98번 정도는
앞면이 나올 확률이 50% 이상이다.
따라서 10,000번 던지기를 10번 하면 앞면이 50% 이하로 나오는 일은 사실상 없어야 하는데
아래 모의실험이 이를 뒷받침한다.

```python
np.random.seed(42)

heads_proba = 0.51
coin_tosses = (np.random.rand(10000, 10) < heads_proba).astype(np.int32)
cumulative_heads_ratio = np.cumsum(coin_tosses, axis=0) / np.arange(1, 10001).reshape(-1, 1)

plt.figure(figsize=(8,3.5))
plt.plot(cumulative_heads_ratio)
plt.plot([0, 10000], [0.51, 0.51], "k--", linewidth=2, label="51%")
plt.plot([0, 10000], [0.5, 0.5], "k-", label="50%")
plt.xlabel("Number of coin tosses")
plt.ylabel("Heads ratio")
plt.legend(loc="lower right")
plt.axis([0, 10000, 0.42, 0.58])
save_fig("law_of_large_numbers_plot")
plt.show()
```

위 결과의 의미는 다음과 같다.

> 앞면이 나올 확률을 분류기의 정확도라고 하면,
> 그런 분류기의 성능은 무작위로 찍는 것보다 아주 조금 좋은 정도이다.
> 그런데 그런 분류기를 1,000 개 사용하여 클래스를 예측하고,
> 가장 많이 예측된 클래스를 최종 선택하면 그 결과가 맞을 확률이 74.7%이고,
> 그런 분류기를 10,000 개 사용하면 정확도가 97.8%까지 올라간다.

물론, 이것은 사용되는 분류기가 완벽하게 서로 독립적이어서 서로의 결과에 어떤 영향도 주지 않아야 한다.
하지만 동전 던지기와는 달리 동일한 훈련 세트를 사용해야 하는 모델 학습에서는
절대로 그런 상호 독립성을 보장할 수 없다.
분류기들이 동종의 오차를 만들 가능성이 기본적으로 높기 때문이며, 따라서 앙상블 모델의 정확도는
이론보다 많이 낮아진다.

### 사이킷런의 다수결 분류기

사이킷런의 `VotingClassifier`가 다수결 분류기 모델을 제공한다.
훈련 데이터셋으로 5장에서 사용한 moons 데이터셋이다.

```python
from sklearn.model_selection import train_test_split
from sklearn.datasets import make_moons

X, y = make_moons(n_samples=500, noise=0.30, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
```

다음은 로지스틱 회귀, 랜덤 포레스트, 서포트 벡터머신 세 분류기를
이용하여 다수결 분류기를 생성한 후 학습한다.

* `voting='hard'`: 직접 투표 방식 선택
    * 간접 투표 방식을 선택하려면 `'hard'` 대신에 `'soft'` 지정해야 함.

**참고**: 사이킷런의 향후 버전과의 호환성을 위해 다음 옵션 인자를 사용함.
* `solver="lbfgs"`
* `n_estimators=100`
* `gamma="scale"`.

사용된 네 개의 분류기의 성능을 정확도를 이용하여 확인할 수 있다.
다수결 분류기의 정확도가 91.2%로 개별 분류기보다 조금 더 성능이 좋다.

```python
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import VotingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC

# 선형 회귀
log_clf = LogisticRegression(solver="lbfgs", random_state=42)
# 랜덤 포레스트
rnd_clf = RandomForestClassifier(n_estimators=100, random_state=42)
# 서포트 벡터 머신
svm_clf = SVC(gamma="scale", random_state=42)

# 다수결 분류기: 직접 투표 방식
voting_clf = VotingClassifier(
    estimators=[('lr', log_clf), ('rf', rnd_clf), ('svc', svm_clf)], voting='hard')

# 다수결 분류기 학습
voting_clf.fit(X_train, y_train)

from sklearn.metrics import accuracy_score

# 분류기 별 정확도 측정
for clf in (log_clf, rnd_clf, svm_clf, voting_clf):
    clf.fit(X_train, y_train)
    y_pred = clf.predict(X_test)
    print(clf.__class__.__name__+':\t', accuracy_score(y_test, y_pred))
```

    LogisticRegression:	 0.864
    RandomForestClassifier:	 0.896
    SVC:	 0.896
    VotingClassifier:	 0.912

간접 투표 방식을 사용하면 정확도가 92%로 조금 더 올라간다.

* `voting='soft'` 사용.
* SVC 모델의 경우 `predict_proba()` 메서드를 사용하기 위해
    `probability=True` 옵션 인자를 지정해야 한다.

```python
log_clf = LogisticRegression(solver="lbfgs", random_state=42)
rnd_clf = RandomForestClassifier(n_estimators=100, random_state=42)
svm_clf = SVC(gamma="scale", probability=True, random_state=42)

# 간접 투표 방식
voting_clf = VotingClassifier(
    estimators=[('lr', log_clf), ('rf', rnd_clf), ('svc', svm_clf)], voting='soft')

voting_clf.fit(X_train, y_train)

from sklearn.metrics import accuracy_score

for clf in (log_clf, rnd_clf, svm_clf, voting_clf):
    clf.fit(X_train, y_train)
    y_pred = clf.predict(X_test)
    print(clf.__class__.__name__+':\t', accuracy_score(y_test, y_pred))
```

    LogisticRegression:	 0.864
    RandomForestClassifier:	 0.896
    SVC:	 0.896
    VotingClassifier:	 0.92

## 핵심 2: 배깅/페이스팅

훈련 세트의 부분집합을 임의로 구성한 후 동일한 분류기를
생성된 부분 집합을 이용하여 학습시키는 방식으로 앙상블 학습을 진행할 수 있다.
즉, 사용되는 분류기 모델은 하나이지만,
훈련 세트를 임의로 다르게 하여 여러 번 학습시킨다.

훈련 세트의 부분집합을 임의로 선택할 때 중복 허용 여부에 따라
앙상블 학습 방식이 달라진다.

* **배깅**(bootstrap aggregating): 중복 허용 샘플링
* **페이스팅**(pasting): 중복 미허용 샘플링

**참고**: 통계학에서 중복을 허용하는 표본 추출 방식을 **부트스트래핑**(bootstrapping)이라 부른다.

아래 그림은 배깅 방식을 묘사한다.

<그림출처> [WikimediaCommons: Bagging](https://commons.wikimedia.org/wiki/File:Bagging.png)

### 앙상블 학습 예측 방식

앙상블 학습 예측기는 최종적으로 **종합 함수**(aggregation function)가
각 개별기의 결과를 종합해서 최종 예측값을 지정한다.

* 분류 모델의 경우: 직접 투표 방식 사용. 즉, 수집된 예측값들 중에서 최빈값(mode) 선택.
* 회귀 모델의 경우: 수집된 예측값들의 평균값 선택.

### 편향과 분산

앙상블 학습을 이용한 예측기의 경우 개별 예측기의 경우에 비해
일반적으로 편향은 비슷하지만 분산은 줄어든다.
즉, 과대적합의 위험성이 줄어든다.

반면에 개별 학습기의 경우, 배깅/페이스팅 방식으로 학습하면
전체 훈련 세트를 대상으로 학습한 경우에 비해
편향이 커지는데, 이는 훈련 세트가 작아지면 학습이 제대로 이루지지 않기 때문에
당연한 결과이다. 즉, 과소적합 위험성이 커질 수밖에 없다.

### 사이킷런의 배깅/페이스팅

사이킷런에서 제공하는 배깅/페이스팅 모델은 다음과 같다.

* 분류 모델: `BaggingClassifier`
* 회귀 모델: `BaggingRegressor`

아래 코드는 의사결정나무를 이용하여 배깅 앙상블 학습을 진행한다.
사용된 모델 옵션은 다음과 같다.

* `n_estimators=500`: 500개의 의사결정나무 학습기 훈련
* `max_samples=100`: 각각의 의사결정나무 분류기에 사용될 훈련 샘플 수가 100개. 기본값은 1.0
    * 정수일 경우 지정된 수 만큼의 샘플 사용
    * 부동소수점 ($\in [0, 1]$): 지정된 비율만큼의 샘플 사용. 즉, 기본값은 전체 샘플 수만큼 사용.
* `bootstrap=True`: 중복 허용 표본 추출. 기본값은 `True`, 즉, 기본적으로 배깅 지원.
* `n_jobs=-1`: 사용할 코어 수. -1은 가능한 모드 코어 사용.

또한 의사결정나무의 경우처럼 `predict_proba()` 메서드가 지원되는 경우
자동으로 간접 투표(soft voting) 방식이 사용된다.

```python
from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier

bag_clf = BaggingClassifier(
    DecisionTreeClassifier(random_state=42),
    n_estimators=500,
    max_samples=100,
    # bootstrap=True 가 기본값임.
#     bootstrap=True,
    n_jobs=-1,
    random_state=42)

bag_clf.fit(X_train, y_train)
y_pred = bag_clf.predict(X_test)
```

앙상블 학습 결과의 정확도는 90.4%이다.

```python
from sklearn.metrics import accuracy_score

print(accuracy_score(y_test, y_pred))
```

    0.904

반면에 하나의 의사결정나무의 정확도는 85.6%이다.

```python
tree_clf = DecisionTreeClassifier(random_state=42)
tree_clf.fit(X_train, y_train)
y_pred_tree = tree_clf.predict(X_test)

print(accuracy_score(y_test, y_pred_tree))
```

    0.856

#### 분류 그래프 그리기 함수

아래 코드에서 정의된 함수 `plot_decision_boundary` 함수는
지정된 분류기가 지정된 훈련 세트를 대상으로 학습한 결과와 결정경계(decision boundary)를
그래프로 보여준다.

```python
from matplotlib.colors import ListedColormap

def plot_decision_boundary(clf, X, y, axes=[-1.5, 2.45, -1, 1.5], alpha=0.5, contour=True):
    x1s = np.linspace(axes[0], axes[1], 100)
    x2s = np.linspace(axes[2], axes[3], 100)
    x1, x2 = np.meshgrid(x1s, x2s)
    X_new = np.c_[x1.ravel(), x2.ravel()]
    y_pred = clf.predict(X_new).reshape(x1.shape)
    custom_cmap = ListedColormap(['#fafab0','#9898ff','#a0faa0'])
    plt.contourf(x1, x2, y_pred, alpha=0.3, cmap=custom_cmap)
    if contour:
        custom_cmap2 = ListedColormap(['#7d7d58','#4c4c7f','#507d50'])
        plt.contour(x1, x2, y_pred, cmap=custom_cmap2, alpha=0.8)
    plt.plot(X[:, 0][y==0], X[:, 1][y==0], "yo", alpha=alpha)
    plt.plot(X[:, 0][y==1], X[:, 1][y==1], "bs", alpha=alpha)
    plt.axis(axes)
    plt.xlabel(r"$x_1$", fontsize=18)
    plt.ylabel(r"$x_2$", fontsize=18, rotation=0)
```

아래 코드는 의사결정나무 하나를 이용한 분류기와 앞서 학습한 배깅 방식 앙상블 학습 결과를 보여준다.

* 왼편 그래프: 의사결정나무 하나로 학습한 분류 결과
* 오른편 그래프: 의사결정나무와 배깅 방식으로 학습한 분류 결과

앙상블 학습의 일반화가 보다 좋으며, 편향은 비슷한 반면에 분산이 더 작다.

* 편향: 잘못된 예측 정도는 두 방식이 거의 비슷함.
* 분산: 과대적합 정도는 왼편이 보다 강함.

```python
fix, axes = plt.subplots(ncols=2, figsize=(10,4), sharey=True)

# 왼편 그래프
plt.sca(axes[0])
plot_decision_boundary(tree_clf, X, y)
plt.title("Decision Tree", fontsize=14)

# 오른편 그래프
plt.sca(axes[1])
plot_decision_boundary(bag_clf, X, y)

plt.title("Decision Trees with Bagging", fontsize=14)
plt.ylabel("")
save_fig("decision_tree_without_and_with_bagging_plot")
plt.show()
```

#### 배깅 대 페이스팅

중복선택을 허용하는 배깅 방식이 사용되는 샘플 부분집합의 다양성을 좀 더 키우며,
다음 효과를 가져온다.

* 일반적으로 배깅 방식이 좀 더 편향은 키우고, 분산은 줄임.
    * 표본 샘플링의 무작위성을 커지면서 예측기들 사이의 상관관계 정도가 약화되기 때문임.
* 전반적으로 배깅 방식이 좀 더 나은 모델 생성.
* 교차검증을 통해 항상 확인 필요.

### oob 평가

배깅을 사용하면 한 번도 선택되지 못하는 샘플이 존재할 수 있다.
뽑히지 않을 확률을 계산할 수 있으며 이론적으로 37% 정도 된다.

실제로 $m$ 개의 샘플에서 임의로 선택되지 않을 확률은 $(1 - 1/m)$ 이며,
따라서 중복을 허용하면서 $m$ 번 뽑았을 때 뽑히지 않을 확률은 $(1 - 1/m)^m$ 이다.
이 값은 $m$이 증가함에 따라 0.368에 수렴한다.

$$
\lim_{m\to \infty} (1-1/m)^m = e^{-1} = 0.368
$$

선택되지 않은  훈련 샘플을 **oob(out-of-bag)** 샘플이라고 부른다.
oob 샘플은 예측기마다 다르며, 예측기가 학습 중에 oob 샘플을 사용하지 않으므로
이 샘플들을 검증 세트로 사용할 수 있다.
앙상블 모델 자체는 예측기별 oob 평가의 평균값을 이용하여 평가할 수 있다.

`BaggingClassifier` 의 `oob_score=True` 로 설정하면 oob 평가를 자동으로 실행한다.
평가 점수는 `oob_score_`에 저장된다.

* `max_samples` 옵션이 언급되지 았았기에 `max_samples=1.0` 기본 옵션이 사용된다.
    즉, 중복을 허용하면서 전체 샘플 수 만큼 표본 추출.
    따라서 전체 데이터의 37% 정도가 oob 샘플이 됨.

```python
bag_clf = BaggingClassifier(
    DecisionTreeClassifier(random_state=42),
    n_estimators=500,
    bootstrap=True,
    # max_samples=1.0이 기본값
#     max_samples=1.0
    oob_score=True,
    random_state=40)
bag_clf.fit(X_train, y_train)

bag_clf.oob_score_
```

    0.9013333333333333

oob 평가와 테스트 셋트에 대한 정확도가 비슷하게 나온다.

```python
from sklearn.metrics import accuracy_score
y_pred = bag_clf.predict(X_test)
accuracy_score(y_test, y_pred)
```

    0.912

oob 샘플에 대한 예측값은 클래스에 속할 확률을 이용하여 결정된다.
모든 테스트 샘플에 대한 클래스 예측 확률은 `oob_decision_function_` 속성에 저장된다.

`moons` 데이터 셋은 두 개의 클래스로 구성되었기 때문에
각 샘플 별로 길이가 2인 리스트가 된다.
처음 10개 샘플에 대한 클래스 예측 확률은 다음과 같다.
예를 들어, 첫째 샘플의 경우 0번(음성) 클래스에 속할 확률은 31.75% 이고
1번(양성) 클래스에 속할 확률은 68.25%이다.
(클래스별 예측 확률의 합은 1임)

```python
bag_clf.oob_decision_function_[:10]
```

    array([[0.31746032, 0.68253968],
           [0.34117647, 0.65882353],
           [1.        , 0.        ],
           [0.        , 1.        ],
           [0.        , 1.        ],
           [0.08379888, 0.91620112],
           [0.31693989, 0.68306011],
           [0.02923977, 0.97076023],
           [0.97687861, 0.02312139],
           [0.97765363, 0.02234637]])

### 랜덤 패치/랜덤 서브스페이스

`BaggingClassifier`는 특성을 대상으로 하는 두 개의 하이퍼파라미터를 제공한다.

* `max_features`:
    * 학습에 사용할 특성 수를 지정.
    * 특성 선택은 무작위.
        * 정수인 경우: 지정된 수만큼 특성 선택
        * 부동소수점($\in [0, 1]$)인 경우: 지정된 비율만큼 특성 선택
    * `max_samples`와 유사 기능 수행.
* `bootstrap_features`:
    * 학습에 사용할 특성을 선택할 때 중복 허용 여부 지정.
    * 기본값은 `False`.
    * `botostrap`과 유사 기능 수행.

이미지 등과 같이 매우 높은 차원의 데이터셋을 다룰 때 유용하게 사용될 수 있다.
특성 샘플링은 더 다양한 예측기를 만들며, 일반적으로 편향이 커지지만 분산은 낮아진다.
이유는 보다 적은 수의 특성을 사용하면 아무래도 예측이 부정확해질 수밖에 없는 반면에,
데이터 샘플에 대해 보다 덜 민감하게 반응하게 된다.

훈련 세트와 특성에 대한 샘플링 방식에 따라 아래 두 가지 기법으로 나뉘어진다.

#### 랜덤 패치 기법

훈련 샘플과 훈련 특성 모두를 대상으로 중복을 허용하며 임의의 샘플 수와 임의의 특성 수만큼을 샘플링 해서
학습하는 기법이 **랜덤 패치 기법**이다.
즉, 다음 두 조건이 참이어야 한다.

* `bootstrap=True` 또는 `max_samples` < 1.0
* `bootstrap_features=True` 또는 `max_features` < 1.0

#### 랜덤 서브스페이스 기법

전체 훈련 세트를 학습 대상으로 삼지만 훈련 특성은 임의의 특성 수만큼 샘플링 해서
학습하는 기법이 **랜덤 서브스페이스 기법**이다.
즉, 다음 두 조건이 참이어야 한다.

* `bootstrap=False` 그리고 `max_samples=1.0`
* `bootstrap_features=True` 또는 `max_features` < 1.0

## 핵심 3: 랜덤 포레스트

배깅/페이스팅 방법을 적용한 의사결정나무의 앙상블을 최적화한 모델이 **랜덤 포레스트**(random forest)이다.

아래 코드는 사이킷런에서 `RandomFrestClassifier`를 이용하여 랜덤 포레스트를 구현한다.

* `n_estimators=500`: 500개의 의사결정나무 학습
* `max_leaf_nodes=16`: 사용되는 의사결정나무의 잎의 수를 16개로 제한
* `n_jobs=-1`: 가용 가능한 모든 CPU 사용

**참조**: 회귀용 예측기는 `RandomForestRegressor`가 지원한다.

```python
from sklearn.ensemble import RandomForestClassifier

rnd_clf = RandomForestClassifier(
    n_estimators=500,
    max_leaf_nodes=16,
    n_jobs=-1,
    random_state=42)

rnd_clf.fit(X_train, y_train)
y_pred_rf = rnd_clf.predict(X_test)
```

### 램덤 포레스트와 앙상블 학습: 번역서 버전

아래 코드는 위 코드를
`DecisionTreeClassifier`를 `BaggingClassifier`와
함께 사용한 앙상블 모델이다.

사용된 `DecisionTreeClassifier`의 특성은 다음과 같다.

* `max_leaf_nodes=16`: 잎의 수를 16개로 제한함.
* `max_samples=1.0`
* `max_features="auto"`: 학습에 사용되는 특성의 수를 전체 특성 수의 제곱근 값으로 제한한다.
    즉, `max_features=sqrt(n_features)` 와 동일하게 작동한다.

이 모델은 `splitter=best` 기본 옵션을 사용한다.
따라서 마디에 사용되는 특성과 임곗값을 선택하기 위해 모든 가능성을 확인하기 때문에
특성 수가 매우 크면 제곱근으로 특성 수를 줄인다 하더라도
시간이 오래 걸릴 수 있다.

```python
bag_clf_auto = BaggingClassifier(
    DecisionTreeClassifier(max_features="auto", max_leaf_nodes=16, random_state=42),
    n_estimators=500,
    # max_samples=1.0 이 기본값임.
    max_samples=1.0,
    # bootstrap=True 가 기본값임.
    bootstrap=True,
    n_jobs=-1,
    random_state=42)

bag_clf_auto.fit(X_train, y_train)
y_pred_auto = bag_clf_auto.predict(X_test)
```

그러면 랜덤 포레스트의 결과와 완벽히 일치한다.
아래 코드는 랜덤 포레스트 모델과 배깅 모델이 동일한 예측을 한다는 것을 보여준다.

* `y_pred_auto == y_pred_rf`: 두 넘파이의 항목별 비교. 항목의 값이 같으면 1, 다르면 0.
* `np.sum(y_pred_auto == y_pred_rf)`: 동일한 항목의 수

```python
np.sum(y_pred_auto == y_pred_rf) / len(y_pred)
```

    1.0

### 램덤 포레스트와 앙상블 학습: 영어 원서 버전

영어 원서에는 아래와 같이 약가 다르게 설정된 배깅 방식으로 작성되었다.
사용된 `DecisionTreeClassifier`의 특성은 다음과 같다.

* `splitter="random"`: 사용되는 특성 중에 무작위로 선택된 특성과 임곗값을 대상으로 최고의 특성과 임곗값 선택
* `max_leaf_nodes=16`: 잎의 수를 16개로 제한함.
* `max_samples=1.0`

이 모델은 `max_features=None`인 기본값을 사용하기에 특성 전체를 대상으로 무작위 특성 선택을 진행한다.
속도는 번역서 버전보다 빠르지만 조금 더 편향이 커진다.
결과도 랜덤 포레스트와 100% 일치하지는 않는다.

```python
bag_clf_rand = BaggingClassifier(
    DecisionTreeClassifier(splitter="random", max_leaf_nodes=16, random_state=42),
    n_estimators=500,
    # max_samples=1.0 이 기본값임.
    max_samples=1.0,
    # bootstrap=True 가 기본값임.
    bootstrap=True,
    n_jobs=-1,
    random_state=42)

bag_clf_rand.fit(X_train, y_train)
y_pred_rand = bag_clf_rand.predict(X_test)
```

두 모델에 의한 예측이 97.6% 동일하다.

```python
np.sum(y_pred_rand == y_pred_rf) / len(y_pred)
```

    0.976

### 엑스트라 트리

앞서 설명하였듯이 랜덤 포레스트는 의사결정나무의 마디를 결정할 때 사용할 특성을 무작위로 선택된
특성과 임곗값 전체를 대상으로 해서 그 중에 최적의 조합을 선택한다.
즉, 의사결정나무의 아래 옵션을 사용하는 것과 동일하다.

* `splitter="best"`: 의사결정나무의 마디 속성을 정할 때 사용 대상 특성 전체를 대상으로해서 최적의 특성과 임곗값 선택.
* `max_features="auto"`: 학습에 사용되는 특성의 수를 전체 특성 수의 제곱근 값으로 제한.

여기에 더해 최적의 임곗값을 찾는 대신 선택된 특성 중에서 일부만을 무작위로 대상으로 삼아
임계값을 무작위로 선택해서 샘플을 분할한 다음 그 중에 최선의 분할을 선택하는 방식을 사용할 수도 있다.
즉, 아래 옵션을 함께 사용한다.

* `splitter="random"`: 의사결정나무의 마디 지정에 사용될 특성과 임곗값을 무작위로 지정해서 분할한 다음
    그 중에 최선의 분할 선택.
* `max_features="auto"`: 학습에 사용되는 특성의 수를 전체 특성 수의 제곱근 값으로 제한.

이렇게 하면 특성 수가 매우 클 때 일반적인 랜덤 포레스트보다 속도가 훨씨 빠르다.
이런 방식을 사용하는 랜덤 포레스트를 **엑스트라 트리**(extra-trees) 또는
**익스트림 랜덤 트리**(extremely randomized trees)라고 부른다.
이 방식을 사용하면 역시 편향은 늘고, 분산은 줄어든다.

아래 코드는 `BaggingClassifier`와 의사결정나무를 이용하여 엑스트라 트리가
작동하는 방식을 구현한 것이다.
앞서 사용한 랜덤 포레스트와의 차이가 좀 더 벌어졌음을 확인할 수 있다.

```python
bag_clf_randAuto = BaggingClassifier(
    DecisionTreeClassifier(splitter="random",
                           max_features="auto",
                           max_leaf_nodes=16,
                           random_state=42),
    n_estimators=500,
    # max_samples=1.0 이 기본값임.
    max_samples=1.0,
    # bootstrap=True 가 기본값임.
    bootstrap=True,
    n_jobs=-1,
    random_state=42)

bag_clf_randAuto.fit(X_train, y_train)
y_pred_randAuto = bag_clf_randAuto.predict(X_test)
```

이때 두 모델에 의한 예측이 96.8%로 일치한다.

```python
np.sum(y_pred_randAuto == y_pred_rf) / len(y_pred)
```

    0.968

사이킷런의 `ExtraTreesClassifier`가 앞서 설명한 엑스트라 트리를 구현한다.

```python
from sklearn.ensemble import ExtraTreesClassifier

extra_clf = ExtraTreesClassifier(
    n_estimators=500,
    max_leaf_nodes=16,
    n_jobs=-1,
    random_state=42)

extra_clf.fit(X_train, y_train)
y_pred_extra = extra_clf.predict(X_test)
```

앞서 구현한 `bag_clf_randAuto`와 완벽하게 동일한 예측을 한다.

```python
np.sum(y_pred_randAuto == y_pred_extra) / len(y_pred)
```

    1.0

앙상블 모델의 하이퍼파라미터에 따라 약간의 차이가 있지만 별 의미를 둘 필요는 없다.
아주 큰 데이터셋, 보다 큰 특성 수를 다뤄야 하는 경우에는
훈련 데이터터와 특성을 무작위적으로 선택하는 게 보다 효율적인데
어쩔 수 없이 약간의 오차는 발생하게 된다.
그리고 이렇게 하면 편향은 약간 높아지지만 분산은 낮아져서 전체적으로 보다
나은 모델을 얻게 된다.

결론적으로 `RandomForestClassifier`와 `ExtraTreesClassifier` 중에
누가 더 좋을지는 미리 알 수 없으며 교차 검증으로 확인하는 수 밖에 없다.
또한 그리드 탐색을 이용하여 하이퍼파라미터를 튜닝할 수도 있다.

### 특성 중요도

랜덤 포레스트는 학습에 사용된 특성의 상대적 중요도도 함께 측정한다.
특성 중요도는 해당 특성을 사용한 마디가 평균적으로 불순도를 얼마나 감소시키는지를 측정한다.
즉, 불순도를 많이 줄이면 그만큼 중요도가 커진다.

사이킷런의 `RandomForestClassifier`는 훈련이 끝난 뒤 특성별 중요도의
전체 합이 1이 되도록 하는 방식으로 특성별 상대적 중요도를 측정한 후
`feature_importances_` 속성에 저장한다.

#### 예제: 붓꽃 데이터셋

아래 코드는 붓꽃 데이터셋에 대해 랜덤 포레스트를 적용한 후
특성별 중요도를 평가한 자료를 확인해준다.

* 꽃잎 길이(petal length): 44.1%
* 곷잎 너비(petal width): 42.3%
* 꽃받침 길이(sepal length): 11.3%
* 곷받침 너비(sepal width): 2.3%

```python
from sklearn.datasets import load_iris

iris = load_iris()
rnd_clf = RandomForestClassifier(n_estimators=500, random_state=42)
rnd_clf.fit(iris["data"], iris["target"])

for name, score in zip(iris["feature_names"], rnd_clf.feature_importances_):
    print(name, score)
```

    sepal length (cm) 0.11249225099876375
    sepal width (cm) 0.02311928828251033
    petal length (cm) 0.4410304643639577
    petal width (cm) 0.4233579963547682

#### 예제: MNIST 손글씨 데이터셋

3장에서 다룬 MNIST 데이터셋에서 다룬 숫자 이미지는 `28x28=784` 개의 픽셀을
샘플 특성으로 사용하였다.
MNIST 데이터셋에 랜덤 포레스트 분류기를 훈련시키고 각 픽셀의 중요도를 확인할 수 있다.

```python
from sklearn.datasets import fetch_openml

mnist = fetch_openml('mnist_784', version=1)
mnist.target = mnist.target.astype(np.uint8)

rnd_clf = RandomForestClassifier(n_estimators=100, random_state=42)
rnd_clf.fit(mnist["data"], mnist["target"])
```

    RandomForestClassifier(bootstrap=True, class_weight=None, criterion='gini',
                           max_depth=None, max_features='auto', max_leaf_nodes=None,
                           min_impurity_decrease=0.0, min_impurity_split=None,
                           min_samples_leaf=1, min_samples_split=2,
                           min_weight_fraction_leaf=0.0, n_estimators=100,
                           n_jobs=None, oob_score=False, random_state=42, verbose=0,
                           warm_start=False)

이전에 설명한 대로 손글씨 숫자는 이미지 중앙에 그려지고 테두리는 흰색으로 채워져 있어서
테두리로 갈수록 중요도가 떨어진다.
아래 코드가 이것을 그림으로 확인해준다.

`plot_digit()` 함수는 중요도를 `cmap=mpl.cm.hot` 속성을 이용하여 표현한다.
어두운 색은 낮은 중요도를, 밝은 색은 높은 중요도를 보여준다.

```python
def plot_digit(data):
    # 28x28 사이즈로 형태 변환
    image = data.reshape(28, 28)
    # 그림 그리기
    plt.imshow(image, cmap = mpl.cm.hot, interpolation="nearest")
    # 축은 사용하지 않음.
    plt.axis("off")

# 이미지 속성 중요도 색으로 표현
plot_digit(rnd_clf.feature_importances_)

# 색상의 중요도를 보여주는 참조용 막대그래프. 오른편에 위치.
cbar = plt.colorbar(ticks=[rnd_clf.feature_importances_.min(), rnd_clf.feature_importances_.max()])
cbar.ax.set_yticklabels(['Not important', 'Very important'])

save_fig("mnist_feature_importance_plot")
plt.show()
```

## 핵심 4: 부스팅

**부스팅**(boosting)은 성능이 약한 학습기의 여러 개를 선형으로 연결하여 강한 성능의 학습기를 만드는
앙상블 기법을 의미한다.
즉, 여러 개의 학습기가 동시에 학습하는 것이 아니라 **순차적으로 이전 학습기의 결과를 바탕으로
성능을 조금씩 높혀가는 방식**으로 학습을 이어간다.
이런 이유로 배깅/페이스팅에 비해 확장성이 떨어진다는 단점을 갖는다.

이와같이 성능이 약한 예측기의 단점을 보완하여 보다 좋은 성능의 예측기를 훈련해 나가는 것이
**부스팅**의 기본 아이디어이다.
여기서는 가장 인기 높은 **에이다부스트**(AdaBoost)와 **그레이디언트부스팅**(Gradient Boosing)을 소개한다.

**참조**:
* 부스트(boost)의 사전적 의미는 늘리다, 활성화하다 등이다.
* 에이다부스트(AdaBoost)는 Adaptive Boosting의 줄임말이다.

### 에이다부스트(AdaBoost)

이전 모델이 제대로 학습하지 못한 샘플들에 대한 가중치를 조절하여 새로운 모델을 만들어 가는 방식을
**에이다부스트**라 부른다.
이 방식을 사용하면 새로운 예측기는 학습하기 어려운 샘플에 조금씩 더 잘 적응하는 모델이 연속적으로 만들어져 간다.

<그림출처> [Medium: Understanding AdaBoost for Decision Tree](https://miro.medium.com/max/1400/0*KYszvMnr3nCtjaGy.png)

#### 에이다부스트 알고리즘

에이다부스트 알고리즘을 설명하기 전에 먼저 아래 사항을 전제한다.

* $m$: 훈련 세트 크기
* $N$: 사용되는 예측기 수
* $\eta$: 학습률

에이다부스트 알고리즘 작동방식은 다음과 같다.

1. 먼저 각 **샘플에 대한 가중치** $w^{(i)}$ 를 $1/m$로 설정.
    <br><br>
1. $j = 1, 2, ..., N$을 대상으로 아래 과정을 순차적으로 반복한다.
    <br><br>
    1. $j$ 번 째 예측기 학습
    <br><br>
    1. 가중치 적용 에러율 $r_j$ 계산.
    <br>
    $$
    r_j = \Big( \sum_{i=1,\,\, \hat y_j^{(i)} \neq y^{(i)}}^{m} w^{(i)} \Big) \Big/
    \Big( \sum_{i=1}^{m} w^{(i)} \Big)
    $$
    <br>
    1. **예측기 가중치** $\alpha_j$ 계산
    <br>
    $$
    \alpha_j = \eta \, \log\Big( \frac{1-r_j}{r_j} \Big)
    $$
    <br>
    1. 각 샘플에 대한 가중치 업데이트
     <br>
    $$
    w(i) \gets \begin{cases} w^{(i)} & \text{if }\,\,  \hat y_j^{(i)} = y^{(i)} \\ w^{(i)}\,\exp(\alpha_j) & \text{if }\,\,  \hat y_j^{(i)} \neq y^{(i)}\end{cases}
    $$
    <br>
1. $j=N$이 되거나 완벽한 예측기가 학습되면 위 과정을 멈춘다.

#### 알고리즘의 수학적 의미

사용된 수식의 의미는 다음과 같다.

* 가중치 적용 에러율 $r_j$
    * 예측값이 실제와 다른 샘플에 사용된 가중치가 차지하는 비중 계산
    * 즉, $j$번 째 예측기의 정확도를 낮추는 샘플에 사용되는 가중치의 비중 측정
    <br><br>
* 예측기 가중치 $\alpha_j$와 샘플 가중치 업데이트
    <br><br>
    * $r_j \le 0.5$ 인 경우
        * 예측기의 적중율에 미치는 가중치 적용 에러율이 높지 않음을 의미.
        * 즉, 예측값이 틀린 샘플에 사용된 가중치의 역할이 크지 않았음을 반증함.
        * 따라서 예측값이 틀린 샘플에 사용된 가중치를 좀 더 높게 설정할 필요 있음.
        * 수학적으로 아래와 같이 설명됨.
            * $\frac{1-r_j}{r_j} \ge 1$, 따라서 $\alpha_j \ge 0$.
            * $\exp(\alpha_j) \ge 1$이 되어 $w^{(i)}$ 값 증가시킴(단, $\hat y_j^{(i)} \neq y^{(i)}$).
    <br><br>
    * $r_j > 0.5$ 인 경우
        * 예측기의 적중율에 미치는 가중치 적용 에러율이 높음을 의미.
        * 즉, 예측값이 틀린 샘플에 사용된 가중치의 역할이 컸음을 반증함.
        * 따라서 예측값이 틀린 샘플에 사용된 가중치를 좀 더 낮게 설정할 필요 있음.
        * 수학적으로 아래와 같이 설명됨.
            * $\frac{1-r_j}{r_j} < 1$, 따라서 $\alpha_j < 0$.
            * $\exp(\alpha_j) < 1$이 되어 $w^{(i)}$ 값 감소시킴(단, $\hat y_j^{(i)} \neq y^{(i)}$).

#### 최종 예측값

모든 예측기의 예측을 계산하고 가장 높은 비중의 가중치가 사용된 예측값을 선택한다.

\begin{equation}
\hat y(\mathbf x) = \mathrm{argmax}_k\sum^{N}_{j=1, \,\, \hat y_j(\mathbf x) = k} \alpha_j
\end{equation}

즉, 가중치를 키웠을 때의 예측값이 가장 많이 선택되었다면 최종적으로 그 값을 선택한다는 의미이다.

#### 사이키런의 에이다부스트

사이킷런은 `AdaBoostClassifier` 분류 모델과 `AdaBoostRegressor` 회귀 모델을 지원한다.
두 모델이 사용하는 알고리즘은 앞서 설명한 알고리즘을 다중 클래스 버전으로 일반화한
**SAMME** 라는 알고리즘을 사용한다.

사용되는 예측기가 `predict_proba()` 메서드를 지원하면
클래스에 속할 확률을 이용하는
**SAMME.R** 이라는 약간 변형된 알고리즘을 사용하며, 일반적으로 보다 성능이 좋다.

아래 코드는 최대 깊이가 1인 의사결정나무 200개를 이용하는 에이다부스트 분류기를 구현하며,
이것이 에이다부스트의 기본 설정이다.

* `max_depth=1`: 잎이 아닌 마디가 단 하나뿐임. 즉, 뿌리 마디와 여러 개의 잎만 존재하는 의사결정나무.

```python
from sklearn.ensemble import AdaBoostClassifier

ada_clf = AdaBoostClassifier(
    DecisionTreeClassifier(max_depth=1),
    n_estimators=200,
    algorithm="SAMME.R",
    learning_rate=0.5,
    random_state=42)

ada_clf.fit(X_train, y_train)
```

    AdaBoostClassifier(algorithm='SAMME.R',
                       base_estimator=DecisionTreeClassifier(class_weight=None,
                                                             criterion='gini',
                                                             max_depth=1,
                                                             max_features=None,
                                                             max_leaf_nodes=None,
                                                             min_impurity_decrease=0.0,
                                                             min_impurity_split=None,
                                                             min_samples_leaf=1,
                                                             min_samples_split=2,
                                                             min_weight_fraction_leaf=0.0,
                                                             presort=False,
                                                             random_state=None,
                                                             splitter='best'),
                       learning_rate=0.5, n_estimators=200, random_state=42)

학습 결과는 다음과 같다.
학습 결과가 과대적합된 것으로 판단되면 추정기 수를 줄이거나 보다 강한 규제를 사용할 수 있다.

```python
plot_decision_boundary(ada_clf, X, y)
```

#### 에이다부스트 직접 구현하기

아래 코드는 서포트벡터머신을 이용하여 에이다부스트 알고리즘과
비슷하게 작동하는 코드이다.

* `rbf` 커널을 사용하는 `SVC` 모델
* 예측값이 틀린 샘플의 가중치를 5번 업데이트할 때의 학습과정을 그래프로 보여줌.
* 왼편 그림: 학습률 1.0
    * 학습이 널뛰는 것을 확인 가능
* 오른편 그림: 학습률 0.5
    * 학습을 잘함.
* 훈련 세트는 moons 데이터셋 그대로 사용.

```python
m = len(X_train)

fix, axes = plt.subplots(ncols=2, figsize=(10,4), sharey=True)

# (그림위치, 학습률) = (0, 1): 왼편 그림, 학습률 1
# (그림위치, 학습률) = (1, 0.5): 오른편 그림, 학습률 0.5
for subplot, learning_rate in ((0, 1), (1, 0.5)):
    # sample_weights 높을 수록 해당 샘플을 더 중요시 여김.
    # 처음엔 m으로 동일하게 시작
    sample_weights = np.ones(m)

    plt.sca(axes[subplot])
    # 5번 학습
    # 반복 학습때마다 예측값이 틀린 샘플에 대한 가중치 높힘.
    for _ in range(5):
        # SVC 모델 학습
        svm_clf = SVC(kernel="rbf", C=0.05, gamma="scale", random_state=42)
        svm_clf.fit(X_train, y_train, sample_weight=sample_weights)
        # 학습 모델 예측
        y_pred = svm_clf.predict(X_train)
        # 예측값 틀린 샘플에 대한 가중치 키우기
        sample_weights[y_pred != y_train] *= (1 + learning_rate)

        # 그래프 그리기
        plot_decision_boundary(svm_clf, X, y, alpha=0.2)
        plt.title("learning_rate = {}".format(learning_rate), fontsize=16)

    # 왼편 그래프에 번호 추가. 1번 ~ 5번
    if subplot == 0:
        plt.text(-0.7, -0.65, "1", fontsize=14)
        plt.text(-0.6, -0.10, "2", fontsize=14)
        plt.text(-0.5,  0.10, "3", fontsize=14)
        plt.text(-0.4,  0.55, "4", fontsize=14)
        plt.text(-0.3,  0.90, "5", fontsize=14)
    else:
        plt.ylabel("")

save_fig("boosting_plot")
plt.show()
```

**주의**: 서포트벡터 머신은 에이다부스트에 잘 사용되지 않는다.
위에서도 보았듯이 학습률에 따른 불안정성이 큰 편이기 때문이다.
여기서를 예시를 위해 사용되었다.

### 그레이디언트 부스팅

이전 학습기에 의한 오차를 보정하도록 새로운 예측기를 순차적으로 추가하는 아이디어는
에이다부스트와 동일하다.
하지만 샘플의 가중치를 수정하는 대신 이전 예측기가 만든 **나머지 오차**(residual error)에
대해 새로운 예측기를 학습시킨다.

**주의**: 나머지 오차를 책에서는 **잔여 오차**로 번역하였음.
하지만 수학의 나머지 개념이 보다 적절해 보임.

#### 그레이디언트 부스팅 기법으로 회귀 모델 학습하기

아래 코드는 회귀 모델을 그레이디언트 부스팅 기법으로 학습해 가는 과정을 보여준다.
훈련 세트는 2차방정식으로 생성한 데이터셋을 사용한다.
가우시안 잡음도 추가되었다.

```python
np.random.seed(42)
X = np.random.rand(100, 1) - 0.5
y = 3*X[:, 0]**2 + 0.05 * np.random.randn(100)
```

* 1 단계: 의사결정나무 회귀모델을 이용하여 학습한다.
    * `max_depth=2`

```python
from sklearn.tree import DecisionTreeRegressor

tree_reg1 = DecisionTreeRegressor(max_depth=2, random_state=42)
tree_reg1.fit(X, y)
```

    DecisionTreeRegressor(criterion='mse', max_depth=2, max_features=None,
                          max_leaf_nodes=None, min_impurity_decrease=0.0,
                          min_impurity_split=None, min_samples_leaf=1,
                          min_samples_split=2, min_weight_fraction_leaf=0.0,
                          presort=False, random_state=42, splitter='best')

* 2 단계: 1단계 예측기의 예측 결과와의 오차에 대한 레이블을 대상으로 의사결정나무 회귀모델 학습
    * `max_depth=2`

```python
y2 = y - tree_reg1.predict(X)
tree_reg2 = DecisionTreeRegressor(max_depth=2, random_state=42)
tree_reg2.fit(X, y2)
```

    DecisionTreeRegressor(criterion='mse', max_depth=2, max_features=None,
                          max_leaf_nodes=None, min_impurity_decrease=0.0,
                          min_impurity_split=None, min_samples_leaf=1,
                          min_samples_split=2, min_weight_fraction_leaf=0.0,
                          presort=False, random_state=42, splitter='best')

* 3 단계: 2단계 예측기의 예측 결과와의 오차에 대한 레이블을 대상으로 의사결정나무 회귀모델 학습
    * `max_depth=2`

```python
y3 = y2 - tree_reg2.predict(X)
tree_reg3 = DecisionTreeRegressor(max_depth=2, random_state=42)
tree_reg3.fit(X, y3)
```

    DecisionTreeRegressor(criterion='mse', max_depth=2, max_features=None,
                          max_leaf_nodes=None, min_impurity_decrease=0.0,
                          min_impurity_split=None, min_samples_leaf=1,
                          min_samples_split=2, min_weight_fraction_leaf=0.0,
                          presort=False, random_state=42, splitter='best')

새로운 샘플에 대한 예측값은 사용된 모든 예측기의 예측값을 더한다.

```python
# 새로운 샘플
X_new = np.array([[0.8]])
# 모든 예측값 더하기
y_pred = sum(tree.predict(X_new) for tree in (tree_reg1, tree_reg2, tree_reg3))

print(y_pred)
```

    [0.75026781]

아래 그림은 학습된 세 개의 회귀 모델를 그래프로 보여준다.

* 왼쪽 열: 세 단계별로 학습된 모델의 예측값을 보여준다.
* 오른쪽 열: 단계별 학습 모델의 누적 예측값을 보여준다.

행별로 설명하면 다음과 같다.

* 1행: 왼쪽과 오른쪽이 동일하다.
* 2행: 오른쪽 그래프는 왼쪽의 1단계 ~ 2단계 예측값의 합을 보여준다.
* 3행: 오른쪽 그래프는 왼쪽의 1단계 ~ 3단계 예측값의 합을 보여준다.

```python
# 예측값 그래프 그리기
def plot_predictions(regressors, X, y, axes, label=None, style="r-", data_style="b.", data_label=None):
    x1 = np.linspace(axes[0], axes[1], 500)
    # 회귀 모델 리스트를 대상으로 예측값의 합 계산
    y_pred = sum(regressor.predict(x1.reshape(-1, 1)) for regressor in regressors)

    # 기존 데이터셋과 레이블
    plt.plot(X[:, 0], y, data_style, label=data_label)
    # 새로운 데이터셋과 예측값
    plt.plot(x1, y_pred, style, linewidth=2, label=label)

    if label or data_label:
        plt.legend(loc="upper center", fontsize=16)
    plt.axis(axes)

plt.figure(figsize=(11,11))

# 왼편 1행
plt.subplot(321)
plot_predictions([tree_reg1], X, y, axes=[-0.5, 0.5, -0.1, 0.8], label="$h_1(x_1)$", style="g-", data_label="Training set")
plt.ylabel("$y$", fontsize=16, rotation=0)
plt.title("Residuals and tree predictions", fontsize=16)

# 오른편 1행
plt.subplot(322)
plot_predictions([tree_reg1], X, y, axes=[-0.5, 0.5, -0.1, 0.8], label="$h(x_1) = h_1(x_1)$", data_label="Training set")
plt.ylabel("$y$", fontsize=16, rotation=0)
plt.title("Ensemble predictions", fontsize=16)

# 왼편 2행
plt.subplot(323)
plot_predictions([tree_reg2], X, y2, axes=[-0.5, 0.5, -0.5, 0.5], label="$h_2(x_1)$", style="g-", data_style="k+", data_label="Residuals")
plt.ylabel("$y - h_1(x_1)$", fontsize=16)

# 오른편 2행
plt.subplot(324)
plot_predictions([tree_reg1, tree_reg2], X, y, axes=[-0.5, 0.5, -0.1, 0.8], label="$h(x_1) = h_1(x_1) + h_2(x_1)$")
plt.ylabel("$y$", fontsize=16, rotation=0)

# 왼편 3행
plt.subplot(325)
plot_predictions([tree_reg3], X, y3, axes=[-0.5, 0.5, -0.5, 0.5], label="$h_3(x_1)$", style="g-", data_style="k+")
plt.ylabel("$y - h_1(x_1) - h_2(x_1)$", fontsize=16)
plt.xlabel("$x_1$", fontsize=16)

# 오른편 3행
plt.subplot(326)
plot_predictions([tree_reg1, tree_reg2, tree_reg3], X, y, axes=[-0.5, 0.5, -0.1, 0.8], label="$h(x_1) = h_1(x_1) + h_2(x_1) + h_3(x_1)$")
plt.xlabel("$x_1$", fontsize=16)
plt.ylabel("$y$", fontsize=16, rotation=0)

save_fig("gradient_boosting_plot")
plt.show()
```

#### 그레이디언트 부스티드 회귀 나무

앞서 설명한 대로 의사결정나무를 그레이디언트 부스팅 알고리즘으로 작동시키는 회귀 모델을
**그레이디언트 트리 부스팅**(gradient tree boosting)
또는
**그레이디언트 부스티드 회귀 나무**(gradient boosted regression tree, GBRT)라 부른다.

#### 사이킷런 그레이디언트 부스팅 모델

앞서 직접 구현한 GBRT 알고리즘과 동일하게 작동하는 모델을
사이킷런의 `GradientBoostingRegressor` 클래스가 제공하며,
`RandomForestRegressor`와 비슷한 하이퍼파라미터를 제공한다.

* `max_depth=2`: 의사결정나무 깊이 2
* `n_estimators=3`: 3개의 의사결정나무 연속으로 훈련

##### `learning_rate` (학습률)

`learnign_rate`는 기존에 설명한 학습률과 다른 의미의 학습률이다.

* 기존 학습률: 하나의 모델이 학습되는 과정에서 샘플 특성의 가중치를 업데이트할 때 사용되는
    손실함수의 그레이디언트 비율 지정
* 이전 의사결정나무에서 학습된 값을 전달할 때 사용되는 비율.
    * 1.0이면 그대로 전달.
    * 1.0보다 작으면 해당 비율 만큼 조금만 전달.

하지만 그레이디언트 부스팅 알고리즘은 모델 내부의 가중치가 아닌
모델 자체를 새로운 모델로 업데이트 하면서
최적의 모델을 찾아가는 과정이다.

이때, 나머지 오차는 특정 손실함수의 그레이디언트와 연관되어 있다.
따라서 나머지 오차를 얼마의 비율로 사용하여 새로운 모델로 업데이트할까를
정하는 문제는 기존에 설명된 학습률과 사실상 동일한 개념이다.
이에 대한 자세한 설명은 하지 않으며, 대신에
[위키백과: Gradient boosting](https://en.wikipedia.org/wiki/Gradient_boosting#cite_note-MasonBaxterBartlettFrean1999a-4)을
읽어보기를 추천한다.

#### 축소 규제

학습률(`learning_rate`)을 이용하여 학습 과정을 조절하는 방식을 **축소**(shrinkage) 규제라 부른다.
학습률을 낮추면 보다 많은 의사결정나무가 필요하지만 일반적으로 예측 성능을 좋아진다.

아래 코드는 학습률을 1로 지정한 경우와 0.1롷 지정한 경우를 보여준다.

* 왼편 그래프: 앞서 수동으로 실험해본 그레이디언트 부스팅 알고리즘과 동일한 결과를 내준다.
    사용되는 의사결정나무의 수를 3으로 좀 적게 지정하였음. 결과는 과소적합
    * `learning_rate=1.0`
    * `n_estimators=3`
    * 과소적합 되었음.
* 오른편 그래프: 학습률을 0.1로 훨씬 작게 만들었음. 따라서 사용되는 의사결정나무의 수를 200으로 늘렸음.
    결과는 과대적합.
    * `learning_rate=0.1`
    * `n_estimators=200`
    * 과소적합 되었음.

```python
from sklearn.ensemble import GradientBoostingRegressor

# 왼편 그래프 모델
gbrt = GradientBoostingRegressor(max_depth=2, n_estimators=3, learning_rate=1.0, random_state=42)
gbrt.fit(X, y)

# 오른편 그래프 모델
gbrt_slow = GradientBoostingRegressor(max_depth=2, n_estimators=200, learning_rate=0.1, random_state=42)
gbrt_slow.fit(X, y)

# 그래프 그리기
fix, axes = plt.subplots(ncols=2, figsize=(10,4), sharey=True)

# 왼편 그래프
plt.sca(axes[0])
plot_predictions([gbrt], X, y, axes=[-0.5, 0.5, -0.1, 0.8], label="Ensemble predictions")
plt.title("learning_rate={}, n_estimators={}".format(gbrt.learning_rate, gbrt.n_estimators), fontsize=14)
plt.xlabel("$x_1$", fontsize=16)
plt.ylabel("$y$", fontsize=16, rotation=0)

# 오른편 그래프
plt.sca(axes[1])
plot_predictions([gbrt_slow], X, y, axes=[-0.5, 0.5, -0.1, 0.8])
plt.title("learning_rate={}, n_estimators={}".format(gbrt_slow.learning_rate, gbrt_slow.n_estimators), fontsize=14)
plt.xlabel("$x_1$", fontsize=16)

save_fig("gbrt_learning_rate_plot")
plt.show()
```

아래 그림은 왼편 모델에 사용되는 의사결정나무를 20으로 늘렸을 때, 오른편과 비슷한 결과를
보여주는 것을 확인해준다.
즉, 학습률(`learning_rate`)와 예측기 수(`n_estimators`) 사이에 반비례 관계가
어느 정도 성립한다는 것을 볼 수 있다.

```python
from sklearn.ensemble import GradientBoostingRegressor

# 왼편 그래프 모델
gbrt = GradientBoostingRegressor(max_depth=2, n_estimators=20, learning_rate=1.0, random_state=42)
gbrt.fit(X, y)

# 오른편 그래프 모델
gbrt_slow = GradientBoostingRegressor(max_depth=2, n_estimators=200, learning_rate=0.1, random_state=42)
gbrt_slow.fit(X, y)

# 그래프 그리기
fix, axes = plt.subplots(ncols=2, figsize=(10,4), sharey=True)

# 왼편 그래프
plt.sca(axes[0])
plot_predictions([gbrt], X, y, axes=[-0.5, 0.5, -0.1, 0.8], label="Ensemble predictions")
plt.title("learning_rate={}, n_estimators={}".format(gbrt.learning_rate, gbrt.n_estimators), fontsize=14)
plt.xlabel("$x_1$", fontsize=16)
plt.ylabel("$y$", fontsize=16, rotation=0)

# 오른편 그래프
plt.sca(axes[1])
plot_predictions([gbrt_slow], X, y, axes=[-0.5, 0.5, -0.1, 0.8])
plt.title("learning_rate={}, n_estimators={}".format(gbrt_slow.learning_rate, gbrt_slow.n_estimators), fontsize=14)
plt.xlabel("$x_1$", fontsize=16)

save_fig("gbrt_learning_rate_plot")
plt.show()
```

### 그레이디언트 부스팅 조기 종료

최적의 트리 수를 찾기 위해 조기 종료 기법을 사용할 수 있으며, 여기서는 두 가지 방식을 소개한다.

#### `staged_predict()` 메서드 활용

`staged_predict()` 메서드는 훈련되는 예측기 전체를 대상으로 하는 반복자(이터레이터)를
리턴값으로 내준다.
따라서 GBRT 학습이 종료된 후 해당 반복자를 이용하여 최적의 의사결정나무 수를 찾는다.
이때 사용되는 최적의 기준은 평균제곱오차(MSE)를 사용한다.

아래 코드는 120개의 의사결정나무를 사용한 GBRT 모델을 훈련한 후
MSE를 최소화하는 의사결정나무의 수를 찾는다.

```python
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error

# 이전 학습 내용을 지우고 새로 훈련하기 위해 훈련 세트 섞기.
# random_state를 다른 값으로 지정.
X_train, X_val, y_train, y_val = train_test_split(X, y, random_state=49)

# GBRT 모델 설정 및 훈련
gbrt = GradientBoostingRegressor(max_depth=2, n_estimators=120, random_state=42)
gbrt.fit(X_train, y_train)

# staged_predict()에 의해 생성된 반복자를 활용하여
# 각 단계별 MSE 수집 후 최소값을 갖는 인덱스 확인
errors = [mean_squared_error(y_val, y_pred)
          for y_pred in gbrt.staged_predict(X_val)]
bst_n_estimators = np.argmin(errors) + 1

# 최적의 의사결정나무 수를 이용하여 새로 학습
gbrt_best = GradientBoostingRegressor(max_depth=2, n_estimators=bst_n_estimators, random_state=42)
gbrt_best.fit(X_train, y_train)
```

    GradientBoostingRegressor(alpha=0.9, criterion='friedman_mse', init=None,
                              learning_rate=0.1, loss='ls', max_depth=2,
                              max_features=None, max_leaf_nodes=None,
                              min_impurity_decrease=0.0, min_impurity_split=None,
                              min_samples_leaf=1, min_samples_split=2,
                              min_weight_fraction_leaf=0.0, n_estimators=56,
                              n_iter_no_change=None, presort='auto',
                              random_state=42, subsample=1.0, tol=0.0001,
                              validation_fraction=0.1, verbose=0, warm_start=False)

위 결과에 의하면 `n_estimators=56`일 때 MSE가 최소값을 갖는다.
즉, 56개의 의사결정나무를 사용하면 충분하다는 의미히다.
이것을 그래프로도 확인할 수 있다.

* 왼편 그래프:  MSE 변화 선그래프
* 오른편 그래프: 최적 모델 그래프

```python
# MSE 최소값 확인
min_error = np.min(errors)

# 그래프 그리기 준비
plt.figure(figsize=(10, 4))

# 왼편 그래프
plt.subplot(121)
plt.plot(errors, "b.-")
plt.plot([bst_n_estimators, bst_n_estimators], [0, min_error], "k--")
plt.plot([0, 120], [min_error, min_error], "k--")
plt.plot(bst_n_estimators, min_error, "ko")
plt.text(bst_n_estimators, min_error*1.2, "Minimum", ha="center", fontsize=14)
plt.axis([0, 120, 0, 0.01])
plt.xlabel("Number of trees")
plt.ylabel("Error", fontsize=16)
plt.title("Validation error", fontsize=14)

# 오른편 그래프
plt.subplot(122)
plot_predictions([gbrt_best], X, y, axes=[-0.5, 0.5, -0.1, 0.8])
plt.title("Best model (%d trees)" % bst_n_estimators, fontsize=14)
plt.ylabel("$y$", fontsize=16, rotation=0)
plt.xlabel("$x_1$", fontsize=16)

save_fig("early_stopping_gbrt_plot")
plt.show()
```

#### `warm_start=True` 하이퍼파라미터 활용

`staged_predict()` 메서드를 이용하면 최적의 모델을 찾은 후 새롭게 학습하는 과정을 거쳐야 한다.
반면에 `warm_start=True` 옵션을 사용하면 `fit()` 메서드가 호출될 때
이전 의사결정나무에서 학습된 결과를 이어 받아 학습을 계속하게 만든다.
즉, 점진적 학습이 가능해진다.

아래 코드는 `n_estimators` 수를 점진적으로 늘려가면서 MSE가 변하는 추이를 관찰한다.
그러면서 MSE가 5번 이상 별 변화가 없거나 좋아지지 않으면 실행을 멈춘다.

```python
# n_estimators 파라미터를 지정하지 않은 채로 모델 설정
gbrt = GradientBoostingRegressor(max_depth=2, warm_start=True, random_state=42)

# MSE 최솟값 보관용
min_val_error = float("inf")

# MSE의 변화가 없거나 나빠지는 횟수 측정. 5회까지 기다리기.
error_going_up = 0

# n_estimators를 1부터 120까지 실험
for n_estimators in range(1, 120):
    # GBRT의 n_estimators 설정 후 학습
    gbrt.n_estimators = n_estimators
    gbrt.fit(X_train, y_train)

    # MSE 측정
    y_pred = gbrt.predict(X_val)
    val_error = mean_squared_error(y_val, y_pred)

    # MSE가 낮아졌을 경우 최솟값 업데이트.
    # 아니면 5번 정도 기다렸다가 조기 종료할 것.
    if val_error < min_val_error:
        min_val_error = val_error
        error_going_up = 0
    else:
        error_going_up += 1
        if error_going_up == 5:
            break
```

61개의 의사결정나무에서 멈춘다.
즉, `n_estimators=56` 일 때부터 MSE가 별로 좋아지지 않았음을 의미하며,
`staged_predict()` 메서드를 사용한 경우와 거의 동일한 결과이다.

```python
print(gbrt.n_estimators)
```

    61

### 그레이디언트 부스팅 기타 옵션

#### 확률적 그레이디언트 부스팅

GBRT 모델을 설정할 때 예를 들어 `subsample=0.25` 라고 `subsample` 파라미터를 지정하면
무작위로 선택된 25%의 훈련 샘플만 이용하여 학습이 이루어진다.
이런 방식을 **확률적 그레이디언트 부스팅**(stochastic gradient boosting)이라 부른다.
이 방식을 사용하면 일반적으로 편향은 높아지고, 분산은 낮아진다.

#### `loss` 매개변수 활용

GBRT 모델이 사용하는 손실함수는 $\ell_2$ 노름과 아이디어가 같은 [least squares](https://ko.wikipedia.org/wiki/최소제곱법)을 사용하며 `loss=ls`로 지정된다.
만약에 $\ell_1$ 노름 방식을 사용하려면 `loss=lad`로 지정해야 하며,
그러면 [least absolute deviation](https://en.wikipedia.org/wiki/Least_absolute_deviations)을
사용한다.
기타 방식에 대해서는 [sklearn: GradientBoostingRegressor 문서](https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.GradientBoostingRegressor.html)를
참조한다.

### XGBoost 사용법 소개

**XGBoost**는 익스트림 그레이디언트 부스팅(extreme gradient boosting)의 약자이며,
그레이디언트 부스팅 기법을 최적화한 기법이다.
매우 빠르며, 확장성과 이식성이 좋아서 머신러닝 경연대회 참가자들에게 애용된다.

XGBost의 API는 사이킷런의 API와 유사하게 작동한다.
기본 설정 파라미터는
[핵심 XGBoost 문서](https://xgboost.readthedocs.io/en/latest/python/python_api.html)에서
확인할 수 있다.
하지만 사이킷런과 별개로 추가 설치되어야 한다.
설치방법은
[XGBoost 설치 안내](https://xgboost.readthedocs.io/en/latest/build.html)를
참조한다.

아래 코드는 `xgboost` 모듈의 설치여부를 파악한다.

```python
try:
    import xgboost
except ImportError as ex:
    print("Error: the xgboost library is not installed.")
    xgboost = None
```

`XGBoostRegressor` 모델 학습은 다음과 같이 설정한다.

```python
if xgboost is not None:
    xgb_reg = xgboost.XGBRegressor(random_state=42)
    xgb_reg.fit(X_train, y_train)
    y_pred = xgb_reg.predict(X_val)

    # 평균 제곱근 오차(RMSE) 계산
    val_error = mean_squared_error(y_val, y_pred)
    print("\nValidation MSE:", val_error)
```

    [15:22:34] WARNING: /home/conda/feedstock_root/build_artifacts/xgboost_1572314959925/work/src/objective/regression_obj.cu:152: reg:linear is now deprecated in favor of reg:squarederror.

    Validation MSE: 0.0028512559726563943

조기 종료 옵션을 활용할 수 있다.

* `early_stopping_rounds=2`: 2번의 학습 단계에서 평균제곱근오차(RMSE)가
    향상되지 않으면 학습을 멈춘다는 의미임.
    이 옵션을 사용하면 `best_score`, `best_iteration`, `best_ntree_limit`
    등의 추가 속성을 이용할 수 있다.

아래 코드를 실행하면 34번째 학습에서 멈추며 `best_iteration`이 32라고 알려준다.

```python
if xgboost is not None:
    xgb_reg.fit(X_train, y_train,
                eval_set=[(X_val, y_val)], early_stopping_rounds=2)
    y_pred = xgb_reg.predict(X_val)
    val_error = mean_squared_error(y_val, y_pred)

    print("\nValidation MSE:", val_error)
```

    [15:22:35] WARNING: /home/conda/feedstock_root/build_artifacts/xgboost_1572314959925/work/src/objective/regression_obj.cu:152: reg:linear is now deprecated in favor of reg:squarederror.
    [0]	validation_0-rmse:0.286719
    Will train until validation_0-rmse hasn't improved in 2 rounds.
    [1]	validation_0-rmse:0.258221
    [2]	validation_0-rmse:0.232634
    [3]	validation_0-rmse:0.210526
    [4]	validation_0-rmse:0.190232
    [5]	validation_0-rmse:0.172196
    [6]	validation_0-rmse:0.156394
    [7]	validation_0-rmse:0.142241
    [8]	validation_0-rmse:0.129789
    [9]	validation_0-rmse:0.118752
    [10]	validation_0-rmse:0.108388
    [11]	validation_0-rmse:0.100155
    [12]	validation_0-rmse:0.09208
    [13]	validation_0-rmse:0.084791
    [14]	validation_0-rmse:0.078699
    [15]	validation_0-rmse:0.073248
    [16]	validation_0-rmse:0.069391
    [17]	validation_0-rmse:0.066277
    [18]	validation_0-rmse:0.063458
    [19]	validation_0-rmse:0.060326
    [20]	validation_0-rmse:0.0578
    [21]	validation_0-rmse:0.055643
    [22]	validation_0-rmse:0.053943
    [23]	validation_0-rmse:0.053138
    [24]	validation_0-rmse:0.052415
    [25]	validation_0-rmse:0.051821
    [26]	validation_0-rmse:0.051226
    [27]	validation_0-rmse:0.051135
    [28]	validation_0-rmse:0.05091
    [29]	validation_0-rmse:0.050893
    [30]	validation_0-rmse:0.050725
    [31]	validation_0-rmse:0.050471
    [32]	validation_0-rmse:0.050285
    [33]	validation_0-rmse:0.050492
    [34]	validation_0-rmse:0.050348
    Stopping. Best iteration:
    [32]	validation_0-rmse:0.050285

    Validation MSE: 0.002528626115371327

다음 두 코드는 XGBoost와 GBRT와의 속도 차이를 보여준다.
결과적으로 XGBoost 모델이 세 배 이상 빠르다.

* `XGBRegressor`에 사용된 `silent=True` 파라미터는
    학습과정을 굳이 보여주지 않도록 하기위해 사용된다.

```python
%timeit xgboost.XGBRegressor(silent=True).fit(X_train, y_train) if xgboost is not None else None
```

    3.18 ms ± 22.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

```python
%timeit GradientBoostingRegressor().fit(X_train, y_train)
```

    12.4 ms ± 106 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

## 핵심 5: 스태킹

차후 설명 추가!

# Exercise solutions

## 1. to 7.

See Appendix A.

## 8. Voting Classifier

Exercise: _Load the MNIST data and split it into a training set, a validation set, and a test set (e.g., use 50,000 instances for training, 10,000 for validation, and 10,000 for testing)._

The MNIST dataset was loaded earlier.

```python
from sklearn.model_selection import train_test_split
```

```python
X_train_val, X_test, y_train_val, y_test = train_test_split(
    mnist.data, mnist.target, test_size=10000, random_state=42)
X_train, X_val, y_train, y_val = train_test_split(
    X_train_val, y_train_val, test_size=10000, random_state=42)
```

Exercise: _Then train various classifiers, such as a Random Forest classifier, an Extra-Trees classifier, and an SVM._

```python
from sklearn.ensemble import RandomForestClassifier, ExtraTreesClassifier
from sklearn.svm import LinearSVC
from sklearn.neural_network import MLPClassifier
```

```python
random_forest_clf = RandomForestClassifier(n_estimators=100, random_state=42)
extra_trees_clf = ExtraTreesClassifier(n_estimators=100, random_state=42)
svm_clf = LinearSVC(random_state=42)
mlp_clf = MLPClassifier(random_state=42)
```

```python
estimators = [random_forest_clf, extra_trees_clf, svm_clf, mlp_clf]
for estimator in estimators:
    print("Training the", estimator)
    estimator.fit(X_train, y_train)
```

    Training the RandomForestClassifier(bootstrap=True, class_weight=None, criterion='gini',
                           max_depth=None, max_features='auto', max_leaf_nodes=None,
                           min_impurity_decrease=0.0, min_impurity_split=None,
                           min_samples_leaf=1, min_samples_split=2,
                           min_weight_fraction_leaf=0.0, n_estimators=100,
                           n_jobs=None, oob_score=False, random_state=42, verbose=0,
                           warm_start=False)
    Training the ExtraTreesClassifier(bootstrap=False, class_weight=None, criterion='gini',
                         max_depth=None, max_features='auto', max_leaf_nodes=None,
                         min_impurity_decrease=0.0, min_impurity_split=None,
                         min_samples_leaf=1, min_samples_split=2,
                         min_weight_fraction_leaf=0.0, n_estimators=100,
                         n_jobs=None, oob_score=False, random_state=42, verbose=0,
                         warm_start=False)
    Training the LinearSVC(C=1.0, class_weight=None, dual=True, fit_intercept=True,
              intercept_scaling=1, loss='squared_hinge', max_iter=1000,
              multi_class='ovr', penalty='l2', random_state=42, tol=0.0001,
              verbose=0)

    /opt/conda/envs/tf2/lib/python3.7/site-packages/sklearn/svm/base.py:929: ConvergenceWarning: Liblinear failed to converge, increase the number of iterations.
      "the number of iterations.", ConvergenceWarning)

    Training the MLPClassifier(activation='relu', alpha=0.0001, batch_size='auto', beta_1=0.9,
                  beta_2=0.999, early_stopping=False, epsilon=1e-08,
                  hidden_layer_sizes=(100,), learning_rate='constant',
                  learning_rate_init=0.001, max_iter=200, momentum=0.9,
                  n_iter_no_change=10, nesterovs_momentum=True, power_t=0.5,
                  random_state=42, shuffle=True, solver='adam', tol=0.0001,
                  validation_fraction=0.1, verbose=False, warm_start=False)

```python
[estimator.score(X_val, y_val) for estimator in estimators]
```

    [0.9692, 0.9715, 0.8397, 0.9602]

The linear SVM is far outperformed by the other classifiers. However, let's keep it for now since it may improve the voting classifier's performance.

Exercise: _Next, try to combine them into an ensemble that outperforms them all on the validation set, using a soft or hard voting classifier._

```python
from sklearn.ensemble import VotingClassifier
```

```python
named_estimators = [
    ("random_forest_clf", random_forest_clf),
    ("extra_trees_clf", extra_trees_clf),
    ("svm_clf", svm_clf),
    ("mlp_clf", mlp_clf),
]
```

```python
voting_clf = VotingClassifier(named_estimators)
```

```python
voting_clf.fit(X_train, y_train)
```

    /opt/conda/envs/tf2/lib/python3.7/site-packages/sklearn/svm/base.py:929: ConvergenceWarning: Liblinear failed to converge, increase the number of iterations.
      "the number of iterations.", ConvergenceWarning)

    VotingClassifier(estimators=[('random_forest_clf',
                                  RandomForestClassifier(bootstrap=True,
                                                         class_weight=None,
                                                         criterion='gini',
                                                         max_depth=None,
                                                         max_features='auto',
                                                         max_leaf_nodes=None,
                                                         min_impurity_decrease=0.0,
                                                         min_impurity_split=None,
                                                         min_samples_leaf=1,
                                                         min_samples_split=2,
                                                         min_weight_fraction_leaf=0.0,
                                                         n_estimators=100,
                                                         n_jobs=None,
                                                         oob_score=False,
                                                         random_st...
                                                beta_2=0.999, early_stopping=False,
                                                epsilon=1e-08,
                                                hidden_layer_sizes=(100,),
                                                learning_rate='constant',
                                                learning_rate_init=0.001,
                                                max_iter=200, momentum=0.9,
                                                n_iter_no_change=10,
                                                nesterovs_momentum=True,
                                                power_t=0.5, random_state=42,
                                                shuffle=True, solver='adam',
                                                tol=0.0001, validation_fraction=0.1,
                                                verbose=False, warm_start=False))],
                     flatten_transform=True, n_jobs=None, voting='hard',
                     weights=None)

```python
voting_clf.score(X_val, y_val)
```

    0.9709

```python
[estimator.score(X_val, y_val) for estimator in voting_clf.estimators_]
```

    [0.9692, 0.9715, 0.8397, 0.9602]

Let's remove the SVM to see if performance improves. It is possible to remove an estimator by setting it to `None` using `set_params()` like this:

```python
voting_clf.set_params(svm_clf=None)
```

    VotingClassifier(estimators=[('random_forest_clf',
                                  RandomForestClassifier(bootstrap=True,
                                                         class_weight=None,
                                                         criterion='gini',
                                                         max_depth=None,
                                                         max_features='auto',
                                                         max_leaf_nodes=None,
                                                         min_impurity_decrease=0.0,
                                                         min_impurity_split=None,
                                                         min_samples_leaf=1,
                                                         min_samples_split=2,
                                                         min_weight_fraction_leaf=0.0,
                                                         n_estimators=100,
                                                         n_jobs=None,
                                                         oob_score=False,
                                                         random_st...
                                                beta_2=0.999, early_stopping=False,
                                                epsilon=1e-08,
                                                hidden_layer_sizes=(100,),
                                                learning_rate='constant',
                                                learning_rate_init=0.001,
                                                max_iter=200, momentum=0.9,
                                                n_iter_no_change=10,
                                                nesterovs_momentum=True,
                                                power_t=0.5, random_state=42,
                                                shuffle=True, solver='adam',
                                                tol=0.0001, validation_fraction=0.1,
                                                verbose=False, warm_start=False))],
                     flatten_transform=True, n_jobs=None, voting='hard',
                     weights=None)

This updated the list of estimators:

```python
voting_clf.estimators
```

    [('random_forest_clf',
      RandomForestClassifier(bootstrap=True, class_weight=None, criterion='gini',
                             max_depth=None, max_features='auto', max_leaf_nodes=None,
                             min_impurity_decrease=0.0, min_impurity_split=None,
                             min_samples_leaf=1, min_samples_split=2,
                             min_weight_fraction_leaf=0.0, n_estimators=100,
                             n_jobs=None, oob_score=False, random_state=42, verbose=0,
                             warm_start=False)),
     ('extra_trees_clf',
      ExtraTreesClassifier(bootstrap=False, class_weight=None, criterion='gini',
                           max_depth=None, max_features='auto', max_leaf_nodes=None,
                           min_impurity_decrease=0.0, min_impurity_split=None,
                           min_samples_leaf=1, min_samples_split=2,
                           min_weight_fraction_leaf=0.0, n_estimators=100,
                           n_jobs=None, oob_score=False, random_state=42, verbose=0,
                           warm_start=False)),
     ('svm_clf', None),
     ('mlp_clf',
      MLPClassifier(activation='relu', alpha=0.0001, batch_size='auto', beta_1=0.9,
                    beta_2=0.999, early_stopping=False, epsilon=1e-08,
                    hidden_layer_sizes=(100,), learning_rate='constant',
                    learning_rate_init=0.001, max_iter=200, momentum=0.9,
                    n_iter_no_change=10, nesterovs_momentum=True, power_t=0.5,
                    random_state=42, shuffle=True, solver='adam', tol=0.0001,
                    validation_fraction=0.1, verbose=False, warm_start=False))]

However, it did not update the list of _trained_ estimators:

```python
voting_clf.estimators_
```

    [RandomForestClassifier(bootstrap=True, class_weight=None, criterion='gini',
                            max_depth=None, max_features='auto', max_leaf_nodes=None,
                            min_impurity_decrease=0.0, min_impurity_split=None,
                            min_samples_leaf=1, min_samples_split=2,
                            min_weight_fraction_leaf=0.0, n_estimators=100,
                            n_jobs=None, oob_score=False, random_state=42, verbose=0,
                            warm_start=False),
     ExtraTreesClassifier(bootstrap=False, class_weight=None, criterion='gini',
                          max_depth=None, max_features='auto', max_leaf_nodes=None,
                          min_impurity_decrease=0.0, min_impurity_split=None,
                          min_samples_leaf=1, min_samples_split=2,
                          min_weight_fraction_leaf=0.0, n_estimators=100,
                          n_jobs=None, oob_score=False, random_state=42, verbose=0,
                          warm_start=False),
     LinearSVC(C=1.0, class_weight=None, dual=True, fit_intercept=True,
               intercept_scaling=1, loss='squared_hinge', max_iter=1000,
               multi_class='ovr', penalty='l2', random_state=42, tol=0.0001,
               verbose=0),
     MLPClassifier(activation='relu', alpha=0.0001, batch_size='auto', beta_1=0.9,
                   beta_2=0.999, early_stopping=False, epsilon=1e-08,
                   hidden_layer_sizes=(100,), learning_rate='constant',
                   learning_rate_init=0.001, max_iter=200, momentum=0.9,
                   n_iter_no_change=10, nesterovs_momentum=True, power_t=0.5,
                   random_state=42, shuffle=True, solver='adam', tol=0.0001,
                   validation_fraction=0.1, verbose=False, warm_start=False)]

So we can either fit the `VotingClassifier` again, or just remove the SVM from the list of trained estimators:

```python
del voting_clf.estimators_[2]
```

Now let's evaluate the `VotingClassifier` again:

```python
voting_clf.score(X_val, y_val)
```

    0.9728

A bit better! The SVM was hurting performance. Now let's try using a soft voting classifier. We do not actually need to retrain the classifier, we can just set `voting` to `"soft"`:

```python
voting_clf.voting = "soft"
```

```python
voting_clf.score(X_val, y_val)
```

    0.9677

Nope, hard voting wins in this case.

_Once you have found one, try it on the test set. How much better does it perform compared to the individual classifiers?_

```python
voting_clf.voting = "hard"
voting_clf.score(X_test, y_test)
```

    0.9708

```python
[estimator.score(X_test, y_test) for estimator in voting_clf.estimators_]
```

    [0.9645, 0.9691, 0.9589]

The voting classifier only very slightly reduced the error rate of the best model in this case.

## 9. Stacking Ensemble

Exercise: _Run the individual classifiers from the previous exercise to make predictions on the validation set, and create a new training set with the resulting predictions: each training instance is a vector containing the set of predictions from all your classifiers for an image, and the target is the image's class. Train a classifier on this new training set._

```python
X_val_predictions = np.empty((len(X_val), len(estimators)), dtype=np.float32)

for index, estimator in enumerate(estimators):
    X_val_predictions[:, index] = estimator.predict(X_val)
```

```python
X_val_predictions
```

    array([[5., 5., 8., 5.],
           [8., 8., 8., 8.],
           [2., 2., 2., 2.],
           ...,
           [7., 7., 7., 7.],
           [6., 6., 6., 6.],
           [7., 7., 7., 7.]], dtype=float32)

```python
rnd_forest_blender = RandomForestClassifier(n_estimators=200, oob_score=True, random_state=42)
rnd_forest_blender.fit(X_val_predictions, y_val)
```

    RandomForestClassifier(bootstrap=True, class_weight=None, criterion='gini',
                           max_depth=None, max_features='auto', max_leaf_nodes=None,
                           min_impurity_decrease=0.0, min_impurity_split=None,
                           min_samples_leaf=1, min_samples_split=2,
                           min_weight_fraction_leaf=0.0, n_estimators=200,
                           n_jobs=None, oob_score=True, random_state=42, verbose=0,
                           warm_start=False)

```python
rnd_forest_blender.oob_score_
```

    0.9695

You could fine-tune this blender or try other types of blenders (e.g., an `MLPClassifier`), then select the best one using cross-validation, as always.

Exercise: _Congratulations, you have just trained a blender, and together with the classifiers they form a stacking ensemble! Now let's evaluate the ensemble on the test set. For each image in the test set, make predictions with all your classifiers, then feed the predictions to the blender to get the ensemble's predictions. How does it compare to the voting classifier you trained earlier?_

```python
X_test_predictions = np.empty((len(X_test), len(estimators)), dtype=np.float32)

for index, estimator in enumerate(estimators):
    X_test_predictions[:, index] = estimator.predict(X_test)
```

```python
y_pred = rnd_forest_blender.predict(X_test_predictions)
```

```python
from sklearn.metrics import accuracy_score
```

```python
accuracy_score(y_test, y_pred)
```

    0.9654

This stacking ensemble does not perform as well as the voting classifier we trained earlier, it's not quite as good as the best individual classifier.

```python

```


### Part 3: City district object

The city district object of pycity is an inheritance of the uesgraph object of uesgraphs package, which itself is an inheritance of networkx.Graph.

If you are not familiar with networkx.Graph or uesgraph, please go through the uesgraphs tutorial first.
You can access it within the uesgraphs package, which is available under:
[UESGraphs](https://github.com/RWTH-EBC/uesgraphs)

You can install it locally via pip. Open a console and type:
'pip install -e your_path_to_uesgraphs' (with your local path to uesgraphs package)

If you have not installed it, please do so now, because you are going to need it to execute the following lines.

```python
import pycity_base.classes.timer as Timer
import pycity_base.classes.weather as Weather
import pycity_base.classes.prices as Prices
import pycity_base.classes.environment as Env

#  Setting up environment
#  Timestep in seconds
timestep = 3600

#  Total number of timesteps
timesteps_total = int(365 * 24 * 3600 / timestep)

#   Generate timer object for environment
timer = Timer.Timer(time_discretization=timestep, timesteps_total=timesteps_total,
                    timesteps_used_horizon=timesteps_total, timesteps_horizon=timesteps_total)
#  Timer object holds timestep, number of timesteps as well as
#  forecast horizon

#  Generate weather object
weather = Weather.Weather(timer)
#  Weather object holds weather data, such as outdoor temperatures,
#  direct and diffuse radiation
#  Default TRY value is TRY2010_05_Jahr.dat
#  (Test reference year 2010 for region 5 in Germany)

#  Generate price object
price = Prices.Prices()
#  Holding energy prices and subsidies

#  Generate environment object
environment = Env.Environment(timer=timer, weather=weather, prices=price)

print(environment)

```

    <pycity_base.classes.environment.Environment object at 0x00000220AC8F7CF8>

```python
import pycity_base.classes.city_district as CityDistrict

#  Generate city district object
city_district = CityDistrict.CityDistrict(environment)
#  You can generate a CityDistrict object without handing over the environment. This is done to enable some
#  networkx functions for the citydistrict object. However, you should add the environment either way,
#  because you will not be able to work with your district later on, if the environment pointer is missing!
#  Thus, we recommmend to add the environment pointer directly!

print(city_district)
```

    <uesgraphs.UESGraph object>

Now we will generate 3 single family buildings with loads and add them to the city district object

```python
import random
import sympy.geometry.point as point

import pycity_base.classes.demand.domestic_hot_water as DomesticHotWater
import pycity_base.classes.demand.electrical_demand as ElectricalDemand
import pycity_base.classes.demand.space_heating as SpaceHeating

import pycity_base.classes.demand.apartment as Apartment
import pycity_base.classes.building as Building

#  Loop to generate building objects
for i in range(3):

    #  Generate space heating object
    heat_demand = SpaceHeating.SpaceHeating(environment,
                                            method=1,  # Standard load profile
                                            living_area=150,
                                            specific_demand=100)

    #  Generate electrical demand object
    el_demand = ElectricalDemand.ElectricalDemand(environment,
                                                  method=1,  # Standard load profile
                                                  annual_demand=3000)

    #  Generate domestic hot water demand object
    dhw_annex42 = DomesticHotWater.DomesticHotWater(environment,
                                                    t_flow=60,
                                                    thermal=True,
                                                    method=1,  # Annex 42
                                                    daily_consumption=70,
                                                    supply_temperature=25)

    #  Generate apartment and add load objects
    apartment = Apartment.Apartment(environment)
    apartment.addEntity(heat_demand)
    apartment.addMultipleEntities([el_demand, dhw_annex42])

    #  Generate building and add apartment
    building = Building.Building(environment)
    building.addEntity(entity=apartment)

    #  Generate sympy point positions (with random coordinates)
    position = point.Point(random.randint(0, 100), random.randint(0, 100))

    #  Add buildings to city district
    city_district.addEntity(entity=building, position=position)

print('Number of building entities:')
print(city_district.get_nb_of_building_entities())
```

    Number of building entities:
    3

You can also add pv- or wind farms to the city district:

```python
import pycity_base.classes.supply.photovoltaic as PV

#  Generate PV field within city district
pv = PV.PV(environment=environment, method=0, area=20, eta_noct=0.15)

#  Generate sympy point positions
position_1 = point.Point(0, 0)

#  Add PV fields to city district
city_district.addEntity(entity=pv, position=position_1)

print('Number of PV farms:')
print(city_district.get_nb_of_entities(entity_name='pv'))
```

    Number of PV farms:
    1

CAUTION: If you add pv- or windfarms to the city district, the number of buildings is not the same as tne length of nodelist_building. The reason is, that pv- and windfarms are also handled as building nodes within uesgraph. If you deal with building nodes, you should also check the _kind of entity within the node!

```python
assert len(city_district.nodelist_building) == city_district.get_nb_of_building_entities()+1,\
    'Be aware that PV units and wind energy converters are also handled as building nodes in uesgraph!'
```

```python
#  Get complete node information of city district (possible, because city district is a graph)

print('Node information:')
print(city_district.nodes(data=True))
print()

print('Node numbers only:')
print(city_district.nodes())
```

    Node information:
    [(1001, {'name': 1001, 'node_type': 'building', 'position': Point2D(87, 32), 'is_supply_heating': False, 'is_supply_cooling': False, 'is_supply_electricity': False, 'is_supply_gas': False, 'is_supply_other': False, 'entity': <pycity_base.classes.building.Building object at 0x00000220C02A3F60>}), (1002, {'name': 1002, 'node_type': 'building', 'position': Point2D(71, 78), 'is_supply_heating': False, 'is_supply_cooling': False, 'is_supply_electricity': False, 'is_supply_gas': False, 'is_supply_other': False, 'entity': <pycity_base.classes.building.Building object at 0x00000220BEA1B048>}), (1003, {'name': 1003, 'node_type': 'building', 'position': Point2D(38, 62), 'is_supply_heating': False, 'is_supply_cooling': False, 'is_supply_electricity': False, 'is_supply_gas': False, 'is_supply_other': False, 'entity': <pycity_base.classes.building.Building object at 0x00000220ACB92860>}), (1004, {'name': 1004, 'node_type': 'building', 'position': Point2D(0, 0), 'is_supply_heating': False, 'is_supply_cooling': False, 'is_supply_electricity': True, 'is_supply_gas': False, 'is_supply_other': False, 'entity': <pycity_base.classes.supply.photovoltaic.PV object at 0x00000220ACB927F0>})]

    Node numbers only:
    [1001, 1002, 1003, 1004]

CityDistrict holds further methods. Some are shown below:

```python
print('Get number of building objects:')
print(city_district.get_nb_of_building_entities())
print()

print('Get list of nodes with building objects:')
print(city_district.get_list_build_entity_node_ids())
```

    Get number of building objects:
    3

    Get list of nodes with building objects:
    [1001, 1002, 1003]

It is also possible to extract aggregated load curves of all building objects

```python
aggr_load_curve = city_district.get_aggr_space_heating_power_curve()

print(aggr_load_curve)
```

    [5682.09607927 5830.32467264 5978.55326602 ... 7451.04147065 5086.44155188
     3708.50922512]

```python

```