From 7f57420901cface2f180b57a006b74bb74204d98 Mon Sep 17 00:00:00 2001 From: gitea_eternal <401029566@qq.com> Date: Mon, 27 Apr 2026 21:48:01 +0800 Subject: [PATCH] Upload README.md --- README.md | 603 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 601 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d72abb3..227745e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,602 @@ -# task-3-2-2-text-classification +# 文本分类实战 - 课堂讲义 -文本分类实战 - 纯NumPy实现 \ No newline at end of file +> 本项目用**纯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 │ + │ (加载预测) │ + └───────────────┘ +```