본문 바로가기
Studies & Courses/Machine Learning & Vision

[cs231n] Minimal Neural Network Case Study

by Air’s Big Data 2021. 1. 14.

간단한 2차원 신경망의 구현에 대한 전체 과정을 살펴 보겠습니다. 먼저 간단한 선형분류기를 구현한 다음 이 코드를 2 layer 신경망으로 확장하고자 합니다.

 

Generating some data

  • np.linspace: np.linspace(배열의 시작 값, 배열의 마지막 값, 값의 개수)
from builtins import range
import numpy as np
from random import shuffle
from past.builtins import xrange
import matplotlib.pyplot as plt

N = 100 # number of points per class
D = 2 # dimensionality
K = 3 # number of classes
X = np.zeros((N*K,D)) # 100x3 # data matrix (each row = single example)
y = np.zeros(N*K, dtype='uint8') # class labels
for j in range(K):
  ix = range(N*j,N*(j+1))
  r = np.linspace(0.0,1,N) #r adius(반지름) # np.linspace(배열의 시작 값, 배열의 마지막 값, 몇 개)
  t = np.linspace(j*4,(j+1)*4,N) + np.random.randn(N)*0.2 # theta
  X[ix] = np.c_[r*np.sin(t), r*np.cos(t)]
  y[ix] = j

# 데이터 시각화
plt.scatter(X[:, 0], X[:, 1], c=y, s=40, cmap=plt.cm.Spectral)
plt.show()

 

 

선형 분리되지 않는 세 클래스(파란색, 빨간색, 노란색)로 구성된 간단한 나선형 데이터가 시각화되었습니다. 데이터 세트를 전처리하여 각 피처의 평균이 0 표준편차가 1이 되로록 만들어야 하지만, 위의 경우 features는 이미 -1에서 1 사이의 괜찮은 범위에 내에 있으므로 이 단계를 건너 뜁니다.


Training a Softmax Linear Classifier

 

Initialize the parameters

먼저 데이터 세트를 사용하여 Softmax 분류기를 학습 시킵니다. Softmax 분류기는 linear score function를 가지고 있으며 cross-entropy loss를 사용합니다. 선형 분류기의 파라미터는 각 class에 대한 weight matrix W 와 bias vector b로 구성됩니다. 먼저 이러한 파라미터를 랜덤으로 초기화합니다.

  • random.randn() : 표준정규분포 (Standard normal distribution)로부터 샘플링된 난수를 반환합니다.
# initialize parameters randomly
W = 0.01 * np.random.randn(D,K)
b = np.zeros((1,K)) #array([[0., 0., 0.]])

 

 

 

Compute the class scores

linear classifier이므로 모든 class scores를 아래와 같은 단일 행렬 곱셈의 병렬 계산으로 간단하게 구할 수 있습니다.

# compute class scores for a linear classifier
scores = np.dot(X, W) + b

우리는 300 개의 2 차원 점(2-D points)를 가지고 있습니다. 곱셈 후에 scores 배열의 크기는 [300 x 3]입니다. 각 행은 3 개의 클래스(파란색, 빨간색, 노란색)에 해당하는 클래스 스코어를 나타냅니다.

 

 

 

Compute the loss

모델 구현에 필요한 두 번째 핵심 요소는 loss function입니다. 이것은 미분 가능한 목적함수로, 계산된 class 스코어의 불일치 정도를 표현할 수 있습니다. 직관적으로, 우리는 정답 class가 다른 class보다 높은 점수를 가지기를 바랍니다. 이와 같은 경우 loss는 작아야 하며, 그렇지 않으면 높아야합니다. 이러한 직관을 정량화하는 방법은 여러 가지가 있지만, 이 예에서는 Softmax 분류기와 관련된 cross-entropy loss을 사용한다. f는 하나의 샘플에 대한 class 점수 배열(예 : 여기서는 3 개의 숫자로 구성된 배열)이며, Softmax 분류기에서 해당 샘플의 로스값을 다음과 같이 계산합니다.

 

 

 

 

Softmax 분류기가 모든 샘플의 클래스 스코어 f를 각 클래스의 (비정규화 된) log 확률로 해석한다는 것을 알 수 있습니다. 정규화 되지 않은 스코어를 지수화(exponentiate) 한 다음, 정규화된 확률을 얻습니다. -log 내 식은 정답 class를 가질 정규화된 확률을 의미합니다. 이제 이 표현식이 어떻게 작동하는지 알아봅시다. log의 quantity는 항상 0과 1 사이며, 정답 class의 확률이 매우 작으면 (0에 가까울 때) 로스 값은 (양의) 무한대로 이동합니다. 반대로 값이 1에 가깝다면 다음 식 log(1) = 0와 같이 0에 가까운 값을 가질 것입니다. 따라서 표현식 L_i는 정답 class에 대한 확률 값이 높다면 작은 값을 가질 것이고 그렇지 않다면 큰 값을 가집니다.

 

전체 Softmax classifier loss는  training examples와 the regularization에 대한 average cross-entropy loss로 정의되는 것을 기억해야 합니다. 

 

 

 

위에서 계산한 주어진 scores배열을 통해, 로스 값을 계산할 수 있습니다. 먼저, 확률을 구하는 간단한 방법을 살펴봅시다.

num_examples = X.shape[0]
# get unnormalized probabilities
exp_scores = np.exp(scores)
# normalize them for each example
probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)

각 행에 클래스 확률이 포함 된 [300 x 3] 크기의 probs라는 배열을 만들었습니다. 정규화를 했기 때문에 각 행의 합은 1이 됩니다. 이제 각 example에서 정답 class 값에 대한 로그 확률을 가져 올 수 ​​있습니다.

 

 

 

correct_logprobs = -np.log(probs[range(num_examples),y])

여기서 correct_logprobs 배열은 각 클래스가 올바로 할당 될 확률의 1D 배열입니다. 아래와 같이 전체 로스(full loss)는 log probabilities와 regularization loss가 됩니다.

 

# compute the loss: average cross-entropy loss and regularization
data_loss = np.sum(correct_logprobs)/num_examples
reg_loss = 0.5*reg*np.sum(W*W)
loss = data_loss + reg_loss

 

이 코드에서 정규화 파라미터(regularization strength) lambda는 reg 안에 저장되어 있습니다. 편의 상 0.5를 곱한 이유는 잠시 후 아래에서 알아보겠습니다.처음 (임의의 파라미터에서) 얻을 수 있는 값은 loss = 1.1일 것입니다. 이는 -np.log(1.0/3)의 값입니다. 작은 초기 임의의 weights로 모든 class에 할당 될 모든 확률이 약 1/3이기 때문입니다. 이제 우리는 절대적 하한선(absolute lower bound)인 loss = 0이 되도록 loss 값을 가능한 한 낮추고 싶습니다. loss가 낮을수록 모든 샘플이 정답 class에 할당될 확률이 높아집니다.

 

Computing the Analytic Gradient with Backpropagation

 

위과 같이 loss 값을 계산하는 방법을 배웠습니다. 이제 그 값을 최소화할 것이며, gradient descent를 사용 할 것입니다. 즉, 위에서와 같이 random하게 parameter를 초기화 한 다음 parameter에 대한 loss 함수의 gradient를 계산하여 파라미터를 업데이트 함으로써 loss를 줄이고자 합니다. 여기서 사용할 중간 변수(intermediate variable)를  p라고 하며, (정규화 된) 확률의 벡터입니다.

 

 

 

 

이제 우리는 loss 𝐿𝑖를 줄이려면 𝑓(the array of class scores) 내부 scores를 어떻게 바꿔야 하는지 알고 싶습니다. 즉, 그라디언트 ∂𝐿𝑖/∂𝑓𝑘를 미분하고 싶습니다. loss 𝐿𝑖는 𝑝로부터 계산되며, 𝑝는 𝑓의 영향을 받습니다. gradient를 미분하기 위해 체인 룰을 사용하지만, 이 과정은 설명에서 생략하고 결과적으로 간단하게 표현된 식은 아래와 같습니다.

 

 

 

예를 들어 우리가 계산 한 확률이 p = [0.2, 0.3, 0.5] 이며, 중간에 있는 확률 0.3을 가지는 클래스가 정답 class라고 가정해 봅시다. 위 미분식에 따라 스코어에 대한 gradient는 df = [0.2, -0.7, 0.5]가 될 것입니다. gradient의 해석을 생각해 보면, 이 결과가 매우 직관적이라는 것을 알 수 있습니다. score vector의 첫 번째 요소 또는 마지막 요소(the scores of the incorrect classes)를 증가 시키면 loss 값이 증가하게 되며, 당연히 loss 값이 증가되는 것은 우리가 바라는 일이 아니겠죠. 그러나 정답 class의 스코어가 커지는 것은 loss 가 작아지며, 따라서 -0.7의 gradient를 사용하여 정답 class에 대한 스코어 값을 높이면, 당연히 loss 값이 작아질 것입니다.

 

probs은 각 example에 대한 모든 클래스의 확률(행)을 저장하고 있다는 생각했을 때, scores에 대한 gradient인 dscores를 다음과 같이 구할 수 있습니다.

#dscores: the gradient on the scores
#probs: 각 행에 클래스 확률이 포함 된 [300 x 3] 크기의 array
dscores = probs
dscores[range(num_examples),y] -= 1
dscores /= num_examples

 

scores = np.dot(X, W) + b라고 위에서 정의했습니다. 그래서 우리는 (dscores에 저장된) scores에 대한 gradient를 구할 수 있고, W와 b로 backpropagation을 할 수 있습니다.

 

dW = np.dot(X.T, dscores)
db = np.sum(dscores, axis=0, keepdims=True)
dW += reg*W # don't forget the regularization gradient

행렬 곱셈 연산을 통해 역전파 할 수 있으며, 또한 정규화로 인한 contribution을 추가했습니다. 정규화 gradient를 보면 앞에서 상수 0.5를 사용했기 때문에 reg*W와 같이 매우 간단한 형식 (𝑑/𝑑𝑤)*((1/2)(𝜆𝑤^2))=𝜆𝑤을 가집니다.

 

Performing a parameter update

이제 그라디언트를 통해 모든 파라미터가 로스 함수에 어떻게 영향을 미치는지 알았으니, 그래디언트의 반대 방향으로 파라미터를 업데이트하여 로스를 줄입니다. 

# perform a parameter update
W += -step_size * dW
b += -step_size * db

 

Putting it all together: Training a Softmax Classifier

위 코드를 종합하면, 아래와 같은 Softmax 분류기를 Gradient descent를 통해 학습하는 전체 코드가 됩니다.

#Train a Linear Classifier

# initialize parameters randomly
W = 0.01 * np.random.randn(D,K)
b = np.zeros((1,K))

# some hyperparameters
step_size = 1e-0
reg = 1e-3 # regularization strength

# gradient descent loop
num_examples = X.shape[0]
for i in range(200):

  # evaluate class scores, [N x K]
  scores = np.dot(X, W) + b

  # compute the class probabilities
  exp_scores = np.exp(scores)
  probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True) # [N x K]

  # compute the loss: average cross-entropy loss and regularization
  correct_logprobs = -np.log(probs[range(num_examples),y])
  data_loss = np.sum(correct_logprobs)/num_examples
  reg_loss = 0.5*reg*np.sum(W*W)
  loss = data_loss + reg_loss
  if i % 10 == 0:
    print("iteration %d: loss %f" % (i, loss))

  # compute the gradient on scores
  dscores = probs
  dscores[range(num_examples),y] -= 1
  dscores /= num_examples

  # backpropate the gradient to the parameters (W,b)
  dW = np.dot(X.T, dscores)
  db = np.sum(dscores, axis=0, keepdims=True)

  dW += reg*W # regularization gradient

  # perform a parameter update
  W += -step_size * dW
  b += -step_size * db

 

 

 

위 결과를 보면 약 190 회 반복 한 후에 어딘가로 수렴한 것을 알 수 있는데, 다음과 같이 트레이닝 세트의 정확도를 평가할 수 있습니다.

# evaluate training set accuracy
scores = np.dot(X, W) + b
predicted_class = np.argmax(scores, axis=1)
print('training accuracy: %.2f' % (np.mean(predicted_class == y)))

 

 

 

 

 

결과적으로 약 50%로 높은 accuracy는 아니지만, 생성 된 데이터 세트는 선형으로 분리 될 수 없다는 점을 고려하면 당연한 일입니다. 아래와 같이 학습된 분류 경계를 시각화해 보겠습니다.

 

# plot the resulting classifier
h = 0.02
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
                     np.arange(y_min, y_max, h))
Z = np.dot(np.c_[xx.ravel(), yy.ravel()], W) + b
Z = np.argmax(Z, axis=1)
Z = Z.reshape(xx.shape)
fig = plt.figure()
plt.contourf(xx, yy, Z, cmap=plt.cm.Spectral, alpha=0.8)
plt.scatter(X[:, 0], X[:, 1], c=y, s=40, cmap=plt.cm.Spectral)
plt.xlim(xx.min(), xx.max())
plt.ylim(yy.min(), yy.max())
#fig.savefig('spiral_linear.png')

 

 

 

위와 같이 선형 분류기는 앞에서 생성한 간단한 나선형 데이터 세트를 학습하지 못합니다.

 


Training a Neural Network

 

위에서 다룬 선형 분류기는 이 데이터 세트에 적합하지 않으며, 우리는 이제 신경망을 사용하고자 합니다. 위 데이터에 대해서는 한 개의 히든 레이어를 추가하며, 이제 두 세트의 웨이트와 바이어스가 필요하게 됩니다.

# initialize parameters randomly
h = 100 # size of hidden layer
W = 0.01 * np.random.randn(D,h)
b = np.zeros((1,h))
W2 = 0.01 * np.random.randn(h,K)
b2 = np.zeros((1,K))

스코어를 계산하기 위한 전방향 패스는 아래와 같이 변경됩니다.

# evaluate class scores with a 2-layer Neural Network
hidden_layer = np.maximum(0, np.dot(X, W) + b) # note, ReLU activation
scores = np.dot(hidden_layer, W2) + b2

이전과 달라진 부분은 코드의 한 줄을 추가하는 것 뿐입니다. 여기서는 먼저 히든 레이어를 계산 한 다음 이 히든 레이어를 기반으로 스코어를 계산합니다. 결정적으로, 여기서는 비선형성이 추가됩니다. 여기서는 0 대한 임계값을 가지는 ReLU를 활성화 함수로 사용하여 히든레이어에 비선형성을 추가합니다.

 

나머지는 모두 동일합니다. 이전과 똑같이 스코어에 따라 로스값을 계산하고 이전과 같이 스코어에 대한 그래디언트(dscores)를 얻습니다. 하지만 당연히 역전파(backpropagate)에 대한 모델 파라미터 그라디언트로 형식은 바뀝니다. 먼저 신경망의 두 번째 레이어에 대한 백프로퍼게이트는 아래와 같이 수행 할 수 있습니다. 여기서는 단지 위 Softmax 분류기 코드에서 X(raw data)를 hidden_layer 변수로 대체합니다.

 

# backpropate the gradient to the parameters
# first backprop into parameters W2 and b2
dW2 = np.dot(hidden_layer.T, dscores)
db2 = np.sum(dscores, axis=0, keepdims=True)

hidden_layer는 다른 파라미터와 데이터의 함수 이기 때문에 이 변수에 대한 역 전파를 계속해야 합니다. 그라디언트는 다음과 같이 계산할 수 있습니다.

dhidden = np.dot(dscores, W2.T)

이제 히든 레이어의 출력에대한 그라디언트를 구했으며, 다음으로 ReLU 비선형성을 backpropate해야 합니다. backward pass 도중 ReLU는 switch이므로, 쉽게 구현할 수 있습니다. 𝑟=𝑚𝑎𝑥(0,𝑥)이므로 𝑑𝑟𝑑𝑥=1(𝑥>0)이 됩니다. 체인 룰을 사용하여, 순방향 패스 중 ReLU 유닛의 입력이 0보다 큰 경우에는 그라디언트가 그대로 통과 할 수 있지만 0보다 작은 그라디언트 값은 '죽게' 됩니다. 따라서 우리는 ReLU를 다음과 같이 단순히 backpropagate할 수 있습니다.

# backprop the ReLU non-linearity
dhidden[hidden_layer <= 0] = 0

이제 첫 번째 레이어에 대한 웨이트와 바이어스의 그라디언트를 구합니다.

# finally into W,b
dW = np.dot(X.T, dhidden)
db = np.sum(dhidden, axis=0, keepdims=True)

dW, db, dW2, db2 파라미터에 대한 그라디언트를 가지고 있으며 업데이트를 수행 할 수 있는 코드를 완성했습니다. 전체 코드는 선형분류기와 매우 유사합니다.

# initialize parameters randomly
h = 100 # size of hidden layer
W = 0.01 * np.random.randn(D,h)
b = np.zeros((1,h))
W2 = 0.01 * np.random.randn(h,K)
b2 = np.zeros((1,K))

# some hyperparameters
step_size = 1e-0
reg = 1e-3 # regularization strength

# gradient descent loop
num_examples = X.shape[0]
for i in range(10000):

  # evaluate class scores, [N x K]
  hidden_layer = np.maximum(0, np.dot(X, W) + b) # note, ReLU activation
  scores = np.dot(hidden_layer, W2) + b2

  # compute the class probabilities
  exp_scores = np.exp(scores)
  probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True) # [N x K]

  # compute the loss: average cross-entropy loss and regularization
  correct_logprobs = -np.log(probs[range(num_examples),y])
  data_loss = np.sum(correct_logprobs)/num_examples
  reg_loss = 0.5*reg*np.sum(W*W) + 0.5*reg*np.sum(W2*W2)
  loss = data_loss + reg_loss
  if i % 1000 == 0:
    print("iteration %d: loss %f" % (i, loss))

  # compute the gradient on scores
  dscores = probs
  dscores[range(num_examples),y] -= 1
  dscores /= num_examples

  # backpropate the gradient to the parameters
  # first backprop into parameters W2 and b2
  dW2 = np.dot(hidden_layer.T, dscores)
  db2 = np.sum(dscores, axis=0, keepdims=True)
  # next backprop into hidden layer
  dhidden = np.dot(dscores, W2.T)
  # backprop the ReLU non-linearity
  dhidden[hidden_layer <= 0] = 0
  # finally into W,b
  dW = np.dot(X.T, dhidden)
  db = np.sum(dhidden, axis=0, keepdims=True)

  # add regularization gradient contribution
  dW2 += reg * W2
  dW += reg * W

  # perform a parameter update
  W += -step_size * dW
  b += -step_size * db
  W2 += -step_size * dW2
  b2 += -step_size * db2

이 네트워크의 training accuracy는 아래와 같습니다.

# evaluate training set accuracy
hidden_layer = np.maximum(0, np.dot(X, W) + b)
scores = np.dot(hidden_layer, W2) + b2
predicted_class = np.argmax(scores, axis=1)
print('training accuracy: %.2f' % (np.mean(predicted_class == y)))

 

 

선형 네트워크보다 훨씬 높은 Accuracy를 얻었습니다. 이제 decision boundaries를 시각화해봅시다.

 

 

 

 

 


Summary

 

간단한 2D 데이터 세트를 사용하여, 선형 네트워크와 2 레이어 뉴럴 네트워크를 학습했습니다. 선형 분류기에서 뉴럴 네트워크로의 변경하는 데는 코드가 거의 바뀌지 않음을 알 수 있습니다. 스코어 함수는 형태를 바꾸는 1줄의 코드 추가와, 파라미터에 대한 backpropagation에서 변경 정도가 있었습니다.

 

 

더보기

댓글