5.4 创建第一个分类器
让我们从前一章中简洁而美观的最邻近方法开始。尽管它看上去不像其他方法那样高端,却颇有威力。因为它不是基于模型的方法,所以几乎可以学习任何数据。然而,这种优美性也带来了一个明显的缺点,我们一会儿就会发现。
5.4.1 从k邻近(kNN)算法开始
这一次,我们并不想自己来实现它,而是使用sklearn 工具来实现。这个分类器在sklearn.neighbors 里。让我们从一个简单的2最邻近分类器(2-nearest neighbor classifier)开始:
>>> from sklearn import neighbors >>> knn = neighbors.KNeighborsClassifier(n_neighbors=2) >>> print(knn) KNeighborsClassifier(algorithm=auto, leaf_size=30, n_neighbors=2, p=2, warn_on_equidistant=True, weights=uniform)
它提供了一个和sklearn 中其他分类器一样的接口。我们用fit() 进行训练,然后用predict() 预测新数据的类别。
>>> knn.fit([[1],[2],[3],[4],[5],[6]], [0,0,0,1,1,1]) >>> knn.predict(1.5) array([0]) >>> knn.predict(37) array([1]) >>> knn.predict(3) NeighborsWarning: kneighbors: neighbor k+1 and neighbor k have the same distance: results will be dependent on data order. neigh_dist, neigh_ind = self.kneighbors(X) array([0])
我们可以用predict_proba() 得到类别的概率。在这个例子中,我们有两个类0和1。它会返回一个含有两个元素的数组,如下列代码所示:
>>> knn.predict_proba(1.5) array([[ 1., 0.]]) >>> knn.predict_proba(37) array([[ 0., 1.]]) >>> knn.predict_proba(3.5) array([[ 0.5, 0.5]])
5.4.2 特征工程
那么,我们可以给分类器提供什么样的特征呢?什么样的特征最具有区分性呢?
TimeToAnswer 属性已经出现在我们的meta 字典里了,但它自己可能并不能提供太多价值。这里只有Text ,但它是原始形式的,我们无法把它传递给分类器,因为分类器需要的特征必须是数值形式的。所以我们必须先进行特征抽取这种琐碎的活儿。
我们能做的是查看答案中的HTML链接数,并用它代表答案的质量。假设答案中的超链越多意味着答案越好,越有可能被采纳为最佳答案。当然我们希望只考虑正常文本中的链接,而不是示例代码中这样的:
import re code_match = re.compile('<pre>(.*?)</pre>', re.MULTILINE|re.DOTALL) link_match = re.compile('<a href="http://.*?".*?>(.*?)</a>', re.MULTILINE|re.DOTALL) def extract_features_from_body(s): link_count_in_code = 0 # 统计代码中的链接,后续会提取它们 for match_str in code_match.findall(s): link_count_in_code += len(link_match.findall(match_str)) return len(link_match.findall(s)) – link_count_in_code
提示 对于一个生产式系统来说,我们不应该用正则表达式来解析HTML内容。相反,我们应该依赖优秀的工具库,如BeautifulSoup。我们可以放心地用它来处理那些经常在HTML中出现的各种奇怪情况,而且效果非凡。
在这些准备工作就绪之后,我们就可以为每个答案都生成一个特征。但在训练分类器之前,先看一下我们要进行训练的数据。我们可以对这个新特征的频率分布有一个初始印象。这个可以通过画出每个取值在数据中出现频率的百分比来得到。如下图所示:
由于多数帖子根本没有任何链接,所以我们知道,这个特征本身并不能生成一个很好的分类器。尽管如此,让我们先尝试一下,以便对目前所处的位置有一个初步的估计。
5.4.3 训练分类器
我们需要把特征数组以及之前定义的Y 标签传进kNN学习器,来得到一个分类器。
X = np.asarray([extract_features_from_body(text) for post_id, text in fetch_posts() if post_id in all_answers]) knn = neighbors.KNeighborsClassifier() knn.fit(X, Y)
我们采用标准参数对数据拟合出了一个5NN(意思是k =5的最邻近模型)。为什么是5NN呢?实际上,以我们现在对数据的了解,并不知道正确的k 是多少。我们一旦对此有更多的认识,那么将会有更好的办法来设置k 值。
5.4.4 评估分类器的性能
我们必须清楚我们要评估的是什么。一个原始但最容易的方法就是简单地计算测试集上的平均预测质量。然后将会得到一个0到1之间的值。0表示错误预测,1表示完美预测。正确率可以通过knn.score() 得到。
但是正如前一章所学到的,我们不会只做一次,而是要使用sklearn.cross_validation 里现成的KFold类进行交叉验证。最后,我们把每一折测试集上的分数平均一下,用标准差来评估它的偏离程度。参考下列代码:
from sklearn.cross_validation import KFold scores = [] cv = KFold(n=len(X), k=10, indices=True) for train, test in cv: X_train, y_train = X[train], Y[train] X_test, y_test = X[test], Y[test] clf = neighbors.KNeighborsClassifier() clf.fit(X, Y) scores.append(clf.score(X_test, y_test)) print("Mean(scores)=%.5f\tStddev(scores)=%.5f"%(np.mean(scores, np.std(scores)))
输出如下:
Mean(scores)=0.49100 Stddev(scores)=0.02888
这还远不可用。由于只有49%的正确率,还不如抛硬币的效果。很明显,帖子中的链接数并不是一个能很好地反映帖子质量的指标。我们说,这个特征并不具有很大的区分性——至少,对于k =5的kNN不具有。
5.4.5 设计更多的特征
除了用超链接数代表帖子质量之外,使用代码行数也可能是一个比较好的选择。至少它预示着帖子的作者对解答这个问题很感兴趣。我们可以找到嵌在<pre>…</pre> 标签中的代码。一旦把它们提取出来,我们就应该在统计帖子词语数目的时候忽略掉所有有代码的行:
def extract_features_from_body(s): num_code_lines = 0 link_count_in_code = 0 code_free_s = s # 删除源代码,并统计有多少行 for match_str in code_match.findall(s): num_code_lines += match_str.count('\n') code_free_s = code_match.sub("", code_free_s) # 有时源代码中包含链接, # 我们并不需要统计它们 link_count_in_code += len(link_match.findall(match_str)) links = link_match.findall(s) link_count = len(links) link_count -= link_count_in_code html_free_s = re.sub(" +", " ", tag_match.sub('', code_free_s)).replace("\n", "") link_free_s = html_free_s # 在统计词语之前从文本中删除链接 for anchor in anchors: if anchor.lower().startswith("http://"): link_free_s = link_free_s.replace(anchor,'') num_text_tokens = html_free_s.count(" ") return num_text_tokens, num_code_lines, link_count
看看下面这几个图,我们注意到一个帖子中的词语数目有很大的变化:
在更大的特征空间中进行训练,可以使正确率提高不少。
Mean(scores)=0.58300 Stddev(scores)=0.02216
但是这仍然意味着在我们的分类结果中,10个答案中大约有4个是错误的。不过至少我们在朝着正确的方向前进。更多的特征会带来更高的正确率。这指引我们去增加更多的特征。因此,让我们拓展一下特征空间,引入更多的特征。
AvgSentLen 这个特征衡量了句子中的平均词语个数。也许存在这么一种模式:非常好的帖子并不会用很长的句子让读者的大脑超负荷运转。
AvgWordLen 这个特征和AvgSentLen 类似;它衡量了帖子中词语的平均字符个数。
NumAllCaps 这个特征衡量了大写形式的词语个数。但这并不是好的字体样式。
NumExclams 这个特征衡量了感叹号的个数。
下列图表中显示了句子和词语的平均长度,以及大写词语和感叹号个数的数值分布:
加上这4个特征,现在有7个特征来表示每个帖子。来看看我们的进展:
Mean(scores)=0.57650 Stddev(scores)=0.03557
这很有趣。我们增加了4个特征之后却得到了更糟糕的分类正确率。怎么会这样?
要理解这个,我们需要明白kNN是如何工作的。我们的5NN分类器通过计算前面描述的这7个特征(包括LinkCount 、NumTextTokens 、NumCodeLines 、AvgSentLen 、AvgWordLen 、NumAllCaps 和NumExclams )来确定每个新帖子的类别,然后找到5个距离最近的帖子。新帖子的类别就是在这些距离最近的帖子中出现次数最多的类别。由于我们没有详细说明,在初始化时,分类器的闵科夫斯基(Minkowski)距离参数的默认值是p=2 。这意味着所有这7个特征都被同等对待。然而kNN并不会知道,例如NumTextTokens 这个特征,虽然有益处,但远远不如NumLinks 重要。让我们考虑如下两个帖子A和B。它们只在下面这些特征上有区别。我们看看它们是如何跟一个新帖子做比较的:
帖子 | NumLinks | NumTextTokens |
A | 2 | 20 |
B | 0 | 25 |
新帖子 | 1 | 23 |
尽管我们觉得链接比纯粹的文本提供了更多的价值,但和帖子A相比,帖子B与新帖子更相似。
很明显,kNN难以正确利用现有数据。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论