本章目录
第二章 逻辑回归与分类问题
2.1 分类问题的数学表示与逻辑回归原理
2.1.1 分类问题的数学表示
在数学上,二元分类问题可以表示为:给定输入变量 X,预测输出变量 Y,其中 Y 只能取两个值,通常用 0 和 1 表示。例如,在银行客户违约预测问题中:
- X 可能包括客户的年龄、收入、信用记录等特征
- Y = 1 表示客户可能违约
- Y = 0 表示客户可能不会违约
Python代码实现:
import numpy as np
# 示例数据
X = np.array([[25, 50000, 5], # 年龄,收入,信用年限
[35, 80000, 10],
[45, 60000, 15]])
y = np.array([0, 0, 1]) # 0: 不违约, 1: 违约
2.1.2 从线性回归到逻辑回归
逻辑回归的核心思想可以分为两步:
- 首先,我们像线性回归一样,计算特征的加权和: z = w₀ + w₁x₁ + w₂x₂ + ... + wₙxₙ 其中 w₀ 是偏置项,w₁, w₂, ..., wₙ 是权重,x₁, x₂, ..., xₙ 是特征。
- 然后,我们将这个加权和输入到一个称为 "sigmoid 函数" 的特殊函数中: σ(z) = 1 / (1 + e⁻ᶻ)
Python代码实现:
def sigmoid(z):
z = np.clip(z, -500, 500) # 避免指数溢出
return 1 / (1 + np.exp(-z))
def logistic_regression(X, weights, bias):
z = np.dot(X, weights) + bias
return sigmoid(z)
# 示例
weights = np.array([0.01, -0.000005, 0.1]) # 为简单起见,我们手动设置权重
bias = -2
predictions = logistic_regression(X, weights, bias)
print("Predictions:", predictions)
历史小知识:
逻辑回归的核心——sigmoid函数有着悠久的历史。比利时数学家Pierre François Verhulst于1838年提出人口增长模型,并在1845年系统阐述逻辑斯蒂曲线。Joseph Berkson于1944年在生物测定研究中推广logit模型。这个例子说明,数学工具常会在跨学科应用中获得新的用途。
sigmoid 函数有一个非常重要的特性:无论输入是什么,它的输出总是在 0 和 1 之间。这使得它非常适合用于二元分类问题,其输出可以解释为属于某一类别的概率。
2.1.3 决策边界
在二维平面上,决策边界是一条线(在高维空间中可能是超平面),它将两个类别分开。对于逻辑回归,决策边界就是满足以下等式的点集:
w₀ + w₁x₁ + w₂x₂ + ... + wₙxₙ = 0
在采用默认分类阈值0.5时,边界上的sigmoid输入为0、输出为0.5。边界一边的点被分为一类,另一边的点被分为另一类;如果根据业务代价调整阈值,决策边界也会相应变化。
Python代码实现(以二维为例):
import matplotlib.pyplot as plt
def plot_decision_boundary(X, labels, weights, bias):
# 获取数据的最小和最大x和y值
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
# 生成网格点
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 200),
np.linspace(y_min, y_max, 200))
# 计算每个点的预测值
Z = logistic_regression(np.c_[xx.ravel(), yy.ravel()], weights[:2], bias)
Z = Z.reshape(xx.shape)
# 绘制决策边界
plt.contour(xx, yy, Z, levels=[0.5], colors='r')
plt.scatter(X[:, 0], X[:, 1], c=labels)
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.title('Decision Boundary')
plt.show()
# 示例(使用前两个特征)
plot_decision_boundary(X[:, :2], y, weights[:2], bias)
2.1.4 模型训练:最大似然估计
在逻辑回归中,我们通常使用最大似然估计(Maximum Likelihood Estimation, MLE)来找到最优参数。简单来说,我们寻找一组参数,使得在这组参数下,观察到当前训练数据的概率最大。
历史小知识:
最大似然估计方法由著名统计学家R.A. Fisher在20世纪初提出。Fisher的工作为现代统计学和机器学习奠定了基础。有趣的是,Fisher也是上文提到的将logistic模型应用于分类问题的统计学家之一。他的贡献展示了如何将复杂的数学理论转化为解决实际问题的有力工具。
对于二元分类问题,似然函数可以写成:
L(w,b) = ∏ᵢ pᵢʸᶦ (1-pᵢ)¹⁻ʸᶦ
其中 $p_i=P(Y_i=1\mid x_i;w,b)$ 是模型预测样本 $x_i$ 属于正类的条件概率,$y_i$ 是真实标签(0或1)。
实际上,我们通常最大化对数似然,这样可以将乘法转换为加法,便于计算:
log L(w,b) = Σᵢ [yᵢ log pᵢ + (1-yᵢ) log(1-pᵢ)]
注:想要深入了解为什么使用对数似然,请参见本章节末尾的"延伸阅读:为什么使用对数似然"。
为什么使用对数似然?
使用对数似然有几个重要的原因:
- 将乘法转换为加法:这使得计算更快、更精确。
- 防止数值下溢:避免连续相乘导致的极小数值问题。
- 简化导数计算:在优化过程中,对数函数的导数通常比原函数的导数更简单。
- 保持单调性:最大化对数似然与最大化似然会得到相同的结果。
Python代码实现:
def log_likelihood(X, y, weights, bias):
z = np.dot(X, weights) + bias
predictions = sigmoid(z)
predictions = np.clip(predictions, 1e-15, 1 - 1e-15)
return np.sum(y * np.log(predictions) + (1 - y) * np.log(1 - predictions))
# 使用梯度下降优化参数
def train_logistic_regression(X, y, learning_rate=0.01, num_iterations=1000):
m, n = X.shape
weights = np.zeros(n)
bias = 0
for _ in range(num_iterations):
z = np.dot(X, weights) + bias
predictions = sigmoid(z)
# 计算梯度
dw = (1/m) * np.dot(X.T, (predictions - y))
db = (1/m) * np.sum(predictions - y)
# 更新参数
weights -= learning_rate * dw
bias -= learning_rate * db
return weights, bias
# 标准化示例特征;实际项目中标准化参数只能由训练集估计
X_mean = X.mean(axis=0)
X_std = X.std(axis=0)
X_scaled = (X - X_mean) / X_std
# 训练模型
trained_weights, trained_bias = train_logistic_regression(X_scaled, y)
print("Trained weights:", trained_weights)
print("Trained bias:", trained_bias)
# 评估模型
final_predictions = logistic_regression(X_scaled, trained_weights, trained_bias)
print("Final predictions:", final_predictions)
print("Actual labels:", y)
通过理解这些基本原理并实现相应的代码,我们不仅能更好地应用逻辑回归模型,还能为学习更复杂的机器学习算法打下坚实的基础。这种理论与实践相结合的方法能帮助我们深入理解算法的工作原理,并在实际问题中灵活运用。
2.2 逻辑回归的实现与优化
在上一节中,我们学习了逻辑回归的基本原理。现在,让我们深入探讨如何实现和优化逻辑回归模型。
2.2.1 基本实现
首先,我们将使用 NumPy 库实现一个基本的逻辑回归模型:
import numpy as np
class LogisticRegression:
def __init__(self, learning_rate=0.01, num_iterations=1000):
self.learning_rate = learning_rate
self.num_iterations = num_iterations
self.weights = None
self.bias = None
def sigmoid(self, z):
z = np.clip(z, -500, 500)
return 1 / (1 + np.exp(-z))
def fit(self, X, y):
num_samples, num_features = X.shape
self.weights = np.zeros(num_features)
self.bias = 0
for _ in range(self.num_iterations):
linear_model = np.dot(X, self.weights) + self.bias
y_predicted = self.sigmoid(linear_model)
dw = (1 / num_samples) * np.dot(X.T, (y_predicted - y))
db = (1 / num_samples) * np.sum(y_predicted - y)
self.weights -= self.learning_rate * dw
self.bias -= self.learning_rate * db
def predict(self, X):
linear_model = np.dot(X, self.weights) + self.bias
y_predicted = self.sigmoid(linear_model)
return (y_predicted >= 0.5).astype(int)
这个实现使用了梯度下降算法来优化模型参数。在每次迭代中,我们计算预测值和真实值之间的差异,然后更新权重和偏置。
历史小知识: 梯度下降算法的历史可以追溯到 19 世纪。它最初由法国数学家 Augustin-Louis Cauchy 在 1847 年提出。然而,直到计算机时代的到来,这种算法才在机器学习中得到广泛应用。这再次说明了纯数学理论和实际应用之间可能存在的时间差。
2.2.2 损失函数
为了评估模型的性能,我们需要一个损失函数。对于逻辑回归,我们通常使用对数损失函数(Log Loss),也称为交叉熵损失:
def log_loss(self, y_true, y_pred):
epsilon = 1e-15
y_pred = np.clip(y_pred, epsilon, 1 - epsilon) # 避免 log(0)
return -np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred))
2.2.3 正则化
为了防止过拟合,我们通常会使用正则化技术。L1 和 L2 正则化是两种常见的方法。
L2正则化逻辑回归的实现:
class LogisticRegressionWithL2(LogisticRegression):
def __init__(self, learning_rate=0.01, num_iterations=1000, lambda_param=0.01):
super().__init__(learning_rate, num_iterations)
self.lambda_param = lambda_param
def fit(self, X, y):
num_samples, num_features = X.shape
self.weights = np.zeros(num_features)
self.bias = 0
for _ in range(self.num_iterations):
linear_model = np.dot(X, self.weights) + self.bias
y_predicted = self.sigmoid(linear_model)
dw = (1 / num_samples) * np.dot(X.T, (y_predicted - y)) + (self.lambda_param / num_samples) * self.weights
db = (1 / num_samples) * np.sum(y_predicted - y)
self.weights -= self.learning_rate * dw
self.bias -= self.learning_rate * db
注意在权重更新步骤中添加了正则化项。
2.2.4 特征缩放
在实际应用中,我们通常需要对特征进行缩放。这是因为不同特征可能有不同的范围,这可能导致某些特征主导模型的学习过程。
def fit_standardizer(X_train):
mean = X_train.mean(axis=0)
std = X_train.std(axis=0)
std[std == 0] = 1
return mean, std
def transform_features(X, mean, std):
return (X - mean) / std
2.2.5 模型评估
最后,我们实现一些评估指标来衡量模型的性能:
def accuracy(y_true, y_pred):
return np.mean(y_true == y_pred)
def precision(y_true, y_pred):
true_positives = np.sum((y_true == 1) & (y_pred == 1))
predicted_positives = np.sum(y_pred == 1)
return true_positives / predicted_positives if predicted_positives > 0 else 0
def recall(y_true, y_pred):
true_positives = np.sum((y_true == 1) & (y_pred == 1))
actual_positives = np.sum(y_true == 1)
return true_positives / actual_positives if actual_positives > 0 else 0
def f1_score(y_true, y_pred):
prec = precision(y_true, y_pred)
rec = recall(y_true, y_pred)
return 2 * (prec * rec) / (prec + rec) if (prec + rec) > 0 else 0
这些指标帮助我们从不同角度评估模型性能。准确率给出正确预测的比例,精确率衡量预测为正的样本中实际为正的比例,召回率衡量实际为正的样本中被正确预测的比例,而F1分数是精确率和召回率的调和平均值。对于类别不平衡或误判代价不同的问题,还应结合混淆矩阵并根据业务代价选择阈值。
通过理解和实现这些技术,我们不仅能够构建有效的逻辑回归模型,还为理解和实现更复杂的机器学习算法打下了坚实的基础。在下一节中,我们将探讨如何将这些知识应用于实际问题。
2.3 案例研究:使用逻辑回归构建植物健康监测系统
在这个案例研究中,我们将结合树莓派、摄像头和逻辑回归算法,构建一个简单的植物健康监测系统。这个系统可以帮助家庭园艺爱好者或小型温室经营者及时发现植物的健康问题。
2.3.1 项目概述
我们的目标是创建一个系统,它可以:
- 使用树莓派和摄像头定期拍摄植物照片
- 分析这些照片以检测植物是否健康
- 如果检测到问题,通过LED指示灯或者发送通知来提醒用户
这是一个二元分类问题,我们将使用逻辑回归来区分健康和不健康的植物。
2.3.2 硬件设置
所需硬件:
- 树莓派(如Raspberry Pi 4)
- 树莓派摄像头模块
- LED指示灯(红色和绿色)
- 220—330Ω限流电阻两个(每只LED一个)
- 面包板和跳线
硬件连接:
- 将摄像头模块连接到树莓派的摄像头端口
- 在面包板上连接LED灯,红色LED经限流电阻连接到GPIO端口18,绿色LED经限流电阻连接到GPIO端口23。不要将LED直接连接到GPIO引脚。
2.3.3 数据收集
首先,我们需要收集训练数据。我们可以编写一个Python脚本在树莓派上定期拍摄植物照片:
import time
from datetime import datetime
from pathlib import Path
from picamera2 import Picamera2
image_dir = Path("/home/pi/plant_images")
image_dir.mkdir(parents=True, exist_ok=True)
camera = Picamera2()
camera.configure(camera.create_still_configuration(main={"size": (1024, 768)}))
camera.start()
def capture_image():
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = image_dir / f"plant_{timestamp}.jpg"
camera.capture_file(str(filename))
print(f"已拍摄 {filename}")
return filename
# 每小时拍摄一次照片
while True:
capture_image()
time.sleep(3600) # 等待1小时
收集足够的数据后(包括健康和不健康的植物照片),需要人工复核并建立 labels.csv。文件至少包含 filename,label 两列,其中健康记为1、不健康记为0。不要根据采集文件名自动推断标签。
2.3.4 图像处理和特征提取
接下来,我们需要从图像中提取特征:
import cv2
import numpy as np
import pandas as pd
from pathlib import Path
def extract_features(image_path):
image = cv2.imread(str(image_path))
if image is None:
raise ValueError(f"无法读取图像: {image_path}")
image = cv2.resize(image, (224, 224)) # 调整图像大小为固定尺寸
hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
# 计算绿色区域的比例
lower_green = np.array([35, 50, 50])
upper_green = np.array([85, 255, 255])
green_mask = cv2.inRange(hsv_image, lower_green, upper_green)
green_ratio = np.mean(green_mask > 0)
# 计算平均色调和饱和度
avg_hue = np.mean(hsv_image[:,:,0])
avg_saturation = np.mean(hsv_image[:,:,1])
return [green_ratio, avg_hue, avg_saturation]
# 按人工标签表处理所有图像
image_dir = Path("/home/pi/plant_images")
labels = pd.read_csv(image_dir / "labels.csv")
label_by_name = dict(zip(labels["filename"], labels["label"]))
X = []
y = []
for image_path in sorted(image_dir.glob("*.jpg")):
if image_path.name not in label_by_name:
continue
X.append(extract_features(image_path))
y.append(label_by_name[image_path.name])
X = np.array(X, dtype=np.float32)
y = np.array(y, dtype=np.float32)
2.3.5 模型训练
现在使用TensorFlow/Keras创建单层逻辑回归模型。若照片来自多株植物,应优先按植物编号分组划分数据,避免同一植物的相邻照片同时出现在训练集和测试集中。下面用分层随机划分演示基本流程:
import tensorflow as tf
from sklearn.model_selection import train_test_split
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Normalization, Dense
from tensorflow.keras.optimizers import Adam
# 分割训练集、验证集和测试集
X_train, X_temp, y_train, y_temp = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
X_val, X_test, y_val, y_test = train_test_split(
X_temp, y_temp, test_size=0.5, random_state=42, stratify=y_temp
)
# 标准化参数只由训练集估计,并随模型一同保存
normalizer = Normalization(axis=-1)
normalizer.adapt(X_train)
# 单个sigmoid输出单元即逻辑回归;没有隐藏层
model = Sequential([
Input(shape=(3,)),
normalizer,
Dense(1, activation='sigmoid')
])
# 编译模型
model.compile(optimizer=Adam(learning_rate=0.01),
loss='binary_crossentropy',
metrics=['accuracy'])
# 训练模型
early_stopping = tf.keras.callbacks.EarlyStopping(
monitor='val_loss', patience=10, restore_best_weights=True
)
history = model.fit(
X_train, y_train,
validation_data=(X_val, y_val),
epochs=100,
batch_size=32,
callbacks=[early_stopping],
verbose=0,
)
# 保存模型
model.save('/home/pi/plant_health_model.keras')
2.3.6 实时监测系统
最后,我们可以创建一个实时监测系统:
import RPi.GPIO as GPIO
import tensorflow as tf
# 加载保存的模型
loaded_model = tf.keras.models.load_model('/home/pi/plant_health_model.keras')
# 设置GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setup(18, GPIO.OUT) # 红色LED
GPIO.setup(23, GPIO.OUT) # 绿色LED
def monitor_plant():
# 拍摄照片
latest_image = capture_image()
# 提取刚拍摄图像的特征
features = extract_features(latest_image)
# 进行预测
prediction = loaded_model.predict(np.array([features]), verbose=0)[0, 0]
# 根据预测结果控制LED
if prediction > 0.5:
GPIO.output(23, GPIO.HIGH) # 绿灯亮
GPIO.output(18, GPIO.LOW) # 红灯灭
print("植物健康")
else:
GPIO.output(18, GPIO.HIGH) # 红灯亮
GPIO.output(23, GPIO.LOW) # 绿灯灭
print("植物可能需要照顾")
# 每小时监测一次
while True:
monitor_plant()
time.sleep(3600)
2.3.7 模型评估
最后在此前保留的测试集上评估一次模型:
# 评估模型
test_loss, test_accuracy = model.evaluate(X_test, y_test, verbose=0)
print(f"Test accuracy: {test_accuracy:.2f}")
# 对测试集进行预测
test_probabilities = model.predict(X_test, verbose=0).ravel()
predictions = (test_probabilities >= 0.5).astype(int)
# 查看混淆矩阵、精确率、召回率和F1分数
from sklearn.metrics import classification_report, confusion_matrix
print(confusion_matrix(y_test, predictions))
print(classification_report(y_test, predictions, digits=3))
这个基本系统可以通过多种方式进行改进:
- 使用更复杂的图像处理技术,如叶片面积计算或病斑检测。
- 整合环境传感器(如土壤湿度、光照强度)以获取更多特征。
- 实现远程监控,例如将结果发送到手机应用。
- 使用更高级的机器学习模型,如卷积神经网络,以提高准确性。
延伸阅读:计算机视觉在农业中的应用
在更大规模的农业场景中,类似方法还可扩展到无人机巡查、病斑检测和自动化温室监测。实际部署时需要覆盖不同光照、季节、相机和植物品种,并持续检查误报与漏报。
通过这个案例研究,我们不仅应用了逻辑回归来解决一个实际问题,还探索了如何将机器学习与硬件结合,创建一个实用的智能系统。这种项目不仅有趣,还能培养跨学科思维和实践能力。
2.4 总结与展望:逻辑回归与深度学习的桥梁
在本章中,我们深入探讨了逻辑回归这一强大而简洁的分类算法。从理论基础到实际应用,我们不仅学习了算法的核心概念,还通过 TensorFlow 实现了一个实际的项目。让我们回顾一下关键点,并展望未来的学习方向。
2.4.1 核心概念回顾
- 分类问题的本质 我们理解了分类问题与回归问题的区别,以及为什么需要特殊的算法来处理分类任务。
- Sigmoid 函数 我们学习了 Sigmoid 函数如何将线性输出转换为概率,这是逻辑回归的核心机制。
- 决策边界 我们探讨了决策边界的概念,理解了逻辑回归如何在特征空间中划分不同类别。
- 损失函数与优化 我们学习了二元交叉熵损失函数,以及如何使用梯度下降算法来优化模型参数。
- 正则化技术 我们讨论了 L1 和 L2 正则化,理解了如何防止模型过拟合。
- 模型评估 我们学习了准确率、精确率、召回率等评估指标,理解了在不同场景下选择合适指标的重要性。
2.4.2 TensorFlow 实现的意义
通过使用 TensorFlow 来实现逻辑回归,我们不仅学会了如何使用现代深度学习框架,还为未来学习更复杂的模型奠定了基础。这种方法帮助我们:
- 理解了深度学习框架的基本工作流程。
- 学会了如何处理数据、构建模型、训练和评估。
- 为后续学习更复杂的神经网络架构做好了准备。
2.4.3 逻辑回归在现代机器学习中的地位
尽管深度学习技术日新月异,逻辑回归仍然在许多场景下扮演着重要角色:
- 基准模型:在尝试更复杂的模型之前,逻辑回归常被用作基准。
- 可解释性:相比复杂的神经网络,逻辑回归的决策过程更容易理解和解释。
- 计算效率:对于某些简单的问题,逻辑回归可能就足够了,且计算成本低。
- 系数解释:在特征尺度可比、共线性受控且模型设定合理时,逻辑回归权重可以帮助解释各特征与对数优势之间的关系。
2.4.4 未来学习方向
- 多类分类问题 学习如何将逻辑回归扩展到多类问题,如 one-vs-rest 策略和 softmax 回归。
- 非线性特征和核方法 探索如何处理非线性决策边界,引入核方法的概念。
- 集成学习 了解如何将多个简单模型(如逻辑回归)组合成更强大的模型。
- 深度神经网络 将逻辑回归的概念扩展到多层神经网络,开始探索深度学习的世界。
- 高级优化算法 学习更复杂的优化算法,如 Adam、RMSprop 等,了解它们如何提高模型训练效率。
- 大规模机器学习 探索如何在大数据集上高效训练模型,学习分布式训练的概念。
2.4.5 实践建议
- 动手实践:尝试将本章学到的知识应用到不同的数据集和问题上。
- 参与竞赛:考虑参加 Kaggle 等平台的入门级竞赛,将所学付诸实践。
- 阅读论文:开始阅读一些经典的机器学习论文,了解算法的发展历史。
- 关注伦理:思考机器学习模型的社会影响,了解公平性和偏见等问题。
延伸阅读
- "Pattern Recognition and Machine Learning" by Christopher Bishop
- "The Elements of Statistical Learning" by Trevor Hastie, Robert Tibshirani, and Jerome Friedman
- TensorFlow 官方文档和教程
通过本章的学习,你已经掌握了机器学习的一个基础而重要的算法。逻辑回归不仅是一个强大的工具,更是理解更复杂算法的跳板。记住,每一个复杂的模型都建立在这些基础概念之上。保持好奇心和实践精神,你将在机器学习和人工智能的精彩世界中不断进步。