第5章:逻辑回归(Logistic Regression)
一、导读
1.1 本章学习目标
逻辑回归是 NLP 中最基础也最重要的分类算法之一。尽管名字里有"回归",但它实际上是一个分类算法。逻辑回归通过sigmoid函数将线性组合映射为概率,是理解神经网络和深度学习的重要基础。
本章的核心学习目标包括:
- 理解逻辑回归的数学原理:sigmoid函数、决策边界、概率输出
- 掌握逻辑回归的训练方法:最大似然估计、梯度下降、随机梯度下降
- 理解特征工程在逻辑回归中的重要性:如何将文本转换为特征向量
- 掌握正则化方法:L1、L2正则化如何防止过拟合
- 了解多分类扩展:softmax函数和多类逻辑回归
- 理解逻辑回归与朴素贝叶斯的关系和区别
1.2 为什么本章如此重要
逻辑回归在 NLP 的发展史上扮演了承前启后的角色。它比朴素贝叶斯更强大——放弃了条件独立假设,通过特征权重直接建模决策边界;同时它又是神经网络的基础——单个神经元就是一个逻辑回归单元。
在深度学习时代之前,逻辑回归(及其扩展)是文本分类、情感分析、命名实体识别等任务的主流方法。即使在今天,逻辑回归仍然在以下场景中发挥重要作用:
- 可解释性要求高的场景:医疗诊断、法律决策、金融风控
- 特征工程成熟的场景:当领域专家能够构造高质量特征时
- 资源受限的场景:训练和推理速度快,内存占用小
- 基线模型:评估更复杂模型的性能提升
更重要的是,理解逻辑回归是理解神经网络的必经之路。sigmoid函数、交叉熵损失、梯度下降——这些在逻辑回归中学到的概念,在深度学习中会被反复使用。
1.3 核心问题引导
逻辑回归如何用线性模型解决分类问题?sigmoid函数的作用是什么?
逻辑回归的损失函数是什么?为什么不用均方误差而用交叉熵?
梯度下降如何工作?学习率的选择有什么影响?
特征工程在逻辑回归中为什么如此重要?如何为文本任务构造特征?
L1正则化和L2正则化有什么区别?分别在什么场景下使用?
逻辑回归与朴素贝叶斯相比,有什么优势和劣势?
二、核心概念详解
5.1 从线性回归到逻辑回归
5.1.1 线性回归的局限
线性回归试图用线性函数拟合数据:$y = \mathbf{w}^T \mathbf{x} + b$。但线性回归的输出范围是 $(-\infty, +\infty)$,不适合分类任务(我们需要输出概率或类别标签)。
5.1.2 Sigmoid 函数
逻辑回归通过 sigmoid 函数将线性输出映射到 $(0, 1)$ 区间:
$$\sigma(z) = \frac{1}{1 + e^{-z}}$$
sigmoid 函数的性质:
- 输出范围:$(0, 1)$,可以解释为概率
- 单调递增:输入越大,输出越大
- 在 $z=0$ 处输出 $0.5$
- 导数简单:$\sigma'(z) = \sigma(z)(1 - \sigma(z))$
import numpy as np
import matplotlib.pyplot as plt
def sigmoid(z):
return 1 / (1 + np.exp(-z))
# 可视化 sigmoid 函数
z = np.linspace(-10, 10, 100)
plt.plot(z, sigmoid(z))
plt.xlabel("z")
plt.ylabel("sigmoid(z)")
plt.title("Sigmoid Function")
plt.grid(True)
plt.axhline(y=0.5, color="r", linestyle="--")
plt.axvline(x=0, color="g", linestyle="--")
plt.show()5.1.3 逻辑回归模型
逻辑回归模型:
$$P(y=1|\mathbf{x}) = \sigma(\mathbf{w}^T \mathbf{x} + b) = \frac{1}{1 + e^{-(\mathbf{w}^T \mathbf{x} + b)}}$$
$$P(y=0|\mathbf{x}) = 1 - P(y=1|\mathbf{x})$$
决策规则:如果 $P(y=1|\mathbf{x}) > 0.5$,则预测为正类。等价于 $\mathbf{w}^T \mathbf{x} + b > 0$。
5.2 训练逻辑回归
5.2.1 损失函数:交叉熵
逻辑回归使用二元交叉熵损失(Binary Cross-Entropy, BCE):
$$\mathcal{L}(\mathbf{w}) = -\frac{1}{N} \sum_{i=1}^{N} \left[ y_i \log \hat{y}_i + (1 - y_i) \log(1 - \hat{y}_i) \right]$$
其中 $\hat{y}_i = \sigma(\mathbf{w}^T \mathbf{x}_i)$。
为什么不用均方误差(MSE)?因为 MSE 在逻辑回归中不是凸函数,会导致优化困难。交叉熵是凸函数,保证梯度下降能找到全局最优。
def binary_cross_entropy(y_true, y_pred):
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))5.2.2 梯度下降
梯度下降通过迭代更新参数来最小化损失函数:
$$\mathbf{w} \leftarrow \mathbf{w} - \eta \nabla_{\mathbf{w}} \mathcal{L}$$
逻辑回归的梯度有简洁的形式:
$$\nabla_{\mathbf{w}} \mathcal{L} = \frac{1}{N} \mathbf{X}^T (\hat{\mathbf{y}} - \mathbf{y})$$
class LogisticRegression:
def __init__(self, learning_rate=0.01, n_iterations=1000, lambda_reg=0.0):
self.lr = learning_rate
self.n_iterations = n_iterations
self.lambda_reg = lambda_reg
self.weights = None
self.bias = None
def sigmoid(self, z):
return 1 / (1 + np.exp(-z))
def fit(self, X, y):
n_samples, n_features = X.shape
self.weights = np.zeros(n_features)
self.bias = 0
for iteration in range(self.n_iterations):
# 前向传播
linear_model = np.dot(X, self.weights) + self.bias
y_predicted = self.sigmoid(linear_model)
# 计算梯度
dw = (1 / n_samples) * np.dot(X.T, (y_predicted - y))
db = (1 / n_samples) * np.sum(y_predicted - y)
# L2 正则化
if self.lambda_reg > 0:
dw += (self.lambda_reg / n_samples) * self.weights
# 更新参数
self.weights -= self.lr * dw
self.bias -= self.lr * db
# 每 100 次迭代打印损失
if iteration % 100 == 0:
loss = binary_cross_entropy(y, y_predicted)
print(f"Iteration {iteration}, Loss: {loss:.4f}")
def predict_proba(self, X):
linear_model = np.dot(X, self.weights) + self.bias
return self.sigmoid(linear_model)
def predict(self, X, threshold=0.5):
y_predicted = self.predict_proba(X)
return (y_predicted >= threshold).astype(int)5.2.3 随机梯度下降(SGD)
当训练数据量很大时,批量梯度下降每次迭代需要计算所有样本的梯度,效率很低。SGD 每次只用一个(或一小批)样本更新参数:
class SGDLogisticRegression:
def __init__(self, learning_rate=0.01, batch_size=32, n_epochs=10):
self.lr = learning_rate
self.batch_size = batch_size
self.n_epochs = n_epochs
def fit(self, X, y):
n_samples, n_features = X.shape
self.weights = np.random.randn(n_features) * 0.01
self.bias = 0
for epoch in range(self.n_epochs):
# 打乱数据
indices = np.random.permutation(n_samples)
X_shuffled = X[indices]
y_shuffled = y[indices]
# 小批量更新
for i in range(0, n_samples, self.batch_size):
X_batch = X_shuffled[i:i+self.batch_size]
y_batch = y_shuffled[i:i+self.batch_size]
linear_model = np.dot(X_batch, self.weights) + self.bias
y_pred = 1 / (1 + np.exp(-linear_model))
dw = (1 / len(X_batch)) * np.dot(X_batch.T, (y_pred - y_batch))
db = (1 / len(X_batch)) * np.sum(y_pred - y_batch)
self.weights -= self.lr * dw
self.bias -= self.lr * db5.3 特征工程
逻辑回归的性能高度依赖特征的质量。在 NLP 中,常用的文本特征包括:
5.3.1 词袋模型(Bag of Words)
from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer(max_features=10000)
X = vectorizer.fit_transform(texts)
# X 是一个稀疏矩阵,每行是一个文档的词频向量5.3.2 TF-IDF
from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer(max_features=10000, sublinear_tf=True)
X = vectorizer.fit_transform(texts)
# TF-IDF 考虑了词频和逆文档频率5.3.3 N-gram 特征
vectorizer = TfidfVectorizer(ngram_range=(1, 2), max_features=20000)
X = vectorizer.fit_transform(texts)
# 同时使用 unigram 和 bigram 特征5.3.4 其他特征
- 词性分布特征
- 句法特征
- 情感词典特征
- 外部知识特征
5.4 正则化
5.4.1 L2 正则化(Ridge)
$$\mathcal{L}_{L2} = \mathcal{L} + \frac{\lambda}{2N} \|\mathbf{w}\|^2$$
L2 正则化使权重趋向较小但不为零的值,防止过拟合。
5.4.2 L1 正则化(Lasso)
$$\mathcal{L}_{L1} = \mathcal{L} + \frac{\lambda}{N} \|\mathbf{w}\|_1$$
L1 正则化使部分权重精确为零,具有特征选择的效果。
from sklearn.linear_model import LogisticRegression
# L2 正则化(默认)
clf_l2 = LogisticRegression(penalty="l2", C=1.0)
# L1 正则化
clf_l1 = LogisticRegression(penalty="l1", solver="liblinear", C=1.0)
# Elastic Net(L1 + L2)
clf_en = LogisticRegression(penalty="elasticnet", solver="saga", l1_ratio=0.5, C=1.0)5.5 多分类逻辑回归
对于多分类问题,使用 softmax 函数替代 sigmoid:
$$P(y=k|\mathbf{x}) = \frac{e^{\mathbf{w}_k^T \mathbf{x}}}{\sum_{j=1}^{K} e^{\mathbf{w}_j^T \mathbf{x}}}$$
from sklearn.linear_model import LogisticRegression
# 多分类逻辑回归(自动使用 One-vs-Rest 或 Multinomial)
clf = LogisticRegression(multi_class="multinomial", solver="lbfgs", max_iter=1000)
clf.fit(X_train, y_train)
predictions = clf.predict(X_test)三、实战案例
3.1 文本情感分析
from sklearn.datasets import load_files
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
import numpy as np
# 模拟数据
texts = [
"这部电影太好看了,强烈推荐",
"非常精彩的表演,值得一看",
"剧情紧凑,演员演技在线",
"浪费时间,烂片一部",
"失望透顶,完全不值得看",
"无聊至极,昏昏欲睡",
"温馨感人的故事,很喜欢",
"特效震撼,剧情引人入胜",
"导演水平太差,演员也不行",
"节奏拖沓,情节老套",
]
labels = [1, 1, 1, 0, 0, 0, 1, 1, 0, 0]
# 特征提取
vectorizer = TfidfVectorizer(max_features=5000, ngram_range=(1, 2))
X = vectorizer.fit_transform(texts)
# 训练
clf = LogisticRegression(C=1.0, max_iter=1000)
clf.fit(X, labels)
# 测试
test_texts = ["这部电影真的很棒", "完全看不下去,太差了"]
X_test = vectorizer.transform(test_texts)
for text, pred in zip(test_texts, clf.predict(X_test)):
print(f"'{text}' → {'正面' if pred == 1 else '负面'}")
# 分析最重要的特征
feature_names = vectorizer.get_feature_names_out()
for label, coef in zip(["负面", "正面"], clf.coef_):
top_positive = np.argsort(coef)[-5:][::-1]
top_negative = np.argsort(coef)[:5]
print(f"\n{label}类最重要的特征:")
print("正面: ", [feature_names[i] for i in top_positive])
print("负面: ", [feature_names[i] for i in top_negative])3.2 使用 scikit-learn 的完整分类管道
from sklearn.pipeline import Pipeline
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.datasets import fetch_20newsgroups
# 加载数据
categories = ["alt.atheism", "soc.religion.christian", "comp.graphics", "sci.med"]
train = fetch_20newsgroups(subset="train", categories=categories, random_state=42)
test = fetch_20newsgroups(subset="test", categories=categories, random_state=42)
# 构建管道
pipeline = Pipeline([
("tfidf", TfidfVectorizer(max_features=10000, stop_words="english")),
("clf", LogisticRegression(C=1.0, max_iter=1000)),
])
# 训练和评估
pipeline.fit(train.data, train.target)
accuracy = pipeline.score(test.data, test.target)
print(f"Accuracy: {accuracy:.4f}")
# 交叉验证
scores = cross_val_score(pipeline, train.data, train.target, cv=5)
print(f"CV Accuracy: {scores.mean():.4f} (+/- {scores.std():.4f})")3.3 特征工程对比实验
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
# 比较不同特征方法的效果
feature_methods = {
"Count (unigram)": CountVectorizer(max_features=5000),
"Count (bigram)": CountVectorizer(max_features=10000, ngram_range=(1, 2)),
"TF-IDF (unigram)": TfidfVectorizer(max_features=5000),
"TF-IDF (bigram)": TfidfVectorizer(max_features=10000, ngram_range=(1, 2)),
"TF-IDF (char)": TfidfVectorizer(max_features=10000, analyzer="char", ngram_range=(2, 5)),
}
for name, vectorizer in feature_methods.items():
X_train = vectorizer.fit_transform(train.data)
X_test = vectorizer.transform(test.data)
clf = LogisticRegression(max_iter=1000)
clf.fit(X_train, train.target)
acc = accuracy_score(test.target, clf.predict(X_test))
print(f"{name:25s} → Accuracy: {acc:.4f}")四、本章小结
本章系统讲解了逻辑回归:
模型原理:通过 sigmoid 函数将线性组合映射为概率,输出范围 (0, 1)。
训练方法:使用交叉熵损失函数和梯度下降优化。交叉熵是凸函数,保证找到全局最优。
特征工程:逻辑回归的性能高度依赖特征质量。TF-IDF 和 N-gram 是常用的文本特征。
正则化:L1 正则化具有特征选择效果,L2 正则化防止过拟合。
多分类扩展:使用 softmax 函数处理多分类问题。
与朴素贝叶斯的比较:逻辑回归放弃了条件独立假设,通常表现更好,但需要更多数据和更仔细的特征工程。
逻辑回归是连接传统机器学习和深度学习的桥梁。下一章我们将学习向量语义学,它将为文本表示带来革命性的变化。
五、思考与练习
思考题
为什么逻辑回归使用交叉熵而不是均方误差作为损失函数?从优化的角度解释。
L1 正则化为什么能产生稀疏解?从几何角度解释 L1 和 L2 的区别。
在文本分类中,逻辑回归相比朴素贝叶斯有什么优势?在什么场景下朴素贝叶斯可能更好?
学习率的选择对梯度下降有什么影响?太大或太小分别会导致什么问题?
编程练习
从零实现逻辑回归(不使用 sklearn),在二分类文本数据上训练。比较不同学习率下的收敛速度。
在 20 Newsgroups 数据集上比较逻辑回归和朴素贝叶斯的性能。分析在不同训练数据量下两者的表现差异。
实现带有 L1 正则化的逻辑回归,分析正则化参数 C 对特征稀疏度和分类性能的影响。