Files
task-3-2-2-text-classification/README.md
2026-04-27 22:00:18 +08:00

603 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 文本分类实战 - 课堂讲义
> 本项目用**纯NumPy**实现文本分类,帮助学生理解文本向量化和神经网络的基本原理。
>
> 类比MNIST图像→ 全连接网络 → 数字分类,本项目是文本版。
---
## 目录
1. [实验概述](#1-实验概述)
2. [数据预处理:如何让计算机"读懂"文本](#2-数据预处理如何让计算机读懂文本)
3. [向量化方法BoW 与 TF-IDF](#3-向量化方法bow-与-tf-idf)
4. [模型一逻辑回归Logistic Regression](#4-模型一逻辑回归logistic-regression)
5. [模型二多层感知机MLP](#5-模型二多层感知机mlp)
6. [训练过程:梯度下降与反向传播](#6-训练过程梯度下降与反向传播)
7. [数据不平衡问题与解决](#7-数据不平衡问题与解决)
8. [实验操作指南](#8-实验操作指南)
9. [预测新文本](#9-预测新文本)
---
## 1. 实验概述
### 1.1 任务
对中文酒店评论进行**情感分类**
- 正面评论(好评)
- 负面评论(差评)
### 1.2 数据集
**ChnSentiCorp**(中文酒店评论数据集)
- 总评论数7765条
- 正面评论5322条68.5%
- 负面评论2443条31.5%
数据集已内置,程序会自动下载。
### 1.3 整体流程
```
原始文本 → 分词 → 向量化 → 模型训练 → 预测
"酒店很好" → ["酒店", "很好"] → [0.3, 0.8, ...] → 正面
```
### 1.4 代码文件
| 文件 | 作用 |
|-----|------|
| `config.py` | 所有超参数配置(改这里来调整实验) |
| `dataset.py` | 数据加载、分词、向量化 |
| `model_numpy.py` | 逻辑回归和MLP模型实现 |
| `train.py` | 训练和对比实验 |
| `predict.py` | 加载模型预测新文本 |
---
## 2. 数据预处理:如何让计算机"读懂"文本
### 2.1 为什么文本不能直接用于计算?
计算机只能处理数字,不能直接处理文字。我们需要把文本转换成数字向量。
### 2.2 分词
**原理**:把连续的中文文本切成离散的词。
```python
# 示例
文本: "酒店服务很好"
分词: ["酒店", "服务", "很好"]
```
本项目使用 `jieba` 库进行分词:
```python
import jieba
text = "酒店服务很好"
words = jieba.lcut(text)
print(words) # ['酒店', '服务', '很好']
```
**注意**:过滤掉单字(如"的"、"了"),因为信息量太少。
```python
words = [w for w in words if len(w) > 1] # 过滤单字
```
### 2.3 构建词表
**原理**:把所有评论中的词收集起来,编上序号。
```python
# 词表示例
{
"酒店": 0,
"服务": 1,
"很好": 2,
"房间": 3,
...
}
```
词表大小由 `MAX_FEATURES` 控制本项目设为3000只保留出现频率最高的3000个词。
---
## 3. 向量化方法BoW 与 TF-IDF
把分词后的文本转换成数字向量。
### 3.1 BoW词袋模型
**原理**:统计每个词出现的次数。
```
文本: "酒店 服务 很好 服务"
分词: ["酒店", "服务", "很好", "服务"]
词表: {"酒店":0, "服务":1, "很好":2, "不错":3, ...}
向量: [1, 2, 1, 0, ...] # 酒店出现1次服务出现2次很好出现1次
```
**代码位置**`dataset.py` 中的 `BoWVectorizer`
```python
class BoWVectorizer:
def transform(self, text):
words = tokenize(text)
vec = [0] * MAX_SEQ_LEN
for i, word in enumerate(words[:MAX_SEQ_LEN]):
if word in self.vocab:
vec[i] = 1 # 也可以用词频 tf[word]
return vec
```
**问题**:所有词权重相同,导致常见词(如"的"、"是")主导。
### 3.2 TF-IDF词频-逆文档频率)
**原理**:给每个词赋予重要程度权重。
```
TF(词频) = 词在本文中出现的次数
IDF(逆文档频率) = log(总文档数 / 包含该词的文档数)
TF-IDF = TF × IDF
```
**直观理解**
- 一个词在本文中出现越多 → TF越高 → 越重要
- 一个词在所有文档中越常见 → IDF越低 → 越不重要
```
例子:
- "酒店"在100篇评论中出现80篇 → IDF = log(100/80) ≈ 0.22
- "惊喜"在100篇评论中出现5篇 → IDF = log(100/5) ≈ 3.0
"惊喜"虽然少见但信息量大IDF更高
```
**代码位置**`dataset.py` 中的 `TFIDFVectorizer`
```python
class TFIDFVectorizer:
def transform(self, text):
words = tokenize(text)
tf = Counter(words) # 词频
tf_sum = len(words)
vec = [0.0] * MAX_SEQ_LEN
for i, word in enumerate(words[:MAX_SEQ_LEN]):
if word in self.vocab:
# TF × IDF
vec[i] = (tf[word] / tf_sum) * self.idf.get(word, 0)
return vec
```
### 3.3 两种方法对比
| 特性 | BoW | TF-IDF |
|-----|-----|--------|
| 公式 | 词频 | TF × IDF |
| 常见词权重 | 相同(偏高) | 降低 |
| 罕见词权重 | 相同(偏低) | 提升 |
| 计算复杂度 | 低 | 稍高 |
| 效果 | 一般 | 通常更好 |
---
## 4. 模型一逻辑回归Logistic Regression
### 4.1 模型结构
最简单的线性分类器:
```
输入 [batch, features]
线性变换: Z = X @ W + b
Softmax → 概率
输出 [batch, 2] # [负面概率, 正面概率]
```
### 4.2 线性变换
```python
Z = X @ W + b
# 例子:
# X: [1, 3000] (一个样本3000维特征)
# W: [3000, 2] (权重矩阵)
# b: [2] (偏置)
# Z: [1, 2] (输出 logits)
```
### 4.3 Softmax
把 logits 转换成概率和为1
```python
def softmax(x):
exp_x = np.exp(x - np.max(x)) # 减最大值防溢出
return exp_x / np.sum(exp_x, axis=1, keepdims=True)
# 示例
logits = [2.0, 1.0]
probs = softmax(logits)
# probs = [0.731, 0.269]
# 解释正面概率73.1%负面概率26.9%
```
### 4.4 代码实现
```python
class LogisticRegression:
def __init__(self, input_size, num_classes=2):
self.W = np.random.randn(input_size, num_classes) * 0.01
self.b = np.zeros(num_classes)
def forward(self, X):
z = X @ self.W + self.b
return softmax(z)
def backward(self, X, y):
# 梯度计算和参数更新
...
```
### 4.5 参数量
```
W: input_size × num_classes = 3000 × 2 = 6000
b: num_classes = 2
总计: 6002
```
---
## 5. 模型二多层感知机MLP
### 5.1 模型结构
比逻辑回归多了一层隐藏层和非线性激活:
```
输入 [batch, features]
线性变换: Z1 = X @ W1 + b1
ReLU激活: A1 = max(0, Z1)
线性变换: Z2 = A1 @ W2 + b2
Softmax → 概率
输出 [batch, 2]
```
### 5.2 ReLU激活函数
```python
def relu(x):
return np.maximum(0, x)
# 示例
relu([1, -2, 3, -1]) = [1, 0, 3, 0]
```
**作用**:引入非线性,让模型能学习复杂模式。
### 5.3 参数量
```
W1: input_size × hidden = 3000 × 64 = 192000
b1: hidden = 64
W2: hidden × num_classes = 64 × 2 = 128
b2: num_classes = 2
总计: 192194
```
### 5.4 与视觉CNN的类比
| 视觉(全连接) | 文本 |
|--------------|------|
| 输入: 784维像素 | 输入: 3000维词向量 |
| 隐藏层: 128神经元 | 隐藏层: 64神经元 |
| 输出: 10类数字 | 输出: 2类情感 |
| ReLU + Softmax | ReLU + Softmax |
---
## 6. 训练过程:梯度下降与反向传播
### 6.1 训练流程
```
for epoch in 轮数:
for batch in 数据:
1. 前向传播: 计算输出概率
2. 计算损失: CrossEntropy(probs, labels)
3. 反向传播: 计算梯度
4. 更新参数: W = W - lr × 梯度
```
### 6.2 损失函数:交叉熵
```python
def cross_entropy_loss(probs, y):
# probs: 预测概率
# y: 真实标签
loss = -np.log(probs[y]) # 正确类的概率越大,损失越小
return loss
```
### 6.3 梯度下降
```python
# 简单示例:单参数
loss = f(w) # 损失是参数的函数
gradient = (loss(w + epsilon) - loss(w)) / epsilon # 数值梯度
# 解析梯度
w = w - learning_rate * gradient
```
### 6.4 反向传播BP
链式法则,从后往前计算梯度:
```
损失 → Softmax → 线性变换 → ReLU → 线性变换 → 输入
链式求导
各层梯度 = 损失对各层参数的偏导
```
### 6.5 训练日志解读
```
Epoch 20/100 | Loss: 0.5844 | 训练准确率: 0.6851 | 测试准确率: 0.6864
│ │ │ │
│ │ │ └─ 测试集上的表现
│ │ └─ 训练集上的表现
│ └─ 损失值(越小越好)
└─ 当前轮数/总轮数
```
---
## 7. 数据不平衡问题与解决
### 7.1 问题
本数据集正负比例约 7:3模型可能"偷懒"
| 策略 | 结果 | 准确率 |
|-----|------|--------|
| 不使用技巧,总是预测正面 | 简单但无效 | 68.5%(假高分) |
| 使用类别权重,认真学习 | 难但有效 | 46%(真学习) |
### 7.2 类别权重
**原理**:给少数类更高的权重,让模型更"怕"漏判少数类。
```python
# 计算权重
n_samples = 7765 # 总样本数
n_pos = 5322 # 正面样本数
n_neg = 2443 # 负面样本数
weight_pos = n_samples / (2 * n_pos) = 0.73 # 正面权重(样本多,权重小)
weight_neg = n_samples / (2 * n_neg) = 1.59 # 负面权重(样本少,权重大)
# 梯度更新时
d_z[y] -= class_weight[y] # 负面样本的梯度更大
```
### 7.3 开关配置
`config.py` 中:
```python
USE_CLASS_WEIGHT = True # 开启类别权重
USE_CLASS_WEIGHT = False # 关闭(总是预测正面)
```
### 7.4 实验对比
| 配置 | 测试准确率 | 预测分布 | 说明 |
|-----|----------|---------|------|
| 关闭权重 | 68.6% | 全预测正面 | 模型偷懒 |
| 开启权重 | 46.4% | 有正有负 | 模型在学习 |
**结论**68%准确率是"假"高分46%是"真"学习。数据不平衡问题没有银弹。
---
## 8. 实验操作指南
### 8.1 安装依赖
```bash
pip install numpy jieba
```
### 8.2 训练模型
```bash
python main.py
```
### 8.3 修改配置
编辑 `config.py`
```python
# 选择模型
MODEL_TYPE = 'mlp' # 'lr' 或 'mlp'
VECTORIZER_TYPE = 'tfidf' # 'bow' 或 'tfidf'
# 开关类别权重
USE_CLASS_WEIGHT = True # 或 False
# 调整超参数
NUM_EPOCHS = 100 # 训练轮数
LEARNING_RATE = 0.05 # 学习率
HIDDEN_SIZE = 64 # MLP隐藏层大小
```
### 8.4 运行对比实验
```python
RUN_COMPARISON = True # 开启
```
会自动进行:
1. BoW vs TF-IDF 对比
2. LR vs MLP 对比
3. 学习率对比
4. 隐藏层大小对比
### 8.5 训练输出示例
```
============================================================
训练配置:
模型: MLP
向量: TF-IDF
学习率: 0.05
隐藏层大小: 64
训练轮数: 100
============================================================
类别权重: 正面=0.73, 负面=1.59
MLP: 100 -> 64 -> 2, 参数量: 6594
Epoch 20/100 | Loss: 0.6694 | 训练准确率: 0.4598 | 测试准确率: 0.4662
...
最终结果:
训练准确率: 0.4596
测试准确率: 0.4668
训练时间: 2.95秒
模型已保存: model_mlp_tfidf_weighted_0427_212802_*.npy
```
---
## 9. 预测新文本
### 9.1 使用方法
```bash
python predict.py
```
### 9.2 操作流程
```
1. 程序列出已保存的模型
2. 输入编号选择模型
3. 输入评论文本
4. 查看预测结果
```
### 9.3 示例
```
请选择模型编号 (1-1): 1
请输入评论文本: 酒店服务很好,环境也不错
预测结果: 正面
置信度: 99.7%
详细: 正面概率=99.7%, 负面概率=0.3%
请输入评论文本: 房间太小,卫生很差
预测结果: 负面
置信度: 85.2%
详细: 正面概率=14.8%, 负面概率=85.2%
```
### 9.4 权重文件命名
每次训练生成唯一的文件名:
```
model_mlp_tfidf_weighted_0427_212802_W1.npy
model_mlp_tfidf_weighted_0427_212802_b1.npy
model_mlp_tfidf_weighted_0427_212802_W2.npy
model_mlp_tfidf_weighted_0427_212802_b2.npy
```
文件名包含:模型类型、向量类型、权重开关、时间戳
---
## 10. 思考题
1. **向量化**为什么TF-IDF通常比BoW效果好
2. **模型复杂度**MLP比LR多了一层带来的优势是什么
3. **数据不平衡**68%准确率一定好吗?有什么陷阱?
4. **类别权重**:开启后准确率反而下降,这说明什么?
5. **调参实践**:学习率过大会怎样?隐藏层太小会怎样?
---
## 附录:完整代码流程图
```
┌─────────────┐
│ config.py │
│ (超参数) │
└──────┬──────┘
┌─────────────────────────────────────────┐
│ dataset.py │
│ ┌───────────┐ ┌──────────────────┐ │
│ │ 下载数据 │───▶│ TF-IDF/BoW向量化 │ │
│ └───────────┘ └────────┬─────────┘ │
│ │ │
└────────────────────────────┼────────────┘
┌────────────────┐
│ 特征向量 X │
│ 标签 y │
└────────┬───────┘
┌─────────────────────────────────────────┐
│ model_numpy.py │
│ ┌───────────────────────────────────┐ │
│ │ LogisticRegression / MLP │ │
│ │ - forward(): 前向传播 │ │
│ │ - backward(): 反向传播 │ │
│ │ - fit(): 训练循环 │ │
│ └───────────────────────────────────┘ │
└────────────────────────────┬────────────┘
┌────────────────┐
│ 保存权重 │
│ model_*.npy │
└────────┬───────┘
┌────────────────┐
│ predict.py │
│ (加载预测) │
└───────────────┘
```