본문 바로가기
AI/머신러닝

[머신러닝] 최소제곱법을 활용한 LinearRegression

by snow_white 2022. 4. 12.

학생들과 성적의 관계

학생들마다 다 다양한 성적 분포를 가지는데...여기에 어떤 연관이 있는지 알아내고그 연관 관계를 이용해서 결국에는 특정학생의 성적을 예측해보자.학생들의 기말고사 성적은 [ ]에 따라 다르다.

[ ]안에 시험성적을 좌우할 만한 요소들로 무엇이 있을까?여기서 [ ]안에 들어갈 내용을 '정보'라 한다. 머신러닝과 딥러닝은 이 정보가 필요하다. 정보를 정확히 준비해 놓기만 하면 성적을 예측하는 방정식을 만들수 있다.

이것을 수학적으로 정의하면, 성적을 변하게 하는 '정보' 요소를 X라 하고,이 값에 따라 변하는 '성적'을 Y라 한다.' X값이 변함에 따라 Y값도 변한다'는 정의 안에서 독립적으로 변할수 있는 값 X를 독립변수라 한다.

또한, 이 독립 변수에 따라 종속적으로 변하는 Y(성적)를 종속변수라 한다.

선형회귀는 독립변수 X(공부 시간)를 이용해서 종속변수 Y를 예측하고 설명하는 작업을 말한다.

 

예측선 그리기

기말고사를 준비하는 학생들을 대상으로 자료를 조사한 결과를 바탕으로 공부한 시간을 통해서 시험성적을 예측하자.

X = [2, 4, 6, 8]

Y = [81, 93, 91, 97]

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import platform
from matplotlib import font_manager, rc
plt.rcParams['axes.unicode_minus'] = False

if platform.system() == 'Darwin':
    rc('font', family='AppleGothic')
elif platform.system() == 'Windows':
    path = "c:/Windows/Fonts/malgun.ttf"
    font_name = font_manager.FontProperties(fname=path).get_name()
    rc('font', family=font_name)
else:
    print('Unknown system!!')
X = [2,4,6,8]
Y = [81,93,91,97]

plt.scatter(X, Y)

plt.xlabel('공부한 시간')
plt.ylabel('기말고사 성적')
plt.show()

왼쪽이 아래로 향하고 오른쪽이 위를 향하는 선형(직선으로 표시되는 형태의 그래프)을 보인다. 선형회귀는 이 점들의 특징을 가장 잘 나타내는 선을 그리는 과정이다. 여기서 선은 곧 일차함수 그래프이며 다음과 같은 공식으로 표현된다.y = ax + b여기서 x값은 독립변수이고 y값은 종속변수이다. 즉, x 값에 따라서 y값은 반드시 달라진다. 다만, 정확하게 계산하려면 상수 a와 b의 값을 알아야한다.

따라서 선형회귀문제는 결과적으로 a와 b값을 알아내는 문제이다.

 

최소제곱법

일차함수의 기울기 a와 절편 b를 구할수 있다 a = (x-x평균)(y-y평균)의 합 / (x-x평균)**의 합 b = y의 평균-(x의 평균x기울기a)

mx = np.mean(X)
my = np.mean(Y)

divisor = sum([(i-mx)**2 for i in X]) # 분모

def top(X,mx,Y,my):
    d = 0
    for i in range(len(X)):
        d +=(X[i] - mx) * (Y[i]- my)
    return d
dividend = top(X,mx,Y,my) # 분자

a = dividend/divisor

b = my-(mx*a)
print(a)
print(b)
'''
2.3
79.0
'''

 

예측 값 구하기

X는 numpy가 아니라 리스트 형태이기 때문에 형 변환 필수이다!

a = 2.3
b = 79
X = np.array(X)
Y = np.array(Y)
predict = a*X+b
predict # array([83.6, 88.2, 92.8, 97.4])

 

X 시간, Y 예측한 성적

plt.scatter(X, Y)

plt.plot(X, predict, c='r')
plt.xlabel('공부한 시간')
plt.ylabel('기말고사 성적')
plt.show()

 

평균 제곱 오차, MSE

평균 제곱 오차가 작다는 것은 실제 값과 예측 값 사이의 간극이 크지 않다는 것을 말한다.

앞으로 딥러닝과 머신러닝을 공부할 때 굉장히 많이 나오는 공식으로 이 값이 작으면 작을수록 예측한 값의 정확도가 높아짐을 의미한다. 선형 회귀란 임의의 직선을 그어서 이에 대한 평균 제곱 오차를 구하고 이 값을 가장 작게 만들어주는 a와 b값을 찾아가는 작업이다.

def mse(y_hat, y):
    return ((y_hat-y)**2).mean()

mes(predict,Y) # 8.299999999999985

MSE 실습하기

import numpy as np
data = [[2,81],[4,93],[6,91],[8,97]]
x = [i[0] for i in data]
y = [i[1] for i in data]
print(x)#[2, 4, 6, 8]
print(y)#[81, 93, 91, 97]

# 1. 임의로 a,b를 지정해본다.
temp_a_b = [3, 76]
# 2. 일차방정식 Y = aX + b 를 반환하는 함수 정의
def predict(x): # 예측값을 알아내는 함수
    return temp_a_b[0] * x + temp_a_b[1]
# 3. 평균제곱근(MSE)공식을 그대로 파이썬 함수로 정의.
def mse(y_hat, y):
    return ((y_hat-y)**2).mean()
def mse_val(predict_result, y):
    return mse(np.array(predict_result), np.array(y))

# 4. 공부시간에 따른 실제 점수와 예측한 점수를 출력
predict_result = [] #예측값이 들어갈 빈 리스트

for i in range(len(x)):   
    # 예측한 결과를 predict_result에 저장,, 출력해보자.
    predict_result.append(predict(x[i]))
    print("공부 시간=%.f, 실제점수=%.f, 예측점수=%.f" %(x[i], y[i], predict(x[i])))
'''
공부 시간=2, 실제점수=81, 예측점수=82
공부 시간=4, 실제점수=93, 예측점수=88
공부 시간=6, 실제점수=91, 예측점수=94
공부 시간=8, 실제점수=97, 예측점수=100
'''

# 5. MSE 평균제곱오차 출력
print('MSE 최종결과 : ', mse_val(predict_result,y)) # MSE 최종결과 :  11.0

 


최소제곱법 (Least Ordinary Squares)

최소제곱법, 또는 최소자승법, 최소제곱근사법, 최소자승근사법(method of least squares, least squares approximation)은 어떤 계의 해방정식을 근사적으로 구하는 방법으로, 근사적으로 구하려는 해와 실제 해의 오차의 제곱의 합이 최소가 되는 해를 구하는 방법이다.

잔차(Residual, Error)제곱의 합이 0에 가깝게 되도록 기울기(Weight)와 편향(bias)을 찾는다.

 

장점

  • 간단하게 식을 유도할 수 있다.

한계

  • 노이즈(outlier)에 취약하다.
  • 특징 변수와 샘플 건수에 비례해서 계산 비용이 높다.

 

RSS(Residual Sum of Square) 공식

실제 값(y)과 가설(𝑦̂ y^)에 의한 예측 값의 차이가 가장 작은 계수 계산

선형함수 : 𝑦=𝑤𝑥+𝑏일때,

 

샘플 데이터를 생성

import numpy as np
import matplotlib.pyplot as plt

# 50개의 X를 생성합니다.
x = np.arange(50)

# a=기울기, b=절편
a = 0.3
b = 0.8

# 선형회귀 식을 작성합니다. y 값을 산출합니다.
y = a * x + b

위의 수식에 근거하여 y 데이터 생성시 일직선으로 표현되는 단순한 선형함수가 완성되므로, 약간의 노이즈를 추가한다.

noise = np.random.uniform(-0.05, 0.05, size=y.shape)
# y 값에 노이즈를 추가합니다.
yy = y + noise
plt.figure(figsize=(8, 6))
plt.plot(x, y, color='r', label='y = 0.3*x + 0.8')
plt.scatter(x, yy, label='data')
plt.legend(fontsize=18)
plt.show()

약간의 노이즈 섞임

샘플 데이터 생성 코드를 함수로 만들기

 

(1) noise = 0.05

def make_linear(w=0.5, b=0.8, size=50, noise=0.05): 
    x = np.arange(size)
    y = w * x + b
    noise = np.random.uniform(-abs(noise), abs(noise), size=y.shape)
    yy = y + noise
    plt.figure(figsize=(10, 7))
    plt.plot(x, y, color='r', label=f'y = {w}*x + {b}')
    plt.scatter(x, yy, label='data')
    plt.legend(fontsize=20)
    plt.show()
    print(f'w: {w}, b: {b}')
    return x, yy
x, y = make_linear(size=50, w=0.3, b=0.8, noise=0.05)

(2) noise = 3.05

x, y = make_linear(size=50, w=0.3, b=0.8, noise=3.05) # 노이즈 많이 넣어줌!

 

Python 코드로 구현

최소제곱법 (Least Square) 공식

 

RSS(Residual Sum of Square)

실제 값(y)과 가설(𝑦̂ y^)에 의한 예측 값의 차이가 가장 작은 계수 계산

선형함수: 𝑦=𝑤𝑥+𝑏일때,

 

(1) noise = 0.05

x_bar(x 평균), y_bar (y 평균) 구하기

x_bar = x.mean()
y_bar = y.mean()

 

w의 계수 값 찾기

calculated_weight = ((x - x_bar) * (y - y_bar)).sum() / ((x - x_bar)**2).sum()
print('w: {:.2f}'.format(calculated_weight)) # w: 0.30

 

b의 계수 값 구현

calculated_bias = y_bar - calculated_weight * x_bar
print('b: {:.2f}'.format(calculated_bias)) # b: 0.80

 

(2) noise = 3.05

w의 계수 값 찾기

w 0.30에서 0.33으로 변화

calculated_weight = ((x - x_bar) * (y - y_bar)).sum() / ((x - x_bar)**2).sum()
print('w: {:.2f}'.format(calculated_weight)) # w: 0.33

 

b의 계수 값 구현

b 0.80에서 0.14으로 변화

calculated_bias = y_bar - calculated_weight * x_bar
print('b: {:.2f}'.format(calculated_bias)) # b: 0.14

 

 

최소제곱법은 노이즈에 취약하다는 단점이 있다.

이상치 값을 넣어 이를 직접 눈으로 확인해 보도록 하자.

calculated_bias = y_bar - calculated_weight * x_bar
print('b: {:.2f}'.format(calculated_bias))
x, y = make_linear(size=100, w=0.7, b=0.2, noise=0.05)

y[5]=60 # 이상치 값 넣음
y[10]=60 # 이상치 값 넣음

plt.figure(figsize=(10, 7))
plt.scatter(x, y)
plt.show()

x_bar = x.mean()
y_bar = y.mean()
calculated_weight = ((x - x_bar) * (y - y_bar)).sum() / ((x - x_bar)**2).sum()
calculated_bias = y_bar - calculated_weight * x_bar

print('w: {:.2f}, b: {:.2f}'.format(calculated_weight, calculated_bias)) # w: 0.64, b: 4.02

위의 결과에서도 볼 수 있듯이, outlier에 취약하다.

근본적으로 x, y의 평균 값을 활용하여 기울기 (w)와 절편 (b)를 구하게 될 때, outlier가 크게 영향을 끼치는 것을 볼 수 있다.

노이즈가 별로 없을 때는 w가 0.7이고, b가 0.2였지만 이상치 값을 넣으니  w가 0.64, b가 4.02로 변화되었다.


최소제곱법 (OLS)를 활용한 LinearRegression

scikit-learn 패키지의 LinearRegression이 바로 최소 제곱 추정 방식으로 회귀 예측 알고리즘

모델 안에서 X(feature)값을 학습시킬 때 input값은 무조건 2차원 형식으로 넣어줘야 한다!

ex) 1행 50열짜리(50, )를 50행 1열짜리(50, 1)로 만들어주기

from sklearn.linear_model import LinearRegression
x, y = make_linear(size=50, w=0.3, b=0.8, noise=0.05)
model = LinearRegression() # 최소제곱법으로 W와 b를 구하기

# x.shape # (50,)
# x.reshape(-1,1).shape # (50, 1)
model.fit(x.reshape(-1,1), y) # 학습,x가 1차원이었으므로 2차원으로 변형해줘야 함

 

  • model.coef_ : 가중치 W 조회(모델의 weight 값을 출력하는 내부 변수) 
  • model.intercept_ : bias 편향 값 조회
print('w', model.coef_, ' , b',model.intercept_) # w [0.30018062]  , b 0.7974324019389041

가중치가 배열 형태로 출력되는 이유?

W(weight)가 x1, x2, x3,... 값마다 각각 또 다른 weight 값이 곱해질 것이기 때문에 배열 형태로 나온다.

반면, bias는 절편, 상수값이기 때문에 하나뿐이다.

print('w:{:.2f}, b:{:.2f}'.format(model.coef_[0], model.intercept_)) # w:0.30, b:0.80

이상치 값으로 인해 평균값이 달라짐

댓글