모델과 파라미터 찾기
[4주차]
작성일자: 2024-04-01
팀 구성원: 도우진, 오소민,오현정,정원준,최준헌
4.1.1 사이킷런을 통해 학습과 예측에 사용할 데이터셋 나누기
1. 라이브러리 로드하기
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline
데이터 분석을 위한 pandas, 수치 계산을 위한 numpy, 시각화를 위한 seaborn, matplotlib.pyplot을 로드한다.
2. 데이터셋 로드하기
df = pd.read_csv("data/diabetes_feature.csv")
df.shape
(768, 16)
전처리한 데이터셋을 로드하고, df.shape를 통해 데이터셋의 행 개수와 열 개수를 확인한다.
df.head()
df.head()를 이용해 데이터를 미리보기한다.
3. 학습과 예측에 사용할 데이터 만들기
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42)
사이킷런에서 제공하는 model_selection의 train_test_split으로 만든다.
테스트셋을 test_size 옵션을 통해 20%로 지정하고, 매번 샘플링할 때마다 데이터가 달라질 수 있으므로 임의로 random_state를 지정한다.
df.columns
해당 컬럼 중에서 지난 주에 스코어가 잘 나왔던 feature들만 사용하기 위해 불필요한 feature들은 제거한다.
X = df[['Glucose', 'BloodPressure', 'SkinThickness',
'BMI', 'DiabetesPedigreeFunction', 'Age', 'Pregnancies_high',
'Insulin_nan', 'low_glu_insulin']]
X.shape
(768, 9)
Pregnancies 대신 Pregnancies_high 값을 사용하고, 불필요한 feature를 제거해준다.
y = df['Outcome']
y.shape
(768,)
Outcome을 정답 데이터셋으로 저장한다.
X_train
랜덤하게 데이터가 섞인 것을 확인할 수 있다.
X_train.shape, y_train.shape
((614, 9), (614,))
train 세트의 문제와 정답의 데이터 수를 확인해준다.
X_test.shape, y_test.shape
((154, 9), (154,))
test 세트의 문제와 정답의 데이터 수를 확인해준다.
4.1.2 랜덤값을 고정하여 디시전트리로 학습과 예측하기
1. 학습과 예측하기
from sklearn.tree import DecisionTreeClassifier
model = DecisionTreeClassifier(max_depth=11, random_state=42)
model
DecissonTreeClassfier를 선언하여 사용한다.
model.fit(X_train, y_train)
모델을 학습시켜준다.
y_predict = model.predict(X_test)
y_predict
예측을 하고, 그 결과를 y_predict에 담는다. 0이면 당뇨병은 발병하지 않고, 1이면 당뇨병이 발병한다.
2. 정확도 측정하기
abs(y_predict - y_test).sum()
28
다르게 예측한 개수를 구해준다. 다르게 예측한 개수로 28개가 나와 저번의 다르게 예측한 개수보다 더 많이 틀렸는데, 이는 데이터를 랜덤하게 섞었기 때문에 발생하는 것이다.
또한, 모델링을 할 때마다 다르게 예측한 개수가 달라지는데, 만약 동일 조건에서 동일한 정확도가 나오게 하고 싶다면 앞에서 지정한 것처럼 random_state를 같은 숫자로 지정해줘야 한다.
from sklearn.metrics import accuracy_score
accuracy_score(y_test, y_predict) * 100
81.81818181818183
accuracy_score를 이용해 정확도 점수를 구해준다. 이를 통해 저번주의 모델에서는 오버피팅이 발생한 것으로 예측할 수 있다.
4.1.3 최적의 max_depth 파라미터 값 찾기
DecisionTree의 파라미터 값을 조정함에 따라 모델의 성능이 달라 질 수 있다고 했다.
그럼 파라미터의 변화를 어떻게 알 수 있을까? plot_tree로 시각화 해보면 알아볼 수 있다.
다음 그림은 sklearn.tree에서 plot_tree를 불러와서 feature_names도 정의해주고 색깔도 넣어준 트리이다.
또 한가지 자세히 살펴봐야할 것은 밑으로 내려갈수록 조건이 세부적으로 나뉜다.
gini계수가 0인 곳은 더이상 트리를 분할하지 않는 것도 볼 수 있다.
max_depth는 트리의 깊이를 결정한다. 뿌리(root)부분을 제외하고 가로로 깊이1,깊이2...로 정의한다.
트리가 너무 얕으면 UnderFitting 결과가 나타나기에 최적의 max_depth 값을 찾는 게 중요하다.
<for문을 통해 최적의 max_depth 찾기>
from sklearn.metrics import accuracy_score
for max_depth in range(3, 12):
model = DecisionTreeClassifier(max_depth=max_depth, random_state=42)
y_predict = model.fit(X_train, y_train).predict(X_test)
score = accuracy_score(y_test, y_predict) * 100
print(max_depth, score)
1.accuarcy_score 알고리즘을 불러온다 → 추후 예측 정확도를 판단하는데 쓰인다.
2. for문 안에 넣고 반복문을 돌린다
※ 이때,DecisionClassifier에 max_depth 옵션과 random_state 옵션을 넣어준다.
※ random_state는 모델이 반복해서 돌려도 고정해주는 역할을 한다.
3. max_depth와 score를 함께 print 해서 보자
max_depth가 4일때 예측의 성능이 가장 높음을 알 수 있다.
또한, 너무 깊어도 성능이 떨어짐을 보인다.
4.1.4 GridSearchCV를 사용하여 최적의 max_depth 값 찾기
GridSearchCV 방법은 사용하여 지정된 매개변수의 모든 조합에 대해 교차 검증을 수행하고 최적의 매개변수 조합을 찾는 데 사용된다.
GridSeachCV에는 max_depth뿐만 아니라 min_sample_split, max_feature,random_state같은 파라미터도 있다.
최적의 매개변수를 사용하여 best_estimator_, best_params_, best_score_ 등의 속성에 접근할 수 있습니다.
이를 통해 최적의 모델을 얻을 수 있습니다.
<GridSearchCV 매개변수>
- estimator: 사용할 모델 객체이다. EX)DecisionTreeClassifier나 RandomForestRegressor와 같은 모델 객체
- param_grid: 딕셔너리 형태로 매개변수 이름을 키로, 탐색할 매개변수 값의 리스트를 값으로 지정한다.
- scoring: 모델의 성능을 평가하는 데 사용되는 지표이다. 기본값은 None으로, 우리가 하는 학습에선 "accuracy"를 사용한다.
- cv: 교차 검증을 위해 사용되는 Fold 수이다.
- n_jobs: 병렬 실행을 위한 작업 수이다. -1로 설정하면 컴퓨터 내 모든 자원들을 사용하여 돌릴 수 있다.
- verbose: 로그(과정에 대한 상세 정보)를 출력한다.숫자가 클수록 더 많은 정보를 산출한다.
- refit: 최적의 하이퍼파라미터를 찾은 후, 전체 데이터셋에 대해 최적의 매개변수를 사용하여 재학습할지를 지정한다. 기본값은 True이다.
<실습>
from sklearn.model_selection import GridSearchCV
model = DecisionTreeClassifier(random_state=42)
param_grid = {"max_depth": range(3, 12),
"max_features": [0.3, 0.5, 0.7, 0.9, 1]}
clf = GridSearchCV(model, param_grid=param_grid, n_jobs=-1, cv=5, verbose=2)
clf.fit(X_train, y_train)
1. sklearn에서 GridSearchCV를 끌어온다.
2. model 객체를 정의해준다.
3. param_grid 값 안에 탐색할 매개변수 값을 넣어준다.
(단, 딕셔너리 형태로 담아준다)
max_depth의 범위는 3~12까지며, max_features는 30%,50%,70%,90%,100%가 있다.
4. 가능한 모든 자원들을 사용하며 fold의 수는 5개로 분할한다. 로그 값도 자세히 출력한다.
5. 마지막으로 모델 학습기능을 적용한다.
#가장 성능이 좋은 파라미터를 찾아준다
clf.best_params_
가장 성능이 좋은 파라미터는 max_depth가 5이고 max_features가 0.7일때 이다.
clf.best_score_
최적의 성능을 가진 모델을 이렇게 찾을 수 있다.
4.1.5 RandomSearchCV를 사용해서 최적의 하이퍼 파라미터 값 찾기
앞서 GridSearch는 사용자가 설정한 범위 안에서만 parameter 값을 탐색하기에, 최적의 하이퍼 파라미터를 찾는 데에 한계가 있다. 따라서 랜덤한 값을 탐색하는 RandomSearch를 사용해보도록 하자.
max_depth = np.random.randint(3,20,10) # 3~19를 10개 뽑아서 리스트로 출력함
max_features = np.random.uniform(0.7, 1.0, 100)
param_distributions = {"max_depth":max_depth,
"max_features":max_features,
"min_samples_split":list(range(2,7))
}
param_distributions
우선 모델에 사용될 하이퍼 파라미터의 범위를 랜덤하게 지정하는 코드를 만들었다.
max_depth 파라미터 : 3에서 19까지의 숫자 중 무작위로 10개를 뽑아서 리스트로 출력한다.
max_features 파라미터 : 0.7에서 1.0까지 중 무작위로 100개를 뽑아서 리스트로 출력한다.
min_samples_split : 모델이 최소한 2개부터 최대 6개의 샘플을 가진 노드에서만 분할을 수행하도록 제한한다.
결과 :
이제 이 param_distributions을 RandomSearch 모델에 넣어보도록 한다.
from sklearn.model_selection import RandomizedSearchCV
clf = RandomizedSearchCV(model,
param_distributions,
n_iter=1000,
scoring="accuracy",
n_jobs=-1,
cv=5,
random_state=42)
clf.fit(X_train, y_train)
sklearn.model_selection 에서 RandomizedSearchCV를 불러 온후, clf 라는 변수에 hyper parameter들을 추가한 RandomizedSearchCV 모델을 넣는다.
n_iter : '1000'번을 돌면서 최적의 hyper parameter를 찾는다.
scoring : 모델의 성능을 측정하는 데에 "정확도"를 사용한다.
n_jobs=-1 : 모든 CPU 자원을 사용하여 처리한다.
cv : '5'번의 cross validation을 시행한다.
random_state : 항상 동일한 무작위 결과가 나오게 한다. 오직 값을 보정하는 역할로, 수치의 높낮이는 상관없다.
# 최적의 하이퍼 파라미터
clf.best_params_
{'min_samples_split': 6, 'max_features': 0.9550861171188045, 'max_depth': 9}
# 학습 셋에서의 최고 정확도
clf.best_score_
0.8713847794215648
# 예측 셋에서의 최고 정확도
clf.score(X_test,y_test)
0.8181818181818182
RandomizedSearchCV 모델의 최적의 하이퍼 파라미터 값은 min_samples_split :6, max_features :0.955, max_depth : 9이다. 그리고 fit 한 결과 가장 좋은 정확도는 0.871 임을 알 수 있다. 학습한 모델에 X_test 를 넣고 y_test 로 정확도를 측정했을 때 약 82%의 정확도를 보여준다.
pd.DataFrame(clf.cv_results_).sort_values(by="rank_test_score").head()
더 좋은 score를 찾기 위해 범위를 조정하는 것도 필요하다. 위의 코드를 통해 score가 높은 순서대로 데이터프레임을 만들어보았다.
결과 :
이를 바탕으로 랜덤하게 설정하는 parameter의 값을 적절하게 바꾸어 모델에 학습시키는 과정이 중요하다. 이를 통해 더욱 정확도 있는 모델을 찾을 수 있다.
4.2.1 랜덤포레스트 사용하기
앞에서는 DecisionTreeClassifier 라는 모델 내의 parameter 범위를 조정하면서 성능을 높이기 위한 작업을 해왔다. 이번에는 다른 모델, 즉 Random Forest를 사용하여 성능을 높이고자 한다.
DecisionTreeClassifier 모델이 하나의 결정트리를 사용하는 것과 달리, Random Forest는 다수의 결정 트리를 생성하여 데이터를 분석한다. 이처럼 다수의 모델을 결합하여 더 나은 예측 가능성을 달성하는 기술을 "앙상블 방법" 이라고 한다. 초기 앙상블 방법인 "배깅(Booststrap aggregating)"은 여러 개의 모델을 독립적으로 훈련시키고, 각 모델의 결과를 평균화하여 최종적인 결과를 만드는 방식이다. RandomForest는 이러한 방식을 사용하여 학습하고 예측한다.
from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier(random_state=42)
model
sklearn.ensemble에서 RandomForestClassifier 모델을 불러온 후, 동일한 무작위 결과를 도출하기 위해 random_state를 설정한다.
# 학습
model.fit(X_train, y_train)
# 예측
y_predict = model.predict(X_test)
# 정확도 측정
(y_predict != y_test).sum()
20
from sklearn.metrics import accuracy_score
accuracy_score(y_test, y_predict)
0.870
만들어진 모델에 학습과 예측을 한 후 정확도를 측정해보자. 위의 정확도를 즉정하는 코드는 y_predict와 y_test 값이 같지 않는 데이터를 True(1)로 반환하고, 이들의 합을 구한 것이다. 즉, 예측 셋의 정답과 예측결과가 다른 데이터의 개수가 20개임을 의미한다.
그 후 사이킷런에 내장된 정확도를 구하는 함수를 통해 정확도를 구해보면 87%라고 출력된다. DecisionTreeClassifier를 사용했을 때보다 성능이 좋아진 모습이다.
model.feature_importances_
RandomForest는 결정 트리를 여러 개 만들기 때문에 시각화가 어렵다. 따라서 feature의 중요도를 구하고, 이를 시각화해본다.
feature_names = X_train.columns.tolist()
sns.barplot(x=model.feature_importances_, y=feature_names)
학습 셋의 열들의 이름을 리스트로 저장하여 "feature_names"에 넣은 후 막대 그래프를 그려보았다.
결과 :
인슐린이 가장 중요한 역할을 한다는 것을 파악할 수 있다. 대부분의 트리 알고리즘에서는 feature importance를 알 수 있는 모습이다.
4.2.2 그라디언트 부스팅(Gradient Boosting) 알고리즘 사용하기
앞서 4.2.1 강의에서 배운 Random Forest와 1주차부터 배운 Decision Tree 알고리즘과는 또 다른 Gradient Boosting 알고리즘에 대해 알아보자. Random Forest, Decision Tree 와 Gradient Boosting 알고리즘의 차이점에 대해 간략하게 설명하면, Decision Tree 모형은 단일 트리 모형으로 하나의 트리로 알고리즘을 구성하지만, 랜덤 포레스트와 그라디언트 부스팅은 기본 데이터에서 학습하는 과정에서 앙상블 기법을 사용해 트리간의 상호작용을 하여 정확도를 높이는 방법이다.
이러한 앙상블 기법에도 차이점은 있는데, 랜덤 포레스트는 배깅 방식을 사용하여 데이터를 복원 추출해 학습하는 반면, 그라디언트 부스팅은 이전 트리 모형의 예측 오차를 점점 보정하면서 예측 성능을 향상시키는 방향으로 학습한다는 차이점이 있다.
본론으로 넘어가서 그라디언트 부스팅 알고리즘을 사용해보자.
from sklearn.ensemble import GradientBoostingClassifier
model = GradientBoostingClassifier(random_state=42)
model
여기서 random_state=42는 앞에서도 배웠듯이 수많은 알고리즘 학습 갈래 중 강의와 같은 환경을 만들기 위해 설정한 임의의 값이다. 알고리즘을 구현하는 데에는 크게 비중있는 부분은 아니다.
그라디언트 부스팅 알고리즘을 불러올 때 사이킷런의 앙상블 패키지에서 불러오는 것을 확인할 수 있다.
강의에서는 처음에 max depth = 10 으로 설정했지만 그라디언트 부스팅은 처음에 약한 모델로 시작해서 학습하면서 정확도를 보정해나가는 알고리즘이기 때문에 학습의 깊이를 정해놓으면 너무 얕게 학습해 정확도가 떨어질 수 있다.
그래서 max_depth를 제거하고 모델을 학습시킨 결과
각 요소 별 중요도는 이렇게 나왔고, 이를 bar plot으로 나타내보니, 인슐린이 영향을 많이 미친 것을 알 수 있었다.
이후 모델의 정확도를 계산해보니 결과가 0.8441558441558441로 나온 것을 알 수 있었고, 앞서 사용한 랜덤포레스트 모델에서는 정확도가 87% 가량 나왔기 때문에 여기에서는 그라디언트 부스팅 알고리즘 기법보다 랜덤 포레스트 기법이 더 정확한 알고리즘이었다.
4.2.3 RamdomSearchCV 로 여러 알고리즘의 최적의 하이퍼 파라미터를 찾기(1)
from sklearn.model_selection import RandomizedSearchCV
max_depth = np.random.randint(2, 20, 10)
max_features = np.random.uniform(0.3, 1.0, 10)
param_distributions = {"max_depth": max_depth,
"max_features": max_features}
clf = RandomizedSearchCV(estimator,
param_distributions,
n_iter=100,
scoring="accuracy",
n_jobs=-1,
cv=5,
verbose=2
)
clf.fit(X_train, y_train)
RandomSearchCV을 이용하여 최적의 하이퍼 파라미터를 찾아보자.
일단 기본 Decision Tree모형에서 고려할 수 있는 하이퍼 파라미터인 최대 깊이와 노드에서 고려할 특성 수를 랜덤으로 설정해주자.
max_depth는 2에서 19 사이의 정수 중에서 랜덤하게 10개 선택
max_features는 0.3에서 1.0 사이의 실수 중에서 랜덤하게 10개 선택
param_distributions에 랜덤 서치를 통해 탐색할 하이퍼파라미터의 후보들을 딕셔너리 형태로 담는다.
n_iter는 랜덤한 조합을 몇 번이나 시도할지를 결정하는데 기본 10번을 반복한다. 여기서는 100을 넣었으니 100번을 시도한다.
n_jobs는 병렬 처리를 위해 사용할 CPU 코어의 개수를 지정하는데 여기서는 -1을 입력해 모든 가능한 코어를 사용한다는 뜻이다.
cv는 교차 검증을 위해 데이터를 몇 개의 fold로 나눌지를 결정합니다. 여기서는 5-fold 교차 검증을 사용한다. 데이터 셋을 5개의 조각으로 나누고 각 반복마다 4개의 조각을 train, 1개 조각을 test 데이터로 사용하며 모델을 평가한다는 뜻이다. 이렇게 총 5번의 시행을 통해 모델의 성능을 측정할 수 있다.
verbose를 2로 지정하여 로그를 찍는다. verbose는 0, 1, 2의 값을 가질 수 있는데, 숫자가 높아질 수록 실행 도중에 더 상세한 정보를 출력한다. 그니까 알고리즘이 어떻게 작동하고 이해하는 데에 도움이 될 수 있다.
clf.best_params_
를 입력하여 가장 좋은 최적의 하이퍼 파라미터 조합을 출력할 수 있다. 앞에서 말한 모델에서 랜덤 학습한 값중에 가장 좋은 성능을 내는 하이퍼 파라미터 값이 딕셔너리 형태로 나온다는 것이다.
최적의 파라미터 값을 적용한 모델의 정확도를 측정해보면 그 결과 0.85996268이 나온다.
최적의 모델을 찾기 위해 estimator를 3개 지정해서 하나의 코드블럭으로 실행해보자.
for 반복문을 통해 estimator 리스트에 있는 각 estimator에 대해 동일한 하이퍼 파라미터 튜닝과 교차 학습을 수행하며 성능을 평가해보자.
4.2.4 RamdomSearchCV 로 여러 알고리즘의 최적의 하이퍼 파라미터를 찾기(2)
● 위에서 생성한 estimators(머신러닝 모델 리스트)를 for문을 돌면서 실행하도록 함
1. 클래스 이름을 출력
for estimator in estimators:
print(estimator.__class__.__name__)
>> 출력 결과 (이름만 출력)
DecisionTreeClassifier
RandomForestClassifier
GradientBoostingClassifier
2. RandomSearchCV를 for문으로 변경 -> clf를 for문 안으로 넣기
for estimator in estimators:
clf = RandomizedSearchCV(estimator,
param_distributions,
n_iter=10,
scoring="accuracy",
n_jobs=-1,
cv=5,
verbose=2
)
clf.fit(X_train, y_train)
● clf.fit을 하게 되면 새로운 값이 들어가게 됨
3. 새로운 값을 resut 값 안에 넣음
● print 하지 않고 result에 넣은 후 출력
result = []
for estimator in estimators:
result.append(estimator.__class__.__name__)
result
>> 출력 결과 (Classifier들이 result 리스트 안에 들어감)
['DecisionTreeClassifier',
'RandomForestClassifier',
'GradientBoostingClassifier']
4. best_params_ , best_score_ 를 한 번에 보기 위해서 리스트에 넣기
● result를 for문 안쪽으로 넣게 되면 for문이 실행될 때마다 초기화 되기 때문에 다음 코드를 활용하여 results라는 리스트에 저장
results = []
for estimator in estimators:
result = []
result.append(estimator.__class__.__name__)
results.append(result)
results
>> 출력 결과 (Classifier list가 results 리스트 안에 들어감)
[['DecisionTreeClassifier',
'RandomForestClassifier',
'GradientBoostingClassifier']]
5. 랜덤 서치 수행
from sklearn.model_selection import RandomizedSearchCV
max_depth = np.random.randint(2, 20, 10)
max_features = np.random.uniform(0.3, 1.0, 10)
param_distributions = {"max_depth": max_depth,
"max_features": max_features}
# 결과를 저장할 빈 리스트 생성
results = []
# 모든 추정기에 대해 반복
for estimator in estimators:
# 결과를 저장할 임시 리스트 생성
result = []
# 만약 추정기가 DecisionTreeClassifier가 아니라면
if estimator.__class__.__name__ != 'DecisionTreeClassifier':
# n_estimators 하이퍼파라미터를 무작위로 선택하여 탐색하기 위해 param_distributions에 추가
param_distributions["n_estimators"] = np.random.randint(100, 200, 10)
# 랜덤 서치 생성
clf = RandomizedSearchCV(estimator,
param_distributions,
n_iter=100,
scoring="accuracy",
n_jobs=-1,
cv=5,
verbose=2
)
# 랜덤 서치 수행
clf.fit(X_train, y_train)
# 결과 리스트에 추정기 클래스 이름, 최적의 하이퍼파라미터, 최고의 교차 검증 점수, 테스트 세트 점수, 교차 검증 결과 저장
result.append(estimator.__class__.__name__)
result.append(clf.best_params_)
result.append(clf.best_score_)
result.append(clf.score(X_test, y_test))
result.append(clf.cv_results_)
# 결과 리스트를 전체 결과 리스트에 추가
results.append(result)
- n_iter: 랜덤 서치에서 무작위로 선택할 후보 하이퍼파라미터 조합의 개수
- scoring: 모델의 성능 평가 지표로서 정확도(accuracy) 사용
- n_jobs: 병렬 처리에 사용할 CPU 코어 수를 지정, -1로 설정하면 가능한 모든 코어를 사용
- cv: 교차 검증(fold)의 수를 지정
- verbose: 실행 과정을 출력할 지정, 값이 클수록 더 많은 정보가 출력
>> 출력 결과
6. 결과를 데이터 프레임 형태로 출력
● 컬럼명 지정하여 데이터 프레임 출력 -> 모델 별로 가장 성능이 좋은 파라키터 수치를 알 수 있음
df = pd.DataFrame(results,
columns=["estimator", "best_params", "train_score", "test_score", "cv_result"])
df
7. 모델에 각각 접근하여 상위에 있는 모델 순서대로 보거나 정확한 parameter 설정 수치보기
● "rank_test_score"열을 기준으로 정렬하여 출력
pd.DataFrame(df.loc[1, "cv_result"]).sort_values(by="rank_test_score")
여러 개의 모델과 다양한 파라미터들을 하나하나 테스트하기에는 너무 번거롭기 때문에 RandomSearchCV를 통해 가장 좋은 모델과 하이퍼파라미터를 찾음으로써 좀 더 효율적으로 모델의 성능을 향상시킬 수 있다.
<4주차 과제>
'Study > CODE 2기 [프로젝트로 배우는 데이터사이언스]' 카테고리의 다른 글
[Trillion(1조)] 프로젝트로 배우는 데이터사이언스_4주차 (0) | 2024.04.01 |
---|---|
[삼위일체(3조)] 프로젝트로 배우는 데이터 사이언스_3주차 (0) | 2024.03.25 |
[All in One(2조)] 프로젝트로 배우는 데이터사이언스_3주차 (0) | 2024.03.25 |
[Trillion(1조)] 프로젝트로 배우는 데이터사이언스_3주차 (0) | 2024.03.25 |
[Trillion(1조)] 프로젝트로 배우는 데이터사이언스_2주차 (0) | 2024.03.18 |