딥러닝(DL)/딥러닝 기초

[DL] 선형 회귀 ( Linear Regression )

Song 컴퓨터공학 2023. 9. 2. 16:42

 

 

오늘의 포스팅에서는 파이썬(Python) 만으로 그리고 파이토치(PyTorch)를 이용하여 간단한 선형 회귀 모델을 만들어 보겠습니다.

 

 

 

 Python 선형 회귀 구현 (Bias X)

 

하루 노동 시간 하루 매출
1 25,000
2 55,000
3 75,000
4 110,000
5 128,000
6 155,000
7 180,000

 

위 표와 같이 하루 노동 시간에 따른 하루 매출 데이터가 있다고 가정하겠습니다. 하루 노동 시간이 8시간일 때 하루 매출은 얼마가 될까요?

 

이 질문에 대답하기 위해서는 1시간, 2시간, ... , 7시간을 일했을 때 얼마를 벌었다는 앞서 나온 정보를 이용해야 합니다. 이 때 위 표처럼 예측을 위해 사용하는 데이터셋을 훈련 데이터셋(training dataset) 이라고 합니다. 학습이 끝난 후, 이 모델이 얼마나 잘 작동하는지 판별하는 데이터셋을 테스트 데이터셋(test dataset) 이라 합니다.

 

우선 훈련 데이터셋을 matplotlib을 통해 시각화해봅시다.

import matplotlib.pyplot as plt

X = [1, 2, 3, 4, 5, 6, 7]
Y = [25000, 55000, 75000, 110000, 128000, 155000, 180000]

plt.plot(X, Y)
plt.scatter(X, Y)

 

훈련 데이터셋을 준비하고 시각화까지 마쳤다면(전처리는 하지 않는다고 가정), 가설(Hypothesis) 클래스를 정의해야 합니다. 머신 러닝에서 어떤 문제를 수학적으로 변환할 때, 즉 수식적으로 풀어야 할 문제를 모델링 할 때의 수식을 가설(Hypothesis) 라고 합니다. 가설은 임의로 추측하여 세운 식일 수도, 경험적으로 알고 있는 식일수도 있습니다. 중요한 기능은 맞는 가설이 아니라고 판단되면 계속 수정하는 방식으로 동작해야 합니다.

 

선형 회귀의 가설은 이미 잘 알려져 있습니다. 선형 회귀란 학습 데이터와 가장 잘 맞는 하나의 직선을 찾는 작업 입니다. 따라서 선형 회귀의 가설은 직선의 방정식 형태로, 아래와 같습니다.

 

$$ y = Wx + b $$

 

가설의 $H$를 따서 $y$ 대신 $H(x) = Wx+b$ 로 표기하기도 합니다. $W$는 weight, $b$는 bias를 의미합니다.

 

 

머신 러닝은 우리가 세운 가설과 정답과의 차이를 손실 함수로 계산하고, 오차를 수정해나가는 작업을 반복함으로써 가설을 정답식에 근사시키는 일입니다. 따라서 가설 클래스에는 손실 함수의 구현도 포함되어있어야 하고, 손실 함수에 대한 개념적인 내용은 이전 포스팅을 링크하도록 하겠습니다.

 

[DL] 손실 함수 ( Loss function )

신경망 학습이란 train 데이터로부터 가중치 매개변수의 최적값을 자동으로 얻는 것을 뜻합니다. 이번 포스팅에서는 신경망이 학습할 수 있도록 해주는 지표인 손실 함수에 대해 알아보도록 하

songsite123.tistory.com

 

손실 함수로 MSE를 사용한다고 하면 선형 회귀에 대한 손실 함수의 수식은 아래와 같이 나타납니다. $cost(W,b)$로 나타냅니다.

 

$$ cost(W,b) = \dfrac{1}{n} \sum_{i=1}^n \left[ y^{(i)}-H(x^{(i)}) \right]^2 $$

 

손실 함수가 의미하는 것은 우리가 예측하여 구한 값, 가설의 출력과 실제 정답과의 차이에 비례합니다. 따라서 $cost(W,b)$를 최소가 되게 만드는 $W$와 $b$를 구하면 훈련 데이터를 가장 잘 나타내는 직선을 구할 수 있습니다.

 

 

그렇다면 $cost(W,b)$를 최소가 되게 만드는 $W$와 $b$의 값을 찾으려면 $W$와 $b$의 값을 바꿔야 한다는 말과 같습니다. 이 때 사용되는 것이 옵티마이저(최적화) 알고리즘입니다. 그리고 이 옵티마이저 알고리즘을 통해 적절한 $W$와 $b$를 찾아내는 과정 학습(training) 이라고 부릅니다. 다양한 옵티마이저 알고리즘이 있지만 간단한 구현이므로 지난 포스팅의 Gradient descent 알고리즘을 사용하겠습니다.

 

 

[DL] 경사 하강법 ( Gradient Descent )

기계학습 문제는 학습의 목표가 최적의 매개변수를 찾는 것이고, 이는 곧 손실 함수가 최솟값이 될 때의 매개변수를 찾는 것과 동일합니다. 이 때 기울기를 사용해서 함수의 최솟값을 찾는 아이

songsite123.tistory.com

 

$$ W:= W - \alpha \dfrac{\partial L}{\partial W} $$

 

Gradient Descent는 위 식으로 나타낼 수 있으며, 기울기를 통해 최솟값을 찾는 알고리즘입니다. 자세한 설명은 위 포스팅에 나와있으니 생략하도록 하겠습니다.

 

즉 정리해보면 가설 클래스는 문제를 수식으로 모델링하는 것과 같으며, 이 클래스 내에는 가중치를 초기화 하는 함수, 순전파 과정(forward propagation)을 통해 모델이 입력을 받아 결과를 출력하는 과정에 대한 함수, 계산한 결과(가설 출력값)과 정답과의 차이를 계산하는 손실 함수, 미분을 통해 기울기를 계산하는 함수, 구한 기울기를 통해 $W$를 업데이트 하는 함수, $W$값을 반환하는 함수가 포함되어야 합니다. 전체 코드를 한 번 보고 부분적으로 해석해보도록 하겠습니다. 이번 구현에서 bias $b$는 고려하지 않습니다.

 

# 가설 모델(학습 시킬 대상)
class H():
    def __init__(self, w):
        self.w = w
    # 결과를 반환하는 함수
    def forward(self, x):
        return self.w * x
    # 가설의 비용을 구하는 함수(낮추어야 할 대상)
    def get_cost(self, X, Y):
        cost = 0
        for i in range(len(X)):
            cost += (self.forward(X[i]) - Y[i]) ** 2
        cost = cost / len(X)
        return cost
    # 기울기를 계산하는 함수
    def get_gradient(self, X, Y):
        cost = self.get_cost(X, Y)
        dw = 0.001
        self.w = self.w + dw
        next_cost = self.get_cost(X, Y)
        self.w = self.w - dw
        dcost = next_cost - cost
        gradient = dcost / dw
        return gradient, next_cost
    # 편미분으로 기울기를 계산하는 함수
    def get_gradient_using_derivative(self, X, Y):
        gradient= 0
        for i in range(len(X)):
            gradient += (h.forward(X[i]) - Y[i]) * X[i]
        gradient = 2 * gradient / len(X)
        cost = self.get_cost(X, Y)
        return gradient, cost
    # w 값을 변경하는 함수
    def set_w(self, w):
        self.w = w
    # w 값을 반환하는 함수
    def get_w(self):
        return self.w

 

부분적으로 살펴보겠습니다.

class H():
    def __init__(self, w):
        self.w = w
    # 결과를 반환하는 함수
    def forward(self, x):
        return self.w * x
    # 가설의 비용을 구하는 함수(낮추어야 할 대상)
    def get_cost(self, X, Y):
        cost = 0
        for i in range(len(X)):
            cost += (self.forward(X[i]) - Y[i]) ** 2
        cost = cost / len(X)
        return cost

가설 클래스 H를 정의 하고, $W$값을 초기화 하는 __init__ 함수입니다. bias를 고려하지 않음으로써 선형 회귀의 가설이 $H(x) = Wx$ 로 변경되기 때문에 forward() 함수에서는 $W$와 $x$를 곱한 결과를 반환하면 됩니다. 그 다음 get_cost 함수는 입력으로 X와 Y(정답 데이터)를 받아서 MSE를 계산하는 함수입니다.

 

    # 기울기를 계산하는 함수
    def get_gradient(self, X, Y):
        cost = self.get_cost(X, Y)
        dw = 0.001
        self.w = self.w + dw
        next_cost = self.get_cost(X, Y)
        self.w = self.w - dw
        dcost = next_cost - cost
        gradient = dcost / dw
        return gradient, next_cost
    # 편미분으로 기울기를 계산하는 함수
    def get_gradient_using_derivative(self, X, Y):
        gradient= 0
        for i in range(len(X)):
            gradient += (h.forward(X[i]) - Y[i]) * X[i]
        gradient = 2 * gradient / len(X)
        cost = self.get_cost(X, Y)
        return gradient, cost
    # w 값을 변경하는 함수
    def set_w(self, w):
        self.w = w
    # w 값을 반환하는 함수
    def get_w(self):
        return self.w

 

그 다음으로 Gradient descent 알고리즘을 사용하기 위해 gradient를 계산하는 함수가 2가지가 있습니다. 첫 번째 get_gradient 함수는 수치미분 방식으로 기울기를 계산합니다. 이 모델의 경우 매우 간단하기 때문에 수치미분으로 gradient를 구해도 상관없지만, 딥러닝으로 가면 수치미분 방식은 매우 느리고 계산량이 너무 많기 때문에 사용하지 않습니다.

 

[DL] 수치 미분 ( Numerical differentiation )

뉴럴 네트워크는 데이터를 통해 학습을 할 때 "미분"을 이용하여 학습을 합니다. $$ f'(x) = \lim_{h \to 0} \dfrac{f(x+h)-f(x)}{h} $$ 미분 계수는 극한으로 정의됩니다. 어떤 함수의 순간 변화율을 구하는 것

songsite123.tistory.com

 

미분으로 기울기를 계산하는 함수라고 주석이 달려있는 get_gradient_using_derivative 함수의 동작에 대해 조금 더 자세하게 살펴보도록 하겠습니다. bias를 고려하지 않은 선형 회귀의 가설을 적용한 gradient descent의 수식은 아래와 같습니다.

 

$$ W:= W - \alpha \dfrac{\partial}{\partial W} cost(W) = W - \alpha \dfrac{2}{n} \sum_{i=1}^n \left[ y^{(i)}-H(x^{(i)}) \right]$$

 

 

먼저 입력 매개변수는 X와 Y로, 맨 위의 표에서 하루 노동 시간(X)과 하루 매출(Y)을 의미합니다. $x$의 길이만큼 반복하여 gradient += (h.forward(X[i]) - Y[i]) * X[i] 를 수행하는 부분은 수식에서 $\sum_{i=1}^n \left[ y^{(i)}-H(x^{(i)}) \right]$ 를 계산하는 작업과 동일합니다. 이 때 $h$는 가설 클래스 $H$의 인스턴스를 의미하는 것이며 아무 알파벳이나 이름을 붙여도 상관없습니다. forward를 멤버로 가지는 인스턴스에 대해 forward를 통해 예측한 값과 정답과의 차이를 누적하여 더하는 것이죠.

 

그 다음 2를 곱해주고, len(X)($=n$)을 나누어주어 갱신해야 하는, 즉 $\dfrac{2}{n}$ 을 곱하는 작업을 해주는 겁니다. 이로써 빼줘야 하는 가중치 갱신값과 손실함수 계산 값을 반환해주는 함수입니다.

 

그 아래의 set_w 와 get_w 는 이름 그대로 $W$값을 설정하거나 변경, 반환 하는 함수입니다.

 

 

$W$ 값을 바꿔보면서 손실함수 값을 plot 할 수 있습니다. 한 번 -300,000 부터 300,000까지 100단위로 점을 찍어보며 손실함수를 그려보도록 하겠습니다.

cost_list = []
w_list = []

# w를 -300,000부터 300,000까지 바꾸어 보며 비용 확인
for i in range(-300, 300):
    w = i * 1000
    h = H(w)
    cost = h.get_cost(X, Y)
    w_list.append(w)
    cost_list.append(cost)

# 결과적으로 약 25,000 정도일 때 최소 비용임을 확인
plt.figure(figsize=(8, 8))
plt.scatter(w_list, cost_list, s=10)

 

우리가 눈으로 확인했을 때는 대략 25,000 부근에서 손실함수가 최소인 것을 확인할 수 있습니다. 선형 회귀 모델은 정말 간단하고 손실 함수가 2차함수 형태로 나타나기 때문에 이렇게 시각화 만으로 최소 지점을 유추할 수 있는 것이고 딥러닝 모델의 경우 파라미터의 차원이 훨씬 크기 때문에 이렇게 시각화하여 나타낼 수 없습니다.

 

따라서 학습을 통해 $W$값들을 갱신해주어야 하고, 그 과정을 학습이라고 한다고 했었죠. 위의 가설 클래스를 통해 가설 인스턴스 $h$를 만들고 실제로 1000번 학습하여 손실함수의 최솟값을 가지는 $W$를 찾아내는지 확인해보겠습니다.

w = 4
h = H(w)
learning_rate = 0.001
    
for i in range(1001):
    gradient, cost = h.get_gradient_using_derivative(X, Y)
    h.set_w(h.get_w() + learning_rate * -gradient)
    if i % 100 == 0:
        print("[ epoch: %d, cost: %.2f ]" % (i, cost))
        print("w = %.2f, w_gradient = %.2f" % (h.get_w(), gradient))
        
print("f(x) = %.2fx" %(h.get_w()))
print("예측값: [%.2f]" %(h.forward(8)))

기울기 함수는 2번째 편미분을 활용한 기울기 함수를 사용하였습니다. 선형 회귀의 경우 local mininum 이 곧 global minimum 이기 때문에 손실함수가 최소가 되는 $W$의 값은 25928.57 인 것을 확인할 수 있습니다.

 

이를 통해 8시간 일했을 때, 9시간 일했을 때, 10시간 일했을 때의 매출량도 대략적으로 예측할 수 있습니다. 이를 시각화 해서 표현해보면 아래와 같습니다.

x_pred = [i for i in range(11)]
y_pred = [h.get_w() * i for i in range(11)]
plt.plot(x_pred, y_pred)
plt.scatter(X, Y)

 

 

 

 Python 선형 회귀 구현 (Bias O)

 

위의 코드에서는 bias를 고려하지 않았습니다. 이번 코드는 Bias를 포함한 선형회귀 코드입니다. 데이터는 같은 데이터를 사용하고, 곧바로 가설 클래스부터 정의해보도록 하겠습니다.

# 가설 모델(학습 시킬 대상)
class H():
    def __init__(self, w, b):
        self.w = w
        self.b = b
    # 결과를 반환하는 함수
    def forward(self, x):
        return self.w * x + self.b
    # 가설의 비용을 구하는 함수(낮추어야 할 대상)
    def get_cost(self, X, Y):
        cost = 0
        for i in range(len(X)):
            cost += (self.forward(X[i]) - Y[i]) ** 2
        cost = cost / len(X)
        return cost
    # 미분으로 기울기를 계산하는 함수
    def get_gradient_using_derivative(self, X, Y):
        w_gradient = 0
        b_gradient = 0
        for i in range(len(X)):
            w_gradient += (self.forward(X[i]) - Y[i]) * X[i]
            b_gradient += (self.forward(X[i]) - Y[i])
        w_gradient = 2 * w_gradient / len(X)
        b_gradient = 2 * b_gradient / len(X)
        return w_gradient, b_gradient, self.get_cost(X, Y)
    # w 값을 변경하는 함수
    def set_w(self, w):
        self.w = w
    # w 값을 반환하는 함수
    def get_w(self):
        return self.w
    # b 값을 변경하는 함수
    def set_b(self, b):
        self.b = b
    # b 값을 반환하는 함수
    def get_b(self):
        return self.b

달라진 점은 오직 $b$가 추가되었고, $b$를 변경하고 반환하기 위한 set_b, get_b 함수의 추가, 그리고 편미분을 통해 기울기를 계산할 때 $W$에 대해서만 기울기를 계산하는 것이 아니라 $b$에 대해서도 기울기를 계산한다는 점입니다. gradient 함수들이 이제 $W$에 대한 기울기, $b$에 대한 기울기, 손실함수 값을 반환합니다.

w = 4
b = 0
h = H(w, b)
learning_rate = 0.001
    
for i in range(10001):
    w_gradient, b_gradient, cost = h.get_gradient_using_derivative(X, Y)
    h.set_w(h.get_w() + learning_rate * -w_gradient)
    h.set_b(h.get_b() + learning_rate * -b_gradient)
    if i % 1000 == 0:
        print("[ epoch: %d, cost: %.2f ]" % (i, cost))
        print("w = %.2f, w_gradient = %.2f" % (h.get_w(), w_gradient))
        print("b = %.2f, b_gradient = %.2f" % (h.get_b(), b_gradient))
        
print("f(x) = %.2fx + %.2f" %(h.get_w(), h.get_b()))
print("예측값: [%.2f]" %(h.forward(8)))

학습을 할 때 $W$만 업데이트하는 것이 아닌 $b$도 업데이트해주게 코드를 한 줄만 추가하면 학습도 동일합니다.

각 epoch 에서 $W$와 $b$를 갱신하고, 결론적으로 절편이 추가된 선형 함수를 얻어낼 수 있습니다.

x_pred = [i for i in range(11)]
y_pred = [h.get_w() * i + h.get_b() for i in range(11)]
plt.plot(x_pred, y_pred)
plt.scatter(X, Y)

 

 

 

 Pytorch 선형 회귀 구현

 

파이토치를 사용하면 훨씬 간단한 형태로 선형 회귀를 구현할 수 있습니다. 위의 2번째 바이어스를 포함한 선형 회귀 코드와 동일한 코드를 파이토치를 사용해 구현합니다. 각 코드의 자세한 설명은 아래 링크의 포스팅을 참고해주세요.

import torch

# 데이터
X = [1, 2, 3, 4, 5, 6, 7]
Y = [25000, 55000, 75000, 110000, 128000, 155000, 180000]

X = [[i] for i in X]
Y = [[i] for i in Y]

x_data = torch.Tensor(X)
y_data = torch.Tensor(Y)
  
class LinearRegressionModel(torch.nn.Module): 
    def __init__(self, input_dim, output_dim):
        super(LinearRegressionModel, self).__init__() 
        self.linear = torch.nn.Linear(input_dim, output_dim)
    def forward(self, x): 
        y_pred = self.linear(x)
        return y_pred

model = LinearRegressionModel(1, 1)

criterion = torch.nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.001) 

for epoch in range(10001): 
    pred_y = model(x_data)
    loss = criterion(pred_y, y_data)
    optimizer.zero_grad() 
    loss.backward() 
    optimizer.step()
    if epoch % 1000 == 0:
        print("[ epoch: %d, cost: %.2f ]" % (epoch, loss.data))
        print("w = %.2f, b = %.2f" % (model.linear.weight, model.linear.bias))

print("f(x) = %.2fx + %.2f" % (model.linear.weight, model.linear.bias))
print("예측값: [%.2f]" % (model(torch.Tensor([[8]]))))

 

 

 

 Pytorch 다변수 선형 회귀 구현

 

실제 학습을 할 때는 변수가 하나인 데이터에 대해 학습하지 않고 여러 개의 변수에 대한 학습을 진행해야 합니다. 우선 전체 코드와 결과를 보고, 부분적으로 해석을 해보도록 하겠습니다. 변수가 2차원인 다변수 선형 회귀이기 때문에 가설은 평면의 방정식 평태로 나타나게 됩니다.

import matplotlib.pyplot as plt
import torch
import torchvision
import numpy as np

X = [[1, 33],
     [2, 27],
     [3, 29],
     [4, 45],
     [5, 27],
     [6, 33],
     [7, 35]]
Y = [25000, 55000, 75000, 125000, 128000, 155000, 182000]

Y = [[i] for i in Y]

x_data = torch.Tensor(X)
y_data = torch.Tensor(Y)
  
class LinearRegressionModel(torch.nn.Module): 
    def __init__(self, input_dim, output_dim):
        super(LinearRegressionModel, self).__init__() 
        self.linear = torch.nn.Linear(input_dim, output_dim)
    def forward(self, x): 
        y_pred = self.linear(x)
        return y_pred

model = LinearRegressionModel(2, 1)

criterion = torch.nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr = 0.0001) 
  
for epoch in range(100001):
    pred_y = model(x_data)
    loss = criterion(pred_y, y_data)
    optimizer.zero_grad() 
    loss.backward()
    optimizer.step()
    if epoch % 10000 == 0:
        print("[ epoch: %d, cost: %.2f ]" % (epoch, loss.data))
        print("w1 = %.2f, w2 = %.2f, b = %.2f" % (model.linear.weight[0][0], model.linear.weight[0][1], model.linear.bias))

print("f(x) = %.2fx1 + %.2fx2 + %.2f" % (model.linear.weight[0][0], model.linear.weight[0][1], model.linear.bias))
print("예측값: [%.2f]" % (model(torch.Tensor([[1, 33]]))))

import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure(figsize=(10, 10))
ax = fig.add_subplot(111, projection='3d')  # 3D 축을 추가
ax.view_init(20, 110)

x1 = [1, 2, 3, 4, 5, 6, 7]
x2 = [33, 27, 29, 45, 27, 33, 35]
y = [25000, 55000, 75000, 110000, 128000, 155000, 180000]
ax.scatter(x1, x2, y, s=40)

y_pred = [model(torch.Tensor(i)).tolist()[0] for i in X]
ax.plot(x1, x2, y_pred)

plt.show()  # 그래프를 화면에 표시

전체적으로 데이터의 경향성만 그래프로 나타낸 코드입니다. 실제로는 평면을 근사시키는 작업으로, 학습을 통해 구한 평면을 시각화 해보는 결과는 오른쪽과 같습니다. 아래는 오른쪽 시각화 코드입니다.

 

import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np

# 데이터 생성
x1 = [1, 2, 3, 4, 5, 6, 7]
x2 = [33, 27, 29, 45, 27, 33, 35]
y = [25000, 55000, 75000, 110000, 128000, 155000, 180000]

# 3D 그래프 생성
fig = plt.figure(figsize=(10, 10))
ax = fig.add_subplot(111, projection='3d')
ax.view_init(20, 110)

# 데이터를 3D 산점도로 플롯
ax.scatter(x1, x2, y, s=40)

# 모델 예측값 생성 (임의의 예시 값)
y_pred = [model(torch.Tensor(i)).tolist()[0] for i in X]

# 3D 그래프에 선 플롯
ax.plot(x1, x2, y_pred)

def custom_function(x1, x2, a, b, c):
    return a * x1 + b * x2 - c

print("w1 = %.2f, w2 = %.2f, b = %.2f" % (model.linear.weight[0][0], model.linear.weight[0][1], model.linear.bias))


# 그리드 포인트 생성
X1, X2 = np.meshgrid(np.linspace(min(x1), max(x1), 10), np.linspace(min(x2), max(x2), 10))
Y_custom = custom_function(X1, X2, float(model.linear.weight[0][0]), float(model.linear.weight[0][1]), float(model.linear.bias))

# 3D 그래프에 함수 플롯
ax.plot_surface(X1, X2, Y_custom, cmap='viridis', alpha=0.5)

plt.show()  # 그래프를 화면에 표시합니다.

 

 

이제 부분적으로 해석을 해보도록 하겠습니다.

import matplotlib.pyplot as plt
import torch
import torchvision
import numpy as np

# 데이터 x의 차원: [2]
# 데이터 X의 차원: [7, 2]
X = [[1, 33],
     [2, 27],
     [3, 29],
     [4, 45],
     [5, 27],
     [6, 33],
     [7, 35]]
Y = [25000, 55000, 75000, 125000, 128000, 155000, 182000]
# 데이터 Y의 차원: [7]

Y = [[i] for i in Y] 

x_data = torch.Tensor(X)
y_data = torch.Tensor(Y)

print(x_data.shape) # torch.Size([7, 2])
print(y_data.shape) # torch.Size([7, 1])

matplotlib 은 시각화 라이브러리, numpy는 행렬 곱같은 수학 연산을 위한 라이브러리, torch는 PyTorch 기본 라이브러리, torchvision 은 PyTorch의 영상 처리 라이브러리 입니다.

 

이 중에서 torch.nn과 torch.data 를 핵심적으로 사용하게 되는데 torch.nn은 뉴럴 네트워크 모델 그 자체를 사용할 수 있게 해주고 torch.data는 뉴럴 네트워크에 들어가는 데이터 등을 처리합니다.

 

2차원 데이터들을 묶어 [7, 2] 차원으로 X를 준비하고 Y를 준비합니다. 이 때 Y를 굳이 한 번 더 감싸주는 이유는 Y의 차원을 [7] 에서 [7, 1] 형태로 맞추어 주기 위함입니다. PyTorch 에서는 기본적으로 항상 첫번째 axis로 배치사이즈를 가집니다. 즉, [batch_size, 데이터 차원] 의 형태로 텐서를 다루기 때문에 형태를 맞춰주기 위해 리스트로 한 번 더 감싼 겁니다.

 

class LinearRegressionModel(torch.nn.Module): 
    def __init__(self, input_dim, output_dim):
        super(LinearRegressionModel, self).__init__() 
        self.linear = torch.nn.Linear(input_dim, output_dim)
    def forward(self, x): 
        y_pred = self.linear(x)
        return y_pred

model = LinearRegressionModel(2, 1)

가설 클래스와 가설 객체(모델)을 생성하는 코드입니다. 위의 Python 코드에 비해 구현해야하는 클래스 내용이 매우 줄어들었는데 이는 PyTorch의 torch.nn.Module 에서 이미 제공을 하기 때문입니다.  torch.nn.Module 은 뉴럴 네트워크의 기능을 가지고 있는 클래스입니다. 내부에 이미 __init__(), forward() 가 정의되어 있습니다. 따라서 torch.nn.Module 을 상속받아서 이 두 개의 기능을 이름은 그대로, 내용만 바꾸어서 사용하는 객체 지향 프로그래밍의 개념을 사용하는 것입니다.

 

 

super(LinearRegressionModel, self).__init__() 는 클래스의 생성자 메서드를 정의하는 작업입니다. 이 메서드를 통해 모델을 초기화하고, 입력 차원과 출력 차원의 인자로 받습니다. super() 함수의 역할은 이 함수를 호출하여 부모 클래스인 torch.nn.Module 의 초기화 메서드를 실행합니다. 자식 클래스로 부모 클래스의 메서드를 오버라이딩 하는 것이죠.

 

 

self.linear = torch.nn.Linear(input_dim, output_dim) 는 또 다른 클래스인 torch.nn.Linear 을 사용하여 모델 내부에 선형 변환을 수행하는 층을 정의하는 코드입니다. linear 도 하나의 객체가 되는 것이죠. 선형 변환을 제공해주기 때문에 선형 회귀의 코드가 한줄로 매우 쉽게 정리됩니다.

 

그 다음으로 모델의 순전파 연산을 정의하는 forward입니다. 주어진 데이터 $x$를 받아 모델을 통과시켜 예측값을 계산하는 역할을 합니다. 이 작업이 linear 을 사용해 수행됩니다.

 

파이썬에서 어떤 클래스를 사용하든 처음 클래스를 초기화할 때 입력으로 넣는 인자는 __init__() 함수에 자동으로 들어갑니다. 따라서 model = LinearRegressionModel(2, 1) 의 의미는 model 이라는 이름을 가지는 LinearRegressionModel 클래스의 객체를 생성한 것이고 이 때 초기값으로 __init__ 함수에 넣어 input_dim 이 2이고, output_dim 이 1 인 객체를 생성한 것입니다.

 

criterion = torch.nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr = 0.0001)

손실함수로 MSE와 옵티마이저로 SGD를 사용한다는 코드입니다. MSE는 torch.nn 내에 MSELoss() 라는 이름으로, SGD는 SGD라는 이름으로 구현되어있습니다. 이 때 자세한 명령어가 기억안난다면 PyTorch 공식 홈페이지를 참조하면서 코드를 구성하시면 되겠습니다. SGD는 인자로 모델의 파라미터와 learning rate를 인자로 사용합니다. lr 또한 이미 SGD 내에 존재하는 멤버 변수가 되는 것이죠.

 

for epoch in range(100001):
    pred_y = model(x_data)
    loss = criterion(pred_y, y_data)
    optimizer.zero_grad() 
    loss.backward()
    optimizer.step()
    if epoch % 10000 == 0:
        print("[ epoch: %d, cost: %.2f ]" % (epoch, loss.data))
        print("w1 = %.2f, w2 = %.2f, b = %.2f" % (model.linear.weight[0][0], model.linear.weight[0][1], model.linear.bias))

print("f(x) = %.2fx1 + %.2fx2 + %.2f" % (model.linear.weight[0][0], model.linear.weight[0][1], model.linear.bias))
print("예측값: [%.2f]" % (model(torch.Tensor([[1, 33]]))))

100,000 epoch 를 for문을 통해 수행합니다. model에 [batch_size, data_size] 크기를 가지는 $x$를 넣으면 예측값이 pred_y에 저장됩니다. 그 이후 예측값과 정답값을 미리 정의해놓았던 손실함수로 구해 loss에 저장합니다. optimizer.zero_grad() 는 편미분을 통해 기울기를 구하는 역전파를 하기 이전에 각 파라미터에 대한 기울기를 초기화해주는 작업을 수행해줍니다. 

 

그 이후 loss.backward() 는 파이썬으로 구현할 때 get_gradient()와 동일한 역할을 해주는 코드입니다. 역전파를 수행하며 gradient를 계산합니다. 그 다음으로 optimizer.step()을 수행하면 학습을 수행합니다. gradient를 구했으니 lr 과 gradient를 곱한 값을 빼줌으로써 가중치를 업데이트하는 과정이 optimizer.step() 에서 수행됩니다.

 

그 이후 1만 epoch마다 출력을 해주며 학습이 완료됩니다. 여기까지 PyTorch를 이용한 다변수 선형 회귀 학습 코드입니다.