softmax와 분류(Classification)
일반적인 분류 문제는 클래스 간의 순서는 의미를 담지 않습니다. 단지 분류만을 목적으로 하는 경우가 많습니다. 그러나 분석을 위해서는 이러한 데이터를 수치로 변환하여야 합니다. one-hot coding은 이러한 목적에 부합합니다. 즉, 다음 표와 같이 샘플이 대응되는 클래스(카테고리)에 1 그렇지 않으면 0을 할당합니다. 예를들어 데이터의 라벨이 cat, dog, chicken의 세 카테고리로 구성되어 있다면 원-핫인코딩은 다음과 같이 나타낼 수 있습니다.
cat | dog | chicken | |
---|---|---|---|
cat | 1 | 0 | 0 |
dog | 0 | 1 | 0 |
chicken | 0 | 0 | 1 |
그러므로 라벨 y의 집합은 다음과 같이 나타낼 수 있습니다.
y ∈ {(1,0,0), (0,1,0), (0,0,1)}
다음 그림은 regression과 classification의 신경망을 나타낸 것입니다.
위 그림은 4개의 feature를 가진 데이터에서 회귀(regression)는 한개의 출력, 분류(classfication)의 경우 3개의 출력을 가지는 경우입니다. 회귀의 경우 4개의 가중치를 가지지만 분류의 경우 각 feature에서 각 출력 카테고리에 대한 가중치를 가지므로 총 4 × 3 =12개의 가중치를 가집니다. 이를 수식으로 나타내면 다음과 같습니다.
\begin{align} \hat{y}_1&=x_1w_{11}+x_2w_{12}+x_3w_{13}+x_4w_{14}+b_1\\\hat{y}_2&=x_1w_{21}+x_2w_{22}+x_3w_{23}+x_4w_{24}+b_2\\\hat{y}_3&=x_1w_{31}+x_2w_{32}+x_3w_{33}+x_4w_{34}+b_3\\ \tag{식 1}\begin{bmatrix}\hat{y}_1\\\hat{y}_2\\\hat{y}_3\end{bmatrix}&=\begin{bmatrix}w_{11}&w_{12}&w_{13}&w_{14}\\w_{21}&w_{22}&w_{23}&w_{24}\\w_{31}&w_{32}&w_{33}&w_{34}\end{bmatrix} \begin{bmatrix}x_1\\x_2\\x_3\\x_4\end{bmatrix}+\begin{bmatrix}b_1\\b_2\\b_3\\b_4\end{bmatrix}\\ &\quad \rightarrow \hat{y}=WX+b\end{align}
위 그림과 같이 단층 신경망으로 이루어진 분류 모델을 softmax regression이라고 합니다. 각 출력 y1, y2, y3는 모든 입력 x1, x2, x3, x4에 관계되므로 softmax regression의 출력층 또한 fully-connected layer가 됩니다.
분류문제의 출력은 그 자체의 의미는 크지 않습니다. 그러므로 위의 출력 $\hat{y}$는 특성들에 의해 각 클래스와의 확률로 간주 합니다. 이 경우 위와 같은 선형회귀의 결과는 다음의 문제를 가집니다.
- 모든 출력의 합 $\sum^k_{j=1}\hat{y}_j=1$에 대해 보장할 수 없습니다. k는 클래스 수입니다.
- $\hat{y}_j \ge 1$을 보장할 수 없습니다.
출력이 음수가 아님을 보장하기 위해 지수함수를 사용합니다. 즉 $P(y=i) \prop \exp(\hat{y}_i$ 또한, 출력의 합이 1임을 보장하기 위해 정규화 방법을 적용합니다. 즉 각 출력을 모든 출력의 합으로 나누어 줍니다. 이 두 과정을 결합한 것을 소프트맥스(softmax)라고 합니다.
\begin{align}\tag{식 2}\hat{y} &= \text{softmax(\hat{y}_i)} \\ \hat{y_j} &=\frac{\exp(o_j)}{\sum^k_j\exp(o_j)} \end{align}
위 분류모형 즉, softmax regression은 모델의 출력을 확률로 해석하는 것입니다. 관찰된 데이터의 가능성을 최대화하는 확률을 생성하기 위해 매개변수를 최적화합니다. 그런 다음 예측을 생성하기 위해 임계값을 설정합니다. 예를 들어 최대 예측 확률이 있는 레이블을 선택합니다. 공식적으로, 출력 $\hat{y}_j$는 j클레스에 속할 확률로 해석하여 최대의 확률을 가진 클래스를 선택합니다. 예를 들어, $\hat{y}_1,\;\hat{y}_2,\;\hat{y}_3$의 값이 각각 0.1, 0.8 및 0.1이면 범주 2를 예측합니다.
$$\tag{식 3}\underset{j}{\text{argmax}}\; \hat{y_j}=\underset{j}{\text{argmax}}\; o_j$$
식 2의 결과는 $\sum^k_{j=1}\hat{y}_j = 1$이 될 것입니다. 실제로는 선형 층의 결과를 비선형으로 변환하여 로그화하여 사용합니다. 비록 softmax가 비선형 함수이긴 하지만 그 회귀의 결과는 여전히 입력 특성들의 affin 변환에 의해 결정됩니다. 결과적으로 softmax regression은 선형모델입니다.
다음은 sklearn.dataset에서 제공하는 iris 데이터에 대한 softmax 회귀를 패키지 pytorch를 사용하여 실행합니다.
import numpy as np from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split import torch import torch.nn.functional as F
da=load_iris() da['data'][:3]
array([[5.1, 3.5, 1.4, 0.2], [4.9, 3. , 1.4, 0.2], [4.7, 3.2, 1.3, 0.2]])
da['target'][:3]
array([0, 0, 0])
라벨을 구성하는 클래스는 numpy.unique()
로 확인할 수 있습니다.
np.unique(da['target'])
array([0, 1, 2])
위 결과와 같이 da['data']가 특성(feature), da['target']는 라벨(label)로 구성되었으며 데이터를 학습과 검정 그룹으로 구분하기 위해 klearn.model_selection 모듈의 train_test_split()
함수를 사용합니다.
xtr, xte, ytr, yte=train_test_split(da['data'], da['target'], test_size=0.3, random_state=3) xtr[:3]
array([[6.4, 3.2, 4.5, 1.5], [5.1, 3.3, 1.7, 0.5], [6. , 2.7, 5.1, 1.6]])
ytr[:3]
array([1, 0, 1])
xtr.shape
(105, 4)
위 자료들은 numpy 배열형이므로 tensor로 전환합니다.
xtr1, xte1, ytr1, yte1=map(torch.tensor, (xtr, xte, ytr, yte)) print(xtr1.shape, xte1.shape)
torch.Size([105, 4]) torch.Size([45, 4])
ytr1[:10]
tensor([1, 0, 1, 2, 1, 0, 0, 2, 1, 1], dtype=torch.int32)
라벨을 원-핫 인코딩으로 변환합니다. nn.functional.one-hot(x, class_num)
를 사용합니다. 이 함수의 인수는 객체 x와 클래스의 수이며 클래스 수는 객체 x의 최대값과 같거나 커야 합니다. 또한 x는 torch.LongTensor 즉, torch.int64이어야 합니다. 위 데이터 ytr1, yte1의 자료형은 int32이므로 torch.as_tensor(객체, 자료형)
함수를 사용하여 변환합니다.
ytr2=torch.as_tensor(ytr1, dtype=torch.int64) yte2=torch.as_tensor(yte1, dtype=torch.int64) ytr2.dtype
torch.int64
ytrOH=F.one_hot(ytr2, 3) yteOH=F.one_hot(yte2, 3) ytrOH[:3]
tensor([[0, 1, 0], [1, 0, 0], [0, 1, 0]])
위 결과에 의하면 label의 클래스는 0,1,2로 3개이고 4개의 feature로 구성되어 있습니다. 그러므로 가중치의 차원은 (4, 3)이 됩니다. 이를 근거로 먼저 모델에 사용될 가중치 객체를 초기화하고 모델을 구성합니다.
모델은 가중치를 고려하여 softmax함수를 적용하고 비용함수를 선언합니다. 이 결과를 기준으로 가중치를 개선하기 위해 과정을 반복합니다.
softmax 함수는 torch.nn.fucntional.sofmax(tensor, dim)
을 사용합니다. 이 경우
- tensor: xtr @ W +b
- 위 행렬곱은 xtr의 각행은 W의 각 열과의 곱으로 데이터 하나의 인스턴스의 예측치를 생성합니다. 결과적으로 하나의 인스턴스의 softmax의 계산은 한 행의 열로 구성되어 있는 각 클래스들의 합을 기준으로 계산됩니다. 열의 차원은 1이므로 위 함수의 dim=1이 됩니다.
위 softmax() 함수내의 계산은 다음과 같이 이루어집니다.
\begin{align}&\begin{bmatrix}x_{11}&\cdots&x_{1p}\\x_{21}&\cdots&x_{2p}\\\vdots& \ddots & \vdots\\x_{n1}&\cdots&x_{np}\end{bmatrix} \begin{bmatrix}W_{A1}&\cdots&W_{N1}\\\vdots&\ddots&\vdots\\W_{AP}&\cdots&W_{NP}\end{bmatrix}+b\\&=\begin{bmatrix} (x_{11}W_{A1}+\cdots+x_{1p}W_{AP})+b&\cdots &(x_{11}W_{N1}+\cdots+x_{1p}W_{NP})+b\\\vdots&\ddots&\vdots\\(x_{n1}W_{A1}+\cdots+x_{np}W_{AP})+b&\cdots &(x_{n1}W_{N1}+\cdots+x_{np}W_{NP})+b \end{bmatrix} \end{align}
위 결과의 각 행이 각 샘플의 예측치가 됩니다. 즉, 확률로 전환하는 softmax() 함수는 각 행의 합에 대해 각 열의 크기를 고려합니다. 그러므로 softmax() 함수의 계산 차원은 열이 됩니다(dim=1). softmax 모델은 다음과 같습니다.
model=F.softmax(data.matmul(W)+b, dim=1)
#가중치와 편차 초기화 W=torch.rand((4, 3), requires_grad=True, dtype=torch.double) b=torch.rand(1, requires_grad=True, dtype=torch.double) # optimizer 설정 optimizer = torch.optim.SGD([W, b], lr=0.1)
W.dtype
def model(x, W, b): return(F.softmax(x.matmul(W)+b, dim=1))
이 모델에서 비용함수는 cross_entropy loss 함수를 사용합니다.
n=10000 costT=[] for epoch in range(n+1): #순전파 pre=model(xtr1, W, b) cost=(-ytrOH*torch.log(pre)).sum(dim=1).mean() costT.append(cost.item()) #역전파 optimizer.zero_grad() cost.backward() optimizer.step()
plt.figure(figsize=(3,2)) plt.plot(costT) plt.xlabel("epoch") plt.ylabel("cost") plt.show()
위 연산 결과의 모델에 데이터를 적용한 예측치의 각행의 합은 1이 됩니다.
xtr_pre=model(xtr1, W, b) xtr_pre[:3].sum(dim=1)
tensor([1., 1., 1.], dtype=torch.float64, grad_fn=<SumBackward1>)
행 단위로 가장 큰 확률의 열에 해당하는 클래스가 최종 예측 결과가 됩니다. 행단위로 가장 큰 값을 찾기 위해 torch.argmax(data, dim)
함수를 적용합니다.
torch.argmax(xtr_pre, dim=1)
tensor([1, 0, 2, 2, 1, 0, 0, 2, 1, 1, 0, 2, 0, 2, 1, 0, 0, 2, 1, 0, 0, 1, 2, 2, 0, 2, 1, 0, 0, 2, 2, 2, 1, 1, 1, 0, 0, 2, 2, 1, 2, 1, 2, 0, 2, 0, 1, 1, 2, 2, 0, 1, 0, 1, 1, 1, 0, 2, 0, 2, 1, 2, 1, 2, 1, 0, 2, 1, 2, 1, 0, 1, 2, 0, 1, 0, 0, 0, 1, 2, 0, 0, 2, 0, 1, 2, 1, 2, 2, 1, 1, 2, 1, 0, 1, 1, 0, 1, 2, 2, 2, 0, 0, 1, 2])
위 결과와 실측값 (ytr1)이 같으면 True(1), 다르면 False(0) 을 부여하여 모두 True인 경우로 나누어주면 이 예측의 정확도를 결정할 수 있습니다.
acc=torch.sum(torch.argmax(xtr_pre, dim=1)==ytr1).item()/len(ytr1) round(acc, 3)
높은 정확도를 보입니다. 검정 데이터에 대해 위 과정을 적용합니다.
xte_pre=model(xte1, W, b) acc_te=torch.sum(torch.argmax(xte_pre, dim=1)==yte1).item()/len(yte1) round(acc_te, 3)
학습데이터와 검정 데이터 사이에 차이가 존재합니다. 즉, 과적합(over estimation)의 가능성이 있습니다. 과적합을 감소시키기 위한 방법을 검토해야 합니다.
위 코드에서 모델에 softmax()함수를 적용하고 비용함수에서 그 결과에 대한 log 값의 전환을 위해 torch.log()함수를 사용했습니다. pytorch는 이 두 함수를 결합한 torch.nn.functional.log_softmax()
를 제공합니다. 다음 코드는 이 두 함수를 비교한 것입니다.
com1=torch.log(F.softmax(xtr1[:3,:].matmul(W)+b, dim=1)) com1
tensor([[-8.8894e+00, -1.4739e-03, -6.6188e+00], [-3.5095e-03, -5.6540e+00, -3.4796e+01], [-1.7533e+01, -3.4193e+00, -3.3283e-02]], dtype=torch.float64, grad_fn=<LogBackward>)
com2=F.log_softmax(xtr1[:3,:].matmul(W)+b, dim=1) com2
tensor([[-8.8894e+00, -1.4739e-03, -6.6188e+00], [-3.5095e-03, -5.6540e+00, -3.4796e+01], [-1.7533e+01, -3.4193e+00, -3.3283e-02]], dtype=torch.float64, grad_fn=<LogSoftmaxBackward>)
torch.nn.functional.log_softmax()
를 적용한 비용함수는 다음과 같습니다.
cost=(-y*torch.nn.functional.log_softmax(x)).sum(dim=1).mean()
위 연산의 y는 one-hot encoding으로 전환된 객체이어야 합니다. 그러나 F.null_loss()
함수는 y를 one-hot으로 전환하지 않은 정수값 그대로 전달하고 비용을 계산하는 함수를 제공합니다. 이 함수는 각 인스턴스의 총 합의 평균을 동시에 계산합니다.
cost=F.nll_loss(F.log_softmax(x), y)
(-ytrOH[:3]*com2).sum(dim=1).mean()
F.nll_loss(com2, ytr2[:3])
위 함수는 null_loss()는 softmax()를 인수로 받습니다. 이 두 함수를 결합한 cross_entropy()함수를 사용할 수 있습니다. 즉, 다음과 같습니다.
F.log_softmax()+F.nll_loss()=F.cross_entropy()
- F.cross_entropy()에 전달되는 예측모형은 선형모형입니다.
- 이 함수에 전달하는 y는 one-hot 인코딩 형태가 아닌 정수형이어야 합니다.
위 함수는 비용함수 내에 softmax()함수를 포함하고 있기 때문에 전달되는 인수는 가중치와 편차를 고려한 선형모형입니다.
F.cross_entropy(xtr1[:3,:].matmul(W)+b, ytr2[:3])
선형모형 nn.Linear()
과 F.cross_entropy()
를 적용하여 모델을 다시 구현합니다.
xtr1.dtype, ytr2.dtype
model2=torch.nn.Linear(4, 3) optimizer=torch.optim.SGD(model2.parameters(), lr=0.1) n=1000 costT=[] accT=[] for epoch in range(n+1): pre=model2(xtr1.to(torch.float)) cost=F.cross_entropy(pre, ytr1.to(torch.int64)) costT.append(cost.item()) acc=torch.mean((torch.argmax(model2(xtr1.to(torch.float)), dim=1)==ytr1).float()) accT.append(acc.item()) optimizer.zero_grad() cost.backward() optimizer.step()
fig, ax=plt.subplots(1,2, figsize=(8, 2)) ax[0].plot(costT) ax[0].set_xlabel("Epoch") ax[0].set_ylabel("Cost") ax[1].plot(accT) ax[1].set_xlabel("Epoch") ax[1].set_ylabel("Accuacy") plt.show()
#검정데이터의 정확도 torch.mean((torch.argmax(model2(xte1.to(torch.float)),dim=1)==yte1).float())
tensor(0.9778)
댓글
댓글 쓰기