跳到主要内容

从线性回归到逻辑回归:解决二元分类问题

从线性回归到逻辑回归 #

之前在线性回归的文章中,我们聊过一元线性回归以及多项式回归,虽然并没有太多的文字说明,但是很简单,相信一眼就可以看懂什么意思。在这篇文章中,我们来聊一聊广义线性回归模型另外一种具体形式——逻辑回归

首先需要清楚地是,逻辑回归和线性回归是不一样的,它是用来处理分类问题的。分类任务的目标就是找到一个函数,将观测值匹配到正确的类别上。它需要成对的特征向量和对应的标签来估计匹配函数的参数,这也就是所谓的监督学习。在二元分类(binary classification)问题中,算法会在一个实例中配置两个类别,在多元分类(Multiclass classification)问题中,算法会在一个实例中配置多个类别。在这篇文章中,我们会针对这两种分类问题做详细讲解。

到底什么是逻辑回归 #

如果刚刚接触到逻辑回归,很可能会被这个概念抽象地不知如何下手。下面我讲一个小故事,你应该就能清晰地理解了逻辑回归的概念。

有这么一个小子,叫吊丝。吊丝每天都会在芍药居地铁站出站口的天桥上卖貂丝。一根貂丝进货几毛钱,如果他卖\(x_1\)元一根,那么当天他就会卖出\(y_1\)根貂丝,如果他卖\(x_2\)元一根,那么当天他就会卖出\(y_2\)根貂丝。吊丝学过数学,外加挺聪明,他就寻思\(x\)和\(y\)会不会有什么关系。所以吊丝就通过之前的销售数据列了个方程式\(y=\alpha x+\beta\)聪明的吊丝发现,貂丝的销售价钱与销售量存在着这样的关系,还能够动手在纸上画出趋势图,但是又不是太准确。吊丝认为,貂丝的销售量不仅仅和价格有关,比如貂丝的质量好与否,貂丝的长度,再比如今天的天气如何,城管的出勤率都会影响着貂丝的销售量。于是貂丝又列了这个么方程:\(z=\alpha A+\beta B+\gamma C+\delta D…\)吊丝犯难了,心想这他妈怎么画出来?且慢,咱先不说这能不能画出来,就这关系式,都不一定对,因为每个变量对貂丝销售量的影响不都是遵守这样的规则,也许还不如只有价格一个变量的好。ok,以上说的这是一元线性回归和多元线性回归。

有时候吊丝想着能不能用数学预测今天的貂丝是否卖得出去的可能性,所以他就想通过回归产生一个类似概率值0到1之间的数值。但是貂丝的销售数量显然不局限于0到1之间,于是引入了Logistic方程,来做归一化。我们来思考一下为什么非得0到1?因为事件中变量不止一个,为了某一个因素的影响不会被另一个因素的影响覆盖掉,所以就都作归一化处理。但是为了区分这些变量的影响的大小,就对变量增加权重。比如貂丝的长度权重为\(0.001\),貂丝的直径权重为\(0.05\),天气的影响占\(0.1\)等等。最后吊丝列了这么一个方程\(g(x)=ln\frac {F(x)}{1-F(x)}=\beta_0 +\beta_1 x_1+\beta_2 x_2+\beta_3 x_3+…+\beta_m x_m\)其中\(g(x)\)就是你的总得分。说到这里,应该对逻辑回归有了清晰地概念了。一句话:逻辑回归就是一个被加了权重值的并做了归一化的线性回归,用来表示某个事件的可能性。

好了,上面说了这么多,我们用程序做几个例子。

逻辑回归处理二元分类 #

在普通线性回归当中,假设响应变量呈正态分布,也就是钟形曲线。上过初中的话应该都记得,正态分布的图形是对称的,其均值、中位数、众数都是相等的。但是在某些问题中,响应变量并不是呈正态分布的,比如投硬币。

对于投硬币这个问题,响应变量类似于在描述掷一个硬币结果为正面的概率。如果响应变量等于或超过了指定的临界值,预测结果就是正面,否则预测结果就是反面。响应变量是一个像线性回归中的解释变量构成的函数表示,称为逻辑函数(logistic function)。一个值在{0,1}之间的逻辑函数如下所示:\(F(t)=\frac {1}{1+e^{-t}}\)在图中这么表示:

%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
plt.figure()
plt.axis([-6, 6, 0, 1])
plt.grid(True)
X = np.arange(-6,6,0.1)
y = 1 / (1 + np.e ** (-X))
plt.plot(X, y, 'b-')
plt.show()

png

在上面的逻辑函数中,\(t\)是解释变量的线性组合,对数函数可以求得它的解,也就是我们之前的那个方程,但没有那么多的变量:\(g(x)=ln\frac {F(x)}{1-F(x)}=\beta_0 +\beta_1 x_1\)接下来我们通过这些定义来完成一个分类任务。

垃圾短信分类 #

我们在UCI上得到一份用于短信分类的数据,然后用TF-IDF方法提取特征向量,最后就可以用逻辑回归做分类了。

首先,先让我们看一看数据的结构:

import pandas as pd
df = pd.read_csv('dataset\\smsspamcollection\\SMSSpamCollection',delimiter='\t',header=None)
print(df.head())
print "Number of spam messages:", df[df[0] =="spam"][0].count()
print "Number of ham messages:", df[df[0] == "ham"][0].count()
      0                                                  1
0   ham  Go until jurong point, crazy.. Available only ...
1   ham                      Ok lar... Joking wif u oni...
2  spam  Free entry in 2 a wkly comp to win FA Cup fina...
3   ham  U dun say so early hor... U c already then say...
4   ham  Nah I don't think he goes to usf, he lives aro...
Number of spam messages: 747
Number of ham messages: 4825

可以看到,每天信息上面都被打上了标签,其中一共有747条spam信息,4825条ham信息。之后会将ham标记为0,spam标记为1。接下来让我们来调用LogisticRegression类做预测:

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model.logistic import LogisticRegression
from sklearn.cross_validation import train_test_split,cross_val_score

X_train_raw, X_test_raw, y_train, y_test = train_test_split(df[1],df[0])
vectorizer = TfidfVectorizer(stop_words='english')
X_train = vectorizer.fit_transform(X_train_raw)
X_test = vectorizer.transform(X_test_raw)
classifier = LogisticRegression()
classifier.fit(X_train, y_train)
predictions = classifier.predict(X_test)
for i, prediction in enumerate(predictions[-5:]):
    print 'Prediction: %s. Message: %s' % (prediction, X_test_raw.iloc[i])
Prediction: ham. Message: As I entered my cabin my PA said, '' Happy B'day Boss !!''. I felt special. She askd me 4 lunch. After lunch she invited me to her apartment. We went there.
Prediction: ham. Message: All was well until slightly disastrous class this pm with my fav darlings! Hope day off ok. Coffee wld be good as can't stay late tomorrow. Same time + place as always?
Prediction: ham. Message: Customer Loyalty Offer:The NEW Nokia6650 Mobile from ONLY £10 at TXTAUCTION! Txt word: START to No: 81151 & get yours Now! 4T&Ctxt TC 150p/MTmsg
Prediction: ham. Message: K tell me anything about you.
Prediction: ham. Message: Was the farm open?

像往常一样,结果出来之后我们需要对其做评估。这里就不能像之前的线性回归用R方做评估了,因为我们现在的关注点是分类的准确性

二元分类的评估方法 #

二元分类效果评估方法分为以下几类:

  • accuracy: 分类器预测正确性的比例;
  • precision:分类器预测出来的垃圾短信中真的是垃圾短信的比率;
  • recall:所有真的垃圾短信被分类器找出来的比率;
  • F1 measure:综合评价指标;
  • ROC AUC score: 所有非垃圾短信中被识别为垃圾短信所占的比例。

计算准确率(accuracy)可以用scikit-learn 提供的 accuracy_score 来计算:

from sklearn.metrics import accuracy_score
y_pred, y_true = [0, 1, 1, 0], [1, 1, 1, 1]
print(accuracy_score(y_true, y_pred))
0.5

也可以通过LogisticRegression.score()来计算,其实调用 sklearn.cross_validation 中的 cross_val_score 来实现:

df.columns = ['label','message']
label = np.where(df['label']== 'spam',1,0) #将label中的'spam' 和 'ham' 转换为 1 和 0
df.label = label
X_train_raw, X_test_raw, y_train, y_test = train_test_split(df['message'],df['label'])
vectorizer = TfidfVectorizer()
X_train = vectorizer.fit_transform(X_train_raw)
X_test = vectorizer.transform(X_test_raw)
classifier = LogisticRegression()
classifier.fit(X_train,y_train)
scores = cross_val_score(classifier, X_train, y_train, cv=5)
print "accuracy score: ",np.mean(scores), scores
accuracy score:  0.95142818306 [ 0.95340502  0.94026284  0.96287425  0.95568862  0.94491018]

计算召回率(recall)、精确率(precision)以及评价综合指标(F1 measure)也都是通过调用 sklearn.cross_validation 中的 cross_val_score 来实现

precisions = cross_val_score(classifier, X_train, y_train, cv=5, scoring='precision')

recalls = cross_val_score(classifier, X_train, y_train, cv=5, scoring='recall')

f1 = cross_val_score(classifier, X_train, y_train, cv=5, scoring='f1')

其中综合评价指标还有其他两种:F0.5 和 F2,表示精确率的权重大于召回率,或者召回率的权重大于精确率。 精确率的公式为:\(P=\frac {TP}{TP+FP}\)其中\(TP\)代表true positive,\(FP\)代表false positive

召回率的公式为:\(R=\frac {TP}{TP+FN}\)其中\(TP\)代表true positive,\(FN\)代表false negative

综合评价指标公式为:\(F1=2\frac {FR}{P+R}\)其中\(P\)代表精确率,R代表召回率

precisions = cross_val_score(classifier,X_train,y_train,cv=5,scoring = 'precision')
print 'precisions:',np.mean(precisions),precisions
recall = cross_val_score(classifier,X_train,y_train,cv = 6,scoring='recall')
print 'recall',np.mean(recall),recall
f1s = cross_val_score(classifier,X_train,y_train,cv=5,scoring='f1')
print "f1 score:",np.mean(f1s),f1s
precisions: 0.986062800098 [ 0.97368421  0.98412698  0.98765432  1.          0.98484848]
recall 0.672101449275 [ 0.68478261  0.64130435  0.70652174  0.69565217  0.68478261  0.61956522]
f1 score: 0.775646886959 [ 0.79144385  0.71264368  0.83769634  0.79781421  0.73863636]

在垃圾短信的分类中,我们为了更清楚地看到分类情况,可以用混淆矩阵(Confusion matrix)来描述结果:

from sklearn.metrics import confusion_matrix
y_test = [0, 0, 0, 0, 0, 1, 1, 1, 1, 1]
y_pred = [0, 1, 0, 0, 0, 0, 0, 1, 1, 1]
confusion_matrix = confusion_matrix(y_test, y_pred)
print(confusion_matrix)
plt.matshow(confusion_matrix)
plt.title("confusion matrix")
plt.colorbar()
plt.ylabel("true")
plt.xlabel("predicted")
plt.show()
[[4 1]
 [2 3]]

png

ROC AUC #

ROC曲线可以用来可视化分类器的效果。和准确率不同,ROC曲线对分类比例不平衡的数据集不敏感,ROC曲线显示的是对超过限定阈值的所有预测结果的分类器效果。ROC曲线画的是分类器的召回率与误警率(fall-out)的曲线。误警率也称假阳性率,是所有阴性样本中分类器识别为阳性的样本所占比例:\(F=\frac {FP}{TN+FP}\)AUC是ROC曲线下方的面积,它把ROC曲线变成一个值,表示分类器随机预测的效果。scikit-learn提供了计算ROC和AUC指标的函数。

from sklearn.metrics import roc_curve, auc

predictions = classifier.predict_proba(X_test)
false_positive_rate, recall, thresholds = roc_curve(y_test, predictions[:, 1])
roc_auc = auc(false_positive_rate, recall)
plt.title('Receiver Operating Characteristic')
plt.plot(false_positive_rate, recall, 'b', label='AUC = %0.2f' % roc_auc)
plt.legend(loc='lower right')
plt.plot([0, 1], [0, 1], 'r--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.0])
plt.ylabel('Recall')
plt.xlabel('Fall-out')
plt.show()

png

网格搜索 #

网格搜索(Grid search)是通过枚举的方法来迭代使用设置的一些参数,根据最后的结果集来选出最优的参数,在其他的算法中也经常会用到。在scikit-learn中可以用GridSearch()函数来解决这个问题:

from sklearn.grid_search import GridSearchCV
from sklearn.pipeline import Pipeline
from sklearn.metrics import precision_score,recall_score,accuracy_score
pipeline = Pipeline([
    ('vect', TfidfVectorizer()),
    ('clf', LogisticRegression())
])
parameters = {
    'vect__max_df': (0.25, 0.5, 0.75),
    'vect__stop_words': ('english', None),
    'vect__max_features': (2500, 5000, 10000, None),
    'vect__ngram_range': ((1, 1), (1, 2)),
    'vect__use_idf': (True, False),
    'vect__norm': ('l1', 'l2'),
    'clf__penalty': ('l1', 'l2'),
    'clf__C': (0.01, 0.1, 1, 10),
}
grid_search = GridSearchCV(pipeline, parameters, n_jobs=-1, verbose=1, scoring='accuracy', cv=3)
df = pd.read_csv('dataset\\smsspamcollection\\SMSSpamCollection',delimiter='\t',header=None)
df.columns = ['label','message']
df.label = np.where(df['label']== 'spam',1,0) 
X_train, X_test, y_train, y_test = train_test_split(df['message'],df['label'])
grid_search.fit(X_train, y_train)
print 'Best score: %0.3f' % grid_search.best_score_  #accuracy
print 'Best parameters set:'
best_parameters = grid_search.best_estimator_.get_params()
for param_name in sorted(parameters.keys()):
    print '\t%s: %r' % (param_name, best_parameters[param_name])
predictions = grid_search.predict(X_test)
print 'Accuracy:', accuracy_score(y_test, predictions)
print 'Precision:', precision_score(y_test, predictions)
print 'Recall:', recall_score(y_test, predictions)
Fitting 3 folds for each of 1536 candidates, totalling 4608 fits


[Parallel(n_jobs=-1)]: Done  46 tasks      | elapsed:   10.7s
[Parallel(n_jobs=-1)]: Done 196 tasks      | elapsed:   37.2s
[Parallel(n_jobs=-1)]: Done 446 tasks      | elapsed:  1.4min
[Parallel(n_jobs=-1)]: Done 796 tasks      | elapsed:  2.4min
[Parallel(n_jobs=-1)]: Done 1246 tasks      | elapsed:  3.8min
[Parallel(n_jobs=-1)]: Done 1796 tasks      | elapsed:  5.7min
[Parallel(n_jobs=-1)]: Done 2446 tasks      | elapsed:  8.5min
[Parallel(n_jobs=-1)]: Done 3196 tasks      | elapsed: 11.4min
[Parallel(n_jobs=-1)]: Done 4046 tasks      | elapsed: 19.8min
[Parallel(n_jobs=-1)]: Done 4608 out of 4608 | elapsed: 22.7min finished


Best score: 0.984
Best parameters set:
	clf__C: 10
	clf__penalty: 'l2'
	vect__max_df: 0.25
	vect__max_features: 2500
	vect__ngram_range: (1, 2)
	vect__norm: 'l2'
	vect__stop_words: None
	vect__use_idf: True
Accuracy: 0.985642498205
Precision: 0.994117647059
Recall: 0.898936170213

运行时间比较长,只能怪公司配的电脑实在垃圾…..

不过结果描述的还是很清晰,如果想要得到更好的模型,可以继续尝试不同的参数。

总结 #

今天在开头我详细解释了什么是逻辑回归,并说明了逻辑回归与线性回归的区别,而且还讲了一个小故事,哈哈哈,见笑了。在python中应用这些算法来处理问题时非常简单,但是要找到最优的参数还是需要多多练习,对数据有足够的了解和理解,将这些技能结合起来去解决这个问题才是真正的难点。所以我也经常提醒自己,做机器学习,真正的难点并不是用这些算法,而是去真正得理解那些数据,并有效地去对数据预处理,比如对残缺值的处理,变量之间的联系,提取特征变量等等。

后面我会再把逻辑回归处理多元分类的笔记补充上去,如果有朋友看到这篇文章非常感兴趣,随时欢迎您与我讨论。知识,只有互相分享才能发挥更大的作用!

小隐
作者
小隐