by Yngie

배깅(Bagging)

|

해당 게시물은 고려대학교 강필성 교수님의 강의를 바탕으로 작성한 것입니다.

Bagging

이번 게시물에서는 앙상블의 방법 중 하나인 배깅(Bagging, Bootstrap aggregating)에 대해서 알아보도록 하겠습니다. 앙상블의 목적은 여러 모델을 결합시켜 다양성을 확보하기 위함입니다. 다양성을 확보하는 방법에는 ‘데이터셋을 다양화하기’와 ‘모델을 다양화하기’의 2가지 방법이 있습니다. 이 중 배깅은 전자에 해당하는 ‘데이터셋을 다양화하기’에 속하는 방법입니다.

Bootstrap

배깅의 풀네임은 Bootstrap aggregating 입니다. 말 그대로 부트스트랩(Bootstrap)을 통해서 다양한 데이터셋을 만들고 이를 학습시킨 모델을 모으는(Aggregating) 방법이지요. 먼저 부트스트랩에 대해서 알아보도록 하겠습니다.

Sampling with replacement

부트스트랩은 모집단으로부터 표본을 추출하는 방식입니다. 중요한 특징은 복원 추출(Sampling with replacement)을 한다는 점인데요. 아래 그림을 보며 설명을 이어나가도록 하겠습니다.

bootstrap_sampling

이미지 출처 : stats.stackexchange.com

원래 데이터셋으로부터 3회 부트스트래핑한 결과를 나타낸 이미지입니다. 표본 집단의 크기는 모집단의 크기와 같습니다. 위처럼 모집단의 크기가 5라면 표본 집단의 크기가 5가 되지요. 각 인스턴스는 균일한(uniformly) 확률로 선택됩니다. 그러다 보니 어떤 인스턴스는 중복되어 선택될 수도 있고, 어떤 인스턴스는 선택되지 못할 수도 있습니다. 가장 오른쪽 부트스트랩 결과를 보면 파랑색, 보라색으로 표시된 인스턴스는 중복되어 선택되었습니다. 노랑과 초록으로 표시된 인스턴스는 선택되지 못했네요.

OOB

각 부트스트랩에서 선택되지 못한 인스턴스를 OOB(Out of Bag)라고 합니다. 특정 인스턴스가 OOB가 될 확률은 아래 식을 통해 구할 수 있습니다.

[p = \bigg(1-\frac{1}{N}\bigg)^N]

$N$ 이 커질수록 아래의 식에 의해서 이 값은 약 $37\%$ 정도에 가까워집니다.

[\begin{aligned} \lim_{N \rightarrow 0}\bigg(1-\frac{1}{N}\bigg)^N &= \lim_{N \rightarrow 0}\bigg[\bigg(1-\frac{1}{N}\bigg)^{-N}\bigg]^{-1}
&= e^{-1} \qquad \bigg(\because e = \lim_{n \rightarrow 0}\bigg(1+\frac{1}{n}\bigg)^n \bigg)
&= 0.3679 \end{aligned}]

즉 $N$이 일정 수준 이상으로 커지면 매 표본집단에 모집단의 약 $2/3$는 포함되고, 나머지 $1/3$ 은 OOB가 됩니다. OOB는 각 모델의 학습 데이터로 사용되지 않으므로 따로 빼두었다가 검증 데이터로 사용하게 됩니다.

Effect

부트스트랩은 데이터의 분포를 변형하는 효과가 있습니다. 원래 데이터의 노이즈 $\epsilon$ 가 특정 분포를 따르고 있다면 이를 통해 만드는 모델은 분포에 종속될 수 밖에 없는데요. 부트스트랩을 통해 분포를 다양하게 만들어 주면 특정 분포에 종속된 모델이 만들어지는 것을 방지함으로써 다양성을 확보할 수 있습니다.

게다가 OOB 데이터를 검증에 사용하면 모든 샘플을 학습과 검증에 활용하여 높은 검증력을 확보할 수 있다는 효과도 있습니다.

Learning

지도학습 알고리즘이라면 어떤 것도 배깅에 적용할 수 있습니다. 하지만 배깅의 목적이 데이터셋을 다양화하여 분산(Variance)을 줄이기 위함이기에 일반적으로는 복잡도(Complexity)가 높은 모델을 선택합니다. 복잡도가 높은 모델은 분산에 의한 오차가 높기 때문에 배깅을 적용했을 때 의미있는 성능 향상 확률이 높기 때문입니다. 복잡도가 높은 모델로는 의사 결정 나무(Decision Tree), 인공 신경망(ANN), 서포트 벡터 머신(SVM) 등이 있습니다.

Result Aggregating

학습이 끝났으니 이제 결과를 취합할(Aggregating) 차례입니다. 수많은 취합 방법이 있지만 본 게시물에서는 대표적인 3가지를 알아보도록 하겠습니다.

Majority Voting

첫 번째는 가장 간단한 방법인 다수결(Majority voting) 입니다. $N$개의 모델 중에서 가장 많이 선택된 레이블을 최종 결과로 도출합니다. 레이블이 ${0,1}$ 인 이진 분류일 때 다수결 방식을 수식으로 나타내면 아래와 같습니다.

[\hat{y}\text{Ensemble} = \underset{i}{\mathrm{argmax}} \bigg(\sum^n{i=1} \delta(\hat{y}_j = i), i \in {0,1} \bigg)]

아래는 $N=10$ 일 때의 결과를 예를 들어 나타내었습니다. 가장 왼쪽은 OOB를 사용하여 얻어낸 검증 정확도(Validation accuracy), 3번째는 테스트 인스턴스의 레이블을 1로 판단할 확률입니다. 이 값이 임계값(threshold)인 $0.5$ 를 넘길 경우 1로 레이블링합니다.

bagging_result

이미지 출처 : github.com/pilsung-kang/Business-Analytics-IME654

다수결을 적용하여 결과를 취합하면 1로 레이블링한 모델은 1,2,3,6,9,10 으로 6개이며 0으로 레이블링한 모델은 4,5,8,9 로 4개가 되어 최종 결과는 더 많은 $(6 > 4)$ 1로 결정됩니다.

Weighted Voting (at Validation Accuracy)

두 번째는 검증 정확도에 가중치를 두어 투표(Weighted voting)하는 방법입니다. 기본적인 아이디어는 ‘검증 정확도가 높은 모델이 테스트 데이터에 대해서도 잘 판단할 것’에서 시작되었습니다. 가중치 투표 방식을 수식으로 나타내면 아래와 같습니다.

[\hat{y}\text{Ensemble} = \underset{i}{\mathrm{argmax}} \bigg(\frac{\sum^n{i=1} (\text{Val_Acc}j) \cdot \delta(\hat{y}_j = i)}{\sum^n{i=1} (\text{Val_Acc}_j)}, i \in {0,1} \bigg)]

위에서 사용했던 예시에 가중치 투표를 적용해보겠습니다. $j$ 번째 모델의 검증 정확도에 가중치를 부여합니다.

weighted_voting

이미지 출처 : github.com/pilsung-kang/Business-Analytics-IME654

검증 정확도에 가중치를 두어 값을 구하면 다음과 같습니다.

[\begin{aligned} \frac{\sum^n_{i=1} (\text{Val_Acc}j) \cdot \delta(\hat{y}_j = 1)}{\sum^n{i=1} (\text{Val_Acc}j)} = \frac{4.69}{8.14} &=0.576
\frac{\sum^n
{i=1} (\text{Val_Acc}j) \cdot \delta(\hat{y}_j = 0)}{\sum^n{i=1} (\text{Val_Acc}_j)} = \frac{3.45}{8.14} &=0.424 \end{aligned}]

위 계산의 결과에서 레이블 1일 때의 값이 더 크기 때문에 $(0.576 > 0.424)$ 최종 결과를 1로 레이블링합니다. 위 예시에서는 다수결과 가중치 투표 방식의 결과가 동일합니다. 하지만 만약 1로 레이블링한 6개 모델의 검증 정확도가 $(0.51, 0.63, 0.54, 0.55, 0.62, 0.55)$이고, 0으로 레이블링한 4개 모델의 검증 정확도가 $(0.91, 0.89, 0.93, 0.94)$ 라면 결과가 달라집니다. 다수결로는 6개 모델이 선택한 1을 최종 레이블로 결정하게 되지만, 가중치 투표 결과는 아래와 같이 $(0.481 < 0.519)$ 가 되어 0을 최종 레이블로 결정합니다.

[\begin{aligned} \frac{\sum^n_{i=1} (\text{Val_Acc}j) \cdot \delta(\hat{y}_j = 1)}{\sum^n{i=1} (\text{Val_Acc}j)} = \frac{3.40}{7.07} &=0.481
\frac{\sum^n
{i=1} (\text{Val_Acc}j) \cdot \delta(\hat{y}_j = 0)}{\sum^n{i=1} (\text{Val_Acc}_j)} = \frac{3.67}{7.07} &=0.519 \end{aligned}]

Weight voting (at Predicted probability)

세 번째는 테스트의 인스턴스의 레이블을 판단하는 확률에 가중치를 두는 방식입니다. 각 모델의 검정력은 동일하다고 보되 ‘더 강하게 주장하는 모델의 결과에 가중치를 주자’라는 아이디어에서 시작되었습니다. 이 방법을 수식으로 나타내면 다음과 같습니다.

[\hat{y}\text{Ensemble} = \underset{i}{\mathrm{argmax}} \bigg(\frac{1}{n}\sum^n{i=1} P(y_j = i), i \in {0,1} \bigg)]

위에서 사용했던 예시에 가중치 투표를 적용해보겠습니다. $j$ 번째 모델의 확률에 가중치를 부여합니다.

weighted_voting2

이미지 출처 : github.com/pilsung-kang/Business-Analytics-IME654

가중치 투표를 사용하여 값을 구하면 다음과 같습니다. 레이블을 0으로 판단할 확률은 1에서 각 확률을 빼준 값입니다.

[\begin{aligned} \frac{1}{n}\sum^n_{i=1} P(y_j = 1) = \frac{1}{10} (6.25) &= 0.625
\frac{1}{n}\sum^n_{i=1} P(y_j = 0) = \frac{1}{10} (3.75) &= 0.375 \end{aligned}]

이 값이 레이블이 1일 때가 더 크므로 $(0.625 > 0.375)$ 최종 레이블을 1로 판단합니다.

Selection?

결과 취합 방법에는 위에서 소개한 3가지 외에도 수많은 방법이 존재합니다. 한 가지 예를 들면, 검증 데이터와 예측 레이블 확률 모두에 가중치를 두어 곱할 수도 있지요. 그렇다면 어떤 방법을 선택하는 것이 좋을까요? ‘공짜 점심은 없다’ 이론은 여기에도 적용됩니다. 절대적으로 좋은 결과 취합 방법이 없다는 말이지요. 경우에 따라 적절한 취합 방법을 선택해야 합니다.

캐글과 같은 경진대회(Competition)에서는 더 높은 성능을 위하여 스태킹(Stacking)이라는 방법을 선택하기도 합니다. 스태킹이란 결과 취합을 위한 분류기(Meta-classifier)를 추가하는 방식입니다. 데이터셋 $\mathbf{x}$ 를 부트스트랩 하여 만들어낸 데이터셋으로 학습한 결과값이 $f_1(\mathbf{x}),f_2(\mathbf{x}), \cdots, f_B(\mathbf{x})$ 라고 하겠습니다. 추가적인 분류기는 이 결과값을 받아 최종 레이블 $\hat{y}$ 를 내는 함수 $g(f_1(\mathbf{x}),f_2(\mathbf{x}), \cdots, f_B(\mathbf{x}))$ 이며, 이 값이 정답 레이블 $y$ 와 같아지도록 학습합니다.

stacking

이미지 출처 : www.researchgate.net

Conclusion

부트스트랩을 통해 다양한 데이터셋을 만들어 다양성을 확보하는 앙상블 방법인 배깅에 대해서 알아보았습니다. 다음 게시물에서는 의사 결정 나무를 기본 모델로 하여 배깅과 랜덤 변수 선택법을 적용한 랜덤 포레스트(Random forest)에 대해서 알아보도록 하겠습니다.

Comment  Read more

앙상블(Ensemble)

|

해당 게시물은 고려대학교 강필성 교수님의 강의를 바탕으로 작성한 것입니다.

Ensemble

이번 게시물에서는 앙상블(Ensemble)에 대해서 알아보도록 하겠습니다. 앙상블이란 무엇일까요? 위키피디아가 말하는 앙상블의 정의는 다음과 같습니다.

앙상블(프랑스어: ensemble)은 전체적인 어울림이나 통일. ‘조화’로 순화한다는 의미의 프랑스어이며 음악에서 2인 이상이 하는 노래나 연주를 말하며 흔히 뮤지컬에서 주, 조연 배우들 뒤에서 화음을 넣으며 춤을 추고 노래를 부르면서 분위기를 돋구는 역할을 말한다.

‘조화로 순화한다’ 라는 문구가 눈에 띄는데요. 머신러닝에서의 앙상블 역시 이런 역할을 합니다. 더 좋은 성능을 내기 위해서 2개 이상의 모델을 잘 조합하지요. 모델 하나 공부하기도 어려운데 2개 이상의 모델을 서로 엮다니… 이런 기이한 짓(?)을 왜 하는지에 대해서 알아보도록 하겠습니다.

No Free Lunch Theorem

“공짜 점심은 없다!(There ain’t no such thing as a free lunch!)”라는 말은 경제학자인 밀턴 프리드먼이 인용하여 유명해졌는데요. 경제학에서는 기회비용을 무시하지 말라는 뜻으로 사용되었습니다. 머신러닝에서도 해당 문장의 일부를 인용하여 『No Free Lunch Theorems for Optimization』1 이라는 논문을 발표하였습니다. 논문의 요지는 머신러닝에도 “모든 문제에 대하여 다른 모델보다 항상 우월한 모델은 없다는 공짜점심없음 이론(NFL Theorem)이 있다”입니다. 다시 말해 ‘좋은 모델’의 기준은 주어지는 문제에 따라 달라진다는 것이지요.

no_free_lunch

이미지 출처 : www.kdnuggets.com

평균 성능이 동일한 두 알고리즘(A, B)이라도 어떤 데이터셋이 주어지느냐에 따라서 성능이 많이 달라집니다. 앞의 세 가지 데이터셋에 대해서는 A의 성능이 월등히 높지만 나머지에 대해서는 B의 성능이 더 좋습니다. 어떤 알고리즘이 “일반적으로 더 우월하다”고 말할 수 없지요. 단지 주어진 데이터셋의 패턴을 잘 인식한 알고리즘이 성능이 잘 나올 뿐입니다.

Power of Ensemble

공짜 점심은 없으니 단일 알고리즘 대신 “여러 모델을 적절히 조합하면 데이터셋의 다양한 패턴을 적절히 인식할 수 있지 않을까?”라는 아이디어에서 앙상블이 시작되었습니다. 단순한 아이디어였지만 앙상블의 힘은 대단했습니다. (효과는 굉장했다!!)

표 형태의(Tabular) 데이터뿐만 아니라 이미지나 자연어 데이터에서도 앙상블 모델이 상위권을 차지하고 있지요. 이미지 데이터셋에 대해 객체 인식 등의 태스크 성능을 경쟁하는 ILSVRC라는 대회가 있는데요. 16~17년도 리더보드 상위권에 수많은 앙상블 모델이 자리잡고 있습니다. 질의 응답의 성능을 비교하는 대표적인 데이터셋 SQuAD의 상위권은 전부 앙상블 모델입니다.

image

이미지 출처 : rajpurkar.github.io/SQuAD-explorer

2016년, 대표적인 머신러닝 컨퍼런스인 MLconf에서는 10가지 요점 중 하나로 “앙상블은 거의 항상 (싱글 모델보다) 잘 작동한다.(Ensembles almost always work better)”를 꼽기도 했습니다. 그렇다면 앙상블이 이렇게 좋은 성능을 보이는 이유는 무엇일까요?

Why Ensemble?

앙상블이 (하나의 단일 모델보다) 잘 작동하는 이유에 대해서 알기 위해서는 ‘편향-분산 (오차)분해 (Bias-Variance Decomposition)’라는 개념을 알고 있어야 합니다. 해당 내용은 이곳에서 다룬 적이 있는데요. 해당 식을 다시 가지고 와보겠습니다.

Bias-Variance Decomposition

먼저 앞으로 나올 여러 기호에 대한 설명을 하고 넘어가겠습니다. $F^*(\mathbf{x})$ 는 정답 모델에 해당하는 함수입니다. 우리가 알고자 하는 함수라고도 할 수 있지요. $\epsilon$ 은 현실적으로 발생하는 소음(Noise)이며 이 값은 동일하고 독립인 분포(i.i.d) $N(0, \sigma^2)$ 를 따른다고 가정하겠습니다. 특정 데이터셋으로부터 추정한 모델의 함수를 $\hat{F_D}(\mathbf{x})$ 라 하고 모든 데이터셋에 대해 이를 평균낸 값을 $\bar{F}(\mathbf{x})$ 라고 하겠습니다. 위 함수들의 관계를 아래와 같이 나타낼 수 있습니다.

[y = F^*(\mathbf{x})+\epsilon, \quad \epsilon \sim N(0,\sigma^2)
\bar{F}(\mathbf{x}) = E[\hat{F_D}(\mathbf{x})]]

특정 데이터 $\mathbf{x}_0$ 에 대한 오차를 다음과 같이 구할 수 있습니다.

[\begin{aligned} \text{Error}(\mathbf{x}_0) &= E\big[(y - \hat{F}(\mathbf{x}) \vert \mathbf{x} = \mathbf{x}_0)^2 \big]
&= E\big[(F^(\mathbf{x})+\epsilon - \hat{F}(\mathbf{x}))^2 \big] \qquad (\because y = F^(\mathbf{x})+\epsilon)
&= E\big[(F^(\mathbf{x}) - \hat{F}(\mathbf{x}))^2 \big] + \sigma^2
&= E\big[(F^
(\mathbf{x}) - \bar{F}(\mathbf{x}) + \bar{F}(\mathbf{x}) - \hat{F}(\mathbf{x}))^2 \big] + \sigma^2
&= E\big[(F^(\mathbf{x}) - \bar{F}(\mathbf{x}))^2 + (\bar{F}(\mathbf{x}) - \hat{F}(\mathbf{x}))^2 + 2(F^(\mathbf{x}) - \bar{F}(\mathbf{x}))(\bar{F}(\mathbf{x}) - \hat{F}(\mathbf{x})) \big] + \sigma^2
&= \color{blue}{E\big[(F^*(\mathbf{x}) - \bar{F}(\mathbf{x}))^2\big]} + \color{red}{E\big[(\bar{F}(\mathbf{x}) - \hat{F}(\mathbf{x}))^2 \big]} + \sigma^2
&= \color{blue}{\text{Bias}^2(\hat{F}(\mathbf{x}_0))} + \color{red}{\text{Var}^2(\hat{F}(\mathbf{x}_0))} + \sigma^2 \end{aligned}]

특정 데이터로부터의 에러를 편향과 분산에 의한 에러로 나눠볼 수 있습니다. 편향은 $E\big[(F^*(\mathbf{x}) - \bar{F}(\mathbf{x}))^2\big]$ 에 해당합니다. 정답과 평균 추정치가 얼마나 차이나는 지를 나타내지요. 편향이 높으면 과소적합(Underfitting)이 발생합니다. 분산은 $E\big[(\bar{F}(\mathbf{x}) - \hat{F}(\mathbf{x}))^2 \big]$ 에 해당합니다. 평균 추정치와 특정 데이터셋에 대한 추정치가 얼마나 차이나는 지를 나타냅니다. 분산이 높으면 과적합(Overfitting)이 발생합니다.

bias-variance decomposition

위 그림은 편향이 높고 낮은 경우와 분산이 높고 낮은 각각의 경우를 도식화하여 보여주고 있습니다. 분산만 높은 경우 어떤 데이터에는 잘 맞지만, 어떤 데이터에는 잘 맞지 않습니다. 편향만 높은 경우 전체적으로 잘 맞지 않지만 데이터별 차이는 그리 크지 않음을 확인할 수 있습니다.

일반적으로 모델의 복잡도(Complexity)가 작으면 분산이 낮고 편향이 높습니다.(3번째 그림) 이런 모델로는 로지스틱 회귀(Logistic Regression), 선형 판별 분석(FLDA), k가 클 때의 k-NN(k-최근접이웃) 등이 있습니다. 반대로 복잡도가 높으면 분산이 높고 편향이 낮습니다.(2번째 그림) 이런 모델로는 (Pruning을 해주지 않은) 의사결정나무, 인공신경망, 서포트 벡터 머신, k가 작을 때의 k-NN 등이 있습니다.

with Ensemble

단일 모델이 2번 혹은 3번 그림이라면 앙상블은 이 모델을 4번으로 만들기 위해 사용합니다. 앙상블의 목적은 각 단일 모델의 좋은 성능을 유지하면서도 다양성을 확보하는 데에 있습니다. 전체 데이터셋의 부분집합에 해당하는 여러 데이터셋을 준비한 뒤에 각각을 따로 학습시키면 Implicit diversity를 확보할 수 있습니다. 이 방법과는 조금 다르게 먼저 생성된 모델의 측정값으로부터 새로운 모델을 생성하면 Explicit diversity를 확보할 수 있지요.

전자에 해당하는 방법은 분산을 줄일 수 있으며 배깅(Bagging)이나 랜덤 포레스트(Random Forest) 등이 있습니다. 후자에 해당하는 방법은 편향을 줄일 수 있으며 부스팅(Boosting)이나 NCL(Negative correlation learning) 등이 있습니다. 아래는 전자(왼쪽)과 후자(오른쪽) 모델이 생성되는 방식을 나타낸 이미지입니다.

implicit vs explicit

이미지 출처 : github.com/pilsung-kang/Business-Analytics-IME654

왼쪽에 해당하는 모델은 여러 데이터셋을 준비한 뒤에 각각의 모델을 병렬적으로 학습합니다. 오른쪽에 해당하는 모델은 첫 번째 모델이 얻어내는 어떤 정보를 바탕으로 두 번째 모델을 생성합니다. 이렇게 여러 개의 모델을 순차적으로 학습하게 됩니다.

앙상블 방법의 오차는 얼마나 될까요? $M$ 개의 모델을 엮은 앙상블 모델이 있다고 해보겠습니다. $m$ 번째 모델에 대해 정답과 오차의 관계를 다음과 같이 나타낼 수 있습니다.

[\begin{aligned} &y_m(\mathbf{x}) = f(\mathbf{x}) + \epsilon_m(\mathbf{x}).
&\mathbb{E}\mathbf{x}[{y_m(\mathbf{x}) - f(\mathbf{x})}^2] = \mathbb{E}\mathbf{x}[\epsilon_m(\mathbf{x})^2] \end{aligned}]

이를 활용하여 각 모델의 오차 평균과 앙상블 모델의 오차를 구하면 다음과 같습니다.

[\begin{aligned} E_\text{Avg} &= \frac{1}{M} \sum_{i=1}^M \mathbb{E}\mathbf{x}[\epsilon_m(\mathbf{x})^2]
E
\text{Ensemble} &= \mathbb{E}\mathbf{x}\bigg[\big{\frac{1}{M}\sum{i=1}^My_m(\mathbf{x}) - f(x)\big}^2\bigg]
&= \mathbb{E}\mathbf{x}\bigg[\big{\frac{1}{M}\sum{i=1}^M\epsilon_m(\mathbf{x})\big}^2\bigg] \end{aligned}]

만약 모델의 $\epsilon$ 평균이 0이라고 가정하면 방정식 $\mathbb{E}_\mathbf{x}[\epsilon_m(\mathbf{x})] = 0$ 을 만족합니다. 그리고 앙상블에 사용된 각각의 모델이 연관되어 있지 않는 가정하에서는 아래의 방정식을 만족합니다.

[\mathbb{E}_\mathbf{x}[\epsilon_m(\mathbf{x})\epsilon_l(\mathbf{x})] = 0 \quad (m \neq l)]

이 두 가지 가정하에서, 앙상블 모델은 각 모델의 평균의 $1/M$ 의 오차를 갖게 됩니다.

[E_\text{Ensemble} = \frac{1}{M} E_\text{Avg}]

하지만 위 가정은 현실에서는 거의 불가능합니다. 앙상블 오차의 상한은 코시-슈바르츠 부등식2을 사용하여 구할 수 있습니다. 코시-슈바르츠 부등식은 다음과 같습니다.

[(a^2+b^2)(x^2+y^2) \geq (ax + by)^2]

이를 일반화하여 각 모델의 오차에 적용하면 다음과 같은 식을 만들 수 있습니다.

[\begin{aligned} M\sum_{i=1}^M \mathbb{E}\mathbf{x}[\epsilon_m(\mathbf{x})^2] &\geq \bigg[\sum{i=1}^M \mathbb{E}\mathbf{x}\epsilon_m(\mathbf{x}) \bigg]^2
\frac{1}{M}\sum
{i=1}^M \mathbb{E}\mathbf{x}[\epsilon_m(\mathbf{x})^2] &\geq \bigg[\frac{1}{M}\sum{i=1}^M \mathbb{E}\mathbf{x}\epsilon_m(\mathbf{x}) \bigg]^2
E
\text{Avg} &\geq E_\text{Ensemble} \end{aligned}]

앙상블의 오차는 무조건 앙상블을 구성하는 모델 오차의 평균보다는 작음을 알 수 있습니다. 또한 경험적으로는 대개 최고 성능을 보이는 개별 모델보다도 낮은 오차를 보인다고 합니다. 이런 이유에서 앙상블은 모델의 성능을 높이기 위해서 자주 사용되며, 실제로도 최고의 성능을 기록하고 있습니다. 이후 게시물에서는 다양한 앙상블 방법에 대해서 알아보도록 하겠습니다.

  1. David H. Wolpert and William G. Macready, No Free Lunch Theorems for Optimization, IEEE Transactions on Evolutionary Computation (Volume: 1, Issue: 1, Apr 1997) 

  2. 위키피디아 : 코시-슈바르츠 부등식 

Comment  Read more

소프트 마진(Soft Margin) SVM

|

해당 게시물은 고려대학교 강필성 교수님의 강의를 바탕으로 작성한 것입니다.

Soft Margin SVM

이번에는 소프트마진 서포트 벡터 머신에 대해서 알아보도록 하겠습니다. 하드 마진 분류기가 보여준 것처럼 모든 데이터를 깨끗하게 두 쪽낼 수 있다면 좋겠지만 실제의 데이터는 그렇게 호락호락하지 않습니다. 실제로 맞닥뜨리는 데이터에는 항상 소음(Noise)이 있기 때문이지요.

보통의 데이터는 아래와 같은 모습입니다. 자신이 속하는 클래스의 마진 평면 밖으로 넘어간 인스턴스(1)가 있거나 심지어는 상대방 클래스의 마진 평면 너머로 넘어간 인스턴스(2)도 있지요. 이렇게 마진 평면을 넘어가는 인스턴스를 허용하는 서포트 벡터 머신을 소프트 마진 서포트 벡터 머신(Soft-margin SVM)이라고 합니다.

soft_margin_svm

이미지 출처 : towardsdatascience.com

소프트마진 방법을 사용할 때에는 마진 평면을 넘어가는 인스턴스에 대해서 페널티(Panelty, $\xi$)를 부여합니다. 페널티는 자신이 속한 클래스의 마진 평면에서 떨어진 거리만큼 부여됩니다.

C-SVM

C-SVM의 목적 함수는 다음과 같습니다. \(\min \frac{1}{2}\Vert \mathbf{w} \Vert^2 + C \sum_{i=1}^N \xi_i \\ s.t. \quad y_i(\mathbf{w}^T\mathbf{x}_i+b) \geq 1 - \xi_i, \quad \xi_i>0, \quad \mu_i > 0, \quad \forall i\)

마진 평면을 넘어가는 즉, $\xi > 0$ 인 인스턴스에 대해서 $y_i(\mathbf{w}^T\mathbf{x}i+b) \geq 1 - \xi$ 식을 만족해야 합니다. 이런 제약조건 하에서 마진의 역수에 각 인스턴스의 오차 $\sum^N{i=1} \xi_i$ 를 더해준 값을 최소화하는 $\mathbf{w}, b, \xi_i$ 를 찾는 것이 목적입니다. $C$ 는 얼마나 강한 페널티를 줄 지를 결정하는 하이퍼파라미터입니다.

Optimization

하드 마진 때와 같이 라그랑주 승수법을 사용하여 최적화 문제를 풀 수 있습니다. 제약식까지 포함하여 나타낸 원초(primal) 문제는 다음과 같습니다. \(\min L_p(\mathbf{w}, b, \alpha_i) = \frac{1}{2}\Vert \mathbf{w} \Vert^2 + C \sum_{i=1}^N \xi_i - \sum_{i=1}^N \alpha_i(y_i(\mathbf{w}^T\mathbf{x}_i+b)-1+\xi_i) - \sum_{i=1}^N \mu_i\xi_i \\ s.t.\quad \alpha_i \geq 0\)

이제 KKT 조건을 적용하여 최저일 때의 조건을 구해보겠습니다.

[\begin{aligned} \frac{\partial L_p}{\partial \mathbf{w}} = 0 \quad &\Rightarrow \quad \mathbf{w} = \sum^N_{i=1} \alpha_iy_i\mathbf{x}i
\frac{\partial L_p}{\partial b} = 0 \quad &\Rightarrow \quad \sum^N
{i=1}\alpha_iy_i = 0
\frac{\partial L_p}{\partial \xi_i} = 0 \quad &\Rightarrow \quad C - \alpha_i-\mu_i = 0 \end{aligned}]

조건을 활용하여 원초 문제를 쌍대 문제로 변형할 수 있습니다. 원초 문제에 KKT 조건에서 도출된 세 식을 각각 대입해주면 됩니다.

[\begin{aligned} \max L_D(\alpha_i) &= \frac{1}{2}\sum_{i=1}^N\sum_{j=1}^N \alpha_i\alpha_jy_iy_j\mathbf{x}i^T\mathbf{x}_j + \sum{i=1}^N (\alpha_i+\mu_i)\xi_i \ &\quad - \sum_{i=1}^N \sum_{j=1}^N \alpha_i(y_i(\alpha_jy_j\mathbf{x}i^T\mathbf{x}_j+b)-1+\xi_i) - \sum{i=1}^N \mu_i\xi_i
&= \frac{1}{2}\sum_{i=1}^N\sum_{j=1}^N \color{blue}{\alpha_i\alpha_jy_iy_j\mathbf{x}i^T\mathbf{x}_j} + \sum{i=1}^N (\color{magenta}{\alpha_i\xi_i}+\color{olive}{\mu_i\xi_i}) \ &\quad - \sum_{i=1}^N \sum_{j=1}^N (\color{blue}{\alpha_i\alpha_jy_iy_j\mathbf{x}i^T\mathbf{x}_j}+b \cdot \color{red}{\alpha_iy_i}-\alpha_i+\color{magenta}{\alpha_i\xi_i}) - \color{olive}{\sum{i=1}^N \mu_i\xi_i}
&= \sum_{i=1}^N\alpha_i-\frac{1}{2}\sum_{i=1}^N\sum_{j=1}^N \alpha_i\alpha_jy_iy_j\mathbf{x}i^T\mathbf{x}_j \end{aligned}
s.t. \quad \sum
{i=1}^N \alpha_i y_i = 0, \quad 0 \leq \alpha_i \leq C]

Regularization cost, C

정칙화(Regularization)의 정도를 결정하는 하이퍼파라미터 $C$ 에 대해 알아보도록 하겠습니다. $C$ 가 크면 클수록 목적 함수에서 오차 $\xi_i$ 의 영향력이 커지게 됩니다. 즉, $C$ 가 클 경우 마진 평면을 벗어나는 인스턴스가 조금만 생겨도 철저히 배제하려 하기 때문에 마진의 크기가 줄어들게 됩니다. 반대로 $C$ 가 작을 경우에는 마진 평면을 벗어나는 인스턴스에 대해 그다지 큰 영향을 받지 않으므로 상대적으로 큰 마진을 갖는 분류기가 만들어집니다.

아래는 각기 다른 $C$ 에 대해서 마진이 어떻게 달라지는 지를 나타낸 이미지입니다.

regularization_parameter

이미지 출처 : dinhanhthi.com

$C$ 가 커질수록 마진의 크기가 줄어드는 것을 확인할 수 있습니다.

Comment  Read more

서포트 벡터 머신(Linear & Hard Margin SVM)

|

해당 게시물은 고려대학교 강필성 교수님의 강의를 바탕으로 작성한 것입니다.

Linear & Hard Margin SVM

이번 게시물부터 이후 몇 개의 게시물에 걸쳐 커널 기반의 학습 방법의 대표격인 서포트 벡터 머신(Support Vector Machine, SVM)에 대해서 알아보겠습니다. 서포트 벡터 머신은 기본적으로 이진 분류(Binary classification)을 위한 알고리즘입니다.

머신러닝에서 사용되는 분류기는 결정 경계를 만들어 나가는 로직을 알고리즘마다 가지고 있습니다. 아래 이미지는 각 분류기의 결정 경계 예시를 나타내고 있습니다.

image-20201120150942988

이미지 출처 : github.com/pilsung-kang/Business-Analytics-IME654

가장 왼쪽에 있는 kNN(k-Nearest Neighbor)은 가장 가까이에 있는 k개의 인스턴스 중 어떤 클래스에 속한 인스턴스가 더 많은 지에 따라서 결정 경계를 형성합니다. 다음에 있는 의사 결정 나무(Decision Tree)는 가장 높은 설명력을 확보하는 수직인 결정 경계를 반복해서 생성합니다. 세 번째에 있는 선형 분류기는 선형의 결정 경계를 형성하며 로지스틱 회귀, 선형 판별 분석(Linear Discriminant Analysis) 등이 이런 분류에 속합니다. 맨 오른쪽에 있는 비선형 분류기에는 뉴럴넷과 같은 것들이 속하게 됩니다. 서포트 벡터 머신은 기본적으로 선형(Linear) 모형에 해당하는 알고리즘입니다.

Support Vector Machine

서포트 벡터 머신에서 사용되는 가정과 표기법에 대해서 알아보도록 하겠습니다. 가장 먼저, 모든 인스턴스가 독립이며 동일한 분포(independent identically distributed, i.i.d.)로 샘플링 되었다고 가정합니다.

서포트 벡터 머신은 인스턴스 레이블을 표기하는 데에 있어서 일반적인 이진 분류기와 다른 점이 있습니다. 대부분의 이진 분류기는 ${1, 0}$, 즉 Positive 클래스를 $1$로, Negative 클래스를 $0$으로 표기하는데요. 서포트 벡터 머신에서는 Negative 클래스를 $-1$로 표기합니다. ${1, -1}$로 나누게 되는 것이지요. 이렇게 표기하는 이유는 서포트 벡터 머신의 알고리즘과도 관련이 있는데 이에 대해서는 아래에서 더 자세히 알아보도록 하겠습니다.

Better Classifier?

사실 많은 경우에 선형 분류기의 결정 경계가 하나로만 나오지는 않습니다. 아래 그림을 보겠습니다. (아래 분류기에서는 검은색과 흰색 클래스로 분류하였습니다.)

svm_vs_others

이미지 출처 : ko.wikipedia.org

위 그림에는 3개의 분류기의 결정 경계 $H_1,H_2,H_3$ 가 나타나 있습니다. 세 분류기의 성능을 비교해보자면 일단 $H_3$ 는 한 눈에 보아도 성능이 좋지 못한 분류기임을 알 수 있습니다. 인스턴스를 제대로 분류하고 있지 못함을 알 수 있지요. 그렇다면 $H_1,H_2$ 의 성능 비교는 어떻게 할 수 있을까요? 일단 두 분류기 모두 주어진 학습 데이터를 잘 구분하여 경험적 위험(Empirical risk)은 모두 $0$입니다 .

Margin

결론부터 이야기하자면 $H_2$ 가 $H_1$ 보다 더 좋은 분류기이며, 서포트 벡터 머신은 알아서 이러한 분류 경계를 찾아갑니다. 어떤 분류기가 더 좋은 지를 알 수 있는 척도를 마진(Margin) 이라고 합니다. 마진이란 ‘결정 경계로부터 등간격으로 확장시켰을 때 가장 가까이 만나는 (양쪽 클래스의) 객체와의 거리’입니다. 위 그림에서 $H_1, H_2$ 와 각각 가장 가까운 양 범주의 인스턴스를 잇는 얇은 선의 길이가 바로 마진이지요.

그렇다면 어떻게 마진이 이런 기준이 될 수 있을까요? 이를 알기 위해서는 지난 게시물에서 언급했던 VC dimension이라는, 모델의 복잡도와 관련있는 개념을 다시 가져와야 합니다. VC dimension $h$ 은 다음과 같은 식을 사용하여 구할 수 있습니다.

[h \leq \min(\bigg[\frac{R^2}{\Delta^2}\bigg], D) + 1]

SER

이미지 출처 : www.researchgate.net

위 식에서 $R$ 은 공간 상에서 모든 데이터를 포함하는 구의 반지름이며, $\Delta$ 는 마진입니다. 그리고 $D$ 는 데이터의 차원입니다. $R, D$ 는 주어진 데이터로부터 결정됩니다. 결정 경계를 어떻게 정하더라도 달라지지 않는 상수이지요. 결정 경계를 어떻게 하느냐에 따라 달라지는 것은 마진 $\Delta$ 뿐입니다.

위 식에 따르면 $\Delta$가 작아서 $\frac{R^2}{\Delta^2}$ 가 $D$ 보다 클 때에는 분류기의 VC dimension이 $D+1$ 이 됩니다. $\Delta$가 커서 $\frac{R^2}{\Delta^2}$ 이 $D$ 보다 작아지게 되면 분류기의 VC dimension은 $\big[\frac{R^2}{\Delta^2}\big]+1$ 입니다. 마진이 클수록 VC dimension이 작아지는 것이지요. VC dimension $h$ 가 작아지면 아래 식에서 파란색에 해당하는 Capacity term이 작아지고 기대 위험(Expected risk) $R[f]$ 역시 작아지게 됩니다.

[R[f] \leq R_\text{emp}[f] + \color{blue}{\sqrt{\frac{h(\ln\frac{2n}{h}+1) - \ln(\frac{\delta}{4})}{n}}}]

다시 말해서 마진이 클수록 VC dimension이 작아집니다. VC dimension이 작아지면 Capacity term이 작아지면서 일반화에서 발생하는 오류가 작아지게 됩니다.

Kernel Machine vs Neural Net

“Margin이 클수록 더 좋은 분류기가 된다”“서포트 벡터 머신은 항상 마진이 최대가 되도록 한다”는 것만 기억한 채 잠깐 샛길로 빠져보겠습니다. 커널 머신이 잘 나가던 시절에 신경망, 즉 딥러닝이 공격받았던 지점이 이 부분입니다. 서포트 벡터 머신은 항상 마진을 최대로 하는 점, 즉 전역 최적점(Global optimum)을 찾습니다. 하지만 신경망은 경험적 위험만을 최적점을 찾아가는 척도로 하기 때문에 지역 최적점(Local optimum)에 빠져버리기 쉽다는 것이지요.

하지만 2014년에 발표된 논문에 의하면 ‘실험적으로 지역 최적점에 갇혀 빠져나가지 못하는 경우가 많지 않음’을 알아냈습니다. 저차원 공간에서는 지역 최적점에 빠졌을 때 경사 하강법 알고리즘으로 빠져 나오기가 어렵습니다. 하지만 실제 데이터는 수 십, 수 백 차원으로 구성되어있습니다. 이런 고차원 공간 상에서는 모든 방향으로 기울기가 0인 점이 거의 없습니다. 그렇기 때문에 학습을 충분히 한다면 경사 하강법으로도 전역 최적점을 찾아갈 수 있게 되지요. 이 논문으로 인해 딥러닝은 오해(?)를 풀고 연구에 박차를 가하게 되었으며 컴퓨터 비전이나 자연어처리 분야에서 각광받을 수 있었습니다.

Quantization

다시 본론으로 돌아와서 서포트 벡터 머신을 더 자세히 알아보도록 하겠습니다.

margin

이미지 출처 : github.com/pilsung-kang/Business-Analytics-IME654

마진을 수식으로 나타내면 다음과 같습니다. 먼저 결정 경계면 위의 점 $x_0$ 에서 법선 벡터 $w$ 를 정의하고, 법선 벡터가 $+$ 마진 평면과 만나는 점을 $x_1$ 이라고 하면 $x_1 = x_0 + pw$ 로 나타낼 수 있습니다. 여기서 $p$ 는 우리가 구하고자 하는 마진의 크기(너비)입니다. 이 값을 $x_1$ 에 대입해주면 $p$ 를 구할 수 있습니다.

[\begin{aligned} w^Tx_1 + b &= 1
w^T(x_0+pw) + b &= 1
w^Tx_0 + pw^Tw + b &= 1
pw^Tw &= 1
(\because w^Tx_0 + b &= 0)
\therefore p = \frac{1}{w^Tw} &= \frac{1}{\Vert w\Vert^2} \end{aligned}]

마진을 $\frac{2}{\Vert w\Vert^2}$ 로 나타내는 책도 있습니다. 마진을 결정 경계와 서포트 벡터 사이의 거리로 볼 지, 아니면 그 2배로 볼 지에 따라 달라집니다. 수치는 다르지만 어느 것을 사용하더라도 최소화하는 방법은 동일하니 일단은 전자 $\frac{1}{\Vert w\Vert^2}$ 로 보도록 하겠습니다.

Hard Margin SVM

서포트 벡터 머신은 결정 경계면이 선형(Linear)인지 아닌지, 예외를 허용하는 지에 따라서 아래와 같이 4가지로 구분할 수 있습니다.

various_svm

이미지 출처 : github.com/pilsung-kang/Business-Analytics-IME654

가장 먼저 결정 경계면이 선형이며 예외를 허용하지 않는 하드 마진(Hard margin) 서포트 벡터 머신에 대해서 알아보겠습니다. 아래는 인스턴스와 결정 경계면, 마진 평면을 나타낸 이미지입니다. 아래 식에서 $x_i$ 는 각 인스턴스를 나타내며 $y_i$ 는 인스턴스의 클래스를 나타냅니다.

margin to support vector

이미지 출처 : github.com/pilsung-kang/Business-Analytics-IME654

위 이미지에서 파란색으로 표현된 인스턴스의 클래스 $y_i = 1$ 이고 모든 인스턴스는 $\mathbf{w}^T\mathbf{x}+b \geq 1$ 을 만족합니다. 빨간색으로 표현된 인스턴스의 클래스 $y_i = -1$ 이고, 인스턴스의 $\mathbf{w}^T\mathbf{x}+b \leq -1$ 을 만족하지요. 그렇기 때문에 모든 인스턴스가 $y_i(\mathbf{w}^T\mathbf{x} + b) \geq 1$ 라는 식을 만족합니다.

목표는 서포트 벡터 사이의 거리 $\frac{2}{\Vert \mathbf{w}^2 \Vert}$ 의 최대화입니다. 목적 함수를 역수인 $\frac{1}{2}\Vert \mathbf{w}\Vert^2$ 로 설정하고 이를 최소화 $\mathbf{w}$ 를 찾는 문제로 변경하겠습니다. 위에서 구한 제약 조건을 반영한 라그랑주 승수법을 사용하여 나타내면 다음은 같습니다.

[\min L_p(\mathbf{w},b,\alpha_i) = \frac{1}{2}\Vert \mathbf{w}\Vert^2 - \sum^N_{i=1}\alpha_i(y_i(\mathbf{w}^T\mathbf{x}_i + b) -1)
s.t. \quad \alpha_i \geq 0]

$L_p$ 는 원초(Primal) 문제이며 여기서 구해진 조건을 사용하여 쌍대(Dual)문제로1 바꿀 수 있습니다. 말로는 어려우니 아래 예시를 살펴보도록 하겠습니다.

Primal-Dual

$f(x) = 0.5x^2-2x +3$ 이고 $x \geq 2$ 라는 제약조건이 있을 때 다음과 같은 원초 문제를 만들 수 있습니다.

[0.5x^2 - 2x + 3 \qquad x \geq 2
L_P : 0.5x^2 - 2x + 3 -\lambda(x-2)]

원초 문제로 주어진 함수를 최소화 하는 조건이 미분했을 때 0이되는 점이므로 이로부터 $x$ 에 관한 식을 만들 수 있습니다.

[\frac{\partial L_p}{\partial x} = x - 2 - \lambda
\frac{\partial L_p}{\partial x} = 0 \rightarrow x = 2+\lambda]

그리고 이렇게 구한 조건을 다시 원초 문제에 대입하면 Wolfe’s 쌍대 문제 $L_D$ 로 변형할 수 있습니다. 대신 원초 문제가 최댓값 찾기라면 쌍대 문제는 최솟값 찾기가 되고, 원초 문제가 최솟값 찾기라면 쌍대 문제는 최댓값 찾기가 됩니다. 아무튼 위 원초 문제를 변환한 쌍대 문제는 다음과 같습니다.

[\begin{aligned} L_D &= 0.5(2+\lambda)^2 - 2(2+\lambda) + 3 - \lambda(\lambda+2-2)
&=-0.5 \lambda^2 +1 \qquad (s.t. \quad \lambda \geq 0) \end{aligned}]

위 쌍대 문제를 최대화하는 $\lambda = 0$ 이므로 $x = 2$ 가 됩니다. 이는 원초 문제를 최소로 하는 $x$ 값과 동일합니다. 이렇게 식을 변형해서 쓰는 이유는 원초 문제보다 쌍대 문제가 쉬울 때가 있기 때문인데요. 다시 원래 서포트 벡터 머신으로 돌아가서 원초 문제를 바꿔보도록 하겠습니다.

KKT Condition

서포트 벡터 머신의 원초 문제는 다음과 같았습니다.

[\min L_p(\mathbf{w},b,\alpha_i) = \frac{1}{2}\Vert \mathbf{w}\Vert^2 - \sum^N_{i=1}\alpha_i(y_i(\mathbf{w}^T\mathbf{x}_i + b) -1)
s.t. \quad \alpha_i \geq 0]

KKT 조건2을 사용하여 해당 식이 최저일 때의 조건을 구해보겠습니다. 원초 문제를 각 변수인 $\mathbf{w}, b$ 로 편미분한 값이 0일 때의 조건을 구합니다.

[\frac{\partial L_p}{\partial \mathbf{w}} = 0 \Rightarrow \mathbf{w} = \sum^N_{i=1} \alpha_iy_i\mathbf{x}i \qquad \frac{\partial L_p}{\partial b} = 0 \Rightarrow \sum^N{i=1} \alpha_iy_i = 0]

$\mathbf{w} = \sum^N_{i=1} \alpha_iy_i\mathbf{x}_i$ 를 다시 대입하여 쌍대 문제로 만들어주면 다음과 같은 $L_D$ 식을 얻을 수 있습니다.

[L_D = \frac{1}{2}\sum^N_{i=1}\sum^N_{j=1} \alpha_i\alpha_jy_iy_j\mathbf{x}i\mathbf{x}_j - \sum^N{i=1}\sum^N_{j=1} \alpha_i\alpha_jy_iy_j\mathbf{x}i\mathbf{x}_j - b\sum^N{i=1}\alpha_iy_i+\sum^N_{i=1}\alpha_i \]

$\sum^N_{i=1} \alpha_iy_i = 0$ 조건을 사용하여 식을 정리하면 다음과 같이 간단하게 식을 변형할 수 있습니다.

[L_D = \sum^N_{i=1}\alpha_i - \frac{1}{2}\sum^N_{i=1}\sum^N_{j=1} \alpha_i\alpha_jy_iy_j\mathbf{x}i\mathbf{x}_j
s.t. \quad \sum^N
{i=1}\alpha_iy_i =0, \quad \alpha_i \geq0]

이렇게 구해진 쌍대문제 $L_D$ 는 $\alpha$ 에 대한 컨벡스 함수(Convex function)이기 때문에 항상 최적해를 찾을 수 있게 됩니다. 항상 최적해를 찾을 수 있기 때문에 일반화로부터 발생하는 오류를 최소화할 수 있지요.

Conclusion

이번 게시물에서는 서포트 벡터 머신의 가장 기본적인 형태인 하드 마진 선형 분류기에 대해 알아보았습니다. 마진이 최대화 되는 과정에서 복잡한 수식이 등장했지만 포인트는 마진을 최대화 하는 결정 경계가 항상 하나로 나온다는 것이지요. 하지만 하드 마진 선형 분류기로 모든 데이터를 분류할 수 있는 것은 아닙니다. 다음 게시물에서는 소프트 마진 선형 분류기나 커널 트릭을 사용한 비선형 분류기에 대해서 알아보도록 하겠습니다.

  1. 쌍대성에 관한 설명은 ratsgo님의 블로그모두를 위한 컨벡스 최적화를 참조하시면 좋습니다. 

  2. KKT 조건에 대한 설명 역시 ratsgo님의 블로그모두를 위한 컨벡스 최적화를 참조하시면 좋습니다. 

Comment  Read more

Numpy (2) - 인덱싱(Indexing)과 슬라이싱(Slicing)

|

이번 포스팅의 내용은 파이썬 라이브러리를 활용한 데이터 분석(2판) 의 내용을 참조하여 작성하였습니다.

Indexing and Slicing

이번 게시물에서는 ndarray 의 요소를 조작하기 위한 인덱싱과 슬라이싱에 대해서 알아보도록 하겠습니다.

Basic

기본적으로 배열의 색인은 파이썬의 리스트(python list)를 다루는 방법과 유사합니다. 슬라이싱의 결과물이 리스트가 아니라 ndarray 라는 점에서만 차이가 있지요.

>>> import numpy as np

>>> arr1 = np.arange(7)
>>> arr1
array([0, 1, 2, 3, 4, 5, 6])

>>> arr1[3]
3

>>> arr1[4:6]
array([4, 5])

Broadcasting

넘파이에는 브로드캐스팅(broadcasting)이라는 기능이 있어서 슬라이싱한 객체에 특정한 값을 대입해주면 모두 해당하는 값으로 변형이 됩니다. 아래 예시를 보겠습니다.

>>> arr1[4:6] = -1
>>> arr1
array([0, 1, 2, 3, -1, -1, 6])

원래 array([4, 5]) 가 있던 자리에 할당된 -1 이 들어가 있음을 확인할 수 있습니다. 이 동작은 파이썬 리스트에서는 작동하지 않습니다.

>>> lst1 = [i for i in range(7)]
>>> lst1[4:6] = -1
TypeError: can only assign an iterable
    
# 만약에 할당하려면 언패킹(Unpacking)을 활용합니다.
>>> lst1[4:6] = -1, -1
>>> lst1
[0, 1, 2, 3, -1, -1, 6]

위와 같이 파이썬 리스트에서는 슬라이싱 객체에 iterable 을 할당하지 않으면 TypeError 가 발생합니다.

View

ndarray 와 파이썬 리스트의 슬라이싱 객체는 브로드캐스팅 외에도 차이점을 가지고 있습니다. 배열 조각이 원본 배열의 뷰(View)라는 점인데요. 뷰는 원본 데이터의 복사본이 아닙니다. 그렇기 때문에 뷰를 배열 조각을 변경할 경우에 원본 데이터의 원소도 변경됩니다. 아래 예시를 보도록 하겠습니다.

>>> arr2 = np.arange(7)
>>> part_of_arr2 = arr2[3:6]
>>> part_of_arr2[1] = -1
>>> print(f"""part_of_arr2 : {part_of_arr2}
arr2 : {arr2}""")

part_of_arr2 : [ 3 -1  5]
arr2 : [ 0  1  2  3 -1  5  6]

arr2의 배열 조각인 part_of_arr2의 원소를 변경해주었을 뿐인데 원본 배열 arr2 의 요소도 함께 바뀌었습니다. 동일한 코드를 파이썬 리스트에 실행하면 어떻게 될까요?

>>> lst2 = [i for i in range(7)]
>>> part_of_lst2 = lst2[3:6]
>>> part_of_lst2[1] = -1
>>> print(f"""part_of_lst2 : {part_of_lst2}
lst2 : {lst2}""")

part_of_lst2 : [3, -1, 5]
lst2 : [0, 1, 2, 3, 4, 5, 6]

리스트 조각 part_of_lst2 의 요소를 변경해주었음에도 원본 리스트 lst2 의 요소는 변함이 없음을 확인할 수 있습니다. 넘파이가 위와 같이 배열 조각을 복사하지 않고 뷰 형태로 나타내는 이유는 메모리를 절약하기 위해서입니다. 애초부터 대용량 데이터 처리를 염두에 두고 설계되었기에 복사로 인한 메모리 남용을 최소화 하기 위한 방책인 것이지요. 그래도 복사본을 만들고 싶다면 .copy() 를 사용할 수 있습니다. 아래는 해당 메서드를 사용하여 배열 조각에 대한 복사본을 저장하고, 복사본의 원소를 변경하여도 원본의 데이터는 변경되지 않음을 확인할 수 있습니다.

>>> arr3 = np.arange(7)
>>> part_of_arr3 = arr2[3:6].copy()
>>> part_of_arr3[1] = -1
>>> print(f"""part_of_arr3 : {part_of_arr3}
arr3 : {arr3}""")

part_of_arr3 : [ 3 -1  5]
arr3 : [0 1 2 3 4 5 6]

in n-D array

Indexing

다음으로 다차원 배열에서의 인덱싱과 슬라이싱에 대해서 알아보도록 하겠습니다. 다차원 배열에서는 바깥부터 안쪽으로 하나하나 접근해 들어가야 합니다. 다음과 같은 2차원 배열이 있다고 해보겠습니다.

>>> arr2d1 = np.arange(9).reshape(3, -1)
>>> arr2d1
array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])

이 배열에서 5를 고르려면 어떻게 해야 할까요. 5는 가장 바깥 차원을 기준으로 1번째 인덱스에 위치하고 있는 배열인 [3,4,5] 안에 있으며 해당 배열 안에선 2번째 리스트에 있습니다. 그렇기 때문에 해당 배열에서 5를 인덱싱하는 방법은 아래와 같습니다.

>>> arr2d1[1][2]
5

# 매번 괄호를 써서 인덱싱하기 귀찮으므로
# , 를 사용하여 인덱싱 할 수 있습니다.
>>> arr2d1[1,2]
5

3차원 배열도 마찬가지 입니다. 아래와 같은 배열에서 5를 인덱싱하는 예시를 알아보겠습니다.

>>> arr3d1 = np.arange(12).reshape(2, 3, -1)
>>> arr3d1
array([[[ 0,  1],
        [ 2,  3],
        [ 4,  5]],
       [[ 6,  7],
        [ 8,  9],
        [10, 11]]])

>>> arr3d1[0, 2, 1]
5

전체 3차원 배열에서 5가 포함되어 있는 2차원 배열인 [[0,1],[2,3],[4,5]] 가 0번째 인덱스에 위치하고 있습니다. 그리고 그 안에서 [4,5] 는 2번째 인덱스에 위치하고 있고, 5는 [4,5] 중 1번째 인덱스에 있습니다.

Slicing

슬라이싱도 유사한 방법으로 할 수 있습니다. 위에서 사용했던 2차원 배열 arr2d1에서 [[3,4], [6,7]] 만 슬라이싱해보겠습니다.

>>> arr2d1[1:, :2]
array([[3, 4],
       [6, 7]])

먼저 가장 바깥 차원에서 필요한 부분은 [[3,4,5],[6,7,8]] 이므로 1: 로 슬라이싱 합니다. 다음으로 각 1차원 배열의 0번째와 1번째 인덱스만 필요하므로 :2 로 슬라이싱합니다. 3차원 배열도 마찬가지 입니다. arr3d1 에서 [[[0], [2], [4]]] 만 슬라이싱 해보겠습니다.

>>> arr3d1[:1, :, :1]
array([[[0],
        [2],
        [4]]])

:1 슬라이싱은 어차피 0번째 인덱스만 가져옵니다. 그렇다면 0 인덱싱으로 인덱싱하는 것과 어떤 차이점을 갖고 있을까요? 정답은 배열 조각의 차원입니다. 슬라이싱으로 가져오면 원래 배열의 차원이 유지되지만 인덱스로 가져오면 해당 위치의 배열 차원이 사라집니다. 아래는 동일하게 0,2,4 라는 원소를 가진 배열 조각을 인덱싱을 섞어 가져오는 방법을 나타낸 코드입니다.

>>> arr3d1[0, :, :1]
array([[0],
       [2],
       [4]])

>>> arr3d1[:1, :, 0]
array([[0, 2, 4]])

>>> arr3d1[0, :, 0]
array([0, 2, 4])

arr3d1[:1, :, :1] 처럼 원래 3차원 배열에 모두 슬라이싱을 사용하여 얻어낸 배열 조각은 3차원입니다. 여기서 앞이나 뒤에 있는 :10 으로 인덱싱하면 각 위치에서의 차원이 사라지면서 2차원 배열을 얻어낼 수 있습니다. 각각 array([[0],[2],[4]])array([[0, 2, 4]]) 이라는 결과가 나왔습니다. :1 을 모두 0 으로 대체하면 2개의 차원이 사라지면서 1차원 배열 array([0, 2, 4]) 이 나오게 됨을 확인할 수 있습니다.

Boolean Indexing

불리언 인덱싱(Boolean indexing, 불린 인덱싱)은 배열에서 원하는 조건에 맞는 원소를 선택하는 방법입니다. 아래의 코드를 보겠습니다.

>>> arr = np.arange(6)
>>> arr
array([0, 1, 2, 3, 4, 5])

>>> bool_odd = (arr%2 == 1)
>>> bool_odd
array([False, True, False, True, False, True])

>>> bool_odd.dtype
dtype('bool')

위와 같이 설정한 조건에 맞는 불리언 배열이 생성됩니다. 이 불리언 배열을 사용하여 특정 배열을 인덱싱해보겠습니다.

>>> weight_init = np.random.randn(6, 4)
>>> weight_init
array([[ 0.82125035,  1.17102989, -0.33640184,  1.21870209],
       [-1.01272699,  1.69531067, -0.72790492, -0.76726316],
       [-0.0283028 ,  1.07953567, -0.3576    , -0.02536179],
       [ 0.65688213,  1.46257328, -0.10013135,  2.19944658],
       [ 0.26648601, -1.67300499, -0.5105097 ,  1.08691335],
       [ 0.08560957,  1.38428115, -0.82035441,  0.22561346]])

>>> weight_init[bool_odd]
array([[-1.01272699,  1.69531067, -0.72790492, -0.76726316],
       [ 0.65688213,  1.46257328, -0.10013135,  2.19944658],
       [ 0.08560957,  1.38428115, -0.82035441,  0.22561346]])

bool_odd 의 원소 값이 True 인 홀수 행만 선택됨을 확인할 수 있습니다. 여기에 조건을 추가하여 선택할 수도 있습니다.

>>> weight_init[bool_odd, :-1]
array([[-1.01272699,  1.69531067, -0.72790492],
       [ 0.65688213,  1.46257328, -0.10013135],
       [ 0.08560957,  1.38428115, -0.82035441]])

마지막 열을 제외하고 슬라이싱 되었음을 확인할 수 있습니다. and(&)or(|) 논리 연산자를 사용하면 여러 개의 조건을 설정할 수 있습니다. 조건을 하나 더 설정한 뒤에

>>> bool_3multiple = (arr%3 == 0)
>>> bool_3multiple
array([ True, False, False,  True, False, False])

>>> weight_init[bool_odd & bool_3multiple]
array([[ 0.65688213,  1.46257328, -0.10013135,  2.19944658]])

>>> weight_init[bool_odd | bool_3multiple]
array([[ 0.82125035,  1.17102989, -0.33640184,  1.21870209],
       [-1.01272699,  1.69531067, -0.72790492, -0.76726316],
       [ 0.65688213,  1.46257328, -0.10013135,  2.19944658],
       [ 0.08560957,  1.38428115, -0.82035441,  0.22561346]])

and 를 사용하였을 때는 두 조건을 모두 만족하는 3번째 인덱스에 있는 배열만 선택되었고, or 을 사용하였을 때는 두 조건 중 하나라도 만족하는 0,1,3,5 번째 인덱스에 있는 배열이 선택되었습니다.

Fancy Indexing

팬시 인덱싱(Fancy indexing)은 정수 배열을 사용하여 배열을 인덱싱하는 방법입니다. 위에서 사용했던 2차원 배열 weight_init 에 팬시 인덱싱을 적용해보겠습니다.

>>> weight_init
array([[ 0.82125035,  1.17102989, -0.33640184,  1.21870209],
       [-1.01272699,  1.69531067, -0.72790492, -0.76726316],
       [-0.0283028 ,  1.07953567, -0.3576    , -0.02536179],
       [ 0.65688213,  1.46257328, -0.10013135,  2.19944658],
       [ 0.26648601, -1.67300499, -0.5105097 ,  1.08691335],
       [ 0.08560957,  1.38428115, -0.82035441,  0.22561346]])

>>> weight_init[[1, 5, 0, 2]]
array([[-1.01272699,  1.69531067, -0.72790492, -0.76726316],
       [ 0.08560957,  1.38428115, -0.82035441,  0.22561346],
       [ 0.82125035,  1.17102989, -0.33640184,  1.21870209],
       [-0.0283028 ,  1.07953567, -0.3576    , -0.02536179]])

1, 5, 0, 2 번째 인덱스에 위치한 행이 그대로 출력되었음을 확인할 수 있습니다. 기본 인덱싱과 같이 , 뒤에 다른 배열을 연결해주면 열(Column)에 대한 인덱싱도 할 수 있습니다.

>>> weight_init[[1, 5, 0, 2], [2, 1, 0, 3]]
array([-0.72790492,  1.38428115,  0.82125035, -0.02536179])

위와 같이 인덱싱한 결과는 [1,2], [5,1], [0,0], [2,3] 로 각각 인덱싱 했을 때의 성분이 하나의 배열로 묶여 있는 것과 같습니다. 만약 1,5,0,2 번째 행에 있는 모든 성분을 2,1,0,3의 순서로 선택하고 싶다면 아래와 같은 코드를 작성할 수 있습니다.

>>> weight_init[[1, 5, 0, 2]][:, [2, 1, 0, 3]]
array([[-0.72790492,  1.69531067, -1.01272699, -0.76726316],
       [-0.82035441,  1.38428115,  0.08560957,  0.22561346],
       [-0.33640184,  1.17102989,  0.82125035,  1.21870209],
       [-0.3576    ,  1.07953567, -0.0283028 , -0.02536179]])

이 행렬은 weight_init[[1, 5, 0, 2]] 로 인덱싱한 행렬을 [:, [2,1,0,3]] 으로 열만 다시 팬시 인덱싱한 것으로 순서가 바뀌어 있음을 알 수 있습니다.

Conclusion

이번 게시물에서는 색인과 슬라이싱을 사용하여 배열의 요소를 선택하고 조작하는 방법에 대해서 알아보았습니다. 다음에는 정말 Numerical 한 작업을 수행하는 유니버셜 함수에 대해서 알아보겠습니다.

Comment  Read more