cs231n 과제1 Q3- Implement a Softmax classifier

이준학·2024년 7월 17일

cs231n 과제

목록 보기
3/15

  이번 글은 softmax classifier를 구현하면서 몰랐거나 중요한 부분들을 다뤄보려고 한다. 꽤 오래 걸려서 힘들었다.

1. Softmax Naive

  먼저 softmax_loss_naive 함수부터 살펴보자. 이 부분을 해결하는데 꽤 오랜 시간을 썼다. softmax 미분이 헷갈린 것이 가장 큰 이유였다. 일단 naive 버전이라 for문을 사용해서 코드를 돌려야 하는데, 이는 강의 자료를 따라서 X_dev의 숫자인 500만큼 돌리면 된다. 문제는 gradient 계산이었다. 아래의 두 사진은 내가 블로그를 참고하여 softmax의 미분을 해본 것이다.

  우리의 목적은 loss function인 LLWkW_k에 대해서 미분하는 것이다. 여기서 kk는 class label을 의미한다. kk라는 변수를 통해 0~9까지의 클래스를 돌며 gradient를 계산하는 것이다. 어김없이 chain rule을 적용해서 차례차례 편미분 값을 계산하면 된다. 위에 쓴 function들은 cs231n 강의 노트에 정의된 개념을 가져온 것이다. ssff로 사용하기도 한다. 내가 계산 실수를 했던 것은 pyip_{y_i}sks_k에 대해 미분하는 부분이다. eskce^{s_k-c}가 나오는 것을 생각하지 못했다. 시그마가 모든 클래스에서의 esjce^{s_j-c}값을 다 더하기 때문에, 이를 미분하면 eskce^{s_k-c}를 얻을 것이다. 이것만 제대로 하면 문제는 없다.

이걸 코드로 써주면 된다.

def softmax_loss_naive(W, X, y, reg):
##############################
#### 함수가 일부분 생략됨. ####
##############################
  for i in range(num_dev):
      score=X[i].dot(W)# (10,) 
      score-=np.max(score) #logC=-max_j(f_j) in the lecture module for numerical stability

      softmax=np.exp(score[y[i]])/np.sum(np.exp(score))
      loss-=np.log(softmax)

      p= lambda k: np.exp(score[k])/np.sum(np.exp(score)) #intermediate value p for gradient calculation. p means softmax function for a given class j.

      for k in range(num_class):
        p_k=p(k) # softmax output for class k.
        dW[:,k]+=(p_k-(k==y[i]))*X[i]
        
    
    loss/=num_dev
    loss+=reg*np.sum(W*W)
    dW/=num_dev
    dW+=2*reg*W
    pass

return loss, dW

미분만 제대로 해준다면 문제가 될 것은 없어보인다.

2. Softmax Vectorized

  위에 naive 버전을 더 빠르게 하기 위한 함수이다. softmax_loss_vectorized 함수의 일부분을 가져와 정리하겠다. 이 과정도 몇 번 해보니까 그닥 어렵진 않았다. 그런데 헷갈렸던 부분은 여전히 있었다.

def softmax_loss_vectorized(W, X, y, reg):
##############################
#### 함수가 일부분 생략됨. ####
##############################

    score=X.dot(W) #(500,10)
    score-=np.max(score,axis=1).reshape(-1,1) #(500,10)
    sum_score=np.sum(np.exp(score),axis=1).reshape(-1,1) #(500,1)
    p=np.exp(score)/sum_score #(500,10)
    loss = np.sum(-np.log(p[np.arange(num_dev), y]))

    mask=np.zeros_like(p) #(500,10)
    mask[np.arange(num_dev),y]=1 #1 for correct classes
    intermediate=p-mask #(500,10)
    dW=X.T.dot(intermediate)



    loss/=num_dev
    loss+=reg*np.sum(W*W)
    dW/=num_dev
    dW+=2*reg*W
    pass

return loss, dW

  위의 함수에서 보면, np.arange를 많이 사용하는 것을 볼 수 있다.

1) mask[np.arange(num_dev),y]=1
2) loss = np.sum(-np.log(p[np.arange(num_dev), y]))

  위 두 식에서 사용했는데, 난 처음에 np.arange(num_dev) 대신에 :를 사용했다. 즉, mask[:,y]=1 이라고 썼다는 것이다. 이렇게 했더니 gradient와 loss difference가 너무 컸다.

  정리하자면, [np.arange(num_dev),y] 랑 [:,y]랑 뭐가 다른지 그 차이를 몰랐던 것이다.
먼저 [np.arange(num_dev),y]부터 살펴보자. np.arange(num_dev)는 num_dev=500이기 때문에 [0,1,2,...,499]의 배열을 준다. 따라서 [np.arange(num_dev),y]를 들여다보면 [[0,1,2...499],[1,3,5,2,0...2]]와 같이 나올 것이다. 뒤의 y 배열은 내가 임의로 'class label이 이런식으로 적혀 있을것이다' 라고 적어놓은 것이다. 실제로 y 배열은 차이가 있을 것이다. 어쨌든 [[0,1,2...499],[1,3,5,2,0...2]]가 가지는 의미는 [0,1], [1,3], [2,5] 와 같이 배열의 원소에 접근해 그 원소들을 1로 만든다는 것만 기억하면 된다.
원활한 이해를 위해서 더 쉬운 예제를 보자.

a=[[1,2,3],
   [4,5,6]]
print(a[[0,1],[1,1]] # [a[0,1] a[1,2]]이 출력 됨. 즉, [2 5]

a=[[1,2,3],
[4,5,6]]
print(a[[0,1],[1,1]]) # [a[0,1] a[1,2]]이 출력 됨. 즉, [2 5]

  이런 식으로 연산이 진행된다는 뜻이다. 반면 [:,y]는 조금 다르다. 위에서 들었던 예시를 이용해서 알아보자. 똑같은 a 배열에 a[:,[1,2]]를 출력하면 아래와 같이 나온다. elementwise로 접근할 수 없는 것이다.

[[2, 3],
 [5, 6]]

그렇기 때문에 여기서는 np.arange를 사용하는 것이 맞는 판단이다.

3. 참고

https://bruceoutdoors.wordpress.com/2016/04/30/cs231n-assignment-1-tutorial-q3-implement-a-softmax-classifier/

내 풀이 링크:

https://github.com/danlee0113/cs231n

profile
AI/ Computer Vision

0개의 댓글