<jupyter_start><jupyter_text>2022/01/10/MON >  `회귀` 회귀 분석은 데이터 값이 평균과 같은 일정한 값으로 돌아가려는 경향으르 이용한 통계학 기법이다. 회귀는 여러 개의 독립 변수와 한 개의 종속 변수 간의 상관관계를 모델링하는 기법을 통칭한다. 예를 들면, 아파트의 방 개수, 방 크기, 주변 학군, 등 여러 개의 독립변수에 따라 아파트 가격이라는 종속변수가 어떤 관계를 나타내는지를 모델링하고 예측하는 것이다. $Y = W_1*X_1 + W_2*X_2 + W_3*X_3 + \dots + W_n*X_n$ 이라는 선형 회귀식을 예로 들면 $Y$는 종속변수, 즉 아파트 가격을 의미하며, 나머지 $X_1,X_2,X_3,\dots,X_n$은 방 개수, 방 크기, 주변 학군 등의 독립 변수를 의미한다. 그리고 $W_1,W_2,W_3,\dots,W_n$은 이 독립변수의 값에 영향을 미치는 회귀 계수이다. 머신 러닝의 관점에서 보면 독립변수는 feature에 해당하며 종속변수는 결정 값, 즉 레이블을 의미한다. 머신러닝 회귀 예측의 핵심은 주어진 feature와 결정 값 데이터 기반에서 학습을 통해 최적의 회귀계수를 찾아내는 것이다.  >  `회귀 종류` 독립 변수의 개수가 1개이면 단일 회귀, 여러 개이면 다중 회귀이다. 또한 회귀계수의 결합이 선형이면 선형 회귀, 비선형이면 비선형 회귀이다.  - `지도학습`은 두 가지 유형으로 나뉘는데 바로 `분류`와 `회귀`이다. 이 두가지 기법의 가장 큰 차이는 분류는 예측값이 카테고리와 같은 이산형 클래스 값이고 회귀는 연속형 숫자값이라는 것이다.- 여러 가지 회귀 중에서 선형 회귀가 가장 많이 사용된다. 선형 회귀는 실제 값과 예측값의 차이(오류의 제곱 값)를 최소화하는 직선형 회귀선을 최적화하는 방식이다. 선형 회귀 모델은 규제 방법에 따라 또 나눌 수 있다. 여기서 규제란 일반적인 선형 회귀의 과적합 문제를 해결하기 위해서 회귀 계수에 페널티 값을 적용하는 것을 의미한다.  --- - 단순 선형 회귀에 대해 알아보자 : 독립 변수도 하나 종속 변수도 하나인 선형 회귀를 의미한. 예를 들면 주택 가격이 주택의 크기로만 결정되는 것. - 예측값 $\hat{Y}$는 $w_0 + w_1*X$로 계산할 수 있다. 독립변수가 1개인 단순 선형 회귀에서는 이 기울기 $w_1$과 절편 $w_0$을 회귀 계수로 지칭한다. 그리고 회귀 모델을 $\hat{Y} = w_0 + w_1*X$와 같은 1차 함수로 모델링했다면 실제 주택 가격은 이러한 1차 함수 값에서 실제 값만큼의 오류 값을 뺀 또는 더한 값이 된다. ($w_0 + w_1*X +$ 오류값)- 이렇게 실제 값과 회귀 모델의 차이에 따른 오류 값을 남은 오류, 즉 잔차라고 부른다. 최적의 회귀 모델을 만든다는 것이 바로 전체 데이터의 잔차 합이 최소가 되는 모델을 만든다는 것이며 상쇄될 것을 고려해 대개 절댓값을 취하거나 제곱을 한 뒤 오류 합을 구한다. $Error^2 = RSS $- RSS는 비용이며 $w$변수(회귀 계수)로 구성되는 $RSS$를 비용 함수라고 한다. 머신 러닝 회귀 알고리즘은 데이터를 계속 학습하면서 이 비용 함수가 반환하는 값(즉, 오류값)을 지속해서 감소시키고 최종적으로는 더 이상 감소하지 않는 최소의 오류값을 구하는 것이다. 비용함수를 손실함수라고도 한다. --- >  `경사하강법` 점진적으로 반복적인 계산을 통해 $W$ 파라미터 값을 업데이트하면서 오류 값이 최소가 되는 $W$ 파라미터를 구하는 방식이다. 경사 하강법은 반복적으로 비용 함수의 반환 값 즉, 예측값과 실제 값의 차이가 작아지는 방향성을 가지고 $W$ 파라미터를 지속해서 보정해 나간다. 오류를 감소시키는 방향으로 $W$값을 계속 업데이트해 나가면서 더 이상 그 오류 값이 작아지지 않으면 그 오류 값을 최소 비용으로 판단하고 그때의 $W$ 값을 최적 파라미터로 반환한다.  - 예를 들어 비용 함수가 포물선 형태의 2차 함수라면 경사 하강법은 최초 $w$에서부터 미분을 적용한 뒤 이 미분 값이 계속 감소하는 방향으로 순차적으로 $w$를 업데이트한다. 마침내 더 이상 미분된 1차 함수의 기울기가 감소하지 않는 지점을 비용 함수가 최소인 지점으로 간주하고 그때의 $w$를 반환한다.  - 경사 하강법을 파이썬 코드로 구현해보자<jupyter_code>import numpy as np
import matplotlib.pyplot as plt

np.random.seed(0)
# y = 4X + 6 식을 근사(w1=4, w0=6). random 값은 Noise를 위해 만듬
X = 2 * np.random.rand(100,1) # X의 범위 설정
y = 6 + 4 * X + np.random.randn(100,1)

# X, y 데이터 셋 scatter plot으로 시각화
plt.scatter(X, y)
# w1 과 w0 를 업데이트 할 w1_update, w0_update를 반환. 
def get_weight_updates(w1, w0, X, y, learning_rate=0.01):
    N = len(y)
    # 먼저 w1_update, w0_update를 각각 w1, w0의 shape와 동일한 크기를 가진 0 값으로 초기화
    w1_update = np.zeros_like(w1)
    w0_update = np.zeros_like(w0)
    # 예측 배열 계산하고 예측과 실제 값의 차이 계산
    y_pred = np.dot(X, w1.T) + w0
    diff = y-y_pred
         
    # w0_update를 dot 행렬 연산으로 구하기 위해 모두 1값을 가진 행렬 생성 
    w0_factors = np.ones((N,1))

    # w1과 w0을 업데이트할 w1_update와 w0_update 계산
    w1_update = -(2/N)*learning_rate*(np.dot(X.T, diff))
    w0_update = -(2/N)*learning_rate*(np.dot(w0_factors.T, diff))    
    
    return w1_update, w0_update
# 입력 인자 iters로 주어진 횟수만큼 반복적으로 w1과 w0를 업데이트 적용함. 
def gradient_descent_steps(X, y, iters=10000):
    # w0와 w1을 모두 0으로 초기화. 
    w0 = np.zeros((1,1))
    w1 = np.zeros((1,1))
    
    # 인자로 주어진 iters 만큼 반복적으로 get_weight_updates() 호출하여 w1, w0 업데이트 수행. 
    for ind in range(iters):
        w1_update, w0_update = get_weight_updates(w1, w0, X, y, learning_rate=0.01)
        w1 = w1 - w1_update
        w0 = w0 - w0_update
              
    return w1, w0
def get_cost(y, y_pred):
    N = len(y) 
    cost = np.sum(np.square(y - y_pred))/N
    return cost

w1, w0 = gradient_descent_steps(X, y, iters=1000)
print("w1:{0:.3f} w0:{1:.3f}".format(w1[0,0], w0[0,0]))
y_pred = w1[0,0] * X + w0
print('Gradient Descent Total Cost:{0:.4f}'.format(get_cost(y, y_pred)))<jupyter_output>w1:4.022 w0:6.162
Gradient Descent Total Cost:0.9935<jupyter_text>y_pred에 기반해 회귀선을 그려보자<jupyter_code>plt.scatter(X, y)
plt.plot(X,y_pred)<jupyter_output><empty_output><jupyter_text>--- - 경사 하강법을 이용해 회귀선이 잘 만들어졌음을 알 수 있다. 일반적으로 경사 하강법은 모든 학습 데이터에 대해 반복적으로 비용함수 최소화를 위한 값을 업데이트하기 때문에 수행 시간이 매우 오래 걸린다. 이 때문에 실전에서는 확률적 경사 하강법(Stochastic Gradient Descent)을 이용한다. 확률적 경사 하강법을 일부 데이터만 이용해 w가 업데이트되는 값을 계산하므로 경사하강법에 비해 빠른 속도를 보장한다. 해보자.<jupyter_code>def stochastic_gradient_descent_steps(X, y, batch_size=10, iters=1000):
    w0 = np.zeros((1,1))
    w1 = np.zeros((1,1))
    prev_cost = 100000
    iter_index =0
    
    for ind in range(iters):
        np.random.seed(ind)
        # 전체 X, y 데이터에서 랜덤하게 batch_size만큼 데이터 추출하여 sample_X, sample_y로 저장
        stochastic_random_index = np.random.permutation(X.shape[0])
        sample_X = X[stochastic_random_index[0:batch_size]]
        sample_y = y[stochastic_random_index[0:batch_size]]
        # 랜덤하게 batch_size만큼 추출된 데이터 기반으로 w1_update, w0_update 계산 후 업데이트
        w1_update, w0_update = get_weight_updates(w1, w0, sample_X, sample_y, learning_rate=0.01)
        w1 = w1 - w1_update
        w0 = w0 - w0_update
    
    return w1, w0<jupyter_output><empty_output><jupyter_text>만들어진 함수를 이용해 w1,w0 및 예측 오류 비용을 계산해 보자<jupyter_code>w1, w0 = stochastic_gradient_descent_steps(X, y, iters=1000)
print("w1:",round(w1[0,0],3),"w0:",round(w0[0,0],3))
y_pred = w1[0,0] * X + w0
print('Stochastic Gradient Descent Total Cost:{0:.4f}'.format(get_cost(y, y_pred)))<jupyter_output>w1: 4.028 w0: 6.156
Stochastic Gradient Descent Total Cost:0.9937<jupyter_text>- 확률적 경사 하강법으로 구한 w0,w1 결과는 경사 하강법으로 구한 w1,w0과 큰 차이가 없으며 예측 오류 비용 또한 경사하강법으로 구한 것보다 아주 조금 높다. 즉 성능 차이가 미미하다. 데이터가 크면 확률적 경사 하강법을 이용하라.  --- >  `사이킷런 LinearRegression을 이용한 보스턴 주택 가격 예측` LinearRegression 클래스는 예측값과 실제 값의 RSS를 최소화 해 OLS 추정 방식으로 구현한 클래스이다. - OLS 기반의 회귀 계수 계산은 입력 feature의 독립성에 많은 영향을 받는다. feature간의 상관관계가 매우 높은 경우 분산이 매우 커져서 오류에 매우 민감해진다. 이러한 현상을 다중공선성(multi-collinearity)문제라고 한다. 일반적으로 상관관계가 높은 feature가 많은 경우 독립적인 중요한 feature만 남기고 제거하거나 규제를 적용한다. 또한 매우 많은 feature가 다중 공선성 문제를 가지고 있다면 PCA를 통해 차원 축소를 수행하는 것도 고려해 볼 수 있다.  - 회귀를 위한 평가 지표 :  실제 값과 회귀 예측값의 차이 값을 기반으로 한 지표가 중심이다.  - 305p 참고!<jupyter_code>import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
from scipy import stats
from sklearn.datasets import load_boston
%matplotlib inline

# boston 데이타셋 로드
boston = load_boston()

# boston 데이타셋 DataFrame 변환 
bostonDF = pd.DataFrame(boston.data , columns = boston.feature_names)

# boston dataset의 target array는 주택 가격임. 이를 PRICE 컬럼으로 DataFrame에 추가함. 
bostonDF['PRICE'] = boston.target
print('Boston 데이타셋 크기 :',bostonDF.shape)
bostonDF.head()<jupyter_output>Boston 데이타셋 크기 : (506, 14)<jupyter_text>다음으로 각 column이 회귀 결과에 미치는 영향이 어느 정도인지 시각화해서 알아보자. 총 8개의 칼럼에 대해 값이 증가할수록 PRICE값이 어떻게 변화하는지 확인하자.<jupyter_code># 2개의 행과 4개의 열을 가진 subplots를 이용. axs는 4x2개의 ax를 가짐.
fig, axs = plt.subplots(figsize=(16,8) , ncols=4 , nrows=2)
lm_features = ['RM','ZN','INDUS','NOX','AGE','PTRATIO','LSTAT','RAD']
for i , feature in enumerate(lm_features):
    row = int(i/4)
    col = i%4
    # 시본의 regplot을 이용해 산점도와 선형 회귀 직선을 함께 표현
    sns.regplot(x=feature , y='PRICE',data=bostonDF , ax=axs[row][col])<jupyter_output><empty_output><jupyter_text>- 다른 칼럼보다 RM과 LSTAT의 PRICE 영향도가 가장 두드러지게 나타난다. RM은 양 방향의 선형성이 가장 크다. 즉 방의 크기가 클수록 방의 가격이 증가하는 모습을 확연히 보여준다.<jupyter_code>from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error , r2_score

y_target = bostonDF['PRICE']
X_data = bostonDF.drop(['PRICE'],axis=1,inplace=False)

X_train , X_test , y_train , y_test = train_test_split(X_data , y_target ,test_size=0.3, random_state=156)

# Linear Regression OLS로 학습/예측/평가 수행. 
lr = LinearRegression()
lr.fit(X_train ,y_train )
y_preds = lr.predict(X_test)
mse = mean_squared_error(y_test, y_preds)
rmse = np.sqrt(mse)

print('MSE : {0:.3f} , RMSE : {1:.3F}'.format(mse , rmse))
print('Variance score : {0:.3f}'.format(r2_score(y_test, y_preds)))
print('절편 값:',lr.intercept_)
print('회귀 계수값:', np.round(lr.coef_, 1))<jupyter_output>절편 값: 40.99559517216445
회귀 계수값: [ -0.1   0.1   0.    3.  -19.8   3.4   0.   -1.7   0.4  -0.   -0.9   0.
  -0.6]<jupyter_text>coef_ 속성은 회귀 계수 값만 가지고 있으므로 이를 feature별 회귀 계수 값으로 다시 mapping하고 높은 값 순으로 출력해보자. 이를 위해 pandas Series의 sort_values() 함수를 이용한다.<jupyter_code># 회귀 계수를 큰 값 순으로 정렬하기 위해 Series로 생성. index가 컬럼명에 유의
coeff = pd.Series(data=np.round(lr.coef_, 1), index=X_data.columns )
coeff.sort_values(ascending=False)
from sklearn.model_selection import cross_val_score

y_target = bostonDF['PRICE']
X_data = bostonDF.drop(['PRICE'],axis=1,inplace=False)
lr = LinearRegression()

# cross_val_score( )로 5 Fold 셋으로 MSE 를 구한 뒤 이를 기반으로 다시  RMSE 구함. 
neg_mse_scores = cross_val_score(lr, X_data, y_target, scoring="neg_mean_squared_error", cv = 5)
rmse_scores  = np.sqrt(-1 * neg_mse_scores)
avg_rmse = np.mean(rmse_scores)

# cross_val_score(scoring="neg_mean_squared_error")로 반환된 값은 모두 음수 
print(' 5 folds 의 개별 Negative MSE scores: ', np.round(neg_mse_scores, 2))
print(' 5 folds 의 개별 RMSE scores : ', np.round(rmse_scores, 2))
print(' 5 folds 의 평균 RMSE : {0:.3f} '.format(avg_rmse))<jupyter_output>5 folds 의 개별 Negative MSE scores:  [-12.46 -26.05 -33.07 -80.76 -33.31]
 5 folds 의 개별 RMSE scores :  [3.53 5.1  5.75 8.99 5.77]
 5 folds 의 평균 RMSE : 5.829
{'fraction_non_alphanumeric': 0.10901969317810901, 'fraction_numerical': 0.04003916875204004, 'mean_word_length': 3.5370187561697928, 'pattern_counts': {'":': 0, '<': 28, '<?xml version=': 0, '>': 32, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '8', 'dir': 'jupyter-structured-clean-dedup', 'id': '587943', 'license': 'Apache-2.0', 'n_tokens_mistral': 6336, 'n_tokens_neox': 7636, 'n_words': 1304, 'path': '_notebooks/2022-01-10-intro.ipynb', 'repo_name': 'rhkrehtjd/INTROml', 'size': '9191'}