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

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

이걸 코드로 써주면 된다.
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
미분만 제대로 해준다면 문제가 될 것은 없어보인다.
위에 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를 사용하는 것이 맞는 판단이다.