[CS231n] 이미지 분류의 pipeline과 K-NN based 이미지 분류, Cross Validation

2024. 9. 25. 15:55AI/CS231n

https://www.youtube.com/watch?v=vT1JzLTH4G4&list=PLC1qU-LWwrF64f4QKQT-Vg5Wr4qEE1Zxk&index=3

스탠포드 대학교의 유명한 컴퓨터 비전 강의를 보고 정리한 글입니다.

https://www.youtube.com/playlist?list=PLetSlH8YjIfXMONyPC1t3uuDlc1Mc5F1A

CS231n 강의와 서울대학교 DSBA 연구실의 강의를 참고하여 공부한 부분을 정리하였습니다.

이번 강의를 들으면서 처음 AI 공부를 시작했고, 그만큼 틀린 부분 해석이 모호한 부분이 있을 수 있습니다.


1. Introduction

Image classification

  • 전체 이미지 한 장에 대해 어느 클래스에 속해있는지 분류하는 작업이다.
  • 예를 들어 한 장의 사진이 고양이 사진인지, 강아지 사진인지 등 분류하는 것이다.
  • 그 분류된 이미지에서 좀 더 local한 지역의 물체를 탐지하는 것을 Object Detection이라 한다.
  • 그러한 Object가 무엇인지, 어떠한 행동을 하고 있는지에 대해 캡션을 달아주는 것을 Image Captioning이라 한다.

2. Image Classification pipeline

1. Image classification & Challenges

Image

  • RGB 형태의 사각형을 세 개 쌓아놓은, 3차원에서는 육면체의 구조로 이루어져 있다.
  • 각각은 0~255의 integers로 이루어져 있다.

Image classification

  • 기존의 정형 데이터를 분류하는 방법을 생각해보자.
    • 정형 데이터는 X, Y가 있고 그것을 어떤 분류기를 통해 클래스가 1인지 2인지 판단하는 형태였다.
  • 이미지에서도 정형 데이터와 똑같이 분류할 수 있다.
  • 이미지라는 input data와 레이블 값이라는 Y에 대해 Classfier를 통과하면 어떤 이미지인지 분류하게 된다.

Challenges

  • 이미지를 촬영한 viewpoint가 큰 영향을 미칠 수 있다.
    • 고양이를 위에서 찍었는지, 아래에서 찍었는지, 확대해서 찍었는지에 따라 다 다른 형태의 숫자 배열이 되고 컴퓨터는 다 다른 데이터로 인식하게 된다.
  • 빛의 밝기가 큰 영향을 미칠 수 있다.
  • 이미지가 예상 외의 포즈를 취하는 것에 취약하다. 예를 들어 고양이가 양반다리를 하는 경우 고양이로 인식할 수 없다.
  • 이미지가 구조물 뒤에 숨어있는 경우 분류하기 힘들다.
  • 배경과 너무 유사한 상황인 경우 detect에 문제가 생길 수 있다.
  • Intra-class variation
    • 같은 고양이지만 다른 종인 경우 혹은 같은 의자지만 소파, 등받이가 있는 의자, 없는 의자 등등 같은 클래스 내부에서 분류를 하는 경우 어려움을 겪을 수 있다.

Data-driven approach

위의 문제들을 해결하기 위해 데이터를 수집해서 딥러닝 모델에 학습시키는 방식으로 해결할 수 있다.

  1. 이미지와 label이 함께 있는 데이터를 수집한다.
  2. image classifier를 머신러닝 방법론을 적용해서 학습시킨다.
  3. test images를 classifier에 돌려서 검증한다.

2. K-NN based image classification

K-NN

  • Lazy model 이라고도 불린다.
    • training data가 들어왔을 때 즉각적으로 모델을 형성하는 것이 아니라, 그 데이터를 가지고 있는 상태에서 test data가 들어왔을 때 training data와 거리를 재서 주변의 data를 바탕으로 새로들어온 test data에 label을 예측해주는 형태의 모델이다.
  • Hyperparameter
    • K : 몇 개의 주변을 살펴볼것인가.
    • Voting : 새로 들어온 test data의 위치에서 주변에 있는 data에 대해 모두 똑같은 voting을 줄 것인지 더 가까우면 비중을 더 줄것인지.
    • Distance metric : 거리를 재는 방식을 어떻게 설정할 것인지

Distance metric

  • L1 distance (Mantattan)
    • 절대값 차이
    • $$ d_1(I_1, I_2) = \sum_{p}^{}|I^p_1 - I^p_2| $$
    • example을 보면 각 픽셀단위로 절대값 차이를 계산한다. 결과를 다 더하면 456이라는 test image와 training image의 거리를 구할 수 있다.
    •  
  • L2 distance (Eculidean)
    • Eculidean 거리
    • $$ d_1(I_1, I_2) = \sqrt{\sum_{p}^{}(I^p_1 - I^p_2)^2} $$

Exercise

CIFAR-10 이라는 데이터로 실습을 해보자. CIFAR-10은 다음과 같이 구성되어 있는 이미지 분류용 데이터셋이다.

  • 10 Label
  • 50,000 training images (each image is tiny : 32 X 32)
  • 10,000 test images

sklearn을 사용해서 실습해보자. 이미지와 라벨링이 어떻게 저장되는지 알아보는 코드이다.

# 시각화 모듈
import matplotlib.pyplot as plt

# Import datasets and performance metrics
from sklearn import datasets, metrics

# The handwritten digits dataset (8X8 image)
digits = datasets.load_digits()

# Target
images_and_labels = list(zip(digits.images, digits.target))

# Visualization of 4 images
for index, (image, label) in enumerate(images_and_labels[:4]):
    plt.subplot(2, 4, index+1) # 2 X 4의 subplot 위치를 할당하고, 각 image를 할당해서 그림
    plt.axis('off') # 축 없음
    plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest') # image 파일을 시각화 하는 함수. 타입을 흑백으로
    plt.title('Training: %i' % label) # title 설정

# 그래프 출력
plt.show()
  • 코드 설명
    • matplotlib.pyplot은 그래프와 이미지를 시각화하기 위한 라이브러리이다.
    • datasets는 scikit-learn에서 제공하는 여러 데이터셋 중 하나를 로드하기 위한 모듈이다. 여기서는 손글씨 숫자 데이터셋을 사용한다.
    digits = datasets.load_digits()
    
    • digits는 손글씨 숫자 데이터셋을 로드하는 함수로, 0부터 9까지의 손글씨 이미지와 그에 해당하는 정답을 포함한다. 각 이미지는 8x8 크기의 픽셀로 구성되어 있다.
    images_and_labels = list(zip(digits.images, digits.target))
    
    • digits.images는 8x8 크기의 이미지 데이터를 포함하고 있으며, digits.target은 그에 해당하는 숫자(정답)를 나타낸다.
    • zip을 사용하여 이미지와 그에 해당하는 레이블(숫자)을 쌍으로 묶은 후, 이를 리스트로 변환한다.
    digits.images와 digits.target은 손글씨 숫자 데이터셋의 두 가지 중요한 구성 요소로, 각각 이미지 데이터와 그에 해당하는 숫자 레이블을 나타낸다.
    • digits.images는 8x8 크기의 손글씨 숫자 이미지가 들어 있는 배열이다.

예를 들어 digits.images[0] 는 아래와 같이 구성되어 있다.

array([[ 0.,  0.,  5., 13.,  9.,  1.,  0.,  0.],
       [ 0.,  0., 13., 15., 10., 15., 5.,  0.],
       [ 0.,  3., 15.,  2.,  0., 11., 8.,  0.],
       [ 0.,  4., 12.,  0.,  0.,  8., 8.,  0.],
       [ 0.,  5.,  8.,  0.,  0.,  9., 8.,  0.],
       [ 0.,  4., 11.,  0.,  1., 12., 7.,  0.],
       [ 0.,  2., 14.,  5., 10., 12., 0.,  0.],
       [ 0.,  0.,  6., 13., 10.,  0.,  0.,  0.]])
  • digits.target은 각 이미지에 대응하는 숫자 레이블을 나타낸다. 각 이미지가 실제로 어떤 숫자를 뜻하는지를 담고 있는 것이다.
  • 예를 들어 digits.target[0] 는 아래와 같다.
  • 0 # 첫 번째 이미지가 나타내는 숫자는 0
for index, (image, label) in enumerate(images_and_labels[:4]):
    plt.subplot(2, 4, index+1)
    plt.axis('off')
    plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest')
    plt.title('Training: %i' % label)

 

  • enumerate(images_and_labels[:4]) 첫 4개의 이미지와 레이블 쌍을 for문 안에서 사용한다. index는 반복의 순서를 나타내는 값이고 (image, label) 이 이미지와 해당 레이블 쌍을 추출하여 두 값을 이용해 그래프를 그리는 작업을 하게 된다.
  • plt.subplot(2, 4, index+1) 2행 4열의 subplot을 만들어 생성된 subplot에 각 이미지가 차지할 위치를 설정한다. index+1 에서부터 이미지를 첫 번째 위치부터 순차적으로 그리게 된다.
  • plt.axis('off') 이미지 주변에 표시되는 축을 제거한다.
  • plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest') 이미지를 흑백(gray_r)으로 표시한다. interpolation은 픽셀 간 보간이라 한다. 이미지를 확대대거나 축소할 때 새로운 픽셀의 값을 nearest방식으로 정해서 새로운 픽셀의값을 가장 가까운 픽셀의 값으로 정하는 것이다.
  • plt.title('Training: %i' % label): 이미지 위에 해당 숫자 레이블을 타이틀로 설정한다.

위의 코드는 gray scale을 사용하기에 1층 구조이다. 8*8의 1층짜리 데이터이다.

이제 K-NN classfication을 수행하는 NearestNeighbor 클래스를 짜보자.

# K-NN classfication
import numpy as np
import matplotlib.pyplot as plt
import math

class NearestNeighbour:
    def __init__(self):
        pass

    def train(self, X, y):
        """X : N x training data set, Y : N x 1 label"""
        self.Xtr = X
        self.ytr = y

    def predict(self, X):
        """X : M x D test data set"""
        num_text = X.shape[0] # 행 개수 (M)
        Ypred = np.zeros(num_text, dtype=self.ytr.dtype) # 먼저 0으로 가득 채워둠

        for i in np.arange(len(X)): # 모든 test data에 대해
            print(i, '\\n')

            distance = np.zeros(len(self.Xtr))
            for j in np.arange(len(self.Xtr)):
                distances = np.sum(np.abs(self.Xtr.iloc[j] - X.iloc[i]), axis=0) # training data와 L1-norm을 구함
                distance[j] = distances
            
            min_index = np.argmin(distance) # 가장 가까운 1-NN의 index를 구함
            Ypred[i] = self.ytr.iloc[min_index] # 가장 가까운 이웃의 label을 적용
            
        return(Ypred)
  • 코드 설명
    • NearestNeighbour : K-NN 알고리즘을 구현하는 클래스이다.
    • __init__ : 클래스 생성자. 빈 함수로 구성되어 있다. self는 클래스 내에서 정의된 메서드가 해당 클래스의 인스턴스에 접근할 수 있도록 하는 변수이다. 객체의 인스턴스 자신을 참조하는 역할을 한다.
    def train(self, X, y):
        """X : N x training data set, Y : N x 1 label"""
        self.Xtr = X
        self.ytr = y
    
    • train : 모델을 학습시키는 역할을 한다. 이때 X는 학습 데이터이며, y는 각 학습 데이터에 대응하는 레이블이다.
    • self.Xtr와 self.ytr에 입력된 학습 데이터와 레이블을 저장한다. K-NN은 실제 학습을 하지 않고, 학습 데이터를 기억해 두기만 한다.
    • X, y는 학습용 데이터 배열이다.
    def predict(self, X):
        """X : M x D test data set"""
        num_text = X.shape[0] # 행 개수 (M)
        Ypred = np.zeros(num_text, dtype=self.ytr.dtype) # 먼저 0으로 가득 채워둠
    
    • X.shape[0] numpy 배열 X의 크기를 나타낸다. shape는 차례대로 첫 번째 차원의 크기, 두 번째 차원의 크기, …가 되고 여기서는 X가 2차원 배열이므로 행의 개수 즉, 샘플 데이터 개수가 된다. 가 된다.
    • np.zeros(num_text, dtype=self.ytr.dtype)
      • num_text 만큼의 배열을 -으로 채운다.
      • dtype=self.ytr.dtype dtype은 배열의 데이터 타입을 지정한다. 여기서는 self.ytr의 데이터 타입을 사용한다.
    for i in np.arange(len(X)): # 모든 test data에 대해
        print(i, '\\n')
    
        distance = np.zeros(len(self.Xtr))
    
    • 모든 테스트 데이터를 반복하며 각각의 테스트 샘플에 대해 예측을 수행한다.
    • distance **** 현재 테스트 샘플과 학습 데이터셋의 모든 샘플 간의 거리를 저장할 배열이다.
    for j in np.arange(len(self.Xtr)):
        distances = np.sum(np.abs(self.Xtr.iloc[j] - X.iloc[i]), axis=0) # training data와 L1-norm을 구함
        distance[j] = distances
    
    • 모든 테스트 데이터 X에 대해 training data Xtr 과의 L1-norm 거리를 계산한다.
    • iloc : pandas 라이브러리에서 사용되는 인덱싱 메서드로, 정수 위치로 데이터를 선택하는 메서드이다.
    • 여기서 j는 training data의 index가 된다.
    min_index = np.argmin(distance) # 가장 가까운 1-NN의 index를 구함
    Ypred[i] = self.ytr.iloc[min_index] # 가장 가까운 이웃의 label을 적용
    
    • 1-NN 이란 K-NN에서 K=1인 알고리즘을 말한다. 즉 하나의 가장 가까운 이웃을 찾는 알고리즘인 것이다.
    • np.argmin(distance) 학습 데이터 중 가장 가까운 샘플의 인덱스를 찾는다. 가장 가깝다고 하면 위에서 구한 L1-distance의 합이 가장 가까운 것이다.
    • 가장 가까운 레이블을 찾아 예측된 값으로 설정한다.
  • class NearestNeighbour: def __init__(self): pass

N개의 examples에 대해 training과 prediction은 얼마나 시간이 들까?

 

Train : O(1), predict O(N) ⇒ 사실 좋은 코드는 아니다. prediction은 빠르고 training이 느린건 괜찮은데 반대가 되버려서 좋지 않다.

 

 

이제 이 코드를 아까 배워둔 sklarn 테스트 데이터에 적용시켜 보자. 실제 강의 코드에 좀 더 살을 붙였는데 아까 배운 sklearn에 테스트를 하는건지 CIFAR-10 에 적용을 하는건지는 잘 모르겠다.

# main.py
import math

import numpy as np
import pandas as pd
from sklearn import datasets

from KNNClassifier import NearestNeighbour

# 데이터 로드
digits = datasets.load_digits()
data = pd.DataFrame(np.c_[digits['data'], digits['target']])

# training / test split
# Randomly shuffle the index of nba.
random_indices = np.random.permutation(data.index)

# Set a cutoff for how many items we want in the test set (in this case 1/3 of the items)
test_cutoff = math.floor(len(data) / 3)

# Generate the test set by taking the first 1/3 of the randomly shuffled indices.
ts = data.loc[random_indices[:test_cutoff]]  # 주석에서는 1을 시작으로 했지만, 일반적으로 첫 번째부터 자르는 게 맞음

# Generate the train set with the rest of the data.
tr = data.loc[random_indices[test_cutoff:]]

# Object
nn = NearestNeighbour()

# training (just save)
nn.train(tr.iloc[:, :-1].values, tr.iloc[:, -1].values)

# 예측 진행 (테스트 셋의 첫 번째 열)
Ypred = nn.predict(ts.iloc[:, :-1].values)

# 예측 결과와 실제 레이블 비교 출력
print("Predicted labels:", Ypred)
print("Actual labels:", ts.iloc[:, -1].values)
  • 코드 설명
    • datasets.load_digits() ****사이킷런의 digits 데이터셋을 불러온다.
    • pd.DataFrame(np.c_[digits['data'], digits['target']]) digits 데이터셋을 pandas 데이터프레임 형식으로 변환한다. np.c_[]는 각 숫자의 이미지 데이터, digits['data']와 레이블 데이터, digits['target']을 하나로 결합한다.
    random_indices = np.random.permutation(data.index)
    test_cutoff = math.floor(len(data) / 3)
    
    • np.random.permutation(data.index) 데이터 인덱스를 랜덤하게 섞는다. 학습 데이터와 테스트 데이터를 무작위로 분리하기 위함이다.
    • test_cutoff = math.floor(len(data) / 3) 전체 데이터셋에서 1/3에 해당하는 인덱스를 계산한다. 테스트 데이터와 학습 데이터를 분할하기 위한 기준이 된다.
    ts = data.loc[random_indices[:test_cutoff]]  # 테스트 데이터
    tr = data.loc[random_indices[test_cutoff:]]  # 학습 데이터
    
    • ts: 섞은 인덱스 중 첫 1/3을 테스트 데이터로 사용한다.
    • tr: 나머지 2/3을 학습 데이터로 사용한다.
    nn = NearestNeighbour()
    
    # 학습 데이터와 레이블로 훈련
    nn.train(tr.iloc[:, :-1].values, tr.iloc[:, -1].values)
    
    • train(tr.iloc[:, :-1].values, tr.iloc[:, -1].values) 학습 데이터를 입력하여 모델을 학습시킵니다.
      • tr.iloc[:, :-1].values: 학습 데이터에서 이미지 데이터를 첫 번째 파라미터로 넘긴다.
      • tr.iloc[:, -1].values: 학습 데이터에서 레이블 데이터를 두 번째 파라미터로 넘긴다.
    Ypred = nn.predict(ts.iloc[:, :-1].values)
    
    • nn.predict(ts.iloc[:, :-1].values) 테스트 데이터의 이미지를 이용해 예측을 수행한다.
  • digits = datasets.load_digits() data = pd.DataFrame(np.c_[digits['data'], digits['target']])

K-NN Validation and Summary

Ypred가 예측된 레이블값이 된다. 결과를 살펴보면 원래 데이터에 대해 1-NN classfier, 5-NN classfier의 결과가 달라진다.

K 값에 대해 결과가 어떻게 되는지, hyperparameter의 값을 Cross validation 방법을 사용해서 탐색해보자.

 

Cross validation 방법은 Training data를 5개로 쪼개게 된다.

이렇게 하면 5 fold cross validation이 된다.

예를 들어, 4번 fold를 validation으로 사용하고 1, 2, 3, 5번을 training data로 사용하여서 hyper parameter를 탐색하게 된다. 실제 성능 검증은 각 폴드가 한 번씩 validation data가 되어 성능을 평가한다.

 

결과를 보면 파란색 점이 single outcome이다. single outcome이란 하나의 fold를 validation으로 넣고 각각의 K값에 대해 predict를 진행했을 때 정확도를 의미한다.

모든 fold을 한 번씩 validation으로 설정하므로 그럼 K가 1일때 5개의 점이 찍히고, 2일때 찍히고, … 결과적으로 K가 100일때 5개의 점이 찍히게 된다.

파란색 선은 5개 점의 평균의 point를 이은 것이다.

최종적으로 결과를 봤을 때 K가 8~10인 경우 성능이 가장 좋다.

이런식으로 K를 먼저 탐색하고 전체 training data를 모델에 적용해서 test를 분류하게 된다.

 

결과적으로는 성능이 너무 안좋기에 K-NN은 절대 사용되지 않는다.

original data를 살짝 옆으로 옮기거나 특정 부분을 조금 가린다거나 조금 어둡게 한, 세 가지 data가 original data와 똑같은 거리를 가지게 된다. 이미지 고유의 특성을 잡지 못한다는 한계점이 있다.


3. Linear image classfication

Parametric approach

처음 주어진 이미지는 32 X 32 X 3의 형태로 총 3072개의 숫자를 가지는 벡터로 생각할 수 있다. 여기서 Weight를 곱하고 절편을 더해주는 방식을 통해 3072차원의 벡터를 10차원으로 만들어주는 함수 f를 찾아서 결과로 나온 10차원의 벡터는 각각의 클래스에 해당할 확률을 나타낸다.

즉, 맨 처음에 들어온 3072 길이의 이미지 x에 대해 10차원으로 매핑시켜주는 행렬 W를 곱하고 절편 b를 더해서 모델을 완성하는 함수 f를 찾는 것이 목적이 된다.

 

예를 들어 이미지가 4개의 픽셀을 가지고 있고 클래스가 cat, dog, ship 세 개만 가지고 있다고 생각하자.

위의 고양이를 벡터로 펼치게 되었을 때 [56, 231, 24, 2] 가 된다고 생각해보자.

그럼 W와 bias도 랜덤으로 줄 수 있다.

여기서 dog가 가장 크므로 dog로 분류된다.

Interpreting a Linear classifier

Linear classification을 학습을 하고 나면 이 weight 들을 템플릿 형태로 확인할 수 있다.

  • 모델을 확인해보면 plane은 파란색이 많다.
  • car는 빨간색이 많다.
    • 아마 training data에 red car data가 많았기 때문이다. 따라서 노란색 car는 frog로 분류될 수 있다.
  • horse는 머리가 양쪽으로 두 개인데, training data에 말이 좌측을 보는 것과 우측을 보는 것이 비슷한 개수가 있기 때문이다.

Result of Linear classifier

랜덤하게 할당한 W를 가지고 있는 함수 f에 대해 3개의 test data를 계산을 해보았을 때 굉장히 성능이 좋지 않다.

따라서 W를 랜덤으로 주는 것이 아니라 수학적으로 최적화를 해줘야 한다. loss를 정의하고 loss를 최적화하는 그런 방법이 필요하다는 것이다.

 

'AI > CS231n' 카테고리의 다른 글

[CS231n] Training Neural Networks Part 1  (9) 2024.10.09
[CS231n] 4. NeuralNetwork, Backpropagation  (5) 2024.10.03
[CS231n] 3. Loss Function & Optimization  (6) 2024.09.26