🤔 从线性回归的困惑说起
还记得我们用线性回归预测房价吗?它能告诉我们"120㎡的房子大概值180万"。
但是,如果我们想问:
- "这套房子值得买还是不值得买?"
- "这个用户会点击广告还是不会点击?"
- "这封邮件是垃圾邮件还是正常邮件?"
这些问题需要的答案是类别,而不是具体数字!
🎯 线性回归的困境
让我们试试直接用线性回归做分类:
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
# 模拟考试成绩数据:学习时间 vs 是否及格(1=及格,0=不及格)
np.random.seed(42)
study_hours = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
pass_exam = np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1]) # 及格=1,不及格=0
# 用线性回归拟合
lr_model = LinearRegression()
lr_model.fit(study_hours.reshape(-1, 1), pass_exam)
# 预测
hours_for_prediction = np.array([0.5, 1.5, 2.5, 3.5, 4.5, 5.5]).reshape(-1, 1)
predictions = lr_model.predict(hours_for_prediction)
print("学习时间 -> 线性回归预测值:")
for hour, pred in zip([0.5, 1.5, 2.5, 3.5, 4.5, 5.5], predictions):
print(f"{hour}小时 -> {pred:.3f}")
# 可视化
plt.figure(figsize=(10, 6))
plt.scatter(study_hours, pass_exam, color='blue', s=80, label='实际数据')
plt.plot(study_hours, lr_model.predict(study_hours.reshape(-1, 1)),
color='red', linewidth=2, label='线性回归')
# 添加分类边界线(y=0.5)
plt.axhline(y=0.5, color='green', linestyle='--', alpha=0.7, label='分类边界(0.5)')
plt.xlabel('学习时间(小时)')
plt.ylabel('是否及格(0=不及格, 1=及格)')
plt.title('线性回归做分类:预测值在0-1之间,但可能超出范围')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
输出结果:
学习时间 -> 线性回归预测值:
0.5小时 -> -0.050
1.5小时 -> 0.167
2.5小时 -> 0.383
3.5小时 -> 0.600
4.5小时 -> 0.817
5.5小时 -> 1.033
发现问题了!
- 预测值出现了负数(-0.050)和大于1的数(1.033)
- 但我们想要的是明确的0或1!
这就是我们需要逻辑回归的原因!
🎯 逻辑回归的核心思想
🔄 从直线到S曲线
逻辑回归不直接预测0或1,而是预测概率(0到1之间的数)。
关键洞察:用Sigmoid函数把直线"压缩"到0-1之间!
def sigmoid(z):
"""Sigmoid函数:把任何数映射到0-1之间"""
return 1 / (1 + np.exp(-z))
# 可视化Sigmoid函数
z_values = np.linspace(-10, 10, 100)
sigmoid_values = sigmoid(z_values)
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.plot(z_values, sigmoid_values, 'b-', linewidth=2)
plt.axvline(x=0, color='red', linestyle='--', alpha=0.7)
plt.axhline(y=0.5, color='red', linestyle='--', alpha=0.7)
plt.xlabel('z (线性组合)')
plt.ylabel('σ(z) (概率)')
plt.title('Sigmoid函数:把直线变成S曲线')
plt.grid(True, alpha=0.3)
plt.subplot(1, 2, 2)
plt.plot(z_values, sigmoid_values, 'b-', linewidth=2)
plt.fill_between(z_values, sigmoid_values, where=(z_values >= 0),
alpha=0.3, color='green', label='P≥0.5 (预测为1)')
plt.fill_between(z_values, sigmoid_values, where=(z_values < 0),
alpha=0.3, color='red', label='P<0.5 (预测为0)')
plt.axvline(x=0, color='black', linestyle='-', alpha=0.7)
plt.xlabel('z (线性组合)')
plt.ylabel('P (概率)')
plt.title('分类决策边界:z=0处')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("Sigmoid函数的一些值:")
print(f"sigmoid(-2) = {sigmoid(-2):.3f}")
print(f"sigmoid(-1) = {sigmoid(-1):.3f}")
print(f"sigmoid(0) = {sigmoid(0):.3f}")
print(f"sigmoid(1) = {sigmoid(1):.3f}")
print(f"sigmoid(2) = {sigmoid(2):.3f}")
📐 逻辑回归的数学表达
z = w₁x₁ + w₂x₂ + ... + b (还是线性组合)
P = σ(z) = 1/(1+e^(-z)) (Sigmoid转换)
预测规则:
- 如果 P ≥ 0.5,预测为类别1
- 如果 P < 0.5,预测为类别0
🛠️ 用Python实现逻辑回归
方法1:手工实现(理解原理)
class SimpleLogisticRegression:
def __init__(self, learning_rate=0.01, n_iterations=1000):
self.learning_rate = learning_rate
self.n_iterations = n_iterations
self.w = None
self.b = 0
self.loss_history = []
def sigmoid(self, z):
"""Sigmoid函数"""
# 防止溢出
z = np.clip(z, -250, 250)
return 1 / (1 + np.exp(-z))
def compute_loss(self, y_true, y_pred):
"""计算交叉熵损失"""
# 避免log(0)的情况
epsilon = 1e-15
y_pred = np.clip(y_pred, epsilon, 1 - epsilon)
return -np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred))
def fit(self, X, y):
"""训练模型"""
X = np.array(X)
y = np.array(y)
# 如果是单特征,转换成二维数组
if X.ndim == 1:
X = X.reshape(-1, 1)
n_samples, n_features = X.shape
self.w = np.zeros(n_features)
# 梯度下降
for i in range(self.n_iterations):
# 前向传播
linear_output = np.dot(X, self.w) + self.b
predictions = self.sigmoid(linear_output)
# 计算损失
loss = self.compute_loss(y, predictions)
self.loss_history.append(loss)
# 计算梯度
dw = (1/n_samples) * np.dot(X.T, (predictions - y))
db = (1/n_samples) * np.sum(predictions - y)
# 更新参数
self.w -= self.learning_rate * dw
self.b -= self.learning_rate * db
# 打印进度
if i % 200 == 0:
print(f"迭代 {i}: 损失 = {loss:.4f}")
def predict_proba(self, X):
"""预测概率"""
X = np.array(X)
if X.ndim == 1:
X = X.reshape(-1, 1)
linear_output = np.dot(X, self.w) + self.b
return self.sigmoid(linear_output)
def predict(self, X, threshold=0.5):
"""预测类别"""
probabilities = self.predict_proba(X)
return (probabilities >= threshold).astype(int)
# 用考试成绩数据训练逻辑回归
X_study = study_hours.reshape(-1, 1) # 学习时间
y_pass = pass_exam # 是否及格
log_reg = SimpleLogisticRegression(learning_rate=0.1, n_iterations=1000)
log_reg.fit(X_study, y_pass)
print(f"\n训练完成!")
print(f"权重 w: {log_reg.w[0]:.3f}")
print(f"偏置 b: {log_reg.b:.3f}")
print(f"决策边界: {log_reg.w[0]:.3f} × 学习时间 + {log_reg.b:.3f} = 0")
print(f"临界学习时间: {-log_reg.b/log_reg.w[0]:.2f}小时")
# 预测
predictions = log_reg.predict(hours_for_prediction)
probabilities = log_reg.predict_proba(hours_for_prediction)
print(f"\n逻辑回归预测结果:")
for hour, pred, prob in zip([0.5, 1.5, 2.5, 3.5, 4.5, 5.5], predictions, probabilities):
status = "及格" if pred == 1 else "不及格"
print(f"{hour}小时 -> 概率:{prob:.3f}, 预测:{status}")
方法2:使用sklearn(实际应用)
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
from sklearn.model_selection import train_test_split
# 使用sklearn的逻辑回归
sk_log_reg = LogisticRegression()
sk_log_reg.fit(X_study, y_pass)
print("=== sklearn逻辑回归结果 ===")
print(f"系数: {sk_log_reg.coef_[0][0]:.3f}")
print(f"截距: {sk_log_reg.intercept_[0]:.3f}")
# 预测
sk_predictions = sk_log_reg.predict(hours_for_prediction)
sk_probabilities = sk_log_reg.predict_proba(hours_for_prediction)[:, 1] # 取第二类的概率
print(f"\nsklearn预测结果:")
for hour, pred, prob in zip([0.5, 1.5, 2.5, 3.5, 4.5, 5.5], sk_predictions, sk_probabilities):
status = "及格" if pred == 1 else "不及格"
print(f"{hour}小时 -> 概率:{prob:.3f}, 预测:{status}")
# 模型评估
train_predictions = sk_log_reg.predict(X_study)
accuracy = accuracy_score(y_pass, train_predictions)
print(f"\n模型准确率: {accuracy:.3f}")
print(f"分类报告:\n{classification_report(y_pass, train_predictions)}")
# 可视化决策边界
plt.figure(figsize=(10, 6))
# 生成平滑的预测线
hours_smooth = np.linspace(0, 11, 100).reshape(-1, 1)
prob_smooth = sk_log_reg.predict_proba(hours_smooth)[:, 1]
plt.scatter(study_hours, pass_exam, color='blue', s=80, label='实际数据')
plt.plot(hours_smooth, prob_smooth, 'red', linewidth=2, label='逻辑回归概率曲线')
plt.axhline(y=0.5, color='green', linestyle='--', alpha=0.7, label='决策边界(P=0.5)')
plt.axvline(x=-sk_log_reg.intercept_[0]/sk_log_reg.coef_[0][0],
color='orange', linestyle='--', alpha=0.7,
label=f'临界学习时间({(-sk_log_reg.intercept_[0]/sk_log_reg.coef_[0][0]):.2f}h)')
plt.xlabel('学习时间(小时)')
plt.ylabel('及格概率')
plt.title('逻辑回归:从线性到概率')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
🔍 深入理解:损失函数
📉 为什么不用均方误差?
如果用线性回归的均方误差来训练逻辑回归,会遇到问题:
def compare_loss_functions():
"""比较不同损失函数在逻辑回归中的表现"""
# 模拟数据
z = np.linspace(-3, 3, 100)
y_true = (z >= 0).astype(float) # 真实标签
# Sigmoid预测概率
y_pred = 1 / (1 + np.exp(-z))
# 均方误差
mse_loss = (y_true - y_pred)**2
# 交叉熵损失
epsilon = 1e-15
y_pred_clipped = np.clip(y_pred, epsilon, 1 - epsilon)
cross_entropy_loss = - (y_true * np.log(y_pred_clipped) +
(1 - y_true) * np.log(1 - y_pred_clipped))
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(z, mse_loss, 'b-', linewidth=2, label='均方误差')
plt.xlabel('z (线性输出)')
plt.ylabel('损失')
plt.title('均方误差在逻辑回归中的问题')
plt.legend()
plt.grid(True, alpha=0.3)
plt.subplot(1, 2, 2)
plt.plot(z, cross_entropy_loss, 'r-', linewidth=2, label='交叉熵损失')
plt.xlabel('z (线性输出)')
plt.ylabel('损失')
plt.title('交叉熵损失:完美的凸函数')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
compare_loss_functions()
交叉熵损失的优点:
- 凸函数:只有一个最小值,容易优化
- 概率意义明确:直接衡量概率分布的差异
- 梯度更大:当预测错误时,梯度更大,学习更快
🎯 多变量逻辑回归
现实中的分类问题通常有多个特征:
📊 乳腺癌分类示例
from sklearn.datasets import load_breast_cancer
from sklearn.preprocessing import StandardScaler
# 加载乳腺癌数据集
data = load_breast_cancer()
X = data.data # 特征:肿瘤的各个医学指标
y = data.target # 标签:0=恶性,1=良性
feature_names = data.feature_names
print(f"数据集信息:")
print(f"样本数: {X.shape[0]}")
print(f"特征数: {X.shape[1]}")
print(f"良性样本: {np.sum(y==1)}, 恶性样本: {np.sum(y==0)}")
# 标准化特征(逻辑回归对特征尺度敏感)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# 分割训练和测试集
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, test_size=0.2, random_state=42)
# 训练逻辑回归
multi_log_reg = LogisticRegression(random_state=42)
multi_log_reg.fit(X_train, y_train)
# 预测和评估
y_pred = multi_log_reg.predict(X_test)
y_pred_proba = multi_log_reg.predict_proba(X_test)[:, 1] # 良性概率
accuracy = accuracy_score(y_test, y_pred)
print(f"\n多变量逻辑回归结果:")
print(f"测试集准确率: {accuracy:.3f}")
# 查看最重要的特征
feature_importance = abs(multi_log_reg.coef_[0])
top_features_idx = np.argsort(feature_importance)[-5:] # 最重要的5个特征
print(f"\n最重要的5个特征:")
for idx in reversed(top_features_idx):
print(f"{feature_names[idx]}: {feature_importance[idx]:.3f}")
# 单个样本的预测解释
sample_idx = 0
sample_features = X_test[sample_idx]
sample_true = y_test[sample_idx]
sample_pred = y_pred[sample_idx]
sample_prob = y_pred_proba[sample_idx]
print(f"\n样本预测解释:")
print(f"真实标签: {'良性' if sample_true == 1 else '恶性'}")
print(f"预测标签: {'良性' if sample_pred == 1 else '恶性'}")
print(f"良性概率: {sample_prob:.3f}")
print(f"预测可信度: {'高' if max(sample_prob, 1-sample_prob) > 0.8 else '低'}")
if sample_prob > 0.5:
confidence = sample_prob
print(f"模型很有信心这是良性肿瘤 (置信度: {confidence:.1%})")
else:
confidence = 1 - sample_prob
print(f"模型很有信心这是恶性肿瘤 (置信度: {confidence:.1%})")
🧠 逻辑回归的决策边界
🎨 可视化二维特征的决策边界
from sklearn.datasets import make_classification
# 生成二维分类数据
X_2d, y_2d = make_classification(n_samples=200, n_features=2, n_redundant=0,
n_informative=2, n_clusters_per_class=1,
random_state=42)
# 训练逻辑回归
log_reg_2d = LogisticRegression()
log_reg_2d.fit(X_2d, y_2d)
# 可视化决策边界
plt.figure(figsize=(10, 8))
# 创建网格点
x_min, x_max = X_2d[:, 0].min() - 1, X_2d[:, 0].max() + 1
y_min, y_max = X_2d[:, 1].min() - 1, X_2d[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.02),
np.arange(y_min, y_max, 0.02))
# 预测网格点
Z = log_reg_2d.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
# 绘制决策边界和数据点
plt.contourf(xx, yy, Z, alpha=0.4, cmap=plt.cm.RdYlBu)
plt.contour(xx, yy, Z, colors='black', linewidths=0.5, alpha=0.5)
scatter = plt.scatter(X_2d[:, 0], X_2d[:, 1], c=y_2d, cmap=plt.cm.RdYlBu,
edgecolors='black', s=50)
plt.xlabel('特征1')
plt.ylabel('特征2')
plt.title('逻辑回归的决策边界\n(不同颜色代表不同类别,黑色线条是决策边界)')
plt.colorbar(scatter)
plt.grid(True, alpha=0.3)
plt.show()
print("决策边界方程:")
print(f"{log_reg_2d.coef_[0][0]:.3f} × 特征1 + {log_reg_2d.coef_[0][1]:.3f} × 特征2 + {log_reg_2d.intercept_[0]:.3f} = 0")
⚠️ 逻辑回归的局限性
1️⃣ 只能处理线性可分问题
# 线性不可分的例子:螺旋数据
from sklearn.datasets import make_moons
X_moons, y_moons = make_moons(n_samples=200, noise=0.1, random_state=42)
# 训练逻辑回归
log_reg_moons = LogisticRegression()
log_reg_moons.fit(X_moons, y_moons)
# 可视化
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.scatter(X_moons[:, 0], X_moons[:, 1], c=y_moons, cmap=plt.cm.RdYlBu,
edgecolors='black', s=30)
plt.title('螺旋数据(线性不可分)')
# 决策边界
x_min, x_max = X_moons[:, 0].min() - 0.5, X_moons[:, 0].max() + 0.5
y_min, y_max = X_moons[:, 1].min() - 0.5, X_moons[:, 1].max() + 0.5
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.02),
np.arange(y_min, y_max, 0.02))
Z = log_reg_moons.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
plt.contourf(xx, yy, Z, alpha=0.4, cmap=plt.cm.RdYlBu)
plt.contour(xx, yy, Z, colors='black', linewidths=0.5)
plt.title('逻辑回归决策边界(无法很好分割螺旋数据)')
plt.tight_layout()
plt.show()
moons_accuracy = accuracy_score(y_moons, log_reg_moons.predict(X_moons))
print(f"螺旋数据分类准确率: {moons_accuracy:.3f} (不理想!)")
2️⃣ 概率校准问题
逻辑回归给出的概率有时不够准确,特别是在数据分布不均匀时。
🤖 逻辑回归的实际应用
📧 垃圾邮件分类器
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.pipeline import Pipeline
# 模拟邮件数据
emails = [
"恭喜您中奖了!点击链接领取100万大奖",
"明天上午10点开会,请准时参加",
"限时优惠!全场商品5折,仅此一天",
"项目进度报告已完成,请查收附件",
"免费领取iPhone!无需付款直接邮寄",
"会议纪要:讨论了Q4的销售策略"
]
labels = [1, 0, 1, 0, 1, 0] # 1=垃圾邮件,0=正常邮件
# 创建文本分类管道
spam_pipeline = Pipeline([
('tfidf', TfidfVectorizer()), # 文本转换为数字特征
('classifier', LogisticRegression()) # 逻辑回归分类器
])
spam_pipeline.fit(emails, labels)
# 测试新邮件
test_emails = [
"周末聚餐,地点在公司楼下餐厅",
"紧急通知:您的账户异常,立即点击验证",
"周报已发送,请各部门负责人查阅"
]
predictions = spam_pipeline.predict(test_emails)
probabilities = spam_pipeline.predict_proba(test_emails)
print("垃圾邮件分类结果:")
for email, pred, prob in zip(test_emails, predictions, probabilities):
category = "垃圾邮件" if pred == 1 else "正常邮件"
confidence = max(prob) * 100
print(f"邮件: {email}")
print(f"分类: {category} (置信度: {confidence:.1f}%)")
print("-" * 50)
📝 本篇小结
- 逻辑回归解决分类问题:预测类别而不是连续数值
- Sigmoid函数是核心:把线性输出压缩到0-1之间变成概率
- 决策边界:当线性组合等于0时,概率为0.5,这是分类的分界线
- 交叉熵损失:专门为分类问题设计的损失函数
- 概率输出:不仅告诉类别,还告诉把握有多大
- 可解释性强:权重告诉我们每个特征的重要性
- 局限性:只能处理线性可分问题,复杂模式需要神经网络
🎯 练习题
- 手推公式:给定数据点 (1,0), (2,0), (3,1), (4,1),试着手工推导逻辑回归的参数
- 实际应用:收集一些电影评论,用逻辑回归判断评论是正面还是负面
- 阈值调整:在医疗诊断中,我们可能希望更严格的标准(比如P>0.8才算患病),试试调整决策阈值
- 特征工程:为逻辑回归创造新的特征组合,看看能否提升分类效果
🔮 下一篇预告
第6篇:决策树——像做选择题一样做预测
逻辑回归虽然强大,但它假设特征和结果之间是线性关系。现实中很多决策过程更像是一连串的"如果...那么...":
- 如果年龄>30岁 AND 收入>50万 → 高价值客户
- 如果肿瘤大小>5cm OR 形状不规则 → 疑似恶性
决策树就是模仿人类这种做选择题的思维方式的算法!它将学习出一套"决策规则",让我们像剥洋葱一样一步步得到答案。
准备好学习AI如何像人类专家一样做决策了吗? 🌳
逻辑回归:分类与概率预测

966

被折叠的 条评论
为什么被折叠?



