내용
경사하강법(Descent Gradient)
경사하강법이란?
최소자승법은 공식화된 계산식에 의해 결정된 회귀계수를 적용합니다. 이에 반면에 경사하강법은 수치적으로 모델을 최적화하는 과정에서 회귀계수를 산출합니다.
식 1의 선형회귀모형은 독립변수(x)와 종속변수(y)의 사이의 다음의 선형식을 만족하는 최적의 회귀계수를 결정하는 것입니다.
$$\begin{align}\tag{1}&y=xW\\&W:\text{회귀계수(가중치)}\end{align}$$예를 들어 다음의 자료는 y=2x를 만족하는 인위적으로 생성한 데이터입니다.
import numpy as np import pandas as pd import matplotlib.pyplot as plt import sympy as sp
x=np.arange(0, 10).reshape(-1,1) y=x*2 x.shape, y.shape
((10, 1), (10, 1))
np.c_[x, y]
array([[ 0, 0], [ 1, 2], [ 2, 4], [ 3, 6], [ 4, 8], [ 5, 10], [ 6, 12], [ 7, 14], [ 8, 16], [ 9, 18]])
역으로 위 자료의 선형회귀 모델을 생성한다고 할 경우 y=xw로부터 식 2와 같이 계산되는 mse가 최소가 되는 방식으로 W를 결정해야 합니다.
$$\begin{align}\tag{2}\text{mse}&=\frac{\sum^n_{i=1}(\hat{y}-y)^2}{n}\\&=\frac{\sum^n_{i=1}(xW-y)^2}{n} \end{align}$$W를 임의적으로 적용할 경우 mse의 변화는 그림 1과 같이 2차 곡선의 형태를 나타냅니다.
plt.figure(dpi=100) coef=np.linspace(-7, 11, 1000)#회귀계수의 변화 mse=[np.sum((x*i-y)**2)/len(y) for i in coef] plt.plot(rng, mse) plt.xlabel("Coeff(W)", size="14") plt.ylabel("MSE", size="14") plt.show()
경사하강법 알고리즘
최소자승법의 경우는 mse의 최소값이 되는 지점에서 회귀계수를 결정하지만 경사하강법의 경우는 임의적으로 지정한 회귀계수의 초기값으로부터 이 값의 변화에 의해 도달하는 mse의 최소로 되는 지점을 발견하며 그 지점에서 회귀계수를 결정하는 방법입니다. 이 방법은 수치해석에 의한 것으로 최소자승법의 여러 전제조건에서 자유로운 분석 방법입니다.
이 방법은 다음의 과정으로 이루어집니다.
- 초기 회귀모형 생성(회귀계수, 편차를 지정합니다.)
- 그 모형에 대한 MSE를 계산합니다. 이것은 모형에 대한 비용함수(cost function) 또는 손실함수(loss function)이라고 합니다.
- 회귀계수에 대해 비용함수 변화율을 고려하여 새로운 계수를 계산합니다.
- 비용함수가 극소점에 도달할 때까지 또는 변화가 없을 때까지 1 ∼ 3번 과정을 반복합니다. 이 과정을 최적화(optimization)라 합니다.
그림 2에서 나타낸 것과 같이 회귀 계수에 대한 비용 변화율은 미분, $\displaystyle \frac{d(cost)}{dw}$,으로 계산됩니다. 회귀 계수에 대해 이 결과를 다시 고려하는 과정을 반복하여 최적화된 값을 도출합니다.
np.random.seed(1) x=np.linspace(-4, 4, 100) y0=0.5*x w=np.random.rand(1) mse=np.sum((w*x-0.2-y0)**2)/len(x) dmse_x=np.sum(2*(w*x-0.2-y0)*w)/len(x) mset=[float(mse)] wt=[float(w)] for i in range(20): w=w-0.03*np.sum(2*(w*x-0.2-y0)*w)/len(x) wt.append(float(w)) mset.append(float(np.sum((w*x-0.2-y0)**2)/len(x))) plt.figure(dpi=100) plt.scatter(wt, mset, color="red") plt.plot(wt, mset, label="Cost Function") plt.legend(loc="best") plt.xlabel("Weight(W)", size=12, weight='bold') plt.ylabel("Cost(mse)", size=12, weight='bold') plt.annotate('w', xy=(wt[4]+0.01, mset[4]), xytext=(wt[1]+0.01, mset[1]), arrowprops=dict(facecolor='navy', width=1)) plt.text(0.44, 0.070, r"$\mathbf{w=w-\eta \frac{d(cost)}{dx}}$") plt.show()
위 과정을 경사하강법(Gradient Descent)이라고 하며 그 알고리즘을 수식으로 전개하면 다음과 같습니다(식 5.3 ∼ 7). 그림 2에서 나타낸 것과 같이 초기의 w, b를 랜덤하게 지정하는 것으로 시작하며 w, b를 감소시키면서 손실(loss)의 변화를 확인합니다. 그러한 변화 동안 최소의 손실이 생성되는 지점에서 w, b를 모수로 하여 모형을 설정합니다.
식 3은 경사하강법의 기본 특성치인 비용함수를 나타낸 것입니다.
$$\begin{align}\tag{3}&\epsilon=\hat{y}-y \\&\begin{aligned}\text{cost}&=\frac{\sum^n_{i=1}(\epsilon)^2}{n}\\&=\frac{\epsilon^T \cdot \epsilon}{n} \end{aligned}\\ &\epsilon: \text{error} \end{align}$$가중치의 수정을 위해 비용함수는 식 4와 같이 각 가중치들과 편차에 의해 미분됩니다.
$$\begin{align}\tag{4}\text{cost}&=\frac{\sum^n_{i=1}(\hat{y}-y)^2}{n}\\&=\frac{\sum^n_{i=1}(wx-b-y)^2}{n}\frac{d(cost)}{dw}\\&=\frac{\sum^n_{i=1}2(wx-b-y)x}{n}\frac{d(cost)}{db}\\&=\frac{\sum^n_{i=1}2(wx-b-y)}{n} \end{align}$$식 4에서 비용함수의 미분 결과를 행렬연산으로 나타내면 식 5와 같습니다.
$$\begin{align}\tag{5}\frac{d(cost)}{dw}&=\frac{2(wx-b-y)x}{n}\\&=\left(2 \times \begin{bmatrix}\epsilon_1&\epsilon_2&\cdots&\epsilon_n\end{bmatrix} \cdot \begin{bmatrix}x_1\\x_2\\ \vdots\\x_n\end{bmatrix}\right)\frac{1}{n}\\\frac{d(cost)}{db}&=\frac{2(wx-b-y)}{n}\\&=\left(2 \times \begin{bmatrix}\epsilon_1&\epsilon_2&\cdots&\epsilon_n\end{bmatrix} \cdot \begin{bmatrix}1\\1\\\vdots\\1\end{bmatrix}\right)\frac{1}{n} \end{align}$$식 5의 두 계산은 식 6과 같이 결합하여 나타낼 수 있습니다.
$$\begin{equation}\tag{6}d(cost)=\left(2 \times \begin{bmatrix}\epsilon_1&\epsilon_2&\cdots&\epsilon_n\end{bmatrix} \cdot \begin{bmatrix}x_1&1\\x_2&1\\\vdots&\vdots\\x_n&1\end{bmatrix}\right) \frac{1}{n}\end{equation}$$식 6의 결과에 의한 각 가중치들과 편차의 개선 과정을 최적화(optimization)이라 하며 식 7과 같이 이루어집니다.
$$\begin{align}\tag{7}&w= w- \varUpsilon \frac{d(cost)}{dw} & \varUpsilon : \text{학습률(learning rate)} \end{align}$$그림 5와 같이 최적화 과정은 각 가중치의 미분 결과에 의존됩니다. 즉, 그 결과가 크다면 최적화의 단계 간 간격이 증가하여 극소점을 발견할 확률이 매우 낮아집니다. 이를 방지하기 위해 최적화 단계의 간격(최적화 속도)을 조절할 필요가 있습니다. 그 조절인자를 학습률(learning rate, η)라고 합니다. 이러한 최적화 과정을 학습(train)과정이라하며 이 과정을 통해 회귀계수가 결정됩니다. 그러나 학습률은 학습과정의 시작과 함께 지정되는 변수입니다. 학습률과 같이 과정의 시작에 지정되는 변수를 초매개변수(hyper-parameter)라고 합니다.
예)
단순한 예로 y=2x를 따르는 자료에 대해 경사하강법을 적용하여 회귀계수 2를 산출해 봅니다.
x | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
y | 0 | 2 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 18 |
선형모델에 편차항을 첨가하기 위해 독립변수에 1로 이루어진 새로운 변수를 첨가해야 합니다.
x=np.arange(0, 10) x1=np.c_[np.ones(10), x] x1
array([[1., 0.], [1., 1.], [1., 2.], [1., 3.], [1., 4.], [1., 5.], [1., 6.], [1., 7.], [1., 8.], [1., 9.]])
y=np.array([2*i for i in x]) y
array([ 0, 2, 4, 6, 8, 10, 12, 14, 16, 18])
임의로 회귀계수와 편차의 초기값을 지정합니다.
np.random.seed(0) param=np.random.rand(2,1) param
array([[0.5488135 ], [0.71518937]])
최초로 생성한 모델에 의해 추정값을 계산합니다.
$$\begin{bmatrix}y_1\\y_2\\\vdots\\ y_n \end{bmatrix}=\begin{bmatrix}1&x_1\\1&x_2\\\vdots\\ 1&x_n \end{bmatrix}\begin{bmatrix}0.5488135\\0.71518937\end{bmatrix}$$#추정값 pre=np.dot(x1, param) pre
array([[0.5488135 ], [1.26400287], [1.97919224], [2.6943816 ], [3.40957097], [4.12476034], [4.8399497 ], [5.55513907], [6.27032843], [6.9855178 ]])
#오차 error=pre-y.reshape(-1,1) error
array([[ 0.5488135 ], [ -0.73599713], [ -2.02080776], [ -3.3056184 ], [ -4.59042903], [ -5.87523966], [ -7.1600503 ], [ -8.44486093], [ -9.72967157], [-11.0144822 ]])
위에서 산출한 오차를 기반으로 비용 즉, mse를 계산합니다.
cost=np.dot(error.T, error)/len(y) cost
array([[41.00114681]])
$\displaystyle \frac{d(\text{cost})}{dw}$와 $\displaystyle\frac{d(\text{cost})}{db}$ 를 계산합니다.
grad=2*np.dot(error.T,x1)/len(error) grad
array([[-10.46566869, -68.29488458]])
위의 미분된 값과 학습률을 고려하여 회귀계수와 편차를 업데이트 합니다.
param=param-0.01*grad.reshape(-1,1) param
array([[0.65347019], [1.39813821]])
sklearn 패키지의 SGDRegressor()
클래스는 위의 과정을 모두 수행합니다. 이 클래스에 의한 생성된 모델을 .fit(x, y)
메소드로 데이타에 적합시키고 결정계수는 .score(x, y)
메소드로 확인할 수 있습니다. 이 메소드에 전달되는 독립변수(x)는 2차원 numpy 배열객체, 반응변수(y)는 1차원 벡터이어야 합니다. 또한 회귀계수와 편차는 각각 .coef_, .intercept_
속성으로 계산할 수 있습니다.
x
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
from sklearn.linear_model import SGDRegressor x2=x.reshape(-1,1) reg=SGDRegressor(max_iter=1000, tol=1e-3, learning_rate='constant', eta0=0.01) reg1=reg.fit(x2, y) print(f'scroe:{np.around(reg1.score(x2, y),4)}, coef:{np.around(reg1.coef_,4)}, intercept:{np.around(reg1.intercept_,4)}')
scroe:0.9996, coef:[1.9651], intercept:[0.2208]
다음 금융자료로부터 1일전의 설명변수에의한 당일 kodex 반도체의 주가를 추정하기 위한 회귀모델을 구축해 봅니다.
설명변수 | 코스피지수(kos), 삼성전자(samE), sk하닉스(skny), ,kodex 반도체(ksemi)의 시가, 고가, 저가, 종가, 거래량의 1일간 변화율, 달러대 환율(kd)의 종가 |
---|---|
반응변수 | kodex 반도체(ksemi)의 시가, 고가, 저가, 종가 |
다음 코드로 자료를 호출하고 각 자료의 Volume의 일간변화율을 DataFrame.pct_change()
메소드를 적용하여 계산합니다.
import FinanceDataReader as fdr st=pd.Timestamp(2021,3, 1) et=pd.Timestamp(2022, 3, 3) nme={"kos":"KS11", 'sam':'005930', 'skny':'000660','ksemi':'091160'} nme2={'kd':"USD/KRW"} daOri={ } for i, j in zip(nme.keys(), nme.values()): x=fdr.DataReader(j,st, et) x2=x[["Open","High","Low","Close"]] x3=x["Volume"].pct_change()*100 #Volume의 일간변화율 daOri[i]=pd.concat([x2, x3], axis=1) daOri[i].columns=[i+ k for k in daOri[i].columns] #열이름 수정 daOri['kos'].head(2)
kosOpen | kosHigh | kosLow | kosClose | kosVolume | |
---|---|---|---|---|---|
Date | |||||
2021-03-02 | 3021.68 | 3096.50 | 3020.74 | 3043.87 | NaN |
2021-03-03 | 3041.20 | 3083.04 | 3029.37 | 3082.99 | 28.0 |
환율자료의 경우 위에서 호출한 자료와 구성에서 차이가 있습니다. 그러므로 별도로 호출하여 위 사전(dictionary)개체 daOri에 결합합니다.
daOri['kd']=fdr.DataReader(nme2['kd'],st, et)["Close"] daOri['kd'].head(2)
Date 2021-03-01 1113.33 2021-03-02 1125.01 Name: Close, dtype: float64
pd.concat()
함수를 작용하여 위의 객체 daOri를 DataFrame으로 전환하고 자료에 포함되어 있는 결측치 NaN을 삭제하여 자료를 정제합니다.DataFrame.dropna()
메소드를 적용합니다.
da=pd.DataFrame() for i in daOri.keys(): da=pd.concat([da, daOri[i]], axis=1) da=da.dropna() da.head(2).iloc[:,:4]
kosOpen | kosHigh | kosLow | kosClose | |
---|---|---|---|---|
Date | ||||
2021-03-03 | 3041.20 | 3083.04 | 3029.37 | 3082.99 |
2021-03-04 | 3076.88 | 3076.88 | 3022.54 | 3043.49 |
설명변수는 반응변수보다 1일 앞선 값들이라는 기준으로 설명과 반응변수를 분리합니다. 반응변수는 'ksemiClose'입니다.
ind=da.iloc[:-1, :].values de= da['ksemiClose'][1:].values new=da.iloc[-1,:].values de[:10]
array([35792., 35488., 34560., 34216., 33712., 34615., 35388., 35114., 36341., 36450.])
위에서 생성된 자료의 각 변수의 스케일(Scale)에 차이가 있으므로 표준화를 진행합니다. 최종 추정치 역시 표준화된 자료이므로 다시 원래의 스케일로 환원하기 위해서 표준화에 사용된 평균과 표준편차의 자료가 필요합니다. sklearn.preprocessing.StandardScaler()
클래스를 적용합니다.
from sklearn.preprocessing import StandardScaler
indScaler=StandardScaler().fit(ind) deScaler=StandardScaler().fit(de.reshape(-1,1)) XNorm=indScaler.transform(ind) yNorm=deScaler.transform(de.reshape(-1,1)) newNorm=indScaler.transform(new.reshape(1,-1)) newNorm
array([[-2.11123117, -2.11530059, -1.96686745, -1.96493631, 0.66249176, -1.29919369, -1.24298305, -1.15764714, -1.11639629, 0.06325032, 0.58655059, 0.58900825, 0.67240477, 0.69696735, -0.02196599, -0.16329146, -0.1660803 , 0.00832936, 0.00550933, 0.16539065, 1.57722766]])
SGDregressor()
클래스를 적용하여 회귀모델을 구현하고 그 모델에 의한 최종 추정치를 계산합니다.
from sklearn.linear_model import SGDRegressor reg=SGDRegressor(alpha=0.01, max_iter=10000,tol=1e-5, learning_rate='invscaling') reg1=reg.fit(XNorm, yNorm.ravel()) print(f'score:{np.around(reg1.score(XNorm, yNorm.ravel()), 4)}\ncoef:{np.around(reg1.coef_, 4)}\nindent intercept:{np.around(reg1.intercept_, 4)}')
score:0.9219 coef:[-0.0207 0.0131 0.0109 0.0398 -0.0283 -0.0919 -0.038 -0.0247 0.0221 -0.0008 -0.006 0.026 0.0402 0.0577 -0.0296 0.1024 0.2254 0.2581 0.3595 0.028 -0.0381] indent intercept:[0.]
pre=reg1.predict(newNorm) pre1=deScaler.inverse_transform(pre.reshape(-1,1)) pre1
array([[36503.10866093]])
위 모델에 의한 오차가 정규분포에 부합여부를 판단하고 신뢰구간을 결정할 수 있습니다.
from scipy import stats
err=de.reshape(-1,1)-deScaler.inverse_transform(reg1.predict(XNorm).reshape(-1,1)) stat=stats.probplot(np.array(err).ravel(), plot=plt)[1] print(f'회귀계수: {np.around(stat[0], 2)}, 편차: {np.around(stat[1], 2)}, 결정계수: {np.around(stat[2], 2)}')
회귀계수: 561.95, 편차: -0.08, 결정계수: 1.0
위 결과에 의하면 이 모델에 의해 생성된 오차는 정규성을 가지고 있으므로 t분포로 신뢰구간을 추정할 수 있습니다. 이 자료의 경우 전체 샘플 수는 260개 이상으로 t분포 대신 정규분포를 적용합니다. 유의수준 0.05에서 오차의 신뢰구간을 위에서 산출한 추정치에 적용하여 추정치의 신뢰구간을 계산합니다.
ci=stats.norm.interval(0.95, err.mean(), pd.DataFrame(err).sem(ddof=ind.shape[1])) print(f'하한: {np.around(ci[0], 3)}, 상한 :{np.around(ci[1], 3)}')
하한: [-72.608], 상한 :[72.439]
np.around(pd.DataFrame(np.r_[pre1+ci[0], pre1, pre1+ci[1]], index=['하한','평균', '상한']),0)
0 | |
---|---|
하한 | 36431.0 |
평균 | 36503.0 |
상한 | 36576.0 |
위 모델은 ksemiClose를 반응변수로 한 것입니다. 반응변수를 ksemi의 다른 변수들로 전환하기 위해서 위 과정 중 설명변수와 반응변수 분리 단계를 수정하고 나머지 과정은 동일하게 실행됩니다. 이를 위해 다음과 같이 코드를 변형시킬 수 있습니다.
score=[] Pre=pd.DataFrame() for i in ['ksemiOpen','ksemiHigh','ksemiLow','ksemiClose']: ind=da.iloc[:-1, :].values de= da[i][1:].values new=da.iloc[-1,:].values indScaler=StandardScaler().fit(ind) deScaler=StandardScaler().fit(de.reshape(-1,1)) XNorm=indScaler.transform(ind) yNorm=deScaler.transform(de.reshape(-1,1)) newNorm=indScaler.transform(new.reshape(1,-1)) mod=SGDRegressor(alpha=0.01, max_iter=10000, tol=1e-5, learning_rate='invscaling').fit(XNorm, yNorm.ravel()) score.append(np.around(reg.score(XNorm, yNorm.ravel()), 3)) pre=reg1.predict(newNorm) pre1=deScaler.inverse_transform(pre.reshape(-1,1)) ci=stats.norm.interval(0.95, err.mean(), pd.DataFrame(err).sem(ddof=ind.shape[1])) re=pd.DataFrame(np.r_[pre1+ci[0], pre1, pre1+ci[1]], index=['하한','평균', '상한']) Pre=pd.concat([Pre, re], axis=1) Pre=np.around(Pre, 0) Pre.columns=['Open',"High",'Low',"Close"] Pre
Open | High | Low | Close | |
---|---|---|---|---|
하한 | 36471.0 | 36768.0 | 36102.0 | 36431.0 |
평균 | 36543.0 | 36841.0 | 36174.0 | 36503.0 |
상한 | 36616.0 | 36914.0 | 36247.0 | 36576.0 |
댓글
댓글 쓰기