Python

[Python] 비지도 학습

orin602 2025. 3. 13. 16:20

비지도 학습(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 알고리즘
    • 동작 과정
      • 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