CNN (컨볼루션 신경망) 이해를 위한 파이썬 예제 (MNIST 손글씨 숫자 분류)

CNN (컨볼루션 신경망) 이해를 위한 파이썬 예제 (MNIST 손글씨 숫자 분류)

CNN(Convolutional Neural Network, 컨볼루션 신경망)은 이미지 인식, 객체 탐지 등 컴퓨터 비전 분야에서 뛰어난 성능을 보이는 딥러닝 모델입니다. 이미지의 공간적 특징(spatial features)을 효과적으로 학습하는 데 특화되어 있습니다.

가장 대표적이고 이해하기 쉬운 예제 중 하나는 MNIST 손글씨 숫자 데이터셋을 분류하는 것입니다. MNIST 데이터셋은 0부터 9까지의 손으로 쓴 숫자 이미지로 구성되어 있습니다.

이 예제에서는 Python과 TensorFlow/Keras 라이브러리를 사용하여 간단한 CNN 모델을 구축하고 MNIST 데이터셋을 학습시켜 손글씨 숫자를 분류하는 과정을 보여줍니다.

핵심 개념:

  1. 컨볼루션(Convolution) 계층 (Conv2D):
    • 이미지 위를 작은 필터(커널)가 이동하면서 합성곱 연산을 수행합니다.
    • 필터는 이미지의 특정 패턴(예: 선, 코너, 질감)을 감지하는 역할을 합니다.
    • 이 과정을 통해 이미지의 지역적 특징(local features)을 추출합니다.
  2. 활성화 함수(Activation Function, 예: ReLU):
    • 컨볼루션 연산 결과에 비선형성을 추가합니다. 이를 통해 모델이 더 복잡한 패턴을 학습할 수 있게 합니다.
    • ReLU(Rectified Linear Unit)는 가장 널리 사용되는 활성화 함수 중 하나입니다.
  3. 풀링(Pooling) 계층 (MaxPooling2D):
    • 특징 맵(feature map)의 크기를 줄여 계산량을 감소시키고, 주요 특징만 남겨 약간의 위치 변화에도 모델이 덜 민감하게 만듭니다(translation invariance).
    • Max Pooling은 특정 영역에서 가장 큰 값만 선택하여 가져옵니다.
  4. 플래튼(Flatten) 계층:
    • 다차원 특징 맵을 1차원 벡터로 변환하여 완전 연결 계층(Dense layer)에 입력할 수 있도록 준비합니다.
  5. 완전 연결(Dense) 계층:
    • 일반적인 인공 신경망 계층입니다. 추출된 특징들을 바탕으로 최종적인 분류를 수행합니다.
    • 마지막 Dense 계층은 분류할 클래스 수(MNIST의 경우 10개)만큼의 뉴런을 가지며, 'softmax' 활성화 함수를 사용하여 각 클래스에 대한 확률을 출력합니다.

샘플 코드:

import os
import urllib.request
import gzip
import shutil
import numpy as np
import matplotlib.pyplot as plt
from mnist import MNIST  # pip install python-mnist

# MNIST 데이터 자동 다운로드 및 압축 해제
def download_mnist():
    base_url = "https://ossci-datasets.s3.amazonaws.com/mnist/"
    files = [
        "train-images-idx3-ubyte.gz",
        "train-labels-idx1-ubyte.gz",
        "t10k-images-idx3-ubyte.gz",
        "t10k-labels-idx1-ubyte.gz"
    ]
    os.makedirs("./mnist_data", exist_ok=True)
    for file in files:
        gz_path = f"./mnist_data/{file}"
        raw_path = gz_path[:-3]
        if not os.path.exists(raw_path):
            if not os.path.exists(gz_path):
                print(f"Downloading {file}...")
                urllib.request.urlretrieve(base_url + file, gz_path)
            print(f"Extracting {file}...")
            with gzip.open(gz_path, "rb") as f_in, open(raw_path, "wb") as f_out:
                shutil.copyfileobj(f_in, f_out)

if __name__ == "__main__" or True:
    download_mnist()

# 간단한 신경망 모델 구현
class SimpleNetwork:
    def __init__(self, input_size, hidden_size, output_size):
        # 가중치 초기화
        self.W1 = 0.01 * np.random.randn(input_size, hidden_size)
        self.b1 = np.zeros(hidden_size)
        self.W2 = 0.01 * np.random.randn(hidden_size, output_size)
        self.b2 = np.zeros(output_size)

    def sigmoid(self, x):
        return 1 / (1 + np.exp(-x))

    def softmax(self, x):
        x = x - np.max(x, axis=1, keepdims=True)  # 오버플로우 방지
        return np.exp(x) / np.sum(np.exp(x), axis=1, keepdims=True)

    def forward(self, x):
        # 순전파
        self.a1 = np.dot(x, self.W1) + self.b1
        self.z1 = self.sigmoid(self.a1)
        self.a2 = np.dot(self.z1, self.W2) + self.b2
        self.y = self.softmax(self.a2)

        return self.y

    def loss(self, x, t):
        # 교차 엔트로피 오차 계산
        y = self.forward(x)

        # 원-핫 인코딩 된 레이블로 변환
        t_one_hot = np.zeros((t.size, 10))
        t_one_hot[np.arange(t.size), t] = 1

        batch_size = y.shape[0]
        return -np.sum(t_one_hot * np.log(y + 1e-7)) / batch_size

    def gradient(self, x, t):
        # 역전파로 기울기 계산
        batch_size = x.shape[0]

        # 순전파
        self.forward(x)

        # 원-핫 인코딩 된 레이블로 변환
        t_one_hot = np.zeros((t.size, 10))
        t_one_hot[np.arange(t.size), t] = 1

        # 출력층 오차
        dy = (self.y - t_one_hot) / batch_size

        # 가중치와 편향에 대한 기울기
        dW2 = np.dot(self.z1.T, dy)
        db2 = np.sum(dy, axis=0)

        # 은닉층 오차
        dz1 = np.dot(dy, self.W2.T)
        da1 = dz1 * (self.z1 * (1 - self.z1))  # 시그모이드 미분

        dW1 = np.dot(x.T, da1)
        db1 = np.sum(da1, axis=0)

        return {
            'W1': dW1, 'b1': db1,
            'W2': dW2, 'b2': db2
        }

    def predict(self, x):
        y = self.forward(x)
        return np.argmax(y, axis=1)

    def accuracy(self, x, t):
        y = self.predict(x)
        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy

mndata = MNIST('./mnist_data')
x_train, t_train = mndata.load_training()
x_test, t_test = mndata.load_testing()

# 데이터 전처리
x_train = np.array(x_train).astype(np.float32) / 255.0
x_test = np.array(x_test).astype(np.float32) / 255.0
t_train = np.array(t_train)
t_test = np.array(t_test)

# 이미지 시각화
def img_show(img):
    plt.imshow(img.reshape(28, 28), cmap='gray')
    plt.axis('off')
    plt.show()

# 첫 번째 훈련 이미지 보기
# print(f"첫 번째 이미지의 레이블: {t_train[0]}")
# img_show(x_train[0])

# 네트워크 초기화
network = SimpleNetwork(input_size=784, hidden_size=50, output_size=10)

# 하이퍼파라미터 설정
learning_rate = 0.1
batch_size = 100
epochs = 10
iterations_per_epoch = max(x_train.shape[0] // batch_size, 1)

# 학습 데이터와 테스트 데이터의 정확도 기록
train_acc_list = []
test_acc_list = []

# 학습 시작
print("모델 학습 시작...")
for epoch in range(epochs):
    # 학습 데이터를 섞기
    indices = np.random.permutation(x_train.shape[0])
    x_train_shuffled = x_train[indices]
    t_train_shuffled = t_train[indices]

    for i in range(iterations_per_epoch):
        batch_mask = np.random.choice(x_train_shuffled.shape[0], batch_size)
        x_batch = x_train_shuffled[batch_mask]
        t_batch = t_train_shuffled[batch_mask]

        # 기울기 계산
        grad = network.gradient(x_batch, t_batch)

        # 매개변수 갱신
        for key in ('W1', 'b1', 'W2', 'b2'):
            network.__dict__[key] -= learning_rate * grad[key]

    # 정확도 계산 (학습 데이터, 테스트 데이터)
    train_acc = network.accuracy(x_train, t_train)
    test_acc = network.accuracy(x_test, t_test)
    train_acc_list.append(train_acc)
    test_acc_list.append(test_acc)

    print(f"에폭 {epoch+1}: 훈련 정확도 {train_acc:.4f}, 테스트 정확도 {test_acc:.4f}")

# 학습 과정 시각화
plt.figure(figsize=(8, 6))
plt.plot(range(1, epochs+1), train_acc_list, label='Train')
plt.plot(range(1, epochs+1), test_acc_list, label='Test')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.title('Accuracy on MNIST Dataset')
plt.legend()
plt.grid()
plt.show()

# 테스트 이미지 예측
def visualize_prediction(index):
    img = x_test[index]
    label = t_test[index]
    pred = network.predict(img.reshape(1, -1))[0]

    print(f"실제 레이블: {label}, 예측 레이블: {pred}")
    img_show(img)

# 랜덤하게 10개의 테스트 이미지 예측 결과 확인
plt.figure(figsize=(15, 8))
for i in range(10):
    plt.subplot(2, 5, i+1)
    idx = np.random.randint(0, len(x_test))
    img = x_test[idx]
    label = t_test[idx]
    pred = network.predict(img.reshape(1, -1))[0]

    plt.imshow(img.reshape(28, 28), cmap='gray')
    plt.title(f"Label: {label}, Pred: {pred}")
    plt.axis('off')
plt.tight_layout()
plt.show()

print("모델 학습 및 테스트 완료!")

코드 설명:

  1. 데이터 로드 및 전처리:
    • keras.datasets.mnist.load_data(): MNIST 데이터셋을 로드합니다.
    • astype("float32") / 255.0: 이미지 픽셀 값을 0~1 사이의 실수로 정규화합니다.
    • np.expand_dims(..., -1): 흑백 이미지(28x28)에 채널 차원(1)을 추가하여 (28, 28, 1) 형태로 만듭니다. CNN은 채널 정보를 포함한 입력을 기대합니다.
    • keras.utils.to_categorical: 숫자 레이블(0~9)을 분류 문제에 적합한 원-핫 인코딩 벡터로 변환합니다.
  2. CNN 모델 구축:
    • keras.Sequential: 레이어를 순차적으로 쌓아 모델을 만듭니다.
    • keras.Input: 모델의 입력 형태를 정의합니다.
    • layers.Conv2D: 컨볼루션 계층. filters는 사용할 필터(커널)의 개수, kernel_size는 필터의 크기, activation='relu'는 ReLU 활성화 함수 사용을 의미합니다.
    • layers.MaxPooling2D: 맥스 풀링 계층. pool_size는 풀링 윈도우의 크기입니다.
    • layers.Flatten: 다차원 특징 맵을 1차원 벡터로 펼칩니다.
    • layers.Dropout: 학습 중 일부 뉴런을 무작위로 비활성화하여 과적합(overfitting)을 방지합니다.
    • layers.Dense: 완전 연결 계층. 마지막 계층은 클래스 수(num_classes)만큼의 뉴런과 softmax 활성화 함수를 사용하여 각 클래스에 대한 확률을 출력합니다.
    • model.summary(): 생성된 모델의 구조(각 계층의 이름, 출력 형태, 파라미터 수)를 보여줍니다.
  3. 모델 컴파일:
    • model.compile(): 모델의 학습 과정을 설정합니다.
    • loss='categorical_crossentropy': 다중 클래스 분류 문제에서 사용하는 손실 함수 (원-핫 인코딩된 레이블과 함께 사용).
    • optimizer='adam': 경사 하강법 기반의 효율적인 옵티마이저.
    • metrics=['accuracy']: 학습 및 평가 과정에서 정확도를 측정합니다.
  4. 모델 학습:
    • model.fit(): 모델을 학습시킵니다.
    • x_train, y_train: 학습 데이터와 레이블.
    • batch_size: 한 번의 가중치 업데이트에 사용할 샘플의 수.
    • epochs: 전체 학습 데이터셋을 몇 번 반복하여 학습할지 결정.
    • validation_split=0.1: 학습 데이터의 일부(여기서는 10%)를 검증 세트로 사용하여 각 에포크마다 모델 성능을 평가합니다.
  5. 모델 평가:
    • model.evaluate(): 학습된 모델을 테스트 데이터(x_test, y_test)로 평가하여 최종 성능(손실 및 정확도)을 측정합니다.
  6. 예측 결과 시각화:
    • model.predict(): 테스트 데이터에 대한 모델의 예측 결과를 얻습니다. (각 클래스에 대한 확률 벡터 형태)
    • matplotlib을 사용하여 실제 이미지, 예측된 레이블, 실제 레이블을 함께 보여줍니다. 예측이 틀린 경우 빨간색으로 표시합니다.
  7. 학습 과정 시각화:
    • history 객체에는 학습 중 각 에포크별 정확도와 손실 값이 저장되어 있습니다. 이를 시각화하여 모델 학습이 잘 진행되었는지, 과적합이 발생하는지 등을 확인할 수 있습니다.

Leave a Comment