【精通特徵工程】學習筆記Day3&2.13&D4章&P52-64頁
4、特徵縮放的效果:從詞袋到 tf-idf
4.1 tf-idf:詞袋的一種簡單擴展
- tf-idf:詞頻 - 逆文檔頻率
- tf-idf 計算的不是 數據集中每個單詞在每個文檔中的原本計數,而是一個歸一化的計數,其中每個單詞的計 數要除以這個單詞出現在其中的文檔數量
bow(w, d) = 單詞 w 在文檔 d 中出現的次數
tf-idf(w, d) = bow(w, d) * N / ( 單詞 w 出現在其中的文檔數量 )
4.2 tf-idf 方法測試
tf-idf 通過乘以一個常數,對單詞計數特徵進行了轉換。因此,它是一種特徵縮放方法
- Step1:使用 Python 加載並清理 Yelp 點評數據集
>>> import json
>>> import pandas as pd
# 加載Yelp商家數據
>>> biz_f = open('yelp_academic_dataset_business.json')
>>> biz_df = pd.DataFrame([json.loads(x) for x in biz_f.readlines()]) >>> biz_f.close()
# 加載Yelp點評數據
>>> review_file = open('yelp_academic_dataset_review.json')
>>> review_df = pd.DataFrame([json.loads(x) for x in review_file.readlines()]) >>> review_file.close()
# 選取出夜店和餐館
>>> two_biz = biz_df[biz_df.apply(lambda x: 'Nightlife' in x['categories'] or ... 'Restaurants' in x['categories'], ... axis=1)]
# 與點評數據連接,得到兩種類型商家的所有點評
>>> twobiz_reviews = two_biz.merge(review_df, on='business_id', how='inner')
# 去除我們不需要的特徵
>>> twobiz_reviews = twobiz_reviews[['business_id',
... 'name',
... 'stars_y',
... 'text',
... 'categories']]
# 創建目標列——夜店類型的商家爲True,否則爲False
>>> two_biz_reviews['target'] = \
... twobiz_reviews.apply(lambda x: 'Nightlife' in x['categories'], ... axis=1)
4.2.1 創建分類數據集
Yelp商店點評數據爲一個類別不平衡數據集,故可做如下處理:
-
對夜店點評數據進行 10% 的隨機抽樣,對餐館點評數據進行 2.1% 的隨機抽樣(選擇這 樣的比例可以使兩個類別的抽樣數據基本相當)。
-
按照 70/30 的比例將這個數據集劃分爲訓練集和測試集。在這個例子中,訓練集有 29 264 條點評數據,測試集有 12 542 條點評數據。
-
訓練數據包含 46 924 個唯一單詞,這就是詞袋錶示法的特徵數量。
-
Step2:創建平衡的分類數據集
# 創建一個類別平衡的子樣本,供練習使用
>>> nightlife = \
... twobiz_reviews[twobiz_reviews.apply(lambda x: 'Nightlife' in x['categories'],
...
>>> restaurants = \
... twobiz_reviews[twobiz_reviews.apply(lambda x: 'Restaurants' in x['categories'], ... axis=1)]
>>> nightlife_subset = nightlife.sample(frac=0.1, random_state=123)
>>> restaurant_subset = restaurants.sample(frac=0.021, random_state=123)
>>> combined = pd.concat([nightlife_subset, restaurant_subset])
# 劃分訓練集和測試集
>>> training_data, test_data = modsel.train_test_split(combined,
...
...
>>> training_data.shape
(29264, 5)
>>> test_data.shape
(12542, 5)
train_size=0.7,
random_state=123)
axis=1)]
4.2.2 使用 tf-idf 變換來縮放詞袋
- Step3:轉換特徵
# 用詞袋錶示點評文本
>>> bow_transform = text.CountVectorizer()
>>> X_tr_bow = bow_transform.fit_transform(training_data['text'])
>>> X_te_bow = bow_transform.transform(test_data['text'])
>>> len(bow_transform.vocabulary_)
46924
>>> y_tr = training_data['target']
>>> y_te = test_data['target']
# 使用詞袋矩陣創建tf-idf表示
>>> tfidf_trfm = text.TfidfTransformer(norm=None)
>>> X_tr_tfidf = tfidf_trfm.fit_transform(X_tr_bow)
>>> X_te_tfidf = tfidf_trfm.transform(X_te_bow)
# 僅出於練習的目的,對詞袋錶示進行l2歸一化
>>> X_tr_l2 = preproc.normalize(X_tr_bow, axis=0)
>>> X_te_l2 = preproc.normalize(X_te_bow, axis=0)
注:測試集上的特徵縮放特徵縮放的微妙之處在於,它要求我們知道一些實際中我們很可能不知道的 特徵統計量,比如均值、方差、文檔頻率、l2 範數,等等。爲了計算出 tf-idf 表示,我們必須基於訓練數據計算出逆文檔頻率,並用這些統計量既縮放訓 練數據也縮放測試數據。在 scikit-learn 中,在訓練數據上擬合特徵轉換器相 當於收集相關統計量。然後可以將擬合好的特徵轉換器應用到測試數據上。
4.2.3 使用邏輯迴歸進行分類
Step4:使用默認參數訓練邏輯迴歸分類器
>>> def simple_logistic_classify(X_tr, y_tr, X_test, y_test, description):
### 輔助函數,用來訓練邏輯迴歸分類器,並在測試數據上進行評分。
... m = LogisticRegression().fit(X_tr, y_tr)
... s = m.score(X_test, y_test)
... print ('Test score with', description, 'features:', s)
return m
>>> m1 = simple_logistic_classify(X_tr_bow, y_tr, X_te_bow, y_te, 'bow')
>>> m2 = simple_logistic_classify(X_tr_l2, y_tr, X_te_l2, y_te, 'l2-normalized')
>>> m3 = simple_logistic_classify(X_tr_tfidf, y_tr, X_te_tfidf, y_te, 'tf-idf')
Test score with bow features: 0.775873066497
Test score with l2-normalized features: 0.763514590974
Test score with tf-idf features: 0.743182905438
結果顯示準確率最高的分類器使用的是詞袋特徵,實際上, 出現這種情況的原因在於分類器沒有很好地“調優”
4.2.4 使用正則化對邏輯迴歸進行調優
-
scikit-learn 中的 GridSearchCV 函數可以執行帶交叉驗證的網格搜索
-
Step5:使用網格搜索對邏輯迴歸進行調優
>>> import sklearn.model_selection as modsel
# 確定一個搜索網格,然後對每種特徵集合執行5-折網格搜索
>>> param_grid_ = {'C': [1e-5, 1e-3, 1e-1, 1e0, 1e1, 1e2]}
# 爲詞袋錶示法進行分類器調優
>>> bow_search = modsel.GridSearchCV(LogisticRegression(), cv=5, ... param_grid=param_grid_)
>>> bow_search.fit(X_tr_bow, y_tr)
# 爲L2-歸一化詞向量進行分類器調優
>>> l2_search = modsel.GridSearchCV(LogisticRegression(), cv=5, ... param_grid=param_grid_)
>>> l2_search.fit(X_tr_l2, y_tr)
# 爲tf-idf進行分類器調優
>>> tfidf_search = modsel.GridSearchCV(LogisticRegression(), cv=5, ... param_grid=param_grid_)
>>> tfidf_search.fit(X_tr_tfidf, y_tr)
# 檢查網格搜索的一個輸出,看看它是如何運行的
>>> bow_search.cv_results_
{'mean_fit_time': array([ 0.43648252, 0.94630651,
5.64090128, 15.31248307, 31.47010217, 42.44257565]),
'mean_score_time': array([ 0.00080056, 0.00392466, 0.00864897, 0 .00784755,
0.01192751, 0.0072515 ]),
'mean_test_score': array([ 0.57897075, 0.7518111 , 0.78283898, 0.77381766,
0.75515992, 0.73937261]),
'mean_train_score': array([ 0.5792185 , 0.76731652, 0.87697341, 0.94629064,
0.98357195, 0.99441294]),
'param_C': masked_array(data = [1e-05 0.001 0.1 1.0 10.0 100.0],
mask = [False False False False False False],
fill_value = ?),
'params': ({'C': 1e-05},
{'C': 0.001},
{'C': 0.1},
{'C': 1.0},
{'C': 10.0},
{'C': 100.0}),
'rank_test_score': array([6, 4, 1, 2, 3, 5]),
'split0_test_score': array([ 0.58028698, 0.75025624, 0.7799795 , 0.7726341 ,
0.75247694, 0.74086095]),
'split0_train_score': array([ 0.57923964, 0.76860316, 0.87560871, 0.94434003,
0.9819308 , 0.99470312]),
'split1_test_score': array([ 0.5786776 , 0.74628396, 0.77669571, 0.76627371,
0 .74867589, 0.73176149]),
'split1_train_score': array([ 0.57917218, 0.7684849 , 0.87945837, 0.94822946,
0.98504976, 0.99538678]),
'split2_test_score': array([ 0.57816504, 0.75533914, 0.78472578, 0.76832394,
0.74799248, 0.7356911 ]),
'split2_train_score': array([ 0.57977019, 0.76613558, 0.87689548, 0.94566657,
0.98368288, 0.99397719]),
'split3_test_score': array([ 0.57894737, 0.75051265, 0.78332194, 0.77682843,
0.75768968, 0.73855092]),
'split3_train_score': array([ 0.57914745, 0.76678626, 0.87634546, 0.94558346,
0.98385443, 0.99474628]),
'split4_test_score': array([ 0.57877649, 0.75666439, 0.78947368, 0.78503076,
0.76896787, 0.75 ]),
'split4_train_score': array([ 0.57876303, 0.7665727 , 0.87655903, 0.94763369,
0.98334188, 0.99325132]),
'std_fit_time': array([ 0.03874582, 0.02297261, 1.18862097, 1.83901079,
4.21516797, 2.93444269]),
'std_score_time': array([ 0.00160112, 0.00605009, 0.00623053, 0.00698687,
0.00713112, 0.00570195]),
'std_test_score': array([ 0.00070799, 0.00375907, 0.00432957, 0.00668246,
0.00612049]),
'std_train_score': array([ 0.00032232, 0.00102466, 0.00131222, 0.00143229,
0.00100223, 0.00073252])}
# 在箱線圖中繪製出交叉驗證結果
# 對分類器性能進行可視化比較
>>> search_results = pd.DataFrame.from_dict({
...
...
...
...
'bow': bow_search.cv_results_['mean_test_score'], 'tfidf': tfidf_search.cv_results_['mean_test_score'], 'l2': l2_search.cv_results_['mean_test_score']
# 常用的matplotlib設置
# seaborn用來美化圖形
>>> import matplotlib.pyplot as plt >>> import seaborn as sns
>>> sns.set_style("whitegrid")
>>> ax = sns.boxplot(data=search_results, width=0.4)
>>> ax.set_ylabel('Accuracy', size=14)
>>> ax.tick_params(labelsize=14)
- Step6:比較不同特徵集合的最終訓練與測試步驟
# 使用前面找到的最優超參數設置,在整個訓練集上訓練一個最終模型
# 在測試集上測量準確度
>>> m1 = simple_logistic_classify(X_tr_bow, y_tr, X_te_bow, y_te, 'bow',
... _C=bow_search.best_params_['C'])
>>> m2 = simple_logistic_classify(X_tr_l2, y_tr, X_te_l2, y_te, 'l2-normalized', ... _C=l2_search.best_params_['C'])
>>> m3 = simple_logistic_classify(X_tr_tfidf, y_tr, X_te_tfidf, y_te, 'tf-idf', ... _C=tfidf_search.best_params_['C'])
Test score with bow features: 0.78360708021
Test score with l2-normalized features: 0.780178599904
Test score with tf-idf features: 0.788470738319
4.3 深入研究,發生了什麼
- tf-idf = 列縮放
- tf-idf 和 l2 歸一化都是數據矩陣上的列操作
- 正確的特徵縮放有助於分類問題。正確縮放可以突出有信息量 的單詞,並削弱普通單詞的影響。它還可以減少數據矩陣的條件數。正確的縮放不一定是 標準的列縮放。
參考:《精通特徵工程》愛麗絲·鄭·阿曼達·卡薩麗
面向機器學習的特徵工程學習筆記:
【精通特徵工程】學習筆記(一)
【精通特徵工程】學習筆記(二)