1 问题背景
我们有一堆新闻文本数据,包含“新闻类别”、“新闻内容”两列,希望建立一个模型,来预测新闻内容属于哪个类别。
这是一个文本分类问题,朴素贝叶斯和逻辑回归可以说是文本分类最常用的Baseline模型,今天小树带大家来看看,朴素贝叶斯和逻辑回归效果如何以及它们两个究竟谁更厉害。
操作环境:jupyter notebook + python3
预备知识:中文分词、tf-idf、朴素贝叶斯模型、逻辑回归模型
2 加载相关包
import numpy as np
import pandas as pd
import random
from sklearn.svm import SVC
from sklearn import preprocessing, decomposition, model_selection, metrics, pipeline
from sklearn.model_selection import GridSearchCV
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.decomposition import TruncatedSVD
from sklearn.linear_model import LogisticRegression
from sklearn.naive_bayes import MultinomialNB
3 加载数据并进行数据预处理
(1)加载数据
# load data
data = [[i.split("\t")[0],i.split("\t")[1]] for i in open(u"data/cnews.train.txt","r", encoding="utf-8")]
# 打乱数据
random.shuffle(data)
data[1]
output
(2)分词,标签转换,求tf-idf
# 对新闻内容进行分词
import jieba
jieba.enable_parallel() #并行分词开启
terms = [" ".join([j for j in jieba.cut(i[1])])
for i in data[:1000]]
#接下来用scikit-learn中的LabelEncoder将文本标签(Text Label)转化为数字(Integer)
lbl_enc = preprocessing.LabelEncoder()
y = lbl_enc.fit_transform([i[0] for i in data[:1000]])
from sklearn.feature_extraction.text import TfidfVectorizer
# 初始化TFidf对象,去停用词,加2元语言模型
tfv = TfidfVectorizer(min_df=3, max_features=None, strip_accents='unicode', analyzer='word',token_pattern=r'\w{1,}', ngram_range=(1, 2), use_idf=1,smooth_idf=1,sublinear_tf=1, stop_words = 'english')
tfv.fit(terms)
X_all = tfv.transform(terms)
(3)观察label分布,划分数据集
len_train = 600
X_train = X_all[:len_train]
y_train = y[:len_train]
X_test = X_all[len_train:]
y_test = y[len_train:]
print('X_train shape: ', X.shape)
print('y_train shape: ', y_train.shape)
print('X_test shape: ', X_test.shape)
print('y_test shape: ', y_test.shape)
# 观察label的分布: 共有10个类别,类别均衡
label_data = pd.DataFrame({'label':y})
print('\nDistribution of all label:\n', label_data.label.value_counts())
output
从上面的数据可以得到两点结论:
我们的特征有29812个,而训练样本只有600个,特征维度远远大于训练样本数量,因此特征空间是非常稀疏的。
从label的分布来看,各个类别是平衡的。
4 定义评估标准
这是一个典型的多分类问题,采用常用的multi-class log loss进行评估。
def multiclass_logloss(actual, predicted, eps=1e-15):
"""
对数损失度量(Logarithmic Loss Metric)的多分类版本。
:param actual: 包含actual target classes的数组
:param predicted: 分类预测结果矩阵, 每个类别都有一个概率
"""
# Convert 'actual' to a binary array if it's not already:
if len(actual.shape) == 1:
actual2 = np.zeros((actual.shape[0], predicted.shape[1]))
for i, val in enumerate(actual):
actual2[i, val] = 1
actual = actual2
clip = np.clip(predicted, eps, 1 - eps)
rows = actual.shape[0]
vsota = np.sum(actual * np.log(clip))
return -1.0 / rows * vsota
好了,万事俱备,开始激动人心的建模时刻!
5 朴素贝叶斯 vs 逻辑回归
step 1 建立一个朴素贝叶斯的baseline模型
clf_nb = MultinomialNB() # 多项式朴素贝叶斯
clf_nb.fit(X_train, y_train)
predictions = clf_nb.predict_proba(X_test)
print ("logloss: %0.3f " % multiclass_logloss(y_test, predictions))
logloss: 1.111
朴素贝叶斯在测试集上的logloss为1.111, 效果不太好,没关系,我们把这个作为baseline。
step 2 建立一个逻辑回归模型来对比看看
clf_lr = LogisticRegression(C=1.0,solver='lbfgs',multi_class='multinomial')
clf_lr.fit(X_train, y_train)
predictions = clf_lr.predict_proba(X_test)
print ("logloss: %0.3f " % multiclass_logloss(y_test, predictions))
logloss: 1.223
看起来似乎逻辑回归比朴素贝叶斯效果还差喔。
step 3 我们来调参吧
# 创建评分函数
mll_scorer = metrics.make_scorer(multiclass_logloss, greater_is_better=False, needs_proba=True)
(1)朴素贝叶斯调参
调参指南:朴素贝叶斯调参一般调alpha,平滑参数,其值越小,越容易过拟合,值越大,容易造成欠拟合。
下面建立一个pipeline来调参。
nb_model = MultinomialNB()
# 创建pipeline
clf = pipeline.Pipeline([('nb', nb_model)])
# 搜索参数设置
param_grid = {'nb__alpha': [0.001, 0.01, 0.1, 1, 10, 100]}
# 网格搜索模型(Grid Search Model)初始化
model = GridSearchCV(estimator=clf, param_grid=param_grid, scoring=mll_scorer,
verbose=10, n_jobs=-1, iid=True, refit=True, cv=6)
# fit网格搜索模型
model.fit(X_train, y_train)
print("Best score: %0.3f" % model.best_score_)
print("Best parameters set:")
best_parameters = model.best_estimator_.get_params()
for param_name in sorted(param_grid.keys()):
print("\t%s: %r" % (param_name, best_parameters[param_name]))
Best parameters set: nb__alpha: 0.1
我们发现alpha = 0.1时,模型效果最好,用这个参数重新跑跑看
# 使用最优参数建模
clf_nb = MultinomialNB(alpha=0.1)
clf_nb.fit(X_train, y_train)
predictions = clf_nb.predict_proba(X_test)
print ("logloss: %0.3f " % multiclass_logloss(y_test, predictions))
logloss: 0.243
朴素贝叶斯调参后logloss 从1.111 直接降到了0.243,惊不惊喜,意不意外!只是简单的调参就收获了如此大的性能提升,而且0.243这个表现说明朴素贝叶斯的效果还是杠杠的哟。
下面来看看逻辑回归调参之后能否反超朴素贝叶斯!
(2)逻辑回归调参
调参指南:逻辑回归调参常调正则项C以及penalty,由于这里我们使用了lgfgs solver,它只支持L2 penalty,因此我们只调C。注意,C是正则化系数的倒数,其值越大,正则项的权重越小,模型越容易过拟合。
lr_model = LogisticRegression(solver='lbfgs',multi_class='multinomial')
# 创建pipeline
clf = pipeline.Pipeline([('lr', lr_model)])
# 搜索参数设置
param_grid = {'lr__C': [0.1, 1.0, 10]}
# 网格搜索模型(Grid Search Model)初始化
model = GridSearchCV(estimator=clf, param_grid=param_grid, scoring=mll_scorer,
verbose=10, n_jobs=-1, iid=True, refit=True, cv=6)
# fit网格搜索模型
model.fit(X_train, y_train)
print("Best score: %0.3f" % model.best_score_)
print("Best parameters set:")
best_parameters = model.best_estimator_.get_params()
for param_name in sorted(param_grid.keys()):
print("\t%s: %r" % (param_name, best_parameters[param_name]))
Best parameters set: lr__C: 10
用这个最优参数重新训练模型:
clf_lr = LogisticRegression(C=10, solver='lbfgs',multi_class='multinomial')
clf_lr.fit(X_train, y_train)
predictions = clf_lr.predict_proba(X_test)
print ("logloss: %0.3f " % multiclass_logloss(y_test, predictions))
logloss: 0.546
调参后逻辑回归的表现也变好了很多,但是跟朴素贝叶斯比,还是相去甚远,看起来好像还是朴素贝叶斯更厉害嘛!逻辑回归还有翻盘的机会吗?
我们来观察一下模型的结果:
print('train accuracy: ',clf_lr.score(X_train, y_train))
print('test accuracy: ',clf_lr.score(X_test, y_test))
train accuracy: 1.0
test accuracy: 0.9675
在测试集上准确率很高啊!奇怪了,我们打印一个预测的结果来看看:
predictions[0]
array([0.02830099, 0.03794662, 0.03657158, 0.02690717, 0.02604819,
0.04728005, 0.02332729, 0.73275343, 0.02299653, 0.01786817])
测试集的第一个数据真实label是7,而模型预测为7的概率为0.73,由于我们使用交叉熵损失作为评估,所以看起来模型的表现不太好。尽管特征很多,样本量很小,我们还想让模型拟合得更好!
那怎么办?我有一个大胆的想法...
我们重新调逻辑回归的正则项参数C,加大加大,人有多大胆,地有多大产!!!说白了,我就是不要正则化了!
lr_model = LogisticRegression(solver='lbfgs',multi_class='multinomial')
# 创建pipeline
clf = pipeline.Pipeline([('lr', lr_model)])
# 搜索参数设置
param_grid = {'lr__C': [1000,10000,100000,1000000, 10000000, 100000000]}
# 网格搜索模型(Grid Search Model)初始化
model = GridSearchCV(estimator=clf, param_grid=param_grid, scoring=mll_scorer,
verbose=10, n_jobs=-1, iid=True, refit=True, cv=6)
# fit网格搜索模型
model.fit(X_train, y_train)
print("Best score: %0.3f" % model.best_score_)
print("Best parameters set:")
best_parameters = model.best_estimator_.get_params()
for param_name in sorted(param_grid.keys()):
print("\t%s: %r" % (param_name, best_parameters[param_name]))
Best parameters set: lr__C: 1000000
这路子有点野...看看它行不行先!
clf_lr = LogisticRegression(C=1000000, solver='lbfgs',multi_class='multinomial')
clf_lr.fit(X_train, y_train)
predictions = clf_lr.predict_proba(X_test)
print ("logloss: %0.3f " % multiclass_logloss(y_test, predictions))
logloss: 0.125
内心诚惶诚恐,但数据显示很牛逼!好了,贝叶斯你跪下叫爸爸吧。。。
。。。。。。
贝叶斯表示这路子太野我不服,能不能来点正常的套路?!
那降维打击了解一下?
#使用SVD(奇异值分解)进行降维,components设为180,对于SVM来说,SVD的components的合适调整区间一般为120~200
svd = decomposition.TruncatedSVD(n_components=180)
svd.fit(X_train)
xtrain_svd = svd.transform(X_train)
xtest_svd = svd.transform(X_test)
#对从SVD获得的数据进行缩放
scl = preprocessing.StandardScaler()
scl.fit(xtrain_svd)
xtrain_svd_scl = scl.transform(xtrain_svd)
xtest_svd_scl = scl.transform(xtest_svd)
# 看看降维后LR的效果
clf_lr = LogisticRegression(C=100, solver='lbfgs',multi_class='multinomial')
clf_lr.fit(xtrain_svd_scl, y_train)
predictions = clf_lr.predict_proba(xtest_svd_scl)
print ("logloss: %0.3f " % multiclass_logloss(y_test, predictions))
logloss: 0.144
贝叶斯,卒。至此,逻辑回归大获全胜!
参考资料: