앞서 배운 x가 1개인 선형 회귀를 단순 선형 회귀(Simple Linear Regression)이라고 합니다.
이번 챕터에서는 다수의 x로부터 y를 예측하는 다중 선형 회귀(Multivariable Linear Regression)에 대해서 이해합니다.
1. 데이터에 대한 이해(Data Definition)
2. 파이토치로 구현하기
우선 필요한 도구들을 임포트하고 랜덤 시드를 고정합니다.
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
torch.manual_seed(1)
이제 훈련 데이터를 선언해보겠습니다.
# 훈련 데이터
x1_train = torch.FloatTensor([[73], [93], [89], [96], [73]])
x2_train = torch.FloatTensor([[80], [88], [91], [98], [66]])
x3_train = torch.FloatTensor([[75], [93], [90], [100], [70]])
y_train = torch.FloatTensor([[152], [185], [180], [196], [142]])
이제 가중치 w와 편향 b를 선언합니다. 가중치 w도 3개 선언해주어야 합니다.
# 가중치 w와 편향 b 초기화
w1 = torch.zeros(1, requires_grad=True)
w2 = torch.zeros(1, requires_grad=True)
w3 = torch.zeros(1, requires_grad=True)
b = torch.zeros(1, requires_grad=True)
이제 가설, 비용 함수, 옵티마이저를 선언한 후에 경사 하강법을 1,000회 반복합니다.
# optimizer 설정
optimizer = optim.SGD([w1, w2, w3, b], lr=1e-5)
nb_epochs = 1000
for epoch in range(nb_epochs + 1):
# H(x) 계산
hypothesis = x1_train * w1 + x2_train * w2 + x3_train * w3 + b
# cost 계산
cost = torch.mean((hypothesis - y_train) ** 2)
# cost로 H(x) 개선
optimizer.zero_grad()
cost.backward()
optimizer.step()
# 100번마다 로그 출력
if epoch % 100 == 0:
print('Epoch {:4d}/{} w1: {:.3f} w2: {:.3f} w3: {:.3f} b: {:.3f} Cost: {:.6f}'.format(
epoch, nb_epochs, w1.item(), w2.item(), w3.item(), b.item(), cost.item()
))
위의 경우 가설을 선언하는 부분인 hypothesis = x1_train * w1 + x2_train * w2 + x3_train * w3 + b에서도 x_train의 개수만큼 w와 곱해주도록 작성해준 것을 확인할 수 있습니다.
3. 벡터와 행렬 연산으로 바꾸기
위의 코드를 개선할 수 있는 부분이 있습니다. 이번에는 w의 개수가 3개였으니까 x1_train, x2_train, x3_train와 w1, w2, w3를 일일히 선언해주었습니다. 그런데 w의 개수가 1,000개라고 가정하면 x_train1 ~ x_train1000을 전부 선언하고, w1 ~ w1000을 전부 선언해야 합니다. 다시 말해 w와 b 변수 선언만 총 합 2,000개를 해야합니다. 또한 가설을 선언하는 부분에서도 마찬가지로 x_train과 w의 곱셈이 이루어지는 항을 1,000개를 작성해야 합니다. 이는 굉장히 비효율적입니다.
이를 해결하기 위해 행렬 곱셈 연산(또는 벡터의 내적)을 사용합니다.
- 행렬의 곱셈 과정에서 이루어지는 벡터 연산을 벡터의 내적(Dot Product)이라고 합니다.
1. 벡터 연산으로 이해하기
위 식은 아래와 같이 두 벡터의 내적으로 표현할 수 있습니다.
두 벡터를 각각 X와 W로 표현한다면, 가설은 다음과 같습니다.
2. 행렬 연산으로 이해하기
훈련 데이터를 살펴보고, 벡터와 행렬 연산을 통해 가설 H(X)를 표현해보겠습니다.
전체 훈련 데이터의 개수를 셀 수 있는 1개의 단위를 샘플(sample)이라고 합니다. 현재 샘플의 수는 총 5개입니다.
각 샘플에서 y를 결정하게 하는 각각의 독립 변수 x를 특성(feature)이라고 합니다. 현재 특성은 3개입니다.
이는 독립 변수 x들의 수가 (샘플의 수 × 특성의 수) = 15개임을 의미합니다. 독립 변수 x들을 (샘플의 수 × 특성의 수)의 크기를 가지는 하나의 행렬로 표현해봅시다. 그리고 이 행렬을 X라고 하겠습니다.
결과적으로 전체 훈련 데이터의 가설 연산을 3개의 변수만으로 표현하였습니다.
이와 같이 벡터와 행렬 연산은 식을 간단하게 해줄 뿐만 아니라 다수의 샘플의 병렬 연산이므로 속도가 빠릅니다.
4. 행렬 연산을 고려하여 파이토치로 구현하기
이번에는 행렬 연산을 고려하여 파이토치로 재구현해보겠습니다.
이번에는 훈련 데이터 또한 행렬로 선언해야 합니다.
x_train = torch.FloatTensor([[73, 80, 75],
[93, 88, 93],
[89, 91, 80],
[96, 98, 100],
[73, 66, 70]])
y_train = torch.FloatTensor([[152], [185], [180], [196], [142]])
이번에는 x_train 하나에 모든 샘플을 전부 선언하였습니다. 다시 말해 (5 x 3) 행렬 X을 선언한 것입니다.
x_train과 y_train의 크기(shape)를 출력해보겠습니다.
print(x_train.shape)
print(y_train.shape)
torch.Size([5, 3])
torch.Size([5, 1])
각각 (5 × 3) 행렬과 (5 × 1) 행렬(또는 벡터)의 크기를 가집니다.
이제 가중치 W와 편향 b를 선언합니다.
# 가중치와 편향 선언
W = torch.zeros((3, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
여기서 주목할 점은 가중치 W의 크기가 (3 × 1) 벡터라는 점입니다. 행렬의 곱셈이 성립되려면 곱셈의 좌측에 있는 행렬의 열의 크기와 우측에 있는 행렬의 행의 크기가 일치해야 합니다. 현재 X_train의 행렬의 크기는 (5 × 3)이며, W 벡터의 크기는 (3 × 1)이므로 두 행렬과 벡터는 행렬곱이 가능합니다. 행렬곱으로 가설을 선언하면 아래와 같습니다.
hypothesis = x_train.matmul(W) + b
가설을 행렬곱으로 간단히 정의하였습니다. 이는 앞서 x_train과 w의 곱셈이 이루어지는 각 항을 전부 기재하여 가설을 선언했던 것과 대비됩니다. 이 경우, 사용자가 독립 변수 x의 수를 후에 추가적으로 늘리거나 줄이더라도 위의 가설 선언 코드를 수정할 필요가 없습니다.
Q : b는 스칼라값인데 어떻게 매트릭스랑 같이 계산이되나요?
A : 파이토치(PyTorch)에서는 이런 경우에도 편향 `b`를 자동으로 각 샘플에 더해주는 브로드캐스팅(broadcasting) 기능을 제공합니다. 브로드캐스팅은 다른 형태의 텐서 간 연산을 가능하게 해줍니다. 구체적으로, 브로드캐스팅은 작은 텐서를 큰 텐서의 크기에 맞춰 자동으로 확장시켜 연산을 수행할 수 있습니다.
이 경우, `x_train.matmul(W)`는 5x3과 3x1 행렬의 곱셈을 수행하여 5x1 크기의 텐서를 생성합니다. 그 다음, 스칼라 값 `b`를 이 5x1 크기의 텐서에 더할 때, `b`는 자동으로 5x1 크기로 확장(broadcast)되어 각각의 값에 `b`가 더해지게 됩니다. 즉, 각 샘플의 결과에 동일한 편향 `b`가 추가됩니다.
브로드캐스팅의 과정을 간단히 요약하겠습니다.
1. 연산을 수행하려는 두 텐서의 형태를 비교합니다.
2. 한 차원이라도 크기가 서로 다르면, 크기가 1인 차원을 다른 텐서의 해당 차원 크기로 확장하여 맞춥니다.
3. 이후 연산을 수행합니다.
이제 해야할 일은 비용 함수와 옵티마이저를 정의하고, 정해진 에포크만큼 훈련을 진행하는 일입니다. 이를 반영한 전체 코드는 다음과 같습니다.
Copyx_train = torch.FloatTensor([[73, 80, 75],
[93, 88, 93],
[89, 91, 80],
[96, 98, 100],
[73, 66, 70]])
y_train = torch.FloatTensor([[152], [185], [180], [196], [142]])
# 모델 초기화
W = torch.zeros((3, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
# optimizer 설정
optimizer = optim.SGD([W, b], lr=1e-5)
nb_epochs = 20
for epoch in range(nb_epochs + 1):
# H(x) 계산
# 편향 b는 브로드 캐스팅되어 각 샘플에 더해집니다.
hypothesis = x_train.matmul(W) + b
# cost 계산
cost = torch.mean((hypothesis - y_train) ** 2)
# cost로 H(x) 개선
optimizer.zero_grad()
cost.backward()
optimizer.step()
print('Epoch {:4d}/{} hypothesis: {} Cost: {:.6f}'.format(
epoch, nb_epochs, hypothesis.squeeze().detach(), cost.item()
))
Q: backward()를 하면 loss함수의 기울기를 찾아서 그 기울기를 바탕으로 W값과 b값을 업데이트하는건데 W가 매트릭스인데 이건 어떻게 구해지는건가요?
A: `backward()` 함수를 호출하면, PyTorch는 자동 미분(automatic differentiation)을 사용하여 주어진 손실 함수(loss function)에 대한 각 매개변수(parameter)의 기울기(gradient)를 계산합니다. 여기서 매개변수는 `W`와 `b`를 말하며, 이들은 모두 `requires_grad=True`로 설정되어 있어서 PyTorch가 그들의 기울기를 자동으로 계산하고 저장하게 됩니다.
다음은 손실 함수에 대한 W와 b의 기울기를 구하는 과정입니다.
1. Forward Pass: 먼저, 모델은 입력 `x_train`을 받아서 예측값 `hypothesis`를 계산합니다. 이 과정에서 `W`와 `b`를 사용합니다.
2. Compute Loss: 손실 함수 (L)는 실제값 `y_train`과 예측값 `hypothesis` 간의 차이를 계산하여 손실값을 도출합니다. 여기서는 평균 제곱 오차(Mean Squared Error, MSE)를 사용합니다.
3. Backward Pass: `cost.backward()`를 호출하면, PyTorch는 손실 함수의 기울기를 계산하는 역전파(backpropagation)를 수행합니다. 역전파는 손실 값에서부터 시작하여, 연쇄 법칙(chain rule)을 사용하여 각 매개변수에 대한 손실 함수의 편미분 값을 계산합니다.
- 이때 `W`가 매트릭스라도 PyTorch는 각 요소에 대한 기울기를 계산합니다. 예를 들어, `W`가 3x1 행렬이라면, PyTorch는 `W`의 각 원소에 대한 손실 함수의 편미분 값을 계산하여 3x1 기울기 행렬에 저장합니다. W가 매트릭스 형태라도 각 원소에 대한 기울기를 구하여 해당 원소를 업데이트할 수 있습니다.
4. Update Parameters: 마지막으로, `optimizer.step()`을 호출하면, 계산된 기울기를 사용하여 `W`와 `b`를 업데이트합니다. 이 과정은 설정한 최적화 알고리즘(SGD, Adam 등)에 따라 달라집니다. SGD의 경우, 각 매개변수는 다음과 같이 업데이트됩니다:
여기서 (theta)는 매개변수(`W` 또는 `b`), (alpha)는 학습률(learning rate), (nabla_theta L)은 매개변수에 대한 손실 함수의 기울기입니다.
이를 통해 `W`와 `b`는 데이터와 손실 함수에 따라 점진적으로 최적화되어, 모델의 예측 성능이 개선됩니다.
'AI > ML' 카테고리의 다른 글
[ML] 미니 배치와 데이터 로드(Mini Batch and Data Load) (0) | 2024.03.28 |
---|---|
[ML] nn.Module로 구현하는 선형 회귀 (0) | 2024.03.26 |
[pytorch] 파이토치 입문 (0) | 2024.03.23 |
[ML] 자동 미분(Autograd) (0) | 2024.03.22 |
[ML] 선형회귀(Linear Regression) (0) | 2024.03.21 |