머신러닝 & 딥러닝

머신러닝&딥러닝 기초(3): 회귀분석

카루-R 2022. 2. 15. 15:31
반응형

환영합니다, Rolling Ress의 카루입니다.

이번 글은 길어요.


회귀란, 두 변수 사이의 상관관계를 분석하는 것이다. 비례, 반비례, 뭐 여하튼 이런 관계가 있으면 한 변수에 대해 다른 변수의 값을 예측해볼 수 있다. 예를 들어,

자, 자연수의 집합에서 정의된 함수 f를 위와 같이 정의하자. 정의역과 공역이 모두 자연수다. X가 1, 2, 3, 4, ...가 될 동안 Y는 2, 4, 6, 8, ...이 되어간다. 그럼 여기서 함수 f를 추론할 수 있다. 그렇다면 2, 4, 6, 8, 다음엔 뭐가 나올까?
 간단하게 2, 4, 6, 8, 다음은 658임을 생각할 수 있다. 함수 y=f(x)의 함수식을 구해보면 위와 같이 나온다. 쉽다.

...는 사실 농담이었습니다. 콜록.

아무튼, 저건 뭐 그냥 내가 마음대로 식 조작해서 만들어낸 식이긴 한데.. 이런 장난질 없이 다음과 같은 함수를 생각해보자.

정의역은 자연수의 집합({1, 2, 3, 4, 5, ....}), 공역=치역은 짝수 집합({2, 4, 6, 8, 10, ...})이다. 어차피 저걸 수열로 만들어 보내면, 둘 다 발산한다. 그런데, '무한집합으로 발산하려는 빠르기는 얼마나 큰가?' 똑같다. 차수가 같으니까. 전체는 부분부다 클까? 글쎄. 칸토어는 그렇게 생각하지 않는다. 어차피 무한집합은 밀도로 비교하는 거니까... 초한기수까지 끌고 간다면 자연수의 집합과 양의 짝수의 집합은 같은 크기다. 둘 다 알레프0...

아무튼, 저걸 정상적인 그래프로 표현하면 k-Nearest Regression(k-최근접 이웃 회귀)의 관점에서 볼 수 있다. 저기 x값이 2.9인 어떤 점이 있다. 그러면? 여기선 극한값의 개념으로 찾아볼 수 있다. 고등학교의 그런 '한없이' 따위의 설명 말고. 엡실론-델타 논법으로 보자.

 

여기서 함수 f(x)=2x라고 이미 우리가 정했다. 연속함수니까 모든 점에서 극한값이 존재한다. 임의의 작은 양수 ε에 대해 적당한 양수 δ를 잡아 x와 a의 차이가 δ보다 작게 하면 f(x)와 L의 차이가 항상 ε보다 작다. (단, δ=δ(ε))

뭐 사실 엄밀한 정의와 과정은 아니지만, 이런 식으로 주변을 탐색해서 값을 찾을 수 있다.

회귀에서도 score을 딸 수는 있긴 한데, 단순히 0.0~1.0 사이의 값이 아니라 '결정계수coefficient of determination' 또는 R²라고 부르는, 조금 복잡한 점수다. '1 - (타깃 - 예측)²의 합/(타깃 - 평균)²의 합'으로 구해진다.

이때, train set과 test set의 R²은 조금 다르다. 보통은 train set이 소폭 우위에 있는데 (당연하다. train set으로 train을 했으니까 당연히 더 높아야지) test set이 이상하리만큼 R²값이 낮을 수 있다. 이것을 과대적합/overfitting이라고 한다. 너무 train set에 맞다 보니 실전에선 개판이 된 경우. 지난번에 설정값을 조작했더니 사자를 더욱더 호랑이라고 굳게 믿는(...) 경우를 예시로 들 수 있겠다.

반대, test set의 점수가 높거나 둘 다 너무 낮다면 그걸 underfitting, 과소적합이라고 한다. 이건 훈련이 제대로 안 된 거다. 전 세계에 있는 모든 데이터를 사용할 수 없으므로 훈련 세트를 만드는 건데, 그걸 제대로 학습하지 못했다는 건 중대한 결함이 있는 거다. 어느쪽이든 좋지 않다.

Underfitting의 해법은 간단하다. 회귀분석에서 사용하는 이웃의 수를 줄이면 된다. 참고로 중간에 보이는 mean_absolute_error(target, prediction) 메서드는 mean의 absolute error를 계산한다.

from sklearn.neighbors import KNeighborsRegressor
knr: KNeighborsRegressor = KNeighborsRegressor(n_neighbors=3)

KNeighborsRegressor 생성자 파라미터에 n_neighbors를 명시해서 값을 넣어주면 된다. 기본값은 5다.

참고로 n_neighbors의 값이 커질수록 그래프의 삐침이 덜하다. 다만 최댓값은 n_samples보다 작거나 같아야 한다. 즉, 넘긴 numpy의 원소의 개수보다 크면 안 된다 (당연한 것 같기도...)

 샘플의 개수보다 큰 값을 넣어버리면 저렇게 ValueError가 raise된다. try~except로 잡을 수 있지 않냐....고는 하지 마라. 그거 그러다 exception swallow 생기면 골치 아파진다. 어쨌든, 최댓값을 넣으면 저렇게 반띵(?)하는 선이 생긴다.

# 필수 모듈 import
import numpy as np
import matplotlib.pyplot as plt
from sklearn.neighbors import KNeighborsRegressor
from sklearn.model_selection import train_test_split

# 분석 데이터 준비
x_data = np.array([ /* 적당한 본인의 데이터 */ ])
y_data = np.array([ /* 적당한 본인의 데이터 */ ])

# train set과 test set으로 분류
train_input, test_input, train_target, test_target = train_test_split(
    x_data, y_data) # 필요한 경우 random_state=N 추가

# 1d => 2d 변환
train_input = train_input.reshape(-1, 1)
test_input = test_input.reshape(-1, 1)

# 머신러닝 학습
knr = KNeighborsRegressor(n_neighbors=3) # 필요시 숫자 적절히 조정
knr.fit(train_input, train_target)

이게 KNeighborsRegressor을 활용하는 기본 코드다. 추가로 활용할 수 있는 메서드는 다음과 같다. 참고로 score, mae는 당연하지만 print()로 출력해야 값이 나온다. 저건 확인용이니까. 그리고, mean_absolute_error의 경우 from ~ import ~ as 로 당겨오는 게 쓰기 더 편하다. (from sklearn.matrics import mean_absolute_error as m_absolute_e)

# 머신러닝 학습치 평가
score = knr.score(test_input, test_target)

# 평균 절댓값 오차 계산
test_prediction = knr.predict(test_input)
mae = sklearn.metrics.mean_absolute_error(test_target, test_prediction)

방금 한 건 K-Neighbors Regression이고, 이번에는 Linear Regression, 선형 회귀를 해보도록 하겠다. 아까 y=2x 그래프를 그렸는데, 비슷한 느낌이다. 선형. 일차함수. 직선으로 나타낸다.

# 선형 회귀 클래스
class sklearn.linear_model.LinearRegression

 

여기서 중요한 개념이 등장한다. LinearRegression은 기울기를 갖는다. 그렇다면 이 기울기는 어딘가에 저장이 되어 잇을 것이다. 맞다. coef_라는 field에 저장되어 있다. 머신러닝에서 기울기는 weight, 가중치라고 부른다. 이게...정체가 이거였어....

머신러닝이 찾은 값. 모델 파라미터(model parameter). 머신러닝 알고리즘의 train은 최적의 model parameter를 찾는 것. model-base learning이 이것이다.

사실 이건 엑셀로도 충분히 가능한 조합이다. 자 그런데, 단순 선형 회귀는 문제가 있다. 예를 들어, 맨 위에서는 (자연수) |-> (짝수) 함수를 정의했다. 그런데, 이거 잘못하면 골로 간다. 음수가 나오면 안 되는 상황에서는 선형회귀를 함부로 쓰면 안 된다. 예를 들어 블로그 조회수가 음수로 나온다든지...히익..

사실 일차식이 아니어도, 선형회귀로 부르긴 한다. 다음과 같은 식을 생각해보자.

두 식은 완전히 같은 함수는 아니지만, 사실상 동일한 식이다. 아 근데 이거 이렇게 나누어지니까 왠지 미분을 하고 싶은걸...?

어 잠깐 망했다. 혹시 왜 망한지 아시는 분들은 댓글 좀....ㅋㅋㅋㅋㅋㅋ

여하튼, 중요한 건 이렇게 고차 변수를 치환해버리면 그냥 다변수 선형 식이 된다는 거다. 그래서, 이것들을 다항 회귀(polynomial regression)이라고 부른다. 다항식을 사용한 선형 회귀.

그래서, 적절한 값을 이용해서 플롯을 그려주면 멋진 선형회귀 식이 그려진 모습을 볼 수 있다. 지수함수 아니냐고? 글쎄. 지수함수도 테일러로 빠개서 무한히(...) 치환시키면 선형식이 된다.

여하튼, 가장 잘 맞는 선형식을 찾는 건 최적의 기울기와 절편을 구하는 것이다. 아마 이게 딥러닝의 기초가 아닐까 싶다. 인공지능 하면서 가중치(기울기)와 편향(절편)에 대한 이야기를 엄청 많이 들었는데... 이게 또 얼마나 나를 괴롭힐지.

train_input, test_input, train_target, test_target = train_test_split(x_data, y_data)

poly = PolynomialFeatures(include_bias=False)
poly.fit([[2, 3]])

train_poly = poly.transform(train_input)
test_poly = poly.transform(test_input)

lr = LinearRegression()
lr.fit(train_poly, train_target)

다중 회귀 모델... 열받는다. 슬슬 이해가 안 되는 지점에 도달했다. 내가 졸린 걸까.

여러 특성을 사용하기 위해 PolynomialFeatures 클래스는 내부에서 인자들을 지지고 볶고 제곱하고 곱한다. 이 과정에서 degree를 설정할 수 있는데, 높으면 3, 4, 5제곱까지 데이터를 강력하게 만들 수 있다. 그러나 Overfitting 문제가 생기게 된다. 학습은 너무나 잘 했는데, 테스트 데이터는 영 형편이 없는 것.

어제 봤던 이것과 비슷한 상황이라고 볼 수 있다. train set을 너무 학습하다보니 다른 걸 보지 못하는 거다. 쉽게 말해서, 내가 라에의 얼굴만을 사람이라고 학습하면 다른 사람의 얼굴은 사람의 얼굴로 인식하지 못한다는 것이다.

여기서 우리 뇌의 특성을 유추할 수 있다. 뇌는 무엇을 기억할 때 100% 정확하게 기억하지 않는다. 만약 그랬다간... 저런 불상사가 날 것이다. 어설프게 기억하는 게 중요하다. 과도하게 정밀한 학습은 오히려 독이 된다.


그래서 필요한 것이 regularization, 규제다. 너무 잘 학습하지 마라! 슬쩍 disturb하는 거다. 쉽게 말해 유연성을 부여하는 것.

train_input, test_input, train_target, test_target = train_test_split(x_data, y_data)

poly = PolynomialFeatures(include_bias=False)
poly.fit([[2, 3]])

train_poly = poly.transform(train_input)
test_poly = poly.transform(test_input)

lr = LinearRegression()
lr.fit(train_poly, train_target)

Ridge와 Lasso 두 가지의 규제 회귀가 존재한다. 제곱이냐 절댓값이냐의 차이다.

ridge = Ridge()
ridge.fit(train_scaled, train_target)
print(ridge.score(train_scaled, train_target))

릿지는 저렇게 가져올 수 있다. 원래 생성자 파라미터로 알파값을 지정하는데, 그건 사람이 직접 찾아야 한다.

train_score: List = []
test_score: List = []
alpha_list: List = [0.001, 0.01, 0.1, 1, 10, 100]
for alpha in alpha_list:
    ridge = Ridge(alpha=alpha)
    ridge.fit(train_scaled, train_target)
    train_score.append(ridge.score(train_scaled, train_target))
    test_score.append(ridge.score(test_scaled, test_target))

plt.plot(np.log10(alpha_list), train_score)
plt.plot(np.log10(alpha_list), test_score)
plt.show()

딱히 어려울 건 없고, 그냥 눈대중으로 봐서 제일 간격이 좁은 쪽을 선택하면 된다. 그냥 range for문 돌리면서 최소가 되는 alpha 값을 찾으면

// C 계열 코드긴 하지만...
save = (list_a[i] - list_b[i]) < save? (list_a[i] - list_b[i]) : save;

이런 식으로 저장할 수도 있긴 하다. 근데 귀찮다. 여하튼, 여기서 적정한 alpha 값을 찾으면 그냥 넣어주면 된다. Lasso를 쓰고 싶다면, 코드들에서 Ridge 대신 Lasso를 넣으면 된다. 쉽죠.


오늘은 여기서 중단. 머리아파요.

내일은 제가 수학시간에 노가다를 뛴, 시그모이드 함수와 로지스틱 회귀에 대해 알아볼 거예요.

반응형