AI study – week 2

by

in

1. Gradient Descent (경사하강법)

이번에는 Linear Regression 에 대해서 실습을 해보기로 하자. 지난번에는 단순히 경사하강법을 실습해보기 위하여 $y = x^2$ 라는 식을 두어 $(2, 4)$ 좌표에서 시작하여 함수의 min 값인 $(0, 0)$ 까지 기울기에 비례하여 움직임으로써 도달할 수 있었다.

이번에는 실제로 선형 회귀에서의 점들을 이용하여 $y = mx + b$ 에서의 $m$과 $b$ 를 기준으로 3차원 그래프를 작성해주고, 이에 대해서 경사하강법을 진행하여 최적화를 진행하는 작업을 해보자.

3d surface graph

먼저, $y = mx + b$라는 그래프에서 변수는 2개이다. 그렇기에 두가지 변수에 따라서 $Loss$ 에 대한 그래프를 그리게 되면, 2가지 축을 독립변수로 하는 1가지의 종속변수가 그래프로 그려질 것이고, 따라서 3차원 상에서 면그래프가 그려질 것이다.

면그래프를 그리게 되면, 각각의 $m$, $b$ 에 대해서 $Loss$ 의 값이 오르락 내리락 하는 모습을 볼 수 있고, 이에 따라 가장 낮은 $\mathcal{L}$ 값을 가지는 $m$, $b$ 값을 최적의 값으로 결정지을 수 있을 것이다. 먼저 3d 면 그래프를 그려보자.

먼저, 기본적인 함수 define 은 다음과 같다.

def graph_linear_regression_mse(
    x,
    y,
    m_sclae=10,
    b_scale=10,
    sample_density=500,
):
    def mse(x,y,m,b):
        pass
Python

각각 점들에 대한 $x$, $y$ 좌표를 각각 입력받고, 내부 mse 라는 함수를 사용하여 $mean squared error$ 라는 값을 계산할 수 있게 한다. 그 후에 이 함수를 이용해서 각각의 $m$, $b$ 값에 대하여 mse 를 계산해주게 되면, 이론상으로 3d 면 그래프를 그릴 수 있겠다.

그럼 각각의 mse를 계산해주는 함수를 작성해보자. 여기서 입력의 $x$, $y$ 변수는 항상 pytorch 의 tensor 자료형을 이용해서 구현할 것임을 미리 알린다.

def graph_linear_regression_mse(
    x,
    y,
    m_sclae=10,
    b_scale=10,
    sample_density=500,
):
    def mse(x,y,m,b):
        return torch.mean((y-(m*x+b))**2)
    
    def new_mse(x, y, m, b):
        ss = 0
        for xx, yy in zip(x, y):
            ss += (yy.item() - m * xx.item() - b)**2
        return ss / len(x)

    fig = plt.figure()
    ax = fig.add_subplot(111, projection='3d')
    ax.dist = 11

    m = np.linspace(-5, 5, 100)
    b = np.linspace(-5, 5, 100)
    X, Y = np.meshgrid(m, b)

    Z = new_mse(x, y, X, Y)

    # 면 그래프
    ax.plot_surface(X, Y, Z, cmap=plt.get_cmap("GnBu"))
    ax.set_xlabel("m")
    ax.set_ylabel("b")
    ax.set_zlabel("Loss")
    
    plt.show()
    
d = torch.tensor([[20, 6.5], [25, 7.7], [30, 9.2], [35, 12.8], [40, 14.3]])
graph_linear_regression_mse(d[:, :1], d[:, 1:])
Python

전 포스팅에서 했던 방식과 같이 $m$, $b$ 에 대한 linspace 를 정의해준 후에, new_mse 라는 함수를 사용하여 결과값을 numpy 로 저장한다. 그런 후에 $Z$($\mathcal{L}$)를 3차원으로 plot_surface 라는 함수를 이용하여 그릴 수 있다.

결과는 다음과 같다.

여기서 최저점을 찾을 수는 있겠지만, $m$ 축을 기준으로는 최저점의 부분이 명확히 보이는 반면 $b$ 축을 기준으로는 최저점을 찾기가 상당히 어려운 것을 볼 수 있다. 실제로 각 부분의 $Loss$를 구해본다면 별 차이가 없음을 볼 수 있다.

그렇다면 이 현상이 일어나는 이유가 무엇일까. 나도 처음 공부해보는 입장이라 잘은 모르지만, 직관적으로 보아도 여러 데이터셋이 있을 때, $b$의 값에 따라 $m$의 값이 잘만 변경되어준다면, $Loss$ 가 그다지 차이가 없어질 수도 있을 것이다. 이 사태를 방지하고자, 우리는 $b$의 값을 의도적으로 0으로 고정시켜줌으로써 $m$ 의 값을 보다 정확히 구할 수 있다.

$b$의 값을 고정시킨다는 말이 잘 이해가 가지 않을 수 있겠지만, 이미 고정된 점인 $(x, y)$ 좌표를 어떻게 조정해야 $\bar{y} = m\bar{x}$ 꼴로 유도할 수 있을까?

바로 표준화 작업이다. 각각의 x 와 y 데이터들을 정규분포화함으로써 평균을 0으로 세팅하고, 표준편차로 나누어 줌으로써 폭을 동일화하여 최종적으로 구해지는 $b$의 값을 0으로 강제할 수 있다.

표준화 작업은 다음과 같다.

$$\acute{x} = \frac{x \;-\; \bar{x}}{\sigma(x)},\; \acute{y} = \frac{y \;-\; \bar{y}}{\sigma(y)}$$

그렇다면 다시 한번 그래프를 그려보자. 아래 부분만 추가해주면 된다.

    x_mean, y_mean = torch.mean(x), torch.mean(y)
    x_std, y_std = torch.std(x), torch.std(y)

    def standardization(x, y):
        return (x - x_mean) / x_std, (y - y_mean) / y_std
    
    def unstandardization(x, y):
        return (x * x_std) + x_mean, (y * y_std) + y_mean

    x, y = standardization(x, y)
Python

이렇게 코드를 실행하게 되면, 아래와 같이 $m$, $b$ 축에 관계없이 모두 최저점이 명확히 보이는 면 그래프를 볼 수 있다. 육안으로 대충 확인해보았을 때, $m$ 은 약 1, 그리고 $b$ 는 약 0 정도가 나오는 것을 확인할 수 있겠다. (표준화 작업의 의도대로 b는 0에 수렴한다.)

Gradient descent visualization

그럼 본격적으로 경사하강법을 적용하여 위의 3차원 그래프에 점을 찍어보자. 편의를 위해 첫 시작은 $(-5, 5)$로 고정하여 진행해보도록 하겠다. 지난번 포스팅에서 진행했던 2차원에서 3차원으로 확장되었기에 막막할 수도 있지만, 3차원이 되면 torch는 알아서 gradient 를 2차원으로 구해주기 때문에, 동일하게 진행해주기만 하면 된다.

		w = torch.tensor([-5., 5.], requires_grad=True)
    learning_rate = 0.02
    num_iterations = 1000

    data = [[w[0].item(), w[1].item()]]

    for _ in trange(num_iterations):
        w.grad = None
        Loss = mse(x, y, w[0], w[1])
        Loss.backward()

        w.data -= learning_rate * w.grad
        data.append([w[0].item(), w[1].item()])

    data = np.array(data)
    x_data = torch.tensor(data[:, 0])
    y_data = torch.tensor(data[:, 1])
    z_data = np.array([new_mse(x, y, xx, yy) for xx, yy in zip(x_data, y_data)])
    
    for xx, yy, zz in zip(x_data, y_data, z_data):
        ax.plot(xx, yy, zz, color='orange', marker='*', zorder=3, markersize=1.5,)
Python

이렇게 $w$라는 점을 정의하고, learning rate 와 iter num을 정의해준 뒤에 Loss를 미분한 값에 비례하여 점 $w$를 최적화해주면 된다.

그 후에 점을 plot을 이용해서 찍어주면, 다음과 같은 그림이 나오게 된다.

마지막에 기울기가 상당히 작아 거의 움직이지 않는 것을 볼 수 있는데, 추후 포스팅에서 다루겠지만 Gradient Descent 의 문제점 중 하나이다. 이를 위해 SGD 나 mini-batch GD 등을 채택하기도 하는데, 다음 포스트에서 다루어 보도록 하자.

여튼 이렇게 마지막에 최적화가 이루어진 $w$ 점의 x 좌표가 $m$ 이라는 기울기를 뜻한다고 볼 수 있다. 하지만 아직 작업이 모두 완료된 것은 아니다. $y = mx + b$ 라는 그래프에서 $y$ 와 $x$에 표준화를 진행했기 때문에, 다시 초기의 $(x, y)$ 쌍에 적용되는 $m$ 의 값을 역연산해야 한다.

$$\acute{m} = m \times \frac{\sigma(y)}{\sigma(x)}$$

그 식은 위와 같은데, 단순히 “배율”의 측면에서 살펴보면 이해하기 쉽다. 기존의 $y = mx + b$ 라는 식에서 $y$ 를 5배 줄이고, $x$ 를 2배 줄였다고 생각해보자. 그러면 $m$ 은 몇배가 줄어야 식을 만족할 수 있을까?

바로 $\frac{2}{5}$ 배가 되어야 식의 “배율” 을 만족시킬 수 있다. 이를 다시 변수로 일반화해보면, $x$ 와 $y$의 표준편차를 곱하고 나눈다는 것이 이해될 것이다. (이해가 안돼도 천천히 생각해보자..😭)

따라서 아래와 같은 함수가 마지막에 진행되고, 그래프를 그려줄 수 있다.

def calculate_m(m):
        return m * y_std / x_std
Python

Result

전체 코드는 다음과 같다.

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

def graph_linear_regression_mse(
    x,
    y,
    m_sclae=10,
    b_scale=10,
    sample_density=500,
):
    def mse(x,y,m,b):
        return torch.mean((y-(m*x+b))**2)
    
    def new_mse(x, y, m, b):
        ss = 0
        for xx, yy in zip(x, y):
            ss += (yy.item() - m * xx.item() - b)**2
        return ss / len(x)
    
    x_mean, y_mean = torch.mean(x), torch.mean(y)
    x_std, y_std = torch.std(x), torch.std(y)

    def standardization(x, y):
        return (x - x_mean) / x_std, (y - y_mean) / y_std
    
    def unstandardization(x, y):
        return (x * x_std) + x_mean, (y * y_std) + y_mean
    
    def calculate_m(m):
        return m * y_std / x_std

    x, y = standardization(x, y)
    
    w = torch.tensor([-5., 5.], requires_grad=True)
    learning_rate = 0.02
    num_iterations = 1000

    data = [[w[0].item(), w[1].item()]]

    for _ in trange(num_iterations):
        w.grad = None
        Loss = mse(x, y, w[0], w[1])
        Loss.backward()

        w.data -= learning_rate * w.grad
        data.append([w[0].item(), w[1].item()])

    data = np.array(data)
    x_data = torch.tensor(data[:, 0])
    y_data = torch.tensor(data[:, 1])
    z_data = np.array([new_mse(x, y, xx, yy) for xx, yy in zip(x_data, y_data)])
    
    fig = plt.figure()
    ax = fig.add_subplot(111, projection='3d')
    ax.dist = 11

    m = np.linspace(-5, 5, 100)
    b = np.linspace(-5, 5, 100)
    X, Y = np.meshgrid(m, b)

    Z = new_mse(x, y, X, Y)

    # 면 그래프
    ax.plot_surface(X, Y, Z, cmap=plt.get_cmap("GnBu"))

    # 등고선
    # ax.contourf(X,Y,Z, zdir='z', offset=0, cmap=plt.cm.summer)

    # 점 찍기
    for xx, yy, zz in zip(x_data, y_data, z_data):
        ax.plot(xx, yy, zz, color='orange', marker='*', zorder=3, markersize=1.5,)

    plt.show()

    answer = data[-1]
    m = calculate_m(answer[0]).item()
    b = (y_mean - m * x_mean).item()

    x, y = unstandardization(x, y)

    linear_regression_x = np.linspace(min(x), max(x), 100)
    linear_regression_y = m * linear_regression_x + b

    plt.plot(linear_regression_x, linear_regression_y)
    plt.scatter(x, y, c='r')

d = torch.tensor([[20, 6.5], [25, 7.7], [30, 9.2], [35, 12.8], [40, 14.3]])
graph_linear_regression_mse(d[:, :1], d[:, 1:])
Python

성공적으로 Linear regression 이 적용된 것을 볼 수 있다 !!

“AI study – week 2” 에 하나의 답글

  1. dobry sklep 아바타

    Wow, amazing blog format! How long have you ever been running a blog for?
    you made running a blog glance easy. The overall look of your website is wonderful,
    as neatly as the content! You can see similar here najlepszy sklep

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다