💡 목표 : Chapter03 + Ch.03(03-1) 2번 문제 출력 그래프 인증하기 + 모델 파라미터에 대해 설명하기
1주차때도 K-최근접을 봤는데…
그건 K-최근접 이웃 알고리즘이고, 이건 회귀이다. 뭐가 다른걸까?
지도 학습의 한 종류인 회귀 문제를 이해하고, K-최근접 이웃 알고리즘을 사용하여 무게를 예측하는 회귀 문제를 풀어보자
1주차에서 도미와 빙어를 구분하는 모델을 개발 했다. 이번에는 농어를 무게 단위로 판매하려는데, 무게 데이터가 잘못 들어와서 다른 데이터들로 무게를 예측해야 한다.
지도 학습 알고리즘은 크게 분류와 회귀로 나뉜다.
이제 알았다. K-최근접 이웃 알고리즘은 알고리즘이니까 회귀나 분류 문제에 사용할 수 있는거다.
아 그래서 저번 주차에 KNeighborsClassifier()를 사용한 거였다. Classifier가 분류니까 분류에 쓰이는 클래스를 사용한 것이다. 또 까먹고 또 공부하고… 정말 공부는 반복인 것 같다…
저번에 쓴 K-최근접 이웃 분류 알고리즘은 다음과 같다.
예측하려는 샘플에 가장 가까운 샘플 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])
데이터가 어떤 형태인지 산점도를 봐보자.
import matplotlib.pyplot as plt
plt.scatter(perch_length, perch_weight)
plt.xlabel('length')
plt.ylabel('weight')
plt.show()
길이가 커질수록 무게도 늘어난다.
당연하다...
이제 훈련 세트와 테스트 세트로 나눠보자.
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)
train_test_split() 함수로 훈련 세트와 테스트 세트를 나눴다.
훈련 세트는 2차원 배열이어야 하는데, perch_length는 1차원 배열이라 2차원 배열로 바꿔줘야 한다.
튜플에서 (3,) 이런 형태면 1차원 배열인데, 여기에 억지로 하나의 열을 추가하면 (3,1)이 되고, 배열을 나타내는 방식만 달라지며 원소 개수는 동일하다.
reshape() 함수를 사용해보자.
train_input = (42,) -> (42,1)
배열의 크기를 자동으로 지정하는 기능도 있는데, 크기에 -1을 지정하면 원소 개수를 몰라도 자동으로 맞춰서 채워준다.
즉, .reshape(-1, 1) 이런 형태로 사용하면 된다.
train_input = train_input.reshape(-1, 1)
test_input = test_input.reshape(-1, 1)
print(train_input.shape, test_input.shape)
결과 -> (42,1) (14, 1)
k-최근접 이웃 회귀 알고리즘을 구현한 클래스는 KNeighborsRegress이다.
저번에 공부한 KNeighborsClassifier와 사용법미 비슷하다. 객체를 생성하고 fit()으로 훈련시키면 된다.
from sklearn.neighbors import KNeighborsRegressor
knr = KNeighborsRegressor()
knr.fit(train_input, train_target)
print(knr.score(test_input, test_target))
결과 -> 0.992809406101064
분류의 경우 이 점수는 테스트 세트에 있는 샘플을 정확하게 분류한 개수의 비율이었다.
회귀는 정확한 숫자를 맞추는 것이 거의 불가능하다. 그래서 조금 다른 값으로 평가하는데 이 점수를 결정계수라고 부른다. 간단히 라고도 부른다.
계산 방식은 다음과 같다.
즉, 0.99면 좋은 값이다. 하지만 결정 계수를 직접적으로 이해하기는 어려우므로 타깃과 예측한 값 사이의 차이를 구해서 직관적으로 알아보자.
from sklearn.metrics import mean_absolute_error
test_prediction = knr.predict(test_input)
mae = mean_absolute_error(test_target, test_prediction)
print(mae)
결과 → 19.157142857142862
즉, 19g 정도 타깃값과 다른 것이다.
지금까지는 테스트 세트로 평가했다. 이번에는 훈련 세트로 출력해보자.
훈련 세트의 결졍계수 점수를 확인해보자.
print(knr.score(train_input, train_target))
결과 → 0.9698823289099254
테스트 세트보다 훈련 세트의 값이 더 낮다.
모델을 훈련 세트를 가지고 훈련했으니 훈련 세트에 더 잘 맞는 모델이 만들어져야 한다.
만약, 훈련 세트 점수보다 테스트 세트 점수가 많이 나쁘면 과대적합 되었다고 말한다.
훈련 세트에만 잘 맞는 모델이라는 것이다.
반대로 훈련 세트보다 테스트 세트 점수가 높거나 두 점수 모두 낮은 경우에는 과소적합 되었다고 한다. 모델이 너무 단순해서 훈련 세트에 적절히 훈련되지 않은 것이다.
조금 전 테스트 세트와 훈련 세트의 의 값을 출력해본 결과 테스트 세트의 점수가 훈련 세트의 점수보다 높았기 때문에 이 경우에는 과소적합이다.
문제를 해결하기 위해서는 모델을 더 복잡하게 만들면 된다.
이웃의 개수를 바꿔보자.
knr.n_neighbors = 3
knr.fit(train_input, train_target)
print(knr.score(train_input, train_target))
결과 → 0.9804899950518966
훈련 세트의 점수가 증가했다.
print(knr.score(test_input, test_target))
결과 → 0.9746459963987609
테스트는 훈련 세트보다 점수가 낮아졌으므로 과소 적합 문제를 해결한 것 같다.
→ 1. 이웃 샘플 클래스 중 다수인 클래스를 가지고 예측하는 것은 분류 모델이다. 회귀는 수치를 추정해야 하므로 타깃값의 평균을 가지고 값을 정한다.
import numpy as np
import matplotlib.pyplot as plt
knr = KNeighborsRegressor()
x = np.arange(5, 45).reshape(-1, 1)
for n in [1, 5, 10]:
knr.n_neighbors = n
knr.fit(train_input, train_target)
prediction = knr.predict(x)
plt.scatter(train_input, train_target)
plt.plot(x, prediction)
plt.title('n_neighbors = {}'.format(n))
plt.xlabel('length')
plt.ylabel('weight')
plt.show
→n이 커지면서 모델이 단순해지고 있다.
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)
train_input = train_input.reshape(-1, 1)
test_input = test_input.reshape(-1, 1)
from sklearn.neighbors import KNeighborsRegressor
knr = KNeighborsRegressor(n_neighbors=3)
knr.fit(train_input, train_target)
print(knr.predict([[50]]))
결과 → [1033.33333333]
1.033g 정도로 예측했다.
산점도로도 봐보자.
import matplotlib.pyplot as plt
distances, indexes = knr.kneighbors([[50]])
plt.scatter(train_input, train_target)
plt.scatter(train_input[indexes], train_target[indexes], marker='D')
plt.scatter(50, 1033, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()
길이가 커질수록 농어의 무게가 증가하는 경향이 있지만, 50cm 농어에서 가장 가까운 것은 45cm 근방이고, 무게를 평균하는 것이므로 예측 값이 빗나가게 된다.
print(knr.predict([[100]]))
결과 → [1033.33333333]
50cm든 100cm든 결과값이 똑같이 나온다.
distances, indexes = knr.kneighbors([[100]])
plt.scatter(train_input, train_target)
plt.scatter(train_input[indexes], train_target[indexes], marker='D')
plt.scatter(100, 1033, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()
이런 식이면 농어가 아무리 커도 무게가 더 늘어나지 않을 것이다.
이러한 문제를 해결할 수 있는 알고리즘이 있다.
대표적인 회귀 알고리즘이다. 특성이 하나인 경우 직선을 학습하는 알고리즘이다.
LinearRegression 클래스를 사용하면 되는데, 이것도 마찬가지로 fit()으로 학습, score()로 평가, predict()로 예측한다.
from sklearn.linear_model import LinearRegression
lr = LinearRegression()
lr.fit(train_input, train_target)
print(lr.predict([[50]]))
결과 → [1241.83860323]
오 아까보다 값이 잘 나왔다.
직선을 그리려면 기울기와 절편이 필요하다.
이런 형태로 쓸 수 있어야 한다. 무게를 예측하는 것이므로 y가 무게, 특성이 길이로 주어졌으므로 x가 길이가 될 것이다.
LinearRegression이 찾은 a, b값은 coef, intercept에 저장이 된다.
print(lr.coef_, lr.intercept_)
결과 → [39.01714496] -709.0186449535477
coef, intercept 두 값은 머신러닝 알고리즘이 찾은 값이라는 의미로 모델 파라미터 라고 부른다. 많은 알고리즘의 훈련 과정은 최적의 모델 파라미터를 찾는 것과 같다. 이를 모델 기반 학습이라고 부르고, k-최근접 이웃에서는 모델 파라미터가 없었는데, 이런 것을 사례 기반 학습이라고 부른다.
plt.scatter(train_input, train_target)
plt.plot([15, 50], [15*lr.coef_ + lr.intercept_, 50*lr.coef_ + lr.intercept_])
plt.scatter(50, 1241.8, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()
이것이 선형 회귀 알고리즘이 찾은 최적의 직선이다.
점수도 확인해보자
print(lr.score(train_input, train_target))
print(lr.score(test_input, test_target))
결과 → 0.939846333997604, 0.8247503123313558
훈련 세트와 테스트 세트의 점수가 차이가 많이 난다. 근데 훈련 세트 점수도 높지 않다. 그래서 과소적합되었다고 할 수 있다.
그리고 그래프 왼쪽 아래를 보면 조금 이상하다.
선형 회귀는 직선이 쭉 뻗어있다. 이 직선대로라면 농어의 무게가 0g 이하로 내려갈 수도 있을 것이다.
산점도를 보면 일직선이라기 보다 왼쪽 위로 조금 구부러진 곡선에 가깝다.
그러면 최적의 직선보다 최적의 곡선을 찾으면 어떨까?
곡선을 찾는 방정식은 다음과 같았다.
이를 위해서는 길이를 제곱한 항이 훈련 세트에 추가되어 있어야 한다.
column_stack() 함수를 사용해보자.
train_poly = np.column_stack((train_input ** 2, train_input))
test_poly = np.column_stack((test_input ** 2, test_input))
print(train_poly.shape, test_poly.shape)
결과 → (42, 2) (14, 2)
이제 선형 회귀 모델을 다시 훈련해보자.
lr = LinearRegression()
lr.fit(train_poly, train_target)
print(lr.predict([[50**2, 50]]))
결과 → [1573.98423528]
값이 더 높아졌다.
print(lr.coef_, lr.intercept_)
결과 → [ 1.01433211 -21.55792498] 116.0502107827827
이런 방정식을 다항식이라 부르며 다항식을 사용한 선형 회귀를 다항 회귀라고 부른다.
point = np.arange(15, 50)
plt.scatter(train_input, train_target)
plt.plot(point, 1.01*point**2 - 21.6*point + 116.05)
plt.scatter(50, 1574, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()
print(lr.score(train_poly, train_target))
print(lr.score(test_poly, test_target))
0.9706807451768623
0.9775935108325122
하지만 아직 테스트 세트 점수가 더 높다. 과소적합이 아직 남아있다.
여러 특성을 사용한 다중 회귀, 과대적합을 막기 위한 릿지와 라쏘 회귀 배우기
이제 다항 회귀로 농어 무게를 어느정도 예측할 수 있지만, 여전히 훈련 세트보다 테스트 세트 점수가 높은 점이 찜찜하다. 그런데, 특성은 길이 뿐만 아니라 높이와 두께 데이터도 있다. PolynomialFeatures 클래스를 사용해보자.
위에서는 하나의 특성을 사용하여 선형 회귀 모델을 훈련시켰다.
여러 개의 특성을 사용한 선형 회귀를 다중 회귀라고 부른다.
특성이 1개면 모델은 직선을 학습한다. 2개의 특성이면 평면을 학습하게 된다.
타깃값과 함께 3차원 공간을 형성한다.
이제 농어 길이와 농어 높이를 곱한 값을 새로운 특성으로 만들어보자.
이렇게 기준의 특성을 사용해 새로운 특성을 뽑아내는 작업을 특성 공학이라고 부른다.
특성이 3개로 늘어났다. 그러니까 판다스와 데이터프레임을 활용해서 바로 값을 읽어와보자.
read_csv()에 주소를 넣어 데이터 프레임을 만든 다음 to_numpy() 메서드를 사용해서 넘파이 배열로 바꿀 수 있다.
import pandas as pd
df = pd.read_csv('https://bit.ly/perch_csv_data')
perch_full = df.to_numpy()
print(perch_full)
타깃 데이터는 이전 방식과 동일하다.
import numpy as np
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_full, perch_weight, random_state=42)
사이킷런에서는 특성을 만들거나 전처리하기 위한 다양한 클래스를 제공하는데, 이런 클래스를 변환기라고 부른다. fit(), transform() 메서드를 제공한다.
PolynomialFeatures 클래스를 사용하려고 한다.
from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures()
poly.fit([[2, 3]])
print(poly.transform([[2, 3]]))
결과 → [[1. 2. 3. 4. 6. 9.]]
fit() 은 새로운 특성 조합을 만들고, transform() 메서드는 실제로 데이터를 변환한다.
2개의 특성으로 6개의 특성을 만들었는데 각 특성을 제곱한 항을 추가하고, 특성끼리 서로 곱한 항을 추가한다. 즉, 2와 3을 제곱한 4와 9가 추가되었고, 2와 3을 곱한 6이 추가되었다.
1은 선형 방정식을 봤을 때 절편을 항상 값이 1인 특성과 곱해지는 계수라고 본다면 1이 필요한 것이다. 필요없으면 include_bias = False로 제거할 수 있다.
train_input에 적용해보자.
poly = PolynomialFeatures(include_bias=False)
poly.fit(train_input)
train_poly = poly.transform(train_input)
print(train_poly.shape)
결과 → (42, 9)
9개의 특성이 어떤 것인지는 get_feature_names() 를 통해 어떤 입력의 조합인지 알 수 있다.
poly.get_feature_names_out()
결과 → array(['x0', 'x1', 'x2', 'x0^2', 'x0 x1', 'x0 x2', 'x1^2', 'x1 x2', 'x2^2'], dtype=object)
이유는 알 수 없지만 get_feature_names()를 하면 'PolynomialFeatures' object has no attribute 'get_feature_names' 오류가 발생해서 get_feature_names_out()을 했다.
test_poly = poly.transform(test_input)
테스트 세트도 변환하자.
from sklearn.linear_model import LinearRegression
lr = LinearRegression()
lr.fit(train_poly, train_target)
print(lr.score(train_poly, train_target))
print(lr.score(test_poly, test_target))
0.9903183436982125
0.9714559911594111
좋다. 과소 적합은 더이상 나타나지 않는다.
이번에는 5제곱까지 특성을 만들어서 추가해보자.
poly = PolynomialFeatures(degree=5, include_bias=False)
poly.fit(train_input)
train_poly = poly.transform(train_input)
test_poly = poly.transform(test_input)
print(train_poly.shape)
(42, 55)
이제 모델을 다시 훈련해보자.
lr.fit(train_poly, train_target)
print(lr.score(train_poly, train_target))
print(lr.score(test_poly, test_target))
0.9999999999996433
-144.40579436844948
훈련 세트는 거의 완벽하게 학습이 되었다. 하지만 과대적합이 되어 테스트 세트에는 형편없는 점수를 만들게 된다.
이 문제를 해결해보자
머신러닝 모델이 훈련 세트를 너무 과도하게 학습하지 못하도록 훼방하는 것을 말한다.
선형 회귀 모델의 경우 특성에 곱해지는 계수의 크기를 작게 만드는 일이다.
왼쪽은 과도 학습한 상태이고, 오른쪽은 기울기를 줄여 보편적인 패턴을 학습시킨 것이다.
그 전에, 특성의 스케일이 정규화되지 않으면 곱해지는 계수 값도 차이가 나므로 정규화가 먼저 필요하다. StandardScaler 클래스를 사용해보자.
from sklearn.preprocessing import StandardScaler
ss = StandardScaler()
ss.fit(train_poly)
train_scaled = ss.transform(train_poly)
test_scaled = ss.transform(test_poly)
ss 객체를 초기화한 후 train_poly로 객체를 훈련한다.
그러면 표준 점수로 변환된 train_scaled, test_scaled가 준비된다.
이렇게 선형 회귀 모델에 규제를 추가한 모델을 릿지와 라쏘라고 부른다.
일반적으로 릿지를 더 선호한다.
from sklearn.linear_model import Ridge
ridge = Ridge()
ridge.fit(train_scaled, train_target)
print(ridge.score(train_scaled, train_target))
print(ridge.score(test_scaled, test_target))
0.9896101671037343
0.9790693977615387
좋다. 규제의 양을 임의로 조절 가능한데, alpha 매개변수로 규제 강도를 조절하면 된다.
적절한 alpha 값을 찾는 한 가지 방법은 alpha 에 대한 값의 그래프를 그려보는 것이다.
import matplotlib.pyplot as plt
train_score = []
test_score = []
alpha_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.xlabel('alpha')
plt.ylabel('R^2')
plt.show()
위는 훈련 세트 그래프, 아래는 테스트 세트 그래프이다.
적절한 alpha 값은 두 그래프가 가장 가깝고 테스트 세트의 점수가 가장 높은 -1, 즉 이다.
0.1로 최종 모델을 훈련해보자.
ridge = Ridge(alpha=0.1)
ridge.fit(train_scaled, train_target)
print(ridge.score(train_scaled, train_target))
print(ridge.score(test_scaled, test_target))
0.9903815817570367
0.9827976465386928
릿지와 비슷하다. Ridge → Lasso 클래스로 바꾸기만 하면 된다.
from sklearn.linear_model import Lasso
lasso = Lasso()
lasso.fit(train_scaled, train_target)
print(lasso.score(train_scaled, train_target))
print(lasso.score(test_scaled, test_target))
0.989789897208096
0.9800593698421883
역시 과대적합을 잘 억제한 결과를 볼 수 있다.
여기서도 alpha 값을 조절해보자.
train_score = []
test_score = []
alpha_list = [0.001, 0.01, 0.1, 1, 10, 100]
for alpha in alpha_list:
lasso = Lasso(alpha=alpha, max_iter=10000)
lasso.fit(train_scaled, train_target)
train_score.append(lasso.score(train_scaled, train_target))
test_score.append(lasso.score(test_scaled, test_target))
plt.plot(np.log10(alpha_list), train_score)
plt.plot(np.log10(alpha_list), test_score)
plt.xlabel('alpha')
plt.ylabel('R^2')
plt.show()
라쏘 모델에서의 최적의 alpha 값은 1 즉 10이다.
다시 모델을 훈련해보자.
lasso = Lasso(alpha=10)
lasso.fit(train_scaled, train_target)
print(lasso.score(train_scaled, train_target))
print(lasso.score(test_scaled, test_target))
0.9888067471131867
0.9824470598706695
print(np.sum(lasso.coef_ == 0))
결과 → 40
라쏘 모델은 계수 값을 0으로 만들 수 있다고 했는데, coef_ 속성에 저장된 계수 중 0인 것을 헤아려보면 총 40개이다. 즉 라쏘 모델이 사용한 특성은 15개밖에 되지 않는다.
이러한 특징 때문에 라쏘 모델을 유용한 특성 골라내는 용도로도 사용할 수 있다.
'PolynomialFeatures' object has no attribute 'get_feature_names' 오류가 발생
문제 : PolynomialFeatures를 통해 새로운 특성 조합을 만들고, 데이터를 변환하는 과정에서 어떤 조합이 만들어냈는지 확인하기 위해 get_feature_names() 메서드를 사용하고자 했다.
해결 : Document를 보면 get_feature_names_out으로 메서드 이름이 바뀐 것 같다.