by museonghwang

CS231n Lecture3 Review

|

Hits


해당 게시물은 Standford 2017 CS231n 강의와 2022년 슬라이드를 바탕으로 작성되었습니다.

image


Linear Classifier: Choose a good $W$

image

  • 본 강의에서는 어떻게 training data를 이용하여 가장 좋은 행렬 $W$를 구하는지에 대해 다룹니다.
  • 위 예시는 세개의 training data에 대한 임의의 행렬W를 가지고 예측한 10개의 클래스 스코어입니다.
  • 알고리즘을 만들고, 어떤 W가 가장 좋은지 결정하기 위해서는 지금 만든 W가 좋은지 나쁜지를 정량화 할 방법이 필요합니다.
    • Loss Function은 입력과 W와의 dot product를 통해 출력한 class score가 정량적으로 얼마나 나쁜지를 결정하는 함수입니다.
    • 즉, 최적의 $W$를 결정하기 위해 필요한 함수입니다.($W$의 optimization에 필요한 함수)
    • optimization은 손실 함수를 최소화하는 최적의 매개변수(parameter) $W$를 찾는 과정입니다.


Suppose: 3 training examples, 3 classes

With some W the scores $f(x, W)=Wx$ are:

image

간단한 예로, 고양이(cat), 자동차(car), 개구리(frog)의 3개의 class를 분류하는 classifier가 임의의 $W$ 값을 가진다고 할때, 위와 같이 세 개의 이미지에 대한 3개의 class score 가 임의로 나오게 됩니다.

  • “고양이” 클래스는 잘 분류되지 못했고 “자동차”는 잘됐고 “개구리”는 최악입니다. 개구리 점수는 다른 것보다 더 낮습니다.
  • 우리가 원하는 진정한 classifier는 고양이 이미지에서는 cat에 대한 score 가 가장 높게 나오고, 자동차 이미지에서는 car에 대한 score 가 가장 높게 나오고, 개구리 이미지에서는 frog에 대한 score 가 가장 높게 나오는 것입니다.


따라서 최적의 $W$ 를 찾아서 classifier가 이미지들을 잘 분류하고 있는지 검사를 해야합니다. 즉, $W$ (weight) 가 좋은지 아닌지 정량화 할 수 있는 기준이 필요합니다.


손실 함수(Loss Function)현재 분류기(classifier)가 얼마나 좋은지 를 알려줍니다. 다르게 표현하면 현재의 $W$ 가 얼마나 BADNESS 한지 를 알려주는 것입니다.

  • 주어진 데이터셋의 샘플 : ${(x_i, y_i)}^N_{i=1}$
  • $x_i$ : 이미지
  • $y_i$ : (정수)라벨(label),
    • 즉, 입력 이미지 $x_i$에 대한 정답 카테고리
    • CIFAR-10의 경우 y는 10개
  • $f(x,W)=Wx$ : 입력 이미지 $x_i$와 가중치 행렬 $W$를 입력으로 받아서 새로운 테스트 이미지에 대해 $y_i$를 예측


데이터셋에 대한 Loss각 N개의 샘플에 대한 손실의 평균 입니다.

[L=\frac{1}{N}∑_iL_i(f(x_i,W),y_i)]

Multi-class SVM loss

Multi-class classification 문제에 사용할 수 있는 Loss function 중 하나인 SVM Loss를 살펴보겠습니다.

우선 스코어 벡터 $s$를 간결하게 나타냅니다.

  • $s=f(x_i,W)$
  • 예를 들어 $j$번째 클래스의 점수 $j$번째 요소입니다.
  • $s_j=f(x_i,W)_j$

그다음 SVM 손실함수 $L_i$를 정의합니다.

[L_i=∑{j≠y_i}max(0, s_j − s{y_i} + 1)]

  • $s_j$ : 정답이 아닌 클래스의 스코어
  • $s_{y_i}$ : 정답 클래스의 스코어
  • 1 : safety margin
  • 여기서 $s_{y_i}$가 $s_j+1$보다 크면 loss는 0이 됩니다.

Hinge loss(힌지 로스)

이 손실함수의 그래프 모양 때문에 SVM Loss를 hinge loss라고 부르기도 합니다. 또한 정답 카테고리의 점수가 올라갈수록 Loss가 선형적으로 줄어드는 것을 알 수 있습니다. 해당 loss는 0이 된 이후에도 Safety margin을 넘어설 때 까지 더 줄어듭니다.

image

  • Linear Classification인 $f(x_i, W)$ 에서 나온 score를 $s_j$ 라고 하고,
  • 정답인 클래스의 score를 $s_{y_i}$ 라고 할때, 나머지 클래스와 이 값을 비교합니다.
  • 정답인 클래스와 나머지를 비교했을때, 정답보다 다른 클래스의 점수가 더 높다면 이 차이 만큼이 Loss 라고 정의합니다.
  • 또한 위에서 구한 Loss에서 safety margin이라는 값을 추가합니다. 이는 정답 클래스가 적어도 다른 클래스보다 safety margin 값 만큼은 커야 한다는 이야기이며, 여기서는 safety margin=1 입니다.
    • 같은말로, 예측 값과 정답 값에 대한 상대적인 차이를 주기 위해 설정 합니다.
    • 즉, safty margin은 정답클래스의 score가 다른 score에 비해 safty margin만큼 높아야, loss값이 줄어들게 하기 위해 적용하는 것 입니다.
  • 이 Loss 값이 0보다 작은 음수 값인 경우에는 포함하지 않습니다.
  • 가로축은 $s_j - s_{y_i} + 1$ 값, 세로축은 $L_i$ 의 값인 Loss 값입니다.


Loss function을 의미하는 $L_i$$x_i$와 $W$로 이루어진 예측함수 $f$를 통해 만들어진 score 와 라벨 값 $y_i$를 입력으로 받아 해당 데이터를 얼마나 나쁘게 예측하는지를 정량화 시켜줍니다. 그리고 최종 Loss인 “L”$N$개의 training data에 대한 $L_i$들의 평균 이 됩니다.

이 함수는 아주 일반적인 공식이며, Image classification 외에도 다양하게 확장할 수 있습니다.

좀 더 나아가서 어떤 알고리즘이던 가장 일반적으로 진행되는 일은, 어떤 X와 Y가 존재하고, 우리가 만들 파라미터 W가 얼마나 좋은지를 정량화하는 손실 함수를 만드는 것입니다. 즉 $W$의 공간을 탐색하면서 training data의 Loss를 최소화하는 어떤 $W$를 찾게 될 것입니다. 예제로 살펴보겠습니다.

Calculate SVM Loss

$L_i$를 구하기 위해 올바른 카테고리의 스코어와 올바르지 않은 카테고리의 스코어를 비교하여 “True인 카테고리” 를 제외한 “나머지 카테고리 Y”의 합을 구합니다. 즉 맞지 않는 카테고리를 전부 합치는 것입니다. 만약 올바른 카테고리의 점수가 올바르지 않은 카테고리의 점수보다 더 높다면, 그리고 그 격차가 일정 마진(safety margin) 이상이라면, 이 경우 True인 스코어가 다른 false 카테고리보다 훨씬 더 크다는 것을 의미하며, 이렇게 되면 Loss는 0이 됩니다.

이미지 내 정답이 아닌 카테고리의 모든 값들을 합치면 그 값이 바로 한 이미지의 최종 Loss가 되고, 전체 training dataset에서 이 Loss들의 평균을 구합니다.

image

위와 같이 각 이미지에서 나온 class별 score을 이용해 SVM Loss를 계산했습니다.

cat image SVM Loss

[L_{cat} = ∑{j≠y{cat}}max(0, s_j − s_{y_{cat}} + 1)
= max(0, 5.1-3.2+1) + max(0, -1.7-3.2+1)
= max(0, 2.9) + max(0, -3.9)
= 2.9 + 0
= 2.9]

고양이 이미지에서는 정답 class(cat)에 대한 scor 가 3.2 인데 car class에 대한 score가 5.1로 더 높은 것을 보아 잘못 분류 한 것을 알 수 있고 계산한 Loss 값은 2.9 정도로 나온 것을 알 수 있습니다.

car image SVM Loss

[L_{car} = ∑{j≠y{car}}max(0, s_j − s_{y_{car}} + 1)
= max(0, 1.3-4.9+1) + max(0, 2.0-4.9+1)
= max(0, -2.6) + max(0, -1.9)
= 0 + 0
= 0]

자동차 이미지에서는 정답 class(car)에 대한 score가 4.9로 나머지 class보다 높아 잘 분류했다고 할 수 있고 그에 따라 계산한 Loss 값은 0이 나온 것을 알 수 있습니다.

frog image SVM Loss

[L_{frog} = ∑{j≠y{frog}}max(0, s_j − s_{y_{frog}} + 1)
= max(0, 2.2-(-3.1)+1) + max(0, 2.5-(-3.1)+1)
= max(0, 6.3) + max(0, 6.6)
= 6.3 + 6.6
= 12.9]

개구리 이미지에서는 정답 class(frog)에 대한 score 가 -3.1로 나머지 모든 class의 score 보다도 더 낮게 나온 것을 보아 엄청 잘 못 분류했음을 알 수 있고 그에 따라 Loss 값도 12.9로 엄청 크게 나온 것을 확인 할 수 있습니다.

Loss over full dataset is average:

[L=(2.9 + 0 + 12.9) / 3
= 5.27]

이를통해 모델이 잘못 예측한 정도(badness)에 따라 Loss값이 높아짐을 알 수 있습니다.

Multiclass SVM Loss의 계산 과정은 다음과 같이 정리할 수 있다.

  • 훈련 데이터 하나하나마다, 정답 class와 정답이 아닌 class간의 score를 비교하고, 이들을 모두 더한다.
    • 비교할 때, 정답 class의 score가 다른 class보다 1이상 높은 경우, 0이 되도록 1을 더해줌
    • 이때의 1을 safety margin이라고 함
  • 앞에서 구한 값들의 평균을 구한다.


Quiz

Q1. hinge loss에서 safety margin 1을 더하는 것은 어떻게 결정하는지?

  • 임의로 선택한 숫자같아 보이긴 하지만, 사실 손실함수의 “스코어가 정확이 몇인지”는 신경쓰지 않습니다. 우리가 궁금한건 여러 스코어 간의 상대적인 차이입니다. 즉 정답 스코어가 다른 스코어에 비해 얼마나 더 큰 스코어를 가지고 있는지 입니다.
  • 행렬 $W$를 전체적으로 스케일링한다 가정한다면 결과 스코어도 이에 따라 스케일이 바뀔 것입니다. 그렇다면 1이라는게 별 상관은 없습니다.

Q2. Car 스코어가 조금 변하면 Loss에는 무슨 일이 일어나는가?

  • SVM loss는 오직 정답 스코어와 그 외의 스코어와의 차이만 고려합니다.
  • 따라서 이 경우에는 Car 스코어가 이미 다른 스코어들보다 엄청 높기 때문에 Car의 스코어를 조금 바꾼다고 해도, 서로 간의 간격(Margin)은 여전히 유지될 것이고, 결국 Loss는 변하지 않습니다. 계속 0일 것입니다.

Q3. SVM Loss가 가질 수 있는 최대/최소값은?

  • 모든 클래스에 걸쳐 정답 클래스의 스코어가 제일 크면 모든 training data에서 loss가 0이 됩니다. 그러므로 최소값은 0입니다.
  • 만약 정답 클래스 스코어가 엄청 낮은 음수 값을 가지고 있다고 할 때, Loss는 무한대 일 것입니다. 그러므로 최대값은 $\infty$ 입니다.

Q4. 파라미터를 초기화하고 처음 학습을 시킬때 보통 $W$를 임의의 작은 값으로 초기화 시키는데 그렇다면 처음 학습 완료 후에는 모든 결과에 대한 score 가 임의의 일정한 작은값 (0에 근사) 을 갖게 됩니다. 이럴 경우의 multiclass SVM Loss 값이 어떻게 되나요?

  • Loss 값은 (class개수 - 1) * safty margin 이 됩니다.
  • 또한 이 방법은 디버깅할 때 유용히 쓰입니다. 모든 score 값이 근사해지면서 정답을 제외한 class의 score 에서 $max(0, s_j − s_{y_i} + 1)$ 의 값이 1 (safty margin)이 되게 됩니다. 그러므로 정답을 제외한 class score에 대한 Loss를 모두 합친 최종 Loss 값은 “(class개수 - 1) * safty margin” 이 됩니다.

Q5. SVM Loss는 정답인 클래스는 빼고 다 더했는데, loss 계산에서, 모든 class(정답 class와 정답 class 자신을 비교하는 경우 포함)에서 값을 구한 후, sum을 취하면 어떻게 되나요?

  • Loss에 1이 더 증가합니다.
  • 정답 클래스만 빼고 계산하는 이유는, 일반적으로 Loss가 0이 되야지만 우리가 “아무것도 잃는 것이 없다”고 쉽게 해석할 수 있으며, Loss에 모든 클래스를 다 더한다고 해서 다른 분류기가 학습되는 것은 아닙니다. 하지만 관례상 정답 클래스는 빼고 계산을 하며, 그렇게 되면 최소 Loss는 0이 됩니다.

Q6. 최종 loss 를 계산할때 정답을 제외한 각 class 에서 계산한 loss 값들의 합(sum) 대신에 평균(mean) 을 이용하면 어떻게 되나요?

  • 전체 loss에 대한 scaling의 의미만 가지므로, 큰 변화는 없으며, 상관 없습니다.
  • 왜냐하면 스코어 값이 몇인지는 신경쓰지 않기 때문입니다.

Q7. multiclass SVM loss 를 계산하는 수식에서 $max(0, s_j − s_{y_i} + 1)$ 대신 $max(0, s_j − s_{y_i} + 1)^2$ 를 사용하면 Loss가 어떻게 변하나요?

image

  • 결과는 달라집니다.
  • 좋은것과 나쁜것의 trade off를 비선형 방식으로 바꾸게 되는 것으로, 다른 loss function이 됩니다. 또한 이런 Loss를 squared hinge loss라 칭합니다.
    • squared loss는 잘못된 것을 아주 잘못된 것으로, hinge loss는 그것보다는 조금 덜하게 계산합니다.
    • 잘못된 것을 얼마나 고려할 것인가? 라는 문제는 에러에 대해 얼마나 신경쓰고 있고, 그것을 어떻게 정량화 할 것인지에 달려있으며, loss function을 고려할 때 생각해야 할 내용입니다.
  • Loss function은 알고리즘에게 “어떤 error를 내가 신경쓰고 있는지” 그리고 “어떤 error가 trade-off 되는 것인지”를 알려주는 것입니다. 때문에 실제로 문제에 따라서 손실함수를 잘 설계하는 것은 엄청 중요합니다.


Regularization

Train Loss = 0: Overfitting

image

만약 Multiclass SVM Loss가 0이 되었다고 할때, 과연 Loss가 0이 되는 이때의 $W$는 unique할지 생각해봐야합니다.


image

위 슬라이드의 오른쪽과 같이 직접 계산을 해보면, $W$를 2배로 해도 Loss는 같게 계산되기 때문에 $W$는 유일하지 않습니다. $W$의 스케일이 변하더라도 그대로 Loss 값은 0으로 변하지 않을 것입니다. 즉, $W$ 를 두 배한 $2W$ 도 Loss값이 0이 나올 것입니다.

조금 이상합니다.

Loss Function 이란 것은, 우리의 classifier가 현재 얼마나 badness한지 알려주는 기준이고, Loss가 최소가 되면 우리의 classifier가 좋은 성능을 보인다고 했습니다. 그러므로 Loss가 최소가 되는 $W$ 값을 찾는게 좋은 classifier를 만드는 것 이라 할 수 있습니다. 이러한 관점에서는 Loss 가 0이 되는 수 많은 $W$ 들 중에 아무거나 선택해서 사용하면 좋은 성능의 classifier가 될 것이라고 생각이 듭니다.

하지만 Loss Function이 정말 classifier에게 우리는 어떤 $W$를 찾고 있고, 어떤 $W$ 에 신경쓰고 있는지를 말해주는 것이라면, Loss값이 0이 되는 수 많은 $W$ 의 값들 중에서 어떤 $W$ 값을 선택하는것은 좀 이상합니다. 즉 본질이 불일치하며 모순적입니다.

image

여기서 간과한 점이 있는데, 위 수식처럼 Loss를 0으로 만드는 $W$ 를 구하는 것에 초점을 맞추는 것은 학습과정에서 training data에 대한 loss에 대해서만 생각한다는 것 입니다.즉 classifier에게 training data에 꼭 맞는 $W$ 를 찾으라고 말하는것 과 같습니다.

하지만 실제 만들려고하는 classifier는 training data를 얼마나 fit한지(잘 분류하는냐)에 대해서는 신경쓰지않습니다.

기계학습의 핵심은, training data를 이용해서 어떤 classifier를 찾는 것인데, 분류기는 test data에 적용할 것이기 때문에 training data의 성능이 아니라, test data의 성능에 관심을 두어야 합니다. 최종적으로는 실제 test data 를 얼마나 잘 분류하느냐가 중요하기 때문입니다. 그러므로 classifier에게 training data의 Loss에만 신경쓰라고 한다면 분류기가 이해할 수 없는 행동을 할 수도 있습니다.

Regularization intuition: Prefer Simpler Models

선형 분류기가 아닌, 기계학습에서 다루는 좀 더 일반적인 개념에 대한 구체적인 예를 들겠습니다.

image

파란 점training data 를 의미하며, 우리가 유일하게 하는 일은 classifier에게 파란 training data에 대해 fitting하라 시키는 것입니다. 그럼 classifier는 모든 training data에 대해 완벽히 fitting 하기위해(즉, training data에 대한 loss가 0이 되기위해) 우리의 모델(classifier)은 구불구불한 파란색 곡선 $f1$ 을 만들 것입니다.

하지만 새로운 흰색 test data에 대한 성능에 대해 전혀 고려하지 않았기 때문에 좋지않습니다. 항상 테스트 데이터의 성능을 고려해야 합니다. 만약 새로운 흰색 test data가 들어오게 되면 앞에서 만든 파란색 곡선의 모델인 $f1$ 은 새로운 흰색 data에 대해 완전히 틀리게 됩니다.

사실 우리가 의도했던건 초록색 선 $f2$ 입니다. 완벽하게 training data에 fit한 복잡하고 구불 구불한 곡선을 원한 것이 아닙니다. 만약 새로운 test data가 들어왔을 때 일반화 성능을 고려하지 않고 training data에만 맞추면, Overfitting이 됩니다. 이 문제는 Overfitting을 의미하며 기계학습에서 가장 중요한 문제입니다. 이러한 문제를 해결하는 것이 바로 Regularization입니다.

image

training data에 대한 loss값을 구하는 기존의 손실함수에 하나의 항인 Regularization term 을 더해 모델이 더 단순한 $W$ 값을 선택하도록 합니다.

  • Overftting을 방지하는 장치입니다.
  • 위 그림의 complex한 파란 $f1$에서 simple한 초록 $f2$가 되도록 합니다.
  • 기계학습에서 “Regularization penalty” 를 만들어 R로 표기를 합니다.
  • 일반적인 손실 함수의 형태는 두가지 항을 가지게 됩니다. 즉 Data lossRegularization loss 입니다.

그리고 $R(W)$에 하이퍼파라미터 $λ$를 붙여 training data에 fit하게 만드는 것에 더 중점을 둘지, 모델을 단순화하는데 더 중점을 둘지에 대한 trade-off 관계를 설정할 수 있습니다.

  • $λ$ 값 높으면 모델이 단순해짐 -> underfitting 위험
  • $λ$ 값 낮으면 모델이 복잡해짐 -> overfitting 위험


Why regularize?

결국 다음과 같은 이유로 Regularization을 사용합니다.

  • 가중치에 대한 선호도를 표현하기 위해서.
  • 모델을 simple하게 만들어 test data에서 작동하게 만들기 위해서.
  • 곡면성(curvature)을 추가하여 Optimization을 향상시키기 위해서.


image

Regularization 에는 여러 종류들이 있습니다.

  • 머신러닝/딥러닝 모두에서 볼 수 있는 것들
    • L2 Regularization(Weight decay) (가장 일반적)
    • L1 Regularization, Elastic net(L1과 L2를 같이 사용), Max norm regularization 등
  • 주로 딥러닝에서 볼 수 있는 것들
    • Dropout, Batch normalization, stochastic depth 등

Regularization: Expressing Preferences

다음 슬라이드는 입력 벡터 $x$와 가중치 벡터 $w$가 있을 때, Linear classification의 관점에서 내적이 같기때문에 $w1$와 $w2$는 같습니다. 출력은 동일하지만 $w$의 형태가 다른 경우를 보여줍니다.

  • $w_1^T x = w_2^T x = 1$ 로 모두 동일

image

이때, L1과 L2 regularization이 weight vector가 어떻게 구성되는 것을 더 선호하는지는 L1, L2 norm을 계산해봄으로써 알 수 있습니다.(여기서, 선호한다는 것은 penalty를 덜 준다는 것을 의미)

  • L1 norm
    • $\left\Vert w_1 \right\Vert _1 = \left \vert 1 \right \vert + \left \vert 0 \right \vert + \left \vert 0 \right \vert + \left \vert 0 \right \vert = 1$
    • $\left\Vert w_2 \right\Vert _1 = \left \vert 0.25 \right \vert + \left \vert 0.25 \right \vert + \left \vert 0.25 \right \vert + \left \vert 0.25 \right \vert = 1$
    • L1 norm이 작게 나오려면 vector의 element들이 0에 가까워야 하므로 sparse해지는 것을 더 선호합니다. (위 예에서는 어쩌다보니 값이 동일하게 나왔음)
  • L2 norm
    • $\left\Vert w_1 \right\Vert _2 = \sqrt{1^2 + 0^2 + 0^2 + 0^2} = 1$
    • $\left\Vert w_2 \right\Vert _2 = \sqrt{0.25^2 + 0.25^2 + 0.25^2 + 0.25^2} = 0.25$
    • L2 norm이 작게 나오려면 vector의 element들이 고르게 분포해야(spread) 하므로 값이 더 넓게 퍼지는 것을 더 선호합니다.

따라서, L1과 L2 Regularization이 모델에 미치는 영향에 대한 직관은 다음과 같이 서로 반대라는 것을 알 수 있습니다.

  • L1 regularization
    • $W$ 가 sparse해지도록 합니다.
    • 작은 가중치들이 0으로 수렴하게 하고, 몇개의 중요한 가중치만 남도록 합니다.
    • 가중치 W에 대해 0의 갯수에 따라 모델의 복잡도를 다룹니다.
    • 즉 L1이 “복잡하다”고 느끼고 측정하는 것은 0이 아닌 요소들의 갯수입니다.
    • 또한 의미 있는 값을 원하면 L1 regularization이 좋습니다.
  • L2 regularization
    • $W$ 에서 특정 값만 모델에 큰 영향을 미치도록 하지 않습니다. 즉 가중치를 0에 가깝게 유도합니다.
    • $W$ 의 값이 고르고 넓게 퍼지도록(spread) 합니다. 즉 모든 데이터를 고려합니다.
    • L2 Regression은 분류기의 복잡도를 상대적으로 $w1$와 $w2$중 어떤 것이 더 coarse한지를 측정합니다. (값이 매끄러워야함)
    • Linear classification에서 $W$가 의미하는 것은, “얼마나 $x$가 Output Class와 닮았는지” 이므로, L2 Regularization이 말하고자 하는것은 $x$의 모든 요소가 영향을 줬으면 하는 것입니다.
    • 그러므로 변동이 심한 어떤 입력 $x$의 특정 요소에만 의존하기 보다, 모든 $x$의 요소가 골고루 영향을 미치길 원한다면, L2 Regularization을 통해 더 강건합니다.
    • 즉 L2의 경우에는 $W$의 요소가 전체적으로 퍼져있을 때 “덜 복잡하다” 라고 생각하게 됩니다.

즉, 풀고자 하는 문제에 따라 모델의 복잡도를 어떻게 바라볼 것인가(모델의 복잡도에 어떻게 penalty를 주어서 제한할 것인가) 를 결정하고, 이에 적절한 regularization을 고르는 것 이 중요합니다.

  • L1
    • w에 0이 아닌 요소가 많을때: 복잡
    • w에 0이 많으면: 덜 복잡
  • L2
    • w의 요소가 퍼져있을 때: 덜 복잡
    • w가 어느 쪽에 치중되어 있으면: 복잡

정리하자면 결국 모델을 덜 복잡하게 만들기 위한 것이 Regularization의 궁극적인 목표입니다. 또다른 말로 Hypothesis class중에 더 간단한 것을 선택하기 위해서 우리는 model에 Penalty를 주는 것입니다. 


Softmax Classifier(Multinomial Logistic Regression)

Multi-class SVM loss 외에도 흔히 많이 사용되는 손실함수가 있는데, 바로 Multinomial Logistic regression, 즉 Softmax 입니다. 딥러닝에서는 일반적으로 softmax를 많이 사용합니다.

어떤 분류문제가 있고 어떤 모델 $F$가 각 클래스에 해당하는 10개의 숫자를 출력할때 multi-class SVM loss 에서 단지 정답 클래스가 정답이 아닌 클래스들 보다 더 높은 스코어를 내기만을 원했지, 해당 스코어 자체에 대한 해석은 하지않았습니다. 즉, 정답 score과 정답 클래스가 아닌 다른 클래스의 score의 차이(Gap) 에만 집중했지 그 차이가 일정 margin만 넘으면 더 이상 성능 개선에 신경쓰지않습니다. 때문에 스코어 자체가 실제로 의미하는 것에는 관심이 없었습니다.

반면, multinomial logistic regression(softmax)의 손실함수는 score 자체에 추가적인 의미를 부여하는데, 차이를 모두 수치화하여 score에 대한 해석이 가능하게 합니다. 이때, Softmax 라고 불리는 함수를 쓰고 score를 통해 클래스 별 확률 분포를 계산합니다. 그리고 최종적으로 정답인 클래스의 score가 1과 가깝게 나오는 것을 목표로 합니다. 따라서 정답 클래스의 score가 다른 클래스의 score보다 이미 높더라도 계속 해서 더 성능을 좋게 하려고(loss값을 줄이려고) 한다 는 특징이 있습니다.

image

image

  • Softmax Classifier의 Loss 계산 방식
    1. Linear Classifier의 출력으로 나오는 각 클래스에 대한 score에서 exponential 함수를 통해 양수로 만들어 줍니다.
    2. 이후 softmax 함수를 거쳐 (normalize) 모든 score 합이 1이 되도록 하여, 각 score를 probability value로 만들어 줍니다. 각 probability value는 해당 클래스일 확률
      • 즉 앞에서 나온 출력들(probabilities)을 모두 더하면 1이 되도록 normalize한다.
    3. 마지막으로, -log(P(정답클래스)) 값을 취해줍니다.
      • 결국 우리가 원하는 것은 정답 클래스에 해당하는 클래스의 확률이 1에 가깝게 계산되는 것입니다.
      • 확률이 1이 되길 원하는데, 확률값을 최대화 시키는 것 보다 그냥 log로 최대화시키는 것이 더 쉽기 때문에 log를 사용합니다.
      • $-$ 를 붙이는 이유는 loss(손실)로서 모델이 얼마나 나쁜지(BADNESS)를 측정함과 동시에, 최소화 하려는 값이기 때문입니다.
    4. 요약하면, 스코어가 있으면, softmax를 거치고, 나온 확률 값에 -log를 취해주면 됩니다.

Quiz

Q1. softmax loss의 최대값과 최소값을 얼마일까요?

  • Loss의 최소값은 0 이고 최대값은 $\infty$ 입니다.

확률 분포를 생각해 보면, 우리는 정답 클래스의 확률은 1이 되길 원하고, 정답이 아닌 클래스는 0이 되길 원합니다. 결국 log안에 있는 어떤 값은 결국 1이 되어야 합니다. Loss는 정답 클래스에 대한 Log 확률이기 때문에 원하는 클래스를 완벽히 분류했다면 -Log(1) = 0 이고, Loss는 0이 될 것입니다.

그렇다면 Loss가 0이 되려면 실제 스코어는 어떤 값이어야 할까

아마 정답 스코어는 극단적으로 높아야 할 것입니다. 지수화를 하고 정규화를 하기 때문에 거의 무한대에 가깝게 높아야 합니다. 즉 우리가 확률 1(정답) 과 0(그외) 를 얻으려면, 정답 클래스의 스코어는 +무한대가 되어야 하고, 나머지는 -무한대가 되어야 합니다. 하지만 유한 정밀도 때문에 Loss가 0인 경우는 절대 없을 것이며, 이론적인 해석을 하여 0은 “이론적으로 최소 Loss이다” 라고 이해하면 됩니다.

또한 최대 Loss는 무한대인데, 이론적으로 확률이 0이 되려면 정답 클래스의 스코어가 음의 무한대일 경우 밖에 없으므로 위와같이 유한 정밀도 때문에 Loss가 $\infty$ 인 경우는 절대 없을 것이며, 이론적인 해석을 하여 $\infty$ 는 “이론적으로 최대 Loss이다” 라고 이해하면 됩니다.

Q2. 훈련을 시작할 때, W를 작은 랜덤값으로 초기화해서 모든 score가 0인 경우의 loss는 어떻게 되나요?

  • $\log(\text{class 수})$이다.
    • $e(s)$가 모두 1이 되므로, $-\log( \dfrac{1}{\text{class 수}}) = -\log(1) + \log(class 수) = \log(class 수)$
  • 따라서, 훈련 초기에 $\log(\text{class 수})$가 Loss로 나오지 않으면, 잘못되고 있다는 것으로 debugging 할 수 있습니다.


Hinge Loss(SVM) vs. Cross-entropy Loss(Softmax)

image

  • Hinge Loss와 Cross-entropy Loss의 차이점
    • $Wx + b$의 score를 계산하는 것은 동일하지만, score를 해석하는 방법이 다릅니다.
    • Hinge Loss
      • 정답 class의 score와 정답이 아닌 class의 score를 간의 마진(margins)을 신경썼습니다.
    • Cross-entropy Loss(softmax)
      • probability distribution를 계산하여 score를 확률적으로 해석합니다.

image

  • 두 Loss함수의 가장 큰 차이점 은 다음의 질문을 통해 이해할 수 있습니다.
    • Q. 데이터 포인트를 흔들면 어떻게 되나?(SVM Loss에서의 Q1. car의 점수를 변화하면 어떻게 되는가?와 동일)
      • Multiclass SVM Loss는 이미 car의 점수가 커서 loss에 변화가 없음
        • 왜냐면 SVM loss는 오직 정답과 그 외 클래스의 마진이 얼마나 되는지에만 관심이 있기 때문입니다.
        • 즉, margin보다 크기만 하면 더이상 성능 개선에 신경쓰지 않습니다.
      • Cross-entropy Loss에서는 정답 score와 정답이 아닌 score의 차이가 크더라도, 계속해서 그 차이를 크게 만들어 언제나 확률을 1로 만드려고 노력합니다.
        • 즉, 계속해서 개선하려고 하는 경향이 있음

image

지금까지 한 것들을 정리하자면 다음과 같습니다.

  • 데이터 셋 $(x,y)$이 있습니다.
  • 입력 x로부터 스코어를 얻기 위해 Linear classifier, 즉 스코어 함수(score function)를 사용합니다.
    • $s=f(x;W)=Wx$
  • softmax, svm loss와 같은 손실함수(loss function)를 이용해서, 모델의 예측값이 정답 값에 비해 얼마나 나쁜지(badness)를 측정합니다.
  • 그리고 모델의 “복잡함” 과 “단순함” 을 통제하기 위해 손실 함수에 regularization term을 추가합니다.
  • 이 모든걸 합쳐서 최종 손실 함수가 최소가 되게 하는 가중치 행렬이자 파라미터인 W를 구합니다.


optimization

image

그렇다면 실제로 어떻게 Loss를 줄이는 $W$를 찾아낼 수 있을까? 란 의문이 듭니다. 이 질문은 우리를 “Optimization” 라는 주제로 이끌어 줍니다.

image

최적화를 하고 있다는걸 상상해볼때, 우리는 다양한 산과 계곡과 시내가 있는 엄청 큰 골짜기를 거닐고 있을겁니다. 어떻게 해야 실제 loss를 줄일 수 있는 $W$를 찾을 수 있을까? 를 고민해봐야 합니다.

  • 그림과 같이 골짜기의 밑바닥을 찾아야 합니다.
    • “산”과 “계곡” 과 같은 풍경들이 바로 파라미터 W입니다.
    • 여기서 loss는 위치의 높이에 해당합니다.
    • loss(높이)는 w의 값에 따라 변합니다.
  • 골짜기의 Global한 밑바닥을 찾기위해 다양한 “iterative한 방법” 들을 씁니다. 즉 임의의 지점에서 시작해서 점차적으로 성능을 향상시키는 방법 입니다.
  • 단순한 방법으로 무작위 탐색(random search)이 있습니다.
  • 임의로 샘플링한 W들을 모아놓고, 각각의 W 에 대해 loss를 계산해 어떤 W가 좋은지 찾는 것입니다.
  • 하지만, 굉장히 좋지 않은 방법이므로 절대 쓰면 안됩니다.

Strategy #2: Follow the slope

image

  • 실제로 더 나은 전략은 지역적인 기하학적 특성을 이용하는 것입니다.(local geometry)
  • 위 그림과 같은 계곡에서 눈을 가린 채 가장 낮은 지점을 찾는다고 가정해볼때, 발로 경사가 있는 지점을 찾으면서 계속해서 나아가다 보면, 가장 낮은 지점에 도달할 수 있을 것입니다.
    • 두 발로 땅의 경사를 느끼고, 어느 방향으로 내려가야 할지 판단합니다.
    • 그 방향으로 한발자국 내딛고, 다시 두발로 느끼는 방향을 찾습니다.
    • 구체적으로, 임의의 $W$에서 시작하고, 또 다른 임의의 방향 $δW$으로 살짝 움직여봅니다.
    • 만약 움직여간 자리$(W+δW)$에서의 손실값(loss)가 더 낮으면, 거기로 움직이고 다시 탐색을 시작합니다.
    • 이런 반복으로, 골짜기를 내려갑니다.


image

  • 경사(slope): 1차원 공간에서 어떤 함수에 대한 미분값(derivative)
    • $\frac{df(x)}{dx}=lim_{h→0}\frac{f(x+h)−f(x)}{h}$
    • x를 입력으로 받으면 출력은 곡선의 높이로 생각할 수 있으며, 곡선의 일부를 구하면 기울기를 계산할 수 있습니다.
    • 어떤 점에서의 함수의 경사이므로, 방향(direction) 정보를 가지고 있습니다.
  • x는 스칼라가 아니라 벡터이기 때문에 위의 개념을 다변수로 확장시켜야 합니다.
    • x가 벡터일 경우, 이 때 미분을 편미분(partial derivatives)이라 합니다.
    • gradient는 벡터 x의 각 요소를 편미분한 집합입니다.
    • 그레이디언트의 각 요소는 “임의의 방향으로 갈때 함수 f의 경사가 어떤지”의 정보를 알려줍니다.
    • gradient의 방향은 함수에서 “가장 많이 올라가는 방향” 입니다. 반대로 생각해보면, gradient의 반대 방향이라면 “가장 많이 내려갈 수 있는 방향” 입니다.
    • 만약 특정 방향에서 얼마나 가파른지 알고싶으면, 해당하는 방향의 unit벡터와 gradient벡터의 내적입니다.

gradient가 함수의 어떤점에서의 선형 1차근사 함수를 알려주기 때문에, gradient는 매우 중요합니다. 실제로 많은 딥러닝 알고리즘들이 gradient를 계산하고, 그 gradient를 여러분들이 파라미터 벡터를 반복적으로 업데이트할 때 사용합니다.

  • 즉 정리하면 다음의 과정으로 나타낼 수 있습니다.
    1. parameter vector에 대한 Loss function의 gradient를 계산합니다.
      • gradient는 partial derivatives로 이루어진 vector
    2. gradient에 음의 값을 취한 후, 해당 방향으로 나아간다.
      • 함수의 기울기는 증가에서 $+$, 감소에서 $-$이므로, gradient가 감소하는 방향을 위해 음의 값을 취합니다.


Numerical Gradient vs. Analytic Gradient

gradient의 각 요소는 한 방향으로 아주 조금씩 이동했을 때, Loss값이 어떻게 변하는지를 알려주는 것입니다.

image

Numerical Gradient는 W의 원소를 아주 조금씩 변화시키면서, gradient를 하나하나 계산하는 방법으로, 위 슬라이드 처럼 아주 작은 값 h를 더해 loss를 다시 계산해 봅니다. 첫 번째 요소를 조금 움직이면 Loss가 1.2534에서 1.25322로 감소합니다. 이후 극한식을 이용해 근사시킨 gradient를 구합니다.

image

  • 하지만, 위의 방법은 비효율적이며, 시간이 엄청 오래 걸립니다.
  • 즉, Numerical gradient를 사용하지 않고, 함수를 미분해서 loss를 계산하는 Analytic gradient를 사용합니다.
    • Numerical gradient : 근사치(approximate), 느림(slow), 쉬운 방법(easy to write)
    • Analytic gradient : 정확(fast), 빠름(exact), 실수하기 쉬운(error-prone)

image

위 슬라이드처럼 $W$ 의 모든 원소를 순회하는 것이 아니라 gradient를 나타내는 식이 뭔지만 먼저 찾아내고, 그걸 수식으로 내타내서 한번에 gradient dW를 계산합니다. 즉 Numerical gradient에서보다 훨씬 효율적이고 빠르게 계산이 가능합니다.

image

  • 정리하자면 다음과 같습니다.
    • Numerical Gradient
      • W의 원소를 아주 조금씩 변화시키면서, gradient를 하나하나 계산하는 방법
      • W의 원소 하나하나마다 모두 계산해야 하므로, 너무 느리고 비효율적
    • Analytic Gradient
      • Loss function은 W에 대한 함수이므로, 그냥 식을 미분해서 gradient를 구하는 방법
      • gradient에 대한 식을 구한 후, dW를 한번에 계산할 수 있으므로 더 빠르고 좋은 방법
  • 따라서, 실제로는 Analytic Gradient를 사용하고 Analytic Gradient의 계산 값을 확인하는 debugging 용도로 Numerical Gradient를 사용합니다. -> gradient check


Gradient Descent

  • 경사 하강법(gradient descent) : 기울기를 반복적으로 평가한 다음 파라미터 업데이트를 수행하는 절차
    • Loss function 값을 최소화하는 값을 찾는 것으로 기울기의 반대 방향으로 일정 크기만큼 이동하는 것을 반복합니다.
    • 경사 하강을 사용하여 함수의 local minimum을 찾으려면 현재 지점에서 함수의 그레이디언트의 음수에 비례하는 단계를 밟습니다.
# Vanilla Gradient Descent

while True:
    weights_grad = evaluate_gradient(loss_fun, data, weights)
    weights += - step_size * weights_grad # perform parameter update
  • Gradient Descent 알고리즘
    • W를 임의의 값으로 초기화합니다.
    • Loss와 gradient를 계산한 뒤에 가중치를 gradient의 반대 방향으로 업데이트합니다.
      • gradient가 함수에서 증가하는 방향이기 때문에 -gradient를 해야 내려가는 방향이 됩니다.
    • 스텝사이즈는 Learning rate 라고 하며, 실제 학습시 가장 중요한 하이퍼파라미터 중 하나입니다.

image

위 그림을 보면 그릇처럼 보이는 것이 손실함수 이며, 가운데 빨간 부분이 낮은 Loss이고, 테두리의 파란영역과 초록 영역은 Loss가 더 높은 곳입니다. 즉 Loss가 높은 곳을 피해가야 합니다.

-gradient를 계산할 것이고 이를 통해 결국 가장 낮은 지점에 도달할 것입니다. 그리고 이걸 계속 반복하게 되면 아마도 결국은 정확한 최저점에 도달하게 될 것입니다. 다음 강의에서 Update Rule을 배울 예정입니다.


Mini-batch Stochastic Gradient Descent(SGD, MSGD)

image

Loss function은 각 training sample을 분류기가 얼마나 나쁜지를 계산하는 것이였고, 전체 Loss는 전체 training set Loss의 평균으로 사용했습니다. 하지만 모든 data에 대해 일일히 이 작업을 하기에는 연산량이 너무 많으며, 시간이 오래 걸려 느립니다. 그래서 실제로는 Mini-batch stochastic gradient descent 라는 방법을 씁니다.

  • 확률적 경사 하강법(Stochastic Gradient Descent; SGD) : 손실 함수(loss function)을 계산할 때, 전체 데이터(batch) 대신 일부 데이터의 모음(mini-batch)를 사용하는 것
    • 전체 데이터 셋의 gradient과 loss를 계산하기 보다는 Minibatch라는 작은 트레이닝 샘플 집합으로 나눠서 학습하는 것입니다.
    • 미니배치에서 그레이디언트를 구해서 더 자주 가중치를 업데이트하면 더 빠른 수렴 결과를 얻습니다.
    • Minibatch는 보통 2의 승수로 정하며 32, 64, 128 을 보통 씁니다.
    • 즉, 작은 minibatch를 이용해서 Loss의 전체 합의 추정치와 실제 그레이디언트의 추정치를 계산한다.
  • 이러한 SGD 학습법은 거의 모든 deep neural network 에서 사용되는 기본적인 학습법이므로 굉장히 중요합니다.
# Vanilla Minibatch Gradient Descent

while True:
    data_batch = sample_training_data(data, 256)
    weights_grad = evaluate_gradient(loss_fun, data_batch, weights)
    weights += - step_size * weights_grad # perform parameter update
  • Stochastic Gradient Descent 알고리즘
    • 임의의 minibatch를 만들어내고, minibatch에서 Loss와 Gradient를 계산합니다.
    • 이후 W를 업데이트합니다.
    • Loss의 “추정치” 와 Gradient의 “추정치” 를 사용하는 것입니다.


Aside: Image Features

image

Deep Neural Network이 유행하기 전에 주로 쓰는 방법으로, Feature Representation을 계산하고 미리 찾은 다음 Linear Classifier 에 넣는 2-stage 방법입니다.

  1. 이미지가 있으면 여러가지 Feature Representation을 계산합니다.
    • 이런 Feature Representation은 이미지의 모양새와 관련된 것일 수 있습니다.
  2. 여러 특징 표현들을 연결시켜(Concat) 하나의 특징 벡터(feature vector)로 만듭니다.
    • 그러면 이 feature vector가 Linear classifier의 입력으로 사용됩니다.


Image Features: Motivation

image

Image Features의 motivation은 그림과 같은 트레이닝 셋이 있다고 할 때, Linear한 결정 경제를 그릴 방법이 없습니다.

즉, 왼쪽과 같이 raw 한 이미지 자체를 입력으로 넣었을 때에는 linear classifier 로는 분류하지 못했던 이미지 데이터들에 대해, 특징 벡터로 변환하여 입력값으로 넣어줌으로써 linear classifier로 분류가 가능해집니다.

특징 벡터(feature vector)를 뽑아내는 방법은 여러 가지가 있으며, 데이터에 맞게 알맞는 특징 벡터를 사용하면 raw한 이미지에 대해서는 분류가 불가능 했던 것이 특징 벡터로 변환되어 입력되며 분류 가능하게 됩니다. 즉 복잡하던 데이터가, 변환 후에 선형으로 분리가 가능하게 바뀌어서, Linear classifier로 완벽하게 분리할 수 있게 됩니다.

Example: Color Histogram

image

  • 각 이미지에서 Hue값만 뽑아서, 모든 픽셀을 어떤 공간에 넣고 각 공간에 담긴 픽셀의 갯수를 세는 방법론입니다.
    • 이는 이미지가 적체적으로 어떤 색인지를 알려줍니다.
    • 컬러 히스토그램은 이미지 전체적으로 어떤 색이 있는지를 나타냅니다.
  • 개구리와 같은 경우 자주색이나 붉은색은 별로 없고, 초록색이 많은 것을 알 수 있습니다.

Example: Histogram of Oriented Gradients(HoG)

image

  • Hubel 과 Wiesel이 Oriented edges가 인간의 시각시스템에서 정말 중요하다 한 것처럼, Local orientation edges를 측정합니다.
  • 이미지를 8*8픽셀로 나눠서 각 픽셀의 지배적인 edge 방향을 계산하고, 각 edge들에 대해서 edge directions을 양자화해서 어떠한 공간에 넣습니다.
    • 다양한 edge oreirentations에 대한 히스토그램을 계산합니다.
    • 이후의 전체 특징 벡터는, 각각의 모든 8x8 지역들이 가진 “edge orientation에 대한 히스토그램” 이 됩니다.
    • HOG는 이미지 내에 전반적으로 어떤 종류의 edge정보가 있는지를 나타냅니다.

Example: Bag of Words

image

  • 이 방법은 NLP에서 영감을 받은 방식으로, 어떤 문장에서 여러 단어들의 발생빈도를 세서 특징벡터로 사용하는 방식을 이미지에 적용한 것입니다.
  • 시각 단어(visual words)를 위한 2단계의 과정
    1. 엄청 많은 이미지들을 임의대로 조각낸 후 그 조각들을 K-means와 같은 알고리즘으로 군집화합니다.
      • 이미지내의 다양한 것들을 표현할 수 있는 다양한 군집들을 만들어 냅니다.
      • 군집화 단계를 거치고나면, 시각 단어(visual words)는 빨간색, 파랑색, 노랑색과 같은 다양한 색을 포착해냅니다.
      • 또한 다양한 종류의 다양한 방향의 oriented edges또한 포착할 수 있습니다.
    2. 시각 단어(visual words) 집합을 만들어 떤 이미지에서의 시각 단어들의 발생 빈도를 통해서 이미지를 인코딩 합니다.
      • 이 이미지가 어떻게 생겼는지에 대한 다양한 정보를 제공하는 것입니다.


Image features vs ConvNets

Image classification의 pipleline은 다음과 같습니다.

CNN 이 나오기 전에는 이런 방식으로 이미지에서 특징 벡터(feature vector)를 추출하고 이후에 classifier를 학습시켜서 분류를 하는 2-stage 방식으로 진행됐었습니다.

이러한 방법은 feature extractor를 통해 특징 벡터가 한번 추출되고 나면 이후 classifier 학습 과정에서 해당 feature extractor가 변하지 않는다는 것입니다. 즉, 학습과정에서 classifier만 학습됩니다.

image

반면, CNN(Convolutional Neural Network)이나 DNN(Deep Neural Network)에서는 미리 정해놓은 특징을 쓰는 것이 아니라, raw 한 입력 이미지 데이터로부터 직접 feature들을 학습하여 특징 표현들을 직접 만들어냅니다. 따라서 Linear classifier만 훈련하는게 아니라 가중치 전체를 한꺼번에 학습하는 1-stage 방법론입니다.

다음시간에는 Neural Networks에 대해 살펴볼 것이고, 역전파(Backpropagation)에 대해서 살펴보겠습니다.

Read more

CS231n Lecture2 Review

|

Hits


해당 게시물은 Standford 2017 CS231n 강의와 2022년 슬라이드를 바탕으로 작성되었습니다.

image


Image Classification: A Core Task in Computer Vision

image

  • Image Classification 은 컴퓨터비전 분야에서 Core Task 에 속합니다.
  • Image Classification을 한다고 할때, 미리 정해놓은 카테고리 집합(discrete labels)이 있는 시스템에 이미지를 입력하여, 컴퓨터가 이미지를 보고 어떤 카테고리에 속하는지 고르는 것입니다.
  • 하지만 사람의 시각체계는 Visual Recognition task에 고도화 되어 있기 때문에 쉬워보이지만, 기계의 입장에서는 어려운 일 입니다.

The Problem: Semantic Gap

image

  • Semantic Gap(의미론적 차이) : 사람 눈으로 보는 이미지, 실제 컴퓨터가 보는 픽셀 값과의 차이
  • 위 그림은 컴퓨터가 이미지를 바라보는 관점
  • 이미지는 0부터 255까지의 숫자로 픽셀이 표현되며, Width(너비) x Height(높이) x Channel(채널)의 크기의 3차원 배열. 각 채널은 red, green, blue를 의미
  • 우리가 보기에는 고양이 이미지이지만, 컴퓨터에게 이미지는 그저 아주 큰 격자 모양의 숫자 집합

위 사진과 같이 기계는 고양이 사진을 입력받으면, RGB(Red, Blue, Green) 값을 기준으로 격자 모양의 숫자들을 나열하여 인식합니다. 하지만 기계는 카메라 각도나 밝기, 객채의 행동 혹은 가려짐 등 여러차이로 인해 이미지의 픽셀 값이 달리 읽어 사물을 다르게 인식하는데, 이러한 기계를 잘 인식할 수있도록 알고리즘 개발을 시도 했으나, 다양한 객체들에게 유연하고 확장성 있는 알고리즘을 개발하는데 한계가 있었습니다.

Viewpoint variation(카메라의 위치 변화)

image

  • 가령 이미지에 아주 미묘한 변화만 주더라도 픽셀 값들은 모조리 변하게 될 것입니다.
  • 고양이 이미지를 예로 들었을때, 고양이 한 마리가 얌전히 앉아만 있으며 아무 일도 일어나지 않겠지만, 카메라를 아주 조금만 옆으로 옮겨도 모든 픽셀 값들이 모조리 달라질 것입니다. 하지만 픽셀 값이 달라진다해도 고양이라는 사실은 변하지 않기 때문에 Classification 알고리즘은 robust해야 합니다.

Illumination(조명에 의한 변화)

image

  • 바라보는 방향 뿐만 아니라 조명 또한 문제가 될 수 있습니다. 어떤 장면이냐에 따라 조명은 각양각생일 것입니다.
  • 고양이가 어두운 곳에 있던 밝은 곳에 있던 고양이는 고양이 이므로, 알고리즘은 robust해야 합니다.

Deformation(객체 변형에 의한 변화)

image

  • 객체 자체에 변형이 있을 수 있습니다.
  • 고양이는 다양한 자세를 취할 수 있는 동물 중 하나인데, deformation에 대해서도 알고리즘은 robust해야 합니다.

Occlusion(객체 가려짐에 의한 변화)

image

  • 가려짐(occlusion)도 문제가 될 수 있습니다. 가령 고양이의 일부밖에 볼 수 없는 상황이 있을 수도 있습니다.
  • 고양이의 얼굴밖에 볼 수 없다던가, 극단적인 경우에는 소파에 숨어들어간 고양이의 꼬리밖에 볼 수 없을지도 모르지만, 사람이라면 고양이라는 사실을 단번에 알아챌 수 있습니다.
  • 즉 이 이미지는 고양이 이미지라는 것을 알 수 있기 때문에, 알고리즘은 robust해야 합니다.

Background Clutter(배경과 유사한 색의 객체)

image

  • Background clutter(배경과 비슷한 경우)라는 문제도 존재.
  • 고양이가 배경과 거의 비슷하게 생겼을 수도 있기때문에, 알고리즘은 robust해야 합니다.

IntraClass variation(한 클래스에 여러 종류)

image

  • 하나의 클래스 내에도 다양성이 존재. 즉 “고양이”라는 하나의 개념으로 모든 고양이의 다양한 모습들을 전부 소화해 내야 합니다.
  • 고양이에 따라 생김새, 크기, 색, 나이가 각양 각색일 것.

Context(주변 환경에 따른 조건 및 문맥)

image

  • 고양이가 주변 환경(철조망)에 의해 호랑이가 되었습니다.

위와 같은 이유들 때문에 Image Classification 문제는 어렵습니다. 사물 인식의 경우, 가령 고양이를 인식해야 하는 상황이라면 객체를 인식하는 직관적이고 명시적인 알고리즘은 존재하지 않습니다.


Image Classification Algorithm - 기존의 시도들

image

  • 기존에는 Edges and corners를 찾는, 즉 Image의 Feature(특징)을 찾고 Feature(특징)을 이용하여 명시적인 규칙을 만드는 방법으로 접근하였다.
  • 하지만 아래의 이유로 잘 동작하지 않았다.
    • 앞에서 살펴본 조건들에서 여전히 robust하지 못하다.
    • 특정 class에 동작하도록 구현된 알고리즘은 다른 class에 적용하지 못한다.

즉 알고리즘이 robust하지 못 할 뿐더러, 각 class에 대해 새로 다시 짜야하므로, 확장성이 전혀 없는 방법입니다. 따라서 이러한 문제를 해결하기위해, 이 세상에 존재하는 다양한 객체들에 대해 적용이 가능한 방식이 필요합니다.


Image Classification Algorithm - Data-Driven Approach

image

  • 기존에는 이미지를 인식시킬 때, 각 객체에 대한 규칙을 하나하나 정하였다. 그러나 실제 생활에서는 수많은 객체들이 존재하기에 객체마다 규칙을 정해주는 것은 한계를 가지기 때문에, 다양한 객체들에 대해 적용하기 위한 Insight로 데이터 중심 접근방법(Data-Driven Approcach)을 사용합니다.
  • Data-Driven Approcach은 객체의 특징을 규정하지 않고, 다양한 사진들과 label을 수집하고, 이를 이용해 Machine Learning Clssifier 모델을 학습하고, 새로운 이미지를 테스트해 이미지를 새롭게 분류하는 방식입니다.

Data-Driven Approcach은 Machine Learning의 key insight이며, Deep Learning 뿐만 아니라 아주 일반적인 개념입니다.


Example Dataset: CIFAR10

image

  • Cifar-10은 Machine Learning에서 자주 쓰는 연습용(테스트용) 데이터셋.
  • CIFAR-10에는 10가지 클래스가 존재(비행기, 자동차, 새, 고양이 등)
  • 총 50,000여개의 학습용 이미지와, 10,000여개의 테스트 이미지가 존재.
  • 32 x 32 이미지

CIFAR-10 데이터셋을 이용해서 Nearest Neighbor(NN) 예제를 살펴보겠습니다. 우선 오른쪽 칸의 맨 왼쪽 열은 CIFAR-10 테스트 이미지이며, 오른쪽 방향으로는 학습 이미지 중 테스트 이미지와 유사한 순으로 정렬했습니다. 테스트 이미지와 학습 이미지를 비교해 보면, 눈으로 보기에는 상당히 비슷해 보입니다.

두 번째 행의 이미지는 “개” 이며, 가장 가까운 이미지(1등)도 “개” 입니다. 하지만 2등, 3등을 살펴보면 “사슴”이나 “말”같아 보이는 이미지들도 있습니다. “개”는 아니지만 눈으로 보기에는 아주 비슷해 보입니다.

가장 간단하고 기본적인 분류방법인 Nearest Neighbor(NN)는 “가장 가까운 이웃 찾기” 알고리즘으로, 직관적인데 새로운 이미지와 이미 알고 있던 이미지를 비교하여 가장 비슷하게 생긴 것을 찾아내는 알고리즘을 말합니다. NN 알고리즘이 잘 동작하지 않을 것 같아 보이지만 그럼에도 해 볼만한 아주 좋은 예제입니다.


Nearest Neighbor(NN)

image

image

  • Nearest Neighbor(NN) : 입력받은 데이터를 저장한 다음 새로운 입력 데이터가 들어오면, 기존 데이터에서 비교하여 가장 유사한 이미지 데이터의 라벨을 예측하는 알고리즘입니다.

즉, 최근접 이웃 분류기는 테스트 이미지를 위해 모든 학습 이미지와 비교를 하고 라벨 값을 예상합니다.

위 코드를 보면 다음과 같이 동작합니다.

  • Train함수 - Train Step에서는 단지 모든 학습 데이터를 기억합니다. (입력은 이미지와 레이블이고, 출력은 우리의 모델)
  • Predict함수 - Predict Step에서는 새로운 이미지가 들어오면 새로운 이미지와 기존의 학습 데이터를 비교해서 가장 유사한 이미지로 레이블링을 예측합니다. (입력이 모델이고, 출력은 이미지의 예측값)

Distance Metric to compare images

image

image

  • 여기에서 중요한 점은 이미지 쌍이 있을 때 얼마나 유사한지를 어떻게 비교를 할 것인지가 관건입니다.
  • 테스트 이미지 하나를 모든 학습 이미지들과 비교할 때 여러가지 비교 방법들이 있습니다.
  • 위 그림에서 $I_1$, $I_2$ 벡터로 나타냈을 때, 벡터 간의 L1 Distance(Manhattan distance)를 사용하여 계산.
  • 결과는 모든 픽셀값 차이의 합

이미지를 Pixel-wise로 비교합니다. 가령 4x4 테스트 이미지가 있다고 가정할 때, training/test image의 같은 자리의 픽셀을 서로 빼고 절댓값을 취합니다. 이렇게 픽셀 간의 차이 값을 계산하고 모든 픽셀의 수행 결과를 모두 더합니다.

“두 이미지간의 차이를 어떻게 측정 할 것인가?” 에 대해 구체적인 방법을 제시합니다. 지금 예제의 경우에는 두 이미지간에 “456” 만큼 차이가 납니다.

NN Classifier - python code

image

  • Train 함수
    • NN의 경우 Train 함수는 단지 학습 데이터를 기억하는 것입니다.
    • N개의 이미지가 있는 X와 N개의 라벨이 있는 y, 훈련데이터를 Xtr과 ytr에 모두 저장합니다.
  • Test 함수
    • 이미지를 입력으로 받고 L1 Distance로 비교합니다. 즉 학습 데이터들 중 테스트 이미지와 가장 유사한 이미지들을 찾아냅니다.
    • 모든 훈련데이터의 이미지가 저장된 Xtr과 비교하고자하는 X의 행을 Xtr과 dimension을 맞춰준다음 빼고 abs를 취해줍니다.
    • axis=1의 의미는 y축을 기준으로 더한 값들로 저장된 배열을 distances에 저장해줍니다.
    • 그중 제일 작은 값을 argmin으로 찾아내서 그 위치를 min_index에 저장하고
    • ytr에 그 인덱스값을 넣어 예측값으로 보냅니다.

여기서 Simple Classifier인 NN알고리즘에 대해 생각할 점이 있습니다.

  1. Trainset의 이미지가 총 N개라면, Train/Test 함수의 Train time은 데이터를 기억만 하면 되기 때문에 상수시간 O(1)입니다.
  2. 하지만 Test time에서는 N개의 학습 데이터 전부를 테스트 이미지와 비교해야만 합니다. 즉 (Train time < Test time) 데이터 학습은 빠르지만, 새로운 데이터를 판단하는데 있어서 걸리는 시간이 많이 필요하므로, Test시 Test time은 Test data 가 많아지면 Test 시간이 늘어납니다.

실제로 Train Time은 조금 느려도 되지만 Test Time에서는 빠르게 동작하길 원합니다. Classifier의 좋은 성능을 보장하기 위해서 Train Time에 많은 시간을 쏟을 수도 있기 때문입니다. 하지만 NN Classifier의 “Test Time” 을 생각해보면, 일반적으로 모델들은 핸드폰, 브라우저 등 Low Power Device에서 동작해야 되기 때문에 test time이 빨라야 합니다. 하지만 Nearest Neighbor 모델은 느립니다.

image

  • NN 알고리즘으로 decision regions를 그려본 이미지입니다.
  • 2차원 평면 상의 각 점은 학습데이터, 점의 색은 클래스 라벨(카테고리)입니다.
  • 2차원 평면 내의 모든 좌표에서 각 좌표가 어떤 학습 데이터와 가장 가까운지 계산하고, 각 좌표를 해당 클래스로 칠했습니다.

NN 알고리즘은 “가장 가까운 이웃” 만을 보기 때문에, 녹색 한 가운데 노란색 영역, 초록색 영역에서 파란색 영역 침범하는 구간 등등 decision boundary가 Robust 하지 않음을 볼 수 있습니다. 해당 점들은 잡음(noise)이거나 가짜(spurious)일 가능성이 높습니다. 이러한 단점들로 NN 알고리즘은 잘 사용하지 않습니다.


K-Nearest Neighbors(K-NN)

  • NN의 일반화된 방법인 K-NNK개의 가장 가까운 지점의 데이터들의 Majority vote를 통해 예측하는 모델입니다.
  • 단순하게 가장 가까운 이웃만 찾기보다는 Distance metric을 이용해서 가까운 이웃을 K개의 만큼 찾고, 이웃끼리 투표를 하는 방법입니다. 그리고 가장 많은 특표수를 획득한 레이블로 예측합니다.
  • K-Nearest Neighbor Algorithm 을 사용할때, 결정해야하는 두 가지 parameter 가 있습니다.
    • K값
    • Distance Metric(거리척도)

K-Nearest Neighbors(K-NN) - K

image

위 그림은 동일한 데이터를 사용한 K-NN 분류기들로, 각각 K=1/3/5 에서의 결과입니다.

  • K=3 의 경우, 앞서 초록색 영역에 자리 잡았던 노란색 점 때문에 생긴 노란 지역이 깔끔하게 사라졌습니다. 중앙은 초록색이 깔끔하게 점령했습니다. 그리고 왼쪽의 빨강/파랑 사이의 뾰족한 경계들도 다수결에 의해 점차 부드러워지고 있습니다.
  • K=5의 경우, 파란/빨간 영역의 경계가 이제는 아주 부드럽고 좋아졌습니다.
  • 흰색 영역은 K-NN이 “대다수”를 결정할 수 없는 지역으로, 어떤 식으로든 추론을 해보거나, 임의로 정할 수도 있습니다.
  • KNN은 위 슬라이드와 같이, K가 커질수록 decision boundary가 더 smooth해지는 경향이 있습니다.

이러한 방식을 이용하면 좀 더 일반화 된 결정 경계를 찾을 수 있습니다. 여기서 K값의 증가함에 따라서 부드러워 지지만, 흰색 영역이 증가 하는 것을 볼 수 있습니다. 이 흰색 영역 은 어느 쪽에도 분류 할지 알 수 없는 영역입니다. 이러한 부분에서 K값이 증가한다고 항상 좋은 것이 아니라. 데이터나 상황에 따라서 알맞은 K값을 찾아야합니다.

K-Nearest Neighbors(K-NN) - Distance Metric

image

  • K-NN을 사용할 때 결정해야 할 한 가지 사항으로, 서로 다른 점들을 어떻게 비교할 것인지 입니다. 즉 데이터 간의 거리를 잴 때 사용하는 기준으로, 학습한 이미지 중 어떤 이미지와 비슷한지 비교하는 척도라고 생각하면 됩니다.
  • L1 Manhattan Distance : “픽셀 간 차이 절대값의 합”
  • L2 Euclidean distance : “제곱 합의 제곱근” 을 거리로 이용
  • 어떤 거리 척도(distance metric)를 선택할지는 아주 흥미로운 주제입니다.
  • 왜냐하면 서로 다른 척도에서는 해당 공간의 근본적인 기하학적 구조 자체가 서로 다르기 때문입니다.

일반적으로 K-NN에서의 Distance Metric은 L1 distance 또는 L2 distance를 사용할 수 있습니다.

  • L1, L2 distance가 원점으로부터 1인 경우를 나타낸 것이라 가정했을때
    • L1 distance
      • 왼쪽에 보이는 사각형은 L1 Distance의 관점에서는 원
      • 학습된 이미지와 테스트 이미지의 픽셀 간의 차이 값을 계산하고 모두 더 하는 방식
      • 같은 거리지만 좌표축 방향에서 가장 크게 뻗어나가는 형태를 보인다.
      • 좌표축에 따라 거리가 달라지기 때문에 특정 벡터가 개별적인 의미(ex. 키, 몸무게)를 가지고 있을때 사용합니다.
      • 따라서, L1을 사용하는 경우에는 특정 feature의 영향이 강하게 적용될 수 있다는 것으로 이해할 수 있다.
    • L2 distance
      • 오른쪽에 보이는 원은 L2 Distance의 관점에서는 원 습된 이미지와 테스트 이미지의 픽셀 간의 차이의 제곱 합의 제곱근을 거리로 이용하는 방식
      • 같은 거리를 가지는 경우에는 모든 방향으로 균일하게 뻗어나가는 형태를 보인다.
      • 특징 벡터가 일반적인 벡터이고, 요소들간의 실질적인 의미를 잘 모르는 경우에 사용합니다.
      • 따라서, L2를 사용하는 경우에는 모든 feature의 영향이 골고루 적용된다는 것으로 이해할 수 있다.

K-NN는 거리척도에 따라 다양한 문제를 해결할 수 있는데, 벡터나 이미지 외에 문장도 분류가 가능합니다. 즉 거리 척도만 정해주면 어떤 종류의 데이터도 다룰 수 있습니다. 어떤 거리 척도를 사용하는지에 따라서 실제 기하학적으로 어떻게 변하는지 살펴보겠습니다.

image

  • 양 쪽 모두 동일한 데이터로, 왼쪽은 L1 Distance를 오른쪽은 L2 Distance를 사용했습니다.
  • 결과를 보면 거리 척도에 따라서 결정 경계의 모양 자체가 달라짐을 알 수 있습니다.
  • 두 경우를 비교해보면, L2 distance에서 decision boundary가 더 smooth해지는 경향이 있습니다.
    • L1은 좌표 시스템의 영향을 받기 때문에 결정 경계가 좌표축 방향으로의 영향을 더 크게 받지만, L2는 모든 방향으로의 영향을 골고루 받기 때문에 조금 더 자연스럽습니다.

Hyperparameters

image

  • Hyperparameter 는 (학습 이전에) 학습할 모델에 대해 설정하는 파라미터들을 의미한다.
    • Ex) K-Nearest Neighbors에서 K와 Distance(L1/L2…)
  • 즉, HyperParameter는 학습을 하는데 영향을 미치는 parameter 이고 학습을 하기 전 선택하는 parameter 입니다.
  • Train time에 학습하는 것이 아니므로 데이터로 직접 학습시킬 방법이 없습니다. 그러므로 학습 전 사전에 반드시 선택해야만 합니다.
  • “해당 문제”와 “데이터”에 맞는 모델의 Hyperparameter를 설정하는 방법 은 다음과 같습니다.
    • 문제에 따라 다르므로(Problem dependent), 가장 잘 동작하는 값을 사용한다.
    • 여러번의 학습을 통해 성능을 구하고, 그래프를 그려서 가장 좋은 hyperparameter 조합으로 설정한다.

Setting Hyperparameters

다양한 방법으로 실험을 하여 최적의 hyperparameter 값을 찾는 일은 아주 중요합니다. 하이퍼파라미터 값들을 실험해 보는 작업도 다양합니다.

Idea #1: 모든 데이터를 Train 으로 사용하는 방법

image

  • 첫번째 로 생각할 수 있는 방법은 전체 데이터셋을 이용해 학습했을때 가장 좋은 성능을 보인 hyperparameter를 사용하는 것입니다.
  • terrible한 방법. 절대 이렇게 하면 안됩니다.

전체 데이터셋의 정확도를 올리는 전략대로 한다면 K-NN 분류기의 경우 K=1 일 때 학습 데이터를 가장 완벽하게 분류합니다. 하지만 앞선 예제에서도 보았듯이, 실제로는 K를 더 큰 값으로 선택하는 것이 학습 데이터에서는 몇 개 잘못 분류할 수는 있지만 학습 데이터에 없던 데이터에 대해서는 더 좋은 성능을 보일 수 있습니다.

궁극적으로 기계학습에서는 학습 데이터를 얼마나 잘 맞추는지는 중요한게 아니라, 우리가 학습시킨 분류기가 한번도 보지 못한 데이터를 얼마나 잘 예측하는지가 중요하므로, 학습 데이터에만 신경쓰는 것은 최악입니다.

Idea #2: 데이터를 Train과 Test 로 나누는 방법

image

  • 두번째 로, 전체 데이터셋을 train data와 test data로 나누어 train data 로 학습을 시킨뒤 test data 에서 가장 성능이 높게 나오는 hyperparameter를 사용하는 것입니다.
  • 해당 방법이 조금 더 합리적인 것 같지만, Test 데이터에 대해서만 좋은 결과 값을 가질 수도 있기에 이 방법 또한 아주 끔찍한 방법입니다. 절대 하면 안됩니다.

다시한번 기계학습의 궁극적인 목적은 한번도 보지 못한 데이터에서 잘 동작해야 합니다.

Idea #3: 데이터를 Train, Validation(Dev), Test 로 나누는 방법

image

  • 데이터의 대부분은 training set으로 나누고, 일부는 validation set, 그리고 나머지는 test set으로 나눕니다.
  • trainning set, validation set을 통해 hyperparmeter 에 대해 실험해보고 마지막으로 새로운 데이터인 test set을 통해 평가하는 방법입니다.

최종적으로 개발/디버깅 등 모든 일들을 다 마친 후에 validation set에서 가장 좋았던 분류기를 가지고, test set에서는 “오로지 한번만” 수행합니다. 이 숫자가 알고리즘이 한번도 보지 못한 데이터에 얼마나 잘 동작해 주는지를 실질적으로 말해줄 수 있는 것입니다. 즉, 이 데이터를 최대한 “Unseen Data”로 활용하는 방법 입니다.

Idea #4: Cross-Validation

image

  • Cross-Validation 은 아래 그림과 같이 Dataset를 Fold 단위로 자르고, 이 fold 중 하나를 validation으로 선택하고 나머지를 train 데이터로 사용합니다.
  • 위 방법으로 Validation으로 사용할 fold를 바꿔가면서 반복 하고, 이중에 가장 좋은 성능을 가지는 Hyperparameter를 찾아내는 방법입니다.
  • 이 방법은 validation의 데이터가 편향되는 현상을 방지할 수 있습니다.
  • 기존에 방식보다 많은 학습시간을 요구하며 이 방법은 딥러닝에서 거의 사용되지 않고 데이터가 적은 상황에서 유용한 장점을 가집니다.

그림에서 데이터를 training/validation으로 딱 나눠 놓는 대신, training data를 여러 부분으로 나눠줍니다. 이런 식으로 번갈아가면서 validation set을 지정해 줍니다.

해당 예제에서는 5-Fold Cross Validation을 사용하고 있습니다. 처음 4개의 fold에서 하이퍼 파라미터를 학습시키고, 남은 한 fold에서 알고리즘을 평가합니다. 그리고 1,2,3,5 fold에서 다시 학습시키고 4 fold로 평가합니다. 이런식으로 계속 순환하여 최적의 하이퍼파라미터를 확인할 수 있을 것입니다. 이런 방식은 거의 표준이긴 하지만 실제로는 딥러닝같은 큰 모델을 학습시킬 때는 학습 자체가 계산량이 많기 때문에 실제로는 잘 쓰지 않습니다.

image

Cross Validation을 수행하고 나면 위와 같은 그래프를 보실 수 있습니다.

  • X축은 K-NN의 K입니다. Y축은 분류 정확도입니다.
  • 그래프는 각 K마다 5번의 Cross Validation을 통해 알고리즘이 얼마나 잘 동작하는지를 알려줍니다.
  • K-NN 의 K 값을 5-fold cross-validation 을 통해 나타내면 위 그래프와 같이 나타낼 수 있고, 이 경우에는 k 가 약 7일때 최적의 hyperparameter 가 됨을 알 수 있습니다.

그리고 Cross Validation을 이용하여, 여러 validation folds 별 성능의 분산(variance) 을 고려하여 “테스트셋이 알고리즘 성능 향상에 미치는 영향” 를 알아볼 수 있습니다. 분산을 같이 계산하게 되면, 어떤 하이퍼파라미터가 가장 좋은지 뿐만 아니라, 그 성능의 분산도 알 수 있습니다.

그러므로 하이퍼파라미터에 따라 모델의 정확도와 성능을 평가할 수 있으며, Validation에서 가장 좋은 성능을 내는 하이퍼파라미터를 선택하는 것이 좋은 전략입니다.

K-Nearest Neighbor on images never used.

하지만 실제로 image classification task 에서는 다음과 같은 이유들 때문에 K-Nearest Neighbor Algorithm을 잘 사용하지 않습니다.

image

  • 우선 한 가지 문제점은 k-nn이 너무 느리다는 것입니다.
  • 또 하나의 문제는 L1/L2 Distance가 이미지간의 거리를 측정하기에 적절하지 않다는 점입니다. 즉 벡터간의 거리 측정 관련 함수들은(L1/L2) 이미지들 간의 “지각적 유사성” 을 측정하는 척도로는 적절하지 않습니다.

위 사진은 가장 왼쪽에 원본 이미지와 변형된 3개의 이미지를 보여줍니다. 여기서 재미있는 부분은 원본사진과 각각의 사진에 거리가 모두 같은 사진입니다. 즉 원본 이미지에서 약간의 변형을 가한 세 개의 이미지는 모두 같은 L2 distance를 가집니다. 이러한 관점에서 이미지의 Distance의 값은 그렇게 의미 있는 값이 아닙니다. L1 distance 도 마찬가지입니다.

image

  • 차원의 저주(Curse of dimensionality) : 차원이 늘어날수록 필요한 train data가 기하급수적으로 증가함
  • k-NN 알고리즘은 training data를 이용해서 공간을 분할하여 분류를 진행했습니다. 이게 잘 동작하려면 전체 공간을 densely 하게 채울 수 있을만한 데이터가 필요한데, 차원이 커지면 필요한 training data 수가 기하급수적으로 증가합니다. 기하급수적인 증가는 언제나 옳지 못합니다.
    • 1차원 에서는 4개의 데이터가 필요 했다면,
    • 2차원 에서는 4 * 4 = 16개의 데이터가,
    • 3차원 에서는 4 * 4 * 4 = 64개의 데이터가 필요합니다.

고차원의 이미지라면 모든 공간을 조밀하게 메울만큼의 데이터를 모으는 일은 현실적으로 불가능합니다. 또한 Nearest한 data point가 실제로는 아주 멀리 떨어진 데이터일 수도 있다.(즉, 아주 밀집된 경우에서만 잘 동작한다는 의미) 그러므로 K-NN을 사용할 시 항상 이 점을 염두해야 합니다.


K-Nearest Neighbors: Summary

image

  • 요약을 해보자면 이미지 분류가 무엇인지 설명하기 위해 K-NN 예제를 들었습니다.
  • “이미지”와 “정답 레이블”이 있는 트레이닝 셋이 있었고 테스트 셋을 예측하는데 이용하였습니다.


Linear Classification

image

image

  • Linear Classification(선형 분류) 은 단순하지만 이후에 배우게 되는 Neural Network와 CNN의 기반이 되는 알고리즘입니다. 즉 아래 그림과 같이 기본 블럭이 되는 것입니다.
  • 앞으로 보게될 다양한 종류의 딥러닝 알고리즘들의 가장 기본이 되는 building block중 하나가 바로 Linear classifier입니다.
  • Linear classification이 어떻게 동작하는지를 정확히 이해하는것은 아주 중요합니다.

Parametric Approach: Linear Classification

Linear classification에서는 K-NN과는 조금은 다른 접근 방법을 이용합니다.

image

위 그림에서 $W$에 train data의 요약된 정보가 들어있습니다. 딥러닝은 **$f(x,W)$** 로 된 가설 함수를 적절하게 잘 설계하는 일입니다. 입력데이터 $x$ 와 가중치(weight) $W$ 를 조합하여 관계식을 만들때 가장 기초적인 방법이 이 둘을 곱하는 것이고 그것이 Linear Classification 입니다.

  • Linear classifier는 Parametric Approach 를 사용하며 “parametric model”의 가장 단순한 형태입니다.
    • Parametric approach는 모델의 파라미터(가중치)를 학습하는 방법입니다
    • K-NN에서는 파라미터 없이 전체 데이터를 저장하고 비교하기 때문에 prediction에서 느렸지만, linear classifier는 training data의 정보를 요약하여 요약된 정보를 파라미터 W에 모아줍니다. 즉 가중치를 학습하므로 prediction을 빠르게 수행할 수 있습니다.
    • 그러므로 딥러닝은 바로 이 함수 $F$의 구조를 적절하게 잘 설계하는 일이라고 할 수 있습니다.
  • Linear classifier의 출력(class score)은 (데이터와 가중치 행렬의 inner product) + bias로 계산한다.
    • $f(x,W)=Wx+b$
      • $f(x,W)$ : class score를 반환
      • $W$ : 모델의 가중치 파라미터
      • $x$ : data
      • $b$ : bias
  • 여기서, bias는 학습과는 무관한 데이터의 일부 클래스에 대한 선호도를 의미합니다. 주로 dataset이 unbalance할 때 사용합니다.(data independent scaling offset)
    • Ex) 개와 고양이의 분류(unbalance)
      • 개의 데이터가 고양이보다 많은 경우, bias는 고양이에서 개보다 높게 됨
  • Ex) CIFAR-10에서의 Linear Classifier 10개의 카테고리 중 하나를 분류
    • Image를 펼침 (입력 이미지 32X32X3 = 3072개의 원소로 구성된 1차원 벡터)
    • $f(x,W)=Wx+b$
      • $f(x,W)$ : 10 x 1
        • 10개의 class이므로, 10개의 숫자로 이루어진 class score를 반환
      • $W$ : 10 x 3072
      • $x$ : 3072 x 1
      • $b$ : 10 x 1

입력이미지는 행렬의 형태로 W(weight)와 곱(내적)해지게 되고 거기에 bias를 더해 각 class에 대한 점수가 나옵니다. 즉, 여기서 곱해지는 w 의 각각의 행(row)이 각 클래스의 평균적인 템플릿 이라고 할 수 있습니다.

Algebraic Viewpoint

함수가 어떻게 동작하는지 그림으로 살펴보겠습니다.

image

이 그림을 보면 왼쪽에 2x2 입력 이미지가 있고 전체 4개의 픽셀입니다. Linear classifier는 2x2 이미지를 입력으로 받고 이미지를 4-dim 열 벡터로 쭉 폅니다. 이 예제에서는 고양이, 개, 배 이렇게 세가지 클래스만 분류하는 가정을 하겠습니다.

  • 가중치 행렬 W는 4x3 행렬이 됩니다.
  • 입력은 픽셀 4개고 클래스는 총 3개 입니다.
  • 추가적으로 3-dim bias 벡터가 있습니다. bias는 데이터와 독립적으로 각 카테고리에 scailing offsets을 더해주어 연결됩니다.
  • “고양이 스코어” 는 입력 이미지의 픽셀 값들과 가중치 행렬을 내적한 값에 bias term을 더한 것입니다.

이러한 관점에서 Linear classification은 템플릿 매칭 과 거의 유사합니다. 가중치 행렬 W의 각 행은 각 이미지에 대한 템플릿으로 볼 수 있고, 가중치 행 벡터와 이미지의 열벡터 간의 내적을 계산하는데, 여기에서 내적이란 결국 클래스 간 탬플릿의 유사도를 측정하는 것과 유사함을 알 수 있습니다.

Interpreting a Linear Classifier

Visual Viewpoint

image

  • Linear Classifier에서 가중치 행렬의 각 행은 각 class에 대한 템플릿 이라고 해석할 수 있습니다.
    • 각 행에서의 위치 값들은 해당 위치의 픽셀이 해당 클래스에 얼마나 영향을 미치는지를 알려줍니다.
    • 따라서, 가중치 행렬의 각 행을 이미지로 시각화하면, linear classifier가 데이터를 어떻게 바라보는지 알 수 있다.
  • 위 슬라이드 하단의 희미한 그림들은 실제 가중치 행렬이 어떻게 학습되는지 볼 수 있는데, CIFAR-10의 plane, car, bird 등 각 10개의 카테고리에 해당하는 가중치 행렬의 각 행을 시각화 한 것입니다.

Linear classifier 의 문제점은 한 class 내에 다양한 특징들이 존재할 수 있지만, 모든 것들을 평균화시킨다는 점 이 있습니다. 그래서 다양한 모습들이 있더라도 각 카테고리를 인식하기위해 단 하나의 템플릿만을 학습하다는 것입니다.

말(馬)을 분류하는 템플릿을 살펴보면 바닥은 푸르스름해 보이며, 보통 말이 풀밭에 서 있으니 템플릿이 바닥을 푸르스름하게 학습한 것입니다. 그런데 유심히 살펴보면 말의 머리가 두 개로 각 사이드 마다 하나씩 달려 있습니다. 머리 두개 달린 말은 존재하지 않습니다. 하지만 Linear classifier가 클래스 당 하나의 템플릿밖에 허용하지 않으므로 이 방법이 최선입니다.

하지만 클래스 당 하나의 템플릿만 학습 할 수 있다는 것과 같은 제약조건이 없는 Neural Network같은 복잡한 모델이라면 조금 더 정확도 높은 결과를 볼 수 있을 것입니다.

Geometric Viewpoint

Linear classifier 을 또 다른 관점으로 해석하면 이미지를 고차원의 한 점 으로 볼 수도 있습니다.

image

  • 각 이미지을 고차원 공간의 한 점이라고 생각했을때, Linear classifier는 각 클래스를 구분시켜주는 선형 결정 경계를 그어주는 역할을 합니다.
  • 가령 왼쪽 상단에 비행기를 예로, Linear classifier는 파란색 선을 학습해서 비행기와 다른 클래스를 구분할 수 있습니다.

Hard cases for a linear classifier

image

  • 하지만 이미지가 고차원 공간의 하나의 점 이라는 관점으로 해석하면 다음과 같은 데이터들은 Linear Classifier 를 통해 분류하기 어렵다는 문제점이 있습니다.
    • linear한 boundary만 그릴 수 있다.
    • boundary가 linear하지 않은 경우, 잘 동작하지 않는다.
    • 데이터가 몇개의 점처럼 나타나는 경우에 잘 동작하지 않는다.

image

Linear classifier는 단순히 행렬과 벡터 곱의 형태라는 것을 알았고, 템플릿 매칭과 관련이 있고, 이 관점에서 해석해 보면 각 카테고리에 대해 하나의 템플릿을 학습한다는 것을 배웠습니다. 그리고 가중치 행렬 W를 학습시키고 나면 새로운 학습 데이터에도 스코어를 매길 수 있습니다.

Linear classifier가 어떻게 생겼고, 어떻게 동작하는지만 가볍게 알아보았습니다. 다음 시간에는 적절한 가중치 행렬 W를 고르는 법과 다양한 알고리즘들에 대해서 다뤄보도록 하며, 더 나아가 Loss function/optimization/ConvNets에 대해서 배울 것입니다.

Read more

Linear Regression 학습을 위한 pytorch 기본2

|

1. nn.Module로 구현하는 선형 회귀

파이토치에서 이미 구현되어져 제공되고 있는 함수들을 불러오는 것으로 더 쉽게 선형 회귀 모델을 구현해보겠습니다.

1.1 단순 선형 회귀 구현하기

import torch
import torch.nn as nn
import torch.nn.functional as F

torch.manual_seed(1)

데이터를 선언합니다. 아래 데이터는 $y=2x$를 가정된 상태에서 만들어진 데이터로 정답이 W=2, b=0임을 알고 있는 상황입니다. 모델이 이 두 W와 b의 값을 제대로 찾아내도록 하는 것이 목표입니다.

# 데이터
x_train = torch.FloatTensor([[1], [2], [3]])
y_train = torch.FloatTensor([[2], [4], [6]])

데이터를 정의하였으니 이제 선형 회귀 모델을 구현할 차례입니다. nn.Linear()는 입력의 차원, 출력의 차원을 인수로 받습니다.

# 모델을 선언 및 초기화. 단순 선형 회귀이므로 input_dim=1, output_dim=1.
model = nn.Linear(1,1)

torch.nn.Linear 인자로 1, 1을 사용하였습니다. 하나의 입력 $x$에 대해서 하나의 출력 $y$을 가지므로, 입력 차원과 출력 차원 모두 1을 인수로 사용하였습니다. model에는 가중치 W와 편향 b가 저장되어져 있습니다. 이 값은 model.parameters()라는 함수를 사용하여 불러올 수 있는데, 한 번 출력해보겠습니다.

print(list(model.parameters()))
[Parameter containing:
tensor([[-0.1119,  0.2710, -0.5435]], requires_grad=True), Parameter containing:
tensor([0.3462], requires_grad=True)]

2개의 값이 출력되는데 첫번째 값이 W고, 두번째 값이 b에 해당됩니다. 두 값 모두 현재는 랜덤 초기화가 되어져 있습니다. 그리고 두 값 모두 학습의 대상이므로 requires_grad=True가 되어져 있는 것을 볼 수 있습니다.

이제 옵티마이저를 정의합니다. model.parameters()를 사용하여 W와 b를 전달합니다. 학습률(learning rate)은 0.01로 정합니다.

# optimizer 설정. 경사 하강법 SGD를 사용하고 learning rate를 의미하는 lr은 0.01
optimizer = torch.optim.SGD(model.parameters(), lr=0.01) 
# 전체 훈련 데이터에 대해 경사 하강법을 2,000회 반복
nb_epochs = 2000
for epoch in range(nb_epochs+1):

    # H(x) 계산
    prediction = model(x_train)

    # cost 계산
    cost = F.mse_loss(prediction, y_train) # <== 파이토치에서 제공하는 평균 제곱 오차 함수

    # cost로 H(x) 개선하는 부분
    # gradient를 0으로 초기화
    optimizer.zero_grad()
    # 비용 함수를 미분하여 gradient 계산
    cost.backward() # backward 연산
    # W와 b를 업데이트
    optimizer.step()

    if epoch % 100 == 0:
    # 100번마다 로그 출력
      print('Epoch {:4d}/{} Cost: {:.6f}'.format(
          epoch, nb_epochs, cost.item()
      ))
Epoch    0/2000 Cost: 13.103540
... 중략 ...
Epoch 2000/2000 Cost: 0.000000

학습이 완료되었습니다. Cost의 값이 매우 작습니다. $W$와 $b$의 값도 최적화가 되었는지 확인해봅시다. $x$에 임의의 값 4를 넣어 모델이 예측하는 $y$의 값을 확인해보겠습니다.

# 임의의 입력 4를 선언
new_var =  torch.FloatTensor([[4.0]]) 
# 입력한 값 4에 대해서 예측값 y를 리턴받아서 pred_y에 저장
pred_y = model(new_var) # forward 연산
# y = 2x 이므로 입력이 4라면 y가 8에 가까운 값이 나와야 제대로 학습이 된 것
print("훈련 후 입력이 4일 때의 예측값 :", pred_y) 
훈련 후 입력이 4일 때의 예측값 : tensor([[7.9989]], grad_fn=<AddmmBackward0>)

이 문제의 정답은 $y=2x$가 정답이므로 $y$값이 8에 가까우면 $W$와 $b$의 값이 어느정도 최적화가 된 것으로 볼 수 있습니다. 실제로 예측된 $y$값은 7.9989로 8에 매우 가깝습니다.

이제 학습 후의 $W$와 $$의 값을 출력해보겠습니다.

print(list(model.parameters()))
[Parameter containing:
tensor([[1.9994]], requires_grad=True), Parameter containing:
tensor([0.0014], requires_grad=True)]

$W$의 값이 2에 가깝고, $b$의 값이 0에 가까운 것을 볼 수 있습니다.

  • $H(x)$ 식에 입력 $x$로부터 예측된 $y$를 얻는 것을 forward 연산이라고 합니다.
  • 학습 전, prediction = model(x_train)은 x_train으로부터 예측값을 리턴하므로 forward 연산입니다.
  • 학습 후, pred_y = model(new_var)는 임의의 값 new_var로부터 예측값을 리턴하므로 forward 연산입니다.
  • 학습 과정에서 비용 함수를 미분하여 기울기를 구하는 것을 backward 연산이라고 합니다.
  • cost.backward()는 비용 함수로부터 기울기를 구하라는 의미이며 backward 연산입니다.

1.2 다중 선형 회귀 구현하기

nn.Linear()nn.functional.mse_loss()로 다중 선형 회귀를 구현해봅니다. 사실 코드 자체는 달라지는 건 거의 없는데, nn.Linear()의 인자값과 학습률(learning rate)만 조절해주었습니다.

데이터를 선언해줍니다. 여기서는 3개의 $x$로부터 하나의 $y$를 예측하는 문제입니다. 즉, 가설 수식은 $H(x) = w_{1}x_{1} + w_{2}x_{2} + w_{3}x_{3} + b$입니다.

# 데이터
x_train = torch.FloatTensor([[73, 80, 75],
                             [93, 88, 93],
                             [89, 91, 90],
                             [96, 98, 100],
                             [73, 66, 70]])
y_train = torch.FloatTensor([[152], [185], [180], [196], [142]])

데이터를 정의하였으니 이제 선형 회귀 모델을 구현할 차례입니다. nn.Linear()는 입력의 차원, 출력의 차원을 인수로 받습니다.

# 모델을 선언 및 초기화. 다중 선형 회귀이므로 input_dim=3, output_dim=1.
model = nn.Linear(3,1)

torch.nn.Linear 인자로 3, 1을 사용하였습니다. 3개의 입력 x에 대해서 하나의 출력 y을 가지므로, 입력 차원은 3, 출력 차원은 1을 인수로 사용하였습니다. model에는 3개의 가중치 w와 편향 b가 저장되어져 있습니다. 이 값은 model.parameters()라는 함수를 사용하여 불러올 수 있는데, 한 번 출력해보겠습니다.

print(list(model.parameters()))
[Parameter containing:
tensor([[-0.1119,  0.2710, -0.5435]], requires_grad=True), Parameter containing:
tensor([0.3462], requires_grad=True)]

첫번째 출력되는 것이 3개의 $w$고, 두번째 출력되는 것이 $b$에 해당됩니다. 두 값 모두 현재는 랜덤 초기화가 되어져 있습니다. 그리고 두 출력 결과 모두 학습의 대상이므로 requires_grad=True가 되어져 있는 것을 볼 수 있습니다.

이제 옵티마이저를 정의합니다. model.parameters()를 사용하여 3개의 w와 b를 전달합니다. 학습률(learning rate)은 0.00001로 정합니다. 파이썬 코드로는 1e-5로도 표기합니다.

optimizer = torch.optim.SGD(model.parameters(), lr=1e-5) 
nb_epochs = 2000
for epoch in range(nb_epochs+1):

    # H(x) 계산
    prediction = model(x_train)
    # model(x_train)은 model.forward(x_train)와 동일함.

    # cost 계산
    cost = F.mse_loss(prediction, y_train) # <== 파이토치에서 제공하는 평균 제곱 오차 함수

    # cost로 H(x) 개선하는 부분
    # gradient를 0으로 초기화
    optimizer.zero_grad()
    # 비용 함수를 미분하여 gradient 계산
    cost.backward()
    # W와 b를 업데이트
    optimizer.step()

    if epoch % 100 == 0:
    # 100번마다 로그 출력
      print('Epoch {:4d}/{} Cost: {:.6f}'.format(
          epoch, nb_epochs, cost.item()
      ))
Epoch    0/2000 Cost: 31667.597656
... 중략 ...
Epoch 2000/2000 Cost: 0.199777

학습이 완료되었습니다. Cost의 값이 매우 작습니다. 3개의 w와 b의 값도 최적화가 되었는지 확인해봅시다. $x$에 임의의 입력 [73, 80, 75]를 넣어 모델이 예측하는 $y$의 값을 확인해보겠습니다.

# 임의의 입력 [73, 80, 75]를 선언
new_var =  torch.FloatTensor([[73, 80, 75]]) 
# 입력한 값 [73, 80, 75]에 대해서 예측값 y를 리턴받아서 pred_y에 저장
pred_y = model(new_var) 
print("훈련 후 입력이 73, 80, 75일 때의 예측값 :", pred_y) 
훈련 후 입력이 73, 80, 75일 때의 예측값 : tensor([[153.7184]], grad_fn=<AddmmBackward0>)

사실 3개의 값 73, 80, 75는 훈련 데이터로 사용되었던 값입니다. 당시 y의 값은 152였는데, 현재 예측값이 151이 나온 것으로 보아 어느정도는 3개의 w와 b의 값이 최적화 된것으로 보입니다. 이제 학습 후의 3개의 w와 b의 값을 출력해보겠습니다.

print(list(model.parameters()))
[Parameter containing:
tensor([[0.8541, 0.8475, 0.3096]], requires_grad=True), Parameter containing:
tensor([0.3568], requires_grad=True)]

2. 클래스로 파이토치 모델 구현하기

파이토치의 대부분의 구현체들은 대부분 모델을 생성할 때 클래스(Class)를 사용하고 있습니다. 앞의 선형 회귀를 클래스로 구현해보겠습니다. 앞서 구현한 코드와 다른 점은 오직 클래스로 모델을 구현했다는 점입니다.

2.1 모델을 클래스로 구현하기

앞서 단순 선형 회귀 모델은 다음과 같이 구현했었습니다.

# 모델을 선언 및 초기화. 단순 선형 회귀이므로 input_dim=1, output_dim=1.
model = nn.Linear(1,1)

이를 클래스로 구현하면 다음과 같습니다.

class LinearRegressionModel(nn.Module): # torch.nn.Module을 상속받는 파이썬 클래스
    def __init__(self): #
        super().__init__()
        self.linear = nn.Linear(1, 1) # 단순 선형 회귀이므로 input_dim=1, output_dim=1.

    def forward(self, x):
        return self.linear(x)
model = LinearRegressionModel()

위와 같은 클래스를 사용한 모델 구현 형식은 대부분의 파이토치 구현체에서 사용하고 있는 방식으로 반드시 숙지할 필요가 있습니다.

클래스(class) 형태의 모델은 nn.Module 을 상속받습니다. 그리고 __init__()에서 모델의 구조와 동적을 정의하는 생성자를 정의합니다. 이는 파이썬에서 객체가 갖는 속성값을 초기화하는 역할로, 객체가 생성될 때 자동으호 호출됩니다. super() 함수를 부르면 여기서 만든 클래스는 nn.Module 클래스의 속성들을 가지고 초기화 됩니다. foward() 함수는 모델이 학습데이터를 입력받아서 forward 연산을 진행시키는 함수입니다. 이 forward() 함수는 model 객체를 데이터와 함께 호출하면 자동으로 실행이됩니다. 예를 들어 model이란 이름의 객체를 생성 후, model(입력 데이터)와 같은 형식으로 객체를 호출하면 자동으로 forward 연산이 수행됩니다.

  • $H(x)$ 식에 입력 $x$로부터 예측된 $y$를 얻는 것을 forward 연산이라고 합니다.

앞서 다중 선형 회귀 모델은 다음과 같이 구현했었습니다.

# 모델을 선언 및 초기화. 다중 선형 회귀이므로 input_dim=3, output_dim=1.
model = nn.Linear(3,1)

이를 클래스로 구현하면 다음과 같습니다.

class MultivariateLinearRegressionModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(3, 1) # 다중 선형 회귀이므로 input_dim=3, output_dim=1.

    def forward(self, x):
        return self.linear(x)
model = MultivariateLinearRegressionModel()

2.2 단순 선형 회귀 클래스로 구현하기

이제 모델을 클래스로 구현한 코드를 보겠습니다. 달라진 점은 모델을 클래스로 구현했다는 점 뿐입니다. 다른 코드는 전부 동일합니다.

import torch
import torch.nn as nn
import torch.nn.functional as F

torch.manual_seed(1)
# 데이터
x_train = torch.FloatTensor([[1], [2], [3]])
y_train = torch.FloatTensor([[2], [4], [6]])
class LinearRegressionModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(1, 1)

    def forward(self, x):
        return self.linear(x)
model = LinearRegressionModel()
# optimizer 설정. 경사 하강법 SGD를 사용하고 learning rate를 의미하는 lr은 0.01
optimizer = torch.optim.SGD(model.parameters(), lr=0.01) 
# 전체 훈련 데이터에 대해 경사 하강법을 2,000회 반복
nb_epochs = 2000
for epoch in range(nb_epochs+1):

    # H(x) 계산
    prediction = model(x_train)

    # cost 계산
    cost = F.mse_loss(prediction, y_train) # <== 파이토치에서 제공하는 평균 제곱 오차 함수

    # cost로 H(x) 개선하는 부분
    # gradient를 0으로 초기화
    optimizer.zero_grad()
    # 비용 함수를 미분하여 gradient 계산
    cost.backward() # backward 연산
    # W와 b를 업데이트
    optimizer.step()

    if epoch % 100 == 0:
    # 100번마다 로그 출력
      print('Epoch {:4d}/{} Cost: {:.6f}'.format(
          epoch, nb_epochs, cost.item()
      ))
Epoch    0/2000 Cost: 4.610449
... 중략 ...
Epoch 2000/2000 Cost: 0.000004

2.3 다중 선형 회귀 클래스로 구현하기

다중 선형 회귀도 마찬가지입니다.

import torch
import torch.nn as nn
import torch.nn.functional as F

torch.manual_seed(1)
# 데이터
x_train = torch.FloatTensor([[73, 80, 75],
                             [93, 88, 93],
                             [89, 91, 90],
                             [96, 98, 100],
                             [73, 66, 70]])
y_train = torch.FloatTensor([[152], [185], [180], [196], [142]])
class MultivariateLinearRegressionModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(3, 1) # 다중 선형 회귀이므로 input_dim=3, output_dim=1.

    def forward(self, x):
        return self.linear(x)
model = MultivariateLinearRegressionModel()
optimizer = torch.optim.SGD(model.parameters(), lr=1e-5) 
nb_epochs = 2000
for epoch in range(nb_epochs+1):

    # H(x) 계산
    prediction = model(x_train)
    # model(x_train)은 model.forward(x_train)와 동일함.

    # cost 계산
    cost = F.mse_loss(prediction, y_train) # <== 파이토치에서 제공하는 평균 제곱 오차 함수

    # cost로 H(x) 개선하는 부분
    # gradient를 0으로 초기화
    optimizer.zero_grad()
    # 비용 함수를 미분하여 gradient 계산
    cost.backward()
    # W와 b를 업데이트
    optimizer.step()

    if epoch % 100 == 0:
    # 100번마다 로그 출력
      print('Epoch {:4d}/{} Cost: {:.6f}'.format(
          epoch, nb_epochs, cost.item()
      ))
Epoch    0/2000 Cost: 52396.968750
... 중략 ...
Epoch 2000/2000 Cost: 0.635827

3. 미니 배치와 데이터 로드(Mini Batch and Data Load)

3.1 미니 배치와 배치 크기(Mini Batch and Batch Size)

데이터가 수십만개 이상이라면 전체 데이터에 대해서 경사 하강법을 수행하는 것은 매우 느릴 뿐만 아니라 많은 계산량이 필요합니다. 정말 어쩌면 메모리의 한계로 계산이 불가능한 경우도 있을 수 있습니다. 그렇기 때문에 전체 데이터를 더 작은 단위로 나누어서 해당 단위로 학습하는 개념이 나오게 되었습니다. 이 단위를 미니 배치(Mini Batch)라고 합니다.

image

위의 그림은 전체 데이터를 미니 배치 단위로 나누는 것을 보여줍니다. 미니 배치 학습을 하게되면 미니 배치만큼만 가져가서 미니 배치에 대한 대한 비용(cost)를 계산하고, 경사 하강법을 수행합니다. 그리고 다음 미니 배치를 가져가서 경사 하강법을 수행하고 마지막 미니 배치까지 이를 반복합니다. 이렇게 전체 데이터에 대한 학습이 1회 끝나면 1 에포크(Epoch)(전체 훈련 데이터가 학습에 한 번 사용된 주기)가 끝나게 됩니다.

미니 배치 학습에서는 미니 배치의 개수만큼 경사 하강법을 수행해야 전체 데이터가 한 번 전부 사용되어 1 에포크(Epoch)가 됩니다. 미니 배치의 개수는 결국 미니 배치의 크기를 몇으로 하느냐에 따라서 달라지는데 미니 배치의 크기를 배치 크기(batch size)라고 합니다.

  • 전체 데이터에 대해서 한 번에 경사 하강법을 수행하는 방법을 ‘배치 경사 하강법’이라고 부릅니다. 반면, 미니 배치 단위로 경사 하강법을 수행하는 방법을 ‘미니 배치 경사 하강법’이라고 부릅니다.
  • 배치 경사 하강법은 경사 하강법을 할 때, 전체 데이터를 사용하므로 가중치 값이 최적값에 수렴하는 과정이 매우 안정적이지만, 계산량이 너무 많이 듭니다. 미니 배치 경사 하강법은 경사 하강법을 할 때, 전체 데이터의 일부만을 보고 수행하므로 최적값으로 수렴하는 과정에서 값이 조금 헤매기도 하지만 훈련 속도가 빠릅니다.
  • 배치 크기는 보통 2의 제곱수를 사용합니다. ex) 2, 4, 8, 16, 32, 64… 그 이유는 CPU와 GPU의 메모리가 2의 배수이므로 배치크기가 2의 제곱수일 경우에 데이터 송수신의 효율을 높일 수 있다고 합니다.

3.2 이터레이션(Iteration)

image

위의 그림은 에포크와 배치 크기와 이터레이션(iteration)의 관계를 보여줍니다. 이터레이션은 한 번의 에포크 내에서 이루어지는 매개변수인 가중치 $W$와 $b$의 업데이트 횟수입니다. 전체 데이터가 2,000일 때 배치 크기를 200으로 한다면 이터레이션의 수는 총 10개입니다. 이는 한 번의 에포크 당 매개변수 업데이트가 10번 이루어짐을 의미합니다.

3.3 데이터 로드하기(Data Load)

파이토치에서는 데이터를 좀 더 쉽게 다룰 수 있도록 유용한 도구로서 데이터셋(Dataset)데이터로더(DataLoader)를 제공합니다. 이를 사용하면 미니 배치 학습, 데이터 셔플(shuffle), 병렬 처리까지 간단히 수행할 수 있습니다. 기본적인 사용 방법은 Dataset을 정의하고, 이를 DataLoader에 전달하는 것입니다.

여기서는 텐서를 입력받아 Dataset의 형태로 변환해주는 TensorDataset을 사용해보겠습니다. TensorDataset과 DataLoader를 임포트합니다.

import torch
import torch.nn as nn
import torch.nn.functional as F

from torch.utils.data import TensorDataset # 텐서데이터셋
from torch.utils.data import DataLoader # 데이터로더

TensorDataset은 기본적으로 텐서를 입력으로 받습니다. 텐서 형태로 데이터를 정의합니다.

x_train  =  torch.FloatTensor([[73,  80,  75], 
                               [93,  88,  93], 
                               [89,  91,  90], 
                               [96,  98,  100],   
                               [73,  66,  70]])  
y_train  =  torch.FloatTensor([[152],  [185],  [180],  [196],  [142]])

이제 이를 TensorDataset의 입력으로 사용하고 dataset으로 저장합니다.

dataset = TensorDataset(x_train, y_train)

파이토치의 데이터셋을 만들었다면 데이터로더를 사용 가능합니다. 데이터로더는 기본적으로 2개의 인자를 입력받는데 하나는 데이터셋, 미니 배치의 크기입니다. 이때 미니 배치의 크기는 통상적으로 2의 배수를 사용합니다. (ex) 64, 128, 256…) 그리고 추가적으로 많이 사용되는 인자로 shuffle이 있습니다. shuffle=True를 선택하면 Epoch마다 데이터셋을 섞어서 데이터가 학습되는 순서를 바꿉니다.

dataloader = DataLoader(dataset, batch_size=2, shuffle=True)

이제 모델과 옵티마이저를 설계합니다.

model = nn.Linear(3,1)
optimizer = torch.optim.SGD(model.parameters(), lr=1e-5) 

이제 훈련을 진행합니다.

nb_epochs = 20
for epoch in range(nb_epochs + 1):
    for batch_idx, samples in enumerate(dataloader):
        # print(batch_idx)
        # print(samples)
        x_train, y_train = samples
        # H(x) 계산
        prediction = model(x_train)

        # cost 계산
        cost = F.mse_loss(prediction, y_train)

        # cost로 H(x) 계산
        optimizer.zero_grad()
        cost.backward()
        optimizer.step()

        print('Epoch {:4d}/{} Batch {}/{} Cost: {:.6f}'.format(
            epoch, nb_epochs, batch_idx+1, len(dataloader),
            cost.item()
            ))
Epoch    0/20 Batch 1/3 Cost: 26085.919922
Epoch    0/20 Batch 2/3 Cost: 3660.022949
Epoch    0/20 Batch 3/3 Cost: 2922.390869
... 중략 ...
Epoch   20/20 Batch 1/3 Cost: 6.315856
Epoch   20/20 Batch 2/3 Cost: 13.519956
Epoch   20/20 Batch 3/3 Cost: 4.262849

Cost의 값이 점차 작아집니다. 이제 모델의 입력으로 임의의 값을 넣어 예측값을 확인합니다.

# 임의의 입력 [73, 80, 75]를 선언
new_var =  torch.FloatTensor([[73, 80, 75]]) 
# 입력한 값 [73, 80, 75]에 대해서 예측값 y를 리턴받아서 pred_y에 저장
pred_y = model(new_var) 
print("훈련 후 입력이 73, 80, 75일 때의 예측값 :", pred_y) 
훈련 후 입력이 73, 80, 75일 때의 예측값 : tensor([[153.2754]], grad_fn=<AddmmBackward0>)

4. 커스텀 데이터셋(Custom Dataset)

파이토치에서는 데이터셋을 좀 더 쉽게 다룰 수 있도록 유용한 도구로서 torch.utils.data.Datasettorch.utils.data.DataLoader를 제공하며, 이를 사용하면 미니 배치 학습, 데이터 셔플(shuffle), 병렬 처리까지 간단히 수행할 수 있습니다. 기본적인 사용 방법은 Dataset을 정의하고, 이를 DataLoader에 전달하는 것입니다.

4.1 커스텀 데이터셋(Custom Dataset)

그런데 torch.utils.data.Dataset을 상속받아 직접 커스텀 데이터셋(Custom Dataset)을 만드는 경우도 있습니다. torch.utils.data.Dataset은 파이토치에서 데이터셋을 제공하는 추상 클래스입니다. Dataset을 상속받아 다음 메소드들을 오버라이드 하여 커스텀 데이터셋을 만들어보겠습니다.

커스텀 데이터셋을 만들 때, 일단 가장 기본적인 뼈대는 아래와 같습니다. 여기서 필요한 기본적인 define은 3개입니다.

class CustomDataset(torch.utils.data.Dataset): 
    def __init__(self):return

    def __len__(self):return

    def __getitem__(self, idx):return

이를 좀 더 자세히 봅시다.

class CustomDataset(torch.utils.data.Dataset): 
    def __init__(self):return
    #  데이터셋의 전처리를 해주는 부분

    def __len__(self):return
    #  데이터셋의 길이. 즉, 총 샘플의 수를 적어주는 부분

    def __getitem__(self, idx):return
    #  데이터셋에서 특정 1개의 샘플을 가져오는 함수
  • len(dataset)을 했을 때 데이터셋의 크기를 리턴할 len
  • dataset[i]을 했을 때 i번째 샘플을 가져오도록 하는 인덱싱을 위한 get_item

4.2 커스텀 데이터셋(Custom Dataset)으로 선형 회귀 구현하기

import torch
import torch.nn.functional as F

from torch.utils.data import Dataset
from torch.utils.data import DataLoader
# Dataset 상속
class CustomDataset(Dataset): 
    def __init__(self):
        self.x_data = [[73, 80, 75],
                        [93, 88, 93],
                        [89, 91, 90],
                        [96, 98, 100],
                        [73, 66, 70]]
        self.y_data = [[152], [185], [180], [196], [142]]

    # 총 데이터의 개수를 리턴
    def __len__(self): 
        return len(self.x_data)

    # 인덱스를 입력받아 그에 맵핑되는 입출력 데이터를 파이토치의 Tensor 형태로 리턴
    def __getitem__(self, idx): 
        x = torch.FloatTensor(self.x_data[idx])
        y = torch.FloatTensor(self.y_data[idx])
        return x, y
dataset = CustomDataset()
dataloader = DataLoader(dataset, batch_size=2, shuffle=True)
model = torch.nn.Linear(3,1)
optimizer = torch.optim.SGD(model.parameters(), lr=1e-5) 
nb_epochs = 20
for epoch in range(nb_epochs + 1):
    for batch_idx, samples in enumerate(dataloader):
        # print(batch_idx)
        # print(samples)
        x_train, y_train = samples
        # H(x) 계산
        prediction = model(x_train)

        # cost 계산
        cost = F.mse_loss(prediction, y_train)

        # cost로 H(x) 계산
        optimizer.zero_grad()
        cost.backward()
        optimizer.step()

        print('Epoch {:4d}/{} Batch {}/{} Cost: {:.6f}'.format(
            epoch, nb_epochs, batch_idx+1, len(dataloader),
            cost.item()
            ))
Epoch    0/20 Batch 1/3 Cost: 27697.917969
Epoch    0/20 Batch 2/3 Cost: 4941.327148
Epoch    0/20 Batch 3/3 Cost: 1006.548645
... 중략 ...
Epoch   20/20 Batch 1/3 Cost: 1.181388
Epoch   20/20 Batch 2/3 Cost: 4.077807
Epoch   20/20 Batch 3/3 Cost: 0.055477
# 임의의 입력 [73, 80, 75]를 선언
new_var =  torch.FloatTensor([[73, 80, 75]]) 
# 입력한 값 [73, 80, 75]에 대해서 예측값 y를 리턴받아서 pred_y에 저장
pred_y = model(new_var) 
print("훈련 후 입력이 73, 80, 75일 때의 예측값 :", pred_y) 
훈련 후 입력이 73, 80, 75일 때의 예측값 : tensor([[149.6803]], grad_fn=<AddmmBackward0>)

Read more

자주 사용되는 pytorch 텐서 조작 함수(Tensor Manipulation Functions)

|

텐서 연산 및 조작 함수

텐서를 다룰때 주로 사용하는 함수에 대해 익히고 다루는 방법을 살펴보겠습니다.


텐서 연산 관련 함수(Tensor Operations Functions)

1. 행렬 곱셈과 곱셈의 차이(Matrix Multiplication Vs. Multiplication)

행렬로 곱셈을 하는 방법은 크게 두 가지가 있습니다.

  • 행렬 곱셈(.matmul)
  • 원소 별 곱셈(.mul)

파이토치 텐서의 행렬 곱셈을 보겠습니다. 이는 matmul()을 통해 수행합니다.

m1 = torch.FloatTensor([[1, 2], [3, 4]])
m2 = torch.FloatTensor([[1], [2]])
print('Shape of Matrix 1: ', m1.shape) # 2 x 2
print('Shape of Matrix 2: ', m2.shape) # 2 x 1
print(m1.matmul(m2)) # 2 x 1

[output]
Shape of Matrix 1:  torch.Size([2, 2])
Shape of Matrix 2:  torch.Size([2, 1])
tensor([[ 5.],
        [11.]])

위의 결과는 2 × 2 행렬과 2 × 1 행렬(벡터)의 행렬 곱셈의 결과를 보여줍니다.

행렬 곱셈이 아니라 element-wise 곱셈이라는 것이 존재하는데, 이는 동일한 크기의 행렬이 동일한 위치에 있는 원소끼리 곱하는 것을 말합니다. 아래는 서로 다른 크기의 행렬이 브로드캐스팅이 된 후에 element-wise 곱셈이 수행되는 것을 보여줍니다. 이는 * 또는 mul()을 통해 수행합니다.

m1 = torch.FloatTensor([[1, 2], [3, 4]])
m2 = torch.FloatTensor([[1], [2]])
print('Shape of Matrix 1: ', m1.shape) # 2 x 2
print('Shape of Matrix 2: ', m2.shape) # 2 x 1
print(m1 * m2) # 2 x 2
print(m1.mul(m2))

[output]
Shape of Matrix 1:  torch.Size([2, 2])
Shape of Matrix 2:  torch.Size([2, 1])
tensor([[1., 2.],
        [6., 8.]])
tensor([[1., 2.],
        [6., 8.]])

m1 행렬의 크기는 (2, 2) 이었습니다. m2 행렬의 크기는 (2, 1) 였습니다. 이때 element-wise 곱셈을 수행하면, 두 행렬의 크기는 브로드캐스팅이 된 후에 곱셈이 수행됩니다. 더 정확히는 여기서 m2의 크기가 변환됩니다.

# 브로드캐스팅 과정에서 m2 텐서가 어떻게 변경되는지 보겠습니다.
[1]
[2]
==> [[1, 1],
     [2, 2]]


2. 평균(Mean)

다음은 평균을 구하는 방법으로 Numpy에서의 사용법과 매우 유사합니다. 우선 1차원인 벡터를 선언하여 .mean()을 사용하여 원소의 평균을 구합니다.

t = torch.FloatTensor([1, 2])
print(t.mean())

[output]
tensor(1.5000)

이번에는 2차원인 행렬을 선언하여 .mean()을 사용해봅시다.

t = torch.FloatTensor([[1, 2], [3, 4]])
print(t)
print(t.mean())

[output]
tensor([[1., 2.],
        [3., 4.]])
tensor(2.5000)

이번에는 dim 즉, 차원(dimension)을 인자로 주는 경우를 보겠습니다.

print(t.mean(dim=0))

[output]
tensor([2., 3.])

dim=0 이라는 것은 첫번째 차원을 의미합니다. 행렬에서 첫번째 차원은 ‘행’을 의미하므로, 인자로 dim을 준다면 해당 차원을 제거한다는 의미가 됩니다. 다시 말해 행렬에서 ‘열’만을 남기겠다는 의미가 됩니다. 기존 행렬의 크기는 (2, 2) 였지만 이를 수행하면 열의 차원만 보존되면서 (1, 2) 가 되며, 이는 (2,) 와 같으며 벡터입니다. 열의 차원을 보존하면서 평균을 구하면 아래와 같이 연산합니다.

# 실제 연산 과정
t.mean(dim=0)은 입력에서 첫번째 차원을 제거한다.

[[1., 2.],
 [3., 4.]]

1과 3의 평균을 구하고, 2와 4의 평균을 구한다.
결과 ==> [2., 3.]

이번에는 인자로 dim=1 을 주겠습니다. 이번에는 두번째 차원을 제거합니다. 즉, 열이 제거된 텐서가 되어야 합니다.

print(t.mean(dim=1))

[output]
tensor([1.5000, 3.5000])

열의 차원이 제거되어야 하므로 (2, 2) 의 크기에서 (2, 1) 의 크기가 됩니다. 이번에는 1과 3의 평균을 구하고 3과 4의 평균을 구하게 됩니다. 그렇다면 결과는 아래와 같습니다.

# 실제 연산 결과는 (2 × 1)
[[ 1.5 ]
 [ 3.5 ]]

하지만 (2 × 1) 은 결국 1차원이므로 (1 × 2) 와 같이 표현되면서 위와 같이 [1.5, 3.5]로 출력됩니다. 이번에는 dim=-1 를 주는 경우를 보겠습니다. 이는 마지막 차원을 제거한다는 의미이고, 결국 열의 차원을 제거한다는 의미와 같습니다. 그러므로 위와 출력 결과가 같습니다.

print(t.mean(dim=-1))

[output]
tensor([1.5000, 3.5000])


3. 덧셈(Sum)

덧셈(Sum)은 평균(Mean)과 연산 방법이나 인자가 의미하는 바는 정확히 동일합니다.

t = torch.FloatTensor([[3, 2], [1, 4]])
print(t)

[output]
tensor([[3., 2.],
        [1., 4.]])
print(t.sum()) # 단순히 원소 전체의 덧셈을 수행
print(t.sum(dim=0)) # 행을 제거
print(t.sum(dim=1)) # 열을 제거
print(t.sum(dim=-1)) # 열을 제거

[output]
tensor(10.)
tensor([4., 6.])
tensor([5., 5.])
tensor([5., 5.])


4. 최대(Max)와 아그맥스(ArgMax)

Max는 원소의 최대값을 리턴하고, ArgMax는 최대값을 가진 인덱스를 리턴합니다.

t = torch.FloatTensor([[1, 2], [3, 4]])
print(t)

[output]
tensor([[1., 2.],
        [3., 4.]])

우선 (2, 2) 행렬을 선언하였습니다. 이제 .max()를 사용합니다.

print(t.max()) # Returns one value: max

[output]
tensor(4.)

이번에는 인자로 dim=0 을 주겠습니다. 첫번째 차원을 제거한다는 의미입니다.

print(t.max(dim=0)) # Returns two values: max and argmax

[output]
torch.return_types.max(
values=tensor([3., 4.]),
indices=tensor([1, 1]))

행의 차원을 제거한다는 의미이므로 (1, 2) 텐서를 만들며, 결과는 [3, 4]입니다.

그런데 [1, 1]이라는 값도 함께 리턴되었습니다. max에 dim 인자를 주면 argmax도 함께 리턴하는 특징 때문입니다. 첫번째 열에서 3의 인덱스는 1이었습니다. 두번째 열에서 4의 인덱스는 1이었습니다. 그러므로 [1, 1]이 리턴됩니다. 어떤 의미인지 다음과 같습니다.

# [1, 1]가 무슨 의미인지 봅시다. 기존 행렬을 다시 상기해봅시다.
[[1, 2],
 [3, 4]]
첫번째 열에서 0번 인덱스는 1, 1번 인덱스는 3입니다.
두번째 열에서 0번 인덱스는 2, 1번 인덱스는 4입니다.
다시 말해 3과 4의 인덱스는 [1, 1]입니다.

만약 두 개를 함께 리턴받는 것이 아니라 max 또는 argmax만 리턴받고 싶다면 다음과 같이 리턴값에도 인덱스를 부여하면 됩니다. 0번 인덱스를 사용하면 max 값만 받아올 수 있고, 1번 인덱스를 사용하면 argmax 값만 받아올 수 있습니다.

print('Max: ', t.max(dim=0)[0])
print('Argmax: ', t.max(dim=0)[1])

[output]
Max:  tensor([3., 4.])
Argmax:  tensor([1, 1])

이번에는 dim=1 로 인자를 주었을 때와 dim=-1 로 인자를 주었을 때를 보겠습니다.

print(t.max(dim=1))
print(t.max(dim=-1))

[output]
torch.return_types.max(
values=tensor([2., 4.]),
indices=tensor([1, 1]))

torch.return_types.max(
values=tensor([2., 4.]),
indices=tensor([1, 1]))


5. 기타 연산

import math

a = torch.rand(1, 2) * 2 - 1
print(a)
print(torch.abs(a))
print(torch.ceil(a))
print(torch.floor(a))
print(torch.clamp(a, -0.5, 0.5))

[output]
tensor([[-0.8096, -0.5772]])
tensor([[0.8096, 0.5772]])
tensor([[-0., -0.]])
tensor([[-1., -1.]])
tensor([[-0.5000, -0.5000]])
print(a)
print(torch.min(a))
print(torch.max(a))
print(torch.mean(a))
print(torch.std(a))
print(torch.prod(a))
print(torch.unique(torch.tensor([1, 2, 3, 1, 2, 2])))

[output]
tensor([[-0.8096, -0.5772]])
tensor(-0.8096)
tensor(-0.5772)
tensor(-0.6934)
tensor(0.1643)
tensor(0.4673)
tensor([1, 2, 3])


텐서 조작 관련 함수(Tensor Manipulation Functions)

1. 인덱싱(Indexing) - NumPy처럼 인덱싱 형태로 사용가능

x = torch.Tensor([[1, 2],
                  [3, 4]])
print(x)

print(x[0, 0])
print(x[0, 1])
print(x[1, 0])
print(x[1, 1])

print(x[:, 0])
print(x[:, 1])

print(x[0, :])
print(x[1, :])
tensor([[1., 2.],
        [3., 4.]])
tensor(1.)
tensor(2.)
tensor(3.)
tensor(4.)
tensor([1., 3.])
tensor([2., 4.])
tensor([1., 2.])
tensor([3., 4.])


2. 뷰(View) - 원소의 수를 유지하면서 텐서의 크기 변경

파이토치 텐서의 뷰(View)는 numpy에서의 Reshape와 같은 역할을 합니다. 텐서의 크기(Shape)를 변경해주는 역할을 합니다.

t = np.array([[[0, 1, 2],
               [3, 4, 5]],
              [[6, 7, 8],
               [9, 10, 11]]])
ft = torch.FloatTensor(t)

ft라는 이름의 3차원 텐서를 만들었습니다. 크기(shape)를 확인해보겠습니다.

print(ft.shape)

[output]
torch.Size([2, 2, 3])
x = torch.randn(4, 5)
print(x)
y = x.view(20)
print(y)
z = x.view(5, -1)
print(z)

[output]
tensor([[ 0.5298,  0.8756, -0.6373,  0.5298,  0.6077],
        [ 0.3980, -0.3816,  2.0473,  0.9791,  0.3316],
        [-0.1470,  0.5306,  0.1874,  1.3221, -1.3989],
        [ 1.0273,  0.2375,  0.4490, -1.6272, -0.8203]])
tensor([ 0.5298,  0.8756, -0.6373,  0.5298,  0.6077,  0.3980, -0.3816,  2.0473,
         0.9791,  0.3316, -0.1470,  0.5306,  0.1874,  1.3221, -1.3989,  1.0273,
         0.2375,  0.4490, -1.6272, -0.8203])
tensor([[ 0.5298,  0.8756, -0.6373,  0.5298],
        [ 0.6077,  0.3980, -0.3816,  2.0473],
        [ 0.9791,  0.3316, -0.1470,  0.5306],
        [ 0.1874,  1.3221, -1.3989,  1.0273],
        [ 0.2375,  0.4490, -1.6272, -0.8203]])


2.1 3차원 텐서에서 2차원 텐서로 변경

ft 텐서를 view를 사용하여 크기(shape)를 2차원 텐서로 변경해봅시다.

print(ft.view([-1, 3])) # ft라는 텐서를 (?, 3)의 크기로 변경
print(ft.view([-1, 3]).shape)

[output]
tensor([[ 0.,  1.,  2.],
        [ 3.,  4.,  5.],
        [ 6.,  7.,  8.],
        [ 9., 10., 11.]])
torch.Size([4, 3])

view([-1, 3]) 이 가지는 의미는 다음과 같습니다. -1은 첫번째 차원은 사용자가 잘 모르겠으니 파이토치에 맡기겠다는 의미이고, 3은 두번째 차원의 길이는 3을 가지도록 하라는 의미입니다. 다시 말해 현재 3차원 텐서를 2차원 텐서로 변경하되 (?, 3) 의 크기로 변경하라는 의미입니다. 결과적으로 (4, 3) 의 크기를 가지는 텐서를 얻었습니다.

즉 내부적으로 크기 변환은 다음과 같이 이루어졌습니다. (2, 2, 3) -> (2 × 2, 3) -> (4, 3)

  • view는 기본적으로 변경 전과 변경 후의 텐서 안의 원소의 개수가 유지되어야 합니다.
  • 파이토치의 view는 사이즈가 -1로 설정되면 다른 차원으로부터 해당 값을 유추합니다.

변경 전 텐서의 원소의 수는 (2 × 2 × 3) = 12개였습니다. 그리고 변경 후 텐서의 원소의 개수 또한 (4 × 3) = 12개였습니다.


2.2 3차원 텐서의 크기 변경

이번에는 3차원 텐서에서 3차원 텐서로 차원은 유지하되, 크기(shape)를 바꾸는 작업을 해보겠습니다. view로 텐서의 크기를 변경하더라도 원소의 수는 유지되어야 합니다.

print(ft.view([-1, 1, 3]))
print(ft.view([-1, 1, 3]).shape)

[output]
tensor([[[ 0.,  1.,  2.]],

        [[ 3.,  4.,  5.]],

        [[ 6.,  7.,  8.]],

        [[ 9., 10., 11.]]])
torch.Size([4, 1, 3])


3. item

텐서에 값이 단 하나라도 존재하면 숫자값을 얻을 수 있습니다.

x = torch.randn(1)
print(x)
print(x.item())
print(x.dtype)

[output]
tensor([1.3015])
1.3014956712722778
torch.float32

스칼라값 하나만 존재해야 item() 사용이 가능합니다.

x = torch.randn(2)
print(x)
print(x.item())
print(x.dtype)

[output]
tensor([ 0.8509, -0.5549])
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-34-7c023f92a1c8> in <module>()
      1 x = torch.randn(2)
      2 print(x)
----> 3 print(x.item())
      4 print(x.dtype)

ValueError: only one element tensors can be converted to Python scalars


4. 스퀴즈(Squeeze) - 1인 차원을 축소(제거)

스퀴즈는 차원이 1인 경우에는 해당 차원을 제거합니다. 우선 2차원 텐서를 만들겠습니다.

ft = torch.FloatTensor([[0], [1], [2]])
print(ft)
print(ft.shape)

[output]
tensor([[0.],
        [1.],
        [2.]])
torch.Size([3, 1])

해당 텐서는 (3 × 1) 의 크기를 가집니다. 두번째 차원이 1이므로 squeeze를 사용하면 (3,) 의 크기를 가지는 텐서로 변경됩니다.

print(ft.squeeze())
print(ft.squeeze().shape)

[output]
tensor([0., 1., 2.])
torch.Size([3])

위의 결과는 1이었던 두번째 차원이 제거되면서 (3,) 의 크기를 가지는 텐서로 변경되어 1차원 벡터가 된 것을 보여줍니다.


5. 언스퀴즈(Unsqueeze) - 특정 위치에 1인 차원을 추가한다. 즉 차원을 증가(생성)

언스퀴즈는 스퀴즈와 정반대입니다. 특정 위치에 1인 차원을 추가할 수 있습니다.

ft = torch.Tensor([0, 1, 2])
print(ft.shape)

[output]
torch.Size([3])

현재는 차원이 1개인 1차원 벡터입니다. 여기에 첫번째 차원에 1인 차원을 추가해보겠습니다. 첫번째 차원의 인덱스를 의미하는 숫자 0을 인자로 넣으면 첫번째 차원에 1인 차원이 추가됩니다.

print(ft.unsqueeze(0)) # 인덱스가 0부터 시작하므로 0은 첫번째 차원을 의미한다.
print(ft.unsqueeze(0).shape)

[output]
tensor([[0., 1., 2.]])
torch.Size([1, 3])

위 결과는 (3,) 의 크기를 가졌던 1차원 벡터가 (1, 3) 의 2차원 텐서로 변경된 것을 보여줍니다. 방금 한 연산을 view로도 구현 가능합니다. 2차원으로 바꾸고 싶으면서 첫번째 차원은 1이기를 원한다면 view에서 (1, -1) 을 인자로 사용하면 됩니다.

print(ft.view(1, -1))
print(ft.view(1, -1).shape)

[output]
tensor([[0., 1., 2.]])
torch.Size([1, 3])

위의 결과는 unsqueeze와 view가 동일한 결과를 만든 것을 보여줍니다. 이번에는 unsqueeze의 인자로 1을 넣어보겠습니다. 인덱스는 0부터 시작하므로 이는 두번째 차원에 1을 추가하겠다는 것을 의미합니다. 현재 크기는 (3,) 이었으므로 두번째 차원에 1인 차원을 추가하면 (3, 1) 의 크기를 가지게 됩니다.

print(ft.unsqueeze(1))
print(ft.unsqueeze(1).shape)

[output]
tensor([[0.],
        [1.],
        [2.]])
torch.Size([3, 1])

이번에는 unsqueeze의 인자로 -1을 넣어보겠습니다. -1은 인덱스 상으로 마지막 차원을 의미합니다. 현재 크기는 (3,) 이었으므로 마지막 차원에 1인 차원을 추가하면 (3, 1) 의 크기를 가지게 됩니다. 다시 말해 현재 텐서의 경우에는 1을 넣은 경우와 -1을 넣은 경우가 결과가 동일합니다.

print(ft.unsqueeze(-1))
print(ft.unsqueeze(-1).shape)

[output]
tensor([[0.],
        [1.],
        [2.]])
torch.Size([3, 1])

맨 뒤에 1인 차원이 추가되면서 1차원 벡터가 (3, 1) 의 크기를 가지는 2차원 텐서로 변경되었습니다. 즉, view(), squeeze(), unsqueeze()텐서의 원소 수를 그대로 유지하면서 모양과 차원을 조절합니다.

다음은 dim을 추가해보겠습니다.

t = torch.rand(3, 3)
print(t)
print(t.shape)

[output]
tensor([[0.6907, 0.8602, 0.4158],
        [0.4249, 0.5767, 0.8503],
        [0.5024, 0.4996, 0.1251]])
torch.Size([3, 3])
tensor = t.unsqueeze(dim=0)
print(tensor)
print(tensor.shape)

[output]
tensor([[[0.6907, 0.8602, 0.4158],
         [0.4249, 0.5767, 0.8503],
         [0.5024, 0.4996, 0.1251]]])
torch.Size([1, 3, 3])
tensor = t.unsqueeze(dim=1)
print(tensor)
print(tensor.shape)

[output]
tensor([[[0.6907, 0.8602, 0.4158]],

        [[0.4249, 0.5767, 0.8503]],

        [[0.5024, 0.4996, 0.1251]]])
torch.Size([3, 1, 3])
tensor = t.unsqueeze(dim=2)
print(tensor)
print(tensor.shape)

[output]
tensor([[[0.6907],
         [0.8602],
         [0.4158]],

        [[0.4249],
         [0.5767],
         [0.8503]],

        [[0.5024],
         [0.4996],
         [0.1251]]])
torch.Size([3, 3, 1])


6. 연결하기(concatenate)

텐서를 결합하는 메소드로, 쌓을 dim이 존재해야 합니다. 해당하는 차원을 늘려준 후 결합합니다.

x = torch.FloatTensor([[1, 2], [3, 4]])
y = torch.FloatTensor([[5, 6], [7, 8]])

이제 두 텐서를 torch.cat([ ])를 통해 연결해보겠습니다. 그런데 연결 방법은 한 가지만 있는 것이 아닙니다. torch.cat 은 어느 차원을 늘릴 것인지를 인자로 줄 수 있습니다. 예를 들어 dim=0 은 첫번째 차원을 늘리라는 의미를 담고있습니다.

print(torch.cat([x, y], dim=0))

[output]
tensor([[1., 2.],
        [3., 4.],
        [5., 6.],
        [7., 8.]])

dim=0 을 인자로 했더니 두 개의 (2 × 2) 텐서가 (4 × 2) 텐서가 된 것을 볼 수 있습니다. 이번에는 dim=1 을 인자로 주겠습니다.

print(torch.cat([x, y], dim=1))

[output]
tensor([[1., 2., 5., 6.],
        [3., 4., 7., 8.]])

dim=1 을 인자로 했더니 두 개의 (2 × 2) 텐서가 (2 × 4) 텐서가 된 것을 볼 수 있습니다.

딥러닝에서는 주로 모델의 입력 또는 중간 연산에서 두 개의 텐서를 연결하는 경우가 많습니다. 두 텐서를 연결해서 입력으로 사용하는 것은 두 가지의 정보를 모두 사용한다는 의미를 가지고 있습니다.


7. 스택킹(Stacking)

연결(concatenate)을 하는 또 다른 방법입니다. 때로는 연결을 하는 것보다 스택킹이 더 편리할 때가 있는데, 이는 스택킹이 많은 연산을 포함하고 있기 때문입니다. 크기가 (2,) 로 모두 동일한 3개의 벡터를 만듭니다.

x = torch.FloatTensor([1, 4])
y = torch.FloatTensor([2, 5])
z = torch.FloatTensor([3, 6])

이제 torch.stack을 통해서 3개의 벡터를 모두 Stacking 해보겠습니다.

print(torch.stack([x, y, z]))

[output]
tensor([[1., 4.],
        [2., 5.],
        [3., 6.]])

위 결과는 3개의 벡터가 순차적으로 쌓여 (3 × 2) 텐서가 된 것을 보여줍니다. 스택킹은 사실 많은 연산을 한 번에 축약하고 있습니다. 예를 들어 위 작업은 아래의 코드와 동일한 작업입니다.

print(torch.cat([x.unsqueeze(0), y.unsqueeze(0), z.unsqueeze(0)], dim=0))

x, y, z는 기존에는 전부 (2,) 의 크기를 가졌습니다. 그런데 .unsqueeze(0) 을 하므로서 3개의 벡터는 전부 (1, 2) 의 크기의 2차원 텐서로 변경됩니다. 여기에 연결(concatenate)를 의미하는 cat 을 사용하면 (3 x 2) 텐서가 됩니다.

tensor([[1., 4.],
        [2., 5.],
        [3., 6.]])

위에서는 torch.stack([x, y, z]) 라는 한 번의 커맨드로 수행했지만, 연결(concatenate)로 이를 구현하려고 했더니 꽤 복잡해졌습니다.

스택킹에 추가적으로 dim을 인자로 줄 수도 있습니다. 이번에는 dim=1 인자를 주겠습니다. 이는 두번째 차원이 증가하도록 쌓으라는 의미로 해석할 수 있습니다.

print(torch.stack([x, y, z], dim=1))

[output]
tensor([[1., 2., 3.],
        [4., 5., 6.]])

위의 결과는 두번째 차원이 증가하도록 스택킹이 된 결과를 보여줍니다. 결과적으로 (2 × 3) 텐서가 됩니다.


8. chunk

텐서를 여러 개로 나눌 때 사용합니다. (몇 개로 나눌 것인가?)

tensor = torch.rand(3, 6)
print(tensor)

t1, t2, t3 = torch.chunk(tensor, 3, dim=0)
print(t1)
print(t2)
print(t3)

[output]
tensor([[0.7229, 0.1978, 0.0996, 0.0198, 0.9782, 0.4380],
        [0.7233, 0.8010, 0.0060, 0.4960, 0.1566, 0.5789],
        [0.0613, 0.8835, 0.7048, 0.8353, 0.4018, 0.3844]])
tensor([[0.7229, 0.1978, 0.0996, 0.0198, 0.9782, 0.4380]])
tensor([[0.7233, 0.8010, 0.0060, 0.4960, 0.1566, 0.5789]])
tensor([[0.0613, 0.8835, 0.7048, 0.8353, 0.4018, 0.3844]])
tensor = torch.rand(3, 6)
print(tensor)

t1, t2, t3 = torch.chunk(tensor, 3, dim=1)
print(t1)
print(t2)
print(t3)

[output]
tensor([[0.2103, 0.3330, 0.2791, 0.9946, 0.2185, 0.7475],
        [0.7396, 0.6518, 0.1193, 0.9112, 0.9514, 0.7630],
        [0.3139, 0.5222, 0.6987, 0.8860, 0.0796, 0.5894]])
tensor([[0.2103, 0.3330],
        [0.7396, 0.6518],
        [0.3139, 0.5222]])
tensor([[0.2791, 0.9946],
        [0.1193, 0.9112],
        [0.6987, 0.8860]])
tensor([[0.2185, 0.7475],
        [0.9514, 0.7630],
        [0.0796, 0.5894]])


9. split

chunk와 동일한 기능이지만 조금 다름 (텐서의 크기는 몇인가?)

tensor = torch.rand(3, 6)
t1, t2 = torch.split(tensor, 3, dim=1)

print(tensor)
print(t1)
print(t2)

[output]
tensor([[0.7732, 0.0393, 0.7892, 0.9389, 0.0273, 0.1751],
        [0.0814, 0.2443, 0.5015, 0.0702, 0.0171, 0.1885],
        [0.3454, 0.2807, 0.1119, 0.1323, 0.3292, 0.7515]])
tensor([[0.7732, 0.0393, 0.7892],
        [0.0814, 0.2443, 0.5015],
        [0.3454, 0.2807, 0.1119]])
tensor([[0.9389, 0.0273, 0.1751],
        [0.0702, 0.0171, 0.1885],
        [0.1323, 0.3292, 0.7515]])


10. ones_like와 zeros_like - 0으로 채워진 텐서와 1로 채워진 텐서

(2 × 3) 텐서를 만듭니다.

x = torch.FloatTensor([[0, 1, 2], [2, 1, 0]])
print(x)

[output]
tensor([[0., 1., 2.],
        [2., 1., 0.]])

위 텐서에 ones_like를 하면 동일한 크기(shape)지만 1으로만 값이 채워진 텐서를 생성합니다.

print(torch.ones_like(x)) # 입력 텐서와 크기를 동일하게 하면서 값을 1로 채우기

[output]
tensor([[1., 1., 1.],
        [1., 1., 1.]])

위 텐서에 zeros_like를 하면 동일한 크기(shape)지만 0으로만 값이 채워진 텐서를 생성합니다.

print(torch.zeros_like(x)) # 입력 텐서와 크기를 동일하게 하면서 값을 0으로 채우기

[output]
tensor([[0., 0., 0.],
        [0., 0., 0.]])


11. In-place Operation (덮어쓰기 연산)

(2 × 2) 텐서를 만들고 x에 저장합니다.

x = torch.FloatTensor([[1, 2], [3, 4]])

곱하기 연산을 한 값과 기존의 값을 출력해보겠습니다.

print(x.mul(2.)) # 곱하기 2를 수행한 결과를 출력
print(x) # 기존의 값 출력

[output]
tensor([[2., 4.],
        [6., 8.]])
tensor([[1., 2.],
        [3., 4.]])

첫번째 출력은 곱하기 2가 수행된 결과를 보여주고, 두번째 출력은 기존의 값이 그대로 출력된 것을 확인할 수 있습니다. 곱하기 2를 수행했지만 이를 x에다가 다시 저장하지 않았으니, 곱하기 연산을 하더라도 기존의 값 x는 변하지 않는 것이 당연합니다.

그런데 연산 뒤에 _를 붙이면 기존의 값을 덮어쓰기 합니다.

print(x.mul_(2.))  # 곱하기 2를 수행한 결과를 변수 x에 값을 저장하면서 결과를 출력
print(x)  # 변경된 값 출력

[output]
tensor([[2., 4.],
        [6., 8.]])
tensor([[2., 4.],
        [6., 8.]])

이번에는 x의 값이 덮어쓰기 되어 2 곱하기 연산이 된 결과가 출력됩니다.


12. torch ↔ numpy

  • Torch Tensor(텐서)를 NumPy array(배열)로 변환 가능
    • numpy()
    • from_numpy()
  • Tensor가 CPU상에 있다면 NumPy 배열은 메모리 공간을 공유하므로 하나가 변하면, 다른 하나도 변함
a = torch.ones(7)
print(a)

[output]
tensor([1., 1., 1., 1., 1., 1., 1.])
b = a.numpy()
print(b)

[output]
[1. 1. 1. 1. 1. 1. 1.]
a.add_(1)
print(a)
print(b)

[output]
tensor([2., 2., 2., 2., 2., 2., 2.])
[2. 2. 2. 2. 2. 2. 2.]
import numpy as np

a = np.ones(7)
b = torch.from_numpy(a)
np.add(a, 1, out=a)
print(a)
print(b)

[output]
[2. 2. 2. 2. 2. 2. 2.]
tensor([2., 2., 2., 2., 2., 2., 2.], dtype=torch.float64)

Read more

pytorch와 텐서 조작하기(Tensor Manipulation)

|

image

파이토치(PyTorch)

  • 페이스북이 초기 루아(Lua) 언어로 개발된 토치(Torch)를 파이썬 버전으로 개발하여 2017년도에 공개
  • 초기에 토치(Torch)는 넘파이(NumPy) 라이브러리처럼 과학 연산을 위한 라이브러리로 공개
  • 이후 GPU를 이용한 텐서 조작 및 동적 신경망 구축이 가능하도록 딥러닝 프레임워크로 발전시킴
  • 파이썬답게 만들어졌고, 유연하면서도 가속화된 계산 속도를 제공

파이토치 모듈 구조

image

파이토치의 구성요소

  • torch: 메인 네임스페이스, 텐서 등의 다양한 수학 함수가 포함
  • torch.autograd: 자동 미분 기능을 제공하는 라이브러리
  • torch.nn: 신경망 구축을 위한 데이터 구조나 레이어 등의 라이브러리
  • torch.multiprocessing: 병럴처리 기능을 제공하는 라이브러리
  • torch.optim: SGD(Stochastic Gradient Descent)를 중심으로 한 파라미터 최적화 알고리즘 제공
  • torch.utils: 데이터 조작 등 유틸리티 기능 제공
  • torch.onnx: ONNX(Open Neural Network Exchange), 서로 다른 프레임워크 간의 모델을 공유할 때 사용


텐서 조작하기(Tensor Manipulation)

image

  • 데이터 표현을 위한 기본 구조로 텐서(tensor)를 사용
  • 텐서는 데이터를 담기위한 컨테이너(container)로서 일반적으로 수치형 데이터를 저장
  • 넘파이(NumPy)의 ndarray와 유사
  • GPU를 사용한 연산 가속 가능


1. 넘파이로 텐서 만들기(벡터와 행렬 만들기)

딥러닝을 하게 되면 다루게 되는 가장 기본적인 단위는 벡터, 행렬, 텐서입니다. 차원이 없는 값을 스칼라, 1차원으로 구성된 값을 벡터라고 합니다. 2차원으로 구성된 값을 행렬(Matrix)라고 합니다. 그리고 3차원이 되면 텐서(Tensor)라고 부릅니다. PyTorch로 텐서를 만들어보기 전에 우선 Numpy로 텐서를 만들어보겠습니다.

import numpy as np

Numpy로 텐서를 만드는 방법은 간단한데 [숫자, 숫자, 숫자]와 같은 형식으로 만들고 이를 np.array()로 감싸주면 됩니다.

1.1 1D with Numpy

Numpy로 1차원 텐서인 벡터를 만들어보겠습니다.

# 파이썬으로 설명하면 list를 생성해서 np.array로 1차원 array로 변환함
t = np.array([0., 1., 2., 3., 4., 5., 6.])
print(t)

[output]
[0. 1. 2. 3. 4. 5. 6.]

이제 1차원 벡터의 차원과 크기를 출력해보겠습니다.

print('Rank of t: ', t.ndim) #1차원 벡터
print('Shape of t: ', t.shape)

[output]
Rank of t:  1
Shape of t:  (7,)
  • .ndim은 몇 차원인지를 출력합니다.
    • 1차원은 벡터, 2차원은 행렬, 3차원은 3차원 텐서였습니다. 현재는 벡터이므로 1차원이 출력됩니다.
  • .shape는 크기를 출력합니다.
    • (7, )(1, 7)을 의미합니다. 다시 말해 (1 × 7)의 크기를 가지는 벡터입니다.


1.2 2D with Numpy

Numpy로 2차원 행렬을 만들어보겠습니다.

t = np.array([[1., 2., 3.], [4., 5., 6.], [7., 8., 9.], [10., 11., 12.]])
print(t)

[output]
[[ 1.  2.  3.]
 [ 4.  5.  6.]
 [ 7.  8.  9.]
 [10. 11. 12.]]
print('Rank  of t: ', t.ndim)
print('Shape of t: ', t.shape)

[output]
Rank  of t:  2
Shape of t:  (4, 3)

현재는 행렬이므로 2차원이 출력되며, shape은 (4, 3) 입니다. 다른 표현으로는 (4 × 3) 입니다. 이는 행렬이 4행 3열임을 의미합니다.


2. 파이토치 텐서(PyTorch Tensor)

파이토치는 Numpy와 매우 유사합니다. 우선 torch를 임포트합니다.

import torch

2.1 텐서 초기화

우선 텐서를 초기화 하는 여러가지 방법을 살펴보겠습니다.

초기화 되지 않은 텐서

x = torch.empty(4, 2)
print(x)

[output]
tensor([[7.2747e-35, 0.0000e+00],
        [3.3631e-44, 0.0000e+00],
        [       nan, 0.0000e+00],
        [1.1578e+27, 1.1362e+30]])

무작위로 초기화된 텐서

x = torch.rand(4, 2)
print(x)

[output]
tensor([[0.7464, 0.7540],
        [0.5432, 0.0055],
        [0.4031, 0.0854],
        [0.6742, 0.8194]])

데이터 타입(dtype)이 long이고, 0으로 채워진 텐서

x = torch.zeros(4, 2, dtype=torch.long)
print(x)

[output]
tensor([[0, 0],
        [0, 0],
        [0, 0],
        [0, 0]])

사용자가 입력한 값으로 텐서 초기화

x = torch.tensor([3, 2.3])
print(x)

[output]
tensor([3.0000, 2.3000])

2 x 4 크기, double 타입, 1로 채워진 텐서

x = x.new_ones(2, 4, dtype=torch.double)
print(x)

[output]
tensor([[1., 1., 1., 1.],
        [1., 1., 1., 1.]], dtype=torch.float64)

x와 같은 크기, float 타입, 무작위로 채워진 텐서

x = torch.randn_like(x, dtype=torch.float)
print(x)

[output]
tensor([[ 0.4575, -0.9619,  1.2463, -0.5515],
        [-1.5581, -0.6273,  0.0430,  0.5415]])

텐서의 크기 계산

print(x.size())

[output]
torch.Size([2, 4])


2.2 데이터 타입(Data Type)

다음은 텐서의 데이터 타입 방법입니다.

Data type dtype CPU tensor GPU tensor
32-bit floating point torch.float32 or torch.float torch.FloatTensor torch.cuda.FloatTensor
64-bit floating point torch.float64 or torch.double torch.DoubleTensor torch.cuda.DoubleTensor
16-bit floating point torch.float16 or torch.half torch.HalfTensor torch.cuda.HalfTensor
8-bit integer(unsinged) torch.uint8 torch.ByteTensor torch.cuda.ByteTensor
8-bit integer(singed) torch.int8 torch.CharTensor torch.cuda.CharTensor
16-bit integer(signed) torch.int16 or torch.short torch.ShortTensor torch.cuda.ShortTensor
32-bit integer(signed) torch.int32 or torch.int torch.IntTensor torch.cuda.IntTensor
64-bit integer(signed) torch.int64 or torch.long torch.LongTensor torch.cuda.LongTensor
ft = torch.FloatTensor([1, 2, 3])
print(ft)
print(ft.dtype)

[output]
tensor([1., 2., 3.])
torch.float32
print(ft.short())
print(ft.int())
print(ft.long())

[output]
tensor([1, 2, 3], dtype=torch.int16)
tensor([1, 2, 3], dtype=torch.int32)
tensor([1, 2, 3])
it = torch.IntTensor([1, 2, 3])
print(it)
print(it.dtype)

[output]
tensor([1, 2, 3], dtype=torch.int32)
torch.int32
print(it.float())
print(it.double())
print(it.half())

[output]
tensor([1., 2., 3.])
tensor([1., 2., 3.], dtype=torch.float64)
tensor([1., 2., 3.], dtype=torch.float16)


2.3 CUDA Tensors

  • .to 메소드를 사용하여 텐서를 어떠한 장치(cpu, gpu)로도 옮길 수 있음
x = torch.randn(1)
print(x)
print(x.item())
print(x.dtype)
x = x.to(device)
print(x)

[output]
tensor([-0.9480])
-0.9479643106460571
torch.float32
tensor([-0.9480], device='cuda:0')

다음은 cuda와 cpu간 변화를 볼 수 있습니다.

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
#torch.device('cuda')
#torch.device('cpu')
print(device)

[output]
cuda
y = torch.ones_like(x, device=device)
print(y)

[output]
tensor([1.], device='cuda:0')
z = x + y
print(z)

[output]
tensor([0.0520], device='cuda:0')
print(z.to('cpu', torch.double))

[output]
tensor([0.0520], dtype=torch.float64)


2.4 다차원 텐서 표현

0D Tensor: Scalar(스칼라)

  • 하나의 숫자를 담고 있는 텐서(tensor)
  • 축과 형상이 없음
# 스칼라 값 정의
scalar1 = torch.tensor(1)
print(scalar1)

scalar2 = torch.tensor(3)
print(scalar2)

[output]
tensor(1)
tensor(3)

dim()을 사용하면 현재 텐서의 차원을 보여줍니다. shapesize()를 사용하면 크기를 확인할 수 있습니다.

print(scalar1.dim())  # rank. 즉, 차원
print(scalar1.shape)  # shape
print(scalar1.size()) # shape

[output]
0
torch.Size([])
torch.Size([])

스칼라 라는 의미로 숫자 한개만을 선언을 했지만 내부적으로 크기가 1인 벡터로 인식합니다. 즉 현재 1차원 텐서이며, 원소는 1개 입니다. 다음은 스칼라 값 간의 사칙연산을 해보겠습니다.

# 스칼라 값 간의 사칙연산: +, -, *, /
add_scalar = scalar1 + scalar2
print(add_scalar)

sub_scalar = scalar1 - scalar2
print(sub_scalar)

mul_scalar = scalar1 * scalar2
print(mul_scalar)

div_scalar = scalar1 / scalar2
print(div_scalar)

[output]
tensor(4)
tensor(-2)
tensor(3)
tensor(0.3333)
# 스칼라 값 간의 사칙연산: torch 모듈에 내장된 메서드 이용해 계산
print(torch.add(scalar1, scalar2))
print(torch.sub(scalar1, scalar2))
print(torch.mul(scalar1, scalar2))
print(torch.div(scalar1, scalar2))

[output]
tensor(4)
tensor(-2)
tensor(3)
tensor(0.3333)

1D Tensor: Vector(벡터)

  • 벡터는 하나의 값을 표현할 때 2개 이상의 수치로 표현한 것.
  • 스칼라의 형태와 동일한 속성을 갖고 있지만, 여러 수치 값을 이용해 표현하는 방식
  • 값들을 저장한 리스트와 유사한 텐서
  • 하나의 축이 존재
# 벡터 값 정의
vector1 = torch.tensor([1., 2., 3.])
print(vector1)

vector2 = torch.tensor([4., 5., 6.])
print(vector2)

[output]
tensor([1.])
tensor([3.])

마찬가지로 현재 텐서의 차원과 크기를 확인할 수 있습니다.

print(vector1.dim())  # rank. 즉, 차원
print(vector1.shape)  # shape
print(vector1.size()) # shape

[output]
1
torch.Size([3])
torch.Size([3])

현재 1차원 텐서이며, 원소는 3개입니다. 다음은 벡터 값 간의 사칙연산을 해보겠습니다.

# 벡터 값 간의 사칙연산: +, -, *, /
# 여기서 곱셈과 나눗셈은 각 요소별로(element-wise) 연산된다.
add_vector = vector1 + vector2
print(add_vector)

sub_vector = vector1 - vector2
print(sub_vector)

mul_vector = vector1 * vector2
print(mul_vector)

div_vector = vector1 / vector2
print(div_vector)

[output]
tensor([5., 7., 9.])
tensor([-3., -3., -3.])
tensor([ 4., 10., 18.])
tensor([0.2500, 0.4000, 0.5000])
# 벡터 값 간의 사칙연산: torch 모듈에 내장된 메서드 이용해 계산
print(torch.add(vector1, vector2))
print(torch.sub(vector1, vector2))
print(torch.mul(vector1, vector2))
print(torch.div(vector1, vector2))
print(torch.dot(vector1, vector2))# 벡터의 내적

[output]
tensor([5., 7., 9.])
tensor([-3., -3., -3.])
tensor([ 4., 10., 18.])
tensor([0.2500, 0.4000, 0.5000])
tensor(32.)

2D Tensor: Matrix(행렬)

  • 행렬은(Matrix)은 2개 이상의 벡터 값을 통합해 구성된 값
  • 벡터 값 간의 연산 속도를 빠르게 진행할 수 있는 선형 대수의 기본 단위
  • 일반적인 수치, 통계 데이터셋이 해당
  • 주로 샘플(samples)과 특성(features)을 가진 구조로 사용

파이토치로 2차원 텐서인 행렬을 만들어보겠습니다.

# 행렬 값 정의
matrix1 = torch.tensor([[1., 2.],
                        [3., 4.]])
print(matrix1)

matrix2 = torch.tensor([[5., 6.],
                        [7., 8.]])
print(matrix2)

[output]
tensor([[1., 2.],
        [3., 4.]])
tensor([[5., 6.],
        [7., 8.]])

마찬가지로 현재 텐서의 차원과 크기를 확인할 수 있습니다.

print(matrix1.dim())  # rank. 즉, 차원
print(matrix1.shape)  # shape
print(matrix1.size()) # shape

[output]
2
torch.Size([2, 2])
torch.Size([2, 2])

현재 2차원 텐서입니다. 다음은 행렬 값 간의 사칙연산을 해보겠습니다.

# 행렬 값 간의 사칙연산: +, -, *, /
sum_matrix = matrix1 + matrix2
print(sum_matrix)

sub_matrix = matrix1 - matrix2
print(sub_matrix)

mul_matrix = matrix1 * matrix2
print(mul_matrix)

div_matrix = matrix1 / matrix2
print(div_matrix)

[output]
tensor([[ 6.,  8.],
        [10., 12.]])
tensor([[-4., -4.],
        [-4., -4.]])
tensor([[ 5., 12.],
        [21., 32.]])
tensor([[0.2000, 0.3333],
        [0.4286, 0.5000]])
# 행렬 값 간의 사칙연산: torch 모듈에 내장된 메서드 이용해 계산
print(torch.add(matrix1, matrix2))
print(torch.sub(matrix1, matrix2))
print(torch.mul(matrix1, matrix2))
print(torch.div(matrix1, matrix2))
print(torch.matmul(matrix1, matrix2)) # 행렬 곱 연산

[output]
tensor([[ 6.,  8.],
        [10., 12.]])
tensor([[-4., -4.],
        [-4., -4.]])
tensor([[ 5., 12.],
        [21., 32.]])
tensor([[0.2000, 0.3333],
        [0.4286, 0.5000]])
tensor([[19., 22.],
        [43., 50.]])

3D Tensor(텐서)

  • 행렬을 2차원 배열이라 표현할 수 있다면, 텐서는 2차원 이상의 배열이라 표현할 수 있다.
  • 텐서 내 행렬 단위의 인덱스 간, 행렬 내 인덱스 간 원소끼리 계산되며 행렬 곱은 텐서 내 같은 행렬 단위의 인덱스 간에 계산된다.
  • 큐브(cube)와 같은 모양으로 세개의 축이 존재
  • 데이터가 연속된 시퀀스 데이터나 시간 축이 포함된 시계열 데이터에 해당
  • 주식 가격 데이터셋, 시간에 따른 질병 발병 데이터 등이 존재
  • 주로 샘플(samples), 타임스텝(timesteps), 특성(features)을 가진 구조로 사용
# 텐서 값 정의
tensor1 = torch.tensor([ [ [1., 2.],
                           [3., 4.] ],
                        
                         [ [5., 6.],
                           [7., 8.] ] ])
print(tensor1)

tensor2 = torch.tensor([ [ [9., 10.],
                           [11., 12.] ],
                        
                         [ [13., 14.],
                           [15., 16.] ] ])
print(tensor2)

[output]
tensor([[[1., 2.],
         [3., 4.]],

        [[5., 6.],
         [7., 8.]]])
tensor([[[ 9., 10.],
         [11., 12.]],

        [[13., 14.],
         [15., 16.]]])

마찬가지로 현재 텐서의 차원과 크기를 확인할 수 있습니다.

print(tensor1.dim())  # rank. 즉, 차원
print(tensor1.shape)  # shape
print(tensor1.size()) # shape

[output]
3
torch.Size([2, 2, 2])
torch.Size([2, 2, 2])

현재 3차원 텐서입니다. 다음은 텐서 값 간의 사칙연산을 해보겠습니다.

# 텐서 값 간의 사칙연산: +, -, *, /
sum_tensor = tensor1 + tensor2
print(sum_tensor)

sub_tensor = tensor1 - tensor2
print(sub_tensor)

mul_tensor = tensor1 * tensor2
print(mul_tensor)

div_tensor = tensor1 / tensor2
print(div_tensor)

[output]
tensor([[[10., 12.],
         [14., 16.]],

        [[18., 20.],
         [22., 24.]]])
tensor([[[-8., -8.],
         [-8., -8.]],

        [[-8., -8.],
         [-8., -8.]]])
tensor([[[  9.,  20.],
         [ 33.,  48.]],

        [[ 65.,  84.],
         [105., 128.]]])
tensor([[[0.1111, 0.2000],
         [0.2727, 0.3333]],

        [[0.3846, 0.4286],
         [0.4667, 0.5000]]])
# 텐서 값 간의 사칙연산: torch 모듈에 내장된 메서드 이용해 계산
print(torch.add(tensor1, tensor2))
print(torch.sub(tensor1, tensor2))
print(torch.mul(tensor1, tensor2))
print(torch.div(tensor1, tensor2))
print(torch.matmul(tensor1, tensor2))# 텐서 간 텐서곱

[output]
tensor([[[10., 12.],
         [14., 16.]],

        [[18., 20.],
         [22., 24.]]])
tensor([[[-8., -8.],
         [-8., -8.]],

        [[-8., -8.],
         [-8., -8.]]])
tensor([[[  9.,  20.],
         [ 33.,  48.]],

        [[ 65.,  84.],
         [105., 128.]]])
tensor([[[0.1111, 0.2000],
         [0.2727, 0.3333]],

        [[0.3846, 0.4286],
         [0.4667, 0.5000]]])
tensor([[[ 31.,  34.],
         [ 71.,  78.]],

        [[155., 166.],
         [211., 226.]]])

4D Tensor

  • 4개의 축
  • 컬러 이미지 데이터가 대표적인 사례 (흑백 이미지 데이터는 3D Tensor로 가능)
  • 주로 샘플(samples), 높이(height), 너비(width), 컬러 채널(channel)을 가진 구조로 사용

5D Tensor

  • 5개의 축
  • 비디오 데이터가 대표적인 사례
  • 주로 샘플(samples), 프레임(frames), 높이(height), 너비(width), 컬러 채널(channel)을 가진 구조로 사용

Read more