본문 바로가기
Study 공부할레나

[스터디/밑시딥3] 제 4고지 신경망 만들기_42. 선형 회귀

by LENA-cfg 2023. 10. 10.

42.1 토이 데이터셋

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(0)
x = np.random.rand(100, 1)
y = 5 + 2 * x + np.random.rand(100, 1)

# Scatter plot
plt.scatter(x, y, c='blue', marker='o', label='Data points')
plt.xlabel('X')
plt.ylabel('Y')
plt.title('Scatter plot of x vs y')
plt.legend(loc='upper left')
plt.grid(True)
plt.show()

x, y라는 두 개의 변수로 구성된 데이터 셋을 생성하고 시각화하였습니다. 

수식처럼 x와 y는 선형 관계인데 y에 노이즈를 추가해서 선형 형태에서 조금 퍼져있는 모습입니다.

 

회귀라는 뜻이 x로부터 실수값 y를 예측하는 것을 말하며 예측값이 선형(직선)을 이루는 것을 '선형 회귀'라고 합니다.

42.2 선형 회귀 이론

통계학에서 회귀는 여러 개의 독립변수와 한 개의 종속변수 간의 상관관계를 모델링하는 기법을 통칭합니다. 

y와 x가 선형 관계일때 y = Wx + b라는 식으로 표현할 수 있고 이 식은 아래처럼 직선의 형태를 띕니다.

여기서 W는 스칼라 값이며 독립변수의 값에 영향을 미치는 회귀 계수 입니다. 

선형 회귀는 실제 값과 예측값의 차이(=잔차)를 최소화하여 직선형 회귀선을 최적화 하는 방식입니다.

 

위의 그림을 보면 데이터와 수직으로 올라가서 예측치(모델)접점의 거리인 잔차를 최소화하는것이 선형 회귀를 최적화하는 방법입니다. 

 

그러면 예측치와 데이터의 오차를 구해야하는데 여기선 평균 제곱 오차로 구합니다. 

식은 아래에 있습니다.

총 N개의 점에 대해 (xi, yi)의 각 점에서 제곱오차를 구한 다음 모두 더합니다. 

그리고 평균을 구하기 위해 1/N을 곱해줍니다. 

 

모델 성능이 얼마나 나쁜가를 평가하는 함수를 손실 함수라고 하며 선형 회귀는 손실 함수로 평균 제곰 오차를 이용합니다.

우리는 손실 함수의 출력을 최소화하는 W와 b를 찾는 것입니다. 

42.3 선형 회귀 구현

if '__file__' in globals():
    import os, sys
    sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
import numpy as np
import matplotlib.pyplot as plt
from dezero import Variable
import dezero.functions as F

# Generate toy dataset
np.random.seed(0)
x = np.random.rand(100, 1)
y = 5 + 2 * x + np.random.rand(100, 1)
x, y = Variable(x), Variable(y)

W = Variable(np.zeros((1, 1)))
b = Variable(np.zeros(1))


def predict(x):
    y = F.matmul(x, W) + b
    return y

매개변수 W와 b를 Variable 인스턴스로 만들어주고 각각의 형상은 (1, 1), (1,)dlqslek. 

predict 함수를 정의해주고 matmul을 사용해서 x와 W를 곱해주고 b를 더해줍니다. 

이렇게 해서 구한 y는 (100, 1)의 형상을 띄게 됩니다. 

한번에 계산으로 모든 데이터의 예측치가 구해집니다. 

x의 차원이 4일 경우는 어떻게 되는지 위의 그림으로 확인 할 수 있습니다.

위의 그림처럼 x.shape[1]과 W.shape[0]을 일치시켜야 행렬 곱이 제대로 계산 될 수 있습니다.

 

y = F.matmul(x, W) + b에서 덧셈 계산 시 브로드캐스트가 일어나고 b의 형상은 (1,)이지만 그 원소를 복제해서 (100, 1) 형상으로 만든 후 원소별로 더해줍니다. 

def mean_squared_error(x0, x1):
    diff = x0 - x1
    return F.sum(diff ** 2) / len(diff)


lr = 0.1
iters = 100

for i in range(iters):
    y_pred = predict(x)
    loss = mean_squared_error(y, y_pred)

    W.cleargrad()
    b.cleargrad()
    loss.backward()

    # Update .data attribute (No need grads when updating params)
    W.data -= lr * W.grad.data
    b.data -= lr * b.grad.data
    print(W, b, loss)

오차를 구하는 함수를 mean_squared_error(x0, x1)로 만들어주고 경사하강법으로 매개변수를 갱신해주는데

W.data -= lr * W.grad.data처럼  인스턴스 변수의 data로 값만 꺼내서 계산을 해줍니다.

코드를 실행해보면 손실함수의 출력값이 줄어드는 것을 볼 수 있습니다.

그리고 예측 모델이 빨간 선으로 잘 그어진 것을 볼 수 있습니다.

42.4 [보충] DeZero의 mean_squared_error 함수

def mean_squared_error(x0, x1):
    diff = x0 - x1
    return F.sum(diff ** 2) / len(diff)

평균 제곱 오차 함수는 개선 할 점이 있습니다. 

위의 그림은 mean_squared_error의 계산 그래프인데 중간에 이름없는 변수들이 계산그래프에 있어서 실제 메모리에 차지하고 있습니다.

 

메모리를 덜 차지하는 방식으로 수정할건데 방법은 Function 클래스를 상속해 구현하는 방식입니다.

class MeanSquaredError(Function):
    def forward(self, x0, x1):
        diff = x0 - x1
        y = (diff ** 2).sum() / len(diff)
        return y

    def backward(self, gy):
        x0, x1 = self.inputs
        diff = x0 - x1
        gx0 = gy * diff * (2. / len(diff))
        gx1 = -gx0
        return gx0, gx1


def mean_squared_error(x0, x1):
    return MeanSquaredError()(x0, x1)

순전파 코드는 이전에 구현했던 로직을 그대로 가져오고 역전파에서는 수식으로 미분을 계산한다음 해당 수식을 코드로 옮겨줍니다.

 

위의 계산 그래프를 보면 중간에 등장하는 변수들이 사라졌기때문에 중간 데이터들이 MeanSquareError 클래스의 forward 메서드에서만 사용됩니다.