머신러닝을 위한 테스트 세트 만들기
데이터를 더 깊게 들여다보기 이전에 테스트 세트를 따로 떼어 놓아야 합니다.
테스트 세트는 추후 훈련된 데이터를 평가하는데 사용되기 때문에 해당 데이터 세트는 훈련에 사용되어서는 안됩니다.
1. 테스트 세트 만들기
테스트 세트를 만드는 일은 이론적으로 매우 간단합니다. 무작위로 어떠한 샘플을 선택하여 데이터셋의 20% 정도를 떼어 놓으면 됩니다.
아래는 데스트 세트를 생성하는 코드입니다.
Code
import numpy as np
def split_train_test(data, test_ratio):
shuffled_indices = np.random.permutation(len(data))
test_set_size = int(len(data) * test_ratio)
test_indices = shuffled_indices[:test_set_size]
train_indices = shuffled_indices[test_set_size:]
return data.iloc[train_indices], data.iloc[test_indices]
train_set, test_set = split_train_test(housing, 0.2)
print(len(train_set), "train +", len(test_set), "test")
Output
전체 20930개의 데이터가 16512개의 훈련 데이터, 4128개의 테스트 데이터로 나뉜 것을 확인할 수 있습니다.
하지만, 위의 코드는 완벽하지 않습니다. 프로그램을 다시 실행하면 다른 테스트 세트가 생성되기 때문입니다.
여러 번 계속하면 전체 데이터 셋을 보는 셈이므로 이러한 상황은 피하는 것이 좋습니다.
따라서, 일반적인 해결책으로 샘플의 식별자를 사용하여 테스트 세트로 보낼지 말지를 정하는 것입니다.
각 샘플마다 식별자의 해시값을 계산하여 해시의 마지막 바이트 값이 51보다 작거나(256의 20%정도) 같은 샘플만 테스트 세트로 보낼 수 있습니다.
새로운 테스트 세트는 새 샘플의 20%를 갖게 되지만, 이전에 훈련 세트에 있던 샘플은 포함시키지 않을 것입니다.
안타깝게도 주택 데이터셋에는 식별자 컬럼이 없습니다. 대신 행의 인덱스를 ID로 사용하면 간단히 해결됩니다.
행의 인덱스를 고유 식별자로 사용할 때 새 데이터는 데이터셋의 끝에 추가되어야 하고 어떤 행도 삭제되지 않아야 합니다.
이것이 불가능할 땐 고유 식별자를 만드는 데 안전한 특성을 사용해야 합니다. (예를 들어 위도와 경도)
다음은 이를 구현한 코드입니다.
Code
from zlib import crc32
def test_set_check(identifier, test_ratio):
return crc32(np.int64(identifier)) & 0xffffffff < test_ratio * 2**32
def split_train_test_by_id(data, test_ratio, id_column):
ids = data[id_column]
in_test_set = ids.apply(lambda id_: test_set_check(id_, test_ratio))
return data.loc[~in_test_set], data.loc[in_test_set]
housing_with_id = housing.reset_index()
train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "index")
housing_with_id["id"] = housing["longitude"] * 1000 + housing["latitude"]
train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "index")
위와 같은 복잡한 과정 없이 사이킷런에서는 데이터셋을 여러 서브셋으로 나누는 다양한 방법을 제공합니다.
가장 간단한 함수는 train_test_split으로, 앞서 우리가 만든 split_train_test와 아주 비슷하지만 두 가지 특징이 있습니다.
첫째, 앞서 설명한 난수 초기값을 지정할 수 있는 random_state 매개변수가 있고,
둘째, 행의 개수가 같은 여러 개의 데이터셋을 넘겨 같은 인덱스를 기반으로 나눌 수 있습니다.
코드는 아래와 같습니다.
Code
from sklearn.model_selection import train_test_split
train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)
print(len(train_set), "train +", len(test_set), "test")
Output
split_train_test() 함수와 동일한 결과를 얻을 수 있는 것을 확인했습니다.
지금까지는 순수한 무작위 샘플링 방식을 살펴 보았습니다. 데이터셋이 충분히 크다면 일반적으로 괜찮지만, 그렇지 않다면 샘플링 편향이 생길 가능성이 큽니다.
따라서, 전체를 대표할 수 있는 샘플들을 선택하기 위해 노력해야 합니다. 미국 인구의 51.3%가 여성이고 48.7%가 남성이라면, 잘 구성된 설문조사는 샘플에서도 이 비율을 유지해야 합니다. 이를 계층적 샘플링(Stratified Sampling) 이라고 합니다.
전체 모수는 계층(Strata) 이라는 동질의 그룹으로 나뉘고, 테스트 세트가 전체 모수를 대표하도록 각 계층에서 올바른 수의 샘플을 추출합니다.
기본 무작위 샘플링을 사용하면 49%보다 적거나 54%보다 많은 여성이 테스트 세트에 들어갈 확률이 약 12%입니다. 어느 방법을 사용하든 설문조사 결과를 편향시키게 됩니다.
따라서, 계층별로 데이터셋에 충분한 샘플 수가 있어야 합니다. 그렇지 않으면 계층의 중요도를 추정하는 데 편향이 발생할 수 있습니다.
이 말은 너무 많은 계층으로 나누면 안 된다는 뜻이고 각 계층이 충분히 커야 합니다. 다음 코드는 중간 소득을 1.5로 나누고(소득의 카테고리 수 제한),
ceil 함수를 사용하여 반올림해 소득 카테고리 특성을 만들고(이산적인 카테고리를 만들기 위해), 5보다 큰 카테고리는 5로 합칩니다.
아래의 코드를 통해 확인할 수 있습니다.
Code
housing["income_cat"] = np.ceil(housing["median_income"] / 1.5)
housing["income_cat"].where(housing["income_cat"] < 5, 5.0, inplace=True)
housing["income_cat"].hist(bins=50, figsize=(20, 15))
plt.show()
Output
소득 카테고리가 5개로 나뉜 히스토그램을 확인할 수 있습니다.
이제 소득 카테고리를 기반으로 계층 샘플링을 할 준비가 되었습니다. 사이킷런의 Stratified ShuffleSplit을 사용할 수 있습니다.
Code
from sklearn.model_selection import StratifiedShuffleSplit
split = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
for train_index, test_index in split.split(housing, housing["income_cat"]):
strat_train_set = housing.loc[train_index]
strat_test_set = housing.loc[test_index]
print(housing["income_cat"].value_counts() / len(housing))
Output
전체 주택 데이터셋에서 소득 카테고리의 비율을 확인할 수 있습니다.
비슷한 코드로 테스트 세트에 있는 소득 카테고리의 비율을 측정합니다. 아래의 그림 Fig. 1은 전체 데이터셋과 계층 샘플링으로 만든 테스트 세트에서 소득 카테고리 비율을 비교했습니다. 그림에서 보듯 계층 샘플링을 사용해 만든 테스트 세트가 전체 데이터셋에 있는 소득 카테고리의 비율과 거의 같습니다. 반면 일반 무작위 샘플링으로 만든 테스트 세트는 비율이 많이 달라졌습니다.
< Fig. 1. 계층 샘플링과 순수한 무작위 샘플링의 편향 비교 >
References
- 오렐리앙 제롱, '핸즈온 머신러닝', 한빛미디어, 2018