선형 회귀 (Linear Regression)
우선 집 크기가 주어졌을 때, 집 값을 예측한다고 해보자.
이때 선형 회귀는 여기 있는 데이터를 가장 잘 대변해 주는 선을 찾아내는 것이다. 이 데이터에 가장 잘 맞는, 가장 적절한 하나의 선을 찾아내는 거다.
통계학에서는 최적선, 영어로는 line of best fit이라고도 한다.
위의 이미지와 같이 데이터에 잘 맞는 최적선을 찾았다고 가정해보자. 그럼 이걸 어떻게 활용할 수 있을까?
아래 이미지의 최적선을 보면 50평 집 가격은 20억으로, 30평인 집은 약 10억 5천만원이라고 볼 수 있다.
이처럼 정확하진 않더라도 꽤나 합리적인 예측을 할 수 있다.
변수
위의 예에서 우리가 맞추려고 하는 '집 가격'은 '목표 변수(target variable)' 또는 output variable이라고 한다. 편하게 '아웃풋'이라고도 부른다.
또 이 목표 변수를 맞추기 위해 사용하는 값을 '입력 변수(inpuut variable)'이라고 하며 '인풋' 이라고도 한다. 일반적으로는 feature라고 하는데 특징, 속성 등을 의미한다.
가설 함수(hypothesis function)
선형 회귀에서 최적선을 찾기 위해 다양한 함수를 시도하는데, 이때 함수 하나하나를 '가설 함수(hypothesis function)'라고 한다.
우리가 찾고자 하는 선은 곡선이 아닌 직선이므로, 일차 함수이기 때문에 y=ax+b의 형태로 표현된다. 즉, 선형 회귀의 목적은 계수 a와 상수 b를 찾아내는 것이다.
가설 함수 표현법
집 가격을 예측할 때, 집의 크기 외에도 다양한 요소들이 영향을 끼칠 수 있다. 그러면 입력 변수가 늘어나게 되는데 a, b, c, d 식으로 적게 되면 헷갈릴 우려가 있어 θ를 사용하여 일관성있게 표현한다.
즉, 선형 회귀의 임무는 가장 적절한 이 세타값들을 찾아내는 것이다.
평균 제곱 오차(MSE)
선형 회귀에서 가설 함수가 얼마나 좋은지 평가하는 방법 중 가장 많이 쓰이는 방법은 평균 제곱 오차(mean squared error)인데, 앞 글자만 따서 MSE라고도 한다.
평균 제곱 오차는 데이터들과 가설 함수가 평균적으로 얼마나 떨어져 있는지 나타내기 위한 하나의 방식이다.
아래 이미지를 보면 최적선의 실제 값과 가설 함수가 예측하는 값의 차이가 나는 것을 볼 수 있다.
가령 집의 크기 47평을 가설 함수에 넣으면 집 값이 18.8억으로 예측되나 실제 가겨겨은 22억이다. 여기서 오차는 예측 값에서 원래 값을 뺀 -3.2이다. 마찬가지로 집의 크기 39평을 가설 함수에 넣었을 때 가격은 15.6억이나 실제 가격은 9억이다. 이때의 오차는 6.6억이 발생하는 것을 알 수 있다.
MSE에서는 이 오차값들을 모두 제곱한 후에 모두 더하고 나서 평균을 내기 위해 총 데이터 개수 만큼 나누게 된다.
정리하자면 평균 제곱 오차가 크다는 건 가설 함수와 데이터들 간의 오차가 크다는 거고, 결국 그 가설 함수는 데이터들을 잘 표현해 내지 못한 것으로 볼 수 있다.
여기서
제곱을 하는 이유
에 대해 알아보자.
1. 오차 값을 계산할 때 양수든 음수든 동일하게 취급해야 한다. 이때, 제곱을 하면 음수도 양수로 바뀌므로 모두 양수로 통일할 수 있다.
2. 또, 오차가 커질수록 더 부각시키기 위함이다. 만약 오차가 2면 제곱 시 4이고, 10이면 제곱 시 100이다. 오차는 8밖에 차이나지 않지만 제곱을 하게 되면 96이 차이가 나게 된다. 더 큰 오차에 대해서는 더 큰 패널티를 주기 위해 제곱을 하는 것이다.
위의 평균 제곱 오차를 일반화하여 수학적으로 표현하면 아래와 같다.
손실 함수(Loss Function)
손실 함수는 어떤 가설 함수를 평가하기 위한 함수다.
손실 함수의 아웃풋이 작을수록 가설 함수의 손실이 적기 때문에 더 좋은 가설 함수라고 할 수 있고, 반대로 손실 함수의 아웃풋이 클수록 가설 함수의 손실이 큰 거기 때문에 더 나쁜 가설 함수라고 할 수 있다.
손실 함수는 보통 J라는 문자를 쓰며, 선형 회귀의 경우에는 평균 제곱 오차가 손실 함수의 아웃풋이다.
특정 가설 함수의 평균 제곱 오차가 크면 이 손실 함수의 아웃풋이 크며 이는 손실이 크기 때문에 안 좋은 가설 함수라고 본다. 반대로 가설 함수의 평균 제곱 오차가 작으면 이 손실 함수의 아웃풋이 작은 것이며 손실이 적기 때문에 좋은 가설 함수인 것이다.
참고로 위 식에서 분모의 2m은 계산의 용이성을 위해 m에서 2m으로 바뀐 것이다.
또, J의 인풋은 θ값으로 되어 있는 것을 알 수 있다.
가설 함수에서 바꿀 수 있는 건 세타 값들이다. 이를 조절하여 적합한 가설 함수를 찾아 내는 것이므로 손실 함수의 아웃풋은 이 세타 값들을 어떻게 설정하느냐에 따라 달려 있는 것이다. 그래서 손실 함수의 인풋이 θ값인 것이다.
반면 x와 y는 변수처럼 보이지만 사실상 정해진 데이터를 대입하는 것이기 때문에 손실 함수에서는 '상수'라고 할 수 있다.
경사 하강법(Gradient Descent)
선형 회귀에서 해야 하는 건 데이터에 가장 잘 맞는 가설 함수를 찾아내는 것이다.
가설 함수는 아래 이미지와 같은데, θ0과 θ1을 조금씩 조율하면서 가설 함수가 최적선이 되도록 하는 것이다.
이때 가설 함수를 최적선이 되게끔 개선하려면 가설 함수를 평가하는 기준이 있어야 한다.
그 기준이 되는 것이 바로 '손실 함수'이다.
손실 함수는 인풋이 θ값인데, θ값들을 어떻게 설정해 주느냐에 따라 손실 함수의 아웃풋이 바뀐다. 그렇다면 손실 함수의 아웃풋이 최소가 되도록 해야 한다.
이 손실 함수의 아웃풋이 최소가 되기 위해서 필요한 개념이 바로 경사 하강법(Gadient Descent)이다.
경사 하강법(Gadient Descent)
경사의 방향과 크기를 이용해서 극소점을 향해 내려가는 방식이기 때문에 '경사 하강법'이라고 부른다.
선형 회귀에서 경사 하강법 공식을 풀면 아래와 같이 된다.
라고 정의하고 벡터 x의 평균을 μ x로 나타내면 아래와 같이 표현할 수 있다.
예제
1. 가설 함수를 사용해서 주어진 데이터를 예측하는 코드를 구현해 보자
import numpy as np
# prediction 함수: 주어진 가설 함수로 얻은 결과를 리턴하는 함수
# 파라미터로는 숫자형 변수(theta_0, theta_1), 모든 입력 변수x들을 나타내는 numpy배열 x
def prediction(theta_0, theta_1, x):
"""주어진 학습 데이터 벡터 x에 대해서 예측 값을 리턴하는 함수"""
return theta_0 + theta_1 * x
# 테스트 코드
# 입력 변수(집 크기) 초기화 (모든 집 평수 데이터를 1/10 크기로 줄임)
house_size = np.array([0.9, 1.4, 2, 2.1, 2.6, 3.3, 3.35, 3.9, 4.4, 4.7, 5.2, 5.75, 6.7, 6.9])
theta_0 = -3
theta_1 = 2
prediction(theta_0, theta_1, house_size)
# 출력값
# array([ -1.2, -0.2, 1. , 1.2, 2.2, 3.6, 3.7, 4.8, 5.8,
6.4, 7.4, 8.5, 10.4, 10.8])
2. 아래 이미지의 예측 값들과 실제 목표 변수들의 차이를 구하는 함수를 구현해보자.
import numpy as np
def prediction(theta_0, theta_1, x):
"""주어진 학습 데이터 벡터 x에 대해서 모든 예측 값을 벡터로 리턴하는 함수"""
return theta_0 + theta_1 * x
# prediction_difference함수는 선형 회귀 구현하기 쉽게 표현하기 레슨에서 본 error 처럼 모든 데이터의 예측 값과 실제 목표 변수의 차이를 리턴
# 파라미터로: θ0 -> theta_0,θ1 -> theta_1, 입력 변수 벡터 -> x, 목표 변수 벡터 -> y
def prediction_difference(theta_0, theta_1, x, y):
"""모든 예측 값들과 목표 변수들의 오차를 벡터로 리턴해주는 함수"""
return prediction(theta_0, theta_1, x) - y
# 입력 변수(집 크기) 초기화 (모든 집 평수 데이터를 1/10 크기로 줄임)
house_size = np.array([0.9, 1.4, 2, 2.1, 2.6, 3.3, 3.35, 3.9, 4.4, 4.7, 5.2, 5.75, 6.7, 6.9])
# 목표 변수(집 가격) 초기화 (모든 집 값 데이터를 1/10 크기로 줄임)
house_price = np.array([0.3, 0.75, 0.45, 1.1, 1.45, 0.9, 1.8, 0.9, 1.5, 2.2, 1.75, 2.3, 2.49, 2.6])
theta_0 = -3
theta_1 = 2
prediction_difference(-3, 2, house_size, house_price)
# 출력값
# array([-1.5 , -0.95, 0.55, 0.1 , 0.75, 2.7 , 1.9 , 3.9 , 4.3 ,
4.2 , 5.65, 6.2 , 7.91, 8.2 ])
3. 선형 회귀 경사 하강법을 직접 구현해보자.
import numpy as np
def prediction(theta_0, theta_1, x):
"""주어진 학습 데이터 벡터 x에 대해서 모든 예측 값을 벡터로 리턴하는 함수"""
return theta_0 + theta_1 * x
def prediction_difference(theta_0, theta_1, x, y):
"""모든 예측 값들과 목표 변수들의 오차를 벡터로 리턴해주는 함수"""
return prediction(theta_0, theta_1, x) - y
# 함수 gradient_descent는 실제 경사 하강법을 구현하는 함수
# 파라미터로는 임의의 값을 갖는 파라미터들 theta_0, theta_1, 입력 변수 x, 목표 변수 y, 경사 하강법을 몇 번을 하는지를 나타내는 변수 iterations, 그리고 학습률 alpha를 가짐
# 처음에 gradient_descent 함수에 넘겨주는 theta_0, theta_1 변수들은 0 또는 임의의 값들이고, gradient_descent 함수는 경사 하강법을 이용해서 최적의 theta_0, theta_1 값들을 찾아서 리턴
def gradient_descent(theta_0, theta_1, x, y, iterations, alpha):
"""주어진 theta_0, theta_1 변수들을 경사 하강를 하면서 업데이트 해주는 함수"""
for _ in range(iterations): # 정해진 번만큼 경사 하강을 한다
error = prediction_difference(theta_0, theta_1, x, y) # 예측값들과 입력 변수들의 오차를 계산
theta_0 = theta_0 - alpha * error.mean()
theta_1 = theta_1 - alpha * (error * x).mean()
# error * x에서 행렬 사이에 * 연산자를 사용하면 원소별 곱하기
return theta_0, theta_1
# 입력 변수(집 크기) 초기화 (모든 집 평수 데이터를 1/10 크기로 줄임)
house_size = np.array([0.9, 1.4, 2, 2.1, 2.6, 3.3, 3.35, 3.9, 4.4, 4.7, 5.2, 5.75, 6.7, 6.9])
# 목표 변수(집 가격) 초기화 (모든 집 값 데이터를 1/10 크기로 줄임)
house_price = np.array([0.3, 0.75, 0.45, 1.1, 1.45, 0.9, 1.8, 0.9, 1.5, 2.2, 1.75, 2.3, 2.49, 2.6])
# theta 값들 초기화 (아무 값이나 시작함)
theta_0 = 2.5
theta_1 = 0
# 학습률 0.1로 200번 경사 하강
theta_0, theta_1 = gradient_descent(theta_0, theta_1, house_size, house_price, 200, 0.1)
theta_0, theta_1
# 출력 결과
# (0.16821801417752186, 0.34380324023511988)
경사하강법을 사용해서 최적화된 theta_0과 theta_1가 리턴된 걸 확인할 수 있다.
경사 하강법 구현 시각화
import numpy as np
import matplotlib.pyplot as plt
def predction(theta_0, theta_1, x):
return theta_0 + theta_1*x
def predction_difference(theta_0, theta_1, x, y):
return predction(theta_0, theta_1, x) - y
def gradient_descent(theta_0, theta_1, x, y, num_iterations, alpha):
m = len(x)
cost_list = []
for i in range(num_iterations):
error = predction_difference(theta_0, theta_1, x, y)
cost = (error@error) / (2*m)
cost_list.append(cost)
theta_0 = theta_0 - alpha*error.mean()
theta_1 = theta_1 - alpha * (error * x).mean()
if i % 10 == 0: # 10번마다 1회씩 그려지도록 함
plt.scatter(house_size, house_price)
plt.plot(house_size, predction(theta_0, theta_1, house_size), color='red')
plt.show()
return theta_0, theta_1, cost_list
house_size = np.array([0.9, 1.4, 2, 2.1, 2.6, 3.3, 3.35, 3.9, 4.4, 4.7, 5.2, 5.75, 6.7, 6.9])
house_price = np.array([0.3, 0.75, 0.45, 1.1, 1.45, 0.9, 1.8, 0.9, 1.5, 2.2, 1.75, 2.3, 2.49, 2.6])
# theta 값들 초기화 (아무 값이나 시작함)
th_0 = 2.5
th_1 = 0
# 학습률 0.1로 200번 경사 하강
th_0, th_1, cost_list = gradient_descent(th_0, th_1, house_size, house_price, 200, 0.1)
학습률 알파
경사 하강법을 하기 위해서는 두 변수, θ값들을 아래처럼 계속 업데이트 하면 된다.
학습률 알파를 잘 못 고를 때 생기는 문제점에 대해서 알아보자.
좀 더 이해를 간단하게 하기 위해서 이렇게 손실 함수 J 가 하나의 변수, θ로만 이뤄졌다고 가정한다.
학습률 α가 너무 큰 경우
알파가 크면 클수록 경사 하강을 한 번을 할 때마다 θ의 값이 많이 바뀐다. 그럼 아래 이미지와 같이 왼쪽과 오른쪽으로 성큼성큼 왔다갔다 하면서 진행이 된다. 심지어 α가 너무 크면 경사 하강법을 진행할수록 손실 함수 J의 최소점에서 멀어질 수도 있다.
학습률 α가 너무 작은 경우
반대로 알파가 작으면 θ가 계속 엄청 찔끔찔끔 움직이게 된다. 그럼 최소 지점을 찾는 게 너무 오래 걸리게 된다. 1분 만에 할 수 있는 작업이 5 분 10 분, 또는 그거보다 더 오래 걸릴 수도 있게 되는 것이다.
적절한 학습률
그렇기 때문에 알파를 적당한 크기로 정하는 게 중요하다. 빠르고 정확하게 최소점까지 도달하는 학습률이 가장 좋다고 할 수 있다.
가장 “적절한” 학습률은 상황과 문제에 따라 다르다.
경사 하강을 하면서 손실이 줄어들고 있는 걸 아래 그래프로 확인했었는데, 이는 일부러 적절한 학습률을 골랐기 때문이다.
학습률이 너무 크면 경사 하강법을 할수록 손실 그래프가 계속 커지고, 작을 때는 이렇게 iteration수가 너무 많아집니다.
일반적으로 1.0 ~ 0.0 사이의 숫자로 정하고(1,0.1, 0.01, 0.001 또는 0.5, 0.05, 0.005 이런 식으로), 여러 개를 실험해보면서 경사 하강을 제일 적게 하면서 손실이 잘 줄어드는 학습률을 선택한다.
실습
scikit-learn을 사용해서 선형 회귀를 직접 연습해 볼게요.
이번 과제에서는 한번 동네의 범죄율, CRIM을 사용해서 선형 회귀를 해보도록 하겠습니다.
1. 범죄율 열을 선택
2. training-test set 나누기
3. 모델을 학습
4. test 데이터로 예측
조건
-입력 변수로는 범죄율 열만 이용하세요.
-train_test_split 함수의 옵셔널 파라미터는 test_size=0.2, random_state=5 이렇게 설정해주세요.
-예측 값 벡터 변수 이름은 꼭 y_test_predict를 쓰세요!
-정답 확인은 모델의 성능으로 합니다. (템플렛 가장 아래 줄에 출력 코드 있음)
# 필요한 라이브러리 import
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
import pandas as pd
# 보스턴 집 데이터 갖고 오기
boston_house_dataset = datasets.load_boston()
# 입력 변수를 사용하기 편하게 pandas dataframe으로 변환
X = pd.DataFrame(boston_house_dataset.data, columns=boston_house_dataset.feature_names)
# 목표 변수를 사용하기 편하게 pandas dataframe으로 변환
y = pd.DataFrame(boston_house_dataset.target, columns=['MEDV'])
X = X[['CRIM']] # 범죄율 열만 사용
# train_test_split를 사용해서 주어진 데이터를 학습, 테스트 데이터로 나눈다
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state=5)
linear_regression_model = LinearRegression() # 선형 회귀 모델을 가지고 오고
linear_regression_model.fit(X_train, y_train) # 학습 데이터를 이용해서 모델을 학습 시킨다
y_test_predict = linear_regression_model.predict(X_test) # 학습시킨 모델로 예측
# 평균 제곱 오차의 루트를 통해서 테스트 데이터에서의 모델 성능 판단
mse = mean_squared_error(y_test, y_test_predict)
mse ** 0.5