반응형

 

 

#선형회귀 #경사하강법 #다항회귀 #데이콘

#LinearRegression #GradientDescent #Polynomial

 

 

 

머신러닝 모델 중 가장 기본이 되는 선형회귀(Linear Regression).

그에서 비롯된 경사하강법(Gradient Descent), 다항회귀(Polynomial Regression), 그리고 (다음 포스팅에서) 릿지, 라쏘, 엘라스틱넷에 대해서 정리해보려 한다.

(python의 scikit learn 라이브러리 활용 방법과 파라미터에 대한 간단한 이해 위주, 수식은 간단한 이해만)

 

 

선형회귀는 이해가 쉽고 직관적이라 특성과 모델 사이 인과관계 파악이 쉽다.

그 활용인 라쏘회귀 모델 특성 선택(feature selection)을 할 수 있다는 특징이 있다.

 

 

비교적 간단한 모델임에도 활용도가 높고, 특히 데이터 기반 의사결정 시 최종 판단은 사람이 한다는 점에서 인과 증명이 명확한 선형 모델을 선호할 수도 있을 것이다. (현업에서도 많이 쓰인다는 건 주워들은 말..)

 

 

 

 

참고한 데이터&코드

(데이콘 MIND 님의 코드 공유 전처리 과정 활용)

 
 

 

준비

sklearn 라이브러리의 linear_model 패키지 안에는 선형 모델 모듈들이 들어있다.

(다항 회귀를 사용하려면 preprocessing 패키지 안에 있는 PolynomialFeatures 모듈을 사용해 다항 특성을 추가해야 한다.)

 

아래와 같은 코드를 실행하여 패키지들을 추가하였다.

from sklearn.linear_model import LinearRegression
from sklearn.linear_model import SGDRegressor
from sklearn.linear_model import Ridge
from sklearn.linear_model import Lasso
from sklearn.linear_model import ElasticNet
from sklearn.preprocessing import StandardScaler
 

 

 

위 링크의 코드에 대해 아래 코드를 실행하여 훈련, 테스트 세트를 만들어 아래에 설명할 모델들에 사용했다.

# train_test_split으로 훈련, 테스트 세트 셔플
from sklearn.model_selection import train_test_split
y_target = tmp_train['등록차량수']
X_data = tmp_train.drop(['단지코드', '등록차량수'], axis=1, inplace=False)
X_train, X_test, y_train, y_test = train_test_split(X_data, y_target, shuffle=False, random_state=42, test_size=0.1)
 

 

전처리된 데이터 셋(tmp_train)은 다음과 같다.

 

 

 

 

 

 

 

 

선형회귀(Linear Regression)

 

선형회귀는 아래와 같은 식으로 표현할 수 있다.

 

y는 예측값, X는 특성벡터, theta는 가중치벡터인데,

X와 theta가 벡터가 아닌 1차원의 값이라고 한다면 가중치는 x의 계수인 일차함수 꼴이 될 것이다.

 

즉, 우리가 예측하려는 y가 X와 선형적인 관계라고 가정하고 가장 이상적인 theta를 구하는 알고리즘이다.

이상적인 theta는 비용 함수(loss function)을 설정하고 그것을 최소화하는 것을 의미하는데,

 

선형회귀의 비용 함수는 MSE(Mean Squared Error)이다.

(실제 타겟의 값(y)와 예측한 값(y, 정확히는 y hat)의 차이의 제곱의 평균 (편차의 제곱의 평균)을 의미한다.)

 

 

특성벡터 X와 타깃의 값들을 담고 있는 벡터 y를 가지고 MSE를 최소화하는 가중치벡터 theta를 바로 구할 수가 있는데,

 

위의 식이 성립함이 알려져 있다. (정규방정식)

 

정규방정식 계산을 위한 X^T * X 의 역행렬 계산복잡도는 일반적으로 아래와 같다. (n은 특성 수)

$\combi{O}\left(n^{2.4}\right)\ \sim \combi{\ O}\left(n^3\right)$O(n2.4) ~ O(n3)

따라서 특성 수에 계산 속도가 굉장히 민감하다.

 

 

하지만 특성의 스케일과는 무관하다. 스케일이 크다면 가중치가 작도록, 스케일이 작다면 가중치가 크도록 정규방정식이 계산될 것이기 때문이다.

 

 

아래 코드로 LinearRegression 모델을 학습해 테스트 세트에 대한 점수를 구해보았다.

# 선형회귀 모델 훈련 및 지표(MAE) 산출
from sklearn.linear_model import LinearRegression

model = LinearRegression()
model.fit(X_train, y_train)

print(f'MAE : {mean_absolu코드te_error(model.predict(X_test), y_test)}')
 

위에서 알 수 있듯이 LinearRegression 모듈은 파라미터가 없다.

정규방정식(정확히는 특잇값 분해라는 방법을 통한 유사역행렬 계산)을 통해 최적의(MSE를 최소화하는) 가중치 벡터 theta를 계산하기 때문에 별도의 파라미터가 존재하지 않는다.

 

 

실행 결과는 아래와 같다. (아래 모델과의 비교를 위한 결과 출력)

MAE : 106.34272775431835

 

 

 

 

 


 

경사하강법(Gradient Descent)

경사하강법은 가중치벡터 theta를 비용 함수의 현재 그레디언트가 감소하는 방향으로 움직이며 최적의 theta를 찾는 방법이다.

쉽게 말하자면, theta를 증가시킬 때 비용이 감소한다면 theta를 증가시키고, 반대라면 감소시키는 방법으로, 비용 함수가 최솟값에 도달할 때까지 theta를 수정한다.

 

 

실제 MSE를 최소화하는 정규방정식이란 식이 존재함에도 경사하강법을 사용하는 데에는 이유가 있다.

첫째는 계산 속도가 빠르다.

둘째는 외부 메모리 학습을 지원한다(확률적 경사하강법, 미니배치 경사하강법)

 

 

가장 중요한 점은, 경사하강법이 특성의 스케일에 민감하다는 점이다. 한 점에서 많은 그레디언트를 계산한다면, 특성의 스케일이 큰 값의 그레디언트에 민감하여 작은 값의 학습은 상대적으로 더딜 것이기 때문이다. 결국에는 최솟값에 도달하겠지만 시간이 매우 오래 걸린다.

 

따라서 아래 코드를 통해 스케일링을 진행한다.

scaler = StandardScaler()
# 훈련 데이터 만들기
tmp_train = df_train[['등록차량수', '단지코드', 'n지역', 'n공급유형', '공가수', '지하철', '버스', '단지내주차면수', '총임대세대수', '평균전용면적', '총전용면적', 'log_평균임대보증금', 'log_평균임대료']]
y_target = tmp_train['등록차량수']
X_data = tmp_train.drop(['단지코드', '등록차량수'], axis=1, inplace=False)
# 전체 scaled
X_data_scaled = scaler.fit_transform(X_data)
# train_test_split으로 훈련, 테스트 세트 셔플
X_train, X_test, y_train, y_test = train_test_split(X_data_scaled, y_target, shuffle=False, random_state=42, test_size=0.1)
 

 

 

자세한 설명은 생략하고, 경사하강법(SGDRegressor) 모델을 사용해 보자.

아래에서 사용한 확률적 경사하강법은 랜덤한 1개의 샘플을 추출해 그레디언트를 계산하는 방법이다.

from sklearn.linear_model import SGDRegressor
sgd_reg = SGDRegressor(max_iter=1000, tol=1e-3, penalty=None, eta0=0.1)
sgd_reg.fit(X_train, y_train.ravel())

print(f'MAE : {mean_absolute_error(sgd_reg.predict(X_test), y_test)}')
 

파라미터의 역할은 아래와 같다.

 

max_iter : 최대 학습 횟수. 해당 파라미터 초과 횟수로는 학습하지 않는다.

 

tol : 허용 오차. 해당 파라미터 값보다 적은 학습 오차를 보이는 경우 학습을 멈춘다. 즉, 학습 후에도 비용 함수가 해당 파라미터 값보다 적다면 최솟값과 아주 근접하다고 판단하고 학습을 멈춘다.

 

penalty : 규제 종류로, 해당 파라미터를 l1, l2 등으로 설정하면 경사하강법으로 근사한 릿지, 라쏘 회귀 등을 구현할 수 있다.

 

eta0 : 학습률을 결정하는 파라미터.

 

 

실행 결과는 다음과 같다.

MAE : 112.38517315046478

 

 

LinearRegressor가 MSE를 최소화하는 수학적 모델이긴 하지만, 경사하강법은 선형 회귀를 근사한 것이므로 MAE 점수도 약간 낮은 것을 확인할 수 있다.

 

 

 

 

 

 

 


 

다항 회귀(Polynomial Regression)

 

선형회귀 모델은 특성과 타겟의 선형관계를 가정한다. 따라서 특성과 타겟이 선형이 아닌 제곱관계, 로그관계 등이라면 선형회귀 모델은 과소적합될 것이다.

 

 

PolynomialFeatures 모듈을 사용하면, 존재하는 특성들의 다항 특성들을 생성해 준다.

예를 들어 [a, b, c]의 특성이 존재한다면

aa, ab, ac, bb, bc, cc 라는 특성을 기존 특성에 추가해 준다. (총 3개에서 9개의 특성을 가지게 됨)

# 다항 특성 추가
from sklearn.preprocessing import PolynomialFeatures
poly_features = PolynomialFeatures(degree=2, include_bias=False)
X_poly = poly_features.fit_transform(X_data_scaled)
 

(위 코드를 실행한 후, 포스팅 상단에 있는 훈련, 테스트 셔플 코드를 실행해야 한다.)

 

파라미터의 역할은 아래와 같다.

 

degree : 차수 (예를 들어 3이라면 각 특성을 2번, 3번 곱한 특성들을 모두 더해준다.)

 

include_bias : 편향 포함 여부(편향이란 일차함수의 상수 같은 개념으로, 특성에 영향받지 않고 더해주는 파라미터이다.)

 

다항 회귀는 다항 특성을 추가한 후 LinearRegression 모델을 사용하기 때문에, 마찬가지로 특성의 스케일과는 무관하다.

 

 

다항 특성을 추가한 데이터로 LinearRegression 모델을 학습해 보자.

model = LinearRegression()
model.fit(X_train, y_train)

print(f'MAE : {mean_absolute_error(model.predict(X_test), y_test)}')
 

MAE : 150.90253260297598

 

결과는 다항 특성을 추가하기 전보다 나빠졌다.

해당 데이터가 특성과 타겟 간 다항관계가 존재하지 않아 보여 그런 것 같다.

(특성 선택 알고리즘을 통해 확인할 수 있을 것이다.)

 

 

 

 

 

 

 

반응형
반응형

 

 

 

참고 데이터

데이콘 - 신용카드 사용자 연체 예측

 

 

전처리 전 데이터셋 (train.csv)

 

숫자 밖에 못 알아듣는 멍청한 모델 학습을 위해 'income_type'과 같은 범주형(category) 컬럼들을 Encoding 해야 한다.

 

 

 

 

 

 

 

 

더미형 변수(Dummy Variable)

import pandas as pd
train = pd.get_dummies(train)
train
 

'pandas' 라이브러리의 'get_dummies' method를 사용하면 더미형 변수로 쉽게 변환할 수 있다.

 

'income_type' 컬럼이 더미형 변수로 변환된 것을 확인할 수 있다.

위에서 확인할 수 있듯이, 더미형 변수는 범주형 변수를 숫자형 변수로 만들어준다.

 

 

 

 

 

 

 

숫자형 컬럼, 범주형 컬럼 분류 (Numeric, Categorical Column Classification)

numerical = train.dtypes[train.dtypes != "object"].index.tolist()
print("Number of Numerical features: ", len(numerical))

categorical = train.dtypes[train.dtypes == "object"].index.tolist()
print("Number of Categorical features: ", len(categorical))
 

print문을 실행하면 'tolist()' 함수에 의해 변환된 list의 길이가 출력된다. (list의 길이는 요소의 개수를 의미한다.)

 

 

 

categorical

 

 

 

 

 

 

 

 

인코딩 (Encoding)

 

위에서 더미형 변수로 변환한 것도 인코딩이다.

추가로 라벨 인코딩(Label Encoding)을 활용하는 방법을 정리한다.

 

라벨 인코딩은 범주의 개수(n)에 맞게 0~(n-1)을 할당해 주는 인코딩이다.

쉽게 말하면 0부터 번호를 매겨준다.

from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
le.fit(train.income_type)
train['income_type']=le.transform(train.income_type)
 

 

범주형 특성이었던 'income_type'을 Label Encoding한 결과는 아래와 같다.

인코딩하지 않은 'edu_type'과 대조된다.

 

 

위에서 활용한 범주형 특성 리스트 추출을 활용해, 모든 범주형 특성을 한 번에 인코딩해 보자.

from sklearn.preprocessing import LabelEncoder

categorical = train.dtypes[train.dtypes == "object"].index.tolist() #범주형 특성 리스트 저장
le = LabelEncoder() #LabelEncoder 모듈을 'le'객체에 저장

for category in categorical :
    le.fit(train[category])
    train[category]=le.transform(train[category])
 

 

 

 

 

 

 

 

 

 

 

반응형
반응형

 

 

 

 

 

데이터 시각화 --> 인사이트 고안 --> 전처리 --> 모델 선정 및 학습

 

 

Visualization & Pre-processing : 공유된 코드 이해에 주안점을 둠

Analysis : 나름의 인사이트를 적용해보고자 함

Model : 인사이트의 직관적 적용을 파악하고자 Linear Regression(선형 회귀) 모델 사용

 

 

참고한 코드 : https://dacon.io/competitions/official/235745/codeshare/3017?page=1&dtype=recent

대회 3위를 수상하신 'MIND' 님께서 데이콘에 공유해주신 코드로 공부했다.

대부분 코드를 참고했지만, 특히 전처리 부분은 모든 부분을 활용했다.

 

 

 

 

 

 

 

 

 

대회 개요

아파트 신축 단지의 주차 수요를 예측하는 모델을 작성한다.

현행되는 인력조사로 과대 또는 과소 산정의 가능성이 생겨, 학습을 통한 모델로 정확한 예측을 가능케 하고자 함.

 

 

 

 

 

준비

# 폰트 변경
from matplotlib import pyplot as plt
plt.rc('font', family='NanumBarunGothic') 
 
# Colab 환경에서 네이버 나눔 폰트 설치
## 설치 후 상단에서 '런타임 > 런타임 다시 시작 > 이전 import 재실행'
!sudo apt-get install -y fonts-nanum
!sudo fc-cache -fv
!rm ~/.cache/matplotlib -rf
 

Matplotlib 그래프를 그리면 한글 폰트가 깨진다.

폰트 변경 코드를 사용하면 되는데,

구글 Colab은 sudo 명령으로 폰트를 설치해준 후, 런타임을 다시 시작해야 한다.

구글링 오지게 많이 했다.

 

 

 

 

 

데이터 시각화

 

 

 

 

import seaborn as sns
plt.figure(figsize=(12, 8))
sns.heatmap(train.corr(), annot=True, cmap="viridis")
plt.show()

 

 
스터디원이 그려주신 상관계수 히트맵

 

처음보는 라이브러리, 모듈이었다

근데 사실 숫자가 더 보기 편했다 (ㅎㅎ;)

 

corr_train = train.corr()
corr_train["등록차량수"].sort_values(ascending=False)
 

 

 

 

 

 

 

데이터 전처리

결측치 : 검색을 통해 채움 (공공임대상가의 공시 가격)

 

2차원 데이터 : 1차원으로 변환

하나의 단지에 여러 개의 전용면적 존재 --> 총전용면적, 평균전용면적 column으로 변환

하나의 단지에 여려 개의 임대료, 보증금 존재 --> 평균임대료, 평균임대보증금 column으로 변환

 

 

 

 

 

 

분석

'단지내주차면수', '임대료', '임대보증금', '총전용면적', '평균전용면적'과 목표값(등록차량수)의 상관계수가 높음.

 

임대료, 임대보증금, 전용면적과 등록차량수가 선형적으로 비례하지 않을 것이라고 판단했다.

예컨데, 24평에 살고 있는 가구와 48평에 살고 있는 가구가 약 2배의 평균등록차량수를 보이지는 않을 것이라고 생각했다.

 

오히려 많은 가구에 필수적인 1대의 차량을 기점으로 로그적인 증가에 가까울 것이라고 예상했다.

따라서 '임대료', '임대보증금', '총전용면적', '평균전용면적' 4개 컬럼을 로그변환하였다.

 

임대료, 임대보증금은 예상대로 로그 변환 후 더 높은 상관계수를 보였지만

평균전용면적, 총전용면적은 오히려 로그 변환 후 더 낮은 상관계수를 보였다.

 

따라서 임대료, 임대보증금만 로그 변환 후의 컬럼을 활용하여 선형 회귀 모델에 대입하기로 했다.

 

 

 

 

 

모델 학습

# 훈련 데이터 만들기
tmp_train = df_train[['등록차량수', '단지코드', 'n지역', 'n공급유형', '공가수', '지하철', '버스', '단지내주차면수', '총임대세대수', '평균전용면적', '총전용면적', 'log_평균임대보증금', 'log_평균임대료']]

# train_test_split으로 훈련, 테스트 세트 셔플
y_target = tmp_train['등록차량수']
X_data = tmp_train.drop(['단지코드', '등록차량수'], axis=1, inplace=False)

X_train, X_test, y_train, y_test = train_test_split(X_data, y_target, shuffle=False, random_state=42, test_size=0.1)

# 선형회귀 모델 훈련 및 지표 산출(R2 Score, MAE)
model = LinearRegression()
model.fit(X_train, y_train)

print(f'R2 Score : {r2_score(model.predict(X_test), y_test)}, MAE : {mean_absolute_error(model.predict(X_test), y_test)}')

# 3Fold 
from sklearn.model_selection import cross_val_score

mae_scores = cross_val_score(model, X_data, y_target, scoring='neg_mean_absolute_error', cv=3)
avg_mae = np.mean(mae_scores)
std_mae = np.std(mae_scores)

print('3Fold 개별 MAE score : ', mae_scores)
print('3Fold 평균 MAE : ', avg_mae)
print('3Fold 표준편차 MAE : ', std_mae)
 

예상한대로, 로그 변환한 컬럼을 사용한 모델의 점수(MAE)가 가장 좋게 나왔다.

(사실 데이터 크기가 작은 탓에 변환한 컬럼을 중복으로 추가한 점수가 가장 좋게 나왔으나, 결과 제출 시 점수는 그렇지 않았다.)

 

 

 

 

 

 

제출

# 다운로드를 원하는 모델 재실행 후 컬럼 가져오기 (등록차량수, 단지코드 제외)
submission['num'] = model.predict(df_test[['n지역', 'n공급유형', '공가수', '지하철', '버스', '단지내주차면수', '총임대세대수', '평균전용면적', '총전용면적', '평균임대보증금', '평균임대료', 'log_평균임대보증금', 'log_평균임대료']])
submission

# csv 파일로 저장
submission.to_csv("주차수요.csv", index = False)
 

 

 

 

Public 기준 약 112점

제출된 500여개 답안 중 300여등이다.

 

간단한 선형 회귀 모델로 나쁘지 않은 점수(?)가 나온 것은

절대적으로 좋은 전처리 코드를 빌려왔기 때문이지만, 로그 변환을 적용한 것도 대략 몇 점의 좋은 영향은 주었다.. ㅠ

 

 

 

 

 

 

 

스터디 피드백

선형회귀에는 많은 가정이 활용되는데, 해당 내용을 추가하면 좋을 것 같다.

--> 회귀 공부 마치고 다시 적용해보기

 

컬럼에 로그 변환한 인사이트가 좋았다.

--> 다른 모델에도 변환한 컬럼 적용해보기

 

stepwise model selection을 사용하면 사용할 컬럼을 지정하지 않아도, 가장 좋은 모델을 위한 컬럼 선택과 선형회귀 모델 파라미터를 결정해준다고 한다.

--> 해당 라이브러리 공부하고 적용해보기

 

 

 

 

 

 

총평

무지하게 어렵다.

 

 

 

 

 

 

 

 

 

 

 

반응형

+ Recent posts