비지도 학습(Unsupervised Learning) : 레이블이 없는 데이터를 학습하는 머신러닝의 한 종류.
- 입력 데이터에 대한 정답(레이블)이 없이 모델이 데이터의 숨겨진 패턴이나 구조를 학습한다.
- 비지도 학습의 목적 : 데이터의 구조를 파악하거나, 유사한 특성을 가진 데이터들을 그룹화하거나,
차원 축소를 통해 데이터의 본질적인 특성을 드러내는 것.
- 장점
- 레이블이 없는 데이터 사용 : 정답(레이블)이 없는 데이터를 사용하여 학습할 수 있으므로, 데이터 레이블을 붙이는 작업이 불필요
- 새로운 패턴 발견 : 기존에 알려지지 않은 패턴이나 구조를 발견하는 데 유용
- 데이터의 이해 : 데이터를 자동적으로 그룹화하거나 축소하여 데이터의 본질을 이해하도록 도와줌
- 비즈니스 적용 가능성 : 고객 세분화(클러스터링), 이상 거래 탐지(이상치 탐지) 등의 비즈니스 문제 해결에 효과적
- 단점
- 목표가 불분명 : 레이블(정답)이 없으므로 학습 목표가 명확하지 않아 결과의 해석이 어려울 수 있음
- 클러스터의 개수 설정 어려움 : 클러스터링에서 클러스터의 개수를 미리 설정해야 하므로, 적절한 클러스터 개수를 찾는 것이 어려울 수 있음
- 결과의 평가 어려움 : 비지도 학습에서는 정확도나 오차율처럼 쉽게 평가할 수 있는 기준이 부족함
>> 결과의 평가 기준을 설정하기 어렵다
- 활용
- 고객 세분화 : 마케팅 분야에서 고객의 구매 패턴을 분석하고, 이를 바탕으로 고객을 다양한 그룹으로 나누어 맞춤형 서비스를 제공
- 이상치 탐지 : 신용카드 사기 탐지나 네트워크 보안에서 이상한 행동이나 거래를 탐지
- 추천 시스템 : 사용자의 관심사나 행동을 기반으로 개인화된 추천을 제공
- 생물학적 데이터 분석 : 유전자 데이터를 분석하여 유사한 유전자 그룹을 찾거나 질병의 패턴 발견
- 자연어 처리 : 텍스트 데이터를 주제별로 클러스터링 하거나, 단어를 벡터 공간에 임베딩하여 의미를 추출
과일 사진 데이터 준비
# 아나콘다 파워쉘 프롬포트에서 curl -L https://bit.ly/fruits_300_data -o fruits_300.npy
# dir *.npy 파일 위치를 찾고, 작업중인 폴더로 이동
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
fruits = np.load('fruits_300.npy')
첫 번째 차원 : 샘플의 갯수(300)
두 번째 차원 : 이미지의 세로 길이(행의 갯수)
세 번째 차원 : 이미지의 가로(너비)
# imshow()를 사용해서 저장된 첫 번째 이미지 확인하기
plt.imshow(fruits[0], cmap = 'gray')
plt.show()
- imshow() : 주어진 이미지를 화면에 출력하는 함
- cmap = 'gary' : 이미지를 회색으로 표시
# 원본 이미지 표시 : cmap = 'gray_r' 으로 색 반전
plt.imshow(fruits[0], cmap='gray_r')
plt.show()
데이터에 있는 바나나, 파인애플 출력하기
fig, axs = plt.subplots(1, 2) # 두번째 파라미터 2 : 2개의 컬럼으로 이미지 출력
axs[0].imshow(fruits[100], cmap = 'gray_r')
axs[1].imshow(fruits[200], cmap = 'gray_r')
plt.show()
- plt.subplots(1, 2) : 서브플롯을 생성하는 함수
- 1개의 행과 2개의 열을 가진 subplot을 생성 >> 두 개의 이미지가 나란히 출력되는 형태로 플롯을 구성
- fig : 전체 그림을 포함하는 figure 객체 / axs : 서브플롯의 Axes 객체를 포함하는 배열
픽셀값 분석하기
# 데이터를 처리하기 쉽게 100 x 100 이미지를 길이가 10000인 1차원 배열로 만들기
apple = fruits[0:100].reshape(-1, 100*100)
pineapple = fruits[100:200].reshape(-1, 100*100)
banana = fruits[200:300].reshape(-1, 100*100)
- fruits[x:y] : furits 배열에서 x번째 부터 y번째 이미지까지 선택 (여기서는 100개의 이미지 추출)
- .reshape(-1, 100*100)
- -1 : 자동으로 나머지 차원을 계산
- 100 * 100 : 각 이미지가 100x100 픽셀 크기이므로, 100x100 = 10,000개의 픽셀
각 이미지는 10000개의 특성을 가진 벡터로 변환
bar chart를 그리기 위해 과일이미지 픽셀의 평균값 계산하기
각 과일의 픽셀 평균값을 히스토그램으로 그리기
plt.hist(np.mean(apple, axis=1), alpha=0.8)
plt.hist(np.mean(pineapple, axis=1), alpha=0.8)
plt.hist(np.mean(banana, axis=1), alpha=0.8)
plt.legend(['apple', 'pineapple', 'banana'])
plt.show()
- np.mean() : 배열의 평균 값 계산
- axis = 1 : 각 행(이미지)의 평균을 계산
- alpha=0.8 : 히스토그램의 투명도를 80%로 설정
- plt.legend(['apple', 'pineapple', 'banana'])
- plt.legend()는 각 히스토그램의 레전드를 추가하는 함수
- 'apple', 'pineapple', 'banana'는 각각 각 히스토그램을 설명하는 레이블을 추가하여 그래프에 표시
바나나 이미지의 평균값이 40 아래에 집중
사과와 파인애플은 픽셀 평균값이 90~100 사이에 집중
바나나는 픽셀 평균값으로 사과나 파인애플과 확실히 구분되지만, 사과와 파인애플을 겹쳐있어서 구분이 어려움
다른 방법으로 구분하기
# 샘플의 평균값이 아닌 픽셀별 평균값으로 비교하기
fig, axs = plt.subplots(1, 3, figsize=(20, 5))
# 첫 번째 컬럼의 바 그래프
axs[0].bar(range(10000), np.mean(apple, axis=0))
axs[1].bar(range(10000), np.mean(pineapple, axis=0))
axs[2].bar(range(10000), np.mean(banana, axis=0))
plt.show()
- plt.subplots(1, 3) : 1개의 행과 3개의 열로 서브플롯을 생성.(한 번에 3개의 그래프를 나란히 표시할 수 있게 설정)
- figsize=(20, 5) : 그림의 크기를 설정하는 파라미터(여기서는 가로로 길게 20, 세로로 5 크기의 그래프를 설정)
- fig는 전체 플롯을 나타내고, axs는 각각의 서브플롯을 관리하는 배열
순서대로 사과 : 파인애플 : 바나나
사과는 아래로 갈수록 값이 커짐
파인애플은 고르게 분류
바나나는 중간의 값이 높음
픽셀 평균값으로 각 과일별 이미지 출력하기
apple_mean = np.mean(apple,axis=0).reshape(100,100)
pineapple_mean = np.mean(pineapple,axis=0).reshape(100,100)
banana_mean = np.mean(banana,axis=0).reshape(100,100)
fig, axs = plt.subplots(1,3,figsize=(20,5))
axs[0].imshow(apple_mean, cmap='gray_r')
axs[1].imshow(pineapple_mean, cmap='gray_r')
axs[2].imshow(banana_mean, cmap='gray_r')
plt.show()
픽셀 평균값과 가까운 이미지 고르기
# fruits 배열에 있는 모든 샘플 이미지에서 apple_mean을 뺀 절대값의 평균 계산
# abs_diff의 배열도 (300,100,100)의 크기
abs_diff = np.abs(fruits - apple_mean)
abs_mean = np.mean(abs_diff, axis=(1,2))
abs_mean.shape
# 평균값이 가장 작은 순서대로 100개 고르기
# abs_mean의 오름차순으로 정렬하고, 맨 위의 100개의 인덱스 출력
apple_index = np.argsort(abs_mean)[:100]
fig, axs = plt.subplots(10,10,figsize=(10,10)) # 10행 10열의 이미지로 출력
for i in range(10):
for j in range(10):
axs[i,j].imshow(fruits[apple_index[i*10 + j]], cmap='gray_r')
axs[i,j].axis('off') # 축 제거
plt.show()
apple_mean과 가장 가까운 사진 100개를 고르는 코드 구현
>> 모두 사과를 예측
비슷한 샘플끼리 모으는 작업을 군집(clustering)이라 함
k-평균 알고리즘
# k-평균 모델 훈련을 위해 3차원 배열을
# (샘플갯수, 너비*높이)의 크기를 가진 2차원 배열로 변경
fruits_2d = fruits.reshape(-1, 100*100)
# 클래스터 갯수 설정 : n_cluster = 3
from sklearn.cluster import KMeans
km = KMeans(n_clusters = 3, random_state = 42)
# 비지도 학습이므로 타깃 데이터 사용 X
km.fit(fruits_2d)
군집 결과는 KMeans 객체의 labels_ 속성에 저장됨
샘플의 갯수 확인 및 이미지 출력하기
# 이미지 출력 함수 구현
def draw_fruits(arr, ratio = 1):
n = len(arr) # 샘플 갯수 저장
# 한 줄에 10개씩 이미지 그리기
rows = int(np.ceil(n/10)) # np.ceil : 소수점 올림
# 행이 1개면 열의 갯수는 샘플의 갯수
cols = n if rows < 2 else 10
fig, axs = plt.subplots(rows, cols, figsize=(cols*ratio, rows*ratio), squeeze = False)
for i in range(rows):
for j in range(cols):
if i * 10 + j < n: # n개 까지만 그리기
axs[i, j].imshow(arr[i*10+j], cmap='gray_r')
axs[i, j].axis('off')
plt.show()
- def draw_fruits(arr, ratio = 1) :
- arr : 이미지 배열을 입력 받음
- ratio : figsize에서 이미지 크기를 조정하는 비율
- n = len(arr) : 이미지 배열의 길이(arr에 포함된 이미지의 개수)
- rows = int(np.ceil(n/10))
- rows : 이미지를 그릴 행(row)의 수
- np.ceil(n/10) : 이미지를 10개씩 한 줄에 표시
- cols = n if rows < 2 elsw 10
- rows < 2 : 이미지가 10개 이하일 경우 한 줄에 모든 이미지를 그리기 위해 n으로 설정
- else 10 : 10개 이상의 이미지가 있을 경우 한 줄에 10개씩 이미지를 그리도록 열 수를 10으로 고정
- fig, axs = plt.subplots(rows, cols, figsize=(cols*ratio, rows*ratio), squeeze=False)
- figsize=(cols*ratio, rows*ratio): figsize는 전체 그림의 크기를 지정(cols*ratio와 rows*ratio는 열과 행에 맞춰 이미지를 출력할 때 크기를 조정하는 비율)
- squeeze=False: axs가 2D 배열 형태로 반환되도록 설정(squeeze=True일 경우, 행렬이 1차원 배열로 변환될 수 있기 때문에 이를 방지하려고 False로 설정)
- for i in range(rows):와 for j in range(cols) :
- 2개의 for 루프를 사용하여 각 행(i)과 열(j)을 순회하며 이미지를 서브플롯에 배치
# 레이블(정답)이 0인 과일 그리기
draw_fruits(fruits[km.labels_ == 0])
# 두번째 클러스터
draw_fruits(fruits[km.labels_ == 1])
# 세번째 클러스터
draw_fruits(fruits[km.labels_ == 2])
레이블이 0인 클러스터에 사과 9개 바나나 2개가 섞여있음.
타깃 데이터를 제공하지 않았음에도 비슷한 샘플을 잘 모았음
클러스터의 중심
# cluster_centers_ 속성에 저장되어 있음
draw_fruits(km.cluster_centers_.reshape(-1, 100, 100), ratio = 3)
첫 번째 클러스터 중심점 까지의 거리가 가장 짧음
최적의 k 찾기
- k-평균 알고리즘의 단점 : 클러스터 갯수를 지정해야 함
- 실전에서는 몇 개의 클러스터인지 알 수 없음
- 적절한 k값을 찾기 위해 엘보우 방법 사용
>> 클로스터의 갯수를 늘려가면서 중심점과 각 이미지의 거리 변화를 관찰하여 찾는 방법
각 이미지의 거리변화(이너셔)를 그래프로 그리면 감소하는 속도가 꺾이는 지점 == 엘보우
엘보우 지점이 클러스터의 갯수
# 클러스터 갯수를 2~6까지 바꿔가면서 KMeans 훈련
inertia = []
for k in range(2, 7):
km = KMeans(n_clusters = k, random_state = 42)
km.fit(fruits_2d)
inertia.append(km.inertia_)
plt.plot(range(2,7), inertia)
plt.xlabel('k')
plt.ylabel('inertia')
plt.show()
그래프에서 꺾이는 지점 : k=3 에서 기울기가 조금 바뀜.
3이 최적의 k값(클러스터 갯수)
주성분 분석(PCA, Principal Component Analysis)
- 차원 축소 기법 중 하나로 데이터의 분산을 최대한 유지하면서 데이터를 더 적은 차원의 공간으로 변환하는 방법
- 개념
- 데이터가 고차원일 경우 분석과 시각화가 어렵기 때문에 중요한 정보만 유지하면서 차원을 줄이는 것이 목표
- 고유벡터와 고유값을 이용해서 데이터의 분산을 설명하는 방향(주성분)을 찾음
- 데이터를 새로운 축(주성분)으로 변환해서 주요한 성분만 선택해 차원을 줄임
- 특징
- 데이터의 분산을 최대한 보존하는 방향으로 차원 축소
- 선형 변환 기법(비선형 관계는 학습하지 못함)
- 상관관계가 높은 변수들의 중복을 제거해서 데이터 요약이 가능
- 적용 과정
- 1. 데이터 전처리 : 평균을 0으로 만들기(표준화), 표준편차를 1로 정규화
- 2. 공분산 행렬 계산 : 변수들 간의 관계(공분산)를 측정해서 데이터 분포 확인
- 3. 고유값과 고유벡터 계산 : 공분산 행렬에서 고유값과 고유벡터를 구해서 주성분 결정
- 4. 주성분 선택 : 고유값이 큰 순서대로 주성분을 선택해서 차원 축소
- 5. 데이터 변환 : 기존 데이터를 선택한 주성분 축으로 변환
- 한계
- 선형 관계만 학습이 가능 >> 데이터가 비선형 구조람녀 커널 PCA 같은 방법이 필요
- 정보 손실 가능 >> 너무 많은 차원을 줄이면 원래 데이터의 특성을 많이 잃을 수 있음
- 데이터의 분포를 잘 파악해야 함 >> 분포가 적절하지 않으면 주성분 분석(PCA)의 성능이 떨어짐
1. 차원과 차원 축소
# 데이터가 가진 속성을 특성(feature) >> 벡터 개념(1차원 배열)에서 '차원'이라고 부르기도 함
# 과일 이미지의 100000개의 특성은 10000개의 차원을 가지고 있음
# 차원을 줄이는 방법 > 주성분 분석(데이터의 분산이 큰 방향을 찾음)
# 일반적으로 주성분은 원본의 특성 갯수만큼 찾을 수 있음
3. PCA 클래스
# 데이터 준비
import numpy as np
fruits = np.load('fruits_300.npy')
fruits_2d = fruits.reshape(-1, 100*100)
from sklearn.decomposition import PCA
pca = PCA(n_components = 50) # 주성분의 갯수를 50으로 지정
pca.fit(fruits_2d)
- fruits.reshape(-1, 100*100)
- fruits = 3차원 배열(300, 100, 100)을 2차원 배열(300, 10000)로 변환
- PCA(n_components = 50)
- 주성분 갯수를 50으로 설정해서 차원 축소
차원 축소를 통해 얻은 이미지 그리기
import matplotlib.pyplot as plt
def draw_fruits(arr, ratio = 1): # arr : 샘플 이미지 배열
n = len(arr) # n : 전체 샘플의 수
# 한줄에 10개의 이미지 그리기
rows = int(np.ceil(n/10)) # 행의 수 계산
# 행이 1개면 열의 갯수는 샘플의 갯수
cols = n if rows < 2 else 10
fig, axs = plt.subplots(rows, cols, figsize = (cols * ratio, rows * ratio), squeeze = False)
for i in range(rows):
for j in range(cols):
if i * 10 + j < n:
axs[i, j].imshow(arr[i * 10 +j], cmap = 'gray_r')
axs[i, j].axis('off')
plt.show()
- def draw_fruits(arr, ratio = 1) :
- arr : 시각화할 샘플 이미지 배열
- ratio : 개별 이미지 크기를 조정하는 비율(기본값 1)
- n = len(arr) : 배열 내 이미지 갯수 확인
- np.ceil(n / 10) : 한 줄에 최대 10개씩 배치하려면 몇 줄(rows)이 필요한지 계산
- cols : 한 줄에 10개씩 배치, 행이 1개일 경우 샘플 갯수만큼 열을 배치
- fig, axs = plt.subplots(rows, cols, figsize=(cols * ratio, rows * ratio), squeeze=False)
- plt.subplots(rows, cols) : rows x cols : 크기의 서브플롯 생성
- figsize = (cols * ratio, rows * ratio) : ratio를 이용해서 그래프 크기 조정
- squeeze = False : axs를 항상 2차원 배열 형태로 유지(1행이여도 1D 배열이 되지 않도록)
# 차원 축소로 얻은 특성을 사용해서 원본 데이터 변환하기
# transform()
fruits_pca = pca.transform(fruits_2d)
print(fruits_pca.shape)
원본 데이터 재구성
# 앞에서 원본 특성 10000개를 50개로 줄여서 손실이 발생할 수 밖에 없음
# inverse_transform() : 원본 데이터 재구성 메서드
fruits_inverse = pca.inverse_transform(fruits_pca)
print(fruits_inverse.shape)
# 100 x 100 크기로 바꿔서 100개씩 출력하기
fruits_reconstruct = fruits_inverse.reshape(-1, 100, 100)
for start in [0, 100, 200]:
draw_fruits(fruits_reconstruct[start : start + 100])
print('\n')
- start부터 start + 100까지의 이미지(100개)를 출력
- start = 0: 첫 번째 100개 출력
- start = 100: 두 번째 100개 출력
- start = 200: 세 번째 100개 출력
사과 100 > 파인애플 100 > 바나나 100
대부분 잘 복원되었음
설명된 분산
# 설명된 분산 : 주성분이 원본 데이터를 얼마나 잘 나타내는지 기록한 값
# PCA 클래스에서 explained_variance_ratio_ 속성에 저장됨
print(np.sum(pca.explained_variance_ratio_))
다른 알고리즘과 함께 사용하기
1. 로지스틱 회귀 모델 사용
from sklearn.linear_model import LogisticRegression
lr = LogisticRegression()
# 지도학습을 사용하기 위한 타깃 데이터
target = np.array([0] * 100 + [1] *100 + [2] * 100)
print(target)
# 측정 점수 평균과 측정 시간을 얻기 위해 cross_validate() 사용
from sklearn.model_selection import cross_validate
scores = cross_validate(lr, fruits_2d, target)
print('모델 평균 점수 :', np.mean(scores['test_score']))
print('평균 훈련 시간 :', np.mean(scores['fit_time']))
99.67%의 정확도 / 약 0.6
PCA를 적용한 축소 데이터 fruits_pca와 비교하기
scores = cross_validate(lr, fruits_pca, target)
print('모델 평균 점수 :', np.mean(scores['test_score']))
print('평균 훈련 시간 :', np.mean(scores['fit_time']))
100%의 정확도 / 훈련시간 0.02초
>> 차원 축소로 저장 공간도 줄어들고, 훈련 속도도 높아짐
# PCA 실행 시 n_components의 값은 특성 값 대신 설명된 분산 비율을 입력할 수 있다
pca = PCA(n_components = 0.5)
pca.fit(fruits_2d)
# 교차 검증
scores = cross_validate(lr, fruits_2d, target)
print('모델 평균 점수 :', np.mean(scores['test_score']))
print('평균 훈련 시간 :', np.mean(scores['fit_time']))
2. k-평균 알고리즘 사용하기
KMeans : 데이터 포인트를 k개의 클러스터로 그룹화하는 비지도 학습 알고리즘
from sklearn.cluster import KMeans
km = KMeans(n_clusters = 3, random_state = 42)
km.fit(fruits_2d)
print(np.unique(km.labels_, return_counts = True))
- n_clusters = 3 : 데이터를 3개의 군집으로 분류
- random_state = 42 : 결과 재현성을 위한 랜덤 시드 고정
- 클러스터 별 샘플 개수
- km.labels_ : 각 데이터가 어느 클러스터(0, 1, 2)로 배정되었는지
- np.unique(km.labels_, return_counts = True) : 각 클러스터에 속한 데이터 갯수 반환
클러스터 0 : 111개
클러스터 1 : 98개
클러스터 2 : 91개
for label in range(0, 3):
draw_fruits(fruits[km.labels_ == label])
print("\n")
>> 파인애플이 사과와 혼돈됨
# 주성분이 2개 > 산점도를 사용해서 그래프를 그릴 수 있음
for label in range(0, 3):
data = fruits_pca[km.labels_ == label]
plt.scatter(data[:, 0], data[:, 1])
plt.legend(['apple', 'pineapple', 'banana'])
plt.show()
- 주성분 분석
- fruits_pca : PCA 변환 후의 데이터(2차원)
- fruits_pca.shape = (샘플 개수, 2)
- km.labels_ == label : 해당 클러스터(label)에 속하는 데이터만 선택
- data[:, 0], data[:, 1] : X축과 Y축에 해당하는 주성분 값
마무리
- 비지도학습(Unsupservised learning) : 정답(label) 없이 데이터를 학습하여 패턴을 찾아내는 방식
- 군집 알고리즘(Clustering) : 비슷한 특성을 가진 데이터끼리 묶는 방법
- KMeans 클러스터링
- 데이터를 k개의 그룹(클러스터)으로 나눔
- 클러스터 중심을 반복적으로 이동시켜 최적의 군집 형성
- 계층적 클러스터링
- 데이터 간의 거리 기반으로 계층적 구조 형성
- KMeans 클러스터링
- KMeans 알고리즘
- 동작 과정
- k개의 초기 중심 선택 > 각 데이터 포인트를 가장 가까운 중심에 할당 > 각 클러스터의 평균값을 계산하여 중심이동 > 중심이 변하지 않을 때까지 반복
- k 값 선택
- intertia_ 를 이용해서 최적의 k값 결정 가능
- 그래프상 급격히 줄어드는(꺾이는) 지점
- 주성분 분석 : 고차원 데이터를 저차원으로 변환하는 차원 축소 기법으로 데이터의 정보 손실을 최소화하면서 중요한 특징을 추출할 수 있음
- 동작 과정
- 데이터의 공분산 행렬 계산 > 고유값과 고유벡터 계산 > 주성분 선택 > 기존 데이터를 새로운 축으로 변환
- n_components=2 로 지정해서 2차원으로 차원 축소
- 데이터를 산점도(scatter plot)로 시각화
- 동작 과정
- 동작 과정
'Python' 카테고리의 다른 글
[Python] 네이버 웹툰 데이터 분석 (0) | 2025.03.20 |
---|---|
[Python] 신경망 모델 훈련 (0) | 2025.03.14 |
[Python] 트리 앙상블 (0) | 2025.03.11 |
[Python] 머신러닝 (3) (0) | 2025.03.10 |
[Python] 머신러닝 (2) (0) | 2025.03.08 |