Chapter 13 비계층적 군집방법
비계층적 군집방법(Nonhierarchical clustering)은 분할방법(Partitioning method)이라고도 하는데, 군집의 수 \(K\)를 사전에 지정하고 대상 객체들을 적절한 군집에 배정하는 방법이다. 즉, 이 방법은 \(n\)개의 객체를 \(K\)개의 군집에 할당하는 최적화 문제로 간주할 수 있다. 본 장에서는 분할방법의 대표적인 K-means 알고리즘, K-medoids 군집방법, 퍼지 K-means 알고리즘, 그리고 모형기반 군집방법에 대하여 주로 알아본다.
13.1 필요 R package 설치
본 장에서 필요한 R 패키지들은 아래와 같다.
package | version |
---|---|
tidyverse | 1.3.1 |
stats | 4.1.3 |
cluster | 2.1.2 |
flexclust | 1.4-0 |
mclust | 5.4.7 |
mvtnorm | 1.1-2 |
13.2 K-means 알고리즘
K-means 알고리즘은 비계층적 군집방법 중 가장 널리 사용되는 것으로 \(K\)개 군집의 중심좌표를 고려하여 각 객체를 가까운 군집에 배정하는 반복적 알고리즘이다.
13.2.1 기본 R 스크립트
10명에 대한 PC의 사용경력(\(x_1\))과 주당 사용시간(\(x_2\))이 다음과 같다.
<- tribble(
df ~id, ~x1, ~x2,
1, 6, 14,
2, 8, 13,
3, 14, 6,
4, 11, 8,
5, 15, 7,
6, 7, 15,
7, 13, 6,
8, 5, 4,
9, 3, 3,
10, 3, 2
)
%>%
df ::kable(
knitrbooktabs = TRUE,
align = c('r', 'r', 'r'),
col.names = c(
'객체번호',
'사용경력($x_1$)', '사용시간($x_2$)'
),caption = 'PC 사용 데이터'
)
객체번호 | 사용경력(\(x_1\)) | 사용시간(\(x_2\)) |
---|---|---|
1 | 6 | 14 |
2 | 8 | 13 |
3 | 14 | 6 |
4 | 11 | 8 |
5 | 15 | 7 |
6 | 7 | 15 |
7 | 13 | 6 |
8 | 5 | 4 |
9 | 3 | 3 |
10 | 3 | 2 |
아래와 같이 stats
패키지의 kmeans
함수를 이용하여 K-means 알고리즘 수행 결과를 얻을 수 있다. 아래 스크립트는 군집 수가 \(K = 3\)라 가정하여 수행한 예이다.
set.seed(123)
<- kmeans(x = df[, -1], centers = 3) kmeans_solution
위 스크립트 실행 결과 도출된 군집 중심좌표는 위에서 얻어진 kmeans
클래스 객체의 centers
값에 저장된다.
$centers kmeans_solution
## x1 x2
## 1 13.250000 6.75
## 2 3.666667 3.00
## 3 7.000000 14.00
또한 각 학습 데이터가 속한 군집은 cluster
값에 저장된다.
$cluster kmeans_solution
## [1] 3 3 1 1 1 3 1 2 2 2
객체의 군집결과는 아래와 같이 도식화하여 보일 수 있다.
%>%
df mutate(cluster = as.factor(kmeans_solution$cluster)) %>%
ggplot(aes(x = x1, y = x2)) +
geom_text(aes(label = id, color = cluster))
13.2.2 알고리즘
K-means 알고리즘의 구체적 절차는 아래와 같다. Table 13.1에 대한 각 단계의 결과를 함께 살펴보자.
[단계 0] (초기 객체 선정) 어떤 규칙에 의하여 \(K\)개의 객체의 좌표를 초기 군집의 중심좌표(centroid)로 선정한다. 군집 \(j\)의 중심좌표를 \(\mathbf{c}_j = \left(\bar{x}^{(j)}_1, \cdots, \bar{x}^{(j)}_p\right)^\top\)라 하자. 초기 군집 중심좌표 \(\mathbf{c}_1, \cdots, \mathbf{c}_K\)를 선정하는 방법은 예를 들어 다음과 같은 규칙이 사용된다.
- 무작위 방법: 대상 객체 중 무작위로 \(K\)개를 선정한다.
- 외각 객체 선정: 전체 객체의 중심좌표에서 가장 멀리 위치하는 \(K\)개의 객체를 선정한다.
[단계 0] 무작위 방법을 이용하여 Table 13.1으로부터 3개의 객체를 초기 군집 중심좌표로 선정하자.
set.seed(123)
<- function(df, k = 1) {
init_cluster <- min(k, nrow(df))
k
%>%
df sample_n(k)
}
<- init_cluster(df[, -1], 3)
cluster_df
cluster_df
## # A tibble: 3 x 2
## x1 x2
## <dbl> <dbl>
## 1 14 6
## 2 3 2
## 3 8 13
[단계 1] (객체의 군집 배정) 각 객체에 대하여 \(K\)개의 군집 중심좌표(centroid)와의 거리(주로 유클리드 거리 사용)를 산출한 후 가장 가까운 군집에 그 객체를 배정한다.
\[\begin{equation*} a_{ij} = \begin{cases} 1 & \text{if } j = \arg\,\max_k d(\mathbf{x}_i, \mathbf{c}_k)\\ 0 & \text{otherwise} \end{cases}, \, i = 1, \cdots, n, \, j = 1, \cdots, K \end{equation*}\]
각 객체에 대하여 3개의 군집 중심좌표와의 거리를 산출해보면 아래와 같다.
::dist2(df[, -1], cluster_df) flexclust
## [,1] [,2] [,3]
## [1,] 11.313708 12.369317 2.236068
## [2,] 9.219544 12.083046 0.000000
## [3,] 0.000000 11.704700 9.219544
## [4,] 3.605551 10.000000 5.830952
## [5,] 1.414214 13.000000 9.219544
## [6,] 11.401754 13.601471 2.236068
## [7,] 1.000000 10.770330 8.602325
## [8,] 9.219544 2.828427 9.486833
## [9,] 11.401754 1.000000 11.180340
## [10,] 11.704700 0.000000 12.083046
이에 각 객체들에 대해 거리가 가장 가까운 군집에 그 객체를 배정한다.
<- function(df, cluster_df) {
assign_cluster <- flexclust::dist2(df, cluster_df) %>%
cluster_ind apply(1, which.min)
map(unique(cluster_ind), ~which(cluster_ind == .))
}
<- assign_cluster(df[, -1], cluster_df)
cluster_objects
cluster_objects
## [[1]]
## [1] 1 2 6
##
## [[2]]
## [1] 3 4 5 7
##
## [[3]]
## [1] 8 9 10
[단계 2] (군집 중심좌표의 산출) 새로운 군집에 대한 중심좌표를 산출한다.
\[\begin{equation*} \bar{x}^{(j)}_l = \frac{\sum_{i} a_{ij} x_{li}}{\sum_{i} a_{ij}}, \, l = 1, \cdots, p, \, j = 1, \cdots, K \end{equation*}\]
<- function(df, cluster) {
find_center map_dfr(cluster, ~df[., ] %>% summarize_all(mean))
}
<- find_center(df[, -1], cluster_objects)
new_cluster_df
new_cluster_df
## # A tibble: 3 x 2
## x1 x2
## <dbl> <dbl>
## 1 7 14
## 2 13.2 6.75
## 3 3.67 3
[단계 3] (수렴 조건 점검) 새로 산출된 중심좌표값과 이전 좌표값을 비교하여 수렴 조건 내에 들면 마치며, 그렇지 않으면 단계 1을 반복한다.
identical(cluster_df, new_cluster_df)
## [1] FALSE
위의 경우, 군집 중심좌표가 다르므로 다음 iteration을 진행한다.
13.2.3 R 스크립트 구현
위의 과정을 군집해가 수렴할 때까지 반복하도록 아래와 같이 R 스크립트를 구현해보자.
<- function(df, k = 1) {
init_cluster <- min(k, nrow(df))
k
%>% sample_n(k)
df
}
<- function(df, cluster_df) {
assign_cluster <- flexclust::dist2(df, cluster_df) %>%
cluster_ind apply(1, which.min)
map(unique(cluster_ind), ~which(cluster_ind == .))
}
<- function(df, cluster) {
find_center map_dfr(cluster, ~df[., ] %>% summarize_all(mean))
}
<- function(df, k = 1, verbose = FALSE) {
kmeans_cluster <- min(k, nrow(df))
k
<- 0L
i
## 단계 0
<- init_cluster(df, k)
cluster_df
while(TRUE) {
<- i + 1L
i
## 단계 1
<- assign_cluster(df, cluster_df)
cluster_objects if (verbose) { # 군집해 출력
cat("Iteration", i, ":",
map(cluster_objects, ~ str_c("{", str_c(., collapse = ", "), "}")) %>%
str_c(collapse = ", "),
"\n"
)
}
## 단계 2
<- find_center(df, cluster_objects)
new_cluster_df
## 단계 3
if(identical(cluster_df, new_cluster_df)) break
<- new_cluster_df
cluster_df
}
<- list(
res cluster_centers = cluster_df,
assgined_objects = cluster_objects,
n_iteration = i
)
return (res)
}
set.seed(123)
<- kmeans_cluster(df[, -1], k = 3, verbose = TRUE) kmeans_solution
## Iteration 1 : {1, 2, 6}, {3, 4, 5, 7}, {8, 9, 10}
## Iteration 2 : {1, 2, 6}, {3, 4, 5, 7}, {8, 9, 10}
위와 같이 2번째 Iteration에서 군집해가 수렴하였으며, 최종 군집해는 아래와 같다.
$assgined_objects kmeans_solution
## [[1]]
## [1] 1 2 6
##
## [[2]]
## [1] 3 4 5 7
##
## [[3]]
## [1] 8 9 10
13.3 K-medoids 군집방법
K-means 알고리즘에서는 각 군집의 중심좌표(centroid)를 군집 중심으로 고려하고 있는 반면, K-medoids 군집방법에서는 각 군집의 대표객체를 군집 중심으로 고려한다.
K-medoids 군집방법의 알고리즘으로 잘 알려진 것에는 다음과 같은 것들이 있다.
- PAM(Partitioning Around Medoids)
- CLARA(Clustering LARge Applications)
- CLARANS(Clustering Large Applications based on RANdomized Search)
- K-means-like 알고리즘
13.3.1 PAM 알고리즘
PAM 알고리즘은 Kaufman and Rousseeuw (1990) 에 의하여 발표된 것으로, 초기 대표객체를 선정하는 방법인 BUILD와 더 나은 군집해를 찾아나가는 과정인 SWAP의 두 부분으로 구성되어 있다.
13.3.1.1 기본 R 스크립트
<- tribble(
df ~id, ~x1, ~x2,
1, 3, 3,
2, 5, 4,
3, 11, 8,
4, 13, 6,
5, 14, 6,
6, 15, 7
)
%>%
df ::kable(
knitrbooktabs = TRUE,
align = c('r', 'r', 'r'),
col.names = c(
'객체번호',
'사용경력($x_1$)', '사용시간($x_2$)'
),caption = 'PC 사용자 데이터'
)
객체번호 | 사용경력(\(x_1\)) | 사용시간(\(x_2\)) |
---|---|---|
1 | 3 | 3 |
2 | 5 | 4 |
3 | 11 | 8 |
4 | 13 | 6 |
5 | 14 | 6 |
6 | 15 | 7 |
PAM 알고리즘은 cluster
패키지 내의 함수 pam
을 이용하여 아래와 같이 간단하게 실행할 수 있다.
<- cluster::pam(df[, -1], k = 2) pam_solution
얻어진 pam
객체의 원소 id.med
는 몇 번째 객체가 군집의 대표객체로 선정되었는지를 나타낸다.
$id.med pam_solution
## [1] 2 5
또한 pam
객체의 원소 clustering
은 각 객체가 어떠한 군집에 할당되었는지를 보여준다.
$clustering pam_solution
## [1] 1 1 2 2 2 2
13.3.1.2 PAM 알고리즘
PAM 알고리즘의 각 단계를 Table 13.2의 예제 데이터에 적용하여 살펴보기로 하자.
13.3.1.2.1 BUILD
BUILD는 다음 절차들을 거쳐서 \(K\)개의 초기 대표객체를 구하는 과정이다.
[단계 0] 우선 각 객체별로 다른 객체 간의 거리를 구한 후, 그 합이 가장 작은 객체 하나를 대표객체로 선정한다. 선정된 대표객체집합을 \(M\)이라 하자.
<- dist(df[, -1], upper = TRUE) %>%
pairwise_distance_df ::tidy() %>%
broommutate_if(is.factor, ~ as.integer(as.character(.)))
<- pairwise_distance_df %>%
M_idx group_by(item1) %>%
summarize(sum_distance = sum(distance)) %>%
top_n(-1, sum_distance) %>%
$item1
.
<- df[M_idx, ]
M
M
## # A tibble: 1 x 3
## id x1 x2
## <dbl> <dbl> <dbl>
## 1 4 13 6
[단계 1] 대표객체로 선정되지 않은 객체 \(j\)에 대하여, 이전에 대표객체로 선정된 객체들 중 객체 \(j\)에 가장 가까운 거리 \(D_j\)를 구한다. 즉,
\[\begin{equation*} D_j = \min_{k \in M} d(j, k), \, j \notin M \end{equation*}\]
<- pairwise_distance_df %>%
D_j filter(
!item1 %in% M$id,
%in% M$id
item2 %>%
) group_by(item1) %>%
top_n(-1, distance) %>%
ungroup() %>%
rename(D_j = distance) %>%
select(-item2)
D_j
## # A tibble: 5 x 2
## item1 D_j
## <int> <dbl>
## 1 1 10.4
## 2 2 8.25
## 3 3 2.83
## 4 5 1
## 5 6 2.24
그리고 대표객체로 선정되지 않은 두 객체 \(i\), \(j\)에 대하여 다음을 산출한다.
\[\begin{equation*} C_{ji} = \max \left(D_j - d(j, i), 0\right) \end{equation*}\]
<- pairwise_distance_df %>%
d_ji filter(
!item1 %in% M$id,
!item2 %in% M$id
)
<- D_j %>%
C_ji inner_join(d_ji, by = "item1") %>%
mutate(C_ji = pmax(D_j - distance, 0))
C_ji
## # A tibble: 20 x 5
## item1 D_j item2 distance C_ji
## <int> <dbl> <int> <dbl> <dbl>
## 1 1 10.4 2 2.24 8.20
## 2 1 10.4 3 9.43 1.01
## 3 1 10.4 5 11.4 0
## 4 1 10.4 6 12.6 0
## 5 2 8.25 1 2.24 6.01
## 6 2 8.25 3 7.21 1.04
## 7 2 8.25 5 9.22 0
## 8 2 8.25 6 10.4 0
## 9 3 2.83 1 9.43 0
## 10 3 2.83 2 7.21 0
## 11 3 2.83 5 3.61 0
## 12 3 2.83 6 4.12 0
## 13 5 1 1 11.4 0
## 14 5 1 2 9.22 0
## 15 5 1 3 3.61 0
## 16 5 1 6 1.41 0
## 17 6 2.24 1 12.6 0
## 18 6 2.24 2 10.4 0
## 19 6 2.24 3 4.12 0
## 20 6 2.24 5 1.41 0.822
이는 객체 \(i\)가 추가로 대표객체가 된다고 할 때, 객체 \(j\)의 입장에서 거리 감소량이다.
[단계 2] 다음과 같이 거리감소량이 가장 큰 객체 \(m\)을 대표객체에 포함시키고,
\[\begin{equation*} m = \arg\,\max_{i \notin M} \sum_{j \notin M} C_{ji} \end{equation*}\]
<- C_ji %>%
m group_by(item2) %>%
summarize(sum_C_ji = sum(C_ji)) %>%
top_n(1, sum_C_ji) %>%
$item2
.
m
## [1] 2
대표객체집합을 수정한다.
\[\begin{equation*} M \leftarrow M \cup \{m\} \end{equation*}\]
<- M %>%
M bind_rows(df[m, ])
[단계 3] \(K\)개의 대표객체가 선정되었으면 Stop, 그렇지 않은 경우에는 [단계 1]로 되돌아간다.
13.3.1.2.2 SWAP
SWAP은 대표객체로 선정되어 있는 객체 \(i\)와 선정되지 않은 객체 \(h\)를 교환할 때 목적함수의 변화량을 산출해 더 나은 목적함수값을 찾아가는 과정이다.
[단계 1] 객체 \(i\)와 객체 \(h\)를 교환할 때 목적함수의 변화량을 산출하기 위하여 우선, 대표객체로 선정되지 않은 임의의 객체 \(j \neq h\)에서의 변화량을 다음과 같이 산출한다.
\[\begin{eqnarray*} C_{jih} &=& \text{($i$와 $h$를 교환 후 객체 $j$와 대표객체와의 거리)}\\ & & - \text{(교환 전 객체 $j$와 대표객체와의 거리)},\\ & & \, (j \notin M, i \in M, h \notin M) \end{eqnarray*}\]
# 교환 전 각 객체와 대표 객체와의 거리
<- pairwise_distance_df %>%
D_j filter(
!item1 %in% M$id,
%in% M$id
item2 %>%
) group_by(item1) %>%
top_n(-1, distance) %>%
ungroup() %>%
rename(j = item1) %>%
select(-item2)
D_j
## # A tibble: 4 x 2
## j distance
## <int> <dbl>
## 1 1 2.24
## 2 3 2.83
## 3 5 1
## 4 6 2.24
# 교환할 객체
<- tibble(
swap_ids i = rep(M$id, each = nrow(df) - nrow(M)),
h = rep(setdiff(df$id, M$id), nrow(M))
)
# 교환 후 각 객체와 대표 객체와의 거리
<- function(i, h, distance_df, center_ids) {
update_distance <- c(setdiff(center_ids, i), h)
new_center_ids
<- distance_df %>%
res filter(
!item1 %in% c(center_ids, h),
%in% new_center_ids
item2 %>%
) group_by(item1) %>%
top_n(-1, distance) %>%
ungroup() %>%
rename(j = item1) %>%
select(-item2) %>%
mutate(
i = i,
h = h
)
return(res)
}
<- pmap_dfr(
D_jih
swap_ids,
update_distance,distance_df = pairwise_distance_df,
center_ids = M$id
)
D_jih
## # A tibble: 24 x 4
## j distance i h
## <int> <dbl> <dbl> <dbl>
## 1 3 7.21 4 1
## 2 5 9.22 4 1
## 3 6 10.4 4 1
## 4 1 2.24 4 3
## 5 5 3.61 4 3
## 6 6 4.12 4 3
## 7 1 2.24 4 5
## 8 3 3.61 4 5
## 9 6 1.41 4 5
## 10 1 2.24 4 6
## # … with 14 more rows
# 거리의 변화량
<- D_j %>%
C_jih inner_join(D_jih, by = "j", suffix = c("", "_new")) %>%
mutate(diff_distance = distance_new - distance)
C_jih
## # A tibble: 24 x 6
## j distance distance_new i h diff_distance
## <int> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 1 2.24 2.24 4 3 0
## 2 1 2.24 2.24 4 5 0
## 3 1 2.24 2.24 4 6 0
## 4 1 2.24 9.43 2 3 7.20
## 5 1 2.24 10.4 2 5 8.20
## 6 1 2.24 10.4 2 6 8.20
## 7 3 2.83 7.21 4 1 4.38
## 8 3 2.83 3.61 4 5 0.777
## 9 3 2.83 4.12 4 6 1.29
## 10 3 2.83 2.83 2 1 0
## # … with 14 more rows
[단계 2] 대표객체 \(i\)를 \(h\)로 교환하는 경우 총 변화량은 다음과 같다.
\[\begin{equation*} T_{ih} = \sum_{j} C_{jih} \end{equation*}\]
<- C_jih %>%
T_ih group_by(i, h) %>%
summarize(total_diff_distance = sum(diff_distance))
## `summarise()` has grouped output by 'i'. You can override using the `.groups` argument.
T_ih
## # A tibble: 8 x 3
## # Groups: i [2]
## i h total_diff_distance
## <dbl> <dbl> <dbl>
## 1 2 1 0
## 2 2 3 7.20
## 3 2 5 7.38
## 4 2 6 8.20
## 5 4 1 20.8
## 6 4 3 4.49
## 7 4 5 -0.0447
## 8 4 6 1.71
이 때 \(\min_{i, h} T_{ih}\)에 대응하는 객체 \(i^*\)와 \(h^*\)를 찾아 \(T_{i^*h^*} < 0\)이면 교환한 후에 다시 [단계 1]으로 돌아가고, \(T_{i^*h^*} \geq 0\)이면 교환하지 않고 stop.
<- T_ih %>%
swap_ih filter(total_diff_distance < 0) %>%
top_n(-1, total_diff_distance)
swap_ih
## # A tibble: 1 x 3
## # Groups: i [1]
## i h total_diff_distance
## <dbl> <dbl> <dbl>
## 1 4 5 -0.0447
본 예제의 경우 객체 4 대신 객체 5가 새로운 대표객체로 선택되고, 다시 [단계 1]로 넘어간다.
<- M %>%
M anti_join(df[swap_ih$i, ]) %>%
bind_rows(df[swap_ih$h, ])
## Joining, by = c("id", "x1", "x2")
M
## # A tibble: 2 x 3
## id x1 x2
## <dbl> <dbl> <dbl>
## 1 2 5 4
## 2 5 14 6
SWAP 과정이 종료된 후, 최종 군집해는 각 객체를 가장 가까운 대표객체가 속한 군집에 할당함으로써 얻어진다.
13.3.1.3 R 스크립트 구현
일련의 과정을 함수로 구현해보자.
# 각 객체로부터 가장 가까운 대표객체까지의 거리
<- function(distance_df, medoids_ids) {
distance_from_medoids %>%
distance_df filter(
!item1 %in% medoids_ids,
%in% medoids_ids
item2 %>%
) group_by(item1) %>%
arrange(distance) %>%
slice(1) %>%
ungroup() %>%
rename(j = item1) %>%
select(-item2)
}
# k개의 초기 대표객체를 선정
<- function(distance_df, k = 1L) {
build_medoids <- min(k, length(unique(distance_df$item1)))
k
# 첫 번째 대표객체 선정
<- distance_df %>%
medoids_ids group_by(item1) %>%
summarize(sum_distance = sum(distance)) %>%
arrange(sum_distance) %>%
slice(1) %>%
$item1
.
while (length(medoids_ids) < k) {
<- distance_from_medoids(distance_df, medoids_ids)
D_j
<- distance_df %>%
d_ji filter(
!item1 %in% medoids_ids,
!item2 %in% medoids_ids
%>%
) rename(j = item1, i = item2)
# 거리 감소량
<- D_j %>%
C_ji inner_join(d_ji, by = "j",
suffix = c("", "_new")) %>%
mutate(diff_distance = pmax(distance - distance_new, 0))
# 거리 감소량이 가장 큰 객체 선택
<- C_ji %>%
m group_by(i) %>%
summarize(total_diff_distance = sum(diff_distance)) %>%
arrange(desc(total_diff_distance)) %>%
slice(1) %>%
$i
.
# 대표객체에 추가
<- c(medoids_ids, m)
medoids_ids
}
return(medoids_ids)
}
# 교환 후 각 객체와 대표 객체와의 거리
<- function(i, h, distance_df, medoids_ids) {
update_distance <- c(setdiff(medoids_ids, i), h)
new_medoids_ids
<- distance_from_medoids(distance_df, new_medoids_ids) %>%
res filter(j != h) %>%
mutate(
i = i,
h = h
)
return(res)
}
# 교환할 medoid 선택
<- function(distance_df, medoids_ids) {
swap_medoids <- unique(distance_df$item1)
observation_ids
# 교환 전 각 객체와 대표 객체와의 거리
<- distance_from_medoids(distance_df, medoids_ids)
D_j
# 교환할 객체
<- tibble(
swap_ids i = rep(medoids_ids,
each = length(observation_ids) - length(medoids_ids)),
h = rep(setdiff(observation_ids, medoids_ids),
length(medoids_ids))
)
<- pmap_dfr(
D_jih
swap_ids,
update_distance,distance_df = distance_df,
medoids_ids = medoids_ids
)
# 거리의 변화량
<- D_j %>%
C_jih inner_join(D_jih, by = "j", suffix = c("", "_new")) %>%
mutate(diff_distance = distance_new - distance)
<- C_jih %>%
T_ih group_by(i, h) %>%
summarize(total_diff_distance = sum(diff_distance))
<- T_ih %>%
swap_ih filter(total_diff_distance < 0) %>%
arrange(total_diff_distance) %>%
slice(1)
return(list(remove = swap_ih$i, add = swap_ih$h))
}
# 전체 PAM 알고리즘
<- function(distance_df, k = 1L) {
pam_medoids # BUILD
<- build_medoids(distance_df, k)
medoids_ids <- length(medoids_ids)
k
# SWAP
while (TRUE) {
<- swap_medoids(distance_df, medoids_ids)
swap_medoids if (is_empty(swap_medoids$remove)) {
break
else {
} <- c(
medoids_ids setdiff(medoids_ids, swap_medoids$remove),
$add
swap_medoids
)
}
}
return (medoids_ids)
}
위 구현한 함수를 이용하여 아래와 같이 PAM을 실행해보자.
<- dist(df[, -1], upper = TRUE) %>%
pairwise_distance_df ::tidy() %>%
broommutate_if(is.factor, ~ as.integer(as.character(.)))
<- pam_medoids(pairwise_distance_df, k = 2) medoids_ids
## `summarise()` has grouped output by 'i'. You can override using the `.groups` argument.
## `summarise()` has grouped output by 'i'. You can override using the `.groups` argument.
df[medoids_ids, ]
## # A tibble: 2 x 3
## id x1 x2
## <dbl> <dbl> <dbl>
## 1 2 5 4
## 2 5 14 6
위와 같이 2개의 대표객체 2, 5가 선정된다.
최종 군집해는 각 객체를 가장 가까운 대표객체가 속한 군집에 할당함으로써 얻어진다.
<- function(distance_df, medoids_ids) {
assign_cluster %>%
distance_df filter(
!item1 %in% medoids_ids,
%in% medoids_ids
item2 %>%
) group_by(item1) %>%
arrange(distance) %>%
slice(1) %>%
ungroup() %>%
bind_rows(
tibble(
item1 = medoids_ids,
item2 = medoids_ids,
distance = 0
)%>%
) rename(object = item1) %>%
arrange(object) %>%
group_by(item2) %>%
mutate(cluster = str_c("{", str_c(object, collapse = ", "), "}")) %>%
ungroup() %>%
select(-item2)
}
assign_cluster(pairwise_distance_df, medoids_ids)
## # A tibble: 6 x 3
## object distance cluster
## <int> <dbl> <chr>
## 1 1 2.24 {1, 2}
## 2 2 0 {1, 2}
## 3 3 3.61 {3, 4, 5, 6}
## 4 4 1 {3, 4, 5, 6}
## 5 5 0 {3, 4, 5, 6}
## 6 6 1.41 {3, 4, 5, 6}
13.3.2 CLARA 알고리즘
PAM 알고리즘은 SWAP 부분에서 모든 가능한 경우를 고려하기 때문에, 전체 객체 수가 많은 경우 계산 시간이 매우 길다는 단점이 있다. 이를 보완하기 위해 CLARA는 적절한 수의 객체를 샘플링한 후 이들에 대해 PAM 알고리즘을 적용하여 중심객체를 선정하는 방법이다. 이러한 샘플링을 여러 번 한 후, 이 중 가장 좋은 결과를 택하는 것인데, 반복수는 5번으로 충분한 것으로 분석되고 있다.
자세한 내용은 교재 (전치혁 2012) 참조
13.3.2.1 기본 R 스크립트
<- cluster::clara(df[, -1], k = 2) clara_solution
얻어진 clara
객체의 원소 i.med
는 몇 번째 객체가 군집의 대표객체로 선정되었는지를 나타낸다.
$i.med clara_solution
## [1] 2 5
또한 clara
객체의 원소 clustering
은 각 객체가 어떠한 군집에 할당되었는지를 보여준다.
$clustering clara_solution
## [1] 1 1 2 2 2 2
13.3.3 CLARANS 알고리즘
교재 (전치혁 2012) 참조
13.3.4 K-means-like 알고리즘
본 알고리즘은 PAM 알고리즘의 단점을 보완하고자 Park and Jun (2009) 에 의해 제안된 것으로, 대표객체를 반복적으로 수정하는데 K-means 알고리즘의 작동 원리를 모방한 K-medoids 군집 방법이다. 따라서 간단하며 계산 시간이 빠른 것이 장점이라 하겠다. 이 알고리즘은 다음과 같이 3단계로 구성되어 있다. 알고리즘의 각 단계를 Table 13.2의 예제 데이터에 적용하여 살펴보기로 하자.
[단계 1] (초기 대표객체 선정) \(K\)개의 초기 대표객체를 선정하며, 각 객체를 가장 가까운 대표객체에 배정하여 초기 군집해를 얻는다.
객체들 간의 거리 \(d(i, j), \, i, j = 1, \cdots, n\)를 구한다.
<- dist(df[, -1], upper = TRUE) %>%
pairwise_distance_df ::tidy() %>%
broommutate_if(is.factor, ~ as.integer(as.character(.)))
각 객체 \(j = 1, \cdots, n\)에 대하여 다음을 산출한다.
\[\begin{equation*} v_j = \sum_{i = 1}^{n} \frac{d(i, j)}{\sum_{k = 1}^{n} d(i, k)} \end{equation*}\]
<- pairwise_distance_df %>%
v_j group_by(item2) %>%
mutate(prop = distance / sum(distance)) %>%
ungroup() %>%
group_by(item1) %>%
summarize(sum_prop = sum(prop)) %>%
rename(j = item1)
v_j
## # A tibble: 6 x 2
## j sum_prop
## <int> <dbl>
## 1 1 1.67
## 2 2 1.33
## 3 3 0.781
## 4 4 0.661
## 5 5 0.713
## 6 6 0.849
이후 \(v_j\)값들을 오름차순으로 정렬하여 가장 작은 \(K\)개의 값을 초기 대표객체로 선정한다. 본 예에서는 \(K = 2\)로 가정하자.
<- v_j %>%
medoids_ids arrange(sum_prop) %>%
slice(1:2) %>%
$j
.
medoids_ids
## [1] 4 5
이후 객체를 배정하여 군집해를 얻는다. 위에서 정의했던 assign_cluster
함수를 재사용하자.
<- assign_cluster(
cluster_solution
pairwise_distance_df,
medoids_ids
)
cluster_solution
## # A tibble: 6 x 3
## object distance cluster
## <int> <dbl> <chr>
## 1 1 10.4 {1, 2, 3, 4}
## 2 2 8.25 {1, 2, 3, 4}
## 3 3 2.83 {1, 2, 3, 4}
## 4 4 0 {1, 2, 3, 4}
## 5 5 0 {5, 6}
## 6 6 1.41 {5, 6}
[단계 2] (대표객체의 수정) 현재의 군집에 배정된 객체들의 대표객체를 구하여 새로운 대표객체로 삼는다. 새로운 대표객체는 같은 군집에 배정된 다른 객체들로부터의 거리의 합이 최소가 되는 객체이다.
<- function(distance_df, ids) {
find_medoids %>%
distance_df filter(
%in% ids,
item1 %in% ids
item2 %>%
) group_by(item1) %>%
summarize(total_distance = sum(distance)) %>%
arrange(total_distance) %>%
slice(1) %>%
$item1
.
}
<- cluster_solution %>%
cluster_objects split(.$cluster) %>%
map(~.$object)
<- map_int(
medoids_ids
cluster_objects,
find_medoids,distance_df = pairwise_distance_df
)
medoids_ids
## {1, 2, 3, 4} {5, 6}
## 2 5
[단계 3] (객체의 배정) 각 객체를 가장 가까운 대표객체에 배정하여 군집해를 얻는다. 군집해가 이전과 동일하면 Stop하고, 그렇지 않으면 [단계 2]를 반복한다.
<- assign_cluster(
new_cluster_solution
pairwise_distance_df,
medoids_ids
)
<- near(
is_converge 1,
# clusteval::cluster_similarity(
# as.factor(cluster_solution$cluster),
# as.factor(new_cluster_solution$cluster),
# similarity = "rand"
# )
::randIndex(
flexclustas.factor(cluster_solution$cluster),
as.factor(new_cluster_solution$cluster)
)
)
print(is_converge)
## ARI
## FALSE
위의 경우 첫 번째 iteration에서 군집해가 수정되었으므로 다음 iteration을 수행한다.
13.3.4.1 R 스크립트 구현
위 일련의 과정들을 수행하는 R 함수 스크립트를 구현해보자.
<- function(distance_df, k = 1) {
init_medoids <- unique(distance_df$item1)
object_ids <- min(k, length(object_ids))
k
<- distance_df %>%
v_j group_by(item2) %>%
mutate(prop = distance / sum(distance)) %>%
ungroup() %>%
group_by(item1) %>%
summarize(sum_prop = sum(prop)) %>%
rename(j = item1)
<- v_j %>%
medoids_ids arrange(sum_prop) %>%
slice(1:k) %>%
$j
.
return(medoids_ids)
}
<- function(distance_df, medoids_ids) {
assign_cluster %>%
distance_df filter(
!item1 %in% medoids_ids,
%in% medoids_ids
item2 %>%
) group_by(item1) %>%
arrange(distance) %>%
slice(1) %>%
ungroup() %>%
bind_rows(
tibble(
item1 = medoids_ids,
item2 = medoids_ids,
distance = 0
)%>%
) rename(object = item1) %>%
arrange(object) %>%
group_by(item2) %>%
mutate(cluster = str_c("{", str_c(object, collapse = ", "), "}")) %>%
ungroup() %>%
select(-item2)
}
<- function(distance_df, ids) {
find_medoids %>%
distance_df filter(
%in% ids,
item1 %in% ids
item2 %>%
) group_by(item1) %>%
summarize(total_distance = sum(distance)) %>%
arrange(total_distance) %>%
slice(1) %>%
$item1
.
}
<- function(distance_df, k = 1) {
kmeans_like_kmedoids <- init_medoids(distance_df, k)
medoids_ids
while (TRUE) {
<- assign_cluster(distance_df, medoids_ids)
cluster_solution
<- cluster_solution %>%
cluster_objects split(.$cluster) %>%
map(~.$object)
<- map_int(
medoids_ids
cluster_objects,
find_medoids,distance_df = distance_df
)
<- assign_cluster(distance_df, medoids_ids)
new_cluster_solution
<- near(
is_converge 1,
::randIndex(
flexclustas.factor(cluster_solution$cluster),
as.factor(new_cluster_solution$cluster)
)
)
if (is_converge) break
}
return(medoids_ids)
}
위에서 정의한 함수 kmeans_like_kmedoids
를 Table 13.2의 데이터에 적용한 군집결과 및 군집 대표객체는 아래와 같이 얻어진다.
<- dist(df[, -1], upper = TRUE) %>%
pairwise_distance_df ::tidy() %>%
broommutate_if(is.factor, ~ as.integer(as.character(.)))
<- kmeans_like_kmedoids(pairwise_distance_df, k = 2)
medoids_ids
medoids_ids
## {1, 2} {3, 4, 5, 6}
## 1 5
13.4 퍼지 K-means 알고리즘
이 방법은 K-means 알고리즘과 유사하나, 하나의 객체가 여러 군집에 속할 가능성을 허용하는 확률 또는 이를 확장한 퍼지(fuzzy) 개념을 도입한 것이다. 객체 \(i\)가 군집 \(j\)에 속할 확률 \(P_{ij}\)를 구하는 문제이다.
13.4.1 기본 R 스크립트
<- tibble(
df id = c(1:10),
x1 = c(6, 8, 14, 11, 15, 7, 13, 5, 3, 3),
x2 = c(14, 13, 6, 8, 7, 15, 6, 4, 3, 2)
)
%>%
df ::kable(
knitrbooktabs = TRUE,
align = c('r', 'r', 'r'),
col.names = c(
'객체번호',
'사용경력($x_1$)',
'사용시간($x_2$)'
),caption = 'PC 사용자 데이터'
)
객체번호 | 사용경력(\(x_1\)) | 사용시간(\(x_2\)) |
---|---|---|
1 | 6 | 14 |
2 | 8 | 13 |
3 | 14 | 6 |
4 | 11 | 8 |
5 | 15 | 7 |
6 | 7 | 15 |
7 | 13 | 6 |
8 | 5 | 4 |
9 | 3 | 3 |
10 | 3 | 2 |
::fanny(df[, -1], k = 3, metric = "SqEuclidean") cluster
## Fuzzy Clustering object of class 'fanny' :
## m.ship.expon. 2
## objective 18.37061
## tolerance 1e-15
## iterations 12
## converged 1
## maxit 500
## n 10
## Membership coefficients (in %, rounded):
## [,1] [,2] [,3]
## [1,] 98 1 1
## [2,] 96 3 2
## [3,] 1 99 1
## [4,] 12 80 8
## [5,] 2 96 2
## [6,] 98 1 1
## [7,] 1 99 1
## [8,] 3 3 94
## [9,] 0 0 99
## [10,] 1 1 98
## Fuzzyness coefficients:
## dunn_coeff normalized
## 0.9225653 0.8838480
## Closest hard clustering:
## [1] 1 1 2 2 2 1 2 3 3 3
##
## Available components:
## [1] "membership" "coeff" "memb.exp"
## [4] "clustering" "k.crisp" "objective"
## [7] "convergence" "diss" "call"
## [10] "silinfo" "data"
13.4.2 알고리즘
[단계 0] 초기 \(K\)개의 군집을 임의로 결정한다.
\[\begin{equation*} P_{ij} = \begin{cases} 1 & \text{ if object $i$ belongs to cluster $j$}\\ 0 & \text{ otherwise} \end{cases} \end{equation*}\]
<- function(df, k = 1) {
init_cluster <- min(k, nrow(df))
k
while (TRUE) {
<- sample.int(k, size = nrow(df), replace = TRUE)
cluster_ind if (length(unique(cluster_ind)) == k) break
}
map_dfc(unique(cluster_ind), ~ as.double(cluster_ind == .))
}
set.seed(4000)
<- init_cluster(df[, -1], k = 3) cluster_membership
## New names:
## * NA -> ...1
## * NA -> ...2
## * NA -> ...3
cluster_membership
## # A tibble: 10 x 3
## ...1 ...2 ...3
## <dbl> <dbl> <dbl>
## 1 1 0 0
## 2 1 0 0
## 3 0 1 0
## 4 1 0 0
## 5 0 0 1
## 6 0 1 0
## 7 0 0 1
## 8 0 1 0
## 9 1 0 0
## 10 1 0 0
[단계 1] 각 군집의 중심좌표를 산출한다.
\[\begin{equation*} \mathbf{c}_j = \frac{\sum_{i = 1}^{n} P_{ij}^{m} \mathbf{x}_i}{\sum_{i = 1}^{n} P_{ij}^{m}} \end{equation*}\]
여기에서 상수 \(m\)은 1보다 큰 값을 사용한다.
<- function(df, p, m) {
find_center <- p ^ m
wt %>%
df summarize_all(weighted.mean, w = wt)
}
<- map_dfr(cluster_membership, find_center, df = df[, -1], m = 2)
cluster_df
cluster_df
## # A tibble: 3 x 2
## x1 x2
## <dbl> <dbl>
## 1 6.2 8
## 2 8.67 8.33
## 3 14 6.5
[단계 2] 군집 membership 계수 \(P_{ij}\)를 업데이트한다.
\[\begin{equation*} P_{ij} = \frac{d(\mathbf{x}_i, \mathbf{c}_j)^{-\frac{1}{m - 1}}}{\sum_{a = 1}^{K} d(\mathbf{x}_i, \mathbf{c}_a)^{-\frac{1}{m - 1}}} \end{equation*}\]
여기에서 거리함수 \(d()\)는 제곱 유클리드 거리를 사용한다.
<- function(df, cluster_df, m) {
update_membership <- flexclust::dist2(df, cluster_df) ^ 2
distance_mat
<- distance_mat ^ (-1 / (m - 1)) %>%
p `/`(rowSums(.)) %>%
as_tibble(.name_repair = "minimal")
p
}
update_membership(df[, -1], cluster_df, m = 2)
## # A tibble: 10 x 3
## `` `` ``
## <dbl> <dbl> <dbl>
## 1 0.451 0.414 0.135
## 2 0.380 0.483 0.137
## 3 0.00381 0.00730 0.989
## 4 0.139 0.576 0.285
## 5 0.0152 0.0285 0.956
## 6 0.406 0.427 0.166
## 7 0.0231 0.0479 0.929
## 8 0.574 0.311 0.115
## 9 0.542 0.315 0.143
## 10 0.508 0.325 0.166
13.4.3 R 스크립트 구현
<- function(df, k = 1, m = 2, max_iter = 1000L, tol = 1e-9) {
fuzzy_kmeans <- min(k, nrow(df))
k <- 0L
i
<- init_cluster(df, k)
cluster_membership
while (i < max_iter) {
<- i + 1L
i
<- map_dfr(cluster_membership, find_center, df = df, m = m)
cluster_df
<- update_membership(df, cluster_df, m)
new_cluster_membership
if (max(abs(cluster_membership - new_cluster_membership)) < tol) break
<- new_cluster_membership
cluster_membership
}
<- list(
res n_iteration = i,
center = cluster_df,
membership = cluster_membership %>% as.matrix()
)
return (res)
}
set.seed(4000)
<- fuzzy_kmeans(df[, -1], k = 3, m = 2) fuzzy_kmeans_solution
## New names:
## * NA -> ...1
## * NA -> ...2
## * NA -> ...3
$n_iteration fuzzy_kmeans_solution
## [1] 16
$center fuzzy_kmeans_solution
## # A tibble: 3 x 2
## x1 x2
## <dbl> <dbl>
## 1 3.64 2.98
## 2 7.00 14.0
## 3 13.4 6.63
$membership fuzzy_kmeans_solution
##
## [1,] 0.007809655 0.983147737 0.009042608
## [2,] 0.015726915 0.957515486 0.026757600
## [3,] 0.006068358 0.006268614 0.987663029
## [4,] 0.078772454 0.120672309 0.800555237
## [5,] 0.017165082 0.022105992 0.960728926
## [6,] 0.006532987 0.984345339 0.009121674
## [7,] 0.005945712 0.005766360 0.988287928
## [8,] 0.939278603 0.026072757 0.034648640
## [9,] 0.993661877 0.002989486 0.003348637
## [10,] 0.981125198 0.008481303 0.010393499
$membership %>%
fuzzy_kmeans_solutionapply(1, which.max)
## [1] 2 2 3 3 3 2 3 1 1 1
13.5 모형기반 군집방법
모형기반 군집방법(model-based clustering)에서는 각 객체가 혼합분포(mixture)를 따른다고 가정하여 객체의 군집배정변수를 통계적으로 추정하는 것이다.
13.5.1 기본 R script
<- tibble(
df id = c(1:7),
x1 = c(4, 6, 6, 10, 11, 12, 12),
x2 = c(12, 13, 15, 4, 3, 2, 5)
)
%>%
df ::kable(
knitrbooktabs = TRUE,
align = c('r', 'r', 'r'),
col.names = c(
'객체번호',
'$x_1$',
'$x_2$'
),caption = '모형기반 군집 학습 데이터'
)
객체번호 | \(x_1\) | \(x_2\) |
---|---|---|
1 | 4 | 12 |
2 | 6 | 13 |
3 | 6 | 15 |
4 | 10 | 4 |
5 | 11 | 3 |
6 | 12 | 2 |
7 | 12 | 5 |
Table 13.4의 7개의 객체를 두 개의 군집에 배정하는 간단한 R 스크립트는 아래와 같다. 여기에서 mclust::meVII
는 각 군집의 분산-공분산 행렬은 서로 다르되, 각 변수의 분산이 동일하며 공분산은 0이라 가정한다.
set.seed(1024)
# 군집 개수
<- 2
K
# z_ik 값 초기화
<- matrix(runif(nrow(df) * K), ncol = K) %>%
init_z `/`(apply(., 1, sum))
# EM 알고리즘 - 분산-공분산 행렬: unequal volume (V), spherical(II)
<- mclust::meVII(data = df[, -1], z = init_z)
sol
# 군집 사후확률
$z sol
## [,1] [,2]
## [1,] 7.596402e-28 1.000000e+00
## [2,] 8.254183e-27 1.000000e+00
## [3,] 9.463816e-36 1.000000e+00
## [4,] 1.000000e+00 6.832084e-20
## [5,] 1.000000e+00 1.473491e-25
## [6,] 1.000000e+00 4.876251e-31
## [7,] 1.000000e+00 1.480388e-20
# 혼합분포
$parameters sol
## $pro
## [1] 0.5714286 0.4285714
##
## $mean
## [,1] [,2]
## x1 11.25 5.333333
## x2 3.50 13.333333
##
## $variance
## $variance$modelName
## [1] "VII"
##
## $variance$d
## [1] 2
##
## $variance$G
## [1] 2
##
## $variance$sigma
## , , 1
##
## x1 x2
## x1 0.96875 0.00000
## x2 0.00000 0.96875
##
## , , 2
##
## x1 x2
## x1 1.222222 0.000000
## x2 0.000000 1.222222
##
##
## $variance$sigmasq
## [1] 0.968750 1.222222
##
## $variance$scale
## [1] 0.968750 1.222222
##
##
## $Vinv
## NULL
13.5.2 EM 알고리즘
우선, 필요한 기호를 다음과 같이 정의하자.
- \(f_k(\mathbf{x} \, | \, \theta_k)\): 군집 \(k\)에 속하는 객체 \(\mathbf{x}\)의 확률밀도함수(\(\theta_k\)는 관련 파라미터)
- \(\tau_k\): 임의의 객체가 군집 \(k\)에 속할 사전확률 (\(\tau_k \geq 0, \, \sum_{k = 1}^{K} \tau = 1\))
이 때, 임의의 객체 \(\mathbf{x}\)는 다음과 같은 혼합 확률밀도함수를 갖는다.
\[\begin{equation*} f(\mathbf{x} \, | \, \boldsymbol\theta) = \sum_{k = 1}^{K} \tau_k f_k(\mathbf{x} \, | \, \theta_k) \end{equation*}\]
본 장에서는 \(f_k\)가 다변량 정규분포를 나타낸다고 가정하자.
추가로, 각 객체가 속하는 군집에 대한 지시변수 \(z_{ik}\)를 아래와 같이 정의하자.
\[\begin{equation*} z_{ik} = \begin{cases} 1 & \text{ if object $i$ belongs to cluster $k$} \\ 0 & \text{ otherwise} \end{cases} \end{equation*}\]
이 \(z_{ik}\) 변수는 실제값이 관측되지 않는 변수이므로, 최우추정법을 이용하여 그 기대값, 즉 객체 \(i\)가 군집 \(k\)에 속할 확률을 추정한다. 보다 자세한 설명은 교재 (전치혁 2012) 참조.
앞의 13.5.1장에서 수행했던 예제를 단계별로 살펴보기로 하자.
[단계 0] \(\hat{z}_{ik}\)를 초기화한다.
set.seed(1024)
# 군집 개수
<- 2
K
# z_ik 추정값 초기화
<- matrix(runif(nrow(df) * K), ncol = K) %>%
z_hat `/`(apply(., 1, sum)) %>%
as_tibble() %>%
`names<-`(str_c("C", 1:K))
z_hat
## # A tibble: 7 x 2
## C1 C2
## <dbl> <dbl>
## 1 0.406 0.594
## 2 0.623 0.377
## 3 0.372 0.628
## 4 0.956 0.0444
## 5 0.0214 0.979
## 6 0.681 0.319
## 7 0.264 0.736
[단계 1] (M-step) \(\hat{z}_{ik}\)를 바탕으로 파라미터를 추정한다.
우선 \(\tau_k\)와 \(\boldsymbol\mu_k\)를 다음과 같이 추정한다.
\[\begin{equation*} \hat{\tau}_k = \frac{\sum_{i = 1}^{n} \hat{z}_{ik}}{n} \end{equation*}\]
<- map(z_hat, mean)
tau_hat tau_hat
## $C1
## [1] 0.4746762
##
## $C2
## [1] 0.5253238
\[\begin{equation*} \hat{\boldsymbol\mu}_k = \frac{\sum_{i = 1}^{n} \hat{z}_{ik} \mathbf{x}_i}{\sum_{i = 1}^{n} \hat{z}_{ik}} \end{equation*}\]
<- map(z_hat, ~colSums(.*df[, -1]) / sum(.))
mu_hat mu_hat
## $C1
## x1 x2
## 8.644042 7.559792
##
## $C2
## x1 x2
## 8.777757 7.853884
분산-공분산 행렬 \(\boldsymbol\Sigma_k\)의 추정은 분산-공분산 구조를 어떻게 가정하느냐에 따라 다르다. 우선, 일반적으로 분산-공분산 행렬 \(\boldsymbol\Sigma_k\)는 아래와 같이 decompose할 수 있다 (Banfield and Raftery 1993).
\[\begin{equation*} \boldsymbol\Sigma_k = \lambda_k \mathbf{D}_k \mathbf{A}_k \mathbf{D}_k^\top \end{equation*}\]
여기에서
- \(\lambda_k = \det{\boldsymbol\Sigma_k}^{1 / 2}\); 군집 \(k\)이 크기와 관련
- \(\mathbf{D}_k\): matrix of eigenvectors of \(\boldsymbol\Sigma_k\); 군집 \(k\)의 방향(orientation)과 관련
- \(\mathbf{A}_k\): diagonal matrix s.t. \(\det{\mathbf{A}_k} = 1\); 군집 \(k\)의 형태와 관련
이다. \(\lambda_k\), \(\mathbf{D}_k\) 및 \(\mathbf{A}_k\)에 적용되는 제약조건에 따라 분산-공분산 모형을 정의할 수 있다. 아래는 본 장에서 다룰 세 가지 모형이다.
tribble(
~model, ~sigma,
"VII", "$\\lambda_k \\mathbf{I}$",
"VEI", "$\\lambda_k \\mathbf{A}$",
"VEE", "$\\lambda_k \\mathbf{D} \\mathbf{A} \\mathbf{D}^\\top$"
%>%
) ::kable(
knitrbooktabs = TRUE,
align = c('c', 'c'),
col.names = c(
'분산-공분산 모형',
'$\\boldsymbol\\Sigma_k$'
),caption = 'Within-group 분산-공분산 모형'
)
분산-공분산 모형 | \(\boldsymbol\Sigma_k\) |
---|---|
VII | \(\lambda_k \mathbf{I}\) |
VEI | \(\lambda_k \mathbf{A}\) |
VEE | \(\lambda_k \mathbf{D} \mathbf{A} \mathbf{D}^\top\) |
즉,
- “VII”
- for all \(k\),\(\mathbf{D}_k = \mathbf{I}\)
- for all \(k\), \(\mathbf{A}_k = \mathbf{I}\)
- “VEI”
- for all \(k\), \(\mathbf{D}_k = \mathbf{I}\)
- for all \(k\), \(\mathbf{A}_k = \mathbf{A}\)
- “VEE”
- for all \(k\), \(\mathbf{D}_k = \mathbf{D}\)
- for all \(k\), \(\mathbf{A}_k = \mathbf{A}\)
Celeux and Govaert (1995) 에 각 분산-공분산 모형에 대한 추정값을 얻는 반복적 알고리즘이 소개되어 있으며, 이는 교재 (전치혁 2012)에도 설명되어 있다.
우선, 각 군집 \(k\) 내에서의 scatter matrix를 아래와 같이 계산한다.
\[\begin{equation*} \mathbf{W}_k = \sum_{i = 1}^{n} \hat{z}_{ik} (\mathbf{x}_i - \hat{\boldsymbol\mu}_k)(\mathbf{x}_i - \hat{\boldsymbol\mu}_k)^\top \end{equation*}\]
<- ncol(df[, -1])
p <- map(mu_hat,
W ~pmap_dfc(list(x = df[, -1], mu = .),
function(x, mu) x - mu)) %>%
map(as.matrix, nrow = p, ncol = p) %>%
map2(z_hat, ~ t(.x) %*% (.y * .x))
W
## $C1
## x1 x2
## x1 28.22794 -44.46516
## x2 -44.46516 82.36848
##
## $C2
## x1 x2
## x1 37.16942 -53.17491
## x2 -53.17491 92.90912
이후 분산-공분산 모형에 따라 아래와 같이 각 군집의 분산-공분산 행렬을 추정한다.
[VII의 경우] 반복 업데이트의 과정 없이 closed-form으로 해가 존재한다.
\[\begin{equation*} \lambda_k = \frac{Tr(\mathbf{W}_k)}{p \sum_{i = 1}^{n} \hat{z}_{ik}} \end{equation*}\]
\[\begin{equation*} \boldsymbol\Sigma_k = \lambda_k \mathbf{I} \end{equation*}\]
이 때 \(p\)는 관측값의 차원수이다 (\(\mathbf{x} \in \mathbb{R}^p\)).
[VEI의 경우]
VEI-0. 행렬 \(\mathbf{B} = \mathbf{I}\)로 초기화한다.
VEI-1. \(\lambda_k\)값을 아래와 같이 계산한다.
\[\begin{equation*} \lambda_k = \frac{Tr(\mathbf{W}_k \mathbf{B}^{-1})}{p \sum_{i = 1}^{n} \hat{z}_{ik}} \end{equation*}\]
VEI-2. 행렬 \(\mathbf{B}\)를 아래와 같이 업데이트한다.
\[\begin{equation*} \mathbf{B} = \frac{diag\left( \sum_{k = 1}^{K} \frac{\mathbf{W}_k}{\lambda_k} \right)}{\left( \det diag\left( \sum_{k = 1}^{K} \frac{\mathbf{W}_k}{\lambda_k} \right) \right)^{1 / p}} \end{equation*}\]
VEI-3. 결과가 수렴하면 종료, 그렇지 않으면 VEI-1로 돌아간다. 최종 수렴한 결과를 통해 각 군집의 분산-공분산 행렬을 아래와 같이 얻는다.
\[\begin{equation*} \boldsymbol\Sigma_k = \lambda_k \mathbf{B} \end{equation*}\]
[VEE의 경우]
VEE-0. 행렬 \(\mathbf{C} = \mathbf{I}\)로 초기화한다.
VEE-1. \(\lambda_k\)값을 아래와 같이 계산한다.
\[\begin{equation*} \lambda_k = \frac{Tr(\mathbf{W}_k \mathbf{C}^{-1})}{p \sum_{i = 1}^{n} \hat{z}_{ik}} \end{equation*}\]
VEE-2. 행렬 \(\mathbf{C}\)를 아래와 같이 업데이트한다.
\[\begin{equation*} \mathbf{C} = \frac{\sum_{k = 1}^{K} \frac{\mathbf{W}_k}{\lambda_k}}{\left( \det \sum_{k = 1}^{K} \frac{\mathbf{W}_k}{\lambda_k} \right)^{1 / p}} \end{equation*}\]
VEE-3. 결과가 수렴하면 종료, 그렇지 않으면 VEE-1로 돌아간다. 최종 수렴한 결과를 통해 각 군집의 분산-공분산 행렬을 아래와 같이 얻는다.
\[\begin{equation*} \boldsymbol\Sigma_k = \lambda_k \mathbf{C} \end{equation*}\]
위 각 분산-공분산 모형을 추정 과정에 대한 설명은, 보다 일반화된 분산-공분산 구조 \(\lambda_k \mathbf{D}_k \mathbf{A}_k \mathbf{D}_k^\top\)을 추정하는 과정을 각 모형에 추가되는 제약에 따라 조금 더 단순하게 표현한 것이다. 보다 자세한 내용은 Celeux and Govaert (1995) 참조.
아래와 같이 군집 내 분산-공분산 행렬을 추정하는 함수 estimate_mixture_cov
를 구현해보자. 해당 함수는 3개의 입력변수를 사용한다.
modelName
: 분산-공분산 모형 이름. “VII”, “VEI”, “VEE” 중 하나를 선택한다.W
:K
개의 scatter matrix (\(\mathbf{W}_1, \cdots, \mathbf{W}_K\))를 원소로 지니는 리스트 (list
)z
: \(z_{ik}\) 값을 지닌 데이터 프레임 (data.frame
). \(n\)개의 행과 \(K\)개의 열로 이루어진다. 즉, 행은 각 관측객체를 나타내며, 열은 각 군집을 나타낸다.
<- function(modelName, W, z) {
estimate_mixture_cov <- ncol(z)
K <- ncol(W[[1]])
p
# 초기화
<- map(1:K, ~ diag(1, p))
D <- map(1:K, ~ diag(1, p))
A
<- function(W, z, D, A, p) {
get_lambda pmap(
list(W, z, D, A),
function(W, z, D, A, p)
sum(diag(W %*% D %*% solve(A) %*% t(D))) / (sum(z) * p),
p = p
)
}
<- function(W, z, lambda, D, A, p) {
objective_value pmap_dbl(
list(W, z, lambda, D, A),
function(W, z, lambda, D, A, p)
sum(diag(W %*% D %*% solve(A) %*% t(D))) / lambda +
* sum(z) * log(lambda),
p p = p
%>%
) sum()
}
<- 0L
i <- Inf
obj
while(TRUE) {
<- i + 1L
i
<- get_lambda(W, z, D, A, p)
lambda <- objective_value(W, z, lambda, D, A, p)
new_obj
if (obj - new_obj < 1e-9) break
if (modelName == "VII") {
else if (modelName == "VEI") {
} <- map2(W, lambda, ~ diag(.x) / .y) %>%
B reduce(`+`) %>%
diag() %>%
`/`(det(.) ^ (1 / p))
<- map(1:K, ~ B)
A else if (modelName == "VEE") {
} <- map2(W, lambda, ~ .x / .y) %>%
C reduce(`+`) %>%
`/`(det(.) ^ (1 / p))
<- svd(C)
s <- map(1:K, ~ diag(s$d))
A <- map(1:K, ~ s$u)
D else {
} stop("Model ", modelName, " is not supported yet.")
}
<- new_obj
obj
}
<- pmap(
Sigma list(lambda, D, A),
function(lambda, D, A) lambda * (D %*% A %*% t(D))
)
return (list(
volume = lambda,
shape = A,
orientation = D,
Sigma = Sigma
)) }
초기값으로 주어진 \(\hat{z}_{ik}\) 를 이용하여 “VII” 구조의 분산-공분산 행렬을 구해보자.
estimate_mixture_cov("VII", W, z_hat)
## $volume
## $volume$C1
## [1] 16.64239
##
## $volume$C2
## [1] 17.68685
##
##
## $shape
## $shape[[1]]
## [,1] [,2]
## [1,] 1 0
## [2,] 0 1
##
## $shape[[2]]
## [,1] [,2]
## [1,] 1 0
## [2,] 0 1
##
##
## $orientation
## $orientation[[1]]
## [,1] [,2]
## [1,] 1 0
## [2,] 0 1
##
## $orientation[[2]]
## [,1] [,2]
## [1,] 1 0
## [2,] 0 1
##
##
## $Sigma
## $Sigma$C1
## [,1] [,2]
## [1,] 16.64239 0.00000
## [2,] 0.00000 16.64239
##
## $Sigma$C2
## [,1] [,2]
## [1,] 17.68685 0.00000
## [2,] 0.00000 17.68685
위 estimate_mixture_cov
함수에서 “VII”보다 일반화된 “VEI”와 “VEE” 분산-공분산 모형에 대한 추정도 구현되어 있다.
# estimate_mixture_cov("VEI", W, z_hat)
# estimate_mixture_cov("VEE", W, z_hat)
위 일련의 파리미터 추정을 하나의 함수 GMM_Mstep
으로 아래와 같이 구성해보자.
<- function(df, z, modelName = "VII") {
GMM_Mstep <- map(z, mean)
tau <- map(z, ~colSums(. * df) / sum(.))
mu
<- ncol(df)
p <- map(mu, ~pmap_dfc(
W list(x = df, mu = .), function(x, mu) x - mu)) %>%
map(as.matrix, nrow = p, ncol = p) %>%
map2(z, ~ t(.x) %*% (.y * .x))
<- estimate_mixture_cov(modelName, W, z)
var_cov
<- list(
res tau = tau,
mu = mu,
Sigma = var_cov$Sigma
)
return (res)
}
<- GMM_Mstep(df[, -1], z_hat)
Mstep_res
Mstep_res
## $tau
## $tau$C1
## [1] 0.4746762
##
## $tau$C2
## [1] 0.5253238
##
##
## $mu
## $mu$C1
## x1 x2
## 8.644042 7.559792
##
## $mu$C2
## x1 x2
## 8.777757 7.853884
##
##
## $Sigma
## $Sigma$C1
## [,1] [,2]
## [1,] 16.64239 0.00000
## [2,] 0.00000 16.64239
##
## $Sigma$C2
## [,1] [,2]
## [1,] 17.68685 0.00000
## [2,] 0.00000 17.68685
[단계 2] (E-step) M-step에서의 파라미터 추정치를 바탕으로 \(\hat{z}_{ik}\)를 산출한다.
\[\begin{equation*} \hat{z}_{ik} = \frac{\hat{\tau}_k f_k(\mathbf{x}_i \, | \, \hat{\boldsymbol{\mu}}_k, \hat{\boldsymbol{\Sigma}}_k)}{\sum_{l = 1}^{K} \hat{\tau}_l f_l(\mathbf{x}_i \, | \, \hat{\boldsymbol{\mu}}_l, \hat{\boldsymbol{\Sigma}}_l)} \end{equation*}\]
<- function(df, tau, mu, Sigma) {
GMM_Estep pmap_dfc(list(tau = tau, mu = mu, Sigma = Sigma),
function(df, tau, mu, Sigma)
* mvtnorm::dmvnorm(df, mean = mu, sigma = Sigma),
tau df = df) %>%
`/`(rowSums(.))
}
<- GMM_Estep(df[, -1], Mstep_res$tau, Mstep_res$mu, Mstep_res$Sigma)
new_z_hat
new_z_hat
## C1 C2
## 1 0.4626878 0.5373122
## 2 0.4568716 0.5431284
## 3 0.4373551 0.5626449
## 4 0.4964082 0.5035918
## 5 0.4934276 0.5065724
## 6 0.4886741 0.5113259
## 7 0.4870085 0.5129915
[단계 3] 수렴조건을 만족하면 stop, 그렇지 않으면 [단계 1]을 반복한다. 일반적으로는 우도함수값의 변화량을 수렴조건으로 사용하나, 본 장에서는 간단하게 \(z_{ik}\)값의 변화량을 기준으로 수렴을 판단하도록 하자.
max(abs(z_hat - new_z_hat)) < 1e-9
## [1] FALSE
위 수식의 값이 TRUE
이면 수렴, FALSE
이면 [단계 1]을 반복한다.
13.5.3 R 스크립트 구현
위 일련의 과정들을 포함하는 하나의 함수 GMM_EM
을 아래와 같이 구현해보자. 앞에서 정의했던 함수 GMM_Mstep
및 GMM_Estep
을 재사용한다.
<- function(df, K, modelName = "VII", tol = 1e-9) {
GMM_EM <- min(K, nrow(df))
K
<- 0L
i
# [단계 0] z_ik 추정값 초기화
<- matrix(runif(nrow(df) * K), ncol = K) %>%
z_hat `/`(apply(., 1, sum)) %>%
as_tibble() %>%
`names<-`(str_c("C", 1:K))
while (TRUE) {
<- i + 1L
i
# [단계 1] M-step
<- GMM_Mstep(df, z_hat, modelName)
Mstep_res
# [단계 2] E-step
<- GMM_Estep(df, Mstep_res$tau, Mstep_res$mu, Mstep_res$Sigma)
new_z_hat
# [단계 3] 수렴조건 확인
if (max(abs(z_hat - new_z_hat)) < tol) break
<- new_z_hat
z_hat
}
return (list(z = z_hat,
parameters = Mstep_res,
n_iteration = i))
}
위 함수를 학습데이터 Table 13.4에 적용한 결과는 아래와 같다.
<- GMM_EM(df[, -1], 2, "VII")
VII_res
# 군집 멤버쉽
$z VII_res
## C1 C2
## 1 1.000000e+00 6.786307e-28
## 2 1.000000e+00 8.771316e-27
## 3 1.000000e+00 8.895007e-36
## 4 5.251505e-17 1.000000e+00
## 5 7.046079e-22 1.000000e+00
## 6 1.843399e-26 1.000000e+00
## 7 1.544788e-17 1.000000e+00
# 혼합분포
$parameters VII_res
## $tau
## $tau$C1
## [1] 0.4285714
##
## $tau$C2
## [1] 0.5714286
##
##
## $mu
## $mu$C1
## x1 x2
## 5.333333 13.333333
##
## $mu$C2
## x1 x2
## 11.25 3.50
##
##
## $Sigma
## $Sigma$C1
## [,1] [,2]
## [1,] 1.222222 0.000000
## [2,] 0.000000 1.222222
##
## $Sigma$C2
## [,1] [,2]
## [1,] 0.96875 0.00000
## [2,] 0.00000 0.96875
13.5.4 R 패키지 내 모형기반 군집분석
R 패키지 mclust
를 통해, 위에서 살펴본 VII, VEI, VEE 외에 보다 다양한 분산-공분산 모형을 가정한 군집분석을 수행할 수 있다 (Scrucca et al. 2016). 다음은 “EEI” 구조에 대한 수행 예이다.
<- mclust::meEEI(df[, -1], z_hat) res_mclust_EEI
위 수행 결과 객체는 리스트 형태이며, 그 원소 중 z
는 \(\hat{z}_{ik}\)값을, parameters
는 혼합분포 파라미터 추정값 (\(\hat{\tau}_k\), \(\hat{\boldsymbol\mu}_k\), \(\hat{\boldsymbol\Sigma}_k\))을 나타낸다.
$z res_mclust_EEI
## [,1] [,2]
## [1,] 6.198742e-26 1.000000e+00
## [2,] 2.193775e-22 1.000000e+00
## [3,] 1.432979e-28 1.000000e+00
## [4,] 1.000000e+00 3.497777e-20
## [5,] 1.000000e+00 1.350932e-26
## [6,] 1.000000e+00 5.217649e-33
## [7,] 1.000000e+00 9.883335e-24
$parameters res_mclust_EEI
## $pro
## [1] 0.5714286 0.4285714
##
## $mean
## [,1] [,2]
## x1 11.25 5.333333
## x2 3.50 13.333333
##
## $variance
## $variance$modelName
## [1] "EEI"
##
## $variance$d
## [1] 2
##
## $variance$G
## [1] 2
##
## $variance$sigma
## , , 1
##
## x1 x2
## x1 0.7738095 0.000000
## x2 0.0000000 1.380952
##
## , , 2
##
## x1 x2
## x1 0.7738095 0.000000
## x2 0.0000000 1.380952
##
##
## $variance$Sigma
## x1 x2
## x1 0.7738095 0.000000
## x2 0.0000000 1.380952
##
## $variance$scale
## [1] 1.033728
##
## $variance$shape
## [1] 0.7485618 1.3358950
##
##
## $Vinv
## NULL
mclust
패키지 내의 함수 densityMclust
는 Bayesian information criterion (BIC)을 기준으로 최적의 혼합분포를 찾는 함수이다. 즉, 내부적으로 여러가지 분산-공분산 모형과 군집 수의 조합에 대한 군집분석을 수행한 뒤, 각 조합의 최종결과에서 얻어진 BIC 값을 기준으로 최적의 조합을 선정한다.
<- mclust::densityMclust(df[, -1], verbose = FALSE) res_mclust_opt
함수 수행결과 객체의 BIC
원소가 나타내는 결과는 아래와 같다.
$BIC res_mclust_opt
## Bayesian Information Criterion (BIC):
## EII VII EEI VEI EVI
## 1 -85.40006 -85.40006 -85.70851 -85.70851 -85.70851
## 2 -62.00992 -63.86240 -63.37677 -65.22485 -65.32204
## 3 -65.47796 NA -66.01911 NA NA
## 4 -69.04346 NA -70.17240 NA NA
## 5 -72.02226 NA -73.96817 NA NA
## 6 -62.27998 NA -64.22589 NA NA
## 7 NA NA NA NA NA
## VVI EEE VEE EVE VVE
## 1 -85.70851 -75.27433 -75.27433 -75.27433 -75.27433
## 2 -67.17013 -64.66516 -66.60332 -65.06811 -66.92481
## 3 NA -67.87781 NA NA NA
## 4 NA -67.31282 NA NA NA
## 5 NA NA NA NA NA
## 6 NA NA NA NA NA
## 7 NA NA NA NA NA
## EEV VEV EVV VVV
## 1 -75.27433 -75.27433 -75.27433 -75.27433
## 2 -65.42506 -67.34154 -66.55375 -68.44666
## 3 -71.48895 NA NA NA
## 4 NA NA NA NA
## 5 NA NA NA NA
## 6 NA NA NA NA
## 7 NA NA NA NA
##
## Top 3 models based on the BIC criterion:
## EII,2 EII,6 EEI,2
## -62.00992 -62.27998 -63.37677
수행 결과 가장 큰 BIC 값을 지닌 최적의 조합은 EII 분산-공분산 모형으로 2개의 군집을 가정했을 때 얻어진다.
densityMclust
함수 수행 결과 얻어진 혼합분포를 plotting해보자.
plot(res_mclust_opt, what = "density",
data = df[, -1], points.cex = 0.5)