从本节开始,我们的机器学习之旅进入了下一个篇章。之前讨论的是回归算法,回归算法主要用于预测数据。而本节讨论的是分类问题,简而言之就是按照规则将数据分类
而要讨论的逻辑回归,虽然名字叫做回归,它要解决的是分类问题
还是老规矩,先来个例子,再讨论原理
假设以下场景:一位老哥想要测试他老婆对于抽烟忍耐度,他进行了以下测试
星期一 | 星期二 | 星期三 | 星期四 | 星期五 | 星期六 | 星期日 | |
---|---|---|---|---|---|---|---|
抽烟(单位:根) | 6 | 18 | 14 | 13 | 5 | 10 | 8 |
是否被老婆打 | 否 | 是 | 是 | 是 | 否 | 是 | 否 |
将以上情形带入模型
from sklearn.linear_model import LogisticRegression
import numpy as np
X = np.array([6, 18, 14, 13, 5, 10, 8]).reshape(-1, 1)
y = np.array([0, 1, 1, 1, 0, 1, 0])
model = LogisticRegression()
model.fit(X, y)
print(f"系数: {model.coef_[0][0]:.4f}")
print(f"截距: {model.intercept_[0]:.4f}")
decision_boundary = -model.intercept_[0] / model.coef_[0][0]
print(f"决策边界: {decision_boundary:.2f}")
脚本!启动:
单特征影响结果,这明显是一个线性模型,所以出现了熟悉的系数与截距,还有一个新的参数:决策边界,这意味着9.1就是分类阈值,>=9.1的结果分类为1,
带入到情景当中,每天9根烟以上,要被老婆打,否则不打
那位大哥说了,怎么和线性回归这么相似,但是最后又有一点不同
总之,逻辑回归虽然也有“回归”2字,但是主要还是更适合分类问题
逻辑回归通过将线性回归的输出映射到概率值(0到1之间),利用Sigmoid函数(或称逻辑函数)实现分类
w 是权重向量,b是偏置项,X 是输入特征向量
[z to infty,sigma(z) to 1 ]
[z to -infty,sigma(z) to 0 ]
通过该函数,把线性方程的值域从((-infty,+infty)),修改为概率的值域([0,1])
与线性回归的mse不同,逻辑回归使用的损失函数为平均交叉熵
[mathcal{L} = - frac{1}{m} sum_{i=1}^{m} left[ y^{(i)} log(hat{y}^{(i)}) + (1 - y^{(i)}) log(1 - hat{y}^{(i)}) right] ]
from sklearn.metrics import log_loss
y_proba = model.predict_proba(X)[:, 1]
loss_sklearn = log_loss(y, y_proba)
print('=='*20)
print(f"损失函数(Log Loss): {loss_sklearn:.4f}")
准确率:顾名思义,分类的准确率
from sklearn.metrics import accuracy_score
y_pred = model.predict(X)
accuracy = accuracy_score(y, y_pred)
print('=='*20)
print(f"准确率:{accuracy:.2f}")
混淆矩阵:对于一个二分类(二元问题,最后的结果可以用0、1来分类)问题,混淆矩阵是一个 2×2 的矩阵,包含以下四个关键指标
[[3 1] # TN=3, FP=1
[1 3]] # FN=1, TP=3
from sklearn.metrics import confusion_matrix
print('=='*20)
print('混淆矩阵:')
y_pred = model.predict(X)
cm = confusion_matrix(y, y_pred)
print(cm)
从混淆矩阵中产生了一系列评估指标:
或者直接使用classification_report
:
from sklearn.metrics import classification_report
print('=='*20)
y_pred = model.predict(X)
print("Logistic Regression 分类报告:n", classification_report(y, y_pred))
ROC-AUC
from sklearn.metrics import roc_curve, roc_auc_score
y_proba = model.predict_proba(X)[:, 1]
auc_score = roc_auc_score(y, y_proba)
print('=='*20)
print(f"AUC = {auc_score:.4f}")
import matplotlib.pyplot as plt
fpr, tpr, thresholds = roc_curve(y, y_proba)
plt.figure(figsize=(6, 5))
plt.plot(fpr, tpr, color='blue', label=f'ROC curve (AUC = {auc_score:.4f})')
plt.plot([0, 1], [0, 1], color='gray', linestyle='--')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
直接丢gpt看下吧
先来讨论一下决策边界,决策边界是先推导出回归系数与截距之后,再带入模型
[hat{y} = sigma(z) = frac{1}{1 + e^{-z}} quad , z = mathbf{w}^top mathbf{x} + b ]
如果是单特征:
[hat{y} = sigma(w_1x_1+b) = frac{1}{1 + e^{-(w_1x_1+b)}} quad ]
取分类阈值为0.5,为什么要取0.5,大部分情况,二分类中0
和1
的可能性是均等的,通常任务>0.5为1
,反之0。但是遇到所谓的分类不平衡的情况,就要变化了,这个后面再讨论,这里先姑且取0.5
[frac{1}{1 + e^{-(w_1x_1+b)}} = 0.5 quad ]
[e^{-(w_1x_1+b)} = 1 quad ]
[-(w_1x_1+b) = 0 quad ]
[x_1 = -frac{b}{w_1} quad ]
可以看到单特征的决策边界是一个点,这就非常容易区分0
和1
了
如果是2个特征:
[hat{y} = sigma(w_1x_1+w_2x_2+b) = frac{1}{1 + e^{-(w_1x_1+w_2x_2+b)}} quad ]
同理(hat{y}=0.5)
[frac{1}{1 + e^{-(w_1x_1+w_2x_2+b)}} = 0.5 quad ]
[x_2=-frac{w_1x_1+b}{w_2} ]
可以看到2个特征的决策边界是y=x
的直线
同理3个特征是一个面,>3个特征就已经不能画出来了
继续刚才的问题,比如除了抽烟被打,再加上喝酒,2个特征
星期一 | 星期二 | 星期三 | 星期四 | 星期五 | 星期六 | 星期日 | |
---|---|---|---|---|---|---|---|
抽烟(单位:根) | 6 | 18 | 14 | 13 | 5 | 10 | 8 |
喝酒(单位:两) | 8 | 1 | 2 | 4 | 3 | 3 | 0 |
是否被老婆打 | 是 | 否 | 否 | 是 | 否 | 是 | 是 |
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report
import numpy as np
X = np.array([
[6,8],
[18,1],
[14,2],
[13,4],
[5,3],
[10,3],
[8,0],
])
y = np.array([1, 0, 0, 1, 0, 1, 1])
model = LogisticRegression()
model.fit(X, y)
coef = model.coef_[0]
intercept = model.intercept_[0]
print(f"系数: {coef}")
print(f"截距: {intercept}")
决策边界:$$ y=frac{0.127x-0.94}{0.26} $$
import matplotlib.pyplot as plt
x_vals = np.linspace(X[:, 0].min() - 1, X[:, 0].max() + 1, 100)
decision_boundary = -(coef[0] * x_vals + intercept) / coef[1]
plt.figure(figsize=(8, 6))
colors = ['red' if label == 0 else 'blue' for label in y]
plt.scatter(X[:, 0], X[:, 1], c=colors, s=80, edgecolor='k')
plt.plot(x_vals, decision_boundary, 'k--', label='Decision Boundary')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
在边界以上的是1,边界以下的0
比如以下代码,1000个样本中,只有14个1
,986个0
,属于严重的类别不平衡
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import make_classification
from sklearn.metrics import classification_report
from sklearn.model_selection import train_test_split
X, y = make_classification(n_samples=1000, n_features=5,
weights=[0.99], flip_y=0.01,
class_sep=0.5, random_state=0)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
model = LogisticRegression()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
c_report = classification_report(y_test, y_pred, zero_division=0)
print("Logistic Regression 分类报告:n", c_report)
1
上完全失败,虽然多数类0
的准确率是99%,但是毫无意义,从未正确预测为1
0
的样本都被找到了(100%);一个1
类都没找到1
的 F1 是 0,说明模型对少数类的预测能力完全崩溃0
有 296 个样本,类别1
只有 4 个样本有位彦祖说了,你这分类只分了1次训练集和测试集,如果带上交叉验证,多分几次类,让其更有机会学习到少数类,情况能不能有所改善?
from sklearn.model_selection import cross_val_predict
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
y_pred = cross_val_predict(model, X, y, cv=cv)
c_report = classification_report(y, y_pred, zero_division=0)
print("Logistic Regression(交叉验证)分类报告:n", c_report)
情况并没有好转,模型依然无法区分少数类
model = LogisticRegression(class_weight='balanced')
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
c_report = classification_report(y_test, y_pred, zero_division=0)
print("Logistic Regression 加权 分类报告:n", c_report)
情况有所好转
1
的recall从0-->0.5,2 个正类样本中至少预测中了 1 个1
的Precision从0-->0.01,模型预测为正类的样本大多数是错的,这是 class_weight 造成的:宁愿错也要猜一猜正类0
的recall从1-->0.7,同样是class_weight造成的,把一部分原本是负类的样本错判为正类了增加少数类样本,复制或生成新样本,通过 SMOTE(Synthetic Minority Over-sampling Technique)进行过采样
from imblearn.over_sampling import SMOTE
from imblearn.pipeline import Pipeline
from sklearn.model_selection import cross_val_predict
model = Pipeline([
('smote', SMOTE(random_state=0)),
('logreg', LogisticRegression(solver='lbfgs', max_iter=1000))
])
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
y_pred = cross_val_predict(model, X, y, cv=cv)
print("SMOTE + LogisticRegression 分类报告:n")
print(classification_report(y, y_pred, zero_division=0))
减少多数类样本(随机删除或聚类),通过RandomUnderSampler进行欠采样
from imblearn.under_sampling import RandomUnderSampler
from imblearn.pipeline import Pipeline
from sklearn.model_selection import cross_val_predict
pipeline = Pipeline([
('undersample', RandomUnderSampler(random_state=0)),
('logreg', LogisticRegression(solver='lbfgs', max_iter=1000))
])
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
y_pred = cross_val_predict(pipeline, X, y, cv=cv)
print("欠采样 + LogisticRegression 分类报告:n")
print(classification_report(y, y_pred, zero_division=0))
与过采样大同小异,效果还不如过采样
lasso与Ridge在这里依然可以使用
from imblearn.pipeline import Pipeline
from sklearn.model_selection import cross_val_predict
pipeline = Pipeline([
('smote', SMOTE(random_state=0)),
('lasso', LogisticRegression(penalty='l1', solver='liblinear', max_iter=1000, random_state=0))
])
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
y_pred = cross_val_predict(pipeline, X, y, cv=cv)
print("SMOTE + Lasso Logistic Regression(L1)分类报告:n")
print(classification_report(y, y_pred, zero_division=0))
这其实也是其中调整的一种,只不过针对于class_weight这个超参数,进行了更精细化得调整
from imblearn.pipeline import Pipeline
from sklearn.model_selection import cross_val_predict
pipeline = Pipeline([
('smote', SMOTE(random_state=0)),
('lasso', LogisticRegression(class_weight={0: 1, 1: 50}))
])
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
y_pred = cross_val_predict(pipeline, X, y, cv=cv)
print("class_weight {0:1, 1:50} 分类报告:n")
print(classification_report(y, y_pred, zero_division=0))
class_weight={0: 1, 1: 50}
的含义:
这是一种牺牲准确率为代价,尽量不要漏掉任何一个少数类,所以表现就是少数类1
的precision很低,但是recall是非常高的。这就是所谓的宁可错杀一千,也绝不放过一个
在逻辑回归中,针对类别不平衡的问题,往往有两种决策
至此,本文结束
在下才疏学浅,有撒汤漏水的,请各位不吝赐教...
本文来自博客园,作者:it排球君,转载请注明原文链接:https://www.cnblogs.com/MrVolleyball/p/19071731
参与评论
手机查看
返回顶部