logo

이미지 다항 분류

실습 준비

import os

os.environ["KERAS_BACKEND"] = "torch"

import keras
import numpy as np

MNIST 데이터

MNIST는 28×28 픽셀의 손글씨 숫자 이미지와 0부터 9까지의 정수 레이블로 구성.

(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()

훈련 이미지 60,000장과 테스트 이미지 10,000장. 이미지 한 장의 형태는 (28, 28).

다항 분류 모형

단일 레이블 다항 분류(single-label multiclass classification)는 한 사례를 서로 배타적인 여러 클래스 중 하나로 분류하는 문제. 클래스가 개이면 출력층도 클래스마다 하나씩 개의 값을 출력.

MNIST에는 10개 클래스가 있으므로 출력층을 Dense(10, activation="softmax")로 구성. 각 출력은 해당 클래스일 확률로 해석.

sparse_categorical_crossentropyy_train0부터 9까지의 정수 클래스 번호일 때 사용.

keras.utils.set_random_seed(812)  # 실행 결과 재현

model = keras.models.Sequential(
    [
        keras.layers.Rescaling(1 / 255),  # 픽셀값을 0~1 범위로 조정
        keras.layers.Flatten(),
        keras.layers.Dense(16, activation="relu"),  # 은닉 뉴런 16개
        keras.layers.Dense(10, activation="softmax"),  # 숫자 클래스 10개
    ]
)
model.compile(
    optimizer=keras.optimizers.SGD(learning_rate=0.001),  # SGD 학습률
    loss="sparse_categorical_crossentropy",
    metrics=["accuracy"],
)
history = model.fit(
    x_train,
    y_train,
    epochs=5,  # 전체 데이터 반복 횟수
    batch_size=32,  # 한 번에 학습할 이미지 수
    verbose=0,  # 실행 시간에 따라 달라지는 진행 로그 생략
)
history.history["loss"][-1]  # 마지막 에포크의 훈련 손실
실행 결과
0.6265072226524353
history.history["accuracy"][-1]  # 마지막 에포크의 훈련 정확도
실행 결과
0.8327666521072388

5번째 에포크의 훈련 손실은 약 0.627, 훈련 정확도는 약 83.3%.

예측

y_prob = model.predict(x_test, verbose=0)
y_prob.shape  # 사례 수와 클래스 수
실행 결과
(10000, 10)
y_prob[0]  # 첫 테스트 이미지의 클래스별 확률
실행 결과
array([1.4675226e-03, 1.9411558e-04, 5.3651520e-04, 1.2395663e-02,
       3.1409499e-03, 1.5326531e-03, 6.0711998e-05, 9.6408945e-01,
       1.1523711e-03, 1.5429925e-02], dtype=float32)
y_pred = y_prob.argmax(axis=1)

y_prob의 행은 사례, 열은 클래스를 뜻함. 각 행의 10개 확률 합은 1이며 y_pred의 각 원소는 0부터 9까지의 예측 클래스 번호.

y_pred[0]  # 첫 테스트 이미지의 예측 클래스
실행 결과
np.int64(7)
y_test[0]  # 첫 테스트 이미지의 실제 클래스
실행 결과
np.uint8(7)

첫 테스트 이미지는 숫자 7로 예측했으며 실제 클래스와 일치.

소프트맥스 함수 softmax

소프트맥스는 클래스별 로짓(logit) 를 같은 개수의 확률 로 변환.

  • 유한한 로짓에 대한 각 출력 범위는
  • 모든 출력의 합은 1
  • 입력 로짓이 클수록 해당 클래스의 확률도 큼
  • 모든 로짓에 같은 값을 더하거나 빼도 확률은 변하지 않음
    • 실제 구현에서는 수치적 안정성을 위해 가장 큰 로짓을 빼고 계산
scores = np.array(
    [-1.0, 0.5, 2.0], dtype="float32"
)  # 세 클래스의 예시 로짓과 MPS 호환 자료형
keras.activations.softmax(scores)  # 클래스별 확률
실행 결과
tensor([0.0391, 0.1753, 0.7856], device='mps:0')

가장 큰 로짓 2.0이 가장 높은 확률을 얻지만, 가장 큰 값만 1이 되는 것은 아님.

시그모이드와 소프트맥스의 관계

서로 배타적인 두 클래스를 예측할 때는 다음 두 방식을 모두 사용 가능.

  • 로짓 하나를 sigmoid로 변환해 한 클래스의 확률 출력
  • 로짓 두 개를 softmax로 변환해 두 클래스의 확률 출력

두 로짓이 , 이면 두 번째 클래스의 softmax 확률은 로짓 차이 에 대한 sigmoid와 같음.

x = 1.0  # 예시 로짓
keras.activations.sigmoid(x)  # 로짓 하나의 시그모이드 확률
실행 결과
tensor(0.7311, device='mps:0')
keras.activations.softmax(
    np.array([0.0, x], dtype="float32")  # MPS 호환 자료형
)  # 로짓 두 개의 소프트맥스 확률
실행 결과
tensor([0.2689, 0.7311], device='mps:0')

클래스가 셋 이상이라는 이유만으로 항상 softmax를 쓰는 것은 아님. 한 사례가 여러 클래스에 동시에 속할 수 있는 다중 레이블 분류에서는 클래스마다 독립적인 sigmoid를 사용. softmax는 클래스 중 정확히 하나를 고르는 단일 레이블 다항 분류에 적합.

출력층과 손실 함수의 일치

레이블 형식출력층손실 함수
정수 클래스 번호softmax 확률SparseCategoricalCrossentropy()
원-핫 벡터softmax 확률CategoricalCrossentropy()
정수 클래스 번호활성화 함수 없는 로짓SparseCategoricalCrossentropy(from_logits=True)
원-핫 벡터활성화 함수 없는 로짓CategoricalCrossentropy(from_logits=True)

from_logits=True는 손실 함수에 입력되는 값이 확률이 아니라 로짓임을 뜻함. 출력층에 softmax를 적용했다면 기본값인 from_logits=False를 사용.

소프트맥스 함수가 없는 모형

로짓과 교차 엔트로피를 한 연산에서 처리하면 수치적으로 더 안정적. from_logits=True인 손실 함수에 softmax 확률을 다시 입력하면 안 됨.

keras.utils.set_random_seed(812)  # 실행 결과 재현

model = keras.models.Sequential(
    [
        keras.layers.Rescaling(1 / 255),  # 픽셀값을 0~1 범위로 조정
        keras.layers.Flatten(),
        keras.layers.Dense(16, activation="relu"),  # 은닉 뉴런 16개
        keras.layers.Dense(10),  # 숫자 클래스 10개의 로짓
    ]
)
model.compile(
    optimizer=keras.optimizers.SGD(learning_rate=0.001),  # SGD 학습률
    loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    metrics=["accuracy"],
)
logit_history = model.fit(
    x_train,
    y_train,
    epochs=5,  # 전체 데이터 반복 횟수
    batch_size=32,  # 한 번에 학습할 이미지 수
    verbose=0,  # 실행 시간에 따라 달라지는 진행 로그 생략
)
logit_history.history["loss"][-1]  # 마지막 에포크의 훈련 손실
실행 결과
0.6265072226524353
logit_history.history["accuracy"][-1]  # 마지막 에포크의 훈련 정확도
실행 결과
0.8327666521072388

소프트맥스를 출력층에서 제거해도 손실 함수가 로짓을 처리하므로 5번째 에포크의 훈련 정확도는 약 83.3%.

로짓 계산

logits = model.predict(x_test, verbose=0)
logits.shape  # 사례 수와 클래스 수
실행 결과
(10000, 10)
logits[0]  # 첫 테스트 이미지의 클래스별 로짓
실행 결과
array([ 0.10877012, -1.9141067 , -0.8974651 ,  2.2425418 ,  0.8697207 ,
        0.15219513, -3.0764194 ,  6.596379  , -0.1329827 ,  2.4615035 ],
      dtype=float32)
probabilities = keras.activations.softmax(logits[0])
probabilities  # 첫 테스트 이미지의 클래스별 확률
실행 결과
tensor([1.4675e-03, 1.9412e-04, 5.3652e-04, 1.2396e-02, 3.1410e-03, 1.5327e-03,
        6.0712e-05, 9.6409e-01, 1.1524e-03, 1.5430e-02], device='mps:0')
logits[0].argmax()  # 로짓이 가장 큰 클래스
실행 결과
np.int64(7)
keras.ops.convert_to_numpy(probabilities).argmax()  # 확률이 가장 큰 클래스
실행 결과
np.int64(7)

로짓은 합이 1일 필요가 없는 정규화 전 점수. softmax는 로짓의 순서를 보존하므로 클래스 번호만 필요하면 로짓에 바로 argmax()를 적용 가능.

Previous
활성화 함수