머신러닝 예제프로젝트 시작하기

박해선님이 번역한 핸즈온 머신러닝을 책을 읽고 정리한 자료입니다.
전체 소스코드는 아래 Reference에서 확인할수 있습니다.

여기서는 부동산 회사에 이제 막 고용된 데이터 과학자라고 가정하고 예제 프로젝트를 A-Z까지 진행해본다.

진행할 주요 단계는 다음과 같다.

  1. 큰 그림보기
  2. 데이터구하기
  3. 데이터를 탐색하고 시각화하기
  4. 머신러닝 알고리즘을 위한 데이터준비
  5. 모델선택후 훈련
  6. 모델 상세조정
  7. 솔루션 제시
  8. 배포후 모니터링 및 유지보수

1. 큰 그림 보기

처음 할 일은 캘리포니아 인구조사 데이터를 사용해 주택 가격 모델을 만든다.
데이터에는 각 구역별 인구, 중간소득, 중간 주택가격을 담고있다.
이 데이터로 모델을 학습시켜서 다른 측정데이터가주어졌을 때 구역의 중간 주택 가격을 예측해야한다.

1.1 문제 정의

첫번째로 비지니스의 목적이 정확히 무엇인지 파악해야 한다.

  1. 어떤 알고리즘을 선택할지
  2. 모델 평가에 어떤 성능지표를 사용할지
  3. 모델 튜닝을 위해 노력을 얼마나 투여할지

아래 그림을 보면 부동산회사에서 이 모델의 출력이 여러가지 다른 신호와 함께 다른 머신러닝 시스템에 입력으로 사용된다고 한다.

부동산 투자를 위한 머신러닝 파이프라인 

데이터에는 1990년 캘리포니아 인구 조사의 정보가 포함되어 있고 열은 다음과 같다.

  1. longitude : 집이 서쪽으로 얼마나 멀리 떨어져 있는지 측정. 서쪽으로 갈수록 높은값
  2. latitude : 집이 북쪽으로 얼마나 멀리 떨어져 있는지 측정. 북쪽으로 갈수록 높은값
  3. HousingMedianAge : 블록 내 주택의 중간 연령. 낮은 숫자는 새로운 건물이다.
  4. totalRooms : 블록 내 총 객실 수
  5. totalBedrooms : 블록 내 총 침실 수
  6. population : 블록 내에 거주하는 총 사람들 수
  7. households :한 블록에 대한 총 가구 수, 한 블록에 대한 사람들의 집단
  8. medianIncome : 한 블록 내 가구의 중위 소득(수만 달러로 측정)
  9. medianHouseValue : 가구의 중위 주택 가치 블록 내(미국 달러로 측정)
  10. oceanProximity : 바다/바다에 있는 집의 위치
  • 레이블된 훈련샘플 즉 중간주택가격이 있으니 전형적인 '지도학습'작업이며 수치를 예측해야하므로 '회귀문제'임을 알 수 있다.
  • 특성이 (구역의 인구, 중간소득)이므로 다중 회귀 (multiple regression)문제이다.
  • 각 구역마다 하나의 값을 예측하므로 '단변량 회귀'(univariate regression)문제이다.
  • 처리할 데이터가 매우 크면 맵리듀스를 사용해 배치학습을 여러서버로분할해 사용해야하지만 여기서는 크기가 작으므로 일반적인 배치학습이 적절하다.

1.2 성능 측정 지표선택

회귀 문제의 전형적인 성능 지표는 평균 제곱근 오차 RMSE(root mean square error) 이다.
오차가 커질수록 이 값이 커지므로 예측에 오류가 있는지 가늠하게 해준다.

$$
RMSE(X, h)=\sqrt{\frac{1}{m} \sum_{i=1}^{n}\left(h\left(X^{(i)}\right)-y^{(i)}\right)^{2}}
$$
위 식은 평균 제곱근 오차 RMSE이다.

  • RMSE(X,h) 는 가설h를 사용하여 일련의 샘플을 평가하는 비용 함수이다.
  • m은 RMSE를 측정할 데이터셋에 있는 샘플수이다. 예를들어 2,000개 구역의 검증셋을 평가한다면 m = 2,000 이다.
  • h는 예측함수이며 가설(hypothesis)라고도 한다. 시스템이 하나의 특성벡터x를 받으면 그 샘플에 대한 예측값을 출력한다. 시스템이 첫 번째 구역의 중간 주택가격을 5,000이라고 예측한다면 아래 식과 같다.

$$
\hat{y}^{(1)}=h\left(X^{(i)}\right) = 5,000
$$

RMSE가 일반적으로 회귀문제에 선호되는 성능 측정 방법이지만 이상치가 많은 구역이라면
MAE(mean absolute error) 평균 절대 오차를 사용한다.
평균절대편차(mean absolute deviation)이라고도 한다.

$$
MAE(X, h)=\frac{1}{m} \sum_{i=1}^{n}\left|h\left(X^{(i)}\right)-y^{(i)}\right|
$$

위 두 방법 모두 예측값이 벡터와 타깃값의 벡터 사이의 거리를 재는 방법이다.
거리 측정에는 여러가지 방법(노름 norm)이 가능하다.

  • 노름의 지수가 클수록 큰 값의 원소에 치우치며 작은 값은 무시된다. 그래서 RMSE가 MAE보다 조금 이상치에 민감하지만 이상치가 매우 드물다면 일반적으로 RMSE가 많이 사용된다.

1.3 가정검사

마지막으로 지금까지의 가정을 검사해야한다. 시스템이 출력한 구역의 가격이 다음 머신러닝의 입력으로 들어가는데 이 값이 ('저렴','보통','고가') 같은 카테고리를 사용하게 된다면 이 문제는 회귀문제가 아니고 분류문제가 되어버리기 때문에 작업전에 미리 철저하게 검사하여 다음단계로 진입해야한다.


2. 데이터 구조 살펴보기

수동으로 다운받아서 데이터를 확인하려면 여기 케글 데이터셋 에서 다운받을 수있다.

2.1 데이터 구조 확인

DataFrame에서 주로 사용하는 메서드는 아래와 같고
데이터 형태, 범위, 스케일, 분산 등을 이해할 수 있다.

메서드 설명
head() 전체 데이터를 열과 행으로 출력해준다.
info() 특성데이터 타입과 널이 아닌 개수를 확인하는데 유용하다.
value_counts() 범주형(categorical) 특성 확인
describe() 숫자형 특성의 요약정보 (count, mean, min, max)

2.2 테스트 세트 만들기

2.2.1 무작위 샘플링

방법 : 전체 데이터 세트에서 무작위로 20% 정도를 떼어놓는다.
이유 : 아래 데이터 스누핑 편향 (data snooping bias)을 방지한다.

  • 테스트 세트가 노출되면, 일반화 성능을 제대로 측정하지 못할 수 있음
  • 낙관적인 추정으로 인해 기대한 성능이 나오지 않을 수 있음
    코딩 : train_set, test_set = split_train_test(housing, 0.2)

위와 같이 코딩하면 난수를 이용하여 간단하게 나눌수 있지만 여러번 학습시킬때
매번 다른 테스트셋이 만들어져서 결국 전체세트가 반영되기때문에 수누핑편향방지가 되지 않는다.

해결방법은 np.random.permutation()호출전에 항상 같은 인덱스가 생성되도록 초기값을
지정하는 것이다. 'np.random.seed(42)'

그럼에도 불구하고 업데이트된 셋을 사용하면 또다시 문제가 되기 때문에 고유한 식별자를 사용하여 나누는 방법을 사용해야 한다. 아래는 각 샘플마다 식별자의 해시값을 계산하여 해시 최댓값의 20%보다 작은 샘플만 테스트로 보내는 코드이다.
이로서 새로운 테스트세트는 이전 훈련세트에 있던 샘플을 제외하고 20%를 나눌 수 있다.

from zlib import crc32

def test_set_check(indentifier, test_ratio):
    ## 비트연산을 사용하는 이유는 파이썬2와 호환성을 유지하기 위함이다.
    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]
    
# 주택 데이터에 식별자 컬럼이 없기때문에 행의 인덱스를 id로 사용했다.
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, "id")

아래는 데이터프레임이 레이블에 따라 여러 개로 나뉘에 있을때 유용한 방법이다.

from sklearn.model_selection import train_test_split

# 파이썬 리스트, 넘파이 배열, 판다스 데이터프레임 객체등을 입력으로도 받을수 있다.
train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)

2.2.2 계층적 샘플링(stratified sampling)

특성 수에 비해 데이터셋이 충분히 크다면 무작위 샘플링방식으로도 괜찮지만 그렇지 않은경우는
샘플링편향이 생길 가능성이 크다.

예를 들어 설문조사할때 무작위로 전화번호부에서 1000명을 뽑는 것 보다
전체 인구를 대표할 수 있는 1000명을 뽑는것이 현명하다.
미국 인구 52%가 여성이고 48%가 남성이라면 샘플에서도 이 비율을 유지해야한다.
이를 '계층적 샘플링' 이라고 한다.

여기서는 중간 소득이 주택 가격을 예측하는데 매우 중요하다고 가정할때
테스트 세트가 전체의 소득 카테고리를 대표해야 한다.
소득에 대한 히스토그램을 살펴보고 각 계층이 충분히 큰 카테고리 특성을 만들어 보면 아래와같다.


#카테고리1은 0~1,5까지,  2는 1.5~3 까지 범위가 된다.
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])
    
    

소득카테고리의 히스토그램

위에서 구한 소득 카테고리로 사이킷런의 Stratified ShuffleSplit을 사용해서 계층 셈플링을 할 수있다.

from sklearn.model_selection import StratifiedShuffleSplit

split = StratifiedShuffleSplit(n_split=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]

>>> strat_test_set["income_cat"].value_counts() / len(strat_test_set)

# income_cat 특성을 삭제해서 데이터를 원래 상태로 되돌린다.
# axis가 0이면 행이고, 1이면 열을 삭제한다.
# inplace의 기본값은 false인데 True이면 데이터프레임자체를 수정하고 아무것도 반환하지 않는다.

for set_ in (strat_train_set, strat_test_set):
    set_.drop("income_cat", axis=1, inplace=True)

아래는 순수한 무작위 샘플링과 계층샘플링 비교로 소득을 비교한 것인데 계층셈플링이 오류가 적다.

No 전체 계층샘플링 무작위샘플링 무작위오류률 계층오류률
1 0.039826 0.039971 0.040213 0.973236 0.364964
2 0.318847 0.318798 0.32437 1.73226 -0.015195
3 0.350581 0.350533 0.358527 2.266446 -0.01382
4 0.176308 0.176357 0.167393 -5.056334 0.02748
5 0.114438 0.114341 0.109496 -4.318374 -0.084674


3. 데이터 시각화

3.1 지리적 시각화

경도와 위도가 있기때문에 모든 구역을 산점도를 이용하여 나타내면 데이터 이해가 쉽다.
빨간색은 높은가격, 큰원은 인구가 밀집된 지역을 나타내므로 주택가격이 바다와 밀접한 곳과 인구 밀도에 관련이 매우 크다는 사실을 알 수 있다.

housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.4,
    s=housing["population"]/100, label="population", figsize=(10,7),
    c="median_house_value", cmap=plt.get_cmap("jet"), colorbar=True,
    sharex=False
)
plt.legend()

3.2 상관관계 조사

데이터셋이 크지 않으므로 모든 특성간의 표준 상관계수 (stancard correlation coefficient)를 corr() 메서드를 이용해 쉽게 계산할 수 있다.
corr_matrix = housing.corr()
상관관계의 범위는 -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))              

각 수치형 특성의 산점도와 히스토그램

3.3 특성 조합으로 실험

  • 정규분포와 유사하게 로그스케일 사용
  • 두 피처를 합침(PCA)
  • 필요없는 특성을 삭제

4.데이터준비

예측 변수와 타깃값에 각각 다른 편집을 하기위해 분리해준다.

housing = strat_train_set.drop("median_house_value", axis=1)
housing_labels = strat_train_set["median_house_value"].copy()

4.1 데이터 정제

대부분 머신러닝은 누락된 특성을 다루지 못하기에 아래 방법으로 수정한다.

  • 해당구역 제거
  • 전체특성 삭제
  • 누락값 채우기 (0, 평균, 중간값)
housing.dropna(subset=["total_bedrooms"])
housing.drop("total_bedrooms", axis=1)
median = housing["total_bedrooms"].median()
housing["total_bedrooms"].fillna(median, inplace=True)

사이킷런의 SimpleImputer로 누락된값 다루기

from sklearn.impute import SimpleInputer
imputer = SimpleImputer(strategy="median")
housing_num = housing.drop("ocean_proximity", axis=1)
imputer.fit(housing_num)
>>> imputer.statistics_
>>> housing_num.median().values

4.2 범주형 텍스트 다루기

사이킷런을 사용해서 범주형 값을 원-핫 벡터로 바꾸기위한 OneHotEncoder를 사용

from sklearn.preprocessing import OneHotEncoder
cat_encoder = OneHotEncoder()
housing_cat_1hot = cat_encoder.fit_transform(housing_cat)

출력을 보면 넘파이 배열이 아닌 희소행렬(sparse matrix)인데 이는 수천개의 범주특성일때 효율적이다 원래 원핫인코딩하면 1과 나머지모두 0으로 채워져있는데 0을 모두 메모리에 저장하면 낭비이므로 희소행렬은 0이 아닌 원소의 위치만 저장한다.

다시 넘파이 배열로 바꾸려면 'toarray()'를 사용하면된다.

국가코드, 직업, 생물 종류처럼 카테고리의 특성이 많을경우 이를 원핫 인코딩하면
트레이닝을 느리게하고 성능을 감소시킨다.

이 경우 간단하게는 범주형 입력값을 숫자형으로 바꾸면 된다.
ex) ocean_proximity -> 해안까지의 거리
국가코드 -> 국가 인구와 1인당 GDP

다른 방법은 각 카테고리를 임베딩(embedding) 이라 부르는 학습가능한 저차원벡터로 바꿀수 있다.

트레이닝하는동안 각 카테고리의 표현이 학습되어 이를 표현학습(representation learning)이라 한다.

4.3 커스텀 변환기

사이킷런이 유용한 변환기를 많이 제공하지만 특별한 정제작업이나 특성들을 조합하는등의 커스텀이 필요한 경우는 지원하는 덕 타이핑을 사용해서 사이킷런의 기능과 매끄럽게 연동할 수 있다.


from sklearn.base import BaseEstimator, TransformerMixin

# 열 인덱스 동적으로 구하기 
col_names = "total_rooms", "total_bedrooms", "population", "households"
rooms_ix, bedrooms_ix, population_ix, households_ix = 
    [housing.columns.get_loc(c) for c in col_names] # 열 인덱스 구하기

class CombinedAttributesAdder(BaseEstimator, TransformerMixin):
    def __init__(self, add_bedrooms_per_room=True): # *args 또는 **kargs 없음
        self.add_bedrooms_per_room = add_bedrooms_per_room
    def fit(self, X, y=None):
        return self  # 아무것도 하지 않습니다
    def transform(self, X):
        rooms_per_household = X[:, rooms_ix] / X[:, households_ix]
        population_per_household = X[:, population_ix] / X[:, households_ix]
        if self.add_bedrooms_per_room:
            bedrooms_per_room = X[:, bedrooms_ix] / X[:, rooms_ix]
            return np.c_[X, rooms_per_household, population_per_household,
                         bedrooms_per_room]
        else:
            return np.c_[X, rooms_per_household, population_per_household]

attr_adder = CombinedAttributesAdder(add_bedrooms_per_room=False)
housing_extra_attribs = attr_adder.transform(housing.to_numpy())


# 위에서 받은 housing_extra_attribs는 넘파이 배열이기때문에 열이 존재하지 않는다.
# 추가하려면 아래와같이 DataFrame으로 복구할 수 있다.
housing_extra_attribs = pd.DataFrame(
    housing_extra_attribs,
    columns=list(housing.columns)+["rooms_per_household", "population_per_household"],
    index=housing.index)
housing_extra_attribs.head()


4.4 특성 스케일링

머신러닝 알고리즘은 입력숫자 특성들의 스케일이 크게 다르면 잘 동작하지 않는다.
전체 방 개수의 범위는 6 ~ 39,320이지만 중간소득은 0 ~ 15이다.
따라서 모든 특성범위를 같게 만들어주는 방법으로 min-max 스케일링과 표준화가 널리 사용된다.

min-max스케일링이 가장 간편한데 이를 정규화라고 부른다. 0 ~ 1 범위에 들도록 값을 이동하고 스케일을 조정하면 된다.
방법은 데이터에서 최솟값을 뺀후 최댓값과 최솟값의 차이로 나누면 된다.
사이킷 런에서는 'MinMaxScaler(정규화)'와 'StandardScaler(표준화)'를 제공한다.

  • 모든 스케일링에서 훈련데이터에 대해서만 fit()메서드를 적용한 후 훈련세트와 테스트세트에 대해 transform()메서드를 사용한다.

4.5 변환 파이프라인

앞선 과정들의 단계가 많을경우 정확한 순서대로 실행되어야 하기에 사이킷런의 Pipeline클래스를 활용하여 위 과정의 숫자 특성을 처리한다.

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

num_pipeline = Pipeline([
   ('imputer', SimpleImputer(strategy="median")),
   ('attribs_addr', CombinedAttributesAdder()),
   ('std_scaler', StandardScaler()),
 ]}
 
housing_num_tr = num_pipeline.fit_transform(housing_num)

하나의 변환기로 모든 열을 처리할 수 있는 기능인 ColumnTransformer를 사용해 주택 가격 데이터에 전체 변환을 적용해본다

from sklearn.compose import ColumnTransformer

num_attribs = list(housing_num)
cat_attribs = ["ocean_proximity"]

full_pipeline = ColumnTransformer([
    ("num", num_pipeline, num_attribs),
    ("cat", OneHotEncoder(), cat_attribs),
    ])
    
housing_prepared = full_pipeline.fit_transform(housing)

OneHotEncoder는 희소행렬을 반환하지만 num_pipeline은 밀집행렬을 반환한다.
이 두개가 섞여 있을때 ColumnTransformer는 최종 행렬의 밀집정도를 추정한다.
밀집도가 임계값(기본값으로 sparse_threshold=0.3이다)보다 낮으면 희소행렬을 반환한다.
이 예제에서는 밀집 행렬이 반환된다.


5 모델 선택과 훈련

앞에서 문제정의 > 데이터로드 > 트레이닝세트작성 > 데이터정제 > 파이프라인 작성 까지 완료했다.
이제는 머신러닝 모델을 선택하고 트레이닝하면 끝이다.

5.1 트레이닝 세트 훈련하기

선형 회귀 모델 트레이닝 하기

from sklearn.linear_model import LinearRegression

lin_reg = LinearRegression()
lin_reg.fit(housing_prepared, housing_labels)

some_data = housing.iloc[:5]
some_labels = housing.iloc[:5]
some_data_prepared = full_pipeline.transform(some_data)

print("예측:", lin_reg.predict(some_data_prepared))
print("레이블:", list(some_labels))

전체 트레이닝 세트에 대한 회귀모델의 RMSE 측정하기

from sklearn.metrics import mean_squared_error
housing_predictions = lin_reg.predict(housing_prepared)
lin_mse = mean_squared_error(housing_labels, housing_predictions)
lin_rmse = np.sqrt(lin_mse)
lin_rmse

# 사이킷런  0.22 버전부터는 squared=False 매개변수로 mean_squared_error() 함수를 호출하면 RMSE를 바로 얻을 수 있다.
from sklearn.metrics import mean_absolute_error

lin_mae = mean_absolute_error(housing_labels, housing_predictions)
lin_mae

중간 주택 가격은 $120,000 ~ $265,000 사이지만 예측오차가 $68,628인것은 만족스럽지 못하다.
이런 상황은 특성들이 충분한 정보를 제공하지 못했거나 좋지못하다는 과소적합문제이다.
해결 방법은 더 좋은 모델을 선택하거나 트레이닝알고리즘에 좋은 특성을 주입 또는 모델규제를 감소시키는 것이다.
여기서는 규제를 사용하지 않았기 때문에 DicisionTreeRegressor모델을 훈련시켜본다.

from sklearn.tree import DecisionTreeRegressor

tree_reg = DecisionTreeRegressor()
tree_reg.fit(housing_prepared, housing_labels)

이제 트레이닝 세트로 평가해본다.

housing_predictions = tree_reg.predict(housing_prepared)
tree_mse = mean_squared_error(housing_labels, housing_predictions)
tree_rmse = np.sqrt(tree_mse)
tree_rmse

여기서는 0.0결과가 나오기때문에 데이터에 심하게 과대적합되었다.

확신이 드는 모델이 준비되기 전까지 테스트 세트는 사용하지 않고 트레이닝 세트의 일부분으로
훈련을 하고 다른 일부분은 모델 검증에 사용해야한다.

5.2 교차검증 평가

위에서 사용한 결정트리 모델을 평가하면 먼저 train_test_split함수로 트레이닝과
검증 세트로 나누고 더 작은 트레이닝 세트에서 모델을 트레이닝시키고 평가하는 방법이 있다.

더 나은 대안으로는 k-fold cross-validation(k겹교차검증) 기능을 사용하는 방법도 있다.
다음 코드는 트레이닝 세트를 폴드라 불리는 10개의 서브셋으로 무작위 분할한다.
그런 다음 결정트리 모델을 10번 훈련하고 평가하는데, 매번 다른 폴드를 선택해 평가에 사용하고
나머지 9개 폴드는 트레이닝에 사용한다. 즉 10개의 평가 점수가 담긴 배열이 결과가 된다.

from sklearn.model_selection import cross_val_score
score = cross_val_score(tree_reg, housing_prepared, housing_labels,
scoring="neg_mean_squared_error", cv=10)

tree_rmse_scores = np.sqrt(-scores)


사이킷런의 교차검증 기능은 scoring 매개변수에 낮을수록 좋은 비용함수가 아니라
클수록 좋은 효용함수를 기대하기때문에 평균제곱오차(MSE)의 반대값(음수)을 계산하는
neg_mean_squared_error함수를 사용한다.
이런 이유로 제곱근을 계산하기전에 -scores로 부호를 변경했다.

def display_scores(scores):
    print("점수:", scores)
    print("평균:", scores.mean())
    print("표준편차:", scores.std())

display_scores(tree_rmse_scores)

결과를 보면 결정트리결과가 이전보다 좋아보이지 않는다.
실제로 선형회귀모델보다 나쁘기 때문에 RandomForestRegressor모델을 시도해본다.

이 모델은 특성을 무작위로 선택해서 많은 결정트리를 만들고 예측을 평균내는 방식으로 작동한다.
다른 모델을 모아서 하나의 모델을 만드는 것을 앙상블학습이라고 하며 머신러닝 알고리즘 성능을
극대화 하는 방법중 하나이다.

from sklearn.ensemble import RandomForestRegressor
forest_reg = RandomForestRegressor()
forest_reg.fit(housing_prepared, housing_labels)
forest_rmse
display_scores(forest_rmse_scores)

결과를 보면 전보다 좋아졌지만 트레이닝세트에 대한 점수가 검증세트에 대한 점수보다 훨씬 낮으므로
이 모델도 여전히 트레이닝세트에 과대적합되어있다. 해결방법은 모델을 간단히 하거나
제한을 하거나 더많은 트레이닝데이터를 모으는 것이다.

여러종류의 머신러닝 알고리즘으로 하이퍼파라미터 조정에 많은 시간을 들이지 않으면서
다양한 모델(커널의 서포트벡터 머신, 신경망등)을 시도해봐야한다.
가능성 있는 2~5개 정도의 모델을 선정하는 것이 목적이다.

6. 모델 세부 튜닝

가능성 있는 모델을 추렸으면 이제 튜닝할 차례이다.

데이터 준비 단계를 하나의 하이퍼파라미터처럼 다룰 수 있다. 예를 들면 그리드 탐색이 확실치않은
특성추가여부를 자동으로 정할 수 있다. 비슷하게 이상치가 값이 빈 특성을 다루거나 특성선택을
자동으로 처리하고자 할때 그리드 탐색을 사용한다.

6.1 그리드 탐색

가장 단순한 방법은 하이퍼파라미터 조합을 찾을때까지 수동으로 조정하는 것이지만
사이킷런의 GridSearchCV를 사용하면 모든 조합에 대해 교차검증을 사용해 평가해준다.

아래는 RandomForestRegressor에 대한 최적의 하이퍼파라미터 조합을 탐색하는 코드다.

어떤 하이퍼파라미터 값을 지정해야할지 모를때는 연속된 10의 거듭제곱수로 찾는다.
더 세밀하게 탐색하려면 아래 예제처럼 n_estimators처럼 작은 값을 지정한다.

from sklearn.model_selection import GridSearchCV

param_grid = [
    # 12(=3×4)개의 하이퍼파라미터 조합을 시도합니다.
    {'n_estimators': [3, 10, 30], 'max_features': [2, 4, 6, 8]},
    # bootstrap은 False로 하고 6(=2×3)개의 조합을 시도합니다.
    {'bootstrap': [False], 'n_estimators': [3, 10], 'max_features': [2, 3, 4]},
  ]

forest_reg = RandomForestRegressor(random_state=42)
# 다섯 개의 폴드로 훈련하면 총 (12+6)*5=90번의 훈련이 일어납니다.
grid_search = GridSearchCV(forest_reg, param_grid, cv=5,
                           scoring='neg_mean_squared_error',
                           return_train_score=True)
grid_search.fit(housing_prepared, housing_labels)


# 시간이 꽤 걸리지만 다음과 같이 최적의 조합을 찾을 수 있다.
>>> grid_search.best_params_

output : {'max_features': 8, 'n_estimators': 30}

# 8과 30은 탐색범위의 최댓값이기 때문에 점수가 향상될 가능성이 있으므로 더 큰 값으로 
# 다시 검색해야 한다.

>>> grid_search.best_estimator_

output : RandomForestRegressor(max_features=8, n_estimators=30, random_state=42)


# 그리드서치에서 테스트한 하이퍼파라미터 조합의 점수를 확인한다.
cvres = grid_search.cv_results_
for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]):
    print(np.sqrt(-mean_score), params)
    

pd.DataFrame(grid_search.cv_results_)

이 예에서는 max_features 하이퍼 파라미터가 8, n_estimators 가 30일때 최적의 솔루션이다.
이때 RMSE점수가 49,682로 앞서 기본 하이퍼파라미터 설정으로 얻은 50,182점보다 조금 좋다.
이로서 최적의 모델을 찾았다.

6.2 랜덤 탐색

그리드 탐색방법은 적은 수의 조합을 구할때는 좋지만 하이퍼파라미터 탐색공간이 커지면
RandomizedSearchCV를 사용하는 편이 더 좋다.

랜덤 탐색의 장점은 다음과 같다.

  • 1000회 반복탐색시 하이퍼 파라미터마다 각기 다른 값을 탐색한다.
  • 반복 횟수를 조절하는 것만으로 하이퍼 파라미터 탐색에 필요한 컴퓨팅자원을 제어가능하다.

6.3 앙상블 방법

모델을 세밀하게 튜닝하는 방법은 최상의 모델을 '연결'해보는 것이다.
모델의 그룹이 최상의 단일 모델보다 더 나은 성능을 발휘할 때가 많다.

6.4 최상의 모델과 오차분석

'RandomForestRegressor'를 사용하면 각 특성의 상대적인 중요도를 알려준다.
결과를 바탕으로 중요하지 않은 특성들을 제외 할 수 있다.
여기서는 ocean_proximity 카테고리 하나만 유용하므로 다른 카테고리는 제외한다.

feature_importances = grid_search.best_estimator_.feature_importances_
feature_importances

extra_attribs = ["rooms_per_hhold", "pop_per_hhold", "bedrooms_per_room"]
#cat_encoder = cat_pipeline.named_steps["cat_encoder"] # 예전 방식
cat_encoder = full_pipeline.named_transformers_["cat"]
cat_one_hot_attribs = list(cat_encoder.categories_[0])
attributes = num_attribs + extra_attribs + cat_one_hot_attr
sorted(zip(feature_importances, attributes), reverse=True)

6.5 테스트 세트로 시스템 평가하기

모델 튜닝이 어느정도 끝났다면 최종 모델을 평가해야한다.

테스트셋에서 예측변수와 레이블을 얻은 후 full_pipeline을 사용해 데이터를 변환하고
테스트셋에서 최종모델을 평가하면된다.

final_model = grid_search.best_estimator_

X_test = strat_test_set.drop("median_house_value", axis=1)
y_test = strat_test_set["median_house_value"].copy()

X_test_prepared = full_pipeline.transform(X_test)
final_predictions = final_model.predict(X_test_prepared)

final_mse = mean_squared_error(y_test, final_predictions)
final_rmse = np.sqrt(final_mse)

final_rmse

scipy.stats.t.interval()를 사용해 일반화 오차의 95% 신뢰구간을 계산할 수 있다.

from scipy import stats

confidence = 0.95
squared_errors = (final_predictions - y_test) ** 2
np.sqrt(stats.t.interval(confidence, len(squared_errors) - 1,
                         loc=squared_errors.mean(),
                         scale=stats.sem(squared_errors)))
                         
                         
# 수동으로 계산하기
m = len(squared_errors)
mean = squared_errors.mean()
tscore = stats.t.ppf((1 + confidence) / 2, df=m - 1)
tmargin = tscore * squared_errors.std(ddof=1) / np.sqrt(m)
np.sqrt(mean - tmargin), np.sqrt(mean + tmargin)
   

# t-점수 대신 z-점수 사용하기 
zscore = stats.norm.ppf((1 + confidence) / 2)
zmargin = zscore * squared_errors.std(ddof=1) / np.sqrt(m)
np.sqrt(mean - zmargin), np.sqrt(mean + zmargin)

7. 론칭, 모니터링, 시스템 유지보수

평가까지 끝났다면 이제 솔루션을 시스템에 적용해야한다.

코드정리 -> 문서정리 -> 테스트케이스 작성

그 다음에 사용환경에 배포 할 수 있다.

일단은 전체 전처리 파이프라인과 예측 파이프라인이 포함된 훈련된 사이킷런 모델을 저장해야한다.


# 전처리와 예측을 포함한 전체 파이프라인
full_pipeline_with_predictor = Pipeline([
        ("preparation", full_pipeline),
        ("linear", LinearRegression())
    ])

full_pipeline_with_predictor.fit(housing, housing_labels)
full_pipeline_with_predictor.predict(some_data)


# joblib를 사용한 모델 저장

my_model = full_pipeline_with_predictor
import joblib
joblib.dump(my_model, "my_model.pkl") # DIFF
my_model_loaded = joblib.load("my_model.pkl") # DIFF

# RandomizedSearchCV를 위한 Scipy 분포 함수
from scipy.stats import geom, expon
geom_distrib=geom(0.5).rvs(10000, random_state=42)
expon_distrib=expon(scale=1).rvs(10000, random_state=42)
plt.hist(geom_distrib, bins=50)
plt.show()
plt.hist(expon_distrib, bins=50)
plt.show()

작성한 모델이 웹사이트 안에서 사용될 수 있다.

사용자가 새로운 구역에 관한 정보를 입력하고 '가격 예측하기' 버튼을 클릭하면
쿼리가 웹서버로 전송되고 이 애플리케이션의 코드가 모델의 'predict()'메서드를 호출한다.
모델로드는 서버시작시에 하는것이 좋다.
다른 활용방법으로는 REST API를 통해 서비스로 모델을 사용할 수 있다.
이렇게 하면 메인 어플리케이션을 건드리지 않고 모델을 새 버젼으로 업그레이드 하기 쉽다.

보통 많이 사용하는 전략은

  1. 모델을 구글 클라우드 AI플랫폼에 배포해 모델을 저장
  2. 구글클라우드 스토리지GCS)에 업로드
  3. AI플랫폼에에서 모델의 버전생성후 GCS파일 지정

이렇게 함으로서 로드밸런싱과 자동확장을 처리하는 간단한 웹서비스가 만들어진다.

그리고 모델에서 사용한 데이터는 항상 변하기때문에 만약 추천시스템의 일부라면
사용자가 관심을 가질 만한 제품을 추천하고 매일 추천상품의 판매량을 모니터링 해야한다.
이 숫자가 추천하지 않은 상품보다 줄으들면 데이터 파이프라인에 문제가 생겼거나
새로운 데이터로 모델을 다시 훈련해야한다.

다음은 데이터셋을 업뎃하고 정기적으로 훈련해야하는 자동화 작업이다.

  • 정기적으로 새로운 데이터를 수집하고 레이블을 단다.
  • 모델을 트레이닝하고 하이퍼파라미터를 자동으로 세부튜닝하는 스크립트를 작성한다.
  • 업데이트된 테스트 세트에서 새로운 모델과 이전 모델을 평가하는 스크립트를 작성한다.
  • 성능이 감소하지 않으면 새로운 모델을 제품에 배포한다.

마지막으로 만든 모델을 백업해야하고 이전모델로 빠르게 백업할수 있는 절차와 도구를 준비해야한다.


Reference

핸즈온 머신러닝:프로젝트 처음부터 끝까지

책의 소스코드 : 코랩