📖 导读 :
这份笔记覆盖NLP实战全流程:文本预处理、文本向量化、RNN/LSTM/GRU、注意力机制。
- 零遗漏原则:完整覆盖所有关键代码
- 显微镜式解析:每个参数、每个函数都讲清楚
- 深度比喻:用生活化的例子解释抽象概念
- 场景化映射:代码对应实际应用场景
🎯 今日目标:
- 掌握文本预处理(分词、清洗)
- 理解文本表示方法(One-Hot、Word2Vec、Embedding)
- 学会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
💡 代码解析:
- 计算相似度 :
hiddenvsencoder_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系列就都掌握了! 🚀