文本处理与RNN:硬核实战笔记

📖 导读

这份笔记覆盖NLP实战全流程:文本预处理、文本向量化、RNN/LSTM/GRU、注意力机制。

  • 零遗漏原则:完整覆盖所有关键代码
  • 显微镜式解析:每个参数、每个函数都讲清楚
  • 深度比喻:用生活化的例子解释抽象概念
  • 场景化映射:代码对应实际应用场景

🎯 今日目标

  1. 掌握文本预处理(分词、清洗)
  2. 理解文本表示方法(One-Hot、Word2Vec、Embedding)
  3. 学会RNN/LSTM/GRU序列模型

🗺️ 一、文本预处理:让机器读懂文字

1.1 分词 --- jieba

文本预处理的第一步,是把句子切成"词"。

python 复制代码
import jieba

text = "我爱自然语言处理技术"
精确模式(推荐)
python 复制代码
words = jieba.lcut(text)
print(words)
# ['我', '爱', '自然语言', '处理', '技术']

💡 代码解析

  • jieba.lcut():返回列表(不是生成器)
  • 精确模式:尽量把相关词连在一起
  • 输出:一个列表
全模式
python 复制代码
words_full = jieba.lcut(text, cut_all=True)
print(words_full)
# ['我', '爱', '自', '然', '语', '言', '处', '理', '技', '术']

💡 代码解析

  • cut_all=True:最细粒度拆分
  • 缺点:可能过于零碎
搜索引擎模式
python 复制代码
words_search = jieba.lcut_for_search(text)
print(words_search)
# ['我', '爱', '自然', '语言', '自然语言', '处理', '技术']

💡 代码解析

  • 先用精确模式,再合并长词
  • 适合:搜索引擎索引

📊 三种模式对比

模式 特点 适用场景
精确模式 连在一起 通用场景
全模式 最细分 词频统计
搜索引擎 折中 搜索索引

1.2 词性标注

告诉机器每个词是什么词性(名词、动词、代词...)

python 复制代码
import jieba.posseg as pseg

text = "我爱自然语言处理"
result = pseg.lcut(text)

for word, flag in result:
    print(f"{word}: {flag}")

# 输出:
# 我: r    (代词 pronoun)
# 爱: v    (动词 verb)
# 自然语言: n  (名词 noun)
# 处理: vn   (动词/名词 verb-noun)

💡 代码解析

  • jieba.posseg:分词+词性标注
  • 常见词性:n(名词)、v(动词)、r(代词)、a(形容词)、ad(副词)

1.3 自定义词典

jieba默认会把一些专有名词分开,需要手动添加。

python 复制代码
# 方式1:代码中添加
jieba.add_word("传智教育")
jieba.add_word("黑马程序员")

# 方式2:词典文件(更推荐)
# my_dict.txt 内容:
# 传智教育 10 n
# 黑马程序员 8 n

jieba.load_userdict("my_dict.txt")

text = "传智教育旗下有黑马程序员"
print(jieba.lcut(text))
# ['传智教育', '旗下', '有', '黑马程序员']

💡 代码解析

  • jieba.add_word():临时添加
  • load_userdict():从文件加载
  • 格式词语 词频 词性
  • 场景:公司名、品牌名、地名等专有名词

1.4 停用词处理

过滤掉"的、了、是"等无意义词。

python 复制代码
# 常用停用词列表
stopwords = {
    '的', '了', '是', '在', '我', '有', '和', '就',
    '不', '人', '都', '一', '一个', '上', '也', '很'
}

text = "我是一名程序员,在北京工作"
words = jieba.lcut(text)

# 过滤停用词
filtered = [w for w in words if w not in stopwords and len(w) > 1]
print(filtered)
# ['程序员', '北京', '工作']

💡 代码解析

  • 列表推导式过滤
  • len(w) > 1:过滤单字
  • 场景:文本分类、情感分析前必做

💻 二、文本向量化:让文字变成数字

核心问题:神经网络只能处理数字,文本无法直接输入。

2.1 One-Hot编码

每个词一个向量,只有一个位置是1,其他都是0。

python 复制代码
from tensorflow.keras.preprocessing.text import Tokenizer

# 词表
vocabs = ["苹果", "香蕉", "橙子", "葡萄"]

tokenizer = Tokenizer()
tokenizer.fit_on_texts(vocabs)

# 词表索引
print(tokenizer.word_index)
# {'苹果': 1, '香蕉': 2, '橙子': 3, '葡萄': 4}

💡 代码解析

  • Tokenizer:文本转向量的工具
  • fit_on_texts:构建词表
  • 自动过滤低频词,建立索引
python 复制代码
# 转One-Hot矩阵
one_hot_matrix = tokenizer.texts_to_matrix(vocabs, mode='binary')
print(one_hot_matrix)

# 输出:
# [[1. 0. 0. 0.]  苹果
#  [0. 1. 0. 0.]  香蕉
#  [0. 0. 1. 0.]  橙子
#  [0. 0. 0. 1.]] 葡萄

💡 代码解析

  • mode='binary':One-Hot形式
  • 每个词对应一行,只有该词位置为1

🔍 One-Hot的问题

复制代码
优点:简单直接
缺点:
- 向量稀疏(10000词 = 10000维,几乎全是0)
- 无法表达相似度
  苹果(1,0,0,0) vs 香蕉(0,1,0,0)
  距离 = √2,完全一样!

2.2 Word2Vec

用稠密向量表示词,相似的词向量也相近。

python 复制代码
import fasttext

# 训练词向量(需要大量文本)
model = fasttext.train_unsupervised(
    'corpus.txt',     # 语料文件
    model='cbow',      # cbow或skipgram
    dim=100,          # 向量维度
    epoch=5,          # 训练轮数
    lr=0.1,          # 学习率
    thread=10         # 线程数
)

💡 代码解析

  • CBOW:用上下文预测中间词
  • Skip-gram:用中间词预测上下文
  • dim=100:每个词表示成100维向量
python 复制代码
# 获取词向量
vec = model.get_word_vector('苹果')
print(vec.shape)  # (100,)

# 找相似词
similar = model.get_nearest_neighbors('苹果')
print(similar)
# [(0.92, '香蕉'), (0.87, '水果'), (0.82, '橙子')]

💡 代码解析

  • get_word_vector():获取词向量
  • get_nearest_neighbors():找最相似的词
  • 返回:相似度 + 词

🍎 核心优势

复制代码
One-Hot:  苹果=[1,0,0,0]  香蕉=[0,1,0,0]  → 距离=√2
Word2Vec: 苹果≈[0.8,0.2]  香蕉≈[0.75,0.25] → 距离≈0.07

相似词在向量空间中距离更近!

2.3 词嵌入Embedding层

在神经网络中自动学习词向量。

python 复制代码
import torch
import torch.nn as nn

# 假设词表大小10000,嵌入成128维
embedding = nn.Embedding(num_embeddings=10000, embedding_dim=128)

# 输入:词的索引
word_indices = torch.tensor([1, 5, 10, 99])  # 4个词

# 输出:4个词的128维向量
vectors = embedding(word_indices)
print(vectors.shape)  # (4, 128)

💡 代码解析

  • num_embeddings:词表大小(多少个词)
  • embedding_dim:向量维度
  • 自动学习:训练过程中自动调整词向量

📊 三、文本数据分析

3.1 标签分布

python 复制代码
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

# 读取数据
train_df = pd.read_csv('train.tsv', sep='\t')

# 统计
print(train_df['label'].value_counts())

# 可视化
sns.countplot(x='label', data=train_df)
plt.title('Label Distribution')
plt.show()

💡 代码解析

  • 查看各类别样本数量
  • 发现:可能类别不平衡!

3.2 句子长度分布

python 复制代码
# 计算句子长度
train_df['length'] = train_df['text'].apply(lambda x: len(x))

# 统计
print(train_df['length'].describe())

# 按标签看
sns.boxplot(x='label', y='length', data=train_df)
plt.show()

💡 代码解析

  • 分析意义:发现positive平均50字,negative只有30字
  • 应用:长度可以作为分类特征

3.3 词频统计

python 复制代码
from collections import Counter
import jieba

# 统计所有词
all_words = []
for text in train_df['text']:
    words = jieba.lcut(text)
    all_words.extend(words)

word_count = Counter(all_words)

# Top 20
print(word_count.most_common(20))

# 过滤后统计
stopwords = {'的', '了', '是', '在'}
filtered = [w for w in all_words if w not in stopwords and len(w) > 1]
print(Counter(filtered).most_common(20))

💡 代码解析

  • Counter:统计词频
  • most_common(20):出现最多的20个词

🧠 四、RNN循环神经网络

4.1 为什么需要RNN

普通神经网络 :输入→输出(无记忆)
RNN:输入+上一个状态→输出+新状态(记得之前的内容)

💡 深度比喻

  • 普通神经网络 = 翻译员,看一句译一句,不记得前面
  • RNN = 连续翻译员,记得上一句的意思,结合着翻

4.2 PyTorch实现RNN

python 复制代码
import torch
import torch.nn as nn

# 参数
input_size = 10     # 输入维度(词向量维度)
hidden_size = 20    # 隐藏状态维度
num_layers = 2      # RNN层数
batch_size = 3      # batch大小
seq_len = 5         # 序列长度

# 创建RNN
rnn = nn.RNN(
    input_size=input_size,
    hidden_size=hidden_size,
    num_layers=num_layers,
    batch_first=True  # batch在第一维
)

# 输入: (batch, seq_len, input_size)
x = torch.randn(batch_size, seq_len, input_size)

# 初始隐藏状态: (num_layers, batch, hidden_size)
h0 = torch.zeros(num_layers, batch_size, hidden_size)

# 前向传播
output, hn = rnn(x, h0)

print(f"output形状: {output.shape}")  # (3, 5, 20)
print(f"hn形状: {hn.shape}")         # (2, 3, 20)

💡 代码解析

  • output:每个时间步的输出,形状 (batch, seq_len, hidden_size)
  • hn:最后一个隐藏状态,形状 (num_layers, batch, hidden_size)
  • 用途:分类用hn,序列标注用output

📐 维度变化

复制代码
输入 x:         (batch, seq_len, input_size)
                (3,     5,      10)
                   ↓ RNN
输出:           (batch, seq_len, hidden_size)
                (3,     5,      20)

4.3 RNN类型

类型 结构 适用场景
N vs N 输入输出等长 词性标注、NER
N vs 1 多对一 文本分类
1 vs N 一对多 图片描述
N vs M 不等长 机器翻译
复制代码
N vs N: [我][爱][中][国] → [O][V][O][LOC]
N vs 1: [我][爱][中][国] → 正面
N vs M: [Hello] → [你好]

💪 五、LSTM与GRU

5.1 RNN的问题

长序列梯度消失:信息传到最后时,前面忘得差不多了。

复制代码
RNN处理长句子:
"我小时候在北京长大,后来去了上海工作了10年,去年搬到了深圳..."
→ 到了"深圳"时,早就忘了"北京"

5.2 LSTM:长短时记忆网络

LSTM通过三个"门"控制信息流动:

💡 深度比喻

  • 遗忘门:决定"忘了之前的"
  • 输入门:决定"记住新的"
  • 输出门:决定"输出什么"

就像一个精明的管家:选择性记住有用的,忘记没用的

python 复制代码
# LSTM实现
lstm = nn.LSTM(
    input_size=10,
    hidden_size=20,
    num_layers=2,
    bidirectional=False  # 双向LSTM
)

# 输入
x = torch.randn(5, 3, 10)  # (seq_len, batch, input)

# 初始状态
h0 = torch.zeros(2, 3, 20)  # 隐藏状态
c0 = torch.zeros(2, 3, 20)  # 细胞状态(关键!)

# 前向传播
output, (hn, cn) = lstm(x, (h0, c0))

print(f"output: {output.shape}")  # (5, 3, 20)
print(f"hn: {hn.shape}")         # (2, 3, 20)

💡 代码解析

  • 比RNN多一个c0(细胞状态)
  • 细胞状态:贯穿整个序列的信息通道
  • 门控:选择性通过/阻断信息

5.3 GRU:LSTM的简化版

GRU只有两个门(更新门+重置门),参数更少,训练更快。

python 复制代码
# GRU实现
gru = nn.GRU(
    input_size=10,
    hidden_size=20,
    num_layers=2,
    bidirectional=True  # 双向GRU
)

output, hn = gru(x, h0)
print(f"output: {output.shape}")  # (5, 3, 40) 双向=20*2

💡 代码解析

  • 比LSTM少一个门
  • 双向:能同时看前后文

📊 RNN vs LSTM vs GRU 对比

模型 门数 参数量 记忆能力 速度
RNN 0 最少 最快
GRU 2 较少
LSTM 3 较多 较慢

选择建议

  • 序列<50 → RNN
  • 序列50-200 → GRU
  • 序列>200 → LSTM
  • 需要双向 → BiLSTM/BiGRU

🎯 六、注意力机制

6.1 为什么需要Attention

RNN/LSTM的局限:离得远的信息容易被稀释

复制代码
例:The animal didn't cross the street because it was too tired.
     → "it" 指什么?animal 还是 street?
     
RNN:难以记住"animal"和"it"的关系
Attention:直接建立"it"和"animal"的联系!

💡 深度比喻

老师提问,全班都举手

→ 我应该听谁?关注分数最高的!

Attention = 关注最重要的部分


6.2 Attention原理

核心:加权求和

复制代码
Attention = 软寻址,通过"注意力"从多个Value中选需要的
python 复制代码
import torch
import torch.nn as nn
import torch.nn.functional as F

class Attention(nn.Module):
    def __init__(self, hidden_size):
        super().__init__()
        self.attn = nn.Linear(hidden_size * 2, hidden_size)
        self.v = nn.Parameter(torch.rand(hidden_size))
    
    def forward(self, hidden, encoder_outputs):
        # hidden: (batch, hidden_size) 当前隐藏状态
        # encoder_outputs: (seq_len, batch, hidden_size) 编码器所有输出
        
        seq_len = encoder_outputs.size(0)
        
        # 复制hidden到每个时间步
        hidden = hidden.unsqueeze(0).repeat(seq_len, 1, 1)
        
        # 计算能量:hidden和encoder_output的相似度
        energy = torch.tanh(
            self.attn(torch.cat([hidden, encoder_outputs], dim=2))
        )
        
        # 调整维度 (batch, hidden, seq_len)
        energy = energy.permute(1, 2, 0)
        
        # v是学习到的"查询向量"
        v = self.v.repeat(encoder_outputs.size(1), 1).unsqueeze(1)
        scores = torch.bmm(v, energy).squeeze(1)
        
        # softmax归一化
        attention_weights = F.softmax(scores, dim=1)
        
        # 加权求和
        context = torch.bmm(
            attention_weights.unsqueeze(1),
            encoder_outputs.permute(1, 0, 2)
        ).squeeze(1)
        
        return context, attention_weights

💡 代码解析

  • 计算相似度hidden vs encoder_outputs
  • softmax:归一化,得到权重
  • 加权求和:权重 × value = context

6.3 Seq2Seq + Attention

python 复制代码
# 编码器
class Encoder(nn.Module):
    def __init__(self, vocab_size, embed_size, hidden_size):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.gru = nn.GRU(embed_size, hidden_size, batch_first=True)
    
    def forward(self, x):
        embedded = self.embedding(x)
        output, hidden = self.gru(embedded)
        return output, hidden

# 解码器 + Attention
class Decoder(nn.Module):
    def __init__(self, vocab_size, embed_size, hidden_size):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.attention = Attention(hidden_size)
        self.gru = nn.GRU(embed_size + hidden_size, hidden_size, batch_first=True)
        self.fc = nn.Linear(hidden_size, vocab_size)
    
    def forward(self, x, encoder_output, hidden):
        # x: (batch, 1) 单个词
        embedded = self.embedding(x)
        
        # 计算注意力
        context, attn_weights = self.attention(hidden[-1], encoder_output)
        
        # 拼接
        gru_input = torch.cat([embedded, context.unsqueeze(1)], dim=2)
        
        # GRU
        output, hidden = self.gru(gru_input, hidden)
        
        # 输出
        prediction = self.fc(output.squeeze(1))
        
        return prediction, hidden, attn_weights

💡 代码解析

  • Encoder:编码源序列
  • Decoder:解码时用Attention关注Encoder的相关部分
  • 优势:解决长距离依赖问题

🐛 七、避坑指南

❌ 错误1:维度顺序

python 复制代码
# batch_first=True vs False
rnn1 = nn.RNN(10, 20, batch_first=True)
input1 = torch.randn(3, 5, 10)  # (batch, seq, feature)

rnn2 = nn.RNN(10, 20, batch_first=False)
input2 = torch.randn(5, 3, 10)  # (seq, batch, feature)

❌ 错误2:双向RNN维度翻倍

python 复制代码
gru = nn.GRU(input_size=10, hidden_size=20, bidirectional=True)
output, _ = gru(x)
print(output.shape)  # (seq, batch, 40) 不是20!

❌ 错误3:混淆h0和output

python 复制代码
# output: 所有时间步的隐藏状态
# hn: 最后一个时间步的隐藏状态

# 分类用hn
prediction = self.fc(hn[-1])

# 序列标注用output
predictions = self.fc(output)

📝 八、总结

核心要点

模块 关键点
分词 jieba.lcut()精确模式
向量化 One-Hot→Word2Vec→Embedding
RNN 有记忆,但长序列有问题
LSTM 门控解决长序列
Attention 直接建立远距离关系

快速查阅表

需求 代码
分词 jieba.lcut(text)
词性 jieba.posseg.lcut(text)
One-Hot tokenizer.texts_to_matrix()
词向量 model.get_word_vector(word)
RNN nn.RNN(input, hidden, num_layers)
LSTM nn.LSTM(input, hidden, num_layers)
GRU nn.GRU(input, hidden, num_layers)
Attention 自定义Attention类

学完这篇,文本处理和RNN系列就都掌握了! 🚀

相关推荐
weixin_446260852 小时前
OpenDataLoader PDF - 高效的PDF解析器,让AI更轻松获取数据!
人工智能·pdf
孟陬2 小时前
国外技术周刊 #4:这38条阅读法则改变了我的人生、男人似乎只追求四件事……
前端·人工智能·后端
Flying pigs~~2 小时前
迁移学习之中文文本分类微调
深度学习·自然语言处理·迁移学习
Tisfy2 小时前
LeetCode 2906.构造乘积矩阵:前后缀分解
算法·leetcode·前缀和·矩阵·题解·前后缀分解
zhangshuang-peta2 小时前
什么是 MCP:模型上下文协议到底解决了什么问题
人工智能·ai agent·mcp
工边页字2 小时前
cursor接上figma mcp ,图形图像模式傻瓜式教学(包教包会版)
前端·人工智能·ai编程
志栋智能2 小时前
AI超自动化运维,让IT运维自动化门槛更低
运维·网络·人工智能·安全·自动化
鱼樱2 小时前
大模型开发实践-milvus向量数据库搭建
人工智能