1. 벡터와 행렬 연산으로 바꾸기
위의 코드를 개선할 수 있는 부분이 있습니다. 이번에는 x의 개수가 3개였으니까 x1_train, x2_train, x3_train와 w1, w2, w3를 일일히 선언해주었습니다. 그런데 x의 개수가 1,000개라고 가정해봅시다. 위와 같은 방식을 고수할 경우 x_train1 ~ x_train1000을 전부 선언하고, w1 ~ w1000을 전부 선언해야 합니다. 다시 말해 x와 w 변수 선언만 총 합 2,000개를 해야합니다. 또한 가설을 선언하는 부분에서도 마찬가지로 x_train과 w의 곱셈이 이루어지는 항을 1,000개를 작성해야 합니다. 이는 굉장히 비효율적입니다.
이를 해결하기 위해 행렬 곱셈 연산(또는 벡터의 내적)을 사용합니다.
- 행렬의 곱셈 과정에서 이루어지는 벡터 연산을 벡터의 내적(Dot Product)이라고 합니다.
위의 그림은 행렬 곱셈 연산 과정에서 벡터의 내적으로 1 × 7 + 2 × 9 + 3 × 11 = 58이 되는 과정을 보여줍니다.
이 행렬 연산이 어떻게 현재 배우고 있는 가설과 상관이 있다는 걸까요?
바로 가설을 벡터와 행렬 연산으로 표현할 수 있기 때문입니다.
1) 벡터 연산으로 이해하기
H(X)=w1x1+w2x2+w3x3
위 식은 아래와 같이 두 벡터의 내적으로 표현할 수 있습니다.
두 벡터를 각각 X와 W로 표현한다면, 가설은 다음과 같습니다.
H(X)=XW
x의 개수가 3개였음에도 이제는 X와 W라는 두 개의 변수로 표현된 것을 볼 수 있습니다.
2) 행렬 연산으로 이해하기
훈련 데이터를 살펴보고, 벡터와 행렬 연산을 통해 가설 H(X)를 표현해보겠습니다.
Quiz 1 (x1)Quiz 2 (x2)Quiz 3 (x3)Final (y)
73 | 80 | 75 | 152 |
93 | 88 | 93 | 185 |
89 | 91 | 80 | 180 |
96 | 98 | 100 | 196 |
73 | 66 | 70 | 142 |
전체 훈련 데이터의 개수를 셀 수 있는 1개의 단위를 샘플(sample)이라고 합니다. 현재 샘플의 수는 총 5개입니다.
각 샘플에서 y를 결정하게 하는 각각의 독립 변수 x를 특성(feature)이라고 합니다. 현재 특성은 3개입니다.
이는 독립 변수 x들의 수가 (샘플의 수 × 특성의 수) = 15개임을 의미합니다. 독립 변수 x들을 (샘플의 수 × 특성의 수)의 크기를 가지는 하나의 행렬로 표현해봅시다. 그리고 이 행렬을 X라고 하겠습니다.
(x11 x12 x13 x21 x22 x23 x31 x32 x33 x41 x42 x43 x51 x52 x53 )
그리고 여기에 가중치 w1,w2,w3을 원소로 하는 벡터를 W라 하고 이를 곱해보겠습니다.
(x11 x12 x13 x21 x22 x23 x31 x32 x33 x41 x42 x43 x51 x52 x53 )(w1w2w3) =(x11w1+x12w2+x13w3 x21w1+x22w2+x23w3 x31w1+x32w2+x33w3 x41w1+x42w2+x43w3 x51w1+x52w2+x53w3 )
위의 식은 결과적으로 다음과 같습니다.
H(X)=XW
이 가설에 각 샘플에 더해지는 편향 b를 추가해봅시다. 샘플 수만큼의 차원을 가지는 편향 벡터 B를 만들어 더합니다.
(x11 x12 x13 x21 x22 x23 x31 x32 x33 x41 x42 x43 x51 x52 x53 )(w1w2w3)+(bbbbb) =(x11w1+x12w2+x13w3+b x21w1+x22w2+x23w3+b x31w1+x32w2+x33w3+b x41w1+x42w2+x43w3+b x51w1+x52w2+x53w3+b )
위의 식은 결과적으로 다음과 같습니다.
H(X)=XW+B
결과적으로 전체 훈련 데이터의 가설 연산을 3개의 변수만으로 표현하였습니다.
이와 같이 벡터와 행렬 연산은 식을 간단하게 해줄 뿐만 아니라 다수의 샘플의 병렬 연산이므로 속도의 이점을 가집니다.
이를 참고로 파이토치로 구현해봅시다.
2. 행렬 연산을 고려하여 파이토치로 구현하기
이번에는 행렬 연산을 고려하여 파이토치로 재구현해보겠습니다.
이번에는 훈련 데이터 또한 행렬로 선언해야 합니다.
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을 3개나 구현했던 것과 다르게 이번에는 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의 수를 후에 추가적으로 늘리거나 줄이더라도 위의 가설 선언 코드를 수정할 필요가 없습니다. 이제 해야할 일은 비용 함수와 옵티마이저를 정의하고, 정해진 에포크만큼 훈련을 진행하는 일입니다. 이를 반영한 전체 코드는 다음과 같습니다.
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]])
# 모델 초기화
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()
))
학습이 끝난 모델에 임의의 입력 값을 넣어 예측을 해봅시다
# 임의의 입력 값에 대한 예측
with torch.no_grad():
new_input = torch.FloatTensor([[75, 85, 72]]) # 예측하고 싶은 임의의 입력
prediction = new_input.matmul(W) + b
print('Predicted value for input {}: {}'.format(new_input.squeeze().tolist(), prediction.item()))
Predicted value for input [75.0, 85.0, 72.0]: 156.8051300048828
with torch.no_grad(): 이 블록 안에서 수행되는 모든 연산에 대해 역전파(즉, 기울기 계산)를 비활성화합니다. 예측을 할 때는 가중치를 업데이트할 필요가 없기 때문에, 메모리와 계산 자원을 절약하기 위해 torch.no_grad()를 사용하는 것이 좋습니다.
'머신러닝 & 딥러닝' 카테고리의 다른 글
역전파(Backpropagation) 개념 (0) | 2025.02.22 |
---|---|
주니어 응애 AI 직무 엔지니어링 면접 대비 (1) | 2025.02.21 |
다중선형회귀 (0) | 2025.02.20 |
선형회귀와 자동미분 (0) | 2025.02.20 |
머신러닝 워크 플로우 기초 (2) | 2024.04.07 |