티스토리 뷰
(한빛미디어에서 모집하는 혼공학습단 활동의 일환으로 혼자 공부하는 머신러닝+딥러닝 책을 공부하고 작성한 글입니다. 책은 제 돈으로 샀습니다. 본문의 코드는 책의 소스코드를 기반으로 하되 글 흐름에 맞게 수정한 것입니다. 원본 코드는 저자 박해선 님의 깃허브에서 보실 수 있습니다. 책에 나오는 넘파이, 판다스 등의 내용은 본 글에는 자세히 넣지 않았습니다. 본 글의 코드는 제 깃허브에서 보실 수 있습니다.)
군집화란?
2장에서 다루었던 생선 데이터를 오랜만에 다시 꺼내보겠습니다. 깃허브에 있는 .py 파일을 wget으로 가져올 수 있습니다. 단 이 방식으로 코랩 드라이브에 저장한 파일은 코랩 연결이 끊기면 없어지므로 매번 다시 가져와야 하는 단점은 있습니다. 아래는 저자 박해선님의 깃허브에 있는 파일 https://bit.ly/bream_smelt를 가져와서 그래프로 그리는 코드입니다.
2장은 지도 학습이었으므로, 그래프에서 좌하단에 오밀조밀 보여있는 점들이 빙어, 우상단에 약간 퍼져있는 점들이 도미임을 미리 알려주는 방식이었습니다. 그런데 그래프를 보면 생선 이름은 모르더라도 두 그룹으로 나눌 수는 있을 것 같습니다.
이렇게 특성값이 비슷한 샘플끼리 모으는 것을 군집clustering 또는 군집화이라고 부릅니다. 모여있는 샘플들의 모음을 클러스터cluster라고 부릅니다.
이렇게 비슷한 샘플끼리 그룹으로 모으는 작업을 군집clustering이라고 합니다. 군집은 대표적인 비지도 학습 작업 중 하나입니다. 군집 알고리즘에서 만든 그룹을 클러스터cluster라고 부릅니다.
박해선, 혼자 공부하는 머신러닝 + 딥러닝, 한빛미디어, 298쪽
군집 알고리즘은 정답을 알려주지도 않아도 특성이 비슷한 샘플들끼리 클러스터를 나눌 수 있습니다. 이와 같이 타겟 없이 학습하는 것을 비지도 학습unsupervised learning이라고 부릅니다.
과일 이미지 분류하기
본격적으로 이번 장에서 다루는 과일 이미지를 불러오겠습니다.
fruits에는 사과 이미지 100장, 파인애플 이미지 100장, 바나나 이미지 100장이 순서대로 들어있습니다. fruits의 크기는 (300, 100, 100)이며 fruits[i]는 100 x 100의 이미지 한장입니다. 이미지는 기본적으로 흑백 반전되어 있는데, 이미지에서 중요한 부분은 백그라운드background가 아니라 과일이 있는 포그라운드foreground이기 때문입니다. 이미지를 반전시키면 백그라운드의 픽셀값은 낮게, 과일이 있는 부분의 픽셀값은 높게 만들 수 있습니다.
이미지를 그리드grid 형태로 쫙 펼쳐서 볼 수 있는 함수를 미리 만들고 시작하겠습니다.
사과, 파인애플, 바나나 이미지를 한번 쫙 보겠습니다.
사과와 파인애플은 과일이 놓인 방향에 무관하게 밝고 어두운 영역이 비슷한 반면, 바나나는 놓인 방향에 따라 밝고 어두운 영역이 많이 다르네요. 생선이 길이와 무게로 클러스터가 나뉘는 것처럼, 과일 이미지도 어떤 특성으로 클러스터를 나눌 수 있으면 좋을 것 같네요. 우선 간단히 각 이미지의 평균 픽셀값을 보겠습니다.
바나나는 확실히 이미지가 평균적으로 어둡습니다. 전체 이미지에서 바나나가 차지하는 영역이 적기 때문이겠죠. 반면 파인애플과 사과는 명확하지 않습니다. 이미지 평균 밝기 외에 다른 특성을 찾아야겠군요. 과일별로 평균 이미지를 구해보겠습니다. 사과, 파인애플, 바나나 각각의 사진 100장을 평균하는 겁니다.
결과를 보니 몇 가지 특징이 보이네요.
- 사과는 포그라운드가 많고, 포그라운드 내에서도 밝기가 균일하지 않습니다.
- 파인애플은 포그라운드가 많고, 포그라운드 내에서는 밝기가 균일한 편입니다.
- 바나나는 포그라운드가 적고, 바나나가 놓인 방향이 일정하지 않은 효과도 보입니다.
이 특징을 시각화할 다른 방법이 있습니다. 위 이미지를 한줄로 펼쳐서 scatter plot을 보겠습니다.
과일별 평균 이미지의 100개 행을 이어붙여서 하나의 행으로 만든 후 scatter plot을 그린 겁니다. 따라서 그래프의 좌/중/우측이 이미지의 상/중/하단에 해당됩니다. 이 그래프에서도 과일별 특징이 보입니다.
- 사과
- 그래프 위쪽의 넓고 고르게 밀한 영역이 포그라운드에 해당됩니다. 밀한 영역이 가로 방향으로 넓은 것은 밝은 포그라운드가 넓다는 뜻이며, 세로 방향으로 넓은 것은 포그라운드 내에서도 밝기의 변동이 있다는 뜻입니다.
- (4000, 50) 쯤에 더 밀한 영역이 있습니다. 평균 이미지에서 좌상단의 어두운 영역에 해당됩니다.
- 파인애플
- 사과와 비슷하게 포그라운드의 영역이 넓습니다.
- 사과에 비해 포그라운드의 픽셀값은 150 근처에 집중되어 있습니다.
- 바나나
- 사과, 파인애플에 비해 확실히 포그라운드가 좁고 평균적으로 어둡습니다.
책에서는 bar()로 막대그래프를 그렸습니다만, bar()는 이미지의 특징을 표현하기에는 다소 문제가 있습니다. 무엇보다 막대들이 겹쳐서 제대로 표시되지 않는 치명적인 문제가 있습니다. 이 문제는 별도의 글로 정리해두었으니 관심 있는 분들은 확인해주세요.
각 과일의 평균 이미지를 구했으니, 평균 이미지와의 차이를 보면 어느 과일인지 알 수 있지 않을까요? 사과 이미지는 다 비슷하게 생겼으니, 사과 이미지에서 사과 평균 이미지를 빼면 픽셀값이 거의 0에 가까울 것 같습니다. 바로 실행에 옮겨보죠. 300장의 이미지에서 사과 평균 이미지를 빼고, 차이의 절대값의 평균이 작은 100개 이미지를 그려보겠습니다.
정확하게 사과 100개가 골라졌군요! 그럼 파인애플과 바나나도 완벽하게 동작할까요?
저런, 스파이가 숨어들었군요. 여기엔 이유가 있습니다. 전체 300개 이미지에서 사과 평균 이미지를 빼고 절대값의 평균을 scatter plot으로 그려보겠습니다.
과일의 영역이 겹치지 않는군요. 사과를 완벽하게 골라낸 것이 이해가 됩니다. 하지만 파인애플에 대해서 같은 계산을 했을 때 위 그래프가 평행이동할 것이라고 생각하면 안됩니다. 사과 평균 이미지와 파인애플 평균 이미지는 완전히 별개이기 때문입니다.
파인애플 평균 이미지를 뺀 절대값을 보니 사과 이미지 3개가 파인애플 영역을 침범한 것이 보입니다. 이 3장이 위 이미지에 보인 3장입니다. 바나나도 확인해볼까요?
어이쿠… 여긴 바나나 하나가 아예 저 멀리 날아가버렸네요. 바나나는 놓인 방향이 워낙 제각각이라 평균 이미지가 제대로 된 역할을 못한 것 같군요.
그런데 잘 생각해보니 지금까지 했던 방식은 비지도 학습이라고 부를 수 없을 것 같습니다. 어떤 이미지가 사과인지 이미 알고 사과 평균 이미지를 구했으니까요. 제대로 된 비지도 학습을 하려면 다른 방식이 필요합니다.
k-평균 알고리즘은 KMeans로
k-평균 알고리즘의 동작 방식은 간단합니다. k는 클러스터의 개수입니다.
- 일단 k개의 점을 아무렇게나 찍습니다. 여기서 말하는 ‘점’이란 특성이 N개일 때 N차원 공간의 점을 말합니다. 맨 앞에 보여드린 생선이라면 랜덤한 (길이, 무게)의 2차원 점이고, 과일 이미지라면 랜덤한 100 x 100 이미지에 해당되는 10000차원의 점입니다. 이 점들을 클러스터 중심이라고 부릅니다.
- 각 샘플에 대해 가장 가까운 클러스터 중심을 찾습니다. 이제 k개의 클러스터가 만들어졌습니다.
- 각 클러스터에 속한 샘플들의 평균을 계산합니다. 이 평균이 새로운 클러스터 중심이 됩니다.
- 다시 한번 각 샘플에 대해 가장 가까운 클러스터 중심을 찾습니다. 이것을 클러스터 중심의 변화가 없을 때까지 반복합니다.
이 과정을 쉽게 설명한 영상이 있어서 아래에 붙입니다.
참고: 차원의 의미
머신러닝을 공부하다 보면 '차원'이라는 단어가 참 많이 나옵니다. 우리에게 익숙한 차원은 2차원, 3차원과 같이 좌표계 표현에 사용되는 차원입니다. 그런데 머신러닝에서는 5차원, 7차원, 심지어 방금 전과 같이 10000차원이라는 어마어마한 숫자가 나옵니다. 다음 문장을 보시죠.
"크기가 28x28인 2차원 이미지는 784차원 공간의 한 점으로 볼 수 있다."
2차원 이미지라고 해놓고 갑자기 784차원이라니? 선형대수를 배우지 않은 분들은 이 개념이 익숙하지 않을 수 있습니다. 차원은 쉽게 말해 독립변수의 개수입니다. 2차원 평면 상 한 점의 좌표를 특정하기 위해서는 2개의 독립변수가 필요합니다. 3차원은 3개의 독립변수가 필요하겠죠. 독립변수의 개념으로 보자면, 2차원 이미지는 이미지가 2차원인 것이 아니고 이미지가 놓인 공간이 2차원입니다. 이미지의 어떤 픽셀의 위치를 특정하기 위해 2개의 독립변수가 필요하기 때문입니다. 이걸 간단히 2차원 이미지라고 부르는 겁니다. 반면 28x28 이미지의 각 픽셀의 값을 독립변수로 본다면 이미지는 784차원이 됩니다. 그래서 28x28 이미지는 784차원 공간의 한 점이 됩니다.
정리하자면, 어떤 대상(예: 이미지)을 구성하는 요소(예: 픽셀)의 위치를 특정하는 데에 필요한 독립변수의 개수를 차원으로 둘 것이냐(예: 2차원 이미지), 대상의 구성에 필요한 요소 하나하나를 독립변수로 보고 요소의 개수를 차원으로 둘 것이냐(예: 784차원 공간의 점)의 차이입니다.
k-평균 동작방식에서 ‘가깝다’는 어떤 의미일까요? N차원 공간에서 L2 norm을 생각하면 됩니다. 위키피디아에서는 k-평균의 목표를 아래 수식으로 표현하고 있네요.
$ \underset{S}{\arg\min} \sum_{i=1}^{k} {\sum_{\textbf{x} \in S_{i}} {|| \textbf{x} - \boldsymbol \mu_i||^2}} $
$ \textbf x $가 샘플이고 $ \mu_i $는 i번째 클러스터의 중심입니다. 사이킷런에는 k-평균 알고리즘을 위해 KMeans 클래스가 준비되어 있습니다. 단 세줄이면 끝납니다.
KMeans의 인스턴스인 km은 k-평균을 담당하는 매니저입니다. 클러스터가 3개임을 n_clusters 파라미터로 전달했습니다. 앞선 주차들과 마찬가지로 fit()으로 학습을 시킵니다. 사용법에 일관성이 있어서 편리하군요. KMeans.labels_에는 각 샘플에 달린 레이블이 저장됩니다. 결과를 보니 300개의 이미지가 레이블 0, 1, 2로 분류되었네요. 비지도 학습이므로 0, 1, 2라는 숫자는 의미가 없습니다. 단지 “쟤와는 다르다”일 뿐입니다. 각 레이블별로 이미지들을 확인해보겠습니다.
레이블 0, 1, 2는 각각 파인애플, 바나나, 사과에 해당됩니다. 사과와 바나나는 완벽한 반면 파인애플은 정확도가 약간 떨어지네요. KMeans가 찾은, 또는 수렴시킨 클러스터 중심은 KMeans.cluster_centers_에 들어있습니다.
각 과일 평균 이미지가 나왔군요?! k-평균 알고리즘이 무엇을 찾아가는지 대충 알 것 같습니다.
KMeans.transform() 메서드는 샘플과 클러스터 중심들까지의 거리를 계산해줍니다. 100번째 이미지와 거리를 보겠습니다.
파인애플이군요. 거리는 0번 클러스터 중심과의 거리가 제일 짧습니다. 앞에서 0번 레이블이 파인애플인 것을 확인했습니다.
KMeans 객체를 만들 때 클러스터가 3개임을 n_clusters 파라미터에 전달해두었습니다. 클러스터가 몇 개인지 모르면 어떻게 하죠?
엘보우 방법: 최적의 k 찾기
군집 알고리즘에서 클러스터의 개수를 찾는 완벽한 방법은 없습니다. k-평균 알고리즘 자체도 사실 완벽하지 않습니다. 앞에 잠깐 보여드린 영상 말미에도 나옵니다. 아래와 같이 군집의 형상이 특이한 경우 k-평균 알고리즘은 잘 동작하지 않습니다.
최적의 클러스터 개수를 찾는 방법 중 엘보우elbow 방법이 있습니다. 엘보우 방법에서는 이너셔inertia를 계산합니다.
앞에서 본 것처럼 k-평균 알고리즘은 클러스터 중심과 클러스터에 속한 샘플 사이의 거리를 잴 수 있습니다. 이 거리의 제곱 합을 이너셔inertia라고 부릅니다. 이너셔는 클러스터에 속한 샘플이 얼마나 가깝게 모여 있는지를 나타내는 값으로 생각할 수 있습니다.
박해선, 혼자 공부하는 머신러닝 + 딥러닝, 한빛미디어, 311쪽
고등학교나 대학교 물리 시간에 관성 모멘트moment of inertia를 배운 적이 있다면 거의 비슷한 개념임을 알 수 있습니다. 클러스터가 많아지면 이너셔도 당연히 작아집니다. 극단적으로 클러스터 개수와 샘플 개수가 같다면 이너셔는 0이 될 겁니다. 엘보우 방법에서는 클러스터 증가에 따른 이너셔 감소율이 갑자기 작아지는 지점을 찾습니다.
k=3에서 그래프가 살짝 꺾이는 곳이 보이시나요? 원래 과일이 3종이었던 것과도 일맥상통하는군요.
주성분 분석
주성분 분석은 굉장히 흥미로운 스킬입니다. 앞에서 100 x 100 이미지를 분류했었죠. 100 x 100 이미지는 10000차원 공간의 한 점입니다. 그런데 이미지들을 보면 다 비슷비슷하게 생겼습니다. 굳이 10000차원까지 필요할까 싶죠. 과일들의 특징을 어떤 형태로든 캐치하면 데이터의 차원을 줄일 수 있지 않을까요? 예를 들어 사과 이미지에서
영역 1과 2의 평균 밝기 사이에 어떤 관계가 있지 않을까요?
몇 개의 아웃라이어가 있기는 하지만, 영역 1과 2의 평균 밝기는 대체로 선형 관계에 있는 것 같습니다.
사과 이미지로부터 새롭게 만든 (영역1 평균 밝기, 영역2 평균 밝기)라는 특성은 빨간색 화살표 방향으로 넓게 퍼져있습니다. 화살표 방향으로 분산이 크다고 표현하기도 합니다. 약간의 정보 손실을 허용한다면 2차원의 데이터를 1차원으로 축소할 수 있습니다. 위 그래프의 모든 점들을 빨간 선 방향으로 투영projection 하는 겁니다. 이제 각 점은 원점에서의 거리만 있으면 됩니다. 이게 바로 주성분 분석에서 차원을 축소하는 방법입니다. 빨간색 화살표가 가리키는 방향이 주성분입니다. 이 방법이 가능한 이유는 특성을 나타내는 점들이 주성분 벡터와 가깝기 때문입니다. 따라서 투영해도 약간의 정보 손실만 발생합니다.
과일 이미지 한장은 10000차원 벡터입니다. 즉, 우리 손에는 지금 10000차원짜리 벡터 300개가 있습니다. 이걸 50차원으로 줄여보겠습니다. 50개의 주성분을 찾아내면 됩니다. 사이킷런의 PCA 클래스를 사용합니다.
fruits_2d는 (300, 10000) 크기의 행렬이었습니다. 10000차원 공간이므로 주성분도 10000차원 공간상의 벡터입니다. PCA.components_에는 주성분 50개가 들어있습니다. 주성분이 어떻게 생겼는지 한번 보죠.
좌상단이 가장 분산이 큰 방향을 표현하는 이미지입니다. 다르게 표현하면 과일 이미지를 가장 잘 표현하는 대표 이미지라고도 볼 수 있습니다. 왠지 사과, 파인애플, 바나나가 적절히 섞여있는 것 같지 않나요?
그럼 주성분은 찾았는데, 이걸 어떻게 이용할까요? 과일 이미지를 각 주성분 방향으로 투영합니다. 그 결과 길이 50인 벡터가 만들어지겠죠. 벡터에는 각 주성분 방향으로의 거리가 저장됩니다. 이제 주성분에 거리를 곱하여 더하면, 즉 선형 결합linear combination 하면 원본 이미지를 비슷하게 복구할 수 있습니다. 결과적으로 10000차원 데이터가 50차원 데이터로 축소되었습니다. 주성분 방향으로 투영하는 것은 transform() 메서드를 사용합니다.
주성분의 활용
축소된 데이터를 원본으로 복구할 수 있습니다. 다만 이미지를 주성분 방향으로 투영하는 과정에서 당연히 정보 손실은 발생합니다. 데이터를 어떻게 복구하는지, 얼마나 손실이 발생하며 그 영향은 어떤지 보겠습니다.
데이터 복구는 PCA 클래스의 inverse_transform 메서드를 사용합니다. transform()으로 만들었던 객체를 inverse_transform에 입력으로 전달하면 됩니다.
이미지 퀄리지는 안 좋아졌지만 웬만큼 잘 복구되었습니다. 원래는 10000개의 숫자로 이루어진 데이터를 단 50개의 숫자만 남겼는데도 말이죠.
PCA를 통해 만든 주성분 50개는 데이터를 표현하는 데에 기여하는 정도가 다릅니다. 첫 번째 주성분의 기여도가 가장 높고 뒤로 갈수록 기여도는 점점 낮아집니다. 주성분 분석에서는 이것을 설명된 분산explained_variance라고 부릅니다. PCA 클래스의 explained_variance_ratio_에 그 값이 들어있습니다.
첫 10개의 주성분만 사용해도 웬만큼 데이터가 잘 복구될 것 같군요.
로지스틱 회귀와 k-평균에 주성분 분석 활용하기
과일을 세 종류로 분류한다고 하니 딱 떠오르는게 있습니다. 로지스틱 회귀입니다. 생선의 무게, 길이, 높이, 두께 등으로 생선을 7종으로 분류했듯이 과일의 이미지를 이용해서 과일을 분류할 수 있지 않을까요? 차이점이라면, 생선 분류는 특성이 5개이고 과일 분류는 특성이 10000개입니다. 특성이 10000개라니 무시무시하네요. 주성분 분석을 해서 특성을 50개로 줄이면 어떨까요? 우선 원본을 그대로 이용해보겠습니다.
굉장히 높은 점수가 나왔네요. 특성은 무려 10000개인데 샘플이 300개밖에 되지 않기 때문인 것 같습니다. 시간은 1.74초가 소요되었습니다. 각 교차 검증 폴드의 시간의 평균입니다. 이제 주성분 50개로 축소한 데이터를 훈련시켜보겠습니다.
정확도는 100%가 나왔습니다. 그리고 시간이 0.07초로 줄었습니다. 무려 25배나 줄었네요!
앞에서는 PCA 객체를 만들 때 주성분 개수를 지정했습니다. 다른 방법도 있습니다. 설명된 분산의 합을 지정할 수도 있습니다. PCA 객체를 만들 때 n_components를 0과 1 사이의 실수로 입력하면 됩니다.
설명된 분산의 합이 0.5가 되도록 하라고 했더니 단 2개의 주성분만 만들었네요. 이것만으로도 분류가 된다고요?
헐? 되는군요. 점수 0.99가 나왔습니다.
k-평균에도 PCA로 차원 축소한 데이터를 사용해볼까요?
대박. 이 정도면 거의 “이게 된다고?” 수준이네요. 주성분을 2개로 했다면 각 샘플을 2차원 평면 상의 점으로 표현할 수 있습니다. fruits_pca를 그대로 scatter plot으로 그려보겠습니다.
클러스터가 잘 만들어진게 보이는군요. 다시 한번 말하지만 주성분을 단 2개만 사용한 결과입니다.
기본 미션: k-평균 알고리즘 작동 방식 설명하기
- 클러스터 중심이 될 k-개의 점을 랜덤하게 정합니다. 여기서 말하는 ‘점’이란 특성이 N개일 때 N차원 공간의 점을 말합니다.
- 각 샘플에 대해 가장 가까운 클러스터 중심을 찾습니다. 이제 k개의 클러스터가 만들어졌습니다.
- 각 클러스터에 속한 샘플들의 평균을 계산합니다. 이 평균이 새로운 클러스터 중심이 됩니다.
- 새로운 클러스터 중심이 이전 중심과 동일하다면 알고리즘을 종료합니다. 그게 아니라면 위 2번으로 돌아갑니다.
선택 미션: 06-3 확인 문제 풀고 풀이과정 정리
1. 특성이 20개인 대량의 데이터셋이 있습니다. 이 데이터셋에서 찾을 수 있는 주성분 개수는 몇 개일까요?
- 정답: 20개
- 해설: 주성분은 특성 개수만큼 찾을 수 있습니다. 따라서 답은 20개입니다. 정확히 말하자면, 주성분은 특성 개수와 샘플 개수 중 작은 값만큼 찾을 수 있습니다. 이는 PCA 클래스의 공식 문서에서도 볼 수 있습니다. 대부분의 경우 특성 개수보다 샘플 개수가 더 많으므로, 특성 개수가 주성분 개수가 됩니다.
2. 샘플 개수가 1,000개이고 특성 개수는 100개인 데이터셋이 있습니다. 즉 이 데이터셋의 크기는 (1000, 100)입니다. 이 데이터를 사이킷런의 PCA 클래스를 사용해 10개의 주성분을 찾아 변환했습니다. 변환된 데이터셋의 크기는 얼마일까요?
- 정답: (1000, 10)
- 해설: 크기 100 x 100인 이미지 300장을 PCA에 전달할 때 (300, 10000)로 만들어서 전달했었죠. axis 0 방향의 크기는 샘플 개수, axis 1 방향 크기는 특성의 개수입니다. 이것 역시 공식 문서에서 볼 수 있습니다.
3. 2번 문제에서 설명된 분산이 가장 큰 주성분은 몇 번째인가요?
- 정답: 첫 번째 주성분
- 해설: 주성분은 분산이 가장 큰 방향부터 찾습니다. 따라서 설명된 분산의 크기 순서대로 반환되며, 첫 번째 주성분의 설명된 분산이 가장 큽니다.
6장 내용정리는 여기서 마치겠습니다. 끝까지 읽어주셔서 고맙습니다.
'혼공머신' 카테고리의 다른 글
[혼공머신] 혼공학습단 8기를 마무리하며 (부제: 천 줄 코딩도 import부터) (0) | 2022.08.21 |
---|---|
[혼공머신] 7장. 딥러닝을 시작합니다 (2) | 2022.08.21 |
[혼공머신] 5장. 트리 알고리즘: 화이트 와인을 찾아라! (0) | 2022.07.31 |
[혼공머신] 4장. 다양한 분류 알고리즘: 럭키백의 확률을 계산하라! (0) | 2022.07.24 |
[혼공머신] 3장. 회귀 알고리즘: 농어의 무게를 예측하라! (0) | 2022.07.17 |