5语音与语言处理(第3版)

逻辑回归

判别式模型

逻辑回归特征工程分类

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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))$
python
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 在逻辑回归中不是凸函数,会导致优化困难。交叉熵是凸函数,保证梯度下降能找到全局最优。

python
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})$$

python
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 每次只用一个(或一小批)样本更新参数:

python
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 * db

5.3 特征工程

逻辑回归的性能高度依赖特征的质量。在 NLP 中,常用的文本特征包括:

5.3.1 词袋模型(Bag of Words)

python
from sklearn.feature_extraction.text import CountVectorizer

vectorizer = CountVectorizer(max_features=10000)
X = vectorizer.fit_transform(texts)
# X 是一个稀疏矩阵,每行是一个文档的词频向量

5.3.2 TF-IDF

python
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 特征

python
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 正则化使部分权重精确为零,具有特征选择的效果。

python
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}}}$$

python
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 文本情感分析

python
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 的完整分类管道

python
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 特征工程对比实验

python
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 对特征稀疏度和分类性能的影响。


5/12逻辑回归
0%