한가지 독립 변수와 그에 대응되는 종속 변수와의 관계를 밝히는 단순 선형 회귀 알고리즘을 학습합니다. 사이킷런(Sklearn) 라이브러리를 사용하여 실습을 진행하고, 최소 제곱법을 손실 함수로 차용하여 해결하는 방법 하나와 확률적 경사하강법(SGD)을 적용시켜 모델을 학습하는 방법 두가지를 살펴봅시다. 또한 경사하강법의 개념에 대해서도 공부해봅시다.
단순 선형회귀
사이킷런
최소제곱법(OLS)
잔차 제곱의 합(RSS)
경사하강법
확률적 경사하강법(SGD)
지난시간에 공부했던 회귀의 개념을 이어 설명하자면, 선형 회귀에는 몇가지 종류가 더 있습니다.
단순 선형회귀는 여기서 일반 선형회귀에 속하는 모델입니다. 독립변수(X)와 종속변수(y)가 하나씩 존재합니다. 예를 들면, 공부시간(X)과 점수(y)의 관계 등을 모델링 할때 사용할 수 있습니다.
X와 y의 관계를 나타낸 좌표평면에 데이터가 흩뿌려져 있다고 해봅시다. 데이터들을 가장 잘 나타내는 직선 하나를 찾아내는 문제를 선형 회귀라고 할때, 데이터의 실제 값과 모델의 예측 값은 분명 차이가 있을 것입니다. 이러한 차이를 통해 모델의 오차를 구할 수 있고, 이 오차를 줄이는 방식으로 모델을 훈련할 수 있습니다.
RSS(Residual Sum of Squares)는 실제 값과 예측 값 사이의 제곱의 합을 의미합니다. 잔차 제곱의 합이 클수록 모델의 예측 값이 부정확하다는 의미이므로, 이 값(RSS)을 가장 적게 갖는 모델을 만드는 것이 중요합니다.
최소제곱법(OLS;Ordinary Least Squares)란 이런 잔차 제곱의 합을 최소로 하는 직선을 찾는 방법입니다. 최소제곱법은 노이즈가 적은 자료를 훈련시킬때 유리하지만, 반대로 말하면 노이즈(극단적인 값들)에 매우 취약하다는 특징이 있습니다.
사이킷런 라이브러리를 통해 최소제곱법으로 선형 회귀를 모델을 만들어봅시다. 공부 시간과 점수와의 관계를 나타낸 CSV 자료를 불러와 실습을 진행하고, 훈련 세트(80%)와 테스트 세트(20%)를 분리하는 과정을 거칩니다.
import matplotlib.pyplot as plt #데이터 시각화
import pandas as pd #CSV 데이터 불러와 활용
dataset = pd.read_csv('LinearRegressionData.csv')
dataset.head()
CSV 파일의 데이터가 판다스 라이브러리를 통해 분석 환경으로 잘 불러졌음을 확인할 수 있습니다.
X = dataset.iloc[:, :-1].values #독립변수
y = dataset.iloc[:, -1].values #종속변수
독립변수(X)에는 데이터에서 공부 시간(hour)에 해당하는 부분을 넣어주고, 종속변수(y)에는 데이터에서 점수(score)에 해당하는 부분을 넣어줍니다.
from sklearn.model_selection import train_test_split #데이터 세트 분리
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
모델에서 학습을 위한 데이터(훈련 데이터)와 모델이 잘 학습되었는지 확인하기 위한 데이터(테스트 데이터)를 구분합니다. 사이킷런 라이브러리의 train_test_split()
메서드를 이용합니다.
X_train, len(X_train)
테스트 데이터의 크기를 0.2로 정했으므로 16개의 훈련 데이터가 2차원 배열에 잘 저장되어있는 것을 확인할 수 있습니다.
X_test, len(X_test)
테스트 데이터의 크기를 0.2로 정했음으로 전체 데이터 20개 중, 4개의 데이터가 2차원 배열로 잘 저장되어 있습니다.
from sklearn.linear_model import LinearRegression #선형 회귀 모델 생성(OLS)
reg = LinearRegression()
사이킷런 라이브러리에서 LinearRegression()
을 import해서 사용하면, OLS 방법을 통해 자동으로 모델을 만들어줍니다.
reg.fit(X_train, y_train) #훈련 세트로 학습
>> LinearRegression()
훈련 세트를 사용하여 모델의 학습을 진행합니다. 종속 변수와 독립 변수를 fit()
메서드 안에 잘 담습니다. 다음의 과정을 통해 모델의 생성과 훈련이 끝났습니다.
reg.predict(X_train)
reg.predict(X)
를 통해 X에 대한 예측값을 확인할 수 있습니다. X는 2차원 배열 형태로 입력되어야 합니다.
reg.coef_
>> array([10.49161294])
reg.intercept_
>> 0.6115562905169796
reg.coef_
는 만들어진 선형 모델의 기울기를 의미합니다. reg.intercept_
는 선형 모델의 y절편을 의미합니다.
plt.scatter(X_train, y_train, color='blue') #산점도 확인
plt.plot(X_train, reg.predict(X_train), color='green') #선그래프
plt.title('Score by hours(train data)') #제목
plt.xlabel('hours') #X축 이름
plt.ylabel('score') #y축 이름
plt.show()
맷플롯립 라이브러리를 통해 만들어진 모델(Linear Model)과 훈련 데이터를 확인해봅시다. 훈련 데이터를 기반으로 상단 그림과 같은 선이 만들어진 것입니다.
plot
메서드를 통해 선 그리기# plot() 함수에 두 개의 리스트를 입력하면 순서대로 x, y 값들로 인식 import matplotlib.pyplot as plt plt.plot([1, 2, 3, 4], [2, 3, 5, 10]) # (1, 2), (2, 3), ... plt.show()
마지막으로, 테스트 세트를 활용한 검증과 모델 평가를 해봅시다.
#테스트 세트는 학습할때 사용하지 않았음
plt.scatter(X_test, y_test, color='blue') #산점도
plt.plot(X_train, reg.predict(X_train), color='green') #선그래프
plt.title('Score by hours(train data)') #제목
plt.xlabel('hours') #X축 이름
plt.ylabel('score') #y축 이름
plt.show()
상단 그래프에서 녹색 선은 훈련 세트로 만든 선형 모델입니다. 훈련에 사용되지 않은 테스트 세트가 네개의 점으로 표시되어있습니다.
reg.score(X_test, y_test) #테스트 세트를 통한 모델 평가
>> 0.9727616474310156
reg.score(X_train, y_train) #훈련 세트를 통한 모델 평가
>> 0.9356663661221668
score()
메서드로 모델을 평가할 수 있습니다. 원래 훈련 세트를 통한 모델 평가 정확도가 더 높은 경향을 보이지만(해당 세트로 훈련시켰기 때문) 오늘 데이터가 워낙 작은 크기로 학습이 진행되어 상단 코드 결과와 같은 양상을 띄게 되었습니다.
모델의 예측값과 정답값의 차이를 통해 선을 구할 수 있었습니다. 하지만 실제 선형회귀를 RSS 방법으로만 구하기란 어려운 일입니다. 모델의 독립 변수(파라미터)가 많을 수도 있고, 그렇게 되면 손실 함수를 RSS 처럼 간단하게 나타낼 수 없을 수도 있습니다. 또한 RSS 방법은 노이즈의 영향을 많이 받아 부정확할 수도 있습니다. 이런 상황에서 경사하강법을 도입할 수가 있겠습니다.
독립 변수로 구성된 손실 함수에서 최솟값을 찾기 위해 사용하는 방법으로, 함수의 기울기(경사)를 구하고 경사의 반대 방향으로 계속 이동시켜 극값에 이를 때까지 반복시키는 것입니다.
왼쪽의 그림이 선형 모델의 매개변수를 통해(기울기:m, y절편:b) 만든 손실 함수이고, 이 손실 함수에서 가장 낮은 지점을 찾을 수 있다면, 그 지점의 매개 변수들은 손실 함수의 값을 가장 낮추는 변수들일 것입니다. 즉, 그 지점에서의 m과 b값이 가장 정확한 선형 회귀 모델을 만드는데 사용될 수 있는 것입니다.
손실 함수를 2차 함수로 가정하고, 경사하강법을 설명해보겠습니다. 선형 모델의 파라미터가 하나만 있을 것이라고 가정하는 것입니다. 우리의 목표는 손실 함수의 값을 가장 적게 갖는 가중치(wi)의 값을 찾아내는 것입니다.
우선 w1에 대한 시작점을 선택합니다. 우리가 가정한 손실 함수는 매끈한 모양의 2차 함수라서 시작점이 그다지 중요하지 않습니다. 어디를 시작점으로 선택해도, 극소값으로 도착하기 때문입니다. 하지만 실제 손실 함수는 여러개의 파라미터가 변수로써 구성되고, 매우 복잡하므로, 시작점을 잘 정하는 것이 매우 중요할 수 있습니다. global minimum이 아니라 local minimum에 빠져버릴 수도 있기 때문입니다.
선택한 시작점에서 손실 곡선의 기울기(Gradient)를 계산합니다. 여기서 기울기를 계산하는 방식으로 편미분이 활용됩니다. 손실 곡선이 다양한 파라미터를 변수로 하여 구성되는 경우, cost function을 우리가 관심 있는 단일 가중치에 대해 편미분 하여 방향을 찾아낼 것이기 때문입니다. 편미분계수(기울기)가 다음 가중치 후보로의 방향을 결정하고, Learning Rate(학습률;step size)가 이동할 보폭을 결정합니다. 위의 방식을 반복하면 최소값에 점점 가까워집니다.
▣학습률(Learning Rate; StepSize)
- 학습률이 너무 작을 경우 : 알고리즘이 수렴하기 위해 반복해야 하는 값이 많으므로 학습시간이 오래걸림. 지역 최소값(local minimum)에 수렴할 수 있음.
- 학습률이 너무 클 경우 : 학습 시간은 적게 걸림. 스텝이 너무 커서 전역 최소값(global minimum)을 가로질러 반대편으로 건너뛰어 최소값에서 멀어질 수 있음.
경사하강법은 한 지점에서 계속적으로 계산을 진행하여 극값을 찾아내는 방식입니다. 조금 더 풀어서 말하자면, 모든 데이터를 한번에 이용하여 cost function을 모델링 하고, 이 함수로 계속해서 경사하강법을 진행하는 것입니다. 하지만 이러한 방식은 학습 데이터가 큰 경우 부담이 있을 수 있습니다. 전체 데이터를 모두 사용해서 기울기를 계산해야 하기 때문입니다. 즉, Gradient Descent의 등고선이 일정한 보폭과 방향을 갖는 것은, 한가지 함수를 올려놓고 같은 학습률로 진행하기 때문입니다.
확률적 경사하강법은 매 step에서 딱 한개의 샘플을 무작위로 선택하고, 그 하나의 샘플에 대한 기울기를 계산합니다. 즉, 한가지 샘플로 cost function을 모델링하고, 이 샘플에 대한 경사하강법을 통해 한번의 움직임을 정합니다.
매 step마다 무작위로 선택하므로, 계속해서 새로운 cost function이 모델링되고, 매번 이동하는 보폭이 다릅니다. 즉, cost funciton이 local minimum에 이를 때까지 부드럽게 감소하지 않고 위아래로 요동치며 평균적으로 감소합니다.
에포크(epoch)는 확률적 경사 하강법에서 훈련세트를 한 번 모두 사용하는 과정을 말합니다. 에포크를 크게 하면 한번 훈련 세트를 사용하여 가중치를 찾아낸 후에도 가중치를 계속해서 탐색합니다.
에포크가 진행될 수록 훈련 세트의 정확도는 꾸준히 증가하게 됩니다. 하지만 에포크 횟수가 지나치게 많으면 훈련된 모델은 훈련 세트에 너무 잘 맞아 테스트 세트에는 오히려 점수가 나쁜 과대적합 모델일 가능성이 높습니다. 또한 에포크 횟수가 적으면 훈련된 모델은 훈련 세트와 테스트 세트에 잘 맞지 않은 과소적합된 모델일 가능성이 높습니다. 각각 에포크 과대적합과 에포크 과소 적합이라고 합니다.
사이킷런 SGDRegressor() 메서드는 손실 함수로 MSE를 사용하여 경사하강법을 진행합니다.
from sklearn.linear_model import SGDRegressor
sr = SGDRegressor(max_iter=1000, eta0=1e-4, random_state=0, verbose=1)
sr.fit(X_train, y_train)
SGDRegressor() 메서드를 통해 확률적 경사 하강법으로 선형 회귀 모델을 훈련시킬 수 있습니다. max_iter
옵션은 에포크를, eta0
옵션은 학습률을 결정합니다.
plt.scatter(X_train, y_train, color='blue') #산점도
plt.plot(X_train, sr.predict(X_train), color='green') #선그래프
plt.title('Score by hours(train data, SGD)') #제목
plt.xlabel('hours') #X축 이름
plt.ylabel('score') #y축 이름
plt.show()
sr.coef_, sr.intercept_
>> (array([10.19197471]), array([1.77603038]))
sr.score(X_test, y_test) #테스트 세트를 통한 모델 평가
>> 0.9748430825252619
sr.score(X_train, y_train) #훈련세트를 통한 모델 평가
>> 0.9347804758954794