{ "cells": [ { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [], "source": [ "# 파이썬 2와 파이썬 3 지원\n", "from __future__ import division, print_function, unicode_literals\n", "\n", "# 공통\n", "import numpy as np\n", "import os\n", "\n", "# 일관된 출력을 위해 유사난수 초기화\n", "np.random.seed(42)\n", "\n", "# 맷플롯립 설정\n", "%matplotlib inline\n", "import matplotlib\n", "import matplotlib.pyplot as plt\n", "plt.rcParams['axes.labelsize'] = 14\n", "plt.rcParams['xtick.labelsize'] = 12\n", "plt.rcParams['ytick.labelsize'] = 12\n", "\n", "# 한글출력\n", "matplotlib.rc('font', family='AppleGothic')\n", "matplotlib.rcParams['axes.unicode_minus'] = False\n", "\n", "# 그림을 저장할 폴드\n", "PROJECT_ROOT_DIR = \".\"\n", "CHAPTER_ID = \"decision_trees\"\n", "\n", "def image_path(fig_id):\n", " return os.path.join(PROJECT_ROOT_DIR, \"images\", CHAPTER_ID, fig_id)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# CHAPTER 6. 결정트리" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- **결정 트리**Decision Tree: 분류와 회귀 작업 그리고 다중출력 작업도 가능한 다재다능한 머신러닝 알고리즘
\n", " - 매우 복잡한 데이터셋도 학습할 수 있는 강력한 알고리즘
\n", " - 랜덤 포레스트의 기본 구성 요소
\n", "
\n", "- 이번 장에서 배울 내용\n", " - 결정 트리의 훈련, 시각화, 예측 방법\n", " - 시이킷런의 CART 훈련 알고리즘\n", " - 트리에 규제를 가하는 방법과 회귀 문제에 적용하는 방법\n", " - 결정 트리의 제약사항" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## 6.1 결정 트리 학습과 시각화" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "다음은 붓꽃 데이터셋에 DecisionTreeClassifier를 훈련시키는 코드" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "DecisionTreeClassifier(class_weight=None, criterion='gini', max_depth=2,\n", " max_features=None, max_leaf_nodes=None,\n", " min_impurity_decrease=0.0, min_impurity_split=None,\n", " min_samples_leaf=1, min_samples_split=2,\n", " min_weight_fraction_leaf=0.0, presort=False, random_state=42,\n", " splitter='best')" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from sklearn.datasets import load_iris\n", "from sklearn.tree import DecisionTreeClassifier\n", "\n", "iris = load_iris()\n", "X = iris.data[:, 2:] # 꽃잎의 길이와 너비\n", "y = iris.target\n", "\n", "tree_clf = DecisionTreeClassifier(max_depth=2, random_state=42)\n", "tree_clf.fit(X, y)" ] }, { "cell_type": "code", "execution_count": 26, "metadata": { "scrolled": false }, "outputs": [ { "data": { "text/plain": [ "['sepal length (cm)',\n", " 'sepal width (cm)',\n", " 'petal length (cm)',\n", " 'petal width (cm)']" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "iris.feature_names" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "export_graphviz() 함수를 사용해 그래프 정의를 iris-tree.dot 파일로 출력하여 훈련된 결정 트리를 시각화" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [], "source": [ "from sklearn.tree import export_graphviz\n", "\n", "export_graphviz(\n", " tree_clf,\n", " out_file=image_path(\"iris_tree.dot\"),\n", " feature_names=[\"꽃잎 길이 (cm)\", \"꽃잎 너비 (cm)\"],\n", " class_names=iris.target_names,\n", " rounded=True,\n", " filled=True\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "이 .dot 파일을 graphviz 패키지를 사용하여 png 포맷으로 변경" ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "Tree\n", "\n", "\n", "\n", "0\n", "\n", "꽃잎 길이 (cm) <= 2.45\n", "gini = 0.667\n", "samples = 150\n", "value = [50, 50, 50]\n", "class = setosa\n", "\n", "\n", "\n", "1\n", "\n", "gini = 0.0\n", "samples = 50\n", "value = [50, 0, 0]\n", "class = setosa\n", "\n", "\n", "\n", "0->1\n", "\n", "\n", "True\n", "\n", "\n", "\n", "2\n", "\n", "꽃잎 너비 (cm) <= 1.75\n", "gini = 0.5\n", "samples = 100\n", "value = [0, 50, 50]\n", "class = versicolor\n", "\n", "\n", "\n", "0->2\n", "\n", "\n", "False\n", "\n", "\n", "\n", "3\n", "\n", "gini = 0.168\n", "samples = 54\n", "value = [0, 49, 5]\n", "class = versicolor\n", "\n", "\n", "\n", "2->3\n", "\n", "\n", "\n", "\n", "\n", "4\n", "\n", "gini = 0.043\n", "samples = 46\n", "value = [0, 1, 45]\n", "class = virginica\n", "\n", "\n", "\n", "2->4\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import graphviz\n", "with open(\"images/decision_trees/iris_tree.dot\") as f:\n", " dot_graph = f.read()\n", "dot = graphviz.Source(dot_graph)\n", "dot.format = 'png'\n", "dot.render(filename='iris_tree', directory='images/decision_trees', cleanup=True)\n", "dot" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## 6.2 예측하기" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "위의 트리가 어떻게 예측을 만들어내는지 살펴보겠음
\n", "- **루트 노드**root node에서 시작
\n", "이 노드는 꽃임의 길이가 2.45cm보다 짧은지 검사. 만약 그렇다면 왼쪽의 자식 노드child node로 이동
\n", "이 경우 이 노드가 **리프 노드**leaf node이므로 추가적인 검사를 하지 않음
\n", "노드에 있는 예측 클래스(class=setosa)를 보고 결정 트리가 새로 발견한 꽃의 품좀을 Iris-Setosa라고 예측
\n", "
\n", "이번에는 꽃잎의 길이가 2.45cm보다 긺. 루트 노드의 오른쪽 자식 노드로 이동
\n", "이 노드는 리프 노드가 아니라서 추가로 '꽃잎의 너비가 1.75cm보다 작은지' 검사
\n", "만약 그렇다면 이 꽃을 Iris-Versicolor로 예측, 그렇지 않다면 Iris-virginica로 예측" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**NOTE_** 결정 트리의 여러 장점 중 하나는 데이터 전처리가 거의 필요하지 않다는 것
\n", "     특히 특성의 스케일을 맞추거나 평균을 원점에 맞추는 작업이 필요하지 않음" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- 노드의 sample 속성: 얼마나 많은 훈련 샘플이 적용되었는지 헤아린 것
\n", "- 노드의 value 속성: 노드에서 각 클래스에 얼마나 많은 훈련 샘플이 있는지 알려줌
\n", "- 노드의 gini 속성: **불순도**impurity를 측정. 한 노드의 모든 샘플이 같은 클래스에 속해 있다면 이 노드를 순수(gini=0)하다고 함
\n", "[식 6-1]은 훈련 알고리즘이 i번째 노드의 gini 점수 Gi를 계산하는 방법을 보여줌
\n", "pi,k는 i번째 노드에 있는 훈련 샘플 중 클래스 k에 속한 샘플의 비율
\n", "예를 들어 깊이 2의 왼쪽 노드의 gini 점수는 1-(0/54)2-(49/54)2-(5/54)2 ≈ 0.168" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "
**식 6-1 지니 불순도**
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**NOTE_** 사이킷런은 **이진 트리**만 만드는 CART 알고리즘을 사용. 그러므로 리프 노드 외의 모든 노드는 자식 노드를 두 개씩 가짐" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[그림 6-2]는 이 결정 트리의 결정 경계를 보여줌" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "
**그림 6-2 결정 트리의 결정 경계**
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**모델 해석: 화이트박스와 블랙박스**\n", "- **화이트박스**white box 모델: 결정 트리처럼 매우 직관적이고 결정 방식을 이해하기 쉬운 모델
\n", "- **블랙박스**black box 모델: 랜덤 포레스트나 신경망이 블랙박스 모델\n", " - 성능이 뛰어나고 예측을 만드는 연산 과정을 쉽게 확인할 수 있음. 그렇지만 왜 그런 예측을 만드는지는 쉽게 설명하기 어려움\n", " - 예를 들어 신경망이 어떤 사람이 사진에 있다고 판단했을 때 무엇이 이런 예측을 낳게 했는지 파악하기 매우 어려움" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## 6.3 클래스 확률 추정" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- 결정 트리는 한 샘플이 특정 클래스 k에 속할 확률을 추정할 수 있음" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "예를 들어 길이가 5cm이고 너비가 1.5cm인 꽃잎을 발견했다고 가정\n", "- 이에 해당하는 리프 노드는 깊이 2에서 왼쪽 노드이므로 결정 트리는 그에 해당하는 확률을 출력\n", " - Iris-Setosa: 0%(0/54), Iris-Versicolor: 90.7%(49/54), Iris-Virginica: 9.3%(5/54)" ] }, { "cell_type": "code", "execution_count": 30, "metadata": { "scrolled": false }, "outputs": [ { "data": { "text/plain": [ "array([[0. , 0.90740741, 0.09259259]])" ] }, "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ "tree_clf.predict_proba([[5, 1.5]])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "클래스를 하나 예측한다면 가장 높은 확률을 가진 Iris-Versicolor(클래스 1)를 출력" ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([1])" ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "tree_clf.predict([[5, 1.5]])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## 6.4 CART 훈련 알고리즘" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- 사이킷런은 결정 트리를 훈련시키기 위해 CARTClassification And Regression Tree 알고리즘을 사용\n", "- 이 알고리즘의 아이디어\n", " - 훈련 세트를 하나의 특성 k의 임곗값 tk를 사용해 두 개의 서브셋으로 나눔(예를 들면 꽃잎의 길이 ≤ 2.45cm)
\n", " - k와 tk를 고르는 방식: (크기에 다른 가중치가 적용된) 가장 순수한 서브셋으로 나눌 수 있는 (k, tk) 짝을 찾음\n", " - 이 알고리즘이 최소화해야 하는 비용 함수는 [식 6-2]와 같음 " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "
**식 6-2 분류에 대한 CART 비용 함수**
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- \n", " - 훈련 세트를 성공적으로 둘로 나누었다면 같은 방식으로 서브셋을 또 나누고 그다음엔 서브셋의 서브셋을 나누고 이런 식으로 계속 반복
\n", " 이 과정은 (max_depth 매개변수로 정의된) 최대 깊이가 되면 중지하거나 불순도를 줄이는 분할을 찾을 수 없을 때 멈추게 됨
\n", " 다른 몇 개의 매개변수도 중지 조건에 관여(min_samples_split, min_samples_leaf, min_weight_fraction_leaf, max_leaf_nodes)\n", " \n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**CAUTION_** CART 알고리즘은 **탐욕적 알고리즘**greedy algorithm
\n", "       맨 위 루트 노드에서 최적의 분할을 찾으며 각 단계에서 이 과정을 반복. 현재 단계의 분할이 몇 단계를 거쳐 가장 낮은 불순도로 이어질 수 있을지 고려하지 않음
\n", "       탐욕적 알고리즘은 종종 납득할만한 휼륭한 솔루션을 만들어내지만 최적의 솔루션을 보장하지는 않음" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- 최적의 트리를 찾는 것은 **NP-완전**NP-Complete 문제
\n", "이 문제는 O(exp(m)) 시간이 필요하고 매우 작은 훈련 세트에도 적용하기 어려움
\n", "그러므로 '납득할 만한 좋은 솔루션'으로만 만족해야 함" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## 6.5 계산 복잡도" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- 예측을 하려면 결정 트리를 루트 노드에서부터 리프 노드까지 탐색해야 함\n", "- 일반적으로 결정 트리는 거의 균형을 이루고 있으므로 결정 트리를 탐색하기 위해서는 약 O(log2(m))개의 노드를 거쳐야 함\n", "- 각 노드는 하나의 특성값만 확인하기 때문에 예측에 필요한 전체 복잡도는 특성 수와 무관하게 O(log2(m)). 그래서 큰 훈련 세트를 다룰 때도 예측 속도가 매우 빠름\n", "- 훈련 알고리즘은 각 노드에서 모든 훈련 샘플의 모든 특성을 비교함. 그래서 훈련 복잡도는 O(n × m log(m))\n", "- 훈련 세트가 (수천 개 이하의 샘플 정도로) 작을 경우 사이킷런은 (presort=True로 지정하면) 미리 데이터를 정렬하여 훈련 속도를 높임
\n", "하지만 훈련 세트가 클 경우 속도가 많이 느려짐" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## 6.6 지니 불순도 또는 엔트로피?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- 기본적으로 지니 불순도가 사용되지만 criterion 매개변수를 \"entropy\"로 지정하면 **엔트로피** 불순도를 사용\n", "- 엔트로피는 분자의 무질서함을 측정하는 것으로 원래 열역학의 개념. 분자가 안정되고 질서 정연하면 엔트로피가 0에 가까움\n", "- 머신러닝에서는 어떤 세트가 한 클래스의 샘플만 담고 있다면 엔트로피가 0\n", "- [식 6-3]에서 i번째 노드의 엔트로피 정의를 보여줌
\n", "[그림 6-1]에서 깊이 2의 왼쪽 노드의 엔트로피는 -$\\frac{49}{54}$log2($\\frac{49}{54}$)-$\\frac{5}{54}$log2($\\frac{5}{54}$) ≈ 0.445" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "
**식 6-3 엔트로피**
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- 지니 불순도와 엔트로피 중 어떤 것을 사용해야 할까?
\n", " - 지니 불순도가 조금 더 계산이 빠르기 때문에 기본값으로 좋음(로그를 계산할 필요가 없기 때문)\n", " - 그러나 다른 트리가 만들어지는 경우 지니 불순도가 가장 빈도 높은 클래스를 한쪽 가지branch로 고립시키는 경향이 있음\n", " - 엔트로피는 조금 더 균형 잡힌 트리를 만듦" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## 6.7 규제 매개변수" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- **비파라미터 모델**nonparametric model\n", " - 결정 트리 같이 모델 파라미터가 전혀 없는 것이 아니라 훈련되기 전에 파라미터 수가 결정되지 않는 모델\n", " - 모델 구조가 데이터에 맞춰져서 고정되지 않고 자유로움\n", " - 제한을 두지 않으면 훈련 데이터에 아주 가깝게 맞추려고 해서 대부분 과대적합되기 쉬움\n", "- **파라미터 모델**parametric model\n", " - 선형 모델 같이 미리 정의된 모델 파라미터 수를 가짐\n", " - 자유도가 제한되고 과대적합될 위험이 줄어듦(하지만 과소적합될 위험은 커짐)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- 훈련 데이터에 대한 과대적합을 피하기 위해 학습할 때 모델의 자유도를 제한함. 이를 규제라고 함\n", "- DecisionTreeClassifier에서 결정 트리의 형태를 제한하는 규제 매개변수들\n", " - max_depth: 결정 트리의 최대 깊이 제어\n", " - min_samples_split: 분할되기 위해 노드가 가져야 하는 최소 샘플 수\n", " - min_samples_leaf: 리프 노드가 가지고 있어야 할 최소 샘플 수\n", " - min_weight_fraction_leaf: min_samples_leaf와 같지만 가중치가 부여된 전체 샘플 수에서의 비율\n", " - max_leaf_nodes: 리프 노드의 최대 수\n", " - max_features: 각 노드에서 분할에 사용할 특성의 최대 수\n", "- min으로 시작하는 매개변수를 증가시키거나 max로 시작하는 매개변수를 감소시키면 모델에 규제가 커짐" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[그림 6-3]은 moons 데이터셋에 훈련시킨 두 개의 결정 트리를 보여줌\n", "- 왼쪽 모델은 과대적합되었고 오른쪽 모델은 일반화 성능이 좋아보임" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "
**그림 6-3 min_samples_leaf 매개변수를 사용한 규제**
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## 6.8 회귀" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "결정 트리는 회귀 문제에도 사용할 수 있음. 다음은 사이킷런의 DecisionTreeRegressor를 사용해 회귀 트리를 만드는 코드" ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "DecisionTreeRegressor(criterion='mse', max_depth=2, max_features=None,\n", " max_leaf_nodes=None, min_impurity_decrease=0.0,\n", " min_impurity_split=None, min_samples_leaf=1,\n", " min_samples_split=2, min_weight_fraction_leaf=0.0,\n", " presort=False, random_state=42, splitter='best')" ] }, "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# 2차식으로 만든 데이터셋 + 잡음\n", "np.random.seed(42)\n", "m = 200\n", "X = np.random.rand(m, 1)\n", "y = 4 * (X - 0.5) ** 2\n", "y = y + np.random.randn(m, 1) / 10\n", "\n", "from sklearn.tree import DecisionTreeRegressor\n", "\n", "tree_reg = DecisionTreeRegressor(max_depth=2, random_state=42)\n", "tree_reg.fit(X, y)" ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "Tree\n", "\n", "\n", "\n", "0\n", "\n", "x1 <= 0.197\n", "mse = 0.098\n", "samples = 200\n", "value = 0.354\n", "\n", "\n", "\n", "1\n", "\n", "x1 <= 0.092\n", "mse = 0.038\n", "samples = 44\n", "value = 0.689\n", "\n", "\n", "\n", "0->1\n", "\n", "\n", "True\n", "\n", "\n", "\n", "4\n", "\n", "x1 <= 0.772\n", "mse = 0.074\n", "samples = 156\n", "value = 0.259\n", "\n", "\n", "\n", "0->4\n", "\n", "\n", "False\n", "\n", "\n", "\n", "2\n", "\n", "mse = 0.018\n", "samples = 20\n", "value = 0.854\n", "\n", "\n", "\n", "1->2\n", "\n", "\n", "\n", "\n", "\n", "3\n", "\n", "mse = 0.013\n", "samples = 24\n", "value = 0.552\n", "\n", "\n", "\n", "1->3\n", "\n", "\n", "\n", "\n", "\n", "5\n", "\n", "mse = 0.015\n", "samples = 110\n", "value = 0.111\n", "\n", "\n", "\n", "4->5\n", "\n", "\n", "\n", "\n", "\n", "6\n", "\n", "mse = 0.036\n", "samples = 46\n", "value = 0.615\n", "\n", "\n", "\n", "4->6\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 35, "metadata": {}, "output_type": "execute_result" } ], "source": [ "export_graphviz(\n", " tree_reg,\n", " out_file=image_path(\"regression_tree.dot\"),\n", " feature_names=[\"x1\"],\n", " rounded=True,\n", " filled=True\n", " )\n", "\n", "import graphviz\n", "with open(\"images/decision_trees/regression_tree.dot\") as f:\n", " dot_graph = f.read()\n", "dot = graphviz.Source(dot_graph)\n", "dot.format = 'png'\n", "dot.render(filename='regression_tree', directory='images/decision_trees', cleanup=True)\n", "dot" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "분류 트리와 주요한 차이점\n", "- 각 노드에서 클래스를 예측하는 대신 어떤 값을 예측\n", "- 각 영역의 예측값은 항상 그 영역에 있는 타깃값의 평균. 알고리즘은 예측값과 가능한 한 많은 샘플이 가까이 있도록 영역을 분할" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "이 모델의 예측은 아래 [그림 6-5]의 왼쪽에 나타나 있고, max_depth=3으로 설정하면 오른쪽 그래프와 같은 예측을 얻게 됨" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "
**그림 6-5 두 개의 결정 트리 회귀 모델의 예측**
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- 이 예측값을 사용해 한 노드의 모든 샘플에 대한 평균제곱오차(MSE)를 계산\n", "- CART 알고리즘은 평균제곱오차(MSE)를 최소화하도록 분할\n", "- [식 6-4]는 알고리즘이 최소화하기 위한 비용 함수를 보여줌" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "
**식 6-4 회귀를 위한 CART 비용 함수**
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "분류에서와 같이 회귀 작업에서도 결정 트리가 과대적합되기 쉬움\n", "- 규제가 없다면(즉, 기본 매개변수를 사용하면) 아래 [그림 6-6]의 왼쪽과 같은 예측을 함. 훈련세트에 과대적합됨\n", "- min_samples_leaf=10으로 지정하면 훨씬 그럴싸한 모델을 만들어줌" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "
**그림 6-6 결정 트리 회귀 모델의 규제**
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## 6.9 불안정성" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "결정 트리의 제한사항\n", "- 결정 트리는 계단 모양의 결정 경계를 만듦(모든 분할은 축에 수직). 그래서 훈련 세트의 회전에 민감
\n", "아래 [그림 6-7]의 왼쪽 결정 트리는 쉽게 데이터셋을 구분하지만, 데이터셋을 45° 회전한 오른쪽의 결정 트리는 불필요하게 구불구불해져 잘 일반화될 것 같지 않음\n", "- 이런 문제를 해결하는 한 가지 방법은 훈련 데이터를 더 좋은 방향으로 회전시키는 PCA 기법을 사용하는 것(8장 참조)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "
**그림 6-7 훈련 세트의 회전에 민감한 결정 트리**
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- 결정 트리의 주된 문제는 훈련 데이터에 있는 작은 변화에도 매우 민감하다는 것
\n", "훈련 세트에서 가장 넓은 Iris-Versicolor(꽃잎 길이가 4.8cm이고 너비가 1.8cm인 것)를 제거하고 결정 트리를 훈련시키면 [그림 6-8]과 같은 모델을 얻게 됨
\n", "이전에 만든 결정 트리(그림 6-2)와는 매우 다른 모습임" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "
**그림 6-8 훈련 세트의 세부사항에 민감한 결정 트리**
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- 랜덤 포레스트는 많은 트리에서 만든 예측을 평균하여 이런 불안정성을 극복할 수 있음" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## 6.10 연습문제" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "1.백만 개의 샘플을 가진 훈련 세트에서 (규제 없이) 훈련시킨 결정 트리의 깊이는 대략 얼마일까요?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- m개의 리프 노드를 포함한 균형이 잘 잡힌 이진 트리의 깊이: log2(m)\n", "- 결정 트리를 제한을 두지 않고 훈련시키면 훈련 샘플마다 하나의 리프 노드가 되므로 어느 정도 균형이 잘 잡힌 트리가 됨\n", "- 훈련 세트에 백만 개 샘플이 있다면 결정 트리의 깊이는 log2(106) ≈ 20 (실제로는 완벽하게 균형 잡힌 트리가 만들어지지 않기 때문에 조금 더 늘어남)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "2.한 노드의 지니 불순도가 보통 그 부모 노드보다 작을까요, 아니면 클까요? 일반적으로 작거나 클까요, 아니면 항상 작거나 클까요?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- 자식의 지니 불순도의 가중치 합이 최소화되는 방향으로 각 노드를 분할하는 CART 훈련알고리즘의 비용함수 때문에 한 노드의 지니 불순도는 일반적으로 부모의 불순도보다 낮음\n", "- 그러나 다른 자식 노드의 지니 불순도 감소량이 어떤 노드의 불순도 증가량보다 큰 경우라면 부모의 불순도보다 큰 노드가 생길 수 있음
\n", "예를 들어 **클래스 A의 샘플을 4개, 클래스 B의 샘플을 1개** 포함한 노드를 생각해보겠습니다. 이 노드의 **지니 불순도는 1-$\\left ( \\frac{1}{5} \\right )^2$-$\\left ( \\frac{4}{5} \\right )^2$ = 0.32** 입니다. 이 데이터셋은 **1차원이고 A, B, A, A, A 순**으로 늘어서 있다고 가정하겠습니다. 알고리즘이 이 노드를 두 번째 샘플 이후에 나누어 **샘플 A, B를 가진 자식 노드**와 **샘플 A, A, A를 가진 자식 노드**를 만듭니다. 첫 번째 자식 노드의 **지니 불순도는 1-$\\left ( \\frac{1}{2} \\right )^2$-$\\left ( \\frac{1}{2} \\right )^2$ = 0.5**가 되어 부모보다 큽니다. 이는 다른 노드가 **순수 노드**가 되는 것에 대한 대가입니다. 가중치를 준 전체 지니 불순도는 $\\frac{2}{5}$×0.5 + $\\frac{3}{5}$×0 = 0.2가 되어 부모의 지니 불순도보다 낮습니다." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "3.결정 트리가 훈련 세트에 과대적합되었다면 max_depth를 줄이는 것이 좋을까요?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- 모델에 제약을 가해 규제해야 하므로 max_depth를 낮추는 것이 좋음" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "4.결정 트리가 훈련 세트에 과소적합되었다면 입력 특성의 스케일을 조정하는 것이 좋을까요?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- 결정 트리는 훈련 데이터의 스케일이나 원점에 맞추어져 있는지 상관하지 않음. 이것이 결정 트리의 장점\n", "- 결정 트리가 훈련 세트에 과소적합되었다고 입력 특성의 스케일을 조정하는 것은 시간 낭비임" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "5.백만 개의 샘플을 가진 훈련 세트에 결정 트리를 훈련시키는 데 한 시간이 걸렸다면, 천만 개의 샘플을 가진 훈련 세트에 결정 트리를 훈련시키는 데는 대략 얼마나 걸릴까요?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- 결정 트리 훈련의 계산 복잡도: O(n × m log(m))   n: 특성 수, m: 샘플 수\n", "- 샘플이 10배 늘어났으므로 훈련시간은 (n × 10m log(10m)) / (n × m log(m)) = 10 × log(10m) / log(m) 배 늘어남\n", "- m = 106이므로 훈련에 대략 11.7시간이 걸릴 것으로 예상됨" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "6.십만 개의 샘플을 가진 훈련 세트가 있다면 presort=True로 지정하는 것이 훈련 속도를 높일까요?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- 데이터셋의 샘플 수가 수천 개 미만일 때 훈련 세트를 사전에 정렬하여 훈련 속도를 높일 수 있음\n", "- 십만 개의 샘플을 포함하고 있을 때 presort=True로 지정하면 훈련 속도가 매우 느려질 것임" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "7.moons 데이터셋에 결정 트리를 훈련시키고 세밀하게 튜닝해보세요." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "a. make_moons(n_samples=1000, noise=0.4)를 사용해 데이터셋을 생성합니다.
\n", "random_state=42를 지정하여 결과를 일정하게 만듭니다:" ] }, { "cell_type": "code", "execution_count": 104, "metadata": {}, "outputs": [], "source": [ "from sklearn.datasets import make_moons\n", "\n", "X, y = make_moons(n_samples=10000, noise=0.4, random_state=42)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "b. 이를 train_test_split()을 사용해 훈련 세트와 테스트 세트로 나눕니다." ] }, { "cell_type": "code", "execution_count": 105, "metadata": {}, "outputs": [], "source": [ "from sklearn.model_selection import train_test_split\n", "\n", "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "c. DecisionTreeClassifier의 최적의 매개변수를 찾기 위해 교차 검증과 함께 그리드 탐색을 수행합니다.(GridSearchCV를 사용하면 됩니다).
\n", "  힌트: 여러 가지 max_leaf_nodes 값을 시도해보세요." ] }, { "cell_type": "code", "execution_count": 106, "metadata": { "scrolled": true }, "outputs": [ { "data": { "text/plain": [ "{'max_leaf_nodes': 17}" ] }, "execution_count": 106, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from sklearn.model_selection import GridSearchCV\n", "\n", "params = {'max_leaf_nodes': list(range(2, 100))}\n", "grid_search_cv = GridSearchCV(DecisionTreeClassifier(random_state=42), params, cv=3, n_jobs=-1)\n", "\n", "grid_search_cv.fit(X_train, y_train)\n", "grid_search_cv.best_params_" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "d. 찾은 매개변수를 사용해 전체 훈련 세트에 대해 모델을 훈련시키고 테스트 세트에서 성능을 측정합니다. 대략 85~87%의 정확도가 나올 것입니다." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "기본적으로 GridSearchCV는 전체 훈련 세트로 찾은 최적의 모델을 다시 훈련시킵니다(refit=False로 지정해서 바꿀 수 있습니다). 그래서 별도로 작업할 필요가 없습니다. 모델의 정확도를 바로 평가할 수 있습니다:" ] }, { "cell_type": "code", "execution_count": 107, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0.8695" ] }, "execution_count": 107, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from sklearn.metrics import accuracy_score\n", "\n", "y_pred = grid_search_cv.predict(X_test)\n", "accuracy_score(y_test, y_pred)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "8.랜덤 포레스트를 만들어보세요." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "a. 이전 연습문제에 이어서, 훈련 세트의 서브셋을 1,000개 생성합니다. 각각은 무작위로 선택된 100개의 샘플을 담고 있습니다.
\n", "  힌트: 사이킷런의 ShuffleSplit을 사용할 수 있습니다." ] }, { "cell_type": "code", "execution_count": 108, "metadata": {}, "outputs": [], "source": [ "from sklearn.model_selection import ShuffleSplit\n", "\n", "n_trees = 1000\n", "n_instances = 100\n", "\n", "mini_sets = []\n", "\n", "rs = ShuffleSplit(n_splits=n_trees, test_size=len(X_train) - n_instances, random_state=42)\n", "for mini_train_index, mini_test_index in rs.split(X_train):\n", " X_mini_train = X_train[mini_train_index]\n", " y_mini_train = y_train[mini_train_index]\n", " mini_sets.append((X_mini_train, y_mini_train))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "b. 앞에서 찾은 최적의 매개변수를 사용해 각 서브셋에 결정 트리를 훈련시킵니다. 테스트 세트로 이 1,000개의 결정 트리를 평가합니다.
\n", "  더 작은 데이터셋에서 훈련되었기 때문에 이 결정 트리는 앞서 만든 결정 트리보다 성능이 떨어져 약 80%의 정확도를 냅니다." ] }, { "cell_type": "code", "execution_count": 109, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0.8054499999999999" ] }, "execution_count": 109, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from sklearn.base import clone\n", "\n", "forest = [clone(grid_search_cv.best_estimator_) for _ in range(n_trees)]\n", "\n", "accuracy_scores = []\n", "\n", "for tree, (X_mini_train, y_mini_train) in zip(forest, mini_sets):\n", " tree.fit(X_mini_train, y_mini_train)\n", " \n", " y_pred = tree.predict(X_test)\n", " accuracy_scores.append(accuracy_score(y_test, y_pred))\n", "\n", "np.mean(accuracy_scores)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "c. 이제 마술을 부릴 차례입니다. 각 테스트 세트 샘플에 대해 1,000개의 결정 트리 예측을 만들고 다수로 나온 예측만 취합니다(사이파이의 mode() 함수를 사용할 수 있습니다).
\n", "  그러면 테스트 세트에 대한 **다수결 예측**majority-vote prediction이 만들어집니다.
" ] }, { "cell_type": "code", "execution_count": 110, "metadata": {}, "outputs": [], "source": [ "Y_pred = np.empty([n_trees, len(X_test)], dtype=np.uint8) # 1000*2000 empty 배열 선언\n", "\n", "for tree_index, tree in enumerate(forest): # 내장함수 enumerate(): 순서가 있는 자료형의 값을 인덱스와 함께 반환\n", " Y_pred[tree_index] = tree.predict(X_test)" ] }, { "cell_type": "code", "execution_count": 118, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(1000, 2000)\n" ] }, { "data": { "text/plain": [ "array([[0, 1, 0, ..., 0, 0, 1],\n", " [1, 1, 1, ..., 0, 0, 0],\n", " [1, 1, 0, ..., 0, 0, 1],\n", " ...,\n", " [1, 1, 0, ..., 0, 0, 0],\n", " [1, 1, 0, ..., 0, 0, 1],\n", " [1, 1, 0, ..., 0, 0, 0]], dtype=uint8)" ] }, "execution_count": 118, "metadata": {}, "output_type": "execute_result" } ], "source": [ "print(Y_pred.shape)\n", "Y_pred" ] }, { "cell_type": "code", "execution_count": 113, "metadata": {}, "outputs": [], "source": [ "from scipy.stats import mode\n", "\n", "y_pred_majority_votes, n_votes = mode(Y_pred, axis=0) # 최빈값과 빈도수 반환" ] }, { "cell_type": "code", "execution_count": 128, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1, 1, 0, ..., 0, 0, 0]], dtype=uint8)" ] }, "execution_count": 128, "metadata": {}, "output_type": "execute_result" } ], "source": [ "y_pred_majority_votes" ] }, { "cell_type": "code", "execution_count": 123, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[951, 912, 963, ..., 919, 994, 602]])" ] }, "execution_count": 123, "metadata": {}, "output_type": "execute_result" } ], "source": [ "n_votes" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "d. 테스트 세트에서 이 예측을 평가합니다. 앞서 만든 모델보다 조금 높은(약 0.5~1.5% 정도) 정확도를 얻게 될 것입니다. 축하합니다. 랜덤 포레스트 분류기를 훈련시켰습니다!" ] }, { "cell_type": "code", "execution_count": 114, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0.872" ] }, "execution_count": 114, "metadata": {}, "output_type": "execute_result" } ], "source": [ "accuracy_score(y_test, y_pred_majority_votes.reshape([-1]))" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.5.4" } }, "nbformat": 4, "nbformat_minor": 2 }