【深度学习入门】基于TextRNN的微博情感分析(一):数据预处理
文章目录
一、项目概述
本文将通过一个完整的微博四分类情感分析项目,从零入门深度学习在自然语言处理(NLP)中的应用。项目使用 TextRNN(双向LSTM) 模型,对微博文本进行四种情感分类:喜悦、愤怒、厌恶、低落。
整个项目分为以下模块:
- 构建字符级词表
- 数据加载、预处理与批量迭代器
- TextRNN模型定义
- 训练、验证与测试逻辑
- 主程序入口,串联全流程
本篇为第一部分,重点讲解数据预处理相关的两个模块:词表构建与数据集加载。
二、数据集介绍
本项目使用的数据集为 simplifyweibo_4_moods.csv,是简化版的微博四情感分类数据集。每条数据包含两部分:
- 情感标签:
0/1/2/3,分别对应喜悦、愤怒、厌恶、低落 - 微博文本:用户发布的微博内容
CSV 文件格式大致如下:
label,text
0,今天天气真好心情愉快
1,简直太让人生气了
2,这种行为真让人无语
3,感觉今天有点低落
...
三、词表构建(vocab_create.py)
在深度学习处理文本之前,需要将文本中的字符映射为数字 ID,这个映射关系就是词表(Vocabulary)。
3.1 核心思路
本项目采用字符级分词,即把每句话拆成单个汉字/符号,而不是按词拆分。这样做的好处是:
- 不需要依赖中文分词工具(如 jieba)
- 词表规模可控(几千个汉字即可覆盖大部分常用文本)
- 不会因为分词错误影响后续模型效果
3.2 完整代码
"""
词表构建
"""
from tqdm import tqdm
import pickle as pkl
# 词表最大容量:最多保留4760个高频字符
MAX_VOCAB_SIZE = 4760
# 特殊标记:<UNK>代表未登录字符,<PAD>用于补齐文本长度
UNK, PAD = '<UNK>', '<PAD>'
def build_vocab(file_path, max_size, min_freq):
"""
构建字符级词表
:param file_path: csv数据集文件路径
:param max_size: 词表最多保留多少个高频字符
:param min_freq: 字符最低出现频次,低于该值会被丢弃
:return: vocab_dic 字符->数字id的映射字典
"""
# 分词器:字符级拆分,一句话拆成单个汉字/符号,例如"你好" -> ['你','好']
tokenizer = lambda x: [y for y in x]
# 第一步:统计每个字符出现的次数
vocab_dic = {}
with open(file_path, 'r', encoding='UTF-8') as f:
i = 0
# tqdm:添加读取进度条,方便查看文件读取进度
for line in tqdm(f):
# i==0 代表第一行,是csv表头,跳过不处理
if i == 0:
i += 1
continue
# line[2:]:舍弃每行前2个字符(情感标签+逗号),只取后面的微博文本
# strip():去除字符串前后换行、空格
lin = line[2:].strip()
# 如果文本为空,跳过当前行
if not lin:
continue
# 遍历文本里每一个字符,统计频次
for word in tokenizer(lin):
# get(word,0): 如果word不在字典,返回0;否则返回已有计数,然后+1
vocab_dic[word] = vocab_dic.get(word, 0) + 1
# 1.筛选:只保留出现次数 > min_freq 的字符
# 2.按出现频次从高到低排序 reverse=True
# 3.切片截取前max_size个高频字符
vocab_list = sorted([_ for _ in vocab_dic.items() if _[1] > min_freq],
key=lambda x: x[1], reverse=True)[:max_size]
# 构建 字符 -> id 的映射:高频字符依次分配id,从0开始
vocab_dic = {word_count[0]: idx for idx, word_count in enumerate(vocab_list)}
# 添加特殊符号:UNK和PAD,放在词表最后
vocab_dic.update({UNK: len(vocab_dic), PAD: len(vocab_dic) + 1})
print(f'Vocab size: {len(vocab_dic)}')
# 使用pickle把词表字典保存到本地文件,后续训练可以直接加载
pkl.dump(vocab_dic, open('simplifyweibo_4_moods.pkl', 'wb'))
return vocab_dic
if __name__ == '__main__':
# 执行构建词表:读取微博情感csv,最多保留4760个字符,过滤出现次数<=3的字符
vocab = build_vocab('simplifyweibo_4_moods.csv', MAX_VOCAB_SIZE, 3)
print('词表构建结束')
3.3 关键知识点详解
(1)字符级分词
tokenizer = lambda x: [y for y in x]
将字符串逐字拆分,例如 "你好" → ['你', '好']。这是一个 lambda 匿名函数,等价于:
def tokenizer(x):
return [y for y in x]
(2)频次统计
vocab_dic[word] = vocab_dic.get(word, 0) + 1
使用字典统计每个字符的出现次数。dict.get(key, default) 方法在 key 不存在时返回默认值,非常适合计数场景,避免了 if word in vocab_dic 的判断。
执行逻辑: 先 get 取出 word 已有的计数(没有则取 0),再 +1 写回字典,从而实现累加。
(3)筛选与排序
vocab_list = sorted([_ for _ in vocab_dic.items() if _[1] > min_freq],
key=lambda x: x[1], reverse=True)[:max_size]
这一行完成了三件事:
- 筛选:只保留出现次数大于
min_freq(本项目为 3)的字符,过滤生僻字和噪声 - 排序:按频次从高到低排序(
reverse=True) - 截断:只取前
max_size(4760)个高频字符
为什么取 max_size = 4760 ?
词向量文件
embedding_Tencent.npz总共有 4762 个向量。
- 4760:留给真实词汇 / 字符
- 剩下 2 个位置:专门预留给特殊 token:
PAD(填充符)、UNK(未知词)
总数量:4760 + 2 = 4762,刚好和腾讯预训练词向量矩阵维度对齐。
(4)特殊标记 <UNK> 和 <PAD>
vocab_dic.update({UNK: len(vocab_dic), PAD: len(vocab_dic) + 1})
<UNK>(Unknown):未登录词。当数据集中出现词表中没有的字符时,用<UNK>的 ID 代替,避免因生僻字导致报错。<PAD>(Padding):填充标记。由于神经网络要求输入长度一致,短句子需要在末尾补<PAD>来统一长度。
这两个特殊标记被放在词表的最后两个位置。最终词表大小为 4760 + 2 = 4762。
(5)pickle 序列化
pkl.dump(vocab_dic, open('simplifyweibo_4_moods.pkl', 'wb'))
pickle 是 Python 的序列化模块,可以将 Python 对象(如字典)保存为二进制文件。后续通过 pkl.load() 直接加载,避免重复构建词表。
'wb':以二进制写入模式打开文件- 对应的读取模式为
'rb'(二进制读取)
(6)tqdm 进度条
for line in tqdm(f):
tqdm 可以为循环添加进度条,在处理大规模数据时直观展示读取进度,是数据处理中的常用工具。
四、数据加载与预处理(load_dataset.py)
词表构建完成后,需要将原始文本转换为模型可接受的数字张量,并划分为训练集、验证集和测试集。
4.1 完整代码
"""
数据集处理
微博四分类情感数据集预处理
功能:读取csv文本,字符级分词、padding截断、词转id,划分训练/验证/测试集
"""
from tqdm import tqdm
import pickle as pkl
import random
import torch
# 定义特殊标记:未知字符、填充占位符
UNK, PAD = '<UNK>', '<PAD>'
def load_dataset(path, pad_size=70):
"""
加载并预处理数据集
:param path: csv数据集文件路径
:param pad_size: 句子统一长度,超过截断,不足补PAD
:return: vocab词典, train训练集, dev验证集, test测试集
每条样本格式:(词id列表, 标签, 有效序列长度)
"""
contents = [] # 存放全部预处理后的样本
# 加载预保存的词表pkl文件,key为字符,value为对应的id
vocab = pkl.load(open('simplifyweibo_4_moods.pkl', 'rb'))
# 字符级分词器:按单个字切分句子(中文,字级别)
tokenizer = lambda x: [y for y in x]
with open(path, 'r', encoding='utf-8') as f:
i = 0
# tqdm:显示文件读取进度条
for line in tqdm(f):
# 跳过csv表头第一行
if i == 0:
i += 1
continue
# 跳过空行
if not line:
continue
label = int(line[0]) # 取出该行情感标签
content = line[2:].strip('\n') # 截取文本内容,去掉末尾换行符
words_line = []
token = tokenizer(content) # 对文本进行字级分词
seq_len = len(token) # 原始句子有效长度
# 序列长度统一处理:截断 / 填充
if pad_size:
if len(token) < pad_size:
# 句子长度不足,末尾补充PAD占位符
token.extend([PAD] * (pad_size - len(token)))
else:
# 句子过长,只保留前pad_size个字
token = token[:pad_size]
seq_len = pad_size # 截断后有效长度等于pad_size
# 将每个字转为词典对应的id,不在词表就用UNK的id
for word in token:
words_line.append(vocab.get(word, vocab.get(UNK)))
# 保存单条样本:(词id序列, 标签, 有效长度)
contents.append((words_line, int(label), seq_len))
# 打乱全部样本顺序,消除数据集顺序带来的偏差
random.shuffle(contents)
# 按8:1:1划分 训练集、验证集、测试集
train_data = contents[: int(len(contents) * 0.8)]
dev_data = contents[int(len(contents) * 0.8): int(len(contents) * 0.9)]
test_data = contents[int(len(contents) * 0.9):]
return vocab, train_data, dev_data, test_data
4.2 关键知识点详解
(1)Padding 与截断
神经网络的批量训练要求每个 batch 中的样本长度一致,因此需要统一序列长度:
- 短句子:末尾补充
<PAD>占位符 - 长句子:截断保留前
pad_size个字符
if len(token) < pad_size:
token.extend([PAD] * (pad_size - len(token)))
else:
token = token[:pad_size]
seq_len = pad_size
本项目中 pad_size=70,即每条微博统一为 70 个字符的长度。微博文本通常较短,70 个字符可以覆盖大部分内容。
(2)字符转 ID
for word in token:
words_line.append(vocab.get(word, vocab.get(UNK)))
遍历每个字符,从词表中查找对应的 ID。
vocab.get(word, vocab.get(UNK)) 的含义是:
- 如果
word在词表中,返回其对应的 ID - 如果不在,返回
<UNK>对应的 ID(即vocab.get(UNK))
这样即使遇到生僻字也不会报错,而是用统一的未知标记代替。
(3)数据集划分
random.shuffle(contents)
train_data = contents[: int(len(contents) * 0.8)]
dev_data = contents[int(len(contents) * 0.8): int(len(contents) * 0.9)]
test_data = contents[int(len(contents) * 0.9):]
- 先
random.shuffle()打乱数据,消除原始数据顺序带来的偏差(例如数据集中前半部分都是某一类别的情况) - 按 8:1:1 比例划分:
| 数据集 | 比例 | 用途 |
|---|---|---|
| 训练集(train) | 80% | 用于模型参数学习 |
| 验证集(dev) | 10% | 用于训练过程中调参和早停判断 |
| 测试集(test) | 10% | 用于最终模型性能评估 |
(4)样本格式
每条样本保存为三元组:
(words_line, label, seq_len)
# 例如:([23, 45, 67, ..., 4761, 4761], 0, 15)
words_line:长度为 70 的字符 ID 列表(不足部分用 PAD 的 ID 填充)label:情感标签(0-3)seq_len:原始句子的有效长度(未 padding 前的长度)
4.3 自定义数据迭代器(DatasetIterater)
PyTorch 训练时通常按 batch(批次)喂数据,这里自定义了一个迭代器来实现批量数据的封装。
class DatasetIterater(object):
"""将数据切分为batch_size的包"""
def __init__(self, batches, batch_size, device):
self.batch_size = batch_size
self.batches = batches
self.n_batches = len(batches) // batch_size
self.residue = False # 记录划分后的数据是否存在剩余的数据
if len(batches) % self.n_batches != 0:
self.residue = True
self.index = 0 # 处理了多少个包数据
self.device = device
def _to_tensor(self, datas):
"""将数据转换为PyTorch张量"""
x = torch.LongTensor([_[0] for _ in datas]).to(self.device) # 评论内容id序列
y = torch.LongTensor([_[1] for _ in datas]).to(self.device) # 评论情感标签
seq_len = torch.LongTensor([_[2] for _ in datas]).to(self.device) # 有效长度
return (x, seq_len), y # 返回格式:((文本张量, 有效长度), 标签)
def __next__(self):
"""返回下一个batch的数据"""
if self.residue and self.index == self.n_batches:
# 处理最后一个不完整的batch(剩余数据)
batches = self.batches[self.index * self.batch_size: len(self.batches)]
self.index += 1
batches = self._to_tensor(batches)
return batches
elif self.index > self.n_batches:
# 所有batch读取完毕,重置索引并抛出停止迭代异常
self.index = 0
raise StopIteration
else:
# 正常读取一个完整的batch
batches = self.batches[self.index * self.batch_size: (self.index + 1) * self.batch_size]
self.index += 1
batches = self._to_tensor(batches)
return batches
def __iter__(self):
return self
def __len__(self):
if self.residue:
return self.n_batches + 1
else:
return self.n_batches
关键知识点详解
(1)Python 迭代器协议
自定义迭代器需要实现两个魔术方法:
__iter__():返回迭代器对象自身,使得对象可以被for循环遍历__next__():返回下一个元素,遍历结束时抛出StopIteration异常
实现了这两个方法的对象可以直接用 for x in iterator 遍历,也可以用 next(iterator) 逐个获取。
(2)数据转张量
x = torch.LongTensor([_[0] for _ in datas]).to(self.device)
torch.LongTensor:创建 64 位整数张量。文本 ID 和标签都是整数,使用 LongTensor.to(device):将张量移动到指定设备(CPU / GPU / MPS)
(3)返回格式
return (x, seq_len), y
返回一个嵌套元组:
- 外层:
(输入数据, 标签) - 输入数据:
(文本id张量, 有效长度张量)
在模型的 forward 方法中,通过 x, _ = x 可以只提取文本 id 张量,忽略有效长度。
五、总结
本文完成了微博四分类情感分析项目的数据预处理模块,包含字符级词表构建、数据集加载与自定义批量迭代器三大部分。
- 词表构建:逐字符统计文本频次,过滤低频字符,保留前 4760 个高频汉字,再追加
<UNK>未知字符、<PAD>填充符,最终词表总大小 4762,与腾讯预训练字符向量维度匹配;使用 pickle 持久化保存词表,避免重复统计。 - 数据集预处理:读取 CSV 微博文本,字符级拆分,统一序列长度(pad_size=70),长句截断、短句补 PAD;将字符转为词表 ID,未登录字符映射为
<UNK>;打乱数据集后按 8:1:1 切分训练、验证、测试集,每条样本存储(id序列,标签,有效长度)三元组。 - 自定义 DatasetIterater 迭代器:实现 Python 迭代器协议,自动按 batch_size 分批,把列表数据转为 PyTorch LongTensor 并迁移至指定设备;支持处理最后不足一个 batch 的残余样本,方便后续 TextRNN 模型循环训练。
更多推荐


所有评论(0)