返回介绍

6.4 创建第一个分类器并调优

发布于 2024-01-30 22:34:09 字数 9801 浏览 0 评论 0 收藏 0

朴素贝叶斯分类器居于sklearn.naive_bayes 工具包之中。那里有不同种类的朴素贝叶斯分类器。

GaussianNB  它假设特征是正太分布的(Gaussian)。它的一个使用场景是,根据给定人物的高度和宽度,判定这个人的性别。而我们的例子,从给定推文文本中提取出词语的个数,很明显不是正太分布的。

MultinomialNB  它假设特征就是出现次数。这和我们是相关的,因为我们会把推文中的词频当做特征。在实践中,这个分类器对TF-IDF向量也处理得不错。

BernoulliNB  这和MultinomialNB 类似,但更适于判断词语是否出现了这种二值特征,而不是词频统计。

由于我们主要要看词语出现次数,所以MultinomialNB 最适合。

6.4.1 先解决一个简单问题

正如我们在推文数据中所看到的,推文的情感并不只有正面或负面。实际上大多数推文并不包含任何情感,它们是中性的或者无关的,例如一些原始信息(New book:Building Machine Learning … http://link )。这会产生4个类别。为避免任务过于复杂,我们只专注于正面和负面的推文:

>>> pos_neg_idx=np.logical_or(Y=="positive", Y=="negative") >>> X = X[pos_neg_idx] >>> Y = Y[pos_neg_idx] >>> Y = Y=="positive"

现在,我们有了原始推文文本(在X 中)和二分类结果(在Y 中);我们将0 赋予负例,将1 赋予正例。

正如在前一章中所学到的,我们可以创建TfidfVectorizer ,将原始推文文本转换为TF-IDF 特征值。我们把它们和标签放在一起,来训练第一个分类器。为方便起见,我们使用Pipeline 类。它允许我们将向量化处理器和分类器结合到一起,并提供相同的接口:

from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.pipeline import Pipeline def create_ngram_model(): tfidf_ngrams = TfidfVectorizer(ngram_range=(1, 3), analyzer="word", binary=False) clf = MultinomialNB() pipeline = Pipeline([('vect', tfidf_ngrams), ('clf', clf)]) return pipeline

create_ngram_model() 函数返回的Pipeline 实例可以用于fit() 和predict() ,就像我们有一个正常的分类器。

由于并没有太多的数据,所以我们要进行交叉验证。然而在这个时候,我们并不使用KFold ,因为它会把数据切分成连续的几折。相反,我们使用ShuffleSplit 。它会将数据打散,但并不能保证相同的数据样本不会出现在多个数据折中。对于每一折数据,我们会跟踪准确-召回曲线下面的面积,以及正确率。

为了使我们的实验进程保持敏捷,让我们把所有东西都打包在一起,放在train_model() 函数中。它会把创建分类器的函数当做参数传入:

from sklearn.metrics import precision_recall_curve, auc from sklearn.cross_validation import ShuffleSplit def train_model(clf_factory, X, Y): # 设置随机状态来得到确定性的行为 cv = ShuffleSplit(n=len(X), n_iter=10, test_size=0.3, indices=True, random_state=0) scores = [] pr_scores = [] for train, test in cv: X_train, y_train = X[train], Y[train] X_test, y_test = X[test], Y[test] clf = clf_factory() clf.fit(X_train, y_train) train_score = clf.score(X_train, y_train) test_score = clf.score(X_test, y_test) scores.append(test_score) proba = clf.predict_proba(X_test) precision, recall, pr_thresholds = precision_recall_curve(y_test, proba[:,1]) pr_scores.append(auc(recall, precision)) summary = (np.mean(scores), np.std(scores), np.mean(pr_scores), np.std(pr_scores)) print "%.3f\t%.3f\t%.3f\t%.3f"%summary >>> X, Y = load_sanders_data() >>> pos_neg_idx=np.logical_or(Y=="positive", Y=="negative") >>> X = X[pos_neg_idx] >>> Y = Y[pos_neg_idx] >>> Y = Y=="positive" >>> train_model(create_ngram_model) 0.805 0.024 0.878 0.016

当我们第一次尝试在向量化的TF-IDF 三元组特征上使用朴素贝叶斯方法的时候,我们得到了80.5%的正确率,以及87.8%的P/R AUC。下图显示了P/R图表,它比前一章中看到的结果更加令人鼓舞。

这是第一次,结果振奋人心。当我们意识到在情感分类任务中100%的正确率可能永远也无法达到的时候,这些结果更加令人印象深刻。对一些推文,我们人类甚至也经常无法对分类的标签达成一致。

6.4.2 使用所有的类

但是,我们再一次简化了任务,虽然只简化了一点,只使用了正/负情感的推文。这意味着我们假设有一个完美的分类器,可以预先对推文中是否包含某种情感进行区分,并把结果传给我们的朴素贝叶斯分类器。

所以,如果我们对推文中是否包含情感也进行分类,那么效果会如何呢?要将此事弄个水落石出,我们先写了一个便捷的函数,用它返回修正后的类别数组。这个数组包含了一个情感列表,我们把它们看作正例。

def tweak_labels(Y, pos_sent_list): pos = Y==pos_sent_list[0] for sent_label in pos_sent_list[1:]: pos |= Y==sent_label Y = np.zeros(Y.shape[0]) Y[pos] = 1 Y = Y.astype(int) return Y

注意,现在我们谈论的是两种不同的正例。一个推文的情感可以是正面的,这可以从训练数据的类别里把它区分出来。如果,例如,我们想要弄清楚从中性推文中区分出带有情感的推文的效果是如何的,那么可以按照下列方法进行:

>>> Y = tweak_labels(Y, ["positive", "negative"])

在Y 中,1(正类别)表示所有包含正面或负面情感的推文,0(负类别)代表中性或者无关的推文。

>>> train_model(create_ngram_model, X, Y, plot=True)
0.767 0.014 0.670 0.022

正如预期的那样,P/R AUC下降得非常多,现在只有67%。正确率仍然很高,但这只是因为我们的数据集非常不平衡。在总共3642个推文中,只有1017个包含正面或负面情感,大约占全部推文的28%。这意味着如果我们创建一个分类器,总把推文分到不包含任何情感的类别中去,那么就已经得到了72%的正确率。这就是为何在训练和测试数据不均衡的情况下要查看准确率和召回率的又一例证。

那么,用朴素贝叶斯分类器对“正面情感的推文 vs. 余下的推文”,或者“负面情感的推文 vs. 余下的推文”进行分类,效果又如何呢?一个字:差。

== Pos vs. rest == 0.866 0.010 0.327 0.017 == Neg vs. rest == 0.861 0.010 0.560 0.020

如果你问我,我会告诉你这个结果非常不可用。看看下图中的P/R曲线,我们还会发现甚至连有用的准确率/召回率折中都没有,这并不像我们在前一章中做到的那样。

6.4.3 对分类器的参数进行调优

当然,我们还没有对当前的实验设置进行足够的探索。而这是应该多研究一下的。大致有两个应当实验一下的地方:TfidfVectorizer 和MultinomialNB 。由于我们还不太清楚具体应该探索哪里,所以让我们先把它们的参数值分一下类。

TfidfVectorizer

使用不同的NGrams设置:一元组(1,1)、二元组(1,2)和三元组(1,3)。

采用min_df :1 或者2 。

探索IDF的影响,在TF-IDF 中使用user_idf 和smooth_idf :False 和True 。

是否删除停用词,通过设置stop_words 为English 或None 。

是否对词频取对数(sublinear_tf )。

通过设置binary 为True 或False ,来试验是否要追踪词语出现次数或者只是简单记录词语出现与否。

MultinomialNB

通过设置alpha值,决定使用下面哪种平滑方法。

加1或拉普拉斯平滑:1。

Lidstone平滑:0.01、0.05、0.1或0.5。

不使用平滑:0。

有一个简单的方法是,对所有这些有意义的取值都训练一个分类器,同时保持其他参数不变,然后查看分类器的效果。由于我们并不知道这些参数是否互相影响,所以要做得正确,就需要我们对每个可能的参数组合都训练分类器。很明显,这样做太过乏味冗长了。

因为这类参数搜索在机器学习任务中经常发生,所以scikit-learn里面有一个专门的类处理它,叫做GridSearchCV 。它使用一个估算器(一个接口跟分类器一样的实例),在我们这里是一个管道实例,和一个包含所有可能值的参数字典。

GridSearchCV 要求字典的键遵守特定的格式,使得能够对正确的估算器设置参数。这个格式如下所示:

<estimator>__<subestimator>__...__<param_name>

现在,如果我们要指定TfidfVectorizer (在Pipline 描述中叫做vect )中min_df 参数的探索预期值,我们要说:

Param_grid={"vect__ngram_range"=[(1, 1), (1, 2), (1, 3)]}

这是说,将一元组(unigrams)、二元组(bigrams)和三元组(trigrams)作为TfidfVectorizer 中ngram_range 参数的参数值,让GridSeachCV 去尝试。

然后,用所有可能的参数/值组合来训练估算器。最后,通过成员变量best_estimator_ 获得最优的估算器。

由于我们要拿返回的最优分类器和当前的最优分类器做比较,我们就需要使用同样的方式进行评估。因此,我们可以在CV 参数中(这就是CV 出现在GridSearchCV 里的原因)把ShuffleSplit 实例传递进去。

这里唯一缺少的东西就是定义GridSearchCV 该如何选择最优评估算器。这可以通过为score_func 参数提供一个目标评分函数(令人感到意外!)来达到。我们可以自己写一个,也可以从sklearn.metrics 包中找一个。当然,我们不能使用metric.accuracy ,因为我们的样本类别是不均衡的(包含情感的推文比中性的推文少得多)。相反,我们希望在两个类别上都得到很好的准确率和召回率:包含情感的推文和没有正面或负面意见的推文。一个将准确率和召回率结合起来的评估标准叫做F-measure 标准。它由metrics.f1_score 实现:

把所有东西放在一起,就得到了如下代码:

from sklearn.grid_search import GridSearchCV from sklearn.metrics import f1_score def grid_search_model(clf_factory, X, Y): cv = ShuffleSplit( n=len(X), n_iter=10, test_size=0.3, indices=True, random_state=0) param_grid = dict(vect__ngram_range=[(1, 1), (1, 2), (1, 3)], vect__min_df=[1, 2], vect__stop_words=[None, "english"], vect__smooth_idf=[False, True], vect__use_idf=[False, True], vect__sublinear_tf=[False, True], vect__binary=[False, True], clf__alpha=[0, 0.01, 0.05, 0.1, 0.5, 1], ) grid_search = GridSearchCV(clf_factory(), param_grid=param_grid, cv=cv, score_func=f1_score, verbose=10) grid_search.fit(X, Y) return grid_search.best_estimator_

在执行下列代码的时候我们需要有一点耐心:

clf = grid_search_model(create_ngram_model, X, Y)
print clf

这是因为我们是在3∙2∙2∙2∙2∙2∙2∙6=1152的参数组合中进行参数搜索——每一个都要在10折数据上进行训练:

... waiting some hours ... Pipeline(clf=MultinomialNB( alpha=0.01, class_weight=None, fit_prior=True), clf__alpha=0.01, clf__class_weight=None, clf__fit_prior=True, vect=TfidfVectorizer( analyzer=word, binary=False, charset=utf-8, charset_error=strict, dtype=<type 'long'>, input=content, lowercase=True, max_df=1.0, max_features=None, max_n=None, min_df=1, min_n=None, ngram_range=(1, 2), norm=l2, preprocessor=None, smooth_idf=False, stop_words=None,strip_accents=None, sublinear_tf=True, token_pattern=(?u)\b\w\w+\b, token_processor=None, tokenizer=None, use_idf=False, vocabulary=None), vect__analyzer=word, vect__binary=False, vect__charset=utf-8, vect__charset_error=strict, vect__dtype=, vect__input=content, vect__lowercase=True, vect__max_df=1.0, vect__max_features=None, vect__max_n=None, vect__min_df=1, vect__min_n=None, vect__ngram_range=(1, 2), vect__norm=l2, vect__preprocessor=None, vect__smooth_idf=False, vect__stop_words=None, vect__strip_accents=None, vect__sublinear_tf=True, vect__token_pattern=(?u)\b\w\w+\b, vect__token_processor=None, vect__tokenizer=None, vect__use_idf=False, vect__vocabulary=None) 0.795 0.007 0.702 0.028

采用之前的设置,最优估算器确实将P/R AUC提升了将近3.3%,达到70.2%。

如果我们用刚发现的参数来配置向量化处理器和分类器,那么“正面情感的推文 vs. 余下的推文”和“负面情感的推文 vs. 余下的推文”的结果将会提升:

== Pos vs. rest == 0.883 0.005 0.520 0.028 == Neg vs. rest == 0.888 0.009 0.631 0.031

确实,P/R曲线看起来好了很多(注意这些图来自于中间的某一折分类器,所以AUC值有一些微小的偏离):

然而,我们可能依然不会使用这些分类器。是时候尝试一些完全不同的东西了!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文