본문 바로가기

NPL with ML

텍스트 분석 실습 - Mercari Price Suggestion Challenge

반응형

 

관련 Raw Data를 내려받습니다.  약 100메가 정도됩니다. [링크]

 


데이타 전처리

관련 Library를 import하고 피처 타입과 Null여부를 확인하기 위해 info()를 실행합니다.

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(r'C:\Users\HANA\PycharmProjects\HANATOUR\NLP\TEXT_Example\mercari.tsv', sep='\t')
print(mercari_df.shape)
print(mercari_df.head(3))
print(mercari_df.info())

 

null 데이타가 많이 보이네요. 이 null데이터는 이후에 적절한 문자열로 치환하겠습니다.

RangeIndex: 693359 entries, 0 to 693358
Data columns (total 7 columns):
 #   Column             Non-Null Count   Dtype 
---  ------             --------------   ----- 
 0   test_id            693359 non-null  int64 
 1   name               693359 non-null  object
 2   item_condition_id  693359 non-null  int64 
 3   category_name      690301 non-null  object
 4   brand_name         397834 non-null  object
 5   shipping           693359 non-null  int64 
 6   item_description   693359 non-null  object
dtypes: int64(3), object(4)
memory usage: 37.0+ MB

 

Target값인 Price칼럼의 데이터 분포도를 살펴보겠습니다. 회귀에서 Target값의 정규 분포도는 매우 중요합니다. 왜곡돼 있을 경우 보통 로그를 씌워서 변환하면 대부분 정규 분포의 형태를 가지게 됩니다. 

import matplotlib.pyplot as plt
import seaborn as sns

y_train_df = mercari_df['price']
plt.figure(figsize=(6,4))
sns.distplot(y_train_df, kde=False)
plt.show()

 

Price값이 비교적 적은 가격을 가진 데이터 값에 왜곡돼 있어, 로그 값으로 분포도를 다시 그려봅니다.

 

로그값으로 변환하면 price값이 비교적 정규 분포에 가까운 데이터를 이루게 됩니다. 그러므로 price값을 log변환값으로 변환합니다. 

import numpy as np

y_train_df = np.log1p(y_train_df)
sns.distplot(y_train_df, kde=False)
plt.show()

mercari_df['price'] = np.log1p(mercari_df['price'])
print(mercari_df['price'].head(3))

 

그 외 다른 피처의 값도 분포도 알아봅니다.

print(mercari_df['shipping'].value_counts())
print(mercari_df['item_condition_id'].value_counts())
boolean_cond = mercari_df['item_description']=='No description yet'

print(mercari_df[boolean_cond]['item_description'].count())

 

item_description의 값이 없을 경우 No description yet의 경우가 많아 해당 값은 적절한 값으로 변환하겠습니다.

Name: price, dtype: float64
0    819435
1    663100

Name: shipping, dtype: int64
1    640549
3    432161
2    375479
4     31962
5      2384

Name: item_condition_id, dtype: int64
82489

 

/으로 분리된 카테고리를 하나의 문자열로 나타내기 위해 대/중/소로 나누어 카테고리를 분리합니다. 중요한것은 zip과 apply lambda식에 적용하여 한번에 분리합니다.

def split_cat(category_name):
    try:
        return category_name.split('/')
    except:
        return ['Other_Null','Other_Null', 'Other_Null']

mercari_df['cat_dae'], mercari_df['cat_jung'], mercari_df['cat_so'] = \
    zip(*mercari_df['category_name'].apply(lambda x : split_cat(x)))

print(mercari_df['cat_dae'].value_counts())
print(mercari_df['cat_jung'].value_counts())
print(mercari_df['cat_so'].value_counts())

print(mercari_df['cat_jung'].nunique())
print(mercari_df['cat_so'].nunique())

 

Women                     664385
Beauty                    207828
Kids                      171689
Electronics               122690
Men                        93680
Home                       67871
Vintage & Collectibles     46530
Other                      45351
Handmade                   30842
Sports & Outdoors          25342
Other_Null                  6327
Name: cat_dae, dtype: int64


Athletic Apparel        134383
Makeup                  124624
Tops & Blouses          106960
Shoes                   100452
Jewelry                  61763
                         ...  
Candles                     64
Ceramics and Pottery        57
Dolls and Miniatures        49
Books and Zines             46
Quilts                      31
Name: cat_jung, Length: 114, dtype: int64


Pants, Tights, Leggings    60177
Other                      50224
Face                       50171
T-Shirts                   46380
Shoes                      32168
                           ...  
Seasonal                       1
Towel                          1
Ephemera                       1
Doll Clothes                   1
Tiles                          1
Name: cat_so, Length: 871, dtype: int64


114

871

 

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['item_description']= mercari_df['item_description'].fillna(value='item_description')

print(mercari_df.isnull().sum())

Null값은 Other_Null로 하고 Null이 있는지 확인합니다.

train_id             0
name                 0
item_condition_id    0
category_name        0
brand_name           0
price                0
shipping             0
item_description     0
dtype: int64

 


피처 인코딩과 피처 벡터화

해당 데이터 세트는 문자열 칼럼이 많습니다. 이 문자열 칼럼 중 레이블 또는 원-핫 인코딩을 수행하거나 피처 벡터화로 변환할 칼럼을 선별해 보겠습니다. 먼저 이 피처를 어떤 방식으로 변환할지 검토한 후에 추후에 일괄적으로 전체 속성의 변환 작업을 적용하겠습니다.

 

Price를 통해 상품 가격을 예측해야 하므로 회귀 모델을 기반으로 합니다. 선형 회귀 모델과 회귀 트리 모델을 모두 적용해 보겠습니다. 특히 선형 회귀의 경우 원-핫 인코딩 적용이 훨씬 선호되므로 인코딩할 피처는 모두 원-핫 인코딩을 적용하겠습니다.

 

피처 벡터화의 경우는 비교적 짧은 텍스트의 경우는 Count 기반의 벡터화를, 긴 텍스트는 TD-IDF기반의 벡터화를 적용하겠습니다.

 

먼저 brand_name 컬럼에 대해 검토해 보겠습니다. brand_name 칼럼은 상품의 브랜드명입니다. 어떤 유형으로 되어 있는지와 유형 건수, 대표적인 브랜드명을 5개 정도 확인해 보겠습니다.   동일한 방법으로 name도 확인합니다.

print(mercari_df['brand_name'].nunique())
print(mercari_df['brand_name'].value_counts()[:5])

print(mercari_df['name'].nunique())
print(mercari_df['name'].value_counts()[:10])

 

brand_name은 4,810개 / name은 1,225,273개 입니다. brand_name의 경우 대부분 명료한 문자열로 되어 있어 원-핫 인코딩으로 변환하겠습니다. Name의 경우 적은 단어 위주의 텍스트 형태로 되어 있어 Count기반으로 피처 벡터화 합니다.

 

이런식으로 확인하면서 category_name컬럼은 원-핫 인코딩을 shipping컬럼은 item_condition_id컬럼 또한 원-핫 인코딩을 사용합니다.

4810

Other_Null           632682
PINK                  54088
Nike                  54043
Victoria's Secret     48036
LuLaRoe               31024
Name: brand_name, dtype: int64

1225273

Bundle                 2232
Reserved                453
Converse                445
BUNDLE                  418
Dress                   410
Coach purse             404
Lularoe TC leggings     396
Romper                  353
Nike                    340
Vans                    334

 

item_description의 경우 평균 길이가 145자로 비교적 크므로 해당 칼럼은 TF-IDF로 변환하겠습니다.

print(mercari_df['item_description'].str.len().mean())
print(mercari_df['item_description'][:3])
145.71140512702905
0                                   No description yet
1    This keyboard is in great condition and works ...
2    Adorable top with a hint of lace and a key hol...
Name: item_description, dtype: object

 

name과 item_description칼럼을 피처 팩터화합니다. name칼럼의 경우는 CountVectorizer로, item_description의 경우는 TfidfVectorizer로 변환하겠습니다.

cnt_vec = CountVectorizer()
X_name = cnt_vec.fit_transform(mercari_df.name)

tfidf_descp = TfidfVectorizer(max_features=5000, ngram_range=(1,3), stop_words='english')
X_descp = tfidf_descp.fit_transform(mercari_df['item_description'])

print(X_name.shape)
print(X_descp.shape)

 

(1482535, 105757)
(1482535, 5000)

CountVectorizer, TfidfVectorizer가 fit_transform()을 통해 반환하는데이터는 희소 행렬입니다. 희소 행렬 객체 변수인 X_name과 X_decp를 새로 결합해 새로운 데이터 세트로 구성해야 하고 , 앞으로 인코딩 될 모든 변수도 X_name, X_descp와 결합돼 ML모델을 실행하는 기반 데이터 세트로 재구성돼야 합니다.

 

이를 위해서 이 인코딩 대상 칼럼도 밀집 행렬 형태가 아닌 희소 행렬 형태로 인코딩을 적용한 뒤, 함께 결합합니다. 

사이킷런은 원-핫 인코딩을 위해 OnehohtEncoder와 LabelBinarizer클래스를 제공합니다. 이 중 LabelBinarizer클래스는 희소 행렬 형태의 원-핫 인코딩 변환을 지원합니다. 생성 시 sparse_out=True로 파라미터를 설정해주기만 하면 됩니다. 

from sklearn.preprocessing import LabelBinarizer

#각 피처를 희소 행렬 원-핫 인코딩 변환
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['lb_item_cond_id'])

lb_shipping = LabelBinarizer(sparse_output=True)
X_shipping = lb_shipping.fit_transform(mercari_df['shipping'])

lb_cat_dae = LabelBinarizer(sparse_output=True) 
X_cat_dae = lb_cat_dae.fit_transform(mercari_df['cat_dae'])

print(type(X_brand), type(X_item_cond_id))
print(X_brand.shape, X_item_cond_id.shape)

 

인코딩 변환된 데이터 세트가 CSR형태로 변환된 csr_matrix타입입니다.

<class 'scipy.sparse.csr.csr_matrix'> <class 'scipy.sparse.csr.csr_matrix'>
(1482535, 4810) (1482535, 5)

 

앞에서 피처 벡터화 변환한 데이터 세트와 희소 인코딩 변환된 데이터 세트를 hstack()을 이용해 모두 결합해 보겠습니다. 만들어진 결합 데이터가 비교적 많은 메모리를 잡아먹기 때문에 개인용 PC에서 메모리 오류가 발생할 수 있기에 메모리를 삭제합니다. 

 

이렇게 만들어진 데이터 세트에 회귀를 적용해 price값을 예측할 수 있도록 하겠습니다.

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)

#데이터 세트가 메모리를 많이 차지하므로 사용 목적이 끝났으면 바로 메모리에서 삭제
del X_features_sparse
gc.collect()

 


릿지 회귀 모델 구축 및 평가

적용할 평가 지표는 RMSLE(Root Mean Square Logarithmic Error)방식으로 합니다. RMSLE는 RMSE와 유사하나 오류 값에 로그를 취해 RMSE를 구하는 방식입니다. 

 

주의할 사항은 원본 데이터의 price칼럼의 값은 왜곡된 데이터 분포를 가지고 있기 때문에 이를 정규 분포 형태로 유도하기 위해 로그 값을 취해 변환했습니다. 

 

Ridge를 이용해 회귀 예측을 수행합니다.

def rmsle(y, y_pred):
    #underflow, overflow를 막기 위해 log가 아닌 log1p로 rmsle계산
    return np.sqrt(np.power(np.log1p(y)-np.log1p(y_pred), 2))

def evaluate_org_price(y_test, preds):
    #원본 데이터는 log1p로 변환되었으므로 exmpm1로 원복 필요
    preds_exmpm = np.expm1(preds)
    y_test_exmpm = np.expm1((y_test))

    #rmsle로 RMSLE값 추출
    rmsle_result = rmsle(y_test_exmpm, preds_exmpm)
    return rmsle_result

import gc
from scipy.sparse import hstack

def model_train_predict(model, matrix_ist):
    #scipy.sparse 모듈의 hstack을 이용해 희소 행렬 결합
    X = hstack(matrix_ist).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_test
    gc.collect()

    return preds, y_test

linear_model = Ridge(solver='lsqr', fit_intercept=False)
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_ist=sparse_matrix_list)
print(evaluate_org_price(y_test, linear_preds))

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_ist=sparse_matrix_list)
print(evaluate_org_price(y_test, linear_preds))

 


LightGBM 회귀 모델 구축과 앙상블을 이용한 최종 예측 평가

LightGBM을 이용해 회귀를 수행한 뒤, 위에서 구한 릿지 모델 예측값과 LightGBM모델 예측값을 간단한 앙상블(Ensemble)방식으로 섞어서 최종 회귀 예측값을 평가합니다.  

 

n_estimator를 1000이상 증가시키면 예측 성능은 조금 좋아지는데, 수행 시간이 PC에서 1시간 이상 걸립니다. n_estimator를 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_ist=sparse_matrix_list)
print(evaluate_org_price(y_test, lgbm_preds))

preds = lgbm_preds*0.45+linear_preds*0.55
print(evaluate_org_price(preds))

 

이 후 2개의 모델을 합쳐 수행합니다.

 

preds = lgbm_preds*0.45+linear_preds*0.55
print(evaluate_org_price(preds))
반응형