인공지능/생성형 AI

비지도 학습

mino28 2025. 8. 21. 09:29

1. 비지도 학습

비지도 학습(Unsupervised Learning)은 정답(label)이 주어지지 않은 데이터를 기반으로 데이터의 구조, 패턴, 분포 등을 스스로 학습하는 방법입니다. 즉, 입력 데이터만 가지고 유사한 데이터끼리 그룹화하거나(클러스터링), 데이터를 더 작은 차원으로 축소(차원 축소)하거나, 중요한 특징을 추출하는 것이 목표입니다. 대표적인 알고리즘으로는 K-means, PCA, Autoencoder, GAN 등이 있으며, 비지도 학습은 레이블링 비용이 많이 드는 현실적인 문제를 해결하고, 데이터에 대한 통찰을 얻는 데 유용하게 활용됩니다.

 

대표적인 비지도 학습 알고리즘

 

2. 클러스터링

클러스터링(Clustering)은 비지도 학습의 대표적인 기법으로, 정답(label) 없이 주어진 데이터를 유사한 특성을 가진 그룹(클러스터)으로 자동으로 나누는 방법입니다. 데이터 간의 거리나 밀도, 연결 구조 등을 기준으로 군집을 형성하며, 대표적인 알고리즘으로는 K-means, DBSCAN, 계층적 군집(Hierarchical Clustering) 등이 있습니다. 클러스터링은 고객 세분화, 문서 분류, 이미지 분할, 이상치 탐지 등 다양한 분야에 활용되며, 데이터를 시각적으로 이해하거나 패턴을 탐색할 때 매우 유용한 도구입니다.

 

1. k-means

K-Means는 가장 널리 사용되는 클러스터링 알고리즘 중 하나로, 주어진 데이터를 K개의 클러스터로 나누는 비지도 학습 기법입니다. 알고리즘은 먼저 무작위로 K개의 중심점을 선택한 후, 각 데이터를 가장 가까운 중심점에 할당하고, 각 클러스터의 중심을 다시 계산하는 과정을 반복합니다. 이 과정을 통해 중심점이 더 이상 크게 이동하지 않을 때까지 수렴하며, 최종적으로 데이터는 유사한 특성을 가진 K개의 그룹으로 분류됩니다. K-Means는 계산이 빠르고 구현이 간단하지만, 클러스터 수(K)를 사전에 정해야 하며, 복잡한 형태나 밀도 차이가 큰 데이터에는 적합하지 않을 수 있습니다.

 

1. "몇 개의 그룹으로 나눌 것인지" 결정해야 합니다. 예를 들어 K=2이라면, 2개의 클러스터로 나누게 됩니다.

2. 데이터 공간에서 K개의 중심점을 무작위로 뽑습니다. 이 중심점은 클러스터의 대표값 역할을 합니다.

3. 모든 데이터 포인트에 대해 각 중심점과의 거리를 계산하고, 가장 가까운 중심점에 소속되도록 할당합니다. 이로써 K개의 그룹이 만들어집니다.

4. 각 클러스터에 속한 데이터들의 평균값을 계산해서 중심점을 새로 위치시킵니다. 즉, 중심점이 데이터 분포의 중심으로 이동합니다.

5. 데이터의 소속 그룹이 바뀌지 않을 때까지, 또는 중심점의 이동이 거의 없을 때까지 반복합니다. 이를 수렴(convergence)이라고 합니다.

 

from sklearn.datasets import make_blobs

X, y = make_blobs(n_samples=100, centers=3, random_state=2025)
print(X)
print(y)

 

X = pd.DataFrame(X)
X

 

sns.scatterplot(x=X[0], y=X[1], hue=y)

 

from sklearn.cluster import KMeans

km = KMeans(n_clusters=3)
km.fit(X)
pred = km.predict(X)

sns.scatterplot(x=X[0], y=X[1], hue=pred)

 

km = KMeans(n_clusters=5)
km.fit(X)
pred = km.predict(X)

sns.scatterplot(x=X[0], y=X[1], hue=pred)

 

km.inertia_

 

inertia_는 각 데이터 포인트와 자신이 속한 클러스터 중심점(centroid) 사이의 거리 제곱합(Sum of Squared Distances)입니다. 즉, 클러스터 내에서 데이터들이 중심점에 얼마나 가까이 모여 있는지를 수치로 나타냅니다.

 

inertia_list = []

for i in range(2, 11):
    km = KMeans(n_clusters=i)
    km.fit(X)
    inertia_list.append(km.inertia_)
    
inertia_list

 

sns.lineplot(x=range(2, 11), y=inertia_list)

 

엘보우(Elbow) 메서드는 클러스터링에서 최적의 클러스터 수(K)를 찾기 위한 직관적인 방법입니다. 이 방법은 K를 1부터 점차 늘려가며 각 K 값에 대한 클러스터 내 거리 제곱합(inertia_)을 계산하고, 이를 그래프로 나타냅니다. K가 증가할수록 inertia 값은 감소하지만, 어느 순간부터 감소 속도가 완만해지는데, 이때 그래프가 팔꿈치(elbow)처럼 꺾이는 지점을 최적의 K로 선택합니다. 이 지점은 모델이 충분히 군집을 잘 나누되, 과도하게 세분화하지 않는 균형점으로 간주됩니다.

 

2. 계층적 군집

계층적 군집(Hierarchical Clustering)은 데이터 간의 유사도를 기준으로 계층 구조의 트리(dendrogram)를 생성하며, 이를 통해 데이터를 점진적으로 군집화하는 비지도 학습 기법입니다. 크게 병합형(agglomerative)과 분할형(divisive)으로 나뉘며, 병합형은 각 데이터를 개별 클러스터로 시작해 가장 유사한 것부터 합쳐가고, 분할형은 전체 데이터를 하나의 클러스터로 시작해 점차 분리해 나갑니다. 이 알고리즘은 클러스터 수를 사전에 정하지 않아도 되며, 덴드로그램을 통해 적절한 군집 수를 시각적으로 판단할 수 있는 장점이 있습니다. 하지만 계산 복잡도가 높아 대용량 데이터에는 다소 비효율적일 수 있습니다.

 

 

병합형 계층적 군집의 예

1. 처음에는 N개의 데이터가 있으면 N개의 클러스터로 간주합니다.

2. 데이터 간 거리(혹은 클러스터 간 거리)를 계산해서 가장 가까운 두 개를 선택해 합칩니다.

3. 두 클러스터가 합쳐졌기 때문에, 새로 생긴 클러스터와 나머지 클러스터들과의 거리를 다시 계산합니다.

4. Step 2로 돌아가 계속 합칩니다.

5. 모든 데이터가 하나의 클러스터가 될 때까지 반복

 

import torch
import numpy as np
from torchvision.transforms import v2
from torchvision.datasets import MNIST

transforms = v2.Compose([
    v2.ToImage(),
    v2.Lambda(torch.flatten),
])

flatten_mnist = MNIST(".", train=False, download=True, transform=transforms)

 

np.random.seed(2025)
mnist_indices = np.random.choice(len(flatten_mnist), 30, replace=False)

flatten_X = np.array([flatten_mnist[idx][0].numpy() for idx in mnist_indices])
flatten_y = np.array([flatten_mnist[idx][1] for idx in mnist_indices])

 

from sklearn.cluster import AgglomerativeClustering

model = AgglomerativeClustering(compute_distances=True)
model = model.fit(flatten_X)

 

from matplotlib import pyplot as plt
from scipy.cluster.hierarchy import dendrogram

def plot_dendrogram(model, labels):
    counts = np.zeros(model.children_.shape[0])
    n_samples = len(model.labels_)
    for i, merge in enumerate(model.children_):
        current_count = 0
        for child_idx in merge:
            if child_idx < n_samples:
                current_count += 1  # leaf node
            else:
                current_count += counts[child_idx - n_samples]
        counts[i] = current_count

    linkage_matrix = np.column_stack(
        [model.children_, model.distances_, counts]
    ).astype(float)

    dendrogram(linkage_matrix, labels=labels, truncate_mode=None, distance_sort='descending')

 

plt.figure(figsize=(20, 8))
plt.title("MNIST Clustering Dendrogram")
plot_dendrogram(model, flatten_y)
plt.show()

 

from torchvision.datasets import CIFAR10
flatten_cifar10 = CIFAR10(".", train=False, download=True, transform=transforms)

cifar10_indices = np.random.choice(len(flatten_cifar10), 30, replace=False)

flatten_X = np.array([flatten_cifar10[idx][0].numpy() for idx in cifar10_indices])
flatten_y = np.array([flatten_cifar10[idx][1] for idx in cifar10_indices])

model = AgglomerativeClustering(compute_distances=True)
model = model.fit(flatten_X)

plt.figure(figsize=(20, 8))
plt.title("CIFAR10 Clustering Dendrogram")
plot_dendrogram(model, flatten_y)
plt.show()

 

transforms = v2.Compose([
    v2.ToImage(),
    v2.ToDtype(torch.float32, scale=True),
    v2.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
normal_cifar10 = CIFAR10(".", train=False, download=True, transform=transforms)

normal_X = torch.stack([normal_cifar10[idx][0] for idx in cifar10_indices])
normal_y = np.array([normal_cifar10[idx][1] for idx in cifar10_indices])

 

import torch.nn as nn
from torchvision.models import resnet18, ResNet18_Weights

feature_extractor = resnet18(weights=ResNet18_Weights.DEFAULT)
feature_extractor.fc = nn.Identity()

 

feature_X = feature_extractor(normal_X).detach().numpy()

 

model = AgglomerativeClustering(compute_distances=True)
model = model.fit(feature_X)

plt.figure(figsize=(20, 8))
plt.title("CIFAR10 Clustering Dendrogram")
plot_dendrogram(model, flatten_y)
plt.show()

 

 

3. 차원 축소

1. 차원의 저주

차원의 저주(Curse of Dimensionality)란 데이터의 차원이 증가할수록 공간의 부피가 기하급수적으로 커져서, 기존에 효과적이던 알고리즘이나 분석 방법들이 성능이 급격히 떨어지는 현상을 말합니다. 고차원에서는 데이터가 희소하게 분포되며, 거리 기반 알고리즘(예: KNN, 클러스터링 등)은 모든 점 사이의 거리가 비슷해지는 문제를 겪게 되고, 학습에 필요한 데이터 양도 급격히 증가합니다. 따라서 차원이 너무 높아지면 오히려 예측 정확도나 일반화 성능이 나빠질 수 있어, 이를 해결하기 위해 차원 축소 기법(PCA, t-SNE 등)이 자주 사용됩니다.

경우의 수에 반해 실제 갖고 있는 데이터의 수가 너무 적음

 

 

 

2. 차원 축소

차원 축소(Dimensionality Reduction)는 고차원 데이터를 더 낮은 차원의 공간으로 변환하여 데이터의 핵심 구조나 패턴을 유지하면서 불필요한 정보나 노이즈를 제거하는 과정입니다. 주로 데이터의 시각화, 계산 효율성 향상, 과적합 방지 등을 목적으로 사용되며, 대표적인 기법으로는 PCA(주성분 분석), t-SNE, UMAP 등이 있습니다. 차원 축소는 수백 개의 변수로 구성된 복잡한 데이터를 2차원이나 3차원으로 압축해도 주요 정보를 유지할 수 있도록 도와주며, 특히 데이터 간의 유사성이나 군집 구조를 직관적으로 파악하는 데 유용한 도구입니다.

 

 

4. PCA

PCA(주성분 분석, Principal Component Analysis)는 고차원 데이터를 보다 낮은 차원으로 변환하면서도, 데이터의 분산(정보)을 최대한 보존하는 차원 축소 기법입니다. PCA는 원본 데이터에서 상관관계를 분석해 가장 큰 분산을 가지는 방향(주성분)을 찾고, 이 방향을 기준으로 데이터를 재투영하여 주요 특성만 남깁니다. 이를 통해 노이즈를 줄이고 계산 효율을 높이며, 시각화나 전처리에 유용하게 활용됩니다. PCA는 선형 변환 기법이며, 각 주성분은 서로 직교(orthogonal)합니다.

 

 

 

5. 매니폴드 가정(Manifold Hypothesis)

매니폴드 가정(Manifold Assumption)은 고차원 데이터가 실제로는 훨씬 더 낮은 차원의 매끄러운 곡면(매니폴드) 위에 놓여 있다고 보는 가정입니다. 즉, 데이터는 전체 고차원 공간을 가득 채우는 것이 아니라, 저차원 구조를 따라 분포한다는 의미입니다. 이 가정은 차원 축소 기법(PCA, t-SNE, Isomap 등)이나 딥러닝에서의 표현 학습에서 핵심적인 이론적 기반이 되며, 복잡해 보이는 데이터도 저차원에서의 규칙성과 구조를 통해 더 잘 이해하고 처리할 수 있다는 통찰을 제공합니다.

 

 

6. t-SNE

t-SNE(t-Distributed Stochastic Neighbor Embedding)는 고차원 데이터를 2차원이나 3차원으로 줄여 시각화하는 데 자주 쓰이는 차원 축소 기법입니다. 이 방법은 매니폴드 가정(manifold assumption)에 기반하여, 고차원 데이터가 사실은 저차원 매니폴드 위에 놓여 있다고 보고 국소적인 구조(근접한 점들의 관계)를 잘 보존하도록 합니다. 구체적으로는 고차원 공간에서 이웃한 점들이 가질 확률 분포와 저차원 공간에서의 확률 분포가 비슷해지도록 최적화하며, 특히 t-분포를 사용해 군집 사이의 거리를 더 넓게 벌려주는 효과를 냅니다. 이 덕분에 t-SNE는 복잡한 데이터의 잠재적인 패턴을 시각적으로 잘 드러낼 수 있습니다.

고차원에서의 분포와 저차원에서의 분포가 비슷해지도록 데이터의 차원을 변환

 

t-분포를 사용하는 이유

  • 정규분포(가우시안): 가운데 뾰족하고, 멀리 가면 값이 너무 빨리 작아짐 → 멀리 떨어진 점들을 잘 구별 못함
  • t-분포: 가운데는 정규랑 비슷하지만, 꼬리가 두꺼움 → 멀리 떨어진 점도 “완전히 0” 취급하지 않고, 적당히 떨어져 있다고 표현 가능

 

 

7. UMAP

UMAP(Uniform Manifold Approximation and Projection)은 고차원 데이터를 저차원 공간으로 효율적으로 축소하는 비선형 차원 축소 기법으로, t-SNE보다 속도가 빠르고 전체 구조와 국소 구조를 모두 잘 보존하는 것이 특징입니다. UMAP은 매니폴드 가정과 위상 공간 이론을 바탕으로, 고차원 공간에서의 이웃 관계를 저차원에서도 유지하려고 하며, 군집의 형태나 거리, 밀도 정보까지 어느 정도 보존합니다. 따라서 시각화뿐만 아니라, 클러스터링 전처리, 특징 추출, 노이즈 제거 등 다양한 머신러닝 작업에 활용됩니다.

 

최근 연구에서는 t-SNE와 UMAP의 초기값 설정(initialization)이 시각화 품질과 신뢰도에 큰 영향을 미친다는 의견이 많아지고 있습니다. 특히 t-SNE에서는 초기값을 PCA로 설정할 경우, 랜덤 초기화보다 더 안정적인 구조 보존과 재현성이 높아지는 것으로 보고되고 있으며, UMAP에서도 PCA 기반 초기화가 더 나은 전역 구조를 유지한다는 결과가 나타나고 있습니다. 이러한 논의는 차원 축소 결과에 대한 해석의 신뢰성을 높이기 위한 중요한 고려 요소로, 단순히 알고리즘만 선택하는 것이 아니라 초기화 방법까지 설계에 포함해야 한다는 인식이 확산되고 있습니다.

 

 

 

import numpy as np
from umap import UMAP
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE
from torchvision.datasets import MNIST, CIFAR10

mnist = MNIST(root='.', train=True, download=True)
cifar10 = CIFAR10(root='.', train=True, download=True)

 

def convert_sklearn_dataset(pytorch_dataset):
  X, y = [], []
  for image, label in pytorch_dataset:
    x = np.array(image)
    x = x / 255
    X.append(x)
    y.append(label)
  X = np.array(X)
  X = X.reshape(len(X), -1)
  y = np.array(y)
  return X, y

 

mnist_X, mnist_y = convert_sklearn_dataset(mnist)
cifar10_X, cifar10_y = convert_sklearn_dataset(cifar10)

 

# 퍼플렉서티 = 유효 이웃 수. 작게 잡으면 국소 구조 강조, 크게 잡으면 전역 구조 반영이며, 
# 데이터 수와 구조에 맞춰 5–50(혹은 100) 사이에서 몇 개 값을 시험해 가장 안정적이고 해석 가능한 시각화를 고르는 것이 가장 안전
tsne_random = TSNE(n_components=2, perplexity=200, init="random", random_state=2025)
tsne_pca = TSNE(n_components=2, perplexity=200, init="pca", random_state=2025)

np.random.seed(2025)
mnist_idx = np.random.choice(len(mnist_X), 1000, replace=False)
cifar10_idx = np.random.choice(len(cifar10_X), 1000, replace=False)

 

def plot_embedding(model, X, y, idx):
    X_set = X[idx]
    y_set = y[idx]

    X_set = model.fit_transform(X_set)
    class_names = set(y_set)

    for i, class_name in enumerate(class_names):
        plt.scatter(
            X_set[y_set == class_name, 0],
            X_set[y_set == class_name, 1],
            color=plt.cm.tab10(i),
            label=class_name,
        )
    plt.xlabel('component 0')
    plt.ylabel('component 1')
    plt.legend()
    plt.show()

 

plot_embedding(tsne_random, mnist_X, mnist_y, mnist_idx)

 

plot_embedding(tsne_pca, mnist_X, mnist_y, mnist_idx)

 

plot_embedding(tsne_random, cifar10_X, cifar10_y, cifar10_idx)

 

plot_embedding(tsne_pca, cifar10_X, cifar10_y, cifar10_idx)

 

!pip install umap-learn

 

umap = UMAP(n_components=2, min_dist=.05, n_neighbors=8, random_state=2025)

plot_embedding(umap, mnist_X, mnist_y, mnist_idx)

 

plot_embedding(umap, cifar10_X, cifar10_y, cifar10_idx)

 

import torch.nn as nn
from torchvision.models import resnet18, ResNet18_Weights

feature_extractor = resnet18(weights=ResNet18_Weights.DEFAULT)
feature_extractor.fc = nn.Identity()

 

import torch
from torchvision.transforms import v2

transforms = v2.Compose([
    v2.ToImage(),
    v2.Resize(size=(96, 96), antialias=True),
    v2.ToDtype(torch.float32, scale=True),
    v2.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
cifar10_imagenet = CIFAR10(root='.', train=True, download=True, transform=transforms)

 

cifar10_imagenet_X = torch.stack([cifar10_imagenet[idx][0] for idx in cifar10_idx])
cifar10_imagenet_y = np.array([cifar10_imagenet[idx][1] for idx in cifar10_idx])
feature_X = feature_extractor(cifar10_imagenet_X).detach().numpy()

 

plot_embedding(tsne_pca, feature_X, cifar10_imagenet_y, list(range(len(cifar10_imagenet_y))))

 

!pip install medmnist

 

import medmnist
print(f"MedMNIST v{medmnist.__version__}")

 

from medmnist import INFO

pathmnist_info = INFO['pathmnist']
DataClass = getattr(medmnist, pathmnist_info['python_class'])

 

pathmnist = DataClass(split='train', download=True)
pathmnist.montage(length=20)

 

pathmnist_X, pathmnist_y = convert_sklearn_dataset(pathmnist)
pathmnist_y = pathmnist_y[:, 0]

 

pathmnist_idx = np.random.choice(len(pathmnist_X), 1000, replace=False)

plot_embedding(tsne_random, pathmnist_X, pathmnist_y, pathmnist_idx)

 

plot_embedding(tsne_pca, pathmnist_X, pathmnist_y, pathmnist_idx)

 

pathmnist_imagenet = DataClass(split='train', download=True, transform=transforms)
pathmnist_imagenet_X = torch.stack([pathmnist_imagenet[idx][0] for idx in pathmnist_idx])
pathmnist_imagenet_y = np.array([pathmnist_imagenet[idx][1] for idx in pathmnist_idx])[:, 0]
feature_X = feature_extractor(pathmnist_imagenet_X).detach().numpy()

 

plot_embedding(tsne_pca, feature_X, pathmnist_imagenet_y, list(range(len(pathmnist_imagenet_y))))
 

'인공지능 > 생성형 AI' 카테고리의 다른 글

GAN  (4) 2025.08.21
오토인코더  (1) 2025.08.21