더북(TheBook)

cvTools::cvFolds()

cvTools는 교차 검증을 위한 패키지로, cvFolds( ) 함수를 사용해 데이터의 분할fold을 만드는 기능을 제공한다.

표 9-17 cvTools를 사용한 교차 검증

cvTools::cvFolds : n개의 관찰을 K겹 교차 검증의 R회 반복으로 분할한다.

cvTools::cvFolds(
  n,   # 관찰(Observation)의 수 또는 데이터의 크기
  K=5, # K겹 교차 검증
  R=1, # R회 반복
  # 분할 방법을 지정. 기본 방식은 임의(random)다. 연속(consecutive)은 각 K마다
  # 연속된 데이터를 검증 데이터로 선택한다. 상호 배치(interleaved)는 연속된 데이터를 서로 다른
  # K번째 교차 검증에서 검증 데이터로 사용한다.
  type=c("random", "consecutive", "interleaved")
)

반환 값은 cvFolds 객체로 n, K, R, subsets(색인의 나열을 컬럼으로 한 행렬), which(각 나열이 어느 분할에 해당하는지를 뜻하는 색인)가 저장되어 있다.

example(cvFolds)에 있는 내용으로 cvFolds의 동작 방식을 확인해보자. 다음은 10개 데이터를 K=5, R=1, 임의random 방식으로 실행한 결과다.

> install.packages("cvTools")
> library(cvTools)
> cvFolds(10, K=5, type="random")
5-fold CV:
Fold Index
   1     5
   2     2
   3     6
   4     1
   5     9
   1    10
   2     4
   3     7
   4     3
   5     8

cvFolds는 각 K마다 검증 데이터로 사용할 값을 반환한다. 위 결과에서 Fold가 1일 때 Index는 5, 10이었다. 따라서 5겹 교차 검증에서 K=1일 때 5번, 10번 데이터를 검증 데이터로 사용하고 나머지를 훈련 데이터로 사용하면 된다. 마찬가지로 K=2일 경우 2번, 4번 데이터를 검증 데이터로 사용한다.

다음으로 cvFolds에서 연속consecutive과 상호 배치interleaved의 차이를 살펴보자.

> cvFolds(10, K=5, type="consecutive")
5-fold CV:
Fold Index
   1     1
   1     2
   2     3
   2     4
   3     5
   3     6
   4     7
   4     8
   5     9
   5    10

> cvFolds(10, K=5, type="interleaved")
5-fold CV:
Fold Index
   1     1
   2     2
   3     3
   4     4
   5     5
   1     6
   2     7
   3     8
   4     9
   5    10

위 결과를 보면 consecutive는 K=1일 때 1번, 2번 데이터를 검증 데이터로 선택하고 K=2일 때 3번, 4번을 선택했다. 이처럼 consecutive는 연속된 데이터를 차례로 검증 데이터로 사용한다. 반면 interleaved는 1번 데이터를 K=1일 때, 2번 데이터를 K=2일 때 검증 데이터로 사용하는 방식으로 연속된 데이터를 차례로 서로 다른 K의 검증 데이터로 할당한다.

다음은 아이리스 데이터에 대해 10겹 교차 검증을 3회 반복 수행하기 위해 cvFolds( )를 사용한 예다. cvFolds( ) 실행 전에 호출한 set.seed( )는 난수를 생성하는 초깃값seed을 지정하기 위해 사용했다. cvFolds( )는 난수를 사용하여 데이터를 분리하므로 매 호출 시마다 서로 다른 분할을 결과로 내놓는다. 하지만 seed를 지정해주면 매번 같은 folds를 결과로 내놓게 되어 교차 검증을 수회 반복하더라도 같은 분할을 사용해 안정적으로 모델을 개선할 수 있다.

> set.seed(719)
> (cv <- cvFolds(NROW(iris), K=10, R=3))

Repeated 10-fold CV with 3 replications:
Fold      1   2   3
1        92   4  86
2        52   3 144
3        17  75   5
4        13  98  61
5        61  30  16
6         9 129 148
7         8 121  49
8        31  89  37
9       136 140  82
10       37 102 141
1        90  51  78
2       119  80 132
3       116  70  97
4        11  29  93
5        39  72  45
6        94 125 114
7        68  84  25
8        75 123  69
9       131  73  87
10      100 132  63
...
6        35 137 139
7        73 133  50
8       133  64   7
9       111  74 134
10       66  27 138

위 결과에서 Fold는 K겹 교차 검증의 K를 의미하고 컬럼 1, 2, 3은 반복 R을 의미한다. 따라서 1행 1열의 92는 K=1, R=1일 때 검증 데이터로 아이리스의 92번째 데이터를 사용하라는 의미다. 1행 2열은 4다. 이는 K=1, R=2일 때 검증 데이터로 4번째 데이터를 사용하라는 의미다.

위 표의 Fold에 해당하는 부분은 cv$which에, 실제 선택할 행을 저장한 부분은 cv$subset에 다음과 같이 저장되어 있다.

> head(cv$which, 20)
[1] 1 2 3 4 5 6 7 8 9 10 1 2 3 4 5 6 7 8 9 10
> head(cv$subset)
    [,1] [,2] [,3]
[1,]  92    4   86
[2,]  52    3  144
[3,]  17   75    5
[4,]  13   98   61
[5,]  61   30   16
[6,]   9  129  148

따라서 첫 번째 반복의 K=1에서 검증 데이터로 사용해야 할 행의 번호는 다음과 같이 구할 수 있다. 아래 코드에서 which( ) 함수는 주어진 조건을 만족하는 행의 번호를 얻기 위해 사용했다.

> (validation_idx <- cv$subset[which(cv$which == 1), 1])
[1]  92  90  14 105  85  95 128  56 121 112 144   5  65 146   4

첫 번째 반복 R=1의 K=1에서 훈련, 검증 데이터는 다음과 같이 구한다.

> train <- iris[-validation_idx, ]
> validation <- iris[validation_idx, ]

이를 사용해 K겹 교차 검증을 반복하는 전체 코드의 모습을 그려보면 다음과 같다.

> library(foreach)
> set.seed(719)
> R = 3
> K = 10
> cv <- cvFolds(NROW(iris), K=K, R=R)
> foreach(r=1:R) %do% {
+   foreach(k=1:K, .combine=c) %do% {
+     validation_idx <- cv$subsets[which(cv$which == k), r]
+     train <- iris[-validation_idx, ]
+     validation <- iris[validation_idx, ]
+     # 데이터 전처리
+
+     # 모델 훈련
+
+     # 예측
+
+     # 성능 평가
+     return(성능 값)
+   }
+ }
> # foreach의 반환 값으로부터 성능이 가장 뛰어난 모델링 방법 식별
> # 아이리스 데이터 전체에 대해 해당 방법으로 모델 생성

코드에서 주목할 만한 점은 for 대신 foreach를 사용한 점이다. foreach( )는 값을 반환할 수 있어 모델을 평가한 결과를 한 번에 모으는 데 유용하다. 또, foreach( ) 사용 시 내부의 foreach( )에서는 .combine에 c를 지정하여 결과가 리스트가 아닌 벡터로 되게 했다. 이렇게 하면 결과가 리스트의 리스트가 아니라 벡터의 리스트가 되어 조작이 용이하다.