[Machine Learning][2] 머신러닝 주요 단계 (1)
1. 데이터 찾기
2. 큰 그림 보기
- 문제 정의
1. 비즈니스의 목적이 무엇인지 알아야 한다. 목적을 아는 것은 문제 구성, 어떤 알고리즘을 선택할 것인지, 성능 지표, 모델 튜닝 등에 얼마 만큼을 투자할지 결정해주기 때문에 매우 중요하다.
2. 현재 솔루션은 어떻게 구성되어 있는지 알아야 한다. 문제 해결 방법 뿐만 아니라 참고 성능을 사용할 수 있다.
3. 모델 훈련에 어떤 지도 방식이 필요한지 결정한다.
- 성능 측정 지표 선택
회귀 문제의 전형적인 성능 지표는 평균 제곱근 오차(RMSE)이다. 오차가 커질 수록 이 값은 커지므로 예측에 얼마나 많은 오차가 있는지 알려주는 역할을 한다.
m : 샘플 수 / x(i) : i번째 샘플의 전체 특성값의 벡터 / y(i) : x의 해당 레이블
/ X : 샘플의 모든 특성 값 => 샘플 하나가 하나의 행 / h : 시스템의 예측 함수 (가설)
데이터에서 이상치로 보이는 구간이 많다면 평균 절대 오차 (MAE)를 고려해보는 것이 좋다.
- 가정 검사
마지막으로 지금까지 만든 가정을 나열하고 검사해본다. 이 과정에서 문제를 일찍 발견할 수도 있다.
3. 실습
3.1 데이터 다운로드
from pathlib import Path
import pandas as pd
import tarfile
import urllib.request
def load_housing_data():
tarball_path = Path("datasets/housing.tgz") # 파일 찾기
if not tarball_path.is_file():
Path("datasets").mkdir(parents=True, exist_ok=True)
url = "https://github.com/ageron/data/raw/main/housing.tgz"
urllib.request.urlretrieve(url, tarball_path)
with tarfile.open(tarball_path) as housing_tarball:
housing_tarball.extractall(path="datasets")
return pd.read_csv(Path("datasets/housing/housing.csv"))
housing = load_housing_data()
3.2 데이터 구조 확인
housing.head()
각 행은 하나의 구역이고 특성은 위와 같이 10개가 있다.
housing.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 20640 entries, 0 to 20639
Data columns (total 10 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 longitude 20640 non-null float64
1 latitude 20640 non-null float64
2 housing_median_age 20640 non-null float64
3 total_rooms 20640 non-null float64
4 total_bedrooms 20433 non-null float64
5 population 20640 non-null float64
6 households 20640 non-null float64
7 median_income 20640 non-null float64
8 median_house_value 20640 non-null float64
9 ocean_proximity 20640 non-null object
dtypes: float64(9), object(1)
memory usage: 1.6+ MB
info() 메서드는 데이터에 관한 간략한 설명을 보여준다.
=> 데이터셋에는 20,640개의 샘플이 들어있다.
=> total_bedrooms 특성은 20,433개만 널값이 아니고 207개의 구역은 이 특성을 가지고 있지 않다는 뜻이다.
=> ocean_proxinity 필드만 빼고 모든 특성이 숫자형이다.
housing.describe()
describe() 메서드는 숫자형 특성의 요약 정보를 보여준다.
# 추가 코드 - 고해상도 PNG 파일로 그래프를 저장하기 위한 코드
IMAGES_PATH = Path() / "images" / "end_to_end_project"
IMAGES_PATH.mkdir(parents=True, exist_ok=True)
def save_fig(fig_id, tight_layout=True, fig_extension="png", resolution=300):
path = IMAGES_PATH / f"{fig_id}.{fig_extension}"
if tight_layout:
plt.tight_layout()
plt.savefig(path, format=fig_extension, dpi=resolution)
import matplotlib.pyplot as plt
housing.hist(bins=50, figsize=(12,8))
# 추가 코드 – 다음 다섯 라인은 기본 폰트 크기를 지정합니다
plt.rc('font', size=14)
plt.rc('axes', labelsize=14, titlesize=14)
plt.rc('legend', fontsize=14)
plt.rc('xtick', labelsize=10)
plt.rc('ytick', labelsize=10)
housing.hist(bins=50, figsize=(12, 8))
save_fig("attribute_histogram_plots") # 추가 코드
plt.show()
1. 중간 소득 특성이 US 달러로 표현되지 않았다. => 머신러닝에서는 전처리된 데이터가 흔하지만 데이터가 어떻게 계산된 것인지 정확하게 이해 해야 한다.
2. 중간 주택 연도 (housing_median_age)와 중간 주택 가격 (median_house_value) 은 최댓값과 최솟값을 한정했다.
중간 주택 가격 같은 경우는 레이블로 사용되기 때문에 문제가 될 수 있다.
3. 특성들의 스케일이 많이 다르다.
4. 분포가 오른쪽 꼬리가 더 길다. 이런 형태는 패턴을 발견하기 어렵다.
3.2 테스트 세트 만들기
import numpy as np
def shuffle_and_split_data(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 = shuffle_and_split_data(housing, 0.2)
len(train_set)
>>> 16512
len(test_set)
>>> 4128
테스트 세트를 생성하려면 랜덤으로 어떤 샘플을 선택해서 데이터셋의 20% 정도 떼어놓는다.
from zlib import crc32
def is_id_in_test_set(identifier, test_ratio):
return crc32(np.int64(identifier)) < test_ratio * 2**32
def split_data_with_id_hash(data, test_ratio, id_column):
ids = data[id_column]
in_test_set = ids.apply(lambda id_: is_id_in_test_set(id_, test_ratio))
return data.loc[~in_test_set], data.loc[in_test_set]
프로그램을 다시 실행되면 다른 테스트 세트가 생성된다. 이 과정을 계속 진행하면 전체 데이터셋을 보는 것과 다름이 없다.
일반적인 해결책은 샘플의 식별자를 사용하여 테스트 세트로 보낼지 말지 결정한다.
housing_with_id = housing.reset_index() # `index` 열 추가
train_set, test_set = split_data_with_id_hash(housing_with_id, 0.2, "index")
주택 데이터셋에는 식별자 컬럼이 없다. 대신 행의 인덱스를 ID로 사용하면 해결할 수 있다.
housing_with_id["id"] = housing["longitude"] * 1000 + housing["latitude"]
train_set, test_set = split_data_with_id_hash(housing_with_id, 0.2, "id")
고유 식별자를 사용할 때는 안전한 특성을 사용해야 한다. 예를 들어 위의 코드 같이 구역의 위도와 경도 같은 값을 사용한다.
from sklearn.model_selection import train_test_split
train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)
사이킷런은 데이터셋을 여러 서브셋으로 나누는 방법을 제공한다.
가장 간단한 함수는 train_test_split 으로 앞서 정의한 shuffle_and_split_data() 함수와 비슷하다.
# 추가 코드 – 나쁜 샘플을 얻을 확률 10.7%를 계산하는 방법
from scipy.stats import binom
sample_size = 1000
ratio_female = 0.511
proba_too_small = binom(sample_size, ratio_female).cdf(485 - 1)
proba_too_large = 1 - binom(sample_size, ratio_female).cdf(535)
print(proba_too_small + proba_too_large)
# 추가 코드 – 나쁜 샘플을 얻을 확률을 시물레이션으로 계산 하는 방법
np.random.seed(42)
samples = (np.random.rand(100_000, sample_size) < ratio_female).sum(axis=1)
((samples < 485) | (samples > 535)).mean()
계층 별로 데이터셋에 충분한 샘플 수가 있어야 한다. 그렇지 않으면 계층의 중요도를 추정하는데 편향이 발생할 것이다.
즉, 너무 많은 계층이면 안 되며 각 계층이 충분히 커야 한다.
housing["income_cat"] = pd.cut(housing["median_income"],
bins=[0., 1.5, 3.0, 4.5, 6., np.inf],
labels=[1, 2, 3, 4, 5])
housing["income_cat"].value_counts().sort_index().plot.bar(rot=0, grid=True)
plt.xlabel("Income category")
plt.ylabel("Number of districts")
save_fig("housing_income_cat_bar_plot") # extra code
plt.show()
pd.cut() 함수를 사용해 카테고리 5개를 가진 소득 카테고리 특성을 만든다.
사이킷런은 여러 가지 분할기 클래스를 제공한다. 분할기란 데이터셋을 훈련 세트와 테스트 세트로 분할하는 다양한 전략을 구현한 것이다.
from sklearn.model_selection import StratifiedShuffleSplit
splitter = StratifiedShuffleSplit(n_splits=10, test_size=0.2, random_state=42)
strat_splits = []
for train_index, test_index in splitter.split(housing, housing["income_cat"]):
strat_train_set_n = housing.iloc[train_index]
strat_test_set_n = housing.iloc[test_index]
strat_splits.append([strat_train_set_n, strat_test_set_n])
strat_train_set, strat_test_set = strat_splits[0]
사이킷런에 있는 split() 메서드는 데이터가 아니라 인덱스를 반환한다.
이 코드는 한 데이터셋으로 각각 다른 10개의 계층 분할을 생성한다.
strat_train_set, strat_test_set = train_test_split(
housing, test_size=0.2, stratify=housing["income_cat"], random_state=42)
계층적 샘플링은 자주 사용되기 때문에 하나의 분할이 필요한 경우 위와 같이 만들 수 있다.
strat_test_set["income_cat"].value_counts() / len(strat_test_set)
>>> income_cat
3 0.350533
2 0.318798
4 0.176357
5 0.114341
1 0.039971
Name: count, dtype: float64
# 추가 코드 – 그림 2–10를 생성합니다.
def income_cat_proportions(data):
return data["income_cat"].value_counts() / len(data)
train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)
compare_props = pd.DataFrame({
"Overall %": income_cat_proportions(housing),
"Stratified %": income_cat_proportions(strat_test_set),
"Random %": income_cat_proportions(test_set),
}).sort_index()
compare_props.index.name = "Income Category"
compare_props["Strat. Error %"] = (compare_props["Stratified %"] /
compare_props["Overall %"] - 1)
compare_props["Rand. Error %"] = (compare_props["Random %"] /
compare_props["Overall %"] - 1)
(compare_props * 100).round(2)
전체 데이터셋에 있는 소득 카테고리의 비율과 계층 샘플링으로 만든 소득 카테고리 비율은 비슷하게 나왔다.
반면 일반 랜덤 샘플링의 비율은 많이 다르다.
4. 데이터 탐색, 시각화
- 지리적 데이터 시각화
housing.plot(kind="scatter", x="longitude", y="latitude", grid=True)
save_fig("bad_visualization_plot") # extra code
plt.show()
데이터셋의 지리 정보를 산점도로 시각화한다. 캘리포니아 지역은 잘 나타나지만 특별한 패턴을 찾기에는 어렵다.
코드에서 alpha = 0.2 로 주면 데이터가 밀집된 영역을 잘 보여준다.
housing.plot(kind="scatter", x="longitude", y="latitude", grid=True,
s=housing["population"] / 100, label="population",
c="median_house_value", cmap="jet", colorbar=True,
legend=True, figsize=(10, 7))
save_fig("housing_prices_scatterplot") # extra code
plt.show()
색이 붉어질 수록 높은 가격, 파란색은 낮은 가격, 큰 원은 인구가 밀집된 지역을 나타낸다.
2. 상관관계 조사하기
corr_matrix["median_house_value"].sort_values(ascending=False)
>>>
median_house_value 1.000000
median_income 0.688380
total_rooms 0.137455
housing_median_age 0.102175
households 0.071426
total_bedrooms 0.054635
population -0.020153
longitude -0.050859
latitude -0.139584
Name: median_house_value, dtype: float64
중간 주택 가격과 다른 특성 사이의 상관관계 크기를 특정한다.
상관관계 범위는 -1 ~ 1까지다. 1에 가까우면 양의 상관관계를, -1에 가까우면 음의 상관관계를 나타낸다.
위도와 중간 주택 가격은 약한 음의 상관관계를 보입니다.
from pandas.plotting import scatter_matrix
attributes = ["median_house_value", "median_income", "total_rooms",
"housing_median_age"]
scatter_matrix(housing[attributes], figsize=(12, 8))
save_fig("scatter_matrix_plot") # 추가 코드
plt.show()
특성 사이의 상관관계를 확인하려면 특성 간 산점도를 그려본다.
대각선 위치에는 각 변수 자신에 대한 산점도가 출력된다. 하지만 이는 그냥 직선이므로 유용하지 않다.
중간 주택 가격을 예측하는데 중간 소득이 가장 유용해 보인다.
상관관계가 매우 강하고 가격의 한곗값이 수평선으로 잘 보인다.
3. 특성 조합으로 실험하기
housing["rooms_per_house"] = housing["total_rooms"] / housing["households"]
housing["bedrooms_ratio"] = housing["total_bedrooms"] / housing["total_rooms"]
housing["people_per_house"] = housing["population"] / housing["households"]
orr_matrix = housing.corr(numeric_only=True)
corr_matrix["median_house_value"].sort_values(ascending=False)
>>>
median_house_value 1.000000
median_income 0.688380
rooms_per_house 0.143663
total_rooms 0.137455
housing_median_age 0.102175
households 0.071426
total_bedrooms 0.054635
population -0.020153
people_per_house -0.038224
longitude -0.050859
latitude -0.139584
bedrooms_ratio -0.256397
Name: median_house_value, dtype: float64
bedrooms_ratio의 특성은 전체 방 개수나 침실 개수보다 중간 주택 가격과의 상관관계가 높다.
이러한 탐색 단계는 오히려 완벽하지 않다. 인사이트를 얻는 것이 합리적인 프로토타입을 만드는 데 도움이 될 것이다.
그래서 프로토타입을 만들고 실행한 후 인사이트를 얻고 이 탐색 단계로 돌아오게 된다.