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 이 적용된 것을 볼 수 있다 !!