CNN (컨볼루션 신경망) 이해를 위한 파이썬 예제 (MNIST 손글씨 숫자 분류)
CNN(Convolutional Neural Network, 컨볼루션 신경망)은 이미지 인식, 객체 탐지 등 컴퓨터 비전 분야에서 뛰어난 성능을 보이는 딥러닝 모델입니다. 이미지의 공간적 특징(spatial features)을 효과적으로 학습하는 데 특화되어 있습니다.
가장 대표적이고 이해하기 쉬운 예제 중 하나는 MNIST 손글씨 숫자 데이터셋을 분류하는 것입니다. MNIST 데이터셋은 0부터 9까지의 손으로 쓴 숫자 이미지로 구성되어 있습니다.
이 예제에서는 Python과 TensorFlow/Keras 라이브러리를 사용하여 간단한 CNN 모델을 구축하고 MNIST 데이터셋을 학습시켜 손글씨 숫자를 분류하는 과정을 보여줍니다.
핵심 개념:
- 컨볼루션(Convolution) 계층 (
Conv2D
):- 이미지 위를 작은 필터(커널)가 이동하면서 합성곱 연산을 수행합니다.
- 필터는 이미지의 특정 패턴(예: 선, 코너, 질감)을 감지하는 역할을 합니다.
- 이 과정을 통해 이미지의 지역적 특징(local features)을 추출합니다.
- 활성화 함수(Activation Function, 예: ReLU):
- 컨볼루션 연산 결과에 비선형성을 추가합니다. 이를 통해 모델이 더 복잡한 패턴을 학습할 수 있게 합니다.
- ReLU(Rectified Linear Unit)는 가장 널리 사용되는 활성화 함수 중 하나입니다.
- 풀링(Pooling) 계층 (
MaxPooling2D
):- 특징 맵(feature map)의 크기를 줄여 계산량을 감소시키고, 주요 특징만 남겨 약간의 위치 변화에도 모델이 덜 민감하게 만듭니다(translation invariance).
- Max Pooling은 특정 영역에서 가장 큰 값만 선택하여 가져옵니다.
- 플래튼(Flatten) 계층:
- 다차원 특징 맵을 1차원 벡터로 변환하여 완전 연결 계층(Dense layer)에 입력할 수 있도록 준비합니다.
- 완전 연결(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("모델 학습 및 테스트 완료!")
코드 설명:
- 데이터 로드 및 전처리:
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)을 분류 문제에 적합한 원-핫 인코딩 벡터로 변환합니다.
- 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()
: 생성된 모델의 구조(각 계층의 이름, 출력 형태, 파라미터 수)를 보여줍니다.
- 모델 컴파일:
model.compile()
: 모델의 학습 과정을 설정합니다.loss='categorical_crossentropy'
: 다중 클래스 분류 문제에서 사용하는 손실 함수 (원-핫 인코딩된 레이블과 함께 사용).optimizer='adam'
: 경사 하강법 기반의 효율적인 옵티마이저.metrics=['accuracy']
: 학습 및 평가 과정에서 정확도를 측정합니다.
- 모델 학습:
model.fit()
: 모델을 학습시킵니다.x_train
,y_train
: 학습 데이터와 레이블.batch_size
: 한 번의 가중치 업데이트에 사용할 샘플의 수.epochs
: 전체 학습 데이터셋을 몇 번 반복하여 학습할지 결정.validation_split=0.1
: 학습 데이터의 일부(여기서는 10%)를 검증 세트로 사용하여 각 에포크마다 모델 성능을 평가합니다.
- 모델 평가:
model.evaluate()
: 학습된 모델을 테스트 데이터(x_test
,y_test
)로 평가하여 최종 성능(손실 및 정확도)을 측정합니다.
- 예측 결과 시각화:
model.predict()
: 테스트 데이터에 대한 모델의 예측 결과를 얻습니다. (각 클래스에 대한 확률 벡터 형태)matplotlib
을 사용하여 실제 이미지, 예측된 레이블, 실제 레이블을 함께 보여줍니다. 예측이 틀린 경우 빨간색으로 표시합니다.
- 학습 과정 시각화:
history
객체에는 학습 중 각 에포크별 정확도와 손실 값이 저장되어 있습니다. 이를 시각화하여 모델 학습이 잘 진행되었는지, 과적합이 발생하는지 등을 확인할 수 있습니다.