cs231n

cs231n Assignment 1: Q4 (Two Layer Network 구현)

츄츄츄츄츄츄츄 2023. 2. 1. 14:41

내 풀이 링크: https://github.com/lionkingchuchu/cs231n.git

 

이번 과제는 본격적으로 layer 을 2개 가진 간단한 이중 신경망을 구현해 보는 것이었다. 각 layer에는 전파, dW를 얻기 위한 역전파를 사용하기 위해 layer.forward(), layer.backward()를 두개씩 구현해야 한다. layer를 지나면서 결과를 마지막 layer인 Loss function을 통과하여 결과를 도출하고, Loss function에서 역전파하여 각 layer들의 dW값을 learning rate에 따라 조절해 가며 신경망을 학습시킨다.

 

def affine_forward(x, w, b):
   
    out = None

    # *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

    num_train = x.shape[0]
    out = np.dot(np.reshape(x, [num_train,-1]), w) + b

    # *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

    cache = (x, w, b)
    return out, cache


def affine_backward(dout, cache):
   
    x, w, b = cache
    dx, dw, db = None, None, None
    
    # *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

    dx = np.reshape(np.dot(dout, w.T), x.shape)
    dw = np.dot(np.reshape(x,[x.shape[0],-1]).T,dout)
    db = np.reshape(np.sum(dout,axis=0),b.shape)

    # *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****
   
    return dx, dw, db

먼저 affine함수의 forward 함수는 간단하게 (X) X (W) + B 로 표현할 수 있다. 중요한점은 각 layer에서 cache를 저장해 놓는데, x, w, b의 cache를 저장해 놓아야 이후에 backward 함수에서 cache를 통해 dx, dw, db를 구할 수 있기 때문이다. backward 함수는 backpropagation 을 이용해 dout에 dx 는 W를 곱해주고 dw 는 X를 곱해주고 (곱셈 함수의 역전파는 switch) db는 dout의 합을 구해주면 gradient를 구할 수 있다.

 

def relu_forward(x):
   
    out = None

    # *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

    zero_mask = x < 0
    out = x * ~zero_mask

    # *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

    cache = x
    return out, cache


def relu_backward(dout, cache):
   
    dx, x = None, cache

    # *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

    zero_mask = x < 0
    dx = dout * ~zero_mask

    # *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

    return dx

relu 함수도 forward와 backward 함수를 구해준다. forward는 0보다 작은 값들은 0으로 만들어 준다. backward는 dout 중 0보다 큰 값만 dx로 흘려준다. 

 

Inline Question 1:

We've only asked you to implement ReLU, but there are a number of different activation functions that one could use in neural networks, each with its pros and cons. In particular, an issue commonly seen with activation functions is getting zero (or close to zero) gradient flow during backpropagation. Which of the following activation functions have this problem? If you consider these functions in the one dimensional case, what types of input would lead to this behaviour?

  1. Sigmoid
  2. ReLU
  3. Leaky ReLU

Answer:

Sigmoid and Relu has the problem of getting zero or close to zero gradient flow. When we differentiate Sigmoid function, it is e^-x / (1+e^-x)^2, and you can see the denominator grows exponentially as x increases. Since the denominator grows exponentially, gradient is close to zero even we put a small value such as 10. ReLU function definietly has this problem since negative inputs are all considered as 0.

 

다음 문제는 몇몇의 activation 함수 중 relu 같은 함수는 gradient가 0 인 문제가 생기게 된다. Sigmoid, ReLU, Leaky ReLU 중 어떤 함수가 위 문제를 가질 수 있는가를 물어보는 문제이다. 당연히 ReLU는 0보다 큰 값만 gradient가 있기에 맞고, Sigmoid 함수도 gradient가 0과 매우 가까운 값을 가질 수 있는 문제가 있다. Sigmoid함수를 미분 해 보면 e^-x / (1+e^-x)^2 인데, 보다시피 x가 증가하거나 감소할수록 Sigmoid의 미분의 분모가 지수적으로 증가하는 것을 볼 수 있다. 실제로 x에 10만 대입해도 0에 매우 가까운 값이 나오기에, Sigmoid 함수도 gradient가 0이 되는 문제가 생길 수 있다.

 

다음으로 이전의 Softmax loss function과 svm loss function을 구현한 방식으로 softmax_loss layer, svm_loss layer 구현하는 것이 있는데 이전에 했으니 설명은 스킵하겠다. softmax에서 numerical stability를 위해 x에서 최대 x를 빼 주고 계산 해 준다. cross entropy 함수에서 e^x 과정에서 score가 지수적으로 증가하여 overflow되는 현상을 예방 해 준다.

def svm_loss(x, y):
   
    loss, dx = None, None

    # *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

    num_train = x.shape[0]

    y_mask = np.zeros_like(x,dtype='bool')
    y_mask[np.arange(num_train),y] = True
    x_correct = x[y_mask]
    margin =  x - np.reshape(x_correct,[-1,1]) + 1
    margin[y_mask] = 0
    x_mask = margin > 0
    margin *= margin > 0
    loss = np.sum(margin) / num_train

    dx = np.zeros_like(x)
    dx[x_mask] = 1
    dx -= y_mask * np.reshape(np.sum(x_mask, axis=1),[-1,1])
    dx /= num_train
    
    # *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****
  
    return loss, dx


def softmax_loss(x, y):

    loss, dx = None, None

    
    # *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****
    
    num_train = x.shape[0]
    x_c = x - np.reshape(np.max(x,axis=1),[-1,1])
    
    y_mask = np.zeros_like(x,dtype='bool')
    y_mask[np.arange(num_train),y] = True
    x_correct = x_c[y_mask]
    loss = -np.sum(np.log(np.exp(x_correct) / np.sum(np.exp(x_c),axis=1)))
    loss /= num_train

    dx = np.zeros_like(x)
    dx += np.exp(x_c) / np.reshape(np.sum(np.exp(x_c),axis=1),[-1,1])
    dx[y_mask] -= 1.
    dx /= num_train

    # *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****
   
    return loss, dx

다음은 TwoLayerNet class에 있는 함수들을 구현하면 된다.

def __init__(
        self,
        input_dim=3 * 32 * 32,
        hidden_dim=100,
        num_classes=10,
        weight_scale=1e-3,
        reg=0.0,
    ):
       
        self.params = {}
        self.reg = reg

        
        # *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

        self.params['W1'] = np.random.randn(input_dim,hidden_dim) * weight_scale
        self.params['b1'] = np.zeros([hidden_dim,])
        self.params['W2'] = np.random.randn(hidden_dim,num_classes) * weight_scale
        self.params['b2'] = np.zeros([num_classes,])

        # *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****
        
    def loss(self, X, y=None):
        
        scores = None
       
        # *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

        Layer1out, Layer1cache = affine_forward(X, self.params['W1'], self.params['b1'])
        Relu1out, Relu1cache = relu_forward(Layer1out)
        Layer2out, Layer2cache = affine_forward(Relu1out, self.params['W2'], self.params['b2'])
        scores = Layer2out

        # *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****
        

        # If y is None then we are in test mode so just return scores
        if y is None:
            return scores

        loss, grads = 0, {}
        
        # *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

        loss, softmaxdx = softmax_loss(Layer2out,y)

        Layer2dx, Layer2dW, Layer2db = affine_backward(softmaxdx,Layer2cache)
        Layer2dW += self.reg * self.params['W2']
        grads['W2'] = Layer2dW
        grads['b2'] = Layer2db

        Reludx = relu_backward(Layer2dx,Relu1cache)
        
        Layer1dx, Layer1dW, Layer1db = affine_backward(Reludx,Layer1cache)
        Layer1dW += self.reg * self.params['W1']
        grads['W1'] = Layer1dW
        grads['b1'] = Layer1db

        Layer1reg = 0.5 * self.reg * self.params['W1'] * self.params['W1']
        Layer2reg = 0.5 * self.reg * self.params['W2'] * self.params['W2']
        loss += np.sum(Layer1reg) + np.sum(Layer2reg)

        # *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****
        
        return loss, grads

먼저 Layer만큼 params를 구현한다. (W1, b1, W2, b2) 각 layer의 크기는 layer에 따라 input_size, hidden_size, output_size 에 맞추어야 한다. 입력값을 순차적으로 affine_forward (layer1), relu_forward, affine_forward (layer2) 해주면 출력 score들이 나오게 되고, 여기서 softmax 함수를 마지막에 취해 주면 loss와 softmaxdx 가 나오게 된다. softmaxdx를 backpropagation 해 주며 각 layer의 dW, db를 구하여 grads에 반영 해 준다. regularization 함수도 잊지 않고 loss, grads에 반영 해 주면 모든 loss와 grads 채우기가 끝나게 된다.

 

다음으로 실제 CIFAR-10 데이터를 분류해 볼 차례이다. Solver() 클래스를 통해 cross validate 하며 최적의 신경망을 찾아낸다. Solver()클래스는 model, data, update_rule, optim_config{learning_rate},lr_decay, num_epochs, batch_size 등을 hyperparameter 로 정해

 

(X_train의 데이터 개수 // batch_size) * num_epochs 만큼 iteration 하며 batch_size만큼의 데이터들을 학습하여 update_rule(sgd, adam, momentum 등) 을 learning_rate, lr_decay를 사용하여 각 layer들의 parameter들을 업데이트 해 준다. 나는 여기서 learning_rates, regularization_strenghts, hidden_dims 를 조절 해 가며 cross validate 해 보았다.

best_model = None
best_accuracy = -1
results = {}

# *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

learning_rates = [1e-3, 1e-4]
regularization_strengths = [0., 1e1]
hidden_dims = [64, 128]
epochs = [10]
for lr in learning_rates:
  for reg in regularization_strengths:
    for hd in hidden_dims:
      for epoch in epochs:
        model = TwoLayerNet(hidden_dim = hd, reg = reg)
        solver = Solver(model,data,optim_config = {'learning_rate': lr}, num_epochs = epoch, verbose = False)
        solver.train()
        accuracy = solver.check_accuracy(data['X_val'],data['y_val'])
        if accuracy > best_accuracy:
          best_model = model
          best_accuracy = accuracy
        results[(lr,reg,hd,epoch)] = accuracy


for lr,reg,hd,epoch in sorted(results):
  print('learning rates:',lr, 'regularization:',reg, 'hidden_dims:',hd, 'epochs:',epoch,'accuracy:', results[(lr,reg,hd,epoch)])

print('best accuracy is:', best_accuracy)
# *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

결과는 lr = 0.001, reg = 0, hidden_dims = 64, epochs = 10 일때 최적의 결과를 보였다.

여기서 hidden_dims란 Twolayer 중간에 있는 layer의 dimension을 뜻한다. 기존의 단일 layer를 통한 분류의 각 W를 시각화 했을때는, 각 class마다의 사진의 평균이 나왔다. TwoLayerNet의 layer를 늘려주면 중간 layer는 실제 class predict 하기 이전의 사진을 분류 해준다. 강의 영상에서 한 학생이 질문한 것이 있는데 기존의 1layer의 horse class의 layer 사진은 양쪽으로 머리가 달린듯한 말이었는데, layer를 두개로 늘린다면 컴퓨터는 중간 layer에서 오른쪽을 본 말, 왼쪽을 본 말 이렇게 두개를 분류 할 수도 있다는 것이다. 그리고 이 두개의 가중치는 당연히 horse class로 가는 가중치가 높게 형성 될 것이다. 그렇게 해서 layer를 늘릴수록 가령 어떤 layer는 물체의 outline을 중심으로 본다던가, 어떤 layer는 물체의 색깔을 본다던가, 어떤 layer는 물체 외의 배경의 색깔을 본다던가 등 각 layer마다의 판단과 분류를 거쳐 컴퓨터가 다양한 데이터에도 대응할 수 있게 된다. 

 

그렇다면 TwoLayerNet에서 hidden_dims가 무조건 높을수록 더 많은 데이터에 대응할 수 있어 좋은 것이 아닌가? 생각할 수 있다. 그렇지만 오히려 너무 많은 분류를 해 오히려 컴퓨터가 헷갈릴 수도 있을 것 같다. 예를들어 오른쪽 본 말, 왼쪽 본 말, 정면 본 말, 뒤를 본 말, 45도 각도 말, .. 등 여러가지를 분류 할 수록, 뒤를 본 말 등의 분류는 뒤를 본 사슴과도 헷갈릴 수 있다. 그렇게 되어 분류가 너무 많으면 각 class의 확실한 특징을 잡지 못할 수 있기 때문에 무조건 정확도는 좋다고 할 수 없다고 생각했다. 실제 위의 데이터에도 몇몇 cross validate 사례를 보면 hidden_dims 가 64 일때가 128보다 정확도가 높게 나왔다. 밑에는 hidden layer를 시각화 한 결과들이다.

hidden_layer = 50 시각화
hidden_layer 100 시각화
best model의 hidden_layer 시각화

우리는 눈으로 보았을때 컴퓨터가 hidden_layer에서 무엇을 중점으로 보았는지 정확히는 알 수 없지만, 컴퓨터가 확실히 중간에 한 layer를 거쳐 분류한다는 것을 볼 수 있다.

 

Inline Question 2:

Now that you have trained a Neural Network classifier, you may find that your testing accuracy is much lower than the training accuracy. In what ways can we decrease this gap? Select all that apply.

  1. Train on a larger dataset.
  2. Add more hidden units.
  3. Increase the regularization strength.
  4. None of the above.

YourAnswer: 1,3

YourExplanation: By Training larger dataset, we can get more various different kinds of data for prediction. For example for when we classify and predict a picture of dog, when we get larger size of data, our model can get more dataset of dog and can classify better. Increasing regularization strength, can also decrease the gap. Remember the egularization helps layers' each W value to be distributed, which can reduce overfitting to the training datasets.

 

마지막 문제는 testing accuracy와 training accuracy를 줄일 수 있는 방법에 대해 고르는 것이다. 먼저

 

1. 훈련 데이터가 많아지면, 당연히 test_accuracy가 오를 수 있을 것이고, 다양한 훈련 데이터가 있을수록 overfitting이 발생할 확률도 줄 것이므로 답이 될 것이다.

2. hidden units은 많아지면 정확도가 높아질 수도 있지만 overfitting이 발생할 확률도 높아지기 때문에 반드시 그렇진 않다.

3. regularization strength를 높이면 overfitting이 줄어들 것이므로 train accuracy를 낮추어 test accuracy 와의 차이를 줄일 수 있을 것이다. 근데 regularization strength를 높인다고 test accuracy도 증가하는 것은 아니다