분류 (classification) - 몇 개의 클래스 중 하나를 분류
회귀 (regression) - 임의의 어떤 숫자를 예측
이전 글에서는 분류문제를 다뤘습니다. 이번엔 회귀문제를 다뤄보려하는데 회귀는 클래스 중 하나로 분류하는 것이 아니라 임의의 어떤 숫자를 예측하는 문제입니다.
k-최근접 이웃 회귀의 방식에 대해 설명해보겠습니다. 예측하려는 샘플에 가장 가까운 샘플 k개를 선택 하여 이 수치들의 평균을 구하는 것입니다.
import numpy as np
perch_length = np.array(
[8.4, 13.7, 15.0, 16.2, 17.4, 18.0, 18.7, 19.0, 19.6, 20.0,
21.0, 21.0, 21.0, 21.3, 22.0, 22.0, 22.0, 22.0, 22.0, 22.5,
22.5, 22.7, 23.0, 23.5, 24.0, 24.0, 24.6, 25.0, 25.6, 26.5,
27.3, 27.5, 27.5, 27.5, 28.0, 28.7, 30.0, 32.8, 34.5, 35.0,
36.5, 36.0, 37.0, 37.0, 39.0, 39.0, 39.0, 40.0, 40.0, 40.0,
40.0, 42.0, 43.0, 43.0, 43.5, 44.0]
)
perch_weight = np.array(
[5.9, 32.0, 40.0, 51.5, 70.0, 100.0, 78.0, 80.0, 85.0, 85.0,
110.0, 115.0, 125.0, 130.0, 120.0, 120.0, 130.0, 135.0, 110.0,
130.0, 150.0, 145.0, 150.0, 170.0, 225.0, 145.0, 188.0, 180.0,
197.0, 218.0, 300.0, 260.0, 265.0, 250.0, 250.0, 300.0, 320.0,
514.0, 556.0, 840.0, 685.0, 700.0, 700.0, 690.0, 900.0, 650.0,
820.0, 850.0, 900.0, 1015.0, 820.0, 1100.0, 1000.0, 1100.0,
1000.0, 1000.0]
)
농어 데이터를 머신러닝 모델에 사용하기 전에 훈련 셋과 테스트 셋으로 나누겠습니다.
from sklearn.model_selection import train_test_split
# 훈련 세트와 테스트 세트로 나눕니다
train_input, test_input, train_target, test_target = train_test_split(
perch_length, perch_weight, random_state=42)
# 훈련 세트와 테스트 세트를 2차원 배열로 바꿉니다
train_input = train_input.reshape(-1, 1)
test_input = test_input.reshape(-1, 1)
print(train_input.shape, test_input.shape)
사이킷런에 사용할 훈련 셋은 2차원 배열이어야 합니다. 그래서 train_input과 test_input이 현재 1차원인데 2차원 배열로 바꿔주었습니다.
이제 회귀에서 모델을 평가하는 기준에 대해 설명드리겠습니다. 분류의 경우는 테스트 셋에 있는 샘플을 정확하게 분류한 개수의 비율로 정확도를 계산합니다. 하지만 회귀에서는 정확한 숫자를 맞힌다는 것은 거의 불가능합니다. 그래서 회귀의 경우에는 좀 다른 값으로 평가하는데 이를 결정계수라고 합니다. 또는 (R의제곱)이라고 부릅니다.
결정계수 = 1 - (타깃 - 예측)제곱의 합/(타깃 - 평균)제곱의 합
만약 타깃의 평균 정도를 예측하는 수준이라면 (즉 분자와 분모가 비슷해져) R제곱은 0에 가까워지고, 예측이 타깃에 아주 가까워지면 (분자가 0에 가까워지기 때문에) 1에 가까운 값이 됩니다.
이최근접 이웃 개수를 3으로 하는 모델을 훈련하여 길이가 50cm인 농어의 무게를 예측해보니 1033g정도로 예측했습니다. 그런데 실제 이 농어의 무게는 훨씬더 많이 나간다고 합니다. 어디서 문제가 생긴 걸까요?
훈련 세트와 50cm농어 그리고 이 농어의 최근접 이웃을 산점도에 표시해보면 길이가 커질수록 농어의 무게가 증가하는 경향이 있는데 50cm에서 가장 가까운 것은 45cm근방 밖에없어서 이 샘플들의 무게를 평균한 값으로 예측을 하지 무게를 원본보다 작게 예측한 것 입니다.
따라서 샘플이 훈련 세트의 범위를 벗어나면 엉뚱한 값으로 예측할 수 있습니다. 예를 들면 100cm의 농어도 1033g으로 예측하는 것처럼요.
그래서 선형 회귀라는 알고리즘을 사용하였습니다.
from sklearn.linear_model import LinearRegression
lr = LinearRegression()
# 선형 회귀 모델 훈련
lr.fit(train_input, train_target)
# 훈련 세트의 산점도를 그립니다
plt.scatter(train_input, train_target)
# 15에서 50까지 1차 방정식 그래프를 그립니다
plt.plot([15, 50], [15*lr.coef_+lr.intercept_, 50*lr.coef_+lr.intercept_])
# 50cm 농어 데이터
plt.scatter(50, 1241.8, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()
훈련 셋과 테스트 셋 점수 둘다 높지않아 과소적합되었다고 볼 수 있습니다. 선형 회귀가 만든 직선은 외쪽아래로 일자로 쭉 뻗어 있습니다. 하지만 이 직선대로 예측하면 농어의 무게가 0g이하로 내려갈 텐데 현실에서는 있을 수 없는 일입니다. 농어의 길이와 무게에 대한 산점도를 자세히 보면 일직선이라기보다 왼쪽 위로 조금 구부러진 곡에 가깝습니다. 그래서 최적의 곡선을 찾으려는 노력을 하게 되는데 이를 2차 방정식으로 그래프로 나타낼 수 있을 것 같습니다.
이런 2차 방정식의 그래프를 그리려면 길이를 제곱한 항이 훈련 세트에도 추가되어야 합니다. cloumn_stack()함수를 사용하면 아주 간단합니다.
무게 = 1.01 x 길이제곱 - 21.6 x 길이 + 116.05
이 모델은 위와 같은 그래프를 학습했습니다. 이런 방정식으로 다항식이라 부르며 다항식을 사용한 선형 회귀를 다항 회귀라고 부릅니다. 이 2차 방저식의 계수와 절편 a,b,c를 알았으니 훈련 세트의 산점도에 그래프로 그려 보겠습니다.
# 구간별 직선을 그리기 위해 15에서 49까지 정수 배열을 만듭니다
point = np.arange(15, 50)
# 훈련 세트의 산점도를 그립니다
plt.scatter(train_input, train_target)
# 15에서 49까지 2차 방정식 그래프를 그립니다
plt.plot(point, 1.01*point**2 - 21.6*point + 116.05)
# 50cm 농어 데이터
plt.scatter([50], [1574], marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()
앞선 단순 선형 회귀 모델보다 훨씬 나은 그래프가 그려졌습니다. 훈련 세트의 경향을 잘 다르고 있고 무게가 음수로 나오는 일도 없을 것 입니다. 그럼 훈련 세트와 테스트 세트의 결정계수 점수를 평가하겠습니다.
실습하며 생각났던 질문들 정리
Q. 왜 1차원 배열을 2차원 배열로 바꾼거야? 어차피 바꾸나 안바꾸나 똑같이 생긴배열아닌가?
A. 대부분의 머신러닝 라이브러리, 특히 `scikit-learn` 같은 경우, 입력 데이터를 여러 개의 샘플과 각 샘플의 특성을 포함하는 2차원 구조로 기대하기 때문입니다.
샘플 : 관측치 하나
특성 : 해당 관측치의 속성이나 변수
- 1차원 배열: 이는 단순히 데이터의 나열로, numpy에서 `(n,)` 형태로 표현됩니다. 여기서 `n`은 배열의 요소 수입니다. 이 구조는 데이터가 단일 차원에만 존재함을 나타냅니다.
- 2차원 배열: numpy에서 `(n, m)` 형태로 표현되며, `n`은 샘플의 수, `m`은 각 샘플의 특성 수를 나타냅니다. 심지어 특성이 하나뿐인 경우에도, 이를 2차원 배열 `(n, 1)`로 변환하여 각 샘플이 하나의 특성을 가진다는 것을 명시적으로 나타냅니다.
변환의 필요성
`scikit-learn`에서는 입력 데이터가 항상 2차원 배열 (즉, `[샘플, 특성]` 구조) 이어야 한다는 규칙이 있습니다. 즉, 특성이 하나인 경우에도 데이터를 `(n, 1)` 형태의 2차원 배열로 제공해야 합니다. 이렇게 하는 이유는 라이브러리가 일관된 데이터 구조를 기대하고, 이를 통해 여러 특성을 가진 복잡한 데이터셋과 동일한 방식으로 단일 특성 데이터셋을 처리할 수 있고, 향후에 더 많은 특성을 모델에 쉽게 추가할 수 있는 기반을 마련할 수 있습니다.
# 50cm 농어의 이웃을 구합니다
distances, indexes = knr.kneighbors([[50]])
Q. distances, indexes = knr.kneighbors([[50]]) 여기는 왜 [[]]이렇게 괄호를 두번이나 감싸?
A. `kneighbors` 메서드에 `[[50]]`과 같이 괄호를 두 번 사용하는 이유는 `scikit-learn` 라이브러리에서 입력 데이터를 2차원 배열로 기대하기 때문입니다.
- 1차원 배열 예: `[50]` - 단순히 값을 나열한 것으로, `scikit-learn` 메서드에서 이를 샘플 배열로 해석하지 않습니다.
- 2차원 배열 예: `[[50]]` - 하나의 샘플이 있으며, 해당 샘플은 하나의 특성 값을 가진다는 것을 명확히 나타냅니다.
Q. 근데 2차원배열에 왜 length만 있어? weight는언제 쓰는건데?
A. 데이터를 모델에 학습시킬 때, 일반적으로 입력(features)과 타겟(target)을 구분합니다.
머신러닝 모델의 학습 과정에서 모델은 입력 데이터(`perch_length`)를 바탕으로 타겟 데이터(`perch_weight`)를 예측하는 방법을 학습합니다. 즉, 물고기의 길이를 바탕으로 무게를 예측하도록 모델을 훈련시키는 것입니다. 이 과정에서 `perch_length`는 2차원 배열로 변환되어 각 샘플의 길이가 모델에 입력되고, `perch_weight`는 모델이 그 길이에 대해 예측해야 하는 실제 무게 값입니다.
Q. 아! train_target은 어차피 무게 값이 하나로 정해져있으니까 그런건가?
A. 네, 맞습니다. `train_target`은 모델이 예측해야 할 값, 즉 타겟(target) 값이며, 이 경우에는 각 샘플에 대응하는 단일 값(예: 물고기의 무게)을 나타냅니다. 타겟 데이터는 각 샘플의 예측하려는 결과를 포함하므로, 보통 1차원 배열로 구성됩니다. 여기서 각 요소는 개별 샘플의 예측 값을 나타내며, 이 값은 모델 학습 과정에서 참고하는 '정답' 역할을 합니다.
기계 학습 문제에서는 입력 데이터(피처)와 타겟 데이터를 구분합니다:
- 입력 데이터는 모델이 예측을 수행하기 위해 사용하는 정보를 포함하며, 이는 하나 이상의 특성(Feature)으로 구성될 수 있습니다. 이 데이터는 2차원 배열(`[샘플 수, 특성 수]`)로 표현되어, 각 샘플이 하나 이상의 특성 값을 가질 수 있게 합니다.
- 타겟 데이터는 각 샘플에 대한 예측하고자 하는 값으로, 대부분의 경우 단일 차원의 값(예: 가격, 무게 등)입니다. 따라서 이는 1차원 배열로 충분히 표현됩니다.
`train_target` 배열의 각 요소는 학습 데이터셋의 각 샘플에 대응되는 타겟 값을 담고 있으며, 모델은 이 타겟 값에 최대한 가까운 예측 값을 출력하도록 학습됩니다. 이러한 구조는 모델이 다루는 문제의 종류(회귀, 분류 등)에 관계없이 일반적으로 적용됩니다. 회귀 문제에서는 타겟 값이 연속적인 실수 값일 수 있으며, 분류 문제에서는 특정 클래스 레이블을 나타낼 수 있습니다.
Q. 그럼 lr.fit(train_input, train_target) 여기서 train_input은 2차원배열로 변환해줘서 2차원이고 train_target은 그대로 1차원 배열이어도 되는거야?
A. 맞아요! `lr.fit(train_input, train_target)`에서 `train_input`은 2차원 배열로 변환해주어야 하고, `train_target`은 1차원 배열로 남겨둬도 괜찮습니다. 여기서 `lr`은 일반적으로 선형 회귀 모델(Linear Regression model)을 가리키는데, 이와 같은 모델을 학습시킬 때 입력 데이터와 타겟 데이터의 차원 요구사항은 다음과 같습니다:
- 입력 데이터(`train_input`): 모델에 학습시킬 입력 데이터는 2차원 배열이어야 합니다. 이 배열의 형태는 `[샘플 수, 특성 수]`로, 각 샘플은 하나 이상의 특성 값을 가질 수 있습니다. 여기서 하나의 특성만 사용하는 경우에도 데이터는 2차원 배열(`[샘플 수, 1]`)로 구성되어야 합니다. 이것은 모델이 각 샘플의 특성들을 구분할 수 있게 해줍니다.
- 타겟 데이터(`train_target`): 타겟 데이터는 모델이 예측하고자 하는 값입니다. 선형 회귀와 같은 회귀 모델의 경우, 이 값은 각 샘플에 대한 실수 값입니다. 타겟 데이터는 일반적으로 1차원 배열로 제공됩니다. 이 배열의 각 요소는 해당 샘플의 타겟 값을 나타냅니다.
Q. 다항회귀로 바꾸려하는데 train_poly = np.column_stack((train_input ** 2, train_input)) test_poly = np.column_stack((test_input ** 2, test_input)) 왜 이런 작업이 필요한거야?
A. 다항 회귀(Polynomial Regression)는 선형 회귀의 한 형태로, 독립 변수의 고차항을 사용하여 모델을 학습시키는 방법입니다. 이는 데이터 사이의 비선형 관계를 모델링할 때 특히 유용합니다. 기본적인 선형 회귀 모델이 (y = ax + b)와 같은 관계를 나타내는 반면, 다항 회귀는 (y = ax^2 + bx + c)와 같이 독립 변수의 제곱(또는 더 높은 차수의 항)을 포함하여 더 복잡한 관계를 표현할 수 있습니다.
- 왜 다항 회귀를 사용하는가?
다항 회귀를 사용하는 주된 이유는 단순 선형 회귀로는 설명할 수 없는 데이터의 복잡한 패턴과 관계를 포착할 수 있기 때문입니다. 예를 들어, 물고기의 길이와 무게 사이의 관계가 단순히 선형적이지 않고, 길이가 증가함에 따라 무게가 제곱으로 증가하는 경향을 보인다면, 이러한 비선형 관계를 모델링하기 위해 다항 회귀가 필요합니다.
다항 회귀를 위한 데이터 변환
`train_poly = np.column_stack((train_input ** 2, train_input))`와 같은 변환 작업은 다항 회귀를 위해 입력 데이터를 준비하는 과정입니다. 여기서 `train_input ** 2`는 기존의 특성(물고기의 길이)을 제곱하여 새로운 특성으로 추가하는 것을 의미하며, 이렇게 함으로써 모델이 길이의 제곱도 고려할 수 있게 됩니다. 그 다음, `np.column_stack` 함수를 사용하여 원래 특성(`train_input`)과 새로 생성된 제곱 특성(`train_input ** 2`)을 나란히 배열로 합치게 됩니다. 이로써 각 샘플에 대해 [길이의 제곱, 길이]와 같은 형태의 특성 집합이 준비됩니다.
이와 같은 과정은 모델이 물고기의 길이뿐만 아니라 길이의 제곱을 고려하여 무게를 예측하도록 하는 것입니다. 이는 모델이 복잡한 비선형 관계를 더 잘 학습할 수 있게 해주며, 결과적으로 예측 성능이 향상될 수 있습니다.
비슷한 방식으로 `test_input`에 대해서도 동일한 변환 작업을 수행하여, 학습된 모델을 사용하여 테스트 데이터에 대한 예측을 수행할 때 일관된 데이터 구조를 유지합니다. 이렇게 함으로써, 학습 데이터와 테스트 데이터가 동일한 형태의 특성을 가지게 되어, 모델이 둘 모두에 대해 정확한 예측을 수행할 수 있습니다.
Q. 그럼 만약 3개 특성 입력하려면 입력배열 3차원으로해야해? 아니면 train_poly = np.column_stack((train_input ** 3, train_input ** 2, train_input)) 이렇게 2차원으로 유지하되 열(특성)만 늘리면되나?
A. 3차 다항 회귀를 사용하고 싶다면, 입력 배열을 3차원으로 만드는 대신, `train_input`의 세제곱 특성까지 포함시켜야 합니다. 따라서 다음과 같은 코드를 사용할 수 있습니다
train_poly = np.column_stack((train_input ** 3, train_input ** 2, train_input))
test_poly = np.column_stack((test_input ** 3, test_input ** 2, test_input))
이 코드는 각 샘플에 대해 원본 특성, 제곱된 특성, 그리고 세제곱된 특성을 결합하여 3차 다항 특성을 만듭니다. 이 방식을 통해 모델은 더 복잡한 비선형 관계를 학습할 수 있게 되며, 이는 선형 회귀 모델을 사용하는 것보다 데이터를 더 잘 설명할 수 있게 합니다.
`np.column_stack` 함수를 사용해서 `train_input ** 3`, `train_input ** 2`, `train_input`과 같이 여러 개의 특성을 결합할 때, 결과는 여전히 2차원 배열이 됩니다. 이 경우 배열의 각 행은 하나의 샘플을 나타내고, 각 열은 다른 차수의 특성(예: 세제곱, 제곱, 원본 특성)을 나타냅니다.
예를 들어, 3차 다항 회귀를 위해 준비된 `train_poly`는 `[샘플 수, 특성 수]`의 형태를 가집니다. 여기서 "특성 수"는 원본 특성 뿐만 아니라, 제곱과 세제곱을 포함한 특성의 총 개수입니다. 따라서, 만약 당신이 하나의 특성(예: 물고기의 길이)으로 시작했다면, 이 특성을 제곱하고 세제곱하여 3차 다항 회귀를 위한 입력 데이터를 만들 때 결과적으로 각 샘플은 3개의 특성을 가지게 됩니다(열이 세개됨). 이렇게 확장된 입력 데이터는 모델이 더 복잡한 데이터 패턴을 학습할 수 있도록 도와줍니다.
Q. lr.fit(train_poly, train_target) 그럼 이렇게 하면 특성이 몇개든 사이킷런이 알아서 학습하는거야?
A. 네, 맞습니다. `lr.fit(train_poly, train_target)`을 사용할 때, `train_poly` 배열에 있는 특성의 개수와 상관없이 `scikit-learn`의 선형 회귀 모델(`LinearRegression`)은 이를 자동으로 처리하고 학습합니다. `train_poly`에 포함된 각 열(특성)은 모델이 학습 과정에서 고려해야 할 독립 변수로 간주됩니다.
작동 원리
`LinearRegression` 모델은 주어진 입력 특성을 사용하여 타겟 변수와의 선형 관계를 찾습니다. 다항 회귀의 경우, 원본 데이터에 대한 비선형 변환(예: 제곱, 세제곱 등)을 통해 추가된 특성들이 입력으로 제공되며, 모델은 이러한 모든 특성을 고려하여 최적의 선형 관계를 학습하려고 시도합니다.
자동 학습
`scikit-learn`의 `LinearRegression` 모델(또는 대부분의 머신러닝 모델)은 입력 데이터의 차원이나 특성의 개수에 상관없이 이를 처리할 수 있도록 설계되어 있습니다. 따라서, 당신이 `train_poly`에 얼마나 많은 특성을 추가하든, 모델은 주어진 특성을 기반으로 타겟 값을 예측하기 위한 최적의 선형 함수를 찾으려고 합니다.
이 과정에서 중요한 것은 모델의 복잡도와 데이터에 적합한 복잡도 사이의 균형을 찾는 것입니다. 너무 많은 특성(고차 다항식)을 사용하면 과대적합의 위험이 있으며, 너무 적으면 과소적합이 발생할 수 있습니다. 따라서 적절한 차수의 다항식을 선택하는 것이 중요합니다.
'AI > 혼공파 머신러닝+딥러닝' 카테고리의 다른 글
[ML 05-1] 결정 트리 (0) | 2024.05.15 |
---|---|
[ML 04-2] 확률적 경사 하강법 (0) | 2024.05.09 |
[ML 04-1] 로지스틱 회귀 (0) | 2024.04.04 |
[ML 03-3] 특성공학과 규제 - 릿지(Ridge), 라쏘(Lasso) (1) | 2024.03.31 |
[KNN 03-1] k-최근접 이웃 알고리즘 (데이터 전처리) (1) | 2024.03.26 |