배운 내용¶
- zip() & zip(*)
- Series.nunique()
- LabelBinarizer
- scipy.sparse.hstack()
- del '객체 변수명'
- gc.collect()
Mercari Price Suggestion Challenge¶
Mercari사의 제품에 대해 가격을 예측하는 과제입니다. 제공되는 데이터 세트는 제품에 대한 여러 속성 및 제품 설명 등의 텍스트 데이터로 구성됩니다. 이 데이터를 기반으로 예상 가격을 판매자들에게 제공하고자 합니다. 데이터 세트는 https://www.kaggle.com/c/mercari-price-suggestion-challenge/data 에서 내려받으실 수 있습니다. train.tsv.7z/train.tsv를 다운로드 합니다.
- train_id: 데이터 id
- name: 제품명
- item_condition_id: 판매자가 제공하는 제품 상태
- category_name: 카테고리 명
- brand_name: 브랜드 이름
- price: 제품 가격.(타깃)
- shipping: 배송비 무료 여부. 1이면 무료, 0이면 유료
- item_description: 제품에 대한 설명
Mercari Price Suggestion이 기존 회귀 예제와 다른 점은 item_description과 같은 텍스트 형태의 비정형 데이터와 다른 정형 피처와 같이 price를 회귀로 예측한다는 점입니다.
데이터 전처리¶
train data를 로딩하고 데이터를 간략하게 살펴보겠습니다.
from sklearn.linear_model import Ridge, LogisticRegression
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
import pandas as pd
mercari_df = pd.read_csv('/content/drive/MyDrive/military/Mercari/train.tsv', sep='\t')
print(mercari_df.shape)
mercari_df.head(3)
다음으로 피처 타입과 Null 여부를 확인하겠습니다.
print(mercari_df.info())
brand_name은 가격에 영향을 미치는 중요한 요인인데, 많은 null값을 가지고 있습니다. category_name도 적지만 null 데이터를 가지고 있습니다. 이후에 null 데이터를 적절한 문자열로 치환하겠습니다.
target값인 price 칼럼의 데이터 분포도를 살펴보겠습니다. 회귀에서 target값이 정규 분포의 형태를 가지는 것은 매우 중요합니다. log변환으로 보통 정규분포로 만듭니다.
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline
y_train_df = mercari_df['price']
plt.figure(figsize=(6, 4))
# kde는 히스토그램처럼 계단 분포를 곡선처럼 부드럽게 그려주는 기능
sns.distplot(y_train_df, kde=False)
price 값이 비교적 적은 가격을 가진 데이터 쪽으로 왜곡돼 분포해있습니다. log 변환을 해보겠습니다.
import numpy as np
y_train_df = np.log1p(y_train_df)
sns.distplot(y_train_df, kde=False)
비교적 정규분포에 가깝게 됐습니다. price 칼럼을 로그 변환한 값으로 바꾸겠습니다.
mercari_df['price'] = np.log1p(mercari_df['price'])
mercari_df['price'].head(3)
다른 피처의 값도 살펴보겠습니다. Shipping, item_condition_id 값의 유형은 다음과 같습니다.
print('Shipping 값 유형:\n', mercari_df['shipping'].value_counts())
print('')
print('item_condition_id 값 유형:\n', mercari_df['item_condition_id'].value_counts())
Item_condition_id는 캐글에 기제돼 있지 않아서 알지 못하지만, 1, 2, 3이 주를 이루고 있습니다.
item_description 칼럼은 Null값이 별로 없지만, 별도 설명이 없는 경우 'No description yet'으로 돼 있습니다. 이러한 값이 얼마나 있는지 알아보겠습니다.
boolean_cond = mercari_df['item_description']=='No description yet'
mercari_df[boolean_cond]['item_description'].count()
item_description이 'No description yet'인 경우도 Null과 마찬가지이므로 의미가 없습니다. 적절한 값으로 변경해야합니다.
category_name을 살펴보면 '/'로 분리된 카테고리를 하나의 문자열로 나타내고 있습니다. category_name은 텍스트이므로 피처 추출시 tokenizer를 '/'로 하여 단어를 분리해 벡터화할 수도 있지만, 여기서는 '/'를 기준으로 단어를 토큰화해 각각 별도의 피처에 저장하고 이를 이용해 알고리즘을 학습시키겠습니다.
zip(*)¶
zip함수는 list 입력 값들을 튜플로 짝지어 반환하는데, zip(*)는 반대로 unzip하여 튜플로 반환한다.
a = ['a', 'b', 'c']
b = ['d', 'e', 'f']
result = list(zip(a, b))
>> [('a', 'd'), ('b', 'e'), ('c', 'f')]
list()로 안묶으면 zip.object가 반환된다.
x, y, z = zip(*[('a', 'b', 'c'), ('d', 'e', 'f')])
>> x=('a', 'd'), y=('b', 'e'), z=('c', 'f')
Series.nunique()¶
# apply lambda에서 호출되는 대, 중, 소 분할 함수 생성, 대, 중, 소 값을 리스트로 반환
def split_cat(category_name):
try:
return category_name.split('/')
except:
return ['Other_Null', 'Other_Null', 'Other_Null']
# 위의 split_cat()을 apply lambda에서 호출해 대, 중, 소 칼럼을 mercari_df에 생성
mercari_df['cat_dae'], mercari_df['cat_jung'], mercari_df['cat_so'] = \
zip(*mercari_df['category_name'].apply(lambda x : split_cat(x)))
# 대분류만 값의 유형과 건수를 살펴보고, 중분류, 소분류는 값의 유형이 많으므로 분류 개수만 추출.
print('대분류 유형 :\n', mercari_df['cat_dae'].value_counts())
print('중분류 개수 :', mercari_df['cat_jung'].nunique())
print('소분류 개수 :', mercari_df['cat_so'].nunique())
Women이 많이 있는 것을 알 수 있고, 중분류는 114개, 소분류는 858개로 구성돼 있습니다.
마지막으로 brand_name, category_name, item_description 칼럼의 Null 값은 일괄적으로 'Other Null'로 동일하게 변경하겠습니다. brand_name은 price값 결정에 많은 영향을 줄 것으로 판단되지만, 어쩔수 없이 Other Null로 바꾸는 것이므로 아쉬워해야합니다.
# mercari_df중 price가 Null인 값은 drop한다.
mercari_df.drop(mercari_df[mercari_df['price']=='Other_Null'].index, axis=0, inplace=True)
mercari_df['brand_name'] = mercari_df['brand_name'].fillna(value='Other_Null')
mercari_df['category_name'] = mercari_df['category_name'].fillna(value='Other_Null')
mercari_df[mercari_df['item_description']=='No description yet'] = 'Other_Null'
mercari_df.isnull().sum()
예제 결과에 없는 null 데이터가 있었는데, 얼마 없어서 행을 drop했습니다.
mercari_df.drop(mercari_df[mercari_df['price'].isnull()==True].index, axis=0, inplace=True)
mercari_df.drop(mercari_df[mercari_df['shipping'].isnull()==True].index, axis=0, inplace=True)
mercari_df.drop(mercari_df[mercari_df['item_description'].isnull()==True].index, axis=0, inplace=True)
피처 인코딩과 피처 벡터화¶
Mercari Price suggestion에 이용되는 데이터 세트는 문자열 칼럼이 많습니다. 문자열 칼럼 중 원-핫 인코딩을 수행하거나 피처 벡터화를 할 칼럼을 살펴보겠습니다. Price값을 예측해야하므로 회귀 모델을 기반으로 합니다. 선형 회귀 모델, 회귀 트리 모델 모두 적용할 예정입니다. 특히 선형 회귀의 경우 원-핫 인코딩 적용이 훨씬 선호되므로 인코딩할 피처는 모두 원-핫 인코딩을 적용하겠습니다.
피처 벡터화의 경우 비교적 짧은 텍스트는 Count 기반의 벡터화를, 긴 텍스트는 TF-IDF 기반의 벡터화를 적용하겠습니다.
첫 번째로 검토할 칼럼은 brand_name입니다. 이 칼럼은 상품의 브랜드 명입니다.(상품명이 아님) 브랜드명 유형 건수와 대표적인 5개의 브랜드만 살펴보겠습니다.
print('brand name의 유형 건수:', mercari_df['brand_name'].nunique())
print('brand name sample 5건: \n', mercari_df['brand_name'].value_counts()[:5])
brand_name은 대부분 명료한 문자열로 돼있고, 별도의 피처 벡터화 형태로 만들 필요 없이 원-핫 인코딩 변환을 적용하면 됩니다. 4236건을 원-핫 인코딩으로 변환하기에 다소 많아 보이나 본 예제의 ML 모델 구축상 큰 문제는 없습니다. 인코딩 적용은 이후에 한 번에 처리하도록 하겠습니다.
print('name의 종류 개수 :', mercari_df['name'].nunique())
print('name sample 10건 :\n', mercari_df['name'][:10])
상품명 name은 속성의 종류가 매우 많습니다. 무려 785222건이나 가지고 있으며, 거의 고유한 상품명을 가지고 있습니다. name 속성은 유형이 매우 많고 적은 단어 위주의 텍스트 형태로 이루어져 있으므로 Count 기반으로 피처 벡터화 변환을 적용하겠습니다.
category_name 칼럼은 이전에 전처리를 통해 cat_dae, cat_jung, cat_so 칼럼으로 분리시켰습니다. 이 세 칼럼 모두 원-핫 인코딩을 적용하겠습니다.
shipping 칼럼은 0, 1로 두 가지 유형의 배송비 무료 여부값을 가지고 있으며, item_condition_id는 상품 상태로 1, 2, 3, 4, 5의 다섯 가지 유형의 값을 가지고 있습니다. 이 두 칼럼 모두 원-핫 인코딩을 적용하겠습니다.
item_description은 상품에 대한 간단한 설명으로 데이터 세트에서 가장 긴 텍스트를 가지고 있습니다. 해당 칼럼의 평균 문자열 크기와 예시 2개 정도만 추출해 확인하겠습니다.
pd.set_option & Series.str.len()¶
pd.set_option('max_colwidth', 200)
# item_description의 평균 문자열 크기
print('item_description 평균 문자열 크기:', mercari_df['item_description'].str.len().mean())
mercari_df['item_description'][:2]
평균 문자열이 145자로 비교적 큰 크기입니다. item_description 칼럼은 TF-IDF로 피처 벡터화 하겠습니다.
이제 주요 칼럼들을 인코딩 및 피처 벡터화 변환하겠습니다. name 칼럼과 item_description 칼럼을 피처 벡터화합니다. name은 CountVectorizer로, item_description은 TfidfVectorizer로 변환하겠습니다.
# name 속성에 대한 피처 벡터화
cnt_vec = CountVectorizer()
X_name = cnt_vec.fit_transform(mercari_df.name)
# item_description에 대한 피처 벡터화
tfidf_descp = TfidfVectorizer(max_features = 50000, ngram_range=(1, 3), stop_words='english')
X_descp = tfidf_descp.fit_transform(mercari_df['item_description'])
print('name vectorization shape:', X_name.shape)
print('item_description vectorization shape:', X_descp.shape)
CountVectorizer, TfidfVectorizer가 fit_transform()을 통해 반환하는 데이터는 희소 행렬 형태 CSR입니다. 희소 행렬 객체 변수인 X_name, X_descp, 앞으로 인코딩 될 cat_dae, cat_jung, cat_so, brand_name, shipping, item_condition_id도 모두 함께 데이터 세트로 재구성 돼야합니다.
그래서 이 인코딩 대상 칼럼도 희소 행렬 형태로 인코딩을 적용할 것인데, 사이킷런에서 원-핫 인코딩을 위해 제공하는 OneHotEncoder와 LabelBinarizer 클래스 중 희소 행렬 형태의 원-핫 인코딩을 제공하는 LabelBinarizer 클래스를 이용하겠습니다. sparse_output=True로 설정하면 희소 행렬 형태로 인코딩을 적용합니다. 개별 칼럼으로 만들어진 희소 행렬은 Scipy 패키지의 sparse 모듈의 hstack()함수를 이용해 결합하겠습니다.
LabelBinarizer¶
from sklearn.preprocessing import LabelBinarizer
# item_contition_id, shipping을 int, float 에서 str로
mercari_df['item_condition_id'] = str(mercari_df['item_condition_id'])
mercari_df['shipping'] = str(mercari_df['shipping'])
# brand_name, item_condition_id, shipping 각 피처들을 희소 행렬로 원-핫 인코딩 변환
lb_brand_name = LabelBinarizer(sparse_output=True)
X_brand = lb_brand_name.fit_transform(mercari_df['brand_name'])
lb_item_cond_id = LabelBinarizer(sparse_output=True)
X_item_cond_id = lb_item_cond_id.fit_transform(mercari_df['item_condition_id'])
lb_shipping = LabelBinarizer(sparse_output=True)
X_shipping = lb_shipping.fit_transform(mercari_df['shipping'])
# cat_dae, cat_jung, cat_so 각 피처들을 희소 행렬 원-핫 인코딩 변환
lb_cat_dae = LabelBinarizer(sparse_output=True)
X_cat_dae = lb_cat_dae.fit_transform(mercari_df['cat_dae'])
lb_cat_jung = LabelBinarizer(sparse_output=True)
X_cat_jung = lb_cat_jung.fit_transform(mercari_df['cat_jung'])
lb_cat_so = LabelBinarizer(sparse_output=True)
X_cat_so = lb_cat_so.fit_transform(mercari_df['cat_so'])
print(type(X_brand), type(X_item_cond_id), type(X_shipping))
print('X_brand shape:{0}, X_item_cond_id shape:{1}'.format(X_brand.shape, X_item_cond_id.shape))
print('X_shipping shape:{0}, X_cat_dae shape:{1}'.format(X_shipping.shape, X_cat_dae.shape))
print('X_cat_jung shape:{0}, X_cat_so shape:{1}'.format(X_cat_jung.shape, X_cat_so.shape))
인코딩 변환된 데이터 세트들도 모두 CSR 형태의 csr_matrix 타입입니다. X_brand_name 칼럼, X_cat_so의 경우도 각각 4236개, 856개의 인코딩 칼럼을 가지게 되어, 인코딩 칼럼이 많이 생겼습니다. 하지만, 피처 벡터화로 텍스트 형태의 문자열이 가지는 벡터 형태의 매우 많은 칼럼과 함께 결합되므로 크게 문제 될 것은 없습니다.
scipy.sparse.hstack() & del '객체 변수명' & gc.collect()¶
이번에는 피처 벡터화한 데이터 세트와 희소 인코딩 변환한 데이터 세트를 hstack()을 이용해 모두 결합해 보겠습니다. 결합된 데이터는 Mercari Price Suggestion의 기반 데이터 세트로 사용되는데, 용량을 많이 잡아먹어서 이 데이터의 크기와 타입만 확인하고 메모리에서 삭제하도록 하겠습니다.
from scipy.sparse import hstack
import gc
sparse_matrix_list = (X_name, X_descp, X_brand, X_item_cond_id, \
X_shipping, X_cat_dae, X_cat_jung, X_cat_so)
# hstack 함수를 이용해 인코딩과 벡터화를 수행한 데이터 세트를 모두 결합.
X_features_sparse = hstack(sparse_matrix_list).tocsr()
print(type(X_features_sparse), X_features_sparse.shape)
# 데이터 세트가 메모리를 많이 차지하므로 사용 목적이 끝났으면 바로 메모리에서 삭제.
del X_features_sparse
gc.collect()
hstack()으로 결합한 데이터 세트는 csr_matrix 타입이며 135991개의 피처를 가지게 됐습니다. 이제 이 데이터 세트를 회귀에 적용해 price 값을 예측할 모델을 만들겠습니다.
릿지 회귀 모델 구축 및 평가¶
모델을 평가하는 함수 rmsle를 만들겠습니다. 적용할 평가 지표는 캐글에서 제시한 RMSLE(Root Mean Square Logarithmic Error) 방식으로 하겠습니다.
ε=√1nn∑i=1(log(pi+1)−log(ai+1))2pi: 정답,ai: 예측값
여기서 주의해야할 점은 이전에 우리는 정규 분포 형태를 만들기 위해 price칼럼 값에 log값을 씌웠씁니다. 따라서 로그의 역변환인 지수(Exponential) 변환을 수행해 원복해서 rmsle() 함수에 적용해야합니다. 이렇게 원복된 데이터에 RMSLE를 적용할 수 있도록 evaluate_org_price() 함수를 생성하겠습니다.
def rmsle(y, y_pred):
# underflow, overflow를 막기 위해 log가 아닌 log1p로 rmsle 계산
return np.sqrt(np.mean(np.power(np.log1p(y)-np.log1p(y_pred), 2)))
def evaluate_org_price(y_test, preds):
# 원본 데이터는 log1p로 변환됐으므로 expm1로 원복 필요
preds_expm = np.expm1(preds)
y_test_expm = np.expm1(np.array(y_test).astype('float'))
# rmsle로 RMSLE 값 추출
rmsle_result = rmsle(y_test_expm, preds_expm)
return rmsle_result
사이킷런의 Estimator 객체와 희소 행렬 데이터 세트들의 list인 matrix_list를 인자로 갖는 함수 model_train_predict() 함수를 만들겠습니다.
import gc
from scipy.sparse import hstack
def model_train_predict(model, matrix_list):
# scipy.sparse 모듈의 hstack을 이용해 희소 생렬 결합
X = hstack(matrix_list).tocsr()
X_train, X_test, y_train, y_test = train_test_split(X, mercari_df['price'],\
test_size=0.2, random_state=156)
# 모델 학습 및 예측
model.fit(X_train, y_train)
preds = model.predict(X_test)
del X, X_train, X_test, y_train
gc.collect()
return preds, y_test
Ridge를 이용해 Mercari Price의 회귀 예측을 수행해보겠습니다. 본격적으로 예측하기 전이므로 test 겸 item_description과 같은 텍스트 형태의 속성이 Mercari Price 예측에 영향을 미치는지 알아보겠습니다.
# price의 Other_Null값이 str이라서 float값인 mean()으로 대체했다.
mercari_df['price'][mercari_df[mercari_df['price']=='Other_Null'].index] = 0
mercari_df['price'][mercari_df[mercari_df['price']== 0].index] = mercari_df['price'].mean()
linear_model = Ridge(solver = 'lsqr', fit_intercept=False)
# item_description을 제외했을 때
sparse_matrix_list = (X_name, X_brand, X_item_cond_id,\
X_shipping, X_cat_dae, X_cat_jung, X_cat_so)
linear_preds, y_test = model_train_predict(model=linear_model, matrix_list=sparse_matrix_list)
print('Item Description을 제외했을 때 rmsle 값:', evaluate_org_price(y_test, linear_preds))
# item_description을 포함했을 때
sparse_matrix_list = (X_descp, X_name, X_brand, X_item_cond_id,\
X_shipping, X_cat_dae, X_cat_jung, X_cat_so)
linear_preds, y_test = model_train_predict(model=linear_model, matrix_list=sparse_matrix_list)
print('Item Description을 포함한 rmsle 값:', evaluate_org_price(y_test, linear_preds))
Item Description을 포함했을 때 rmsle 값이 많이 감소한 것으로 보아 Item description 영향이 중요함을 알 수 있습니다.
LightGBM 회귀 모델 구축과 앙상블을 이용한 최종 예측 평가¶
본격적으로 LightGBM을 이용해 회귀를 수행한 뒤, 위에서 구한 Ridge 모델 예측값가 LightGBM 모델 예측값을 간단한 Ensemble 방식으로 섞어서 최종 회귀 예측값을 평가하겠습니다.
먼저 LightGBM으로 회귀를 수행하겠습니다. n_estimators를 1000 이상 증가시키면 예측 성능은 좋아지는데, PC에서 수행 시간이 1시간 이상 걸립니다. n_estimators를 200으로 작게 설정하고 예측 성능을 측정해보겠습니다.
from lightgbm import LGBMRegressor
sparse_matrix_list = (X_descp, X_name, X_brand, X_item_cond_id,\
X_shipping, X_cat_dae, X_cat_jung, X_cat_so)
lgbm_model = LGBMRegressor(n_estimators=200, learning_rate=0.5, num_leaves=125, random_state=156)
lgbm_preds, y_test = model_train_predict(model = lgbm_model, matrix_list=sparse_matrix_list)
print('LightGBM rmsle 값:', evaluate_org_price(y_test, lgbm_preds))
앞 예제의 Rigge보다 예측 성능이 좋아졌습니다. LightGBM과 Ridge의 예측 결과값을 서로 앙상블해 결과값을 도출하겠습니다. 배합 비율을 임의로 정했습니다.
preds = lgbm_preds * 0.45 + linear_preds*0.55
print('LightGBM과 Ridge를 ensemble한 최종 rmsle 값:', evaluate_org_price(y_test, preds))
간단한 앙상블 방식으로 예측 성능을 더 개선했습니다.
궁금한점¶
- pd.set_option
- Series.str.len()
- LabelBinarizer
- scipy.sparse.hstack()
- del '객체 변수명'
- gc.collect()
- np.power()은 지수 함수인 듯
출처: 파이썬 머신러닝 완벽가이드
사진출처:
'파이썬 머신 러닝 완벽 가이드' 카테고리의 다른 글
콘텐츠 기반 필터링 실습 - TMDB 5000 영화 데이터 세트 (0) | 2021.12.13 |
---|---|
추천 시스템 (0) | 2021.12.11 |
텍스트 분류 실습 - 20 뉴스그룹 분류 (0) | 2021.12.05 |
텍스트 분석 (0) | 2021.12.03 |
군집화 실습 - 고객 세그먼테이션 (0) | 2021.12.02 |