Chapter 7 판별분석

7.1 개요

판별분석(discriminant analysis)은 범주들을 가장 잘 구별하는 변수들의 하나 또는 다수의 함수를 도출하여 이를 기반으로 분류규칙을 제시한다. 본 장에서는 변수의 분포에 대한 가정이 필요 없는 피셔(Fisher) 방법과 다변량 정규분포를 가정하는 선형 및 비선형 판별분석을 설명한다.

7.2 필요 R 패키지 설치

본 장에서 필요한 R 패키지들은 아래와 같다.

package version
tidyverse 1.3.1
MASS 7.3-54
mvtnorm 1.1-2

7.3 피셔 방법

7.3.1 기본 R 스크립트

train_df <- tibble(
  id = c(1:9),
  x1 = c(5, 4, 7, 8, 3, 2, 6, 9, 5),
  x2 = c(7, 3, 8, 6, 6, 5, 6, 6, 4),
  class = factor(c(1, 2, 2, 2, 1, 1, 1, 2, 2), levels = c(1, 2))
)

knitr::kable(train_df, booktabs = TRUE,
             align = c('r', 'r', 'r', 'r'),
             col.names = c('객체번호', '$x_1$', '$x_2$', '범주'),
             
             caption = '판별분석 학습표본 데이터')
Table 7.1: 판별분석 학습표본 데이터
객체번호 \(x_1\) \(x_2\) 범주
1 5 7 1
2 4 3 2
3 7 8 2
4 8 6 2
5 3 6 1
6 2 5 1
7 6 6 1
8 9 6 2
9 5 4 2

Table 7.1와 같이 두 독립변수 x1, x2와 이분형 종속변수 class의 관측값으로 이루어진 9개의 학습표본을 train_df라는 data frame에 저장한다.

fisher_da <- MASS::lda(class ~ x1 + x2, train_df)

print(fisher_da)
## Call:
## lda(class ~ x1 + x2, data = train_df)
## 
## Prior probabilities of groups:
##         1         2 
## 0.4444444 0.5555556 
## 
## Group means:
##    x1  x2
## 1 4.0 6.0
## 2 6.6 5.4
## 
## Coefficients of linear discriminants:
##           LD1
## x1  0.6850490
## x2 -0.7003859

7.3.2 피셔 판별함수

각 객체는 변수벡터 \(\mathbf{x} \in \mathbb{R}^p\)와 범주 \(y \in \{1, 2\}\)로 이루어진다고 하자. 아래는 변수 \(\mathbf{x}\)의 기대치와 분산-공분산행렬(varinace-covariance matrix)을 나타낸다.

\[\begin{eqnarray*} \boldsymbol\mu_1 = E(\mathbf{x} | y = 1)\\ \boldsymbol\mu_2 = E(\mathbf{x} | y = 2)\\ \boldsymbol\Sigma = Var(\mathbf{x} | y = 1) = Var(\mathbf{x} | y = 2) \end{eqnarray*}\]

다음과 같이 변수들의 선형조합으로 새로운 변수 \(z\)를 형성하는 함수를 피셔 판별함수(Fisher’s discriminant function)라 한다.

\[\begin{equation} z = \mathbf{w}^\top \mathbf{x} \tag{7.1} \end{equation}\]

여기서 계수벡터 \(\mathbf{w} \in \mathbb{R}^p\)는 통상 아래와 같이 변수 \(z\)의 범주간 평균 차이 대 변수 \(z\)의 분산의 비율을 최대화하는 것으로 결정한다.

\[\begin{equation} {\arg\!\min}_{\mathbf{w}} \frac{\mathbf{w}^\top ( \boldsymbol\mu_1 - \boldsymbol\mu_2 )}{\mathbf{w}^\top \boldsymbol\Sigma \mathbf{w}} \tag{7.2} \end{equation}\]

위 식 (7.2)의 해는

\[\begin{equation*} \mathbf{w} \propto \boldsymbol\Sigma^{-1}(\boldsymbol\mu_1 - \boldsymbol\mu_2) \end{equation*}\]

의 조건을 만족하며, 편의상 비례상수를 1로 두면 아래와 같은 해가 얻어진다.

\[\begin{equation} \mathbf{w} = \boldsymbol\Sigma^{-1}(\boldsymbol\mu_1 - \boldsymbol\mu_2) \tag{7.3} \end{equation}\]

실제 모집단의 평균 및 분산을 알지 못하는 경우, 학습표본으로부터 \(\boldsymbol\mu_1, \boldsymbol\mu_2, \boldsymbol\Sigma\)의 추정치를 얻어 식 (7.3)에 대입하는 방식으로 판별계수를 추정한다. 자세한 내용은 교재 (전치혁 2012) 참조.

Table 7.1에 주어진 학습표본을 이용하여 피셔 판별함수를 구해보도록 하자. 우선 각 범주별 평균벡터 \(\hat{\boldsymbol\mu}_1, \hat{\boldsymbol\mu}_2\)를 아래와 같이 구한다.

mu_hat <- train_df %>% 
  group_by(class) %>%
  summarize(x1 = mean(x1),
            x2 = mean(x2)) %>%
  arrange(class)

print(mu_hat)
## # A tibble: 2 x 3
##   class    x1    x2
##   <fct> <dbl> <dbl>
## 1 1       4     6  
## 2 2       6.6   5.4

또한 범주별 표본 분산-공분산행렬 \(\mathbf{S}_1, \mathbf{S}_2\)를 다음과 같이 구한다. 리스트 S_within_group의 첫번째 원소는 범주 1의 분산-공분산행렬 \(\mathbf{S}_1\), 두번째 원소는 범주 2의 분산-공분산행렬 \(\mathbf{S}_2\)를 나타낸다.

S_within_group <- lapply(
  unique(train_df$class) %>% sort(), function(x) {
    train_df %>% filter(class == x) %>% select(x1, x2) %>% var()
  }
)

print(S_within_group)
## [[1]]
##          x1        x2
## x1 3.333333 1.0000000
## x2 1.000000 0.6666667
## 
## [[2]]
##      x1   x2
## x1 4.30 2.95
## x2 2.95 3.80

위에서 얻은 범주별 표본 분산-공분산행렬을 이용하여 합동 분산-공분산행렬을 아래와 같이 추정한다.

\[\begin{equation*} \hat{\boldsymbol\Sigma} = \mathbf{S}_p = \frac{(n_1 - 1)\mathbf{S}_1 + (n_2 - 1)\mathbf{S}_2}{n_1 + n_2 - 2} \end{equation*}\]

이 때 \(n_1, n_2\)는 각각 범주 1, 2에 속한 학습표본 객체의 수를 나타낸다. 아래 R 스크립트에서는 임의의 범주 표본수 벡터 n과 범주별 표본 분산-공분산행렬 리스트 S에 대해 합동 분산-공분산행렬을 구하는 함수 pooled_variance를 정의하고, 주어진 학습표본에 대한 입력값을 대입하여 합동 분산-공분산행렬 추정치 Sigma_hat을 구한다.

pooled_variance <- function(n, S) {
  lapply(1:length(n), function(i) (n[i] - 1)*S[[i]]) %>% 
    Reduce(`+`, .) %>%
    `/`(sum(n) - length(n))
}

n_obs <- train_df %>%
  group_by(class) %>%
  count() %>%
  ungroup() %>%
  mutate(pi = n / sum(n)) %>%
  arrange(class)

Sigma_hat <- pooled_variance(n_obs$n, S_within_group)

print(Sigma_hat)
##          x1       x2
## x1 3.885714 2.114286
## x2 2.114286 2.457143

위에서 구한 추정치들을 이용하여 아래와 같이 판별함수 계수 추정치 \(\hat{\mathbf{w}}\)를 구한다.

\[\begin{equation*} \hat{\mathbf{w}} = \hat{\boldsymbol\Sigma}^{-1}(\hat{\boldsymbol\mu}_1 - \hat{\boldsymbol\mu}_2) \end{equation*}\]

w_hat <- solve(Sigma_hat) %*% t(mu_hat[1, c('x1', 'x2')] - mu_hat[2, c('x1', 'x2')])

print(w_hat)
##         [,1]
## x1 -1.508039
## x2  1.541801

7.3.3 분류 규칙

피셔 판별함수에 따른 분류 경계값은 학습표본에 대한 판별함수값의 평균으로 아래와 같이 구할 수 있다.

\[\begin{equation*} \bar{z} = \frac{1}{N} \sum_i^N \hat{\mathbf{w}}^\top \mathbf{x}_i \end{equation*}\]

z_mean <- t(w_hat) %*% (train_df %>% select(x1, x2) %>% colMeans()) %>% drop()

print(z_mean)
## [1] 0.526438

위 결과를 통해, 분류규칙은 다음과 같이 주어진다.

  • \(\hat{\mathbf{w}}^\top \mathbf{x} \ge \bar{z}\) 이면, \(\mathbf{x}\)를 범주 1로 분류
  • \(\hat{\mathbf{w}}^\top \mathbf{x} < \bar{z}\) 이면, \(\mathbf{x}\)를 범주 2로 분류
train_prediction_df <- train_df %>%
  mutate(
    z = w_hat[1]*x1 + w_hat[2]*x2,
    predicted_class = factor(if_else(z >= z_mean, 1, 2), levels = c(1, 2))
    )

knitr::kable(train_prediction_df, booktabs = TRUE,
             align = c('r', 'r', 'r', 'r', 'r', 'r'),
             col.names = c('객체번호', '$x_1$', '$x_2$', '실제범주', '$z$', '추정범주'),
             caption = '학습표본에 대한 피셔 분류 결과')
Table 7.2: 학습표본에 대한 피셔 분류 결과
객체번호 \(x_1\) \(x_2\) 실제범주 \(z\) 추정범주
1 5 7 1 3.2524116 1
2 4 3 2 -1.4067524 2
3 7 8 2 1.7781350 1
4 8 6 2 -2.8135048 2
5 3 6 1 4.7266881 1
6 2 5 1 4.6929260 1
7 6 6 1 0.2025723 2
8 9 6 2 -4.3215434 2
9 5 4 2 -1.3729904 2

위 결과 객체 3, 7가 오분류된다.

7.3.4 R 패키지를 이용한 분류규칙 도출

패키지 MASS내의 함수 lda 수행 시 얻어지는 판별계수 \(\hat{\mathbf{w}}\)는 위 결과와는 사뭇 다른데, lda 함수의 경우 아래와 같이 1) 제약식을 포함하여 비례계수를 구하기 때문에 계수의 크기가 달라지며, 2) 목적함수를 최소화하는 대신 최대화하는 값을 찾기 때문에 부호가 달라진다.

\[\begin{equation*} \begin{split} \max \text{ } & \mathbf{w}^\top ( \boldsymbol\mu_1 - \boldsymbol\mu_2 )\\ \text{s.t. } & \mathbf{w}^\top \boldsymbol\Sigma \mathbf{w} = 1 \end{split} \end{equation*}\]

이에 따른 lda 함수의 계수 추정 결과는 아래와 같다.

fisher_da <- MASS::lda(class ~ x1 + x2, train_df)

w_hat_lda <- fisher_da$scaling
print(w_hat_lda)
##           LD1
## x1  0.6850490
## x2 -0.7003859
z_mean_lda <- t(fisher_da$scaling) %*% (train_df %>% select(x1, x2) %>% colMeans()) %>% drop()
print(z_mean_lda)
##        LD1 
## -0.2391423

위 결과는 아래와 같은 계산을 통해 앞 장에서 보았던 결과와 동일한 분류 경계식으로 표현될 수 있음을 볼 수 있다.

scale_adjust <- t(w_hat) %*% Sigma_hat %*% w_hat %>% drop() %>% sqrt()
sign_adjust <- -1

w_hat <- w_hat_lda * scale_adjust * sign_adjust
print(w_hat)
##          LD1
## x1 -1.508039
## x2  1.541801
z_mean <- z_mean_lda * scale_adjust * sign_adjust 
print(z_mean)
##      LD1 
## 0.526438

아래 스크립트는 위 lda 함수로부터의 경계식 추정을 기반으로 아래 수식값을 계산한다.

\[\begin{equation*} \hat{\mathbf{w}}^\top \mathbf{x} - \bar{z} \end{equation*}\]

predict(fisher_da, train_df)$x
##          LD1
## 1 -1.2383140
## 2  0.8781805
## 3 -0.5686020
## 4  1.5172187
## 5 -1.9080261
## 6 -1.8926892
## 7  0.1471208
## 8  2.2022677
## 9  0.8628436

피셔 분류규칙에 따라 해당 값이 0보다 작으면 범주 1, 0보다 크면 범주 2로 분류한다.

train_df %>%
  mutate(
    centered_z = predict(fisher_da, .)$x,
    predicted_class = factor(if_else(centered_z <= 0, 1, 2), levels = c(1, 2))
    ) %>%
  knitr::kable(booktabs = TRUE,
             align = c('r', 'r', 'r', 'r', 'r', 'r'),
             col.names = c('객체번호', '$x_1$', '$x_2$', 
                           '실제범주', '$z - \\bar{z}$', '추정범주'),
             caption = '학습표본에 대한 피셔 분류 결과 - `MASS::lda` 분류 경계식 기준')
Table 7.3: 학습표본에 대한 피셔 분류 결과 - MASS::lda 분류 경계식 기준
객체번호 \(x_1\) \(x_2\) 실제범주 \(z - \bar{z}\) 추정범주
1 5 7 1 -1.2383140 1
2 4 3 2 0.8781805 2
3 7 8 2 -0.5686020 1
4 8 6 2 1.5172187 2
5 3 6 1 -1.9080261 1
6 2 5 1 -1.8926892 1
7 6 6 1 0.1471208 2
8 9 6 2 2.2022677 2
9 5 4 2 0.8628436 2

Table 7.3는 Table 7.2와 동일한 범주 추정 결과를 보인다.

7.4 의사결정론에 의한 선형분류규칙

다음과 같이 객체가 각 범주에 속할 사전확률과 각 범주 내에서의 분류변수의 확률밀도함수에 대한 기호를 정의한다.

  • \(\pi_k\): 임의의 객체가 범주 \(k\)에 속할 사전확률
  • \(f_k(\mathbf{x})\): 범주 \(k\)에 대한 변수의 확률밀도함수

이 때 통상적으로 \(\mathbf{x}\)는 다변량 정규분포를 따르는 것으로 가정하여 아래와 같이 평균벡터 \(\boldsymbol\mu_k\)와 분산-공분산행렬 \(\boldsymbol\Sigma\)로 확률밀도함수를 정의할 수 있다. 이 때 분산-공분산행렬 \(\boldsymbol\Sigma\)는 모든 범주에 대해 동일하다고 가정한다.

\[\begin{equation} f_k(\mathbf{x}) = \frac{1}{(2\pi)^{p/2}|\boldsymbol\Sigma|^{1/2}} \exp \{ -\frac{1}{2} \left(\mathbf{x} - \boldsymbol\mu_k\right)^\top \boldsymbol\Sigma^{-1} \left(\mathbf{x} - \boldsymbol\mu_k\right) \} \tag{7.4} \end{equation}\]

본 장에서는 두 범주(\(k = 1, 2\)) 분류 문제만 다루며, 세 범주 이상에 대한 분류 문제는 뒷 장에서 추가적으로 다루기로 한다.

7.4.1 기본 R 스크립트

Table 7.1의 학습표본에 대해 선형판별분석을 적용하는 R 스크립트는 아래에 보이는 것처럼 피셔 판별함수를 구하기 위한 동일하며, prior 파라미터를 정의하지 않음으로써 \(\pi_1\)\(\pi_2\)를 학습표본의 범주 1, 2의 비율로 설정한다.

lda_fit <- MASS::lda(class ~ x1 + x2, train_df)

print(lda_fit)
## Call:
## lda(class ~ x1 + x2, data = train_df)
## 
## Prior probabilities of groups:
##         1         2 
## 0.4444444 0.5555556 
## 
## Group means:
##    x1  x2
## 1 4.0 6.0
## 2 6.6 5.4
## 
## Coefficients of linear discriminants:
##           LD1
## x1  0.6850490
## x2 -0.7003859

7.4.2 선형판별함수

두 범주 문제에 있어서, 범주를 알지 못하는 변수 \(\mathbf{x}\)에 대한 확률밀도함수는 아래와 같다.

\[\begin{equation*} f(\mathbf{x}) = \pi_1 f_1(\mathbf{x}) + \pi_2 f_2(\mathbf{x}) \end{equation*}\]

베이즈 정리(Bayes’s theorem)에 따라 변수 \(\mathbf{x}\)값이 주어졌을 때 범주 \(k\)에 속할 사후확률(posterior)은 아래와 같이 구할 수 있다.

\[\begin{equation} P(y = k \, | \, \mathbf{x}) = \frac{\pi_k f_k(\mathbf{x})}{f(\mathbf{x})} \tag{7.5} \end{equation}\]

각 범주에 대한 사후확률을 계산하여, 확률이 높은 쪽으로 범주를 추정한다.

\[\begin{equation} \hat{y} = \begin{cases} 1, & \text{if } P(y = 1 \, | \, \mathbf{x}) \ge P(y = 2 \, | \, \mathbf{x})\\ 2, & \text{otherwise} \end{cases} \tag{7.6} \end{equation}\]

이를 다시 정리하면 아래와 같다.

\[\begin{equation*} \hat{y} = \begin{cases} 1, & \text{if } \frac{f_1(\mathbf{x})}{f_2(\mathbf{x})} \ge \frac{\pi_2}{\pi_1}\\ 2, & \text{otherwise} \end{cases} \end{equation*}\]

위 분류규칙에 식 (7.4)을 대입하여 정리하면 다음과 같다. 보다 자세한 내용은 교재 (전치혁 2012) 참조.

\[\begin{equation*} \hat{y} = \begin{cases} 1, & \text{if } \boldsymbol\mu_1^\top \boldsymbol\Sigma^{-1}\mathbf{x} - \frac{1}{2} \boldsymbol\mu_1^\top \boldsymbol\Sigma^{-1} \boldsymbol\mu_1 + \ln \pi_1 \ge \boldsymbol\mu_2^\top \boldsymbol\Sigma^{-1}\mathbf{x} - \frac{1}{2} \boldsymbol\mu_2^\top \boldsymbol\Sigma^{-1} \boldsymbol\mu_2 + \ln \pi_2 \\ 2, & \text{otherwise} \end{cases} \end{equation*}\]

따라서, 각 범주에 대한 판별함수를

\[\begin{equation} u_k(\mathbf{x}) = \boldsymbol\mu_k^\top \boldsymbol\Sigma^{-1}\mathbf{x} - \frac{1}{2} \boldsymbol\mu_k^\top \boldsymbol\Sigma^{-1} \boldsymbol\mu_k + \ln \pi_k \tag{7.7} \end{equation}\]

라 하면, 아래와 같이 분류규칙을 정의할 수 있다.

\[\begin{equation} \hat{y} = \begin{cases} 1, & \text{if } u_1(\mathbf{x}) \ge u_2(\mathbf{x}) \\ 2, & \text{otherwise} \end{cases} \tag{7.8} \end{equation}\]

Table 7.1의 학습표본에 대해 판별함수값을 계산하고 범주를 추정하면 아래와 같다.

discriminant_func <- function(X, mu, Sigma, pi) {
  Sigma_inv <- solve(Sigma)
  (t(mu) %*% Sigma_inv %*% X %>% drop()) -  
    0.5 * (t(mu) %*% Sigma_inv %*% mu %>% drop()) + 
    log(pi)
}

lda_discriminant_result_df <- train_df %>% 
  mutate(
    u1 = discriminant_func(
      .[c("x1", "x2")] %>% t(),
      mu_hat[1, c("x1", "x2")] %>% unlist(),
      Sigma_hat,
      n_obs$pi[1]
      ),
    u2 = discriminant_func(
      .[c("x1", "x2")] %>% t(),
      mu_hat[2, c("x1", "x2")] %>% unlist(),
      Sigma_hat,
      n_obs$pi[2]
      )
    ) %>%
  mutate(
    predicted_class = factor(if_else(u1 >= u2, 1, 2), levels = c(1, 2))
    )

knitr::kable(
  lda_discriminant_result_df,
  booktabs = TRUE,
  align = rep('r', dim(lda_discriminant_result_df)[2]),
  col.names = c('객체번호', '$x_1$', '$x_2$',
                '실제범주', '$u_1(\\mathbf{x})$', '$u_2(\\mathbf{x})$',
                '추정범주'),
  caption = '학습표본에 대한 LDA 적용 결과: 판별함수값 및 추정범주')
Table 7.4: 학습표본에 대한 LDA 적용 결과: 판별함수값 및 추정범주
객체번호 \(x_1\) \(x_2\) 실제범주 \(u_1(\mathbf{x})\) \(u_2(\mathbf{x})\) 추정범주
1 5 7 1 9.2051470 6.971538 1
2 4 3 2 -1.9363321 0.489223 2
3 7 8 2 11.0057900 10.246458 1
4 8 6 2 4.5909990 8.423307 2
5 3 6 1 7.4045039 3.696619 1
6 2 5 1 5.0411598 1.367036 1
7 6 6 1 5.7164010 6.532631 2
8 9 6 2 4.0282981 9.368644 2
9 5 4 2 0.4270119 2.818805 2

또한 식 (7.5)에 따른 사후확률과 식 (7.6)에 따른 추정범주는 아래와 같이 얻어진다.

lda_posterior_result_df <- train_df %>%
  mutate(
    f1 = mvtnorm::dmvnorm(
      .[c("x1", "x2")],
      mu_hat[1, c("x1", "x2")] %>% unlist(),
      Sigma_hat),
    f2 = mvtnorm::dmvnorm(
      .[c("x1", "x2")],
      mu_hat[2, c("x1", "x2")] %>% unlist(),
      Sigma_hat),
    f = n_obs$pi[1] * f1 + n_obs$pi[2] * f2
    ) %>%
  mutate(
    p1 = n_obs$pi[1] * f1 / f,
    p2 = n_obs$pi[2] * f2 / f
  ) %>%
  mutate(
    predicted_class = factor(if_else(p1 >= p2, 1, 2), levels = c(1, 2))
  ) %>%
  select(
    id, x1, x2, class, p1, p2, predicted_class
  )

knitr::kable(
  lda_posterior_result_df,
  booktabs = TRUE,
  align = rep('r', dim(lda_posterior_result_df)[2]),
  col.names = c('객체번호', '$x_1$', '$x_2$',
                '실제범주', 
                '$P(y = 1 \\vert \\mathbf{x})$', 
                '$P(y = 2 \\vert \\mathbf{x})$',
                '추정범주'),
  caption = '학습표본에 대한 LDA 적용 결과: 사후확률 및 추정범주')
Table 7.5: 학습표본에 대한 LDA 적용 결과: 사후확률 및 추정범주
객체번호 \(x_1\) \(x_2\) 실제범주 \(P(y = 1 \vert \mathbf{x})\) \(P(y = 2 \vert \mathbf{x})\) 추정범주
1 5 7 1 0.9032273 0.0967727 1
2 4 3 2 0.0812446 0.9187554 2
3 7 8 2 0.6812088 0.3187912 1
4 8 6 2 0.0212004 0.9787996 2
5 3 6 1 0.9760579 0.0239421 1
6 2 5 1 0.9752562 0.0247438 1
7 6 6 1 0.3065644 0.6934356 2
8 9 6 2 0.0047713 0.9952287 2
9 5 4 2 0.0838007 0.9161993 2

패키지 MASS내의 함수 lda를 통해 위 Table 7.5 결과를 간편하게 얻을 수 있다.

lda_fit <- MASS::lda(class ~ x1 + x2, train_df)

train_df %>% bind_cols(
  predict(lda_fit, train_df)$posterior %>% 
    `colnames<-`(paste0("p", colnames(.))) %>% as_data_frame()
  ) %>%
  mutate(
    predicted_class = predict(lda_fit, .)$class
    )
## # A tibble: 9 x 7
##      id    x1    x2 class      p1     p2 predicted_class
##   <int> <dbl> <dbl> <fct>   <dbl>  <dbl> <fct>          
## 1     1     5     7 1     0.903   0.0968 1              
## 2     2     4     3 2     0.0812  0.919  2              
## 3     3     7     8 2     0.681   0.319  1              
## 4     4     8     6 2     0.0212  0.979  2              
## 5     5     3     6 1     0.976   0.0239 1              
## 6     6     2     5 1     0.975   0.0247 1              
## 7     7     6     6 1     0.307   0.693  2              
## 8     8     9     6 2     0.00477 0.995  2              
## 9     9     5     4 2     0.0838  0.916  2

위 결과들은 교재 (전치혁 2012)의 예제 결과와는 다소 차이가 있는데, 이는 교재에서는 사전확률을 학습표본 내 비율 대신 \(\pi_1 = \pi_2 = 0.5\)로 지정하였기 때문이다. 교재와 동일한 결과는 아래의 스크립트처럼 lda 함수 실행 시 사전확률 파리미터 prior의 값을 지정함으로써 얻을 수 있다.

lda_fit_equal_prior <- MASS::lda(class ~ x1 + x2, train_df, prior = c(1/2, 1/2))

train_df %>% bind_cols(
  predict(lda_fit_equal_prior, train_df)$posterior %>% 
    `colnames<-`(paste0("p", colnames(.))) %>% as_data_frame()
  ) %>%
  mutate(
    predicted_class = predict(lda_fit_equal_prior, .)$class
    ) %>%
  knitr::kable(
    booktabs = TRUE,
    align = rep('r', dim(lda_posterior_result_df)[2]),
    col.names = c('객체번호', '$x_1$', '$x_2$',
                '실제범주', 
                '$P(y = 1 \\vert \\mathbf{x})$', 
                '$P(y = 2 \\vert \\mathbf{x})$',
                '추정범주'),
    caption = '학습표본에 대한 LDA 적용 결과: 사후확률 및 추정범주 (사전확률 = 0.5)')
Table 7.6: 학습표본에 대한 LDA 적용 결과: 사후확률 및 추정범주 (사전확률 = 0.5)
객체번호 \(x_1\) \(x_2\) 실제범주 \(P(y = 1 \vert \mathbf{x})\) \(P(y = 2 \vert \mathbf{x})\) 추정범주
1 5 7 1 0.9210538 0.0789462 1
2 4 3 2 0.0995341 0.9004659 2
3 7 8 2 0.7275992 0.2724008 1
4 8 6 2 0.0263608 0.9736392 2
5 3 6 1 0.9807542 0.0192458 1
6 2 5 1 0.9801065 0.0198935 1
7 6 6 1 0.3559269 0.6440731 2
8 9 6 2 0.0059571 0.9940429 2
9 5 4 2 0.1026013 0.8973987 2

7.5 오분류비용을 고려한 분류규칙

위 Table 7.5의 객체 3, 7와 같이 선형분류함수가 모든 객체의 범주를 정확하게 추정하지 못하고 오분류가 발생하는 경우가 있다. 이 때 다음과 같이 두 종류의 오분류 비용이 있다고 가정하자.

  • \(C(1 \, | \, 2)\): 범주 2를 1로 잘못 분류 시 초래 비용
  • \(C(2 \, | \, 1)\): 범주 1를 2로 잘못 분류 시 초래 비용

이 때 총 기대 오분류 비용은 다음과 같다.

\[\begin{equation} C(1 \, | \, 2) \pi_2 \int_{\mathbf{x} \in R_1} f_2(\mathbf{x}) d\mathbf{x} + C(2 \, | \, 1) \pi_1 \int_{\mathbf{x} \in R_2} f_1(\mathbf{x}) d\mathbf{x} \tag{7.9} \end{equation}\]

여기에서 \(R_1 \subset \mathbb{R}^p, R_2 = \mathbb{R}^p - R_{1}\)는 판별함수에 의해 각각 범주 1, 2로 분류되는 판별변수 영역을 나타낸다. 즉,

\[\begin{equation*} \hat{y} = \begin{cases} 1, & \text{if } \mathbf{x} \in R_1 \\ 2, & \text{otherwise} \end{cases} \end{equation*}\]

(7.9)을 최소화하는 영역 \(R_1, R_2\)는 아래와 같다.

\[\begin{eqnarray*} R_1 &=& \left\{\mathbf{x} \in \mathbb{R}^p \, : \, \frac{f_1(\mathbf{x})}{f_2(\mathbf{x})} \ge \frac{\pi_2}{\pi_1} \left( \frac{C(1 \, | \, 2)}{C(2 \, | \, 1)} \right) \right\}\\ R_2 &=& \left\{\mathbf{x} \in \mathbb{R}^p \, : \, \frac{f_1(\mathbf{x})}{f_2(\mathbf{x})} < \frac{\pi_2}{\pi_1} \left( \frac{C(1 \, | \, 2)}{C(2 \, | \, 1)} \right) \right\} \end{eqnarray*}\]

위 중 \(R_1\)에 대한 식을 아래와 같이 단계적으로 전개할 수 있다.

\[\begin{eqnarray*} R_1 &=& \left\{\mathbf{x} \in \mathbb{R}^p \, : \, \frac{\pi_1 f_1(\mathbf{x})}{\pi_2 f_2(\mathbf{x})} \ge \frac{C(1 \, | \, 2)}{C(2 \, | \, 1)} \right\}\\ &=& \left\{\mathbf{x} \in \mathbb{R}^p \, : \, \frac{\frac{\pi_1 f_1(\mathbf{x})}{\pi_1 f_1(\mathbf{x}) + \pi_2 f_2(\mathbf{x})}}{\frac{\pi_2 f_2(\mathbf{x})}{\pi_1 f_1(\mathbf{x}) + \pi_2 f_2(\mathbf{x})}} \ge \frac{C(1 \, | \, 2)}{C(2 \, | \, 1)} \right\}\\ &=& \left\{\mathbf{x} \in \mathbb{R}^p \, : \, \frac{P(y = 1 \, | \, \mathbf{x})}{P(y = 2 \, | \, \mathbf{x})} \ge \frac{C(1 \, | \, 2)}{C(2 \, | \, 1)} \right\}\\ &=& \left\{\mathbf{x} \in \mathbb{R}^p \, : \, C(2 \, | \, 1) P(y = 1 \, | \, \mathbf{x}) \ge C(1 \, | \, 2) P(y = 2 \, | \, \mathbf{x}) \right\} \end{eqnarray*}\]

따라서 오분류비용을 고려한 분류규칙은 1) 사후확률에 오분류 비용을 곱한 뒤, 2) 그 값이 큰 범주로 분류하여 오분류비용을 최소화한다.

Table 7.5에 오분류비용 \(C(1 \, | \, 2) = 1, C(2 \, | \, 1) = 5\)를 적용한 결과는 아래와 같이 구할 수 있다.

lda_fit <- MASS::lda(class ~ x1 + x2, train_df)

misclassification_cost <- c(5, 1)

lda_unequal_cost_result_df <- train_df %>% bind_cols(
  predict(lda_fit, train_df)$posterior %*% diag(misclassification_cost) %>% 
    as_data_frame() %>%
    `names<-`(paste0("s", lda_fit$lev)) 
  ) %>%
  mutate(
    predicted_class = factor(if_else(s1 >= s2, 1, 2), levels = c(1, 2))
    )

knitr::kable(
  lda_unequal_cost_result_df,
  booktabs = TRUE,
  align = rep('r', dim(lda_unequal_cost_result_df)[2]),
  col.names = c('객체번호', '$x_1$', '$x_2$',
                '실제범주', 
                '$C(2 \\, \\vert \\, 1) P(y = 1 \\vert \\mathbf{x})$', 
                '$C(1 \\, \\vert \\, 2) P(y = 2 \\vert \\mathbf{x})$',
                '추정범주'),
  caption = '학습표본에 대한 오분류 비용을 고려한 LDA 적용 결과')
Table 7.7: 학습표본에 대한 오분류 비용을 고려한 LDA 적용 결과
객체번호 \(x_1\) \(x_2\) 실제범주 \(C(2 \, \vert \, 1) P(y = 1 \vert \mathbf{x})\) \(C(1 \, \vert \, 2) P(y = 2 \vert \mathbf{x})\) 추정범주
1 5 7 1 4.5161363 0.0967727 1
2 4 3 2 0.4062232 0.9187554 2
3 7 8 2 3.4060438 0.3187912 1
4 8 6 2 0.1060019 0.9787996 2
5 3 6 1 4.8802897 0.0239421 1
6 2 5 1 4.8762808 0.0247438 1
7 6 6 1 1.5328222 0.6934356 1
8 9 6 2 0.0238567 0.9952287 2
9 5 4 2 0.4190033 0.9161993 2

위 Table 7.7에서 보는 바와 같이 오분류 객체는 3로, 이전 장의 Table 7.5에 비해 실제범주가 1인 객체를 더 정확하게 분류함을 확인할 수 있다. 범주 1인 객체를 범주 2로 분류할 때 발생하는 비용이 범주 2인 객체를 범주 1로 분류할 때 발생하는 비용보다 다섯 배나 크기 때문에, 오분류비용을 고려한 분류규칙은 실제 범주가 2인 객체를 범주 2로 정확하게 분류할 확률이 줄어든다 할지라도, 실제 범주가 1인 객체를 범주 1로 정확하게 분류하는 확률을 높이는 방향으로 학습된다.

7.6 이차판별분석

이차판별분석은 판별함수가 변수들에 대한 이차함수로 표현되는 경우인데, 각 범주에 대한 변수벡터 \(\mathbf{x}\)가 서로 다른 분산-공분산행렬을 갖는 다변량 정규분포를 따를 때 의사결정론에 의한 분류규칙으로부터 유도된다.

7.6.1 기본 R 스크립트

Table 7.1의 학습표본에 대해 이차판별분석을 적용하는 R 스크립트는 아래에 보이는 것과 같이 MASS 패키지의 qda 함수를 사용한다.

qda_fit <- MASS::qda(class ~ x1 + x2, train_df)

print(qda_fit)
## Call:
## qda(class ~ x1 + x2, data = train_df)
## 
## Prior probabilities of groups:
##         1         2 
## 0.4444444 0.5555556 
## 
## Group means:
##    x1  x2
## 1 4.0 6.0
## 2 6.6 5.4

7.6.2 이차 판별함수

각 범주의 확률밀도함수는 아래와 같이 다변량 정규분포로 정의된다.

\[\begin{equation} f_k(\mathbf{x}) = \frac{1}{(2\pi)^{p/2}|\boldsymbol\Sigma_k|^{1/2}} \exp \{ -\frac{1}{2} \left(\mathbf{x} - \boldsymbol\mu_k\right)^\top \boldsymbol\Sigma_k^{-1} \left(\mathbf{x} - \boldsymbol\mu_k\right) \} \tag{7.10} \end{equation}\]

위 식 (7.10)이 선형판별함수에서 사용한 식 (7.4)과 다른 부분은 분산-공분산분포 \(\boldsymbol\Sigma_k\)가 범주 \(k\)에 대해 각각 정의된다는 점이다.

이 경우 각 범주에 대한 판별함수는 아래와 같이 정의된다.

\[\begin{equation} u_k(\mathbf{x}) = - \frac{1}{2} (\mathbf{x} - \boldsymbol\mu_k)^\top \boldsymbol\Sigma_k^{-1} (\mathbf{x} - \boldsymbol\mu_k) - \frac{1}{2} \ln \left| \boldsymbol\Sigma_k \right| + \ln \pi_k \tag{7.11} \end{equation}\]

데이터 행렬 \(X = (\mathbf{x}_1, \mathbf{x}_2, \cdots , \mathbf{x}_N)\)의 각 객체에 대한 판별함수값을 얻는 함수를 아래와 같이 구현할 수 있다.

qda_discriminant_func <- function(X, mu, Sigma, pi) {
  Sigma_inv_sqrt <- chol(solve(Sigma))
  - 0.5 * rowSums((t(X - mu) %*% t(Sigma_inv_sqrt))^2) - 0.5 * log(det(Sigma)) + log(pi)
}

7.6.3 이차판별함수에 의한 분류

분류기준은 선형판별분석과 마찬가지로 판별함수값이 큰 범주로 분류한다.

\[\begin{equation*} \hat{y} = \begin{cases} 1, & \text{if } u_1(\mathbf{x}) \ge u_2(\mathbf{x}) \\ 2, & \text{otherwise} \end{cases} \end{equation*}\]

Table 7.1의 학습표본에 대해 이차판별함수값을 계산하고 범주를 추정하면 아래와 같다.

qda_discriminant_result_df <- train_df %>% mutate(
  u1 = qda_discriminant_func(
      .[c("x1", "x2")] %>% t(),
      mu_hat[1, c("x1", "x2")] %>% unlist(),
      S_within_group[[1]],
      n_obs$pi[1]
      ),
  u2 = qda_discriminant_func(
      .[c("x1", "x2")] %>% t(),
      mu_hat[2, c("x1", "x2")] %>% unlist(),
      S_within_group[[2]],
      n_obs$pi[2]
      )
  ) %>%
  mutate(
    predicted_class = factor(if_else(u1 >= u2, 1, 2), levels = c(1, 2))
  )

knitr::kable(
  qda_discriminant_result_df,
  booktabs = TRUE,
  align = rep('r', dim(qda_discriminant_result_df)[2]),
  col.names = c('객체번호', '$x_1$', '$x_2$',
                '실제범주', '$u_1(\\mathbf{x})$', '$u_2(\\mathbf{x})$',
                '추정범주'),
  caption = '학습표본에 대한 QDA 적용 결과: 판별함수값 및 추정범주')
Table 7.8: 학습표본에 대한 QDA 적용 결과: 판별함수값 및 추정범주
객체번호 \(x_1\) \(x_2\) 실제범주 \(u_1(\mathbf{x})\) \(u_2(\mathbf{x})\) 추정범주
1 5 7 1 -1.729447 -3.950639 1
2 4 3 2 -13.183993 -2.497284 2
3 7 8 2 -3.911266 -3.145402 2
4 8 6 2 -5.274902 -1.868806 2
5 3 6 1 -1.183993 -5.764060 1
6 2 5 1 -1.729447 -6.202685 1
7 6 6 1 -2.002175 -1.934273 2
8 9 6 2 -7.729447 -2.582391 2
9 5 4 2 -8.274902 -1.927726 2

위 Table 7.8에서 보듯이 모든 학습객체가 올바로 분류되고 있다.

또한 선형판별분석의 경우와 마찬가지로 사후확률 비교를 통한 범주 분류를 수행할 수 있다.

qda_posterior_result_df <- train_df %>%
  mutate(
    f1 = mvtnorm::dmvnorm(
      .[c("x1", "x2")],
      mu_hat[1, c("x1", "x2")] %>% unlist(),
      S_within_group[[1]]),
    f2 = mvtnorm::dmvnorm(
      .[c("x1", "x2")],
      mu_hat[2, c("x1", "x2")] %>% unlist(),
      S_within_group[[2]]),
    f = n_obs$pi[1] * f1 + n_obs$pi[2] * f2
    ) %>%
  mutate(
    p1 = n_obs$pi[1] * f1 / f,
    p2 = n_obs$pi[2] * f2 / f
  ) %>%
  mutate(
    predicted_class = factor(if_else(p1 >= p2, 1, 2), levels = c(1, 2))
  ) %>%
  select(
    id, x1, x2, class, p1, p2, predicted_class
  )

knitr::kable(
  qda_posterior_result_df,
  booktabs = TRUE,
  align = rep('r', dim(qda_posterior_result_df)[2]),
  col.names = c('객체번호', '$x_1$', '$x_2$',
                '실제범주', 
                '$P(y = 1 \\vert \\mathbf{x})$', 
                '$P(y = 2 \\vert \\mathbf{x})$',
                '추정범주'),
  caption = '학습표본에 대한 QDA 적용 결과: 사후확률 및 추정범주')
Table 7.9: 학습표본에 대한 QDA 적용 결과: 사후확률 및 추정범주
객체번호 \(x_1\) \(x_2\) 실제범주 \(P(y = 1 \vert \mathbf{x})\) \(P(y = 2 \vert \mathbf{x})\) 추정범주
1 5 7 1 0.9021365 0.0978635 1
2 4 3 2 0.0000228 0.9999772 2
3 7 8 2 0.3173746 0.6826254 2
4 8 6 2 0.0321055 0.9678945 2
5 3 6 1 0.9898499 0.0101501 1
6 2 5 1 0.9887184 0.0112816 1
7 6 6 1 0.4830310 0.5169690 2
8 9 6 2 0.0057829 0.9942171 2
9 5 4 2 0.0017486 0.9982514 2

이 또한 MASS 패키지의 predict.qda 함수를 통해 아래와 같이 동일한 결과값을 보다 간편하게 얻을 수 있다.

train_df %>% bind_cols(
  predict(qda_fit, train_df)$posterior %>% 
    `colnames<-`(paste0("p", colnames(.))) %>% as_data_frame()
  ) %>%
  mutate(
    predicted_class = predict(qda_fit, .)$class
    )
## # A tibble: 9 x 7
##      id    x1    x2 class        p1     p2 predicted_class
##   <int> <dbl> <dbl> <fct>     <dbl>  <dbl> <fct>          
## 1     1     5     7 1     0.902     0.0979 1              
## 2     2     4     3 2     0.0000228 1.00   2              
## 3     3     7     8 2     0.317     0.683  2              
## 4     4     8     6 2     0.0321    0.968  2              
## 5     5     3     6 1     0.990     0.0102 1              
## 6     6     2     5 1     0.989     0.0113 1              
## 7     7     6     6 1     0.483     0.517  2              
## 8     8     9     6 2     0.00578   0.994  2              
## 9     9     5     4 2     0.00175   0.998  2

7.7 세 범주 이상의 분류

7.7.1 기본 R 스크립트

3개의 범주를 지닌 붓꽃(iris) 데이터에 대해 선형판별분석을 적용하는 R 스크립트는 아래와 같다. 본 예제에서는 각 범주별 50개 데이터 중 첫 30개 관측치만을 학습표본으로 삼아 판별함수를 유도한다.

iris_train_df <- datasets::iris %>%
  rename(x1 = Sepal.Length,
         x2 = Sepal.Width,
         x3 = Petal.Length,
         x4 = Petal.Width,
         class = Species) %>%
  group_by(class) %>%
  slice(1:30) %>%
  ungroup() %>%
  mutate(id = row_number())

iris_lda_fit <- MASS::lda(class ~ . -id, iris_train_df)

print(iris_lda_fit)
## Call:
## lda(class ~ . - id, data = iris_train_df)
## 
## Prior probabilities of groups:
##     setosa versicolor  virginica 
##  0.3333333  0.3333333  0.3333333 
## 
## Group means:
##                  x1       x2       x3        x4
## setosa     5.026667 3.450000 1.473333 0.2466667
## versicolor 6.070000 2.790000 4.333333 1.3533333
## virginica  6.583333 2.933333 5.603333 2.0066667
## 
## Coefficients of linear discriminants:
##           LD1        LD2
## x1  0.5711419 -1.2397647
## x2  1.8752911  3.0223980
## x3 -1.7361767  0.3159667
## x4 -3.4672646  1.3954748
## 
## Proportion of trace:
##    LD1    LD2 
## 0.9929 0.0071

7.7.2 일반화된 판별함수

\(K (> 2)\)개의 범주가 있는 경우에 대한 판별분석은 아래와 같이 일반화된다.

  • \(\pi_k\): 범주 \(k\)에 속할 사전확률, \(k = 1, 2, \cdots, K\)
  • \(C(k' \, | \, k) \ge 0\): 실제 범주 \(k\)에 속하는 데 범주 \(k'\)로 분류할 때 소요 비용 (\(C(k' \, | \, k) = 0 \text{ if } k' = k\))
  • \(f_k(\mathbf{x})\): 범주 \(k\)에 속하는 \(\mathbf{x}\)의 확률밀도함수
  • \(R_k \subset \mathbb{R}^p\): 범주 \(k\)로 분류되는 \(\mathbf{x}\)의 영역
  • \(P(k' \, | \, k) = \int_{\mathbf{x} \in R_{k'}} f_k(\mathbf{x}) d\mathbf{x}\): 실제범주 \(k\)에 속하는 데 범주 \(k'\)로 분류할 확률

이 때, 총 기대 오분류 비용은 아래와 같다.

\[\begin{equation*} \sum_{k = 1}^{K} \pi_k \sum_{k' \neq k} C(k' \, | \, k) \int_{\mathbf{x} \in R_{k'}} f_k(\mathbf{x}) d\mathbf{x} \end{equation*}\]

따라서 분류문제는 위 총 기대 오분류 비용을 최소화하는 \(R_1, \cdots, R_K\)를 찾는 것이다.

우선, 범주를 고려하지 않은 \(\mathbf{x}\)의 확률밀도함수는 아래와 같이 정의된다.

\[\begin{equation*} f(\mathbf{x}) = \sum_{k=1}^{K} \pi_k f_k(\mathbf{x}) \end{equation*}\]

베이즈 정리에 의하여, 변수 \(\mathbf{x}\)가 주어졌을 때 범주 \(k\)에 속할 사후확률은 아래와 같다.

\[\begin{equation*} P(y = k \,|\, \mathbf{x}) = \frac{\pi_k f_k(\mathbf{x})}{f(\mathbf{x})} \end{equation*}\]

오분류비용이 동일한 경우에는 각 객체에 대해 위의 사후확률이 가장 큰 범주로 추정한다. 위 식에서

\[\begin{equation*} P(y = k \,|\, \mathbf{x}) \propto \pi_k f_k(\mathbf{x}) \end{equation*}\]

이므로, 아래와 같이 범주가 추정된다.

\[\begin{equation*} \hat{y} = {arg\,max}_{k} \pi_k f_k(\mathbf{x}) \end{equation*}\]

앞 장들에서 살펴본 것과 마찬가지로, 선형판별분석의 경우 각 범주의 확률밀도함수 \(f_k(\mathbf{x})\)가 동일 분산-공분산행렬을 가정하며, 이차판별분석의 경우 서로 다른 분산-공분산행렬을 가정한다.

아래 스크립트는 MASS 패키지의 lda 함수를 통해 각 범주에 속할 사후확률과 범주 추정값을 얻는 과정을 보여준다.

iris_lda_fit <- MASS::lda(class ~ x1 + x2 + x3 + x4, iris_train_df)

iris_lda_result <- iris_train_df %>%
  bind_cols(
    predict(iris_lda_fit, .)$posterior %>%
      as_data_frame()
    ) %>%
  mutate(
    predicted_class = predict(iris_lda_fit, .)$class
  )

print(iris_lda_result)
## # A tibble: 90 x 10
##       x1    x2    x3    x4 class     id setosa versicolor
##    <dbl> <dbl> <dbl> <dbl> <fct>  <int>  <dbl>      <dbl>
##  1   5.1   3.5   1.4   0.2 setosa     1   1      5.57e-22
##  2   4.9   3     1.4   0.2 setosa     2   1      3.32e-17
##  3   4.7   3.2   1.3   0.2 setosa     3   1      2.75e-19
##  4   4.6   3.1   1.5   0.2 setosa     4   1      8.12e-17
##  5   5     3.6   1.4   0.2 setosa     5   1      1.14e-22
##  6   5.4   3.9   1.7   0.4 setosa     6   1      3.20e-21
##  7   4.6   3.4   1.4   0.3 setosa     7   1      8.74e-19
##  8   5     3.4   1.5   0.2 setosa     8   1      3.27e-20
##  9   4.4   2.9   1.4   0.2 setosa     9   1.00   2.22e-15
## 10   4.9   3.1   1.5   0.1 setosa    10   1      9.36e-19
## # … with 80 more rows, and 2 more variables:
## #   virginica <dbl>, predicted_class <fct>
knitr::kable(
  iris_lda_result %>% 
    select(id, class, predicted_class,
           setosa, versicolor, virginica) %>% 
    filter(class != predicted_class),
  booktabs = TRUE,
  align = rep('r', 6),
  col.names = c('객체번호', '실제범주', '추정범주',
                'setosa', 'versicolor', 'virginica'),
  caption = '붓꽃 학습표본에 대한 LDA 적용 결과 - 오분류 객체 사후 확률')
Table 7.10: 붓꽃 학습표본에 대한 LDA 적용 결과 - 오분류 객체 사후 확률
객체번호 실제범주 추정범주 setosa versicolor virginica
51 versicolor virginica 0 0.3088912 0.6911088

아래 스크립트는 MASS 패키지의 qda 함수를 통해 각 범주에 속할 사후확률과 범주 추정값을 얻는 과정을 보여준다.

iris_qda_fit <- MASS::qda(class ~ x1 + x2 + x3 + x4, iris_train_df)

iris_qda_result <- iris_train_df %>%
  bind_cols(
    predict(iris_qda_fit, .)$posterior %>%
      as_data_frame()
    ) %>%
  mutate(
    predicted_class = predict(iris_qda_fit, .)$class
  )

knitr::kable(
  iris_qda_result %>% 
    select(id, class, predicted_class,
           setosa, versicolor, virginica) %>% 
    filter(class != predicted_class),
  booktabs = TRUE,
  align = rep('r', 6),
  col.names = c('객체번호', '실제범주', '추정범주',
                'setosa', 'versicolor', 'virginica'),
  caption = '붓꽃 학습표본에 대한 QDA 적용 결과 - 오분류 객체 사후 확률')
Table 7.11: 붓꽃 학습표본에 대한 QDA 적용 결과 - 오분류 객체 사후 확률
객체번호 실제범주 추정범주 setosa versicolor virginica
51 versicolor virginica 0 0.4274712 0.5725288

위 결과에서 선형판별분석과 이차판별분석은 동일한 객체를 오분류한다. 해당 객체의 실제 범주에 대한 사후확률은 이차판별분석 결과에서 보다 높게 나타난다.

References

전치혁. 2012. 데이터마이닝 기법과 응용. 한나래출판사.