自然语言处理实战(一),地址:https://www.jianshu.com/p/cf14318db049
自然语言处理实战(三),地址:https://www.jianshu.com/p/effed68ae9a5
在自然语言处理实战(一)中,我们使用KNN对搜狗新闻语聊进行分类,KNN分类准确率达到的0.864,而且精确率、召回率和F1值也都在0.86以上,说明仅仅是一个192维的向量,在最简单的机器分类算法下,已经达到了相当不错的分类效果。为了分析这个192维的向量的表现力到底能到到什么程度,在本文中,我们使用在文本分类中常用的SVM对预处理后的搜狗新闻数据进行分类。
使用SVM对搜狗新闻预料进行分类研究
SVM模型有两个非常重要的参数C与gamma。其中 C是惩罚系数,即对误差的宽容度。c越高,说明越不能容忍出现误差,容易过拟合。C越小,容易欠拟合。C过大或过小,泛化能力变差。gamma是选择RBF函数作为kernel后,该函数自带的一个参数。隐含地决定了数据映射到新的特征空间后的分布,gamma越大,支持向量越少,gamma值越小,支持向量越多。支持向量的个数影响训练与预测的速度。
为维持模型在过拟合和欠拟合之间的平衡,往往最佳的参数范围是C比较大,gamma比较小;或者C比较小,gamma比较大。也就是说当模型欠拟合时,我们需要增大C或者增大gamma,不能同时增加,调节后如果模型过拟合,我们又很难判断是C过大了,还是gamma过大了;同理,模型欠拟合的时候,我们需要减小C或者减小gamma。
C和gamma的有效范围是:10-8~108,一般使用网格搜索来寻找最佳的值。下面展示SVM训练模型的代码:
def svm_model():
# 数据路径
data_path = 'D:\python\demo\paper\material\\train4\\train3000random.pkl'
# 读取数据
data = pd.read_pickle(data_path)
# x:特征,y:标签
x = data.iloc[1, :].tolist()
y = data.iloc[0, :].tolist()
# 划分训练集和测试集
x_train, x_test, y_train, y_test = train_test_split(x, y, train_size=0.8, random_state=0)
svc = SVC()
# 使用网格搜索寻找最好的svm超参数c和伽玛
c_range = np.logspace(-5, 5, 11, base=2)
gamma_range = np.logspace(-9, 3, 13, base=2)
# 设置网格搜索用来优化的参数
param_grid = [{'kernel': ['rbf'], 'C': c_range, 'gamma': gamma_range}]
grid = GridSearchCV(svc, param_grid, cv=3, n_jobs=-1)
#训练模型
print("开始训练模型...")
grid.fit(x_train, y_train)
# 保存模型
joblib.dump(grid, 'D:\python\demo\paper\model\sogou_svm.m')
print("模型保存成功!")
在KNN分类中,我仅仅花了几分钟就跑完了十折交叉验证,但SVM模型的训练却花费了7741秒,两个多小时,所以不建议将模型的预测和评价操作也一同写在一个函数里面,而是应该先保存模型,再调用模型,避免重复训练数据话费大量时间。
接下来,使用训练好的SVM模型对测试数据进行预测,代码如下:
def svm_assess():
# 加载模型
svm_v1 = joblib.load('D:\python\demo\paper\model\sogou_svm.m')
# 获得测试数据
data_path = 'D:\python\demo\paper\material\\train4\\train3000random.pkl'
# 读取数据
data = pd.read_pickle(data_path)
# x:特征,y:标签
x = data.iloc[1, :].tolist()
y = data.iloc[0, :].tolist()
# 划分训练集和测试集,一定要注意random_state=0,保证和上次划分相同
x_train, x_test, y_train, y_test = train_test_split(x, y, train_size=0.8, random_state=0)
predict_labels = svm_v1.predict(x_test)
assess_model(predict_labels, y_test)
def assess_model(predict_labels, y_test):
"""
将对模型的评估指标抽象为一个函数
"""
model_confusion_matrix = confusion_matrix(y_test, predict_labels)
print("混淆矩阵:\n", model_confusion_matrix)
# 准确率:预测正确的样本占所有样本的比重
model_accuracy = accuracy_score(y_test, predict_labels)
print("准确率:", model_accuracy)
# 精确率:查准率。即正确预测为正的占全部预测为正的比例。
model_precision = precision_score(y_test, predict_labels, average='macro')
print("精确率:", model_precision)
# 召回率:查全率。即正确预测为正的占全部实际为正的比例。
model_recall = recall_score(y_test, predict_labels, average='macro')
print("召回率:", model_recall)
# F1值:同时考虑准确率和召回率,越大越好
model_f1 = f1_score(y_test, predict_labels, average='macro')
print("F1值:", model_f1)
最终得到的结果如下:
混淆矩阵:
[[375 3 211 4 2]
[ 4 388 222 4 3]
[ 15 4 577 13 11]
[ 3 6 182 377 5]
[ 6 2 204 5 374]]
准确率: 0.697
精确率: 0.8377886688759263
召回率: 0.6952924712364587
F1值: 0.7225795628388778
尴尬,SVM的分类效果竟然还没有KNN来的好,为了进一步改进SVM的分类效果,需要做两步调整:
第一步:对数据特征进行归一化;
第二部:改进网格搜索的两个超参数C和gamma。
归一化和标准化
归一化是对不同特征维度进行伸缩变换,目的是使各个特征维度对目标函数的影响权重是一致的,即使得那些扁平分布的数据伸缩变换成类圆形,提高迭代求解的收敛速度。这也就改变了原始数据的一个分布。
标准化对不同特征维度的伸缩变换的目的是使得不同度量之间的特征具有可比性。同时不改变原始数据的分布。
介绍两种归一化方法:
-
最小最大归一化,将数据映射到[0, 1],转换函数如下:
-
零-均值归一化,得到的数据均值为0,标准差为1,转换函数如下;
这两种最常用方法使用场景:
1、在分类、聚类算法中,需要使用距离来度量相似性的时候、或者使用PCA技术进行降维的时候,第二种方法(Z-score standardization)表现更好。
2、在不涉及距离度量、协方差计算、数据不符合正太分布的时候,可以使用第一种方法或其他归一化方法。比如图像处理中,将RGB图像转换为灰度图像后将其值限定在[0 255]的范围。
原因是使用第一种方法(线性变换后),其协方差产生了倍数值的缩放,因此这种方式无法消除量纲对方差、协方差的影响,对PCA分析影响巨大;同时,由于量纲的存在,使用不同的量纲、距离的计算结果会不同。而在第二种归一化方式中,新的数据由于对方差进行了归一化,这时候每个维度的量纲其实已经等价了,每个维度都服从均值为0、方差1的正态分布,在计算距离的时候,每个维度都是去量纲化的,避免了不同量纲的选取对距离计算产生的巨大影响。
数据归一化代码:
def data_normalization(x):
"""
z-score 标准化(zero-mean normalization)
"""
# 使用pandas处理数据
x_D = pd.DataFrame(x)
# 归一化
# 注意,np.std()计算的是标准差,df.std()计算的是标准差的无偏估计
x_norm = x_D.apply(lambda i : (i -np.mean(i)) / (np.std(i)), axis=0)
return np.array(x_norm)
重新选择SVM的两个超参数的搜索空间
def svm_hyperparameter():
"""
生成SVM所需的C和gamma两个超参数的搜索空间
"""
# C在0.001到100之间差异化取值
c1 = np.arange(0.001, 0.1, 0.01)
c2 = np.append(c1, np.arange(0.1, 1, 0.1))
c3 = np.append(c2, np.arange(1, 10, 1))
c4 = np.append(c3, np.arange(10, 101, 5))
# gamma在0.0001到10之间差异化取值
g1 = np.arange(0.0001, 0.01, 0.001)
g2 = np.append(g1, np.arange(0.01, 0.1, 0.01))
g3 = np.append(g2, np.arange(0.1, 1, 0.1))
g4 = np.append(g3, np.arange(1, 11, 1))
return c4, g4
所以,原来的代码需要做一些修改,分别是:
# 输入特征需要修改为
x = data_normalization(x)
# C和gamma的范围需要修改为
c_range, gamma_range = svm_hyperparameter()
这一修改不得了,模型训练直接花了将近20个小时,一方面可能是我的电脑配置不太好,一方面是SVM在处理大量高维数据的时候确实显得力不从心。
当我使用这个新的SVM模型在测试集上测试的时候,得到以下结果:
混淆矩阵:
[[209 367 14 5 0]
[ 2 612 6 1 0]
[ 8 569 41 2 0]
[ 23 473 15 62 0]
[ 22 505 29 8 27]]
准确率: 0.317
精确率: 0.6438589874100563
召回率: 0.3113569010610703
F1值: 0.253302493767853
发现所有指标都不如之前的结果,C和gamma的搜索空间比之前更加精细了,所以有可能是归一化出了问题,当去除归一化重新训练的时候,得到以下结果:
准确率: 0.8866666666666667
精确率: 0.88692177478191
召回率: 0.8870255143795784
F1值: 0.8866414914602677
果然是归一化的问题,看来我对归一化的理解还是不够准确,并不是所有特征都适合归一化,本项目所使用的特征不具有“特征尺度”差异性,更重要的是特征之间有极强的相关性,所以归一化虽然提高了一点训练速度,但造成了分类效果迅速下降的灾难。(注意,归一化会改变分布!)
总算是得到一个比较好的结果了,但是训练却花费了25个小时,由此可以得知,SVM在处理高维大量数据的时候并不高效。另外,利用网格搜索时,C包括:
[1.0e-03 1.1e-02 2.1e-02 3.1e-02 4.1e-02 5.1e-02 6.1e-02 7.1e-02 8.1e-02
9.1e-02 1.0e-01 2.0e-01 3.0e-01 4.0e-01 5.0e-01 6.0e-01 7.0e-01 8.0e-01
9.0e-01 1.0e+00 2.0e+00 3.0e+00 4.0e+00 5.0e+00 6.0e+00 7.0e+00 8.0e+00
9.0e+00 1.0e+01 1.5e+01 2.0e+01 2.5e+01 3.0e+01 3.5e+01 4.0e+01 4.5e+01
5.0e+01 5.5e+01 6.0e+01 6.5e+01 7.0e+01 7.5e+01 8.0e+01 8.5e+01 9.0e+01
9.5e+01 1.0e+02]
gamma包括:
[1.0e-04 1.1e-03 2.1e-03 3.1e-03 4.1e-03 5.1e-03 6.1e-03 7.1e-03 8.1e-03
9.1e-03 1.0e-02 2.0e-02 3.0e-02 4.0e-02 5.0e-02 6.0e-02 7.0e-02 8.0e-02
9.0e-02 1.0e-01 2.0e-01 3.0e-01 4.0e-01 5.0e-01 6.0e-01 7.0e-01 8.0e-01
9.0e-01 1.0e+00 2.0e+00 3.0e+00 4.0e+00 5.0e+00 6.0e+00 7.0e+00 8.0e+00
9.0e+00 1.0e+01]
交叉验证数是3,所以一共要循环 47 x 38 x 3 = 5358次,这也是造成训练耗时过长的重要原因。
结果似乎是比较满意的,虽然花了很多力气,但准确率和F1值都是比较高的,尤其是所有指标都高于KNN。
然后,打印看看结果最好的地方,C和gamma到底是什么值:
{'C': 20.0, 'gamma': 0.0001, 'kernel': 'rbf'}
非常尴尬,gamma在我们设定的最小边界处取得最好效果,这意味着gamma的搜索空间设置仍然不是最好的。
我们知道,C越大,模型越容易过拟合;C越小,模型越容易欠拟合。gamma越小,模型的泛化性变好,但过小,模型实际上会退化为线性模型;gamma越大,理论上SVM可以拟合任何非线性数据。
既然 gamma 取到了0.0001甚至可能更小,说明样本空间中的样本不用做过多的映射即可做到线性可分,而且,很多问题中,比如维度过高,或者样本海量的情况下,大家更倾向于用线性核,因为效果相当,但是在速度和模型大小方面,线性核会有更好的表现。
Andrew Ng给的建议:
n:特征的维度
m:训练集的条数
1、n相对m很大时,如n=10000,m = 10,......,1000
建议用逻辑回归或者SVM(不带核函数即线性核)
2、n比较小,m居中,如n=1-1000,m=10,......,10000
建议使用带高斯核(rbf,径向基函数)的SVM
我这边n比较小,m在十几万的样子,又没法增加标签做到线性可分,所以用第二个,高斯核
3、n比较小,m很大,如n=1-1000,m=50000+
新增更多的特征,然后使用逻辑回归或SVM(不带核函数即线性核)
所以,将模型更换为线性核,C的搜索范围是 [18. , 18.5, 19. , 19.5, 20. , 20.5, 21. , 21.5],重新训练模型。耗时: 39183.2230784893,说明线性核并不一定比高斯核快。最终测试结果如下:
混淆矩阵:
[[527 15 31 16 6]
[ 20 562 5 30 4]
[ 45 15 506 36 18]
[ 18 14 31 501 9]
[ 16 6 11 30 528]]
准确率: 0.8746666666666667
精确率: 0.8756794651528986
召回率: 0.8749163662818411
F1值: 0.8747415249077493
选到的C为 {'C': 19.0, 'kernel': 'linear'}。
综上,我们的文本表示模型,在选用 SVM 进行训练的时候,最高能达到0.88以上的准确率和F1值,即使选用线性核,也可以达到0.87以上的准确率和F1值,好于KNN的分类效果。另外,在最好的分类效果时,惩罚系数C达到了19甚至20,说明线性分类时,存在较大的噪声。
参考资料:
https://www.cnblogs.com/yuehouse/p/9742697.html
https://blog.csdn.net/wusecaiyun/article/details/49681431
https://www.zhihu.com/question/20467170?sort=created
https://blog.csdn.net/pipisorry/article/details/52247379