Py) ML - 정규화

Py) ML - 정규화

sklearn 라이브러리의 정규화 클래스를 사용하여 데이터를 정규화(normalization) 하는 방법에 대해 알아본다.


※ 본 내용에서 사용하는 diamonds.csv 파일은 별도로 다운로드 받아야 한다.
diamonds.csv 다운받기 [클릭]

개요

정규화는 “특정 기준 또는 규칙에 맞춰 값을 조정하는 행위”로 데이터간 단위가 달라 객관적인 비교가 어렵거나, 특정 지수 산출값을 직관적으로 보기 위하여 사용하기도 한다. 특히 전자의 사유로 군집분석이나 추천시스템 등 거리계산 알고리즘을 사용하는 경우 그 결과가 편향될 수 있어 보통 모델링 이전에 정규화를 진행한다.

구간화(Min-Max)

최대값과 최소값을 사용하여 원 데이터의 최소값을 0, 최대값을 1로 만드는 방법이다. 여기에 100을 곱하여 지표관리 등 다양한 곳에 활용하기도 한다.

$$MinMax(x) = \frac{x - min(x)}{max(x) - min(x)}$$

여기서 최소값을 빼는 것은 최소값을 0으로 만들며 범위(max - min)를 나누는 것은 범위를 1로 만들기 때문에 최종적으로 최대값이 1이 된다.

구간화 전과 후의 분포를 비교하면 다음과 같다.
정규화 비교(Min-Max)

최소값과 최대값은 0과 1로 바뀌긴 했지만 분포는 그대로인 것을 볼 수 있다.

표준화(Standardization)

평균과 표준편차를 사용하여 평균이 0, 표준편차를 1로 만드는 방법이다. 흔히 z-scoring 이라고 하기도 한다.

$$Standardization(x) = \frac{x - mean(x)}{std(x)}$$

여기서 평균을 빼는 것은 데이터의 중심을 0으로 옮기는 것이며 표준편차를 나누는 것은 자료의 편차를 1로 만드는 것이 된다.

구간화 전과 후의 분포를 비교하면 다음과 같다.
정규화 비교(Standardization)

표준화 이후에도 분포는 그대로인 것을 볼 수 있다.

주의점

학습 데이터와 평가 데이터 세트를 정규화하고자 할 때 다음과 같이 세 가지 방법이 있다.

  1. 학습 데이터와 평가 데이터를 같이 합쳐서 정규화 하는 경우
  2. 학습 데이터와 평가 데이터를 별도로 정규화 하는 경우
  3. 학습 데이터 기반으로 학습 데이터와 평가 데이터를 정규화 하는 경우

1번의 경우는 학습데이터를 정규화 할 때 평가 데이터에 영향을 받기 때문에 마치 시험 문제를 시험 치기 전에 일부 보고 공부하는 것과 같다. 예를 들어 학습 데이터의 값 범위가 1부터 10까지고 평가 데이터세트의 값 범위가 11부터 15까지면 두 데이터를 합쳐서 변환할 경우 최대값이 15이기 때문에 Min-Max 정규화를 실시하는 경우 15가 1로 변환된다. 이렇게 되면 학습 데이터만 변환할 경우 10이 1로 변환될텐데 평가 데이터세트 때문에 10이 0.643으로 변환된다. 물론 평가 데이터세트가 학습 데이터 세트 범위 내에 있는 경우는 상관이 없겠지만 그 범위가 차이가 나는 경우 평가 데이터세트가 학습 데이터세트의 변환에 영향을 주기 때문에 이를 Data Leakage 문제라고 한다. 그래서 평가 데이터 세트는 기존에 확보한 데이터에서 분리를 하는 것이지만 모른다고 가정하고 학습 데이터세트와 분리해서 별도로 처리해야 하겠다.

2번의 경우는 1번에서 언급한 Data Leakage 문제를 피하기 위해 선택할 수 있는 방법이긴 하다. 그러나 여기도 문제가 있다. 학습 데이터는 보통 충분히 갖춰져 있을 수 있으나 극단적으로 평가 데이터가 1개 데이터 포인트(tabular의 경우 1개 row)만 있는 경우도 있다. 이 때 1개를 대상으로 정규화를 실시하게 되면 Min-Max 정규화를 실시할 경우 모든 값이 0으로 반환된다. 더 나아가서 1시간 마다 신규 데이터가 유입되는 상황을 생각해보자. 그러면 데이터가 더이상 유입이 안될 때 까지 기다렸다가 한 번에 정규화를 해야 할까? 아니면 1시간 마다 유입되는 신규 데이터만 따로 정규화를 해야 할까? 아무튼 둘 다 못할짓이다. 그래서 평가 데이터를 정규화 하고자 하는 경우 정규화를 실시할 기준이 별도로 준비 되어야 한다.

그래서 3번이다. 학습 데이터 세트 기반으로 정규화 규칙(모델)을 별도로 저장(또는 기록)해두면 Data Leakage 문제도 없고 새로 조금씩 유입(또는 확보)되는 미래 데이터를 정규화 하기도 용이하다.

이해를 돕기 위해 그림으로 표현하면 다음과 같다.
정규화 유형별 시각화

위 그림에서 보듯이 1번의 경우는 학습 데이터와 평가 데이터를 합쳐서 정규화를 실시하는 경우이고 2번의 경우는 학습 데이터와 평가 데이터를 별도로 정규화를 실시한 것이다. 그리고 3번의 경우는 학습 데이터를 기반으로 정규화 규칙(모델)을 만들어 두고 평가 데이터를 정규화 하는 방식이다. 학습 데이터 세트(train set)와 평가 데이터 세트(test set)의 범위를 비교했을 때 평가 데이터 세트가 학습 데이터 세트의 범위보다 같거나 작으면 문제가 되지 않지만 평가 데이터 세트가 학습 데이터 세트의 최대 범위를 넘어서게 되면 그림의 가장 오른쪽 사례 처럼 1, 2, 3번의 경우가 모두 같은 결과를 반환하는 것은 아니다. 즉, 이 때 학습 데이터 세트와 평가 데이터 세트를 합쳐서 정규화 한 결과는 Data Leakage 문제가 야기된다고 할 수 있다.

그런데 여기서 이런 의문이 들 수 있다. 예를 들어 3번의 방법으로 Min-Max Scaling을 실시하는 경우 평가 데이터 세트의 최대값이 학습 데이터보다 크다면 1보다 큰 값이 반환이 될 수 있다는 것이다. 이것은 문제가 되는 것도 아니고 또한 Data Leakage 문제도 아니다. 즉, 자연스러운 연산으로 산출된 값 그대로를 받아들이면 되겠다. 물론 구축하는 시스템에서 0에서 1 사이의 값에 대한 처리를 기준으로 설계를 했다면 그에 따른 조치를 해야겠지만 기본적으로 정규화의 관점에서는 문제가 없다고 이해하면 되겠다.

실습

사용자 정의 함수를 활용하여 정규화를 실시할 수 있으나 여기서는 sklearn 라이브러리의 MinMaxScaler() 클래스와 StandardScaler() 클래스의 사용 사례를 다루고자 한다.

실습을 위해 다음의 “diamonds.csv” 데이터세트를 준비한다.
※ “diamonds.csv” 파일은 포스팅 상단 링크를 통해 다운로드 가능

1
2
3
4
5
6
7
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
from sklearn.preprocessing import StandardScaler

df = pd.read_csv("data/diamonds.csv")
df.head(2)
carat cut color clarity depth table price x y z
0 0.23 Ideal E SI2 61.5 55.0 326 3.95 3.98 2.43
1 0.21 Premium E SI1 59.8 61.0 326 3.89 3.84 2.31

MinMaxScaler

먼저 샘플 데이터를 사용하여 Min-Max 정규화를 실시해보자.

1
2
df_s = pd.DataFrame(dict(value = list(range(6))))
df_s
value
0 0
1 1
2 2
3 3
4 4
5 5

정규화를 1회성으로 실시하고 정규화 모델 객체가 굳이 필요없다면 한 번에 변환시키는 .fit_transform() 메서드를 MinMaxScaler() 클래스에 이어붙여 사용할 수 있다. 그리고 정규화된 객체는 2차원 NumPy 어레이 객체이다.

1
2
3
4
5
6
7
MinMaxScaler().fit_transform(df_s)
## array([[0. ],
## [0.2],
## [0.4],
## [0.6],
## [0.8],
## [1. ]])

그리고 다음과 같이 정규화 객체를 사용해서 정규화를 실시할 수도 있다. .fit()으로 입력된 객체의 변수별 최대값, 최소값 같이 정규화에 필요한 정보를 취득하여 “model_nor1” 객체에 저장한다. 그리고 정규화 객체 “model_nor1”로 부터 .transform() 메서드로 원하는 객체를 정규화 하면 .fit_transform() 메서드와 같이 2차원 NumPy 어레이 객체가 반환된다.

1
2
3
4
5
6
7
8
model_nor1 = MinMaxScaler().fit(df_s)
model_nor1.transform(df_s)
## array([[0. ],
## [0.2],
## [0.4],
## [0.6],
## [0.8],
## [1. ]])

그리고 상기 코드로 생성한 정규화 객체 “model_nor1”는 다음과 같이 각 변수별 다양한 정보를 담고 있다.

1
2
3
4
5
6
7
8
9
10
11
model_nor1.data_min_
## array([0.])

model_nor1.data_max_
## array([5.])

model_nor1.data_range_
## array([5.])

model_nor1.feature_names_in_
## array(['value'], dtype=object)

이제 앞에서 준비한 “diamonds.csv” 파일의 데이터를 사용하여 정규화를 실시해보자. “df” 객체를 그대로 쓰면 범주형 변수 때문에 제대로 변환이 되지 않기 때문에 .select_dtypes() 메서드로 수치형 변수만 추출하여 정규화를 실시하였다.

1
2
3
4
5
df_nums = df.select_dtypes(include = "number")
arr_nums_nor = MinMaxScaler().fit_transform(df_nums)
arr_nums_nor[:2, ]
## array([[0.00623701, 0.51388889, 0.23076923, 0. , 0.36778399, 0.06757216, 0.07641509],
## [0.002079 , 0.46666667, 0.34615385, 0. , 0.36219739, 0.06519525, 0.07264151]])

앞에서 샘플 데이터를 정규화 한 것은 변수가 하나였기 때문에 별 문제가 되지 않았지만 이번에는 변수가 여러개이기 때문에 정규화된 각 수치가 어떤 변수의 원소인지 바로 알기 어렵다. 이 경우는 다음과 같이 데이터프레임 객체로 변환하는 것도 좋은 선택이다.

1
2
df_nums_nor = pd.DataFrame(arr_nums_nor, columns = df_nums.columns)
df_nums_nor.head(2)
carat depth table price x y z
0 0.006237 0.513889 0.230769 0.0 0.367784 0.067572 0.076415
1 0.002079 0.466667 0.346154 0.0 0.362197 0.065195 0.072642

그리고 각 변수별 최대값과 최소값을 확인해보면 다음과 같다.

1
df_nums_nor.agg(["min", "max"])
carat depth table price x y z
min 0.0 0.0 0.0 0.0 0.0 0.0 0.0
max 1.0 1.0 1.0 1.0 1.0 1.0 1.0

이번에는 정규화 객체를 사용한 변환을 해보도록 하자 .fit().transform() 을 사용한 결과는 다음과 같다.

1
2
3
4
5
model_nor2 = MinMaxScaler().fit(df_nums)
arr_nums_nor2 = model_nor2.transform(df_nums)
arr_nums_nor2[:2, ]
## array([[0.00623701, 0.51388889, 0.23076923, 0. , 0.36778399, 0.06757216, 0.07641509],
## [0.002079 , 0.46666667, 0.34615385, 0. , 0.36219739, 0.06519525, 0.07264151]])

그리고 정규화 객체 “model_nor2” 에 들어있는 내용을 확인하면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
model_nor2.data_min_
## array([2.00e-01, 4.30e+01, 4.30e+01, 3.26e+02, 0.00e+00, 0.00e+00, 0.00e+00])

model_nor2.data_max_
## array([5.0100e+00, 7.9000e+01, 9.5000e+01, 1.8823e+04, 1.0740e+01, 5.8900e+01, 3.1800e+01])

model_nor2.data_range_
## array([4.8100e+00, 3.6000e+01, 5.2000e+01, 1.8497e+04, 1.0740e+01, 5.8900e+01, 3.1800e+01])

model_nor2.feature_names_in_
## array(['carat', 'depth', 'table', 'price', 'x', 'y', 'z'], dtype=object)

그리고 역변환을 하려면 .inverse_transform() 메서드를 다음과 같이 사용할 수 있다.

1
2
3
4
arr_nums_inv = model_nor2.inverse_transform(arr_nums_nor2)
arr_nums_inv[:2, ]
## array([[2.30e-01, 6.15e+01, 5.50e+01, 3.26e+02, 3.95e+00, 3.98e+00, 2.43e+00],
## [2.10e-01, 5.98e+01, 6.10e+01, 3.26e+02, 3.89e+00, 3.84e+00, 2.31e+00]])

그리고 .inverse_tranform() 메서드의 결과도 NumPy 어레이 객체이기 때문에 보다 깔끔하게 보고싶다면 다음과 같이 데이터프레임으로 바꾸는 것도 방법이다.

1
2
df_nums_inv = pd.DataFrame(arr_nums_inv, columns = df_nums.columns)
df_nums_inv.head(2)
carat depth table price x y z
0 0.23 61.5 55.0 326.0 3.95 3.98 2.43
1 0.21 59.8 61.0 326.0 3.89 3.84 2.31

이제 상기 코드를 실전처럼 처리해보자. 데이터 분할, 정규화, 역변환까지 모아보면 다음과 같다.

1
2
3
df_train, df_test = train_test_split(df, train_size = 0.8, random_state = 123)
len(df_train), len(df_test)
## (43152, 10788)

범주형과 수치형 변수를 분리하기 위해 .select_dtypes() 메서드를 활용한다.

1
2
3
4
5
df_train_num = df_train.select_dtypes(include = "number")
df_train_obj = df_train.select_dtypes(exclude = "number")
df_test_num = df_test.select_dtypes(include = "number")
df_test_obj = df_test.select_dtypes(exclude = "number")
df_train_num.head(2)
carat depth table price x y z
13361 0.24 62.9 58.0 419 3.94 4.01 2.50
18592 1.02 62.4 58.0 7587 6.42 6.47 4.02
1
df_train_obj.head(2)
cut color clarity
13361 Very Good F VS2
18592 Very Good F VS1

수치형 변수가 있는 객체를 대상으로 정규화를 실시하도록 하자.

1
2
3
4
5
6
model_nor_mm = MinMaxScaler().fit(df_train_num)
arr_train_num_nor = model_nor_mm.transform(df_train_num)
arr_test_num_nor = model_nor_mm.transform(df_test_num)
arr_train_num_nor[:2, ]
## array([[0.00831601, 0.55277778, 0.28846154, 0.00502784, 0.36685289, 0.06808149, 0.07861635],
## [0.17047817, 0.53888889, 0.28846154, 0.39255014, 0.59776536, 0.1098472 , 0.12641509]])

만약 정규화된 수치형 변수와 기존에 나눴던 범주형 변수를 같이 합쳐서 다루고자 한다면 다음과 같이 코드를 작성할 수 있다.

1
2
3
df_train_nor = pd.concat([pd.DataFrame(arr_train_num_nor, columns = df_train_num.columns), 
df_train_obj.reset_index(drop = True)], axis = 1)
df_train_nor.head(2)
carat depth table price x y z cut color clarity
0 0.008316 0.552778 0.288462 0.005028 0.366853 0.068081 0.078616 Very Good F VS2
1 0.170478 0.538889 0.288462 0.392550 0.597765 0.109847 0.126415 Very Good F VS1

여기서 객체를 이어붙일 때 사용한 concat() 함수의 경우 Pandas 객체를 대상으로 사용할 수 있기에 “arr_train_num_nor” 객체를 데이터프레임으로 만들어주었고, “df_train_obj” 객체의 인덱스를 초기화(재설정)한 이유는 데이터프레임으로 변환된 “arr_train_num_nor” 객체의 인덱스는 0부터 1씩 증가하는 등차수열인 반면 “df_train_obj” 객체의 인덱스는 “df” 객체로부터 단순 임의추출된 번호로 되어있기 때문이다.

여기서는 실시하지 않지만, 이 다음으로 통계 또는 머신러닝 모델링을 실시하고자 하는 경우 One-Hot Encoding이나 Label Encoding 등 범주형 변수를 별도 조치하여 수치형으로 변환해야 한다.

마지막으로 역변환은 다음과 같이 실시한다.

1
2
3
4
arr_train_num_inv = model_nor_mm.inverse_transform(arr_train_num_nor)
arr_train_num_inv[:2, ]
## array([[2.400e-01, 6.290e+01, 5.800e+01, 4.190e+02, 3.940e+00, 4.010e+00, 2.500e+00],
## [1.020e+00, 6.240e+01, 5.800e+01, 7.587e+03, 6.420e+00, 6.470e+00, 4.020e+00]])

StandardScaler

StandardScaler() 정규화 클래스는 MinMaxScaler() 의 사용법과 완전히 같다. 다만 정규화 수식이 다를 뿐이다.

“diamonds.csv” 파일의 데이터가 있는 “df” 객체를 사용하여 표준화를 실시하면 다음과 같다.

1
2
3
4
5
df_nums = df.select_dtypes(include = "number")
arr_nums_nor = StandardScaler().fit_transform(df_nums)
arr_nums_nor[:2, ]
## array([[-1.19816781, -0.17409151, -1.09967199, -0.90409516, -1.58783745, -1.53619556, -1.57112919],
## [-1.24036129, -1.36073849, 1.58552871, -0.90409516, -1.64132529, -1.65877419, -1.74117497]])

“arr_nums_nor” 객체를 데이터프레임 객체로 변환하면 다음과 같다.

1
2
df_nums_nor = pd.DataFrame(arr_nums_nor, columns = df_nums.columns)
df_nums_nor.head(2)
carat depth table price x y z
0 -1.198168 -0.174092 -1.099672 -0.904095 -1.587837 -1.536196 -1.571129
1 -1.240361 -1.360738 1.585529 -0.904095 -1.641325 -1.658774 -1.741175

이번엔 정규화 객체를 활용하여 표준화를 실시해보자.

1
2
3
4
5
model_nor3 = StandardScaler().fit(df_nums)
arr_nums_nor3 = model_nor3.transform(df_nums)
arr_nums_nor3[:2, ]
## array([[-1.19816781, -0.17409151, -1.09967199, -0.90409516, -1.58783745, -1.53619556, -1.57112919],
## [-1.24036129, -1.36073849, 1.58552871, -0.90409516, -1.64132529, -1.65877419, -1.74117497]])

StandardScaler() 클래스로 생성되는 정규화 객체에는 각 변수의 평균, 분산, 표준편차가 “.mean_”, “.var_”, “.scale_” 어트리뷰트에 들어있으며 다음과 같이 확인할 수 있다.

1
2
3
4
5
pd.DataFrame([model_nor3.mean_,
model_nor3.var_,
model_nor3.scale_],
index = ["mean", "var", "std"],
columns = model_nor3.feature_names_in_)
carat depth table price x y z
mean 0.797940 61.749405 57.457184 3.932800e+03 5.731157 5.734526 3.538734
var 0.224682 2.052366 4.992856 1.591533e+07 1.258324 1.304447 0.498002
std 0.474007 1.432608 2.234470 3.989403e+03 1.121750 1.142124 0.705692

그리고 역변환을 실시하면 다음과 같다.

1
2
3
4
arr_nums_inv = model_nor3.inverse_transform(arr_nums_nor3)
arr_nums_inv[:2, ]
## array([[2.30e-01, 6.15e+01, 5.50e+01, 3.26e+02, 3.95e+00, 3.98e+00, 2.43e+00],
## [2.10e-01, 5.98e+01, 6.10e+01, 3.26e+02, 3.89e+00, 3.84e+00, 2.31e+00]])

주의점

정규화를 실시할 때 자주 보게되는 에러나 경고 메세지를 소개하고자 한다.

먼저 1차원 객체를 입력으로 했을 때 발생하는 에러이다. 다음의 코드에서는 리스트 객체를 사용했지만 1차원 NumPy 어레이나 Pandas 시리즈 객체를 입력으로 해도 유사한 에러메세지를 볼 수 있다.

1
2
3
4
5
6
7
8
9
MinMaxScaler().fit_transform([1, 2, 3])
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[82], line 1
----> 1 MinMaxScaler().fit_transform([1, 2, 3])
......
ValueError: Expected 2D array, got 1D array instead:
array=[1. 2. 3.].
Reshape your data either using array.reshape(-1, 1) if your data has a single feature or array.reshape(1, -1) if it contains a single sample.

여기서 가장 중요한 문구는 다음과 같다.

ValueError: Expected 2D array, got 1D array instead:

즉, 2차원 리스트, NumPy 어레이 객체 또는 Pandas 데이터프레임 객체를 사용해야 하는데 그렇지 못해서 저런 에러가 발생한다.

이제 2차원 객체를 입력했으나 에러가 발생하는 경우를 알아본다.

다음의 코드는 “model_nor4” 객체는 “df”객체의 3개 변수를 사용하여 정규화를 실시하였으나 .transform() 메서드로 변환을 실시할 때는 2개의 변수만 넣은 결과이다.

1
2
3
4
5
6
7
8
9
model_nor4 = MinMaxScaler().fit(df.iloc[:, [5, 6, 7]])
arr_test_nor1 = model_nor4.transform(df.iloc[:, [5, 6]])
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[89], line 2
1 model_nor4 = MinMaxScaler().fit(df.iloc[:, [5, 6, 7]])
----> 2 arr_test_nor1 = model_nor4.transform(df.iloc[:, [5, 6]])
......
ValueError: X has 2 features, but MinMaxScaler is expecting 3 features as input.

에러 메세지의 마지막 문구를 보면 변수가 3개가 아닌 2개가 입력이 되어 문제라고 하는 것을 알 수 있다.

다음의 코드는 일부러 변환 대상의 변수의 순서를 바꿔보았다. 그랬더니 경고 메세지(FutureWarning)가 발생한 것을 알 수 있으며 sklearn 라이브러리 버전이 1.2 이상인 경우 에러가 발생할 것이라고 한다.

1
2
3
4
5
6
7
8
model_nor4 = MinMaxScaler().fit(df.iloc[:, [5, 6, 7]])
arr_test_nor2 = model_nor4.transform(df.iloc[:, [7, 6, 5]])
---------------------------------------------------------------------------
C:\Users\aaa\anaconda3\lib\site-packages\sklearn\base.py:493: FutureWarning: The feature names should match those that were passed during fit. Starting version 1.2, an error will be raised.
Feature names must be in the same order as they were in fit.

warnings.warn(message, FutureWarning)
---------------------------------------------------------------------------

그리고 학습 객체는 데이터프레임이고 변환 대상 객체는 NumPy 데이터프레임인 경우 변수명이 명시되지 않아 경고 메세지가 발생한다. 그래도 정규화는 올바르게 진행된다.

1
2
3
4
5
arr_test_nor3 = model_nor4.transform(df.iloc[:, [7, 6, 5]].values)
---------------------------------------------------------------------------
C:\Users\encai\anaconda3\lib\site-packages\sklearn\base.py:450: UserWarning: X does not have valid feature names, but MinMaxScaler was fitted with feature names
warnings.warn(
---------------------------------------------------------------------------

학습과 변환을 모두 NumPy 어레이 객체로 실시하는 경우는 변수의 순서가 다르다고 해서 경고메세지가 발생하지는 않는다. 그렇기 때문에 정규화를 수행할 때 좀 더 주의를 기울여야 하겠다.

1
2
model_nor5 = MinMaxScaler().fit(df.iloc[:, [5, 6, 7]].values)
arr_test_nor5 = model_nor5.transform(df.iloc[:, [7, 6, 5]].values)
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×