1 #!/usr/bin/env python 2 #!-*- coding:utf-8 -*- 3 #!@Author:sjl 4 5 import numpy as np 6 from sklearn.naive_bayes import MultinomialNB, BernoulliNB 7 from sklearn.datasets import fetch_20newsgroups 8 from sklearn.feature_extraction.text import TfidfVectorizer 9 from sklearn.linear_model import RidgeClassifier 10 from sklearn.neighbors import KNeighborsClassifier 11 from sklearn.svm import SVC 12 from sklearn.ensemble import RandomForestClassifier 13 from sklearn.model_selection import GridSearchCV 14 from sklearn import metrics 15 from time import time 16 from pprint import pprint 17 import matplotlib.pyplot as plt 18 import matplotlib as mpl 19 20 21 def test_clf(clf): 22 print(u'分类器:', clf) 23 alpha_can = np.logspace(-3, 2, 10) 24 model = GridSearchCV(clf, param_grid={'alpha': alpha_can}, cv=5) 25 m = alpha_can.size 26 if hasattr(clf, 'alpha'): 27 model.set_params(param_grid={'alpha': alpha_can}) 28 m = alpha_can.size 29 if hasattr(clf, 'n_neighbors'): 30 neighbors_can = np.arange(1, 15) 31 model.set_params(param_grid={'n_neighbors': neighbors_can}) 32 m = neighbors_can.size 33 if hasattr(clf, 'C'): 34 C_can = np.logspace(1, 3, 3) 35 gamma_can = np.logspace(-3, 0, 3) 36 model.set_params(param_grid={'C':C_can, 'gamma':gamma_can}) 37 m = C_can.size * gamma_can.size 38 if hasattr(clf, 'max_depth'): 39 max_depth_can = np.arange(4, 10) 40 model.set_params(param_grid={'max_depth': max_depth_can}) 41 m = max_depth_can.size 42 t_start = time() 43 model.fit(x_train, y_train) 44 t_end = time() 45 t_train = (t_end - t_start) / (5*m) 46 print(u'5折交叉验证的训练时间为:%.3f秒/(5*%d)=%.3f秒' % ((t_end - t_start), m, t_train)) 47 print(u'最优超参数为:', model.best_params_) 48 t_start = time() 49 y_hat = model.predict(x_test) 50 t_end = time() 51 t_test = t_end - t_start 52 print(u'测试时间:%.3f秒' % t_test) 53 acc = metrics.accuracy_score(y_test, y_hat) 54 print(u'测试集准确率:%.2f%%' % (100 * acc)) 55 name = str(clf).split('(')[0] 56 print(name) 57 index = name.find('Classifier') 58 if index != -1: 59 name = name[:index] # 去掉末尾的Classifier 60 if name == 'SVC': 61 name = 'SVM' 62 return t_train, t_test, 1-acc, name 63 64 65 if __name__ == "__main__": 66 print(u'开始下载/加载数据...') 67 t_start = time() 68 # remove = ('headers', 'footers', 'quotes') 69 remove = () 70 categories = ('alt.atheism', 'talk.religion.misc', 'comp.graphics', 'sci.space') 71 # categories = None # 若分类所有类别,请注意内存是否够用 72 data_train = fetch_20newsgroups(subset='train', categories=categories, shuffle=True, random_state=0, remove=remove) 73 data_test = fetch_20newsgroups(subset='test', categories=categories, shuffle=True, random_state=0, remove=remove) 74 t_end = time() 75 print(u'下载/加载数据完成,耗时%.3f秒' % (t_end - t_start)) 76 print(u'数据类型:', type(data_train)) 77 print(u'训练集包含的文本数目:', len(data_train.data)) 78 print(u'测试集包含的文本数目:', len(data_test.data)) 79 print(u'训练集和测试集使用的%d个类别的名称:' % len(categories)) 80 categories = data_train.target_names 81 pprint(categories) 82 y_train = data_train.target 83 y_test = data_test.target 84 print(u' -- 前10个文本 -- ') 85 for i in np.arange(10): 86 print(u'文本%d(属于类别 - %s):' % (i+1, categories[y_train[i]])) 87 print(data_train.data[i]) 88 print(' ') 89 vectorizer = TfidfVectorizer(input='content', stop_words='english', max_df=0.5, sublinear_tf=True) 90 x_train = vectorizer.fit_transform(data_train.data) # x_train是稀疏的,scipy.sparse.csr.csr_matrix 91 x_test = vectorizer.transform(data_test.data) 92 print(u'训练集样本个数:%d,特征个数:%d' % x_train.shape) 93 print(u'停止词: ',) 94 pprint(vectorizer.get_stop_words()) 95 feature_names = np.asarray(vectorizer.get_feature_names()) 96 97 print(u' =================== 分类器的比较: ') 98 clfs = (MultinomialNB(), # 0.87(0.017), 0.002, 90.39% 99 BernoulliNB(), # 1.592(0.032), 0.010, 88.54% 100 KNeighborsClassifier(), # 19.737(0.282), 0.208, 86.03% 101 RidgeClassifier(), # 25.6(0.512), 0.003, 89.73% 102 RandomForestClassifier(n_estimators=200), # 59.319(1.977), 0.248, 77.01% 103 SVC() # 236.59(5.258), 1.574, 90.10% 104 ) 105 result = [] 106 for clf in clfs: 107 a = test_clf(clf) 108 result.append(a) 109 print(' ') 110 result = np.array(result) 111 time_train, time_test, err, names = result.T 112 x = np.arange(len(time_train)) 113 mpl.rcParams['font.sans-serif'] = [u'simHei'] 114 mpl.rcParams['axes.unicode_minus'] = False 115 plt.figure(figsize=(10, 7), facecolor='w') 116 ax = plt.axes() 117 b1 = ax.bar(x, err, width=0.25, color='#77E0A0') 118 ax_t = ax.twinx() 119 b2 = ax_t.bar(x+0.25, time_train, width=0.25, color='#FFA0A0') 120 b3 = ax_t.bar(x+0.5, time_test, width=0.25, color='#FF8080') 121 plt.xticks(x+0.5, names, fontsize=10) 122 leg = plt.legend([b1[0], b2[0], b3[0]], (u'错误率', u'训练时间', u'测试时间'), loc='upper left', shadow=True) 123 # for lt in leg.get_texts(): 124 # lt.set_fontsize(14) 125 plt.title(u'新闻组文本数据不同分类器间的比较', fontsize=18) 126 plt.xlabel(u'分类器名称') 127 plt.grid(True) 128 plt.tight_layout(2) 129 plt.show()
这个案例是一个新闻标题分类的案例,NLPCC 2017 Shared Task也有一个类似的案例。因此我们先拿这个下手了。整个过程概括起来分为以下几步:
- 数据采集
- 特征提取
- 模型训练
- 模型评估
接下来我们对这4个部分的代码进行详细的讲解。
2.1 数据采集
从上面的代码中,我们可以看到获取数据很简单:
1 print(u'开始下载/加载数据...') 2 t_start = time() 3 # remove = ('headers', 'footers', 'quotes') 4 remove = () 5 categories = ('alt.atheism', 'talk.religion.misc', 'comp.graphics', 'sci.space') 6 # categories = None # 若分类所有类别,请注意内存是否够用 7 data_train = fetch_20newsgroups(subset='train', categories=categories, shuffle=True, random_state=0, remove=remove) 8 data_test = fetch_20newsgroups(subset='test', categories=categories, shuffle=True, random_state=0, remove=remove) 9 t_end = time() 10 print(u'下载/加载数据完成,耗时%.3f秒' % (t_end - t_start)) 11 print(u'数据类型:', type(data_train)) 12 print(u'训练集包含的文本数目:', len(data_train.data)) 13 print(u'测试集包含的文本数目:', len(data_test.data)) 14 print(u'训练集和测试集使用的%d个类别的名称:' % len(categories)) 15 categories = data_train.target_names 16 pprint(categories) 17 y_train = data_train.target 18 y_test = data_test.target 19 print(u' -- 前10个文本 -- ') 20 for i in np.arange(10): 21 print(u'文本%d(属于类别 - %s):' % (i+1, categories[y_train[i]])) 22 print(data_train.data[i]) 23 print(' ') 24 vectorizer = TfidfVectorizer(input='content', stop_words='english', max_df=0.5, sublinear_tf=True)
这里最重要的就是这个fetch_20newsgroups
方法了,下面我们来详细讲解:
##函数原型是这样的。 ''' fetch_20newsgroups(data_home=None,subset='train',categories=None,shuffle=True,random_state=42,remove=(),download_if_missing=True) ''' ''' data_home指的是数据集的地址,如果默认的话,所有的数据都会在'~/scikit_learn_data'文件夹下. subset就是train,test,all三种可选,分别对应训练集、测试集和所有样本。 categories:是指类别,如果指定类别,就会只提取出目标类,如果是默认,则是提取所有类别出来。 shuffle:是否打乱样本顺序,如果是相互独立的话。 random_state:打乱顺序的随机种子 remove:是一个元组,用来去除一些停用词的,例如标题引用之类的。 download_if_missing: 如果数据缺失,是否去下载。 '''
另外的一种解释是:
fetch_20newsgroups的作用是加载文件名,加载20个新闻群组数据集中的数据
参数:data_home:可选参数,默认值为:None
指定一个电脑中的路径来存储加载的数据。如果选择默认,那所有的scikit-learn数据都存储在'~/scikit_learn_data'这个子文件夹中
subset:'train'或者'test','all',可选参数
选择加载得到的数据集用来做训练还是做测试,或者是两者都选择,可以随用户需要来选择
categories:空集,或者是字符串集合,或者是unicode码
shuffle:bool布尔类型,可选参数
是否需要打乱数据:这一参数对于一些需要让假设样本数据具有独立同分布的模型来说至关重要,如随机梯度下降
random_state:numpy随机数产生器,或者是种子整数
主要是用来清洗数据
remove:元组
包含头文件(‘headers’,‘footers’,'‘quotes’)的所有子集。都是从新闻群组帖子中被检测或者是移除的各种各样的文本,防止分类器在利用复杂数据特征属性进行分类过程中过拟合
'headers'去除新闻的头部数据, 'footers'去除新闻位置最后类似于签名区域的一整块区域,'quotes'移除引用其他新闻帖子的行
'headers'遵从一个精确的标准;其他的过滤器不一定一直正确
download_if_missing:可选参数,默认值是:真(True)
如果是Flase, 数据不是本地可获取的就会引起一个IOError,而不是尝试着从资源网站下载。
2.2 特征提取
数据采集完成以后,就要开始提取特征了,我们这里使用的是TFIDF特征。
1 vectorizer = TfidfVectorizer(input='content', stop_words='english', max_df=0.5, sublinear_tf=True) 2 x_train = vectorizer.fit_transform(data_train.data) # x_train是稀疏的,scipy.sparse.csr.csr_matrix 3 x_test = vectorizer.transform(data_test.data) 4 print(u'训练集样本个数:%d,特征个数:%d' % x_train.shape) 5 print(u'停止词: ',) 6 pprint(vectorizer.get_stop_words()) 7 feature_names = np.asarray(vectorizer.get_feature_names())
这里最重要的是TfidfVectorizer函数,其解释如下:
关于参数:
input:string{'filename', 'file', 'content'}
如果是'filename',序列作为参数传递给拟合器,预计为文件名列表,这需要读取原始内容进行分析
如果是'file',序列项目必须有一个”read“的方法(类似文件的对象),被调用作为获取内存中的字节数
否则,输入预计为序列串,或字节数据项都预计可直接进行分析。
encoding:string, ‘utf-8’by default
如果给出要解析的字节或文件,此编码将用于解码
decode_error: {'strict', 'ignore', 'replace'}
如果一个给出的字节序列包含的字符不是给定的编码,指示应该如何去做。默认情况下,它是'strict',这意味着的UnicodeDecodeError将提高,其他值是'ignore'和'replace'
strip_accents: {'ascii', 'unicode', None}
在预处理步骤中去除编码规则(accents),”ASCII码“是一种快速的方法,仅适用于有一个直接的ASCII字符映射,"unicode"是一个稍慢一些的方法,None(默认)什么都不做
analyzer:string,{'word', 'char'} or callable
定义特征为词(word)或n-gram字符,如果传递给它的调用被用于抽取未处理输入源文件的特征序列
preprocessor:callable or None(default)
当保留令牌和”n-gram“生成步骤时,覆盖预处理(字符串变换)的阶段
tokenizer:callable or None(default)
当保留预处理和n-gram生成步骤时,覆盖字符串令牌步骤
ngram_range: tuple(min_n, max_n)
要提取的n-gram的n-values的下限和上限范围,在min_n <= n <= max_n区间的n的全部值
stop_words:string {'english'}, list, or None(default)
如果未english,用于英语内建的停用词列表
如果未list,该列表被假定为包含停用词,列表中的所有词都将从令牌中删除
如果None,不使用停用词。max_df可以被设置为范围[0.7, 1.0)的值,基于内部预料词频来自动检测和过滤停用词
lowercase:boolean, default True
在令牌标记前转换所有的字符为小写
token_pattern:string
正则表达式显示了”token“的构成,仅当analyzer == ‘word’时才被使用。两个或多个字母数字字符的正则表达式(标点符号完全被忽略,始终被视为一个标记分隔符)。
max_df: float in range [0.0, 1.0] or int, optional, 1.0 by default
当构建词汇表时,严格忽略高于给出阈值的文档频率的词条,语料指定的停用词。如果是浮点值,该参数代表文档的比例,整型绝对计数值,如果词汇表不为None,此参数被忽略。
min_df:float in range [0.0, 1.0] or int, optional, 1.0 by default
当构建词汇表时,严格忽略低于给出阈值的文档频率的词条,语料指定的停用词。如果是浮点值,该参数代表文档的比例,整型绝对计数值,如果词汇表不为None,此参数被忽略。
max_features: optional, None by default
如果不为None,构建一个词汇表,仅考虑max_features--按语料词频排序,如果词汇表不为None,这个参数被忽略
vocabulary:Mapping or iterable, optional
也是一个映射(Map)(例如,字典),其中键是词条而值是在特征矩阵中索引,或词条中的迭代器。如果没有给出,词汇表被确定来自输入文件。在映射中索引不能有重复,并且不能在0到最大索引值之间有间断。
binary:boolean, False by default
如果未True,所有非零计数被设置为1,这对于离散概率模型是有用的,建立二元事件模型,而不是整型计数
dtype:type, optional
通过fit_transform()或transform()返回矩阵的类型
norm:'l1', 'l2', or None,optional
范数用于标准化词条向量。None为不归一化
use_idf:boolean, optional
启动inverse-document-frequency重新计算权重
smooth_idf:boolean,optional
通过加1到文档频率平滑idf权重,为防止除零,加入一个额外的文档
sublinear_tf:boolean, optional
应用线性缩放TF,例如,使用1+log(tf)覆盖tf
2.3 模型训练
这里选用多种模型进行训练
1 clfs = (MultinomialNB(), # 0.87(0.017), 0.002, 90.39% 2 BernoulliNB(), # 1.592(0.032), 0.010, 88.54% 3 KNeighborsClassifier(), # 19.737(0.282), 0.208, 86.03% 4 RidgeClassifier(), # 25.6(0.512), 0.003, 89.73% 5 RandomForestClassifier(n_estimators=200), # 59.319(1.977), 0.248, 77.01% 6 SVC() # 236.59(5.258), 1.574, 90.10% 7 ) 8 result = [] 9 for clf in clfs: 10 a = test_clf(clf) 11 result.append(a) 12 print(' ') 13 result = np.array(result) 14 time_train, time_test, err, names = result.T 15 x = np.arange(len(time_train))
1 def test_clf(clf): 2 print(u'分类器:', clf) 3 alpha_can = np.logspace(-3, 2, 10) 4 model = GridSearchCV(clf, param_grid={'alpha': alpha_can}, cv=5) 5 m = alpha_can.size 6 if hasattr(clf, 'alpha'): 7 model.set_params(param_grid={'alpha': alpha_can}) 8 m = alpha_can.size 9 if hasattr(clf, 'n_neighbors'): 10 neighbors_can = np.arange(1, 15) 11 model.set_params(param_grid={'n_neighbors': neighbors_can}) 12 m = neighbors_can.size 13 if hasattr(clf, 'C'): 14 C_can = np.logspace(1, 3, 3) 15 gamma_can = np.logspace(-3, 0, 3) 16 model.set_params(param_grid={'C':C_can, 'gamma':gamma_can}) 17 m = C_can.size * gamma_can.size 18 if hasattr(clf, 'max_depth'): 19 max_depth_can = np.arange(4, 10) 20 model.set_params(param_grid={'max_depth': max_depth_can}) 21 m = max_depth_can.size 22 t_start = time() 23 model.fit(x_train, y_train) 24 t_end = time() 25 t_train = (t_end - t_start) / (5*m) 26 print(u'5折交叉验证的训练时间为:%.3f秒/(5*%d)=%.3f秒' % ((t_end - t_start), m, t_train)) 27 print(u'最优超参数为:', model.best_params_) 28 t_start = time() 29 y_hat = model.predict(x_test) 30 t_end = time() 31 t_test = t_end - t_start 32 print(u'测试时间:%.3f秒' % t_test) 33 acc = metrics.accuracy_score(y_test, y_hat) 34 print(u'测试集准确率:%.2f%%' % (100 * acc)) 35 name = str(clf).split('(')[0] 36 print(name) 37 index = name.find('Classifier') 38 if index != -1: 39 name = name[:index] # 去掉末尾的Classifier 40 if name == 'SVC': 41 name = 'SVM' 42 return t_train, t_test, 1-acc, name
2.4模型的分析评估
1 mpl.rcParams['font.sans-serif'] = [u'simHei'] 2 mpl.rcParams['axes.unicode_minus'] = False 3 plt.figure(figsize=(10, 7), facecolor='w') 4 ax = plt.axes() 5 b1 = ax.bar(x, err, width=0.25, color='#77E0A0') 6 ax_t = ax.twinx() 7 b2 = ax_t.bar(x+0.25, time_train, width=0.25, color='#FFA0A0') 8 b3 = ax_t.bar(x+0.5, time_test, width=0.25, color='#FF8080') 9 plt.xticks(x+0.5, names, fontsize=10) 10 leg = plt.legend([b1[0], b2[0], b3[0]], (u'错误率', u'训练时间', u'测试时间'), loc='upper left', shadow=True)
#此处因为b1返回的是BarContainer object of 6 artists,也就是有6个直方图,任选其一即可,这里也可以用b[1]等 11 # for lt in leg.get_texts(): 12 # lt.set_fontsize(14) 13 plt.title(u'新闻组文本数据不同分类器间的比较', fontsize=18) 14 plt.xlabel(u'分类器名称') 15 plt.grid(True) 16 plt.tight_layout(2) 17 plt.show()