Feature Engineering, Stacking và Ensemble cho cuộc thi Phân loại sắc thái bình luận

nlp
ensemble-learning
ensemble-modeling
demo_nlp_contest
stacking

#1

1. Giới thiệu chung

Để tăng kết quả của bài toán, mọi người hay nhắc đến 3 khái niệm: Stacking, BlendingEnsemble. Qua việc tiếp xúc với nhiều người thì mình thấy mỗi người hiểu các khái niệm này khác nhau. Với mình thì mình chỉ dùng hai thuật ngữ StackingEnsemble. Trong bài viết này mình sẽ giới thiệu về hai khái niệm này. Định nghĩa có thể bị nhầm lẫn, mong các bạn bình luận để góp ý chỉnh trong bài viết này.

2. Định nghĩa

Giả sử hiện tại bạn có rất nhiều model, thì

2.1. Ensemble

Là việc bạn lấy trung bình cộng hoặc trung bình theo hệ số của các kết quả đó lại.

2.2. Stacking

  • Là việc bạn chia tập dữ liệu thành nhiều phần (fold) khác nhau.
  • Với mỗi model, sẽ dùng 1 phần cho validation và các phần còn lại để train.
  • Sau mỗi lần huấn luyện một fold, model sẽ dự đoán tập validation của fold đó (gọi là out-of-fold) và giữ lại làm feature cho các layer tiếp theo.
  • Tương tự đối với tập test, sau mỗi fold, tập test sẽ được dự đoán và giữ lại làm feature để test cho các layer tiếp theo
  • Việc lặp đi lặp lại như vậy được gọi là stacking.
  • Mỗi một layer có thể bao gồm 1 hoặc nhiều model với các param khác nhau.

Để làm rõ và so sánh hiệu quả của hai phương pháp này, mình sẽ demo bằng dữ liệu của cuộc thi Phân loại sắc thái bình luận. Code demo của bài viết này được public ở repo này

3. Làm features

Mình làm một số feature đơn giản sau.

3.1 Dựa vào icon cảm xúc (emoji)

Mình nhận thấy có trong bình luận đó có sử dụng một số icon thể hiện cảm xúc như: :slight_smile:, :smiley:, :blush:, :wink:, … xuất hiện nhiều trong những bình luận tốt. Và những icon như: :frowning: , :cry:, :anger:, … xuất hiện trong những bình luận không tốt. Như vậy ta có thể bám vào những yếu tốt này để làm feature

  • Kiểm tra xem trong bình luận có những emoji

      import emoji
    
      def extract_emojis(str):
        return [c for c in str if c in emoji.UNICODE_EMOJI]
    
      good_df = train_df[train_df['label'] == 0]
      good_comment = good_df['comment'].values
      good_emoji = []
      for c in good_comment:
          good_emoji += extract_emojis(c)
    
      good_emoji = np.unique(np.asarray(good_emoji))
    
    
      bad_df = train_df[train_df['label'] == 1]
      bad_comment = bad_df['comment'].values
    
      bad_emoji = []
      for c in bad_comment:
          bad_emoji += extract_emojis(c)
    
      bad_emoji = np.unique(np.asarray(bad_emoji))
    

    Kết quả đạt được như sau: Screenshot%20from%202019-02-19%2012-07-35

    Screenshot%20from%202019-02-19%2012-07-43

    Nhận thấy là trong những bình luận tốt/xấu thì có cả những emoji xấu/tốt. Có một chút nhiễu ở đây hay là do label gán nhãn sai thì mình k chắc :smiley:. Tuy nhiên đừng lo lắng quá vì mình thấy nhiễu ở đây không nhiều.

  • Đếm số lượng emoji trong bình luận và một số feature liên quan

      def count_good_bad_emoji(row):
          comment = row['comment']
          n_good_emoji = 0
          n_bad_emoji = 0
          for c in comment:
              if c in good_emoji_fix:
                  n_good_emoji += 1
              if c in bad_emoji_fix:
                  n_bad_emoji += 1
          
          row['n_good_emoji'] = n_good_emoji
          row['n_bad_emoji'] = n_bad_emoji
          
          return row
      df = df.apply(count_good_bad_emoji, axis=1)
    
      df['good_bad_emoji_ratio'] = df['n_good_emoji'] / df['n_bad_emoji']
      df['good_bad_emoji_ratio'] = df['good_bad_emoji_ratio'].replace(np.nan, 0)
      df['good_bad_emoji_ratio'] = df['good_bad_emoji_ratio'].replace(np.inf, 99)
      df['good_bad_emoji_diff'] = df['n_good_emoji'] - df['n_bad_emoji']
      df['good_bad_emoji_sum'] = df['n_good_emoji'] + df['n_bad_emoji']
    

3.2 Một số statistic feature

  # Some features
  df['comment'] = df['comment'].astype(str).fillna(' ')
  df['comment'] = df['comment'].str.lower()
  df['num_words'] = df['comment'].apply(lambda s: len(s.split()))
  df['num_unique_words'] = df['comment'].apply(lambda s: len(set(w for w in s.split())))
  df['words_vs_unique'] = df['num_unique_words'] / df['num_words'] * 100

3.3 TFIDF features

tfidf = TfidfVectorizer(
    min_df = 5, 
    max_df = 0.8, 
    max_features=10000,
    sublinear_tf=True
)

4. Stacking

Mình sử dụng pystacknet của idol Kazanova trên Kaggle

models=[ 
    ######## First level ########
    [
        RandomForestClassifier (n_estimators=100, criterion="entropy", max_depth=5, max_features=0.5, random_state=1),
        ExtraTreesClassifier (n_estimators=100, criterion="entropy", max_depth=5, max_features=0.5, random_state=1),
        GradientBoostingClassifier(n_estimators=100, learning_rate=0.1, max_depth=5, max_features=0.5, random_state=1),
        LogisticRegression(random_state=1)
    ],
    ######## Second level ########
    [
        RandomForestClassifier (n_estimators=200, criterion="entropy", max_depth=5, max_features=0.5, random_state=1)
    ]
]

Ở đây mình dùng 4 models cho layer 0 bao gồm:

  • RandomForestClassifier
  • ExtraTreesClassifier
  • GradientBoostingClassifier
  • LogisticRegression

Và model RandomForestClassifier cho layer cuối cùng. Các bạn có thể sử dụng bao nhiêu layer và model tùy ý.

Việc training diễn ra tự động

from pystacknet.pystacknet import StackNetClassifier

model = StackNetClassifier(
    models, metric="f1", 
    folds=5,
    restacking=False, 
    use_retraining=True, 
    use_proba=True, 
    random_state=12345, n_jobs=1, verbose=1
)

model.fit(X_train, y_train)
preds=model.predict_proba(X_test)

Kết quả log được hiển thị như sau:

====================== Start of Level 0 ======================
Input Dimensionality 2687 at Level 0 
4 models included in Level 0 
/home/ngxbac/anaconda3/envs/general/lib/python3.6/site-packages/sklearn/linear_model/logistic.py:433: FutureWarning: Default solver will be changed to 'lbfgs' in 0.22. Specify a solver to silence this warning.
  FutureWarning)
Level 0, fold 1/5 , model 0 , f1===0.789457 
Level 0, fold 1/5 , model 1 , f1===0.813472 
Level 0, fold 1/5 , model 2 , f1===0.856148 
Level 0, fold 1/5 , model 3 , f1===0.875267 
=========== end of fold 1 in level 0 ===========
/home/ngxbac/anaconda3/envs/general/lib/python3.6/site-packages/sklearn/linear_model/logistic.py:433: FutureWarning: Default solver will be changed to 'lbfgs' in 0.22. Specify a solver to silence this warning.
  FutureWarning)
Level 0, fold 2/5 , model 0 , f1===0.812040 
Level 0, fold 2/5 , model 1 , f1===0.824688 
Level 0, fold 2/5 , model 2 , f1===0.868097 
Level 0, fold 2/5 , model 3 , f1===0.882749 
=========== end of fold 2 in level 0 ===========
/home/ngxbac/anaconda3/envs/general/lib/python3.6/site-packages/sklearn/linear_model/logistic.py:433: FutureWarning: Default solver will be changed to 'lbfgs' in 0.22. Specify a solver to silence this warning.
  FutureWarning)
Level 0, fold 3/5 , model 0 , f1===0.804277 
Level 0, fold 3/5 , model 1 , f1===0.821736 
Level 0, fold 3/5 , model 2 , f1===0.873440 
Level 0, fold 3/5 , model 3 , f1===0.882118 
=========== end of fold 3 in level 0 ===========
/home/ngxbac/anaconda3/envs/general/lib/python3.6/site-packages/sklearn/linear_model/logistic.py:433: FutureWarning: Default solver will be changed to 'lbfgs' in 0.22. Specify a solver to silence this warning.
  FutureWarning)
Level 0, fold 4/5 , model 0 , f1===0.800000 
Level 0, fold 4/5 , model 1 , f1===0.825202 
Level 0, fold 4/5 , model 2 , f1===0.873396 
Level 0, fold 4/5 , model 3 , f1===0.881789 
=========== end of fold 4 in level 0 ===========
/home/ngxbac/anaconda3/envs/general/lib/python3.6/site-packages/sklearn/linear_model/logistic.py:433: FutureWarning: Default solver will be changed to 'lbfgs' in 0.22. Specify a solver to silence this warning.
  FutureWarning)
Level 0, fold 5/5 , model 0 , f1===0.802153 
Level 0, fold 5/5 , model 1 , f1===0.813953 
Level 0, fold 5/5 , model 2 , f1===0.862033 
Level 0, fold 5/5 , model 3 , f1===0.879886 
=========== end of fold 5 in level 0 ===========
Level 0, model 0 , f1===0.801585 
Level 0, model 1 , f1===0.819810 
Level 0, model 2 , f1===0.866623 
Level 0, model 3 , f1===0.880362 
/home/ngxbac/anaconda3/envs/general/lib/python3.6/site-packages/sklearn/linear_model/logistic.py:433: FutureWarning: Default solver will be changed to 'lbfgs' in 0.22. Specify a solver to silence this warning.
  FutureWarning)
Output dimensionality of level 0 is 4 
====================== End of Level 0 ======================
 level 0 lasted 96.712902 seconds 
====================== Start of Level 1 ======================
Input Dimensionality 4 at Level 1 
1 models included in Level 1 
Level 1, fold 1/5 , model 0 , f1===0.874386 
=========== end of fold 1 in level 1 ===========
Level 1, fold 2/5 , model 0 , f1===0.887487 
=========== end of fold 2 in level 1 ===========
Level 1, fold 3/5 , model 0 , f1===0.888330 
=========== end of fold 3 in level 1 ===========
Level 1, fold 4/5 , model 0 , f1===0.887324 
=========== end of fold 4 in level 1 ===========
Level 1, fold 5/5 , model 0 , f1===0.884063 
=========== end of fold 5 in level 1 ===========
Level 1, model 0 , f1===0.884318 
Output dimensionality of level 1 is 1 
====================== End of Level 1 ======================
 level 1 lasted 18.309358 seconds 
====================== End of fit ======================
 fit() lasted 115.023421 seconds 
====================== Start of Level 0 ======================
1 estimators included in Level 0 
====================== Start of Level 1 ======================
1 estimators included in Level 1 

Như vậy kết quả Cross-Validation của StackingF1=0.884318. Hãy thử submit xem sao:


pred_cls = np.argmax(preds, axis=1)
submission = pd.read_csv("./data/SA_demo/sample_submission.csv")
submission['label'] = pred_cls
submission.to_csv("stack_demo.csv", index=False)

=> Kết quả Public LB là: 0.88

5. Ensemble

Mình sẽ train lại 4 model trên tại layer 0 của Stacking và lấy trung bình kết quả lại để so sánh.

from sklearn.model_selection import cross_val_predict
models = [
    RandomForestClassifier (n_estimators=100, criterion="entropy", max_depth=5, max_features=0.5, random_state=1),
    ExtraTreesClassifier (n_estimators=100, criterion="entropy", max_depth=5, max_features=0.5, random_state=1),
    GradientBoostingClassifier(n_estimators=100, learning_rate=0.1, max_depth=5, max_features=0.5, random_state=1),
    LogisticRegression(random_state=1)
]

def cross_val_and_predict(clf, X, y, X_test, nfolds):
    kf = StratifiedKFold(n_splits=nfolds, shuffle=True, random_state=42)
    
    oof_preds = np.zeros((X.shape[0], 2))
    sub_preds = np.zeros((X_test.shape[0], 2))
    
    for fold, (train_idx, valid_idx) in enumerate(kf.split(X, y)):
        X_train, y_train = X[train_idx], y[train_idx]
        X_valid, y_valid = X[valid_idx], y[valid_idx]
        
        clf.fit(X_train, y_train)
        
        oof_preds[valid_idx] = clf.predict_proba(X_valid)
        sub_preds += clf.predict_proba(X_test) / kf.n_splits
        
    return oof_preds, sub_preds

sub_preds = []

for clf in models:
    oof_pred, sub_pred = cross_val_and_predict(clf, X_train, y_train, X_test, nfolds=5)
    oof_pred_cls = oof_pred.argmax(axis=1)
    oof_f1 = f1_score(y_pred=oof_pred_cls, y_true=y_train)
    
    print(clf.__class__)
    print(f"F1 CV: {oof_f1}")
    
    sub_preds.append(sub_pred)

sub_preds = np.asarray(sub_preds)
sub_preds = sub_preds.mean(axis=0)
sub_pred_cls = sub_preds.argmax(axis=1)

submission_ensemble = submission.copy()
submission_ensemble['label'] = sub_pred_cls
submission_ensemble.to_csv("ensemble.csv", index=False)

Kết quả log khi train như sau:

<class 'sklearn.ensemble.forest.RandomForestClassifier'>
F1 CV: 0.8028473369772468
<class 'sklearn.ensemble.forest.ExtraTreesClassifier'>
F1 CV: 0.8157690315898497
<class 'sklearn.ensemble.gradient_boosting.GradientBoostingClassifier'>
F1 CV: 0.8664189047051398
/home/ngxbac/anaconda3/envs/general/lib/python3.6/site-packages/sklearn/linear_model/logistic.py:433: FutureWarning: Default solver will be changed to 'lbfgs' in 0.22. Specify a solver to silence this warning.
  FutureWarning)
<class 'sklearn.linear_model.logistic.LogisticRegression'>
F1 CV: 0.8793558041758711

Ta có thể thấy kết quả Cross validation của từng mô hình khá giống với các mô hình tại Layer 0 của stacking. Sau khi submit kết quả thì Ensemble cho mình Public LB: 0.873

6 Thảo luận

  • Trong ví dụ trên Stacking cho kết quả tốt hơn Ensemble. Có thể việc điều chỉnh hệ số cho Ensemble sẽ mang đến kết quả tốt hơn nữa.
  • Thường trong các cuộc thi thì top team hay sử dụng Stacking hơn Ensemble đặc biệt là trong các cuộc thi có tính rủi ro cao (shap-up giữa public và private). Điều này là Lession learn của mình qua một số cuộc thi
  • Các bạn có thể custom models (bằng keras, pytorch,…) cho thích hợp với thư viện pystacknet này
  • Stacking không phải là nhất, chưa chắc đã tốt. Cái này tùy vào kinh nghiệm chọn model của mỗi người.
  • Stacking mất thời gian hơn Ensemble.
  • Ở cuộc thi này, do data đơn giản, ít nên việc sử dụng các model của sklearn mang lại kết quả tốt => Stacking đạt kết quả cao. Tuy nhiên, khi data nhiều dần thì ta cần xem xét lại.

Chúc các bạn đạt kết quả cao trong cuộc thi,

Cheers,


Mô hình ensemble đơn giản cho phân loại sắc thái bình luận (7th place solution)
#2

Anh ơi, trường hợp những câu không có emoj thì phân tích như thế nào ạ ?:thinking::thinking:


#3

Mình có làm các feature liên quan đến emoji ở trên như là:

  • n_good_emoji
  • n_bad_emoji
  • good_bad_emoji_ratio
  • good_bad_emoji_diff
  • good_bad_emoji_sum

Với việc câu không có emoji thì nó được thể hiện qua các feature như là: n_good_emoji = 0, n_bad_emoji = 0, ... Như vậy model có thể học được đặc tính kể cả có emoji hay không


#4

Dạ, cho em hỏi thêm ý tưởng đoạn code này là gì vậy anh:

EXCLUED_COLS = ['id', 'comment', 'label']
static_cols = [c for c in train_df.columns if not c in EXCLUED_COLS]
X_train_static = train_df[static_cols].values
X_test_static = test_df[static_cols].values
X_train = hstack([X_train_tfidf, csr_matrix(X_train_static)]).tocsr()
X_test = hstack([X_test_tfidf, csr_matrix(X_test_static)]).tocsr()

#5

Đoạn này dùng để concat TFIDF và statistic features thôi bạn.


#6

Đây là stack kiểu bagging đúng không anh?


#7

Như mình đã nói ở trên thì có thể mỗi người sẽ định nghĩa về stacking, ensemble,… riêng. Thật khó để trả lời câu hỏi của bạn nếu bạn k định nghĩa nó :smiley: . Nhưng theo những gì mình đc nghe thì có vẻ như không phải. Bagging là loại dùng nhiều version của một model để ensemble. VD bạn chạy một model với nhiều seed khác nhau rồi ensemble chúng lại.


#8

Về stacking, ý bạn là làm CV trên cả 2 tập test và train phải ko?


#9

Bagging là từ tập train bạn tạo ra nhiều tập khác gọi là bootstrap sample, sau đó bạn train trên các tập này rồi nôm na là lấy trung bình cộng của các models thu đc và dùng model đó tạo ra dự đoán cuối cùng.

Stacking là bạn có nhiều models, sử dụng dự đoán của các models này để làm input để train cho các models khác rồi mới ra dự đoán cuối cùng.

Bagging lấy dữ liệu đầu vào là tập train, còn stacking lấy đầu vào là dự đoán của các models,