传统机器学习——特征工程之文本数据(特征缩放的效果:从词袋到tf-idf)(三)
前言
词袋表示法简单易行,但存在明显缺点:有些单词会被过分强调:
举例:
还是用这篇文本为例,希望使用的方法能够强调两个主角“Emma”和“raven”,这两个词都出现了3次,但“the”出现了8次,“and”出现了5次,“it”和“was”都出现了4次,显然,仅通过词频计数,无法突显出文本的主要用意。
基于以上问题,提出了一种能强调有意义的单词表示方法。
tf-idf:词袋的一种简单扩展
tf-idf表示词频-逆文档频率,计算的不是数据集中每个单词在每个文档中的原本计数,而是一个归一化的计数,其中每个单词的计数要除以这个单词出现在其中的文档数量。即:
**b(w,d) = 单词w在文档d中出现的次数
tf-idf(w,d) = bow(w,d) * N /(单词w在其中的文档数量) ###N是数据集中的文档总数**
关于上面公式中的逆文档数,有下面的一些定义和变形:
" N /(单词w在其中的文档数量)"被定义为**逆文档数**:
- 如果一个单词出现在很多文档中,那么他的逆文档数就接近1
- 如果一个单词只出现在少数几个文档中,那么他的逆文档数就会高很多
如果使用逆文档频率的对数变换,即:
**tf-idf(w,d) = biw(w,d) *log(N/单词w出现在其中的文档数量)**
作用:将一个几乎出现在所有单个文档中的单词的计数归零,而一个只出现在少数几个文档中的单词的计数将会被放大。
tf-idf方法测试
本文的标题“特征缩放的效果:从词袋到tf-idf”,怎样理解特征缩放?从上一节中tf-idf的公式可以看出,单词计数乘上了一个常数,这样就实现了特征的缩放。下面就结合着代码来学习使用tf-idf的方法,目的是能否通过点评数据区分一个商家是餐馆还是夜店。
1、加载并清理数据集
去除一些不必要的特征
import json
import pandas as pd
#加载Yelp商家数据
biz_f = open("yelp_academic_dataset_business.json")
biz_df = pd.DataFrame([json.load(x) for x in biz_f.readlines()])
biz_f.close()
#加载Yelp点评数据
review_file = open("yelp_academic_dataset_review.json")
review_df = pd.DataFrame([json.load(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"],aixs = 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
twobiz_reviews["target"] = twobiz_reviews.apply(lambda x:"Nightlife" in x["categories"],axis = 1)
2、创建分类数据集
因原Yelp数据集中餐馆和夜店个数比例相差较大,所以需要对优势类别(餐馆)进行下采样,使他的数量与劣势(夜店)基本相同:
流程:
1. 对夜店点评数据进行10%的随机抽样,对餐馆点评数据进行2.1%的随机抽样(选择这样的比例可以使两个类别的抽样数据基本相当)。
2. 按照70/30的比例将这个数据集划分为训练集和测试集。在这个例子中,训练集有29264条点评数据,测试集有12542条点评数据。
3. 训练数据包含46924个唯一单词,这就是词袋表示法的特征数量
代码:
#创建一个类别平衡的子样本
nightlift = twobiz_reviews[twobiz_reviews.apply(lambda x:"Nightlift" in x["categories"],axis = 1)]
restaurants = twobiz_reviews[twobiz_reviews.apply(lambda x:"Restaurants" in x["categories"],axis = 1)]
#对夜店和餐馆分别进行相对比例的采样
nightlift_subset = nightlift.sample(frac = 0.1,random_state = 123)
restaurant_subset = restaurants.sample(frac = 0.021,random_state = 123)
combined = pd.concat([nightlift_subset,restaurant_subset])
#划分训练集和测试集
import sklearn.model_selection as modsel
training_data, test_data = modsel.train_test_split(combined,train_size = 0.7,random_state = 123)
print(training_data.shape,test_data.shape)
结果:
(29264, 5) (12542, 5)
3、使用tf-idf变换来缩放词袋
比较一下词袋、tf-idf和归一化在线性分类问题中的效果。
#####在(一)中,使用scikit-learn 中的CountVectorizer 将点评文本转换为词袋。所有文本特征化方法都依赖于一个分词器,它是一个能将文本字符串转换为标记(单词)列表的程序模块。在这个例子中,scikit-learn 使用默认的分词模式,搜索由2 个或2 个以上的字母和数字组成的序列,标点符号被当作标记分隔符。
#用词袋表示点评文本
from sklearn.feature_extraction import text
bow_transform = text.CountVectorizer()
X_tr_bow = bow_transform.fit_transform(training_data["text"])
X_te_bow = bow_transform.transform(test_data["text"])
y_tr = training_data["target"]
y_te = test_data["target"]
#使用词袋矩阵创建tf-id表示
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归一化
import sklearn.preprocessing as preproc
X_tr_l2 = preproc.normalize(X_tr_bow, axis=0)
X_te_l2 = preproc.normalize(X_te_bow, axis=0)
4、使用逻辑回归进行分类
from sklearn.linear_model import LogisticRegression
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
分析:很明显,与预期结论不同,使用词袋的准确率反而更高。实际上,出现这种情况的原因在于分类器没有很好地“调优”,这是在比较分类器时经常犯的错误。
5、使用正则化对逻辑回归进行调优
要使用正则化,必须确定一个正则化参数。正则化参数是一种超参数,不能在模型训练过程中自动学习。相反,它们必须根据具体的问题进行调优,并提供给训练算法,这个过程就是超参数调优。
一种基本的超参数调优方法称为网格搜索:先确定一个超参数网格,然后使用调优程序自动搜索,找到网格中的最优超参数设置。找到最优超参数设置之后,你可以使用该设置在整个训练集上训练一个模型,然后使用它在测试集上的表现作为这类模型的最终评价。
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)
# 在箱线图中绘制出交叉验证结果
# 对分类器性能进行可视化比较
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)
打印的箱型图如下:
也可以统计出在每个超参数下,三种方法各自的准确率的平均值,以标的形式展示:
可以看出ℓ2 归一化特征的结果非常差。但不要被这张图蒙蔽,准确率如
此之低是因为使用了非常不合适的正则化参数设置。这个具体的例子说明了不合适的超参数设置可以导致得出非常错误的结论。
# 使用前面找到的最优超参数设置,在整个训练集上训练一个最终模型
# 在测试集上测量准确度
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-nor_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
恰当地调优可以提高所有特征集合的准确率,这三种特征集合经过正则化逻辑回归后,都得到了相似的分类准确率。