工作上需要用到文本分类,这里用 sklearn 做为工具,记录下学习过程
目录
1. SVM 文本分类范例
2. sklearn 做文本分类其他可选分类器
3. 文本分类的数据预处理
3. 中文文本分类方法
SVM 文本分类范例
import numpy as np
from sklearn import metrics
from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import SGDClassifier
from sklearn.svm import LinearSVC
from sklearn.utils.extmath import density
categories = [
'alt.atheism',
'talk.religion.misc',
'comp.graphics',
'sci.space',
]
print("Loading 20 newsgroups dataset for categories:")
print(categories if categories else "all")
dataHome = '/Users/xinsheng/PycharmProjects/PythonPlaygroud/dataset'
data_train = fetch_20newsgroups(data_home= dataHome, subset='train', categories=categories,
shuffle=True, random_state=42)
data_test = fetch_20newsgroups(data_home=dataHome, subset='test', categories=categories,
shuffle=True, random_state=42)
print('data loaded')
y_train, y_test = data_train.target, data_test.target
target_names = data_train.target_names
vectorizer = TfidfVectorizer(sublinear_tf=True, max_df=0.5, stop_words='english')
X_train = vectorizer.fit_transform(data_train.data)
X_test = vectorizer.transform(data_test.data)
feature_names = vectorizer.get_feature_names()
# 在 as array 之前是什么呢
feature_names = np.asarray(feature_names)
results = []
def benchmark(clf):
clf.fit(X_train, y_train)
pred = clf.predict(X_test)
if hasattr(clf, 'coef_'):
print("dimensionality: %d" % clf.coef_.shape[1])
print("density: %f" % density(clf.coef_))
# if opts.print_top10 and feature_names is not None:
print("top 10 keywords per class:")
for i, label in enumerate(target_names):
top10 = np.argsort(clf.coef_[i])[-10:]
print("%s: [%s]" % (label, " ".join(feature_names[top10])))
print()
print(metrics.classification_report(y_test, pred, target_names=target_names))
for penalty in ["l2", "l1"]:
print('=' * 80)
print("%s penalty" % penalty.upper())
# Train Liblinear model
# svc stands for c-support vector classification
print('Linear SVC')
benchmark(LinearSVC(penalty=penalty, dual=False, tol=1e-3))
print('SGDClassifier')
# Train SGD model
benchmark(SGDClassifier(alpha=.0001, n_iter=200, penalty=penalty))
上面这个例子是官方文档给出的 SVM 分类实例,数据集是 20 new groups, 如果本地没有数据集,sklearn 会自动下载
sklearn 的匹配模式非常简单,选中模型 clf 后直接调用 clf.fit(X_train, Y_train)
即可完成模型训练,然后调用 clf.predict(X_test)
即可返回结果
需要关注的是参数的配置,比如上文中对于 LinearSVC 和 SGD 来讲,都有一个很多参数可以配置,比如 penalty, alpha 等等,下面是参数的翻译
SVM 的参数翻译
Similar to SVC with parameter kernel='linear', but implemented in terms of
liblinear rather than libsvm, so it has more flexibility in the choice of
penalties and loss functions and should scale better to large numbers of samples.
This class supports both dense and sparse input and the multiclass support
is handled according to a one-vs-the-rest scheme.
SVM 多 label 分类是多个 binary classifier 的组合
penalty : string, 'l1' or 'l2' (default='l2')
Specifies the norm used in the penalization. The 'l2'
penalty is the standard used in SVC. The 'l1' leads to ``coef_``
vectors that are sparse.
从数据集上来看,l2 的效果好一些
dual : bool, (default=True)
Select the algorithm to either solve the dual or primal
optimization problem. Prefer dual=False when n_samples > n_features.
一般来说 sample > feature, 但是对于文本分来来说,这个也不一定为真
tol : float, optional (default=1e-4)
Tolerance for stopping criteria.
max_iter : int, (default=1000)
The maximum number of iterations to be run.
tol 和 max_iter 是训练集的终止条件,一个是按照 loss function 的误差,一个是按照最大迭代次数
C : float, optional (default=1.0)
Penalty parameter C of the error term.
相当于惩罚松弛变量,希望松弛变量接近0,即对误分类的惩罚增大,趋向于对训练集全分对的情况,这样对训练集测试时准确率很高,
但泛化能力弱。C值小,对误分类的惩罚减小,允许容错,将他们当成噪声点,泛化能力较强,
对于那些比较粗糙的数据,比如说文本分类应该用较大一些的 C
coef_ : array, shape = [n_features] if n_classes == 2 else [n_classes, n_features]
Weights assigned to the features (coefficients in the primal
problem). This is only available in the case of a linear kernel.
本来以为 SVC 非线性核的分类效果会好一些,结果发现奇差无比,然后搜索了下大家对 SVM 的看法
预测函数简单f(x) = w’*x+b,分类速度快。对于类别多的问题,分类速度的确需要考虑到,线性分类器的w可以事先计算出来,而非线性分类器在高维空间时支持向量数会非常多,分类速度远低于线性分类器。
线性SVM的推广性有保证,而非线性如高斯核有可能过学习。再举个例子,基于人脸的性别识别,即给定人脸图像,判断这个人是男还是女。我们提取了3700多维的特征,用线性SVM就能在测试集上达到96%的识别正确率。因此,线性SVM是实际应用最多的,实用价值最大的。
如果在你的应用中,特征维数特别低,样本数远超过特征维数,则选用非线性核如高斯核是比较合理的。如果两类有较多重叠,则非线性SVM的支持向量特别多,选择稀疏的非线性SVM会是一个更好的方案,支持向量少分类速度更快。
随意,非线性 SVM 还不如线性 SVM。
penalty 属性是 loss function 的定义
另外 sklearn 给出了训练模型的评价指标,以 precision, recall, f1-score, support 来反应模型的好坏。
最后,clf 模型甚至可以返回最能影响分类器分类效果的 top K 个单词,但是要求分类器有 coef 属性,目前用到的几个分类器,似乎都有这个这个属性。
sklearn 做文本分类其他可选分类器
除了 SVM 分类外,还要很多分类器可选,下面列出若干常用分类器
results = []
for clf, name in (
(RidgeClassifier(tol=1e-2, solver="lsqr"), "Ridge Classifier"),
(Perceptron(n_iter=50), "Perceptron"),
(PassiveAggressiveClassifier(n_iter=50), "Passive-Aggressive"),
(KNeighborsClassifier(n_neighbors=10), "kNN"),
(RandomForestClassifier(n_estimators=100), "Random forest")
):
print('=' * 80)
benchmark(clf)
除了上面提到的分类器以外,还有一个是贝叶斯分类,贝叶斯分类我在研究生阶段就学习过,它根据单词和文本的对应关系来分类文档,因为某种类型的文档就会出现某种类型的单词,下面是我找到的别人写好的例子。
nbc = Pipeline([
('vect', TfidfVectorizer()),
('clf', MultinomialNB(alpha=1.0)), # 或者使用 BernoulliNB
])
nbc_6.fit(train_data, train_target) #训练我们的多项式模型贝叶斯分类器
predict = nbc_6.predict(test_data) #在测试集上预测结果
count = 0 #统计预测正确的结果个数
for left , right in zip(predict, test_target):
if left == right:
count += 1
print(count/len(test_target))
out: 0.793
benchmark 函数还是定义的那个,从分类的结果来看,这几种分类器的分类效果也都不错,且运算时间都不长,考虑到未来,深度学习的时间可以控制在数十秒以内,人工智能肯定再会上一个台阶。
随机森林分类器:
经典的决策树是一个颗树,按照特征的信息增益来选择分裂样本,计算公式不是很复杂。一句来来解释,就是样本的正反和特征的正反吻合的比较好,这种特征认为和数据关系较大,因此可以用来选取作为分裂特征
随机森林是从数据集中有放回的拿数据,每个子数据的数量和原数据一样,所以子数据很可能有重复数据,然后每个子数据训练数据集,最后大家投票,选择分类。
KNN
KNN 是有监督的学习方法,不同于 Kmeans 的无监督学习方法。它的核心是每个样本都可以用它最近的几个邻居来表示,如果一个样本在特征空间中的k个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别,则该样本也属于这个类别。KNN算法中,所选择的邻居都是已经正确分类的对象。该方法在定类决策上只依据最邻近的一个或者几个样本的类别来决定待分样本所属的类别。 KNN方法虽然从原理上也依赖于极限定理,但在类别决策时,只与极少量的相邻样本有关。由于KNN方法主要靠周围有限的邻近的样本,而不是靠判别类域的方法来确定所属类别的,因此对于类域的交叉或重叠较多的待分样本集来说,KNN方法较其他方法更为适合。
优缺点:
特别适合于多分类问题(multi-modal,对象具有多个类别标签), kNN比SVM的表现要好
该算法在分类时有个主要的不足是,当样本不平衡时,如一个类的样本容量很大,而其他类样本容量很小时,有可能导致当输入一个新样本时,该样本的K个邻居中大容量类的样本占多数。 该算法只计算“最近的”邻居样本,某一类的样本数量很大,那么或者这类样本并不接近目标样本,或者这类样本很靠近目标样本。无论怎样,数量并不能影响运行结果。
该方法的另一个不足之处是计算量较大,因为对每一个待分类的文本都要计算它到全体已知样本的距离,才能求得它的K个最近邻点。
Perceptron
感知器分类器,从文档上来看,它似乎是线性回归的
SGD
随机梯度下降,好处是能够跳出局部最优解
文本分类中的数据预处理方法
上面使用到的数据集都是 sklearn 自带的,真正应用时,需要自己处理数据,在一开始,这是最耗费时间的过程。
下面是一个生成一个分类器可用数据的格式
def fetchTrainData(datadir, type, data = [], target = []):
for file in os.listdir(datadir):
with open(datadir + file) as f:
file_data = []
for row in f:
file_data.append(row)
data.append(" ".join(file_data))
target.append(type)
def jiebaTfIdf(data, virgin, vectorizer):
# 语料库是一个一维数组,数组的每一个元素是一个文档(经过预处理)
corpus = []
for lines in data['data']:
# cut_all 是什么意思呢?
words = jieba.cut(lines, cut_all=True)
stopFreeLine = []
for word in words:
if word != '' and word != "\n" and word != "\n\n" and word not in STOP_WORDS:
stopFreeLine.append(word)
# 再把此行加入到命令中
corpus.append(" ".join(stopFreeLine))
# 使用对单词进行 tfidf 预处理
# 返回的 shape 是 112 * 6111 这是什么意思呢?
# fit_transform 和 transform 的区别是什么呢
if virgin:
tfidf = vectorizer.fit_transform(corpus)
else:
tfidf = vectorizer.transform(corpus)
# feature_names = vectorizer.get_feature_names()
return tfidf
fetchTrainData(api_train_dir, '0', train_data, target)
fetchTrainData(other_train_dir, '1', train_data, target)
cache = dict(data = train_data, target=target,target_name=target_name)
vectorizer = TfidfVectorizer(sublinear_tf=True, stop_words='english')
X_train = jiebaTfIdf(cache, True, vectorizer)
clf.fit(X_train, y_train)
X_train 和 y_train 函数都是数组,在放入 vectorizer 之前,x_train.data 就是邮件
本身,它是一个长度为 2034 的 array: len(x_train.data) -> 2034, 下面是数组中的
第一个元素
"From: rych@festival.ed.ac.uk (R Hawkes)\nSubject: 3DS: Where did all the
texture rules go?\nLines: 21\n\nHi,\n\nI've noticed that if you only save a
model (with all your mapping planes\npositioned carefully) to a .3DS file
that when you reload it after restarting\n3DS, they are given a default
position and orientation. But if you save\nto a .PRJ file their
positions/orientation are preserved. Does anyone\nknow why this information
is not stored in the .3DS file? Nothing is\nexplicitly said in the manual
about saving texture rules in the .PRJ file. \nI'd like to be able to
read the texture rule information, does anyone have \nthe format for
the .PRJ file?\n\nIs the .CEL file format available from somewhere?
\n\nRych\n\n=====================================================
=================\nRycharde Hawkes\t\t\t\temail: rych@festival.ed
.ac.uk\nVirtual Environment Laboratory\nDept. of Psychology\t\t\tTel :
+44 31 650 3426\nUniv. of Edinburgh\t\t\tFax : +44 31 667 0150\n======
================================================================\n"
在进入 vectorizer 之后,就变成了一个矩阵, x_train.shape = (2034, 33809), 每一个文档
变成了一个有 33809 个词向量表示的数组,这个地方需要注意,fit_transform 和 transform
的区别,fit_transform 会计算需要多少个词来描述一个文档,33809 是这个函数的输出之一,而
transform 就用把自己的数据变成 33809 个词的矩阵,因此对于训练数据集用 fit_transform,
对测试集使用 transform, 千万不能多次调用 fit_transform.
TF-IDF Vectorizer 对一个文档的描述
(0, 26977) 0.29200533962
(0, 13788) 0.223534884596
(0, 12254) 0.143033187036
(0, 4287) 0.112190889038
(0, 31453) 0.104409646825
(0, 15786) 0.23558806939
(0, 2362) 0.260611070013
(0, 11264) 0.0532815955186
(0, 30376) 0.223037634455
(0, 26918) 0.134980938364
(0, 1621) 0.0655837805259
(0, 16025) 0.0699392301968
(0, 32232) 0.0508220378498
(0, 22173) 0.100565143276
(0, 27212) 0.134980938364
起初,我对这个数据很疑惑,为什么不表示成一个矩阵的形式,后来看到一个例子才搞明白,因为
数组较长,所以我们把那些不为 0 的词拿出来就好,比如第一行,就是 26977 个位置的词不为0 ,
其值为 0.29200533962。
sklearn 不支持中文的分词器,所以我们要手动处理,在上面的例子中,我们使用结巴分词,并
使用某大学的停词词库进行过滤单词。 words = jieba.cut(lines, cut_all=True)
这句话的意思是把一个单词分解成它所有可能组合的形式,比如中华人民共和国会分解成
中华,中华人民,和中国人民共和国。
cache = dict(data = train_data, target=target,target_name=target_name)
这行代码我其实也不是很明白,python 的数据结构然后这么的随意,dict 的 key 可以是一个
不存在的变量而不需要加上引号?
关于 CountVectorizer,我一直以为它是 BOW 类似的实现,但是看到下面这个例子后才发现
并不是
texts=["dog cat fish","dog cat cat","fish bird", 'bird']
cv = CountVectorizer()
cv_fit=cv.fit_transform(texts)
print(cv.get_feature_names())
print(cv_fit.toarray())
#['bird', 'cat', 'dog', 'fish']
#[[0 1 1 1]
# [0 2 1 0]
# [1 0 0 1]
# [1 0 0 0]]
print(cv_fit.toarray().sum(axis=0))
#[2 3 2 2]
只是简单的词频相加,看来 sklearn 还是适合用一些简单的算法,深度学习相关的东西还是交给
tensorflow 等框架。但是一般并不是不好,sklearn 对大部分场景已经很好了,并且它超级
快,在笔记本上就能训练出一个模型。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。