资讯动态

深度学习文本分类实战:从NLTK预处理到TFRecord的完整数据管线

发布时间:2026/9/28 6:03:59 来源:尧图企业网站定制
简介这是一份基于深度学习的自动文本分类系统完整源码包面向希望掌握Python自然语言处理与深度学习分类流程的开发者。系统整合NLTK文本预处理、特征向量化以及CNN/RNN/LSTM等模型训练与预测模块可应用于垃圾邮件识别、情感分析、新闻分类等场景。压缩包共37个文件主要由16个Python源码文件、8个Shell脚本、5个C文件及辅助配置文档组成py文件覆盖数据转换、词向量训练、模型导出到预测等核心流程sh脚本用于自动化部署与数据处理整体仅121KB。已有350人学习下载。通过研读项目文件可以理解从原始文本到TFRecords格式转换、fastText与HAM等模型实现、以及模型评估的完整工程结构附带的requirements与readme还能帮助快速搭建运行环境。适合作为NLP课程设计、文本分类任务入门或毕设改造的参考资料。1. 深度学习自动文本分类别急着选模型先把这条数据管线跑通很多刚接触自动文本分类的人第一反应是去搜哪个模型效果最好结果在TextCNN、TextRNN、LSTM、FastText之间来回摇摆最后卡在数据预处理上两三天动不了。这套基于Python和NLTK的源码给我的感觉恰恰相反它把“文本分类”这件事当成一条完整的流水线来设计从原始文本到TFRecord再到词向量、模型训练、导出和预测每一步都有独立脚本。里面确实包含CNN、RNN、LSTM、FastText和一个HAM模型但真正值钱的是那套能把数据喂给模型的标准流程。适合做垃圾邮件识别、情感分析、新闻分类这类需要快速落地、又希望对比多个深度模型的场景。我拆完这套源码后最大的感受是模型超参可以慢慢调数据管线的坑如果没填平后面全白搭。2. 先把37个文件理成一条主线从原始文本到TFRecord再到生产预测拿到这种源码包第一件事不是打开模型文件而是先把文件清单过一遍分清哪些是入口、哪些是数据准备、哪些是模型定义、哪些是辅助配置。这套包从文件名看结构很典型我把它分成五组对应五类职责。2.1 文件分组数据、词向量、模型、入口、配置数据相关的是upload.zip、train目录、data_utils.py、text_to_tfrecords.py、create_map_file.py。upload.zip大概率是原始语料压缩包train目录可能是整理好的训练数据或者某个子模块。data_utils.py负责文本清洗、分词、构建词表这些脏活text_to_tfrecords.py把清洗后的文本序列化成 TFRecordcreate_map_file.py生成标签映射表也就是把“类别名”和“整数id”对应起来。词向量相关的是word2vec.py和third_party下的word2vec目录以及prepare_vec.py。word2vec.py用于训练词向量或者加载预训练好的向量prepare_vec.py则把词向量与TFRecord里的token id对齐这一层很容易出问题后面避坑章节会细说。模型部分都在models目录下text_rnn.py、text_cnn.py、c_lstm.py、fasttext.py、HAM.py。每个文件对应一个独立的模型类或构建函数__init__.py把它们包装成统一的导入接口。入口和流程脚本是train.py、export_model.py、predict.py、main.sh。train.py控制训练循环export_model.py导出固化模型predict.py对新文本做推断main.sh把整个流程串起来。剩余的是requirements.txt、LICENSE、.gitignore、readme.txt这类标准工程文件。requirements.txt建议先看它决定了你能不能一键把环境装起来。别小看辅助文件很多时候环境装不上就是因为你跳过了它。2.2 数据流主线五个脚本按顺序执行把文件分组后主线就清晰了。我按常见执行顺序整理了一下这套流程和现在TensorFlow的标准做法基本一致create_map_file.py扫描训练数据生成从原始类别名到整数id的映射文件比如{sports: 0, tech: 1}。data_utils.py被其他脚本调用负责读取upload.zip解压后的文本做分词、去停用词、去特殊符号。text_to_tfrecords.py把清洗后的token序列加上标签写成TFRecord二进制文件。这一步等于把文本变成了模型能直接读的固定维度样本。word2vec.py在全部语料上训练词向量或者读取预训练向量prepare_vec.py把词向量表按词表顺序重排确保token id和向量行号一一对应。train.py加载TFRecord、初始化模型、开始训练训练完export_model.py把checkpoint固化成可直接部署的模型文件predict.py加载固化模型对输入文本输出分类概率。main.sh的作用就是把上面这些环节串成一个shell脚本。如果你在Linux上跑一般是一行一个脚本调用中间用或set -e保证失败即停。我习惯先手动跑前两步确认数据没问题再让main.sh一次跑完否则脚本报错时经常分不清是数据问题还是模型问题。这个流程里最容易被忽略的是create_map_file.py和prepare_vec.py之间的依赖关系。标签映射词表顺序和词向量表顺序必须是同一次构建的结果不能今天建一个、明天重建一个否则训练和预测的id全乱了。我之前接手过类似项目就是因为有人反复重建词表导致模型精度忽高忽低最后查了两天才发现是id错位。提示拿到源码后先跑通默认参数的小数据集确认五个脚本能一气呵成再去换自己的数据和超参。3. 四个模型加一个HAM选型逻辑与超参设计models目录下有五个模型文件覆盖了文本分类最常用的几个套路。它们之间不是简单的“谁更好”而是“谁更适合你的数据量、文本长度和算力约束”。下面按我从工程角度给它们的定位逐一分析同时给出可抄作业的参数配置思路。3.1 TextCNN第一选择快且稳text_cnn.py实现的是标准的卷积文本分类网络。核心思想是用多个不同宽度的卷积核去捕捉局部n-gram特征再用全局池化把每个卷积核的输出压成一个特征值。比如filter_sizes(2,3,4)就分别提取2-gram、3-gram、4-gram的组合特征。对短文本和中等长度文本TextCNN的训练速度非常快效果也稳定尤其适合新闻标题、商品评论这类长度在几十到两百个词的文本。构建时我通常会这样设置def text_cnn(vocab_size, num_classes, max_len100, embed_size128, num_filters128, filter_sizes(2, 3, 4)): # 输入是固定长度的token id序列shape: (batch, max_len) inputs tf.keras.Input(shape(max_len,), dtypeint32) # 词嵌入层vocab_size是表大小embed_size是向量维度 emb tf.keras.layers.Embedding(vocab_size, embed_size)(inputs) # 对每个卷积核宽度做Conv1D 全局最大池化 conv_pools [] for fsz in filter_sizes: conv tf.keras.layers.Conv1D(num_filters, fsz, activationrelu)(emb) pool tf.keras.layers.GlobalMaxPooling1D()(conv) conv_pools.append(pool) # 把不同宽度特征拼接起来 concat tf.keras.layers.Concatenate(axis-1)(conv_pools) # 全连接输出层 output tf.keras.layers.Dense(num_classes, activationsoftmax)(concat) model tf.keras.Model(inputs, output) return model这里max_len控制了输入文本的最大长度超过就截断不足就补0。它直接决定了TFRecord里序列化数据的大小所以text_to_tfrecords.py里的max_len必须和这里的完全一致否则训练时直接报shape不匹配。embed_size一般是128或300和word2vec.py里训练的词向量维度保持一致。如果用了预训练向量初始化Embedding层的weights参数要传入词向量矩阵同时设trainableTrue做微调效果通常比完全随机初始化好。3.2 TextRNN与C-LSTM捕捉序列信息的代价text_rnn.py和c_lstm.py都是走序列建模的路子。TextRNN常见的是单层或双层LSTM把最后一个时间步的隐状态作为整个文本的表示。C-LSTM则是先把文本过一层CNN提取局部特征再把这个特征序列送进LSTM相当于用CNN做特征抽取、用LSTM做全局序列建模。这两个模型适合段落级文本比如需要捕捉“虽然…但是…”这种跨长距离转折关系的场景。TextRNN的经典参数我一般这么配def text_rnn(vocab_size, num_classes, max_len200, embed_size128, lstm_units128, num_layers2, dropout0.5): inputs tf.keras.Input(shape(max_len,), dtypeint32) emb tf.keras.layers.Embedding(vocab_size, embed_size)(inputs) # 返回所有时间步的输出方便堆多层LSTM x emb for _ in range(num_layers): x tf.keras.layers.Bidirectional( tf.keras.layers.LSTM(lstm_units, return_sequencesTrue, dropoutdropout) )(x) # 只取最后一个时间步的隐状态作为句子表示 x tf.keras.layers.LSTM(lstm_units, dropoutdropout)(x) output tf.keras.layers.Dense(num_classes, activationsoftmax)(x) model tf.keras.Model(inputs, output) return model注意return_sequencesTrue只在中间层使用最后一层不要设否则输出是序列而不是最终表示。max_len要比CNN设置得更大因为LSTM吃长文本的能力更强但代价是显存占用高、训练慢。我踩过一个坑把CNN用的max_len100直接搬给LSTM结果长文本被截得太多模型完全学不到后置的转折关系准确率反而比CNN低。后来把max_len提到200以上效果才回来。C-LSTM的配置可以理解为上面两个模型的拼接CNN部分的卷积核宽度小一些比如(3, 4, 5)输出通道数不要太大64到128即可LSTM部分单层就够因为CNN已经做了局部特征压缩。这个模型参数量比较大如果你的数据量只有几千条很容易过拟合所以正则化要跟上dropout设在0.3到0.5之间。3.3 FastText朴素但适合基线fasttext.py这个名字容易让人以为它调用了Facebook的fasttext库其实文本分类场景里我们自己也能实现一个极简版本把词向量平均再接softmax。这种模型没有卷积和循环只有一层Embedding加一层平均池化。但它有一个变体对词加入bigram和trigram特征后再平均能部分捕捉词序信息效果在很多评测上接近TextCNN。极简实现思路def fasttext(vocab_size, num_classes, max_len100, embed_size128, ngram_features0): # 输入可以是token id序列也可以拼接n-gram id inputs tf.keras.Input(shape(max_len,), dtypeint32) emb tf.keras.layers.Embedding(vocab_size, embed_size)(inputs) # 平均池化将整个句子的词向量求平均 avg tf.keras.layers.GlobalAveragePooling1D()(emb) output tf.keras.layers.Dense(num_classes, activationsoftmax)(avg) model tf.keras.Model(inputs, output) return modelFastText最大的优势是训练极快参数量小在小数据集上不容易过拟合。我一般会用它先跑一遍作为后续深度模型的性能下限。如果你的任务对推理延迟极其敏感比如在线垃圾评论过滤FastText的效果和速度平衡可能是五个模型里最好的。数据量大、文本长的时候再考虑升级到CNN或LSTM。HAM模型我理解是层次注意力网络Hierarchical Attention Model在句子级和文档级分别做注意力加权适合超长文档分类。它的编码器结构更复杂训练时间也最长一般放在最后作为效果上限参考。如果数据只是短文本没必要上HAM。提示选型没有银弹。我的习惯是数据量小于5万条时先跑FastText短文本用TextCNN长文本用TextRNN或C-LSTM最后用HAM验证是否还有提升空间。五个模型共享同一套TFRecord和词表切换成本很低这才是这套源码最大的价值。4. NLTK预处理和词向量把文本变成模型能吃的数字模型再花哨输入还是数字。这套源码里data_utils.py和text_to_tfrecords.py承担了从原始文本到数字序列的重任。NLTK在这条链路里负责分词、去停用词和词干提取。很多人在这一步偷懒直接空格切分结果英文的标点、单复数、大小写全混在一起词表膨胀不说模型还学不到干净的语义特征。4.1 分词与停用词NLTK的标准动作在data_utils.py里我一般会按这个思路做基础清洗import re import nltk from nltk.corpus import stopwords from nltk.stem import SnowballStemmer # 确保首次使用前下载NLTK数据包nltk.download(punkt) 和 stopwords stemmer SnowballStemmer(english) stop_words set(stopwords.words(english)) def clean_text(raw_text): # 去掉HTML标签和多余空白 text re.sub(r[^], , raw_text) text re.sub(r\s, , text).strip().lower() # NLTK句子分词后按词再分词 words nltk.word_tokenize(text) # 滤掉停用词和长度只有1的噪声词 words [w for w in words if w not in stop_words and len(w) 1] # 可选词干还原把“running”变成“run” words [stemmer.stem(w) for w in words] return wordsnltk.word_tokenize对英文效果很好能正确拆出标点前的单词比单纯split()干净得多。stopwords.words(english)返回的停用词表覆盖了常见虚词比如“the”“is”“and”这些词对分类几乎没有判别力去掉能显著减小词表规模。词干还原这一步要看任务如果你的文本是新闻英语可以保留原文形态如果是用户评论词干还原能合并 “loved”和“love”缓解稀疏问题。中文用户注意NLTK的默认分词器对中文无效需要换成 jieba 或其他中文分词库 然后在clean_text里加一个语言判断分支。4.2 词表构建与TFRecord序列化分词之后就要建立词表。create_map_file.py会同时完成两类映射一是标签名到整数id二是词到整数id。词表可以用collections.Counter统计词频保留出现次数超过min_count的词其余统一映射为UNK。这一步的顺序至关重要我通常是先扫描全部训练语料统计词频再按词频从高到低排序分配id。from collections import Counter def build_vocab(tokenized_texts, min_count2): freq Counter() for tokens in tokenized_texts: freq.update(tokens) # 出现次数低于min_count的词直接丢弃减少长尾噪声 vocab {PAD: 0, UNK: 1} for word, count in freq.most_common(): if count min_count: vocab[word] len(vocab) return vocab这里PAD固定为0用于补齐短句子UNK固定为1用于替换词表外词。固定这两个id后面训练和预测都必须遵守。然后text_to_tfrecords.py把每个样本的token id序列连同标签写入TFRecordimport tensorflow as tf def write_tfrecords(token_ids_list, labels, vocab, out_path, max_len128): # token_ids_list是已经映射成id的样本列表每个样本是list of int writer tf.io.TFRecordWriter(out_path) for ids, label in zip(token_ids_list, labels): # 裁剪到max_len不足的补PAD id0 if len(ids) max_len: ids ids[:max_len] else: ids ids [vocab[PAD]] * (max_len - len(ids)) feature { input_ids: tf.train.Feature( int64_listtf.train.Int64List(valueids)), label: tf.train.Feature( int64_listtf.train.Int64List(value[label])) } example tf.train.Example( featurestf.train.Features(featurefeature)) writer.write(example.SerializeToString()) writer.close()max_len的选择直接影响模型效果和显存占用。太短会截掉关键信息太长会造成大量PAD占位、浪费显存。我一般会统计训练集中文本长度的分布取95%分位点作为max_len。比如常见的新闻标题集100左右就够了长文档任务可能需要200到300。训练数据加载TFRecord的时候tf.io.parse_single_example里声明的input_ids长度也必须和max_len一致多一个少一个都会在建模时暴露。word2vec.py和prepare_vec.py解决的问题是这个数字序列最初的表示质量。word2vec.py通常用CBOW或Skip-gram在全部语料上训练低维稠密向量我习惯直接用Gensim的Word2Vec来做设置vector_size128, window5, min_count2, epochs5。prepare_vec.py的作用是把Gensim训练出的词向量矩阵按build_vocab生成的id顺序重新排列得到一个vocab_size * embed_size的二维矩阵给Embedding层。这里最容易犯的错是不排序直接取向量导致id为5的词用了id为8的向量模型整个学歪。注意TFRecord是TensorFlow的原生二进制格式读取速度比逐行读文本快很多。但如果你在PyTorch下使用这套源码不兼容TFRecord需要改成torch.utils.data.Dataset配合torch.save序列化。这套包里的模型构建代码是Keras风格明显是TF生态所以沿用TFRecord最合适。5. 避坑指南预处理、TFRecord和训练中的五个常见翻车点这套源码我跑了不下五遍每遍都踩出不同的问题。下面五条是出现频率最高、也最容易让人卡住的情况全部按“现象→原因→解决”写清楚。5.1 NLTK数据包下载慢程序启动就卡住现象第一次运行data_utils.py时执行nltk.download(punkt)或nltk.download(stopwords)长时间没反应或者下载到一半报网络错误程序就停在那里。原因NLTK的默认数据源服务器在国外国内的网络环境访问很不稳定尤其点开多个数据包时经常断流。解决我现在的做法是从依赖环境变量入手。先手动下载所需数据包然后解压到本机的~/nltk_data或项目内的nltk_data目录再在代码开头指定nltk.data.path.append(./nltk_data)。这样代码就不会去访问远程服务器直接读本地数据。同时把requirements.txt里的NLTK版本固定好避免旧方法在新版本被移除。5.2 TFRecord的shape在训练时突然不对现象train.py跑到第二个batch就报ValueError: Shapes (None, 100) and (None, 120) are incompatible有时是输入维度对不上有时是标签维度多出[1]。原因text_to_tfrecords.py里写的max_len是120但模型输入的Input(shape(max_len,))里写的还是100或者标签在序列化时写成了value[label]整型列表读取时误用了FixedLenFeature([], tf.int64)。本质是序列化与解析两端的约定不一致。解决把TFRecord的写入和解析放在同一个模块里定义一个常量维护MAX_LEN和特征格式不要把数字硬编码散落各处。我在data_utils.py顶部写死了MAX_LEN 128所有脚本统一from data_utils import MAX_LEN。另外用tf.io.FixedLenFeature([MAX_LEN], tf.int64)读id序列用tf.io.FixedLenFeature([], tf.int64)读标签写的时候注意Int64List(valueids)传入的是list不能传多维数组。5.3 词表顺序变了训练和预测完全错位现象模型训练时准确率正常但换到predict.py跑新文本输出的标签概率混乱甚至出现 “sports” 文本被高概率分成 “tech”。原因create_map_file.py生成的词表和标签映射是固定的但有人在训练后又跑了一次create_map_file.py或者word2vec.py重新训练了向量导致词表id顺序和词向量矩阵行号没有对齐。预测时虽然走得通但输入id对应的是新词表的顺序Embedding矩阵还是旧顺序的向量等于对不上号。解决把词表、标签映射、词向量矩阵统一导出到固定目录命名包含数据集的version号。每次训练前检查映射文件的生成时间训练完导出模型时附带把词表和标签映射一起打包。predict.py加载模型时强制要求传入同一个版本的映射文件不存在就报错。从那以后我每次换数据集都会在main.sh里先验证映射文件是否存在不存在才允许执行创建脚本。5.4 LSTM训练loss不降甚至越跑越高现象TextRNN或C-LSTM训练几十步后训练loss在1附近震荡验证准确率始终在随机水平偶尔loss突然变成NaN。原因LSTM对学习率很敏感默认的0.01或0.001对LSTM来说常常偏大梯度范数会爆炸另外词向量初始化范围过大也会放大梯度。很多新手直接照搬CNN的优化器参数给LSTM必出问题。解决给LSTM加梯度裁剪比如optimizer tf.keras.optimizers.Adam(learning_rate0.001, clipnorm5.0)同时把学习率降到0.0005甚至0.0001。更常用的做法是在模型前向传播里加入tf.keras.layers.LayerNormalization()稳定隐状态。词嵌入初始化用tf.keras.initializers.RandomUniform(minval-0.05, maxval0.05)不要用默认的随机正态分布。我现在的习惯是任何RNN类模型第一版配置直接以clipnorm5.0和lr0.0005起步跑通了再逐步调大学习率。5.5 训练正常但预测结果全是同一个类现象predict.py对任何输入都输出 “tech” 或概率几乎均匀分布但训练时的验证准确率明明正常。原因分类不均衡导致模型偏向多数类。假如 tech 占80%模型只要全输出 tech 就有80%准确率训练时的batch采样随机模型很快学会偷懒。验证指标没有用f1或recall只看accuracy所以没暴露问题。解决训练前统计每个类的样本数在train.py里给损失函数加权loss tf.keras.losses.SparseCategoricalCrossentropy(from_logitsFalse)并通过class_weight参数传入{0: 1.0, 1: 10.0}这类比例。更彻底的办法是在data_utils.py里做分层抽样确保每个batch都包含各类别样本。验证时不要只看准确率至少加一个Precision、Recall和F1的打印这点也可以用sklearn.metrics在验证集上计算。提示五个坑里词表错位和TFRecord shape不一致是最隐蔽的因为程序不报错只是效果差。我建议每次改动后都跑一遍从predict.py到原始文本的往返测试随机抽一条训练数据预测它的类别必须和原标签一致否则数据管线就有问题。6. 进阶把模型性能往上拉的三个验证习惯模型能跑通只是起点想拿到可以上线用的精度我总结了三个自己常用的验证习惯。第一个是混淆矩阵。准确率只能告诉你整体对不对不能告诉你哪几类容易混。我每轮训练完都会把验证集预测结果存下来用sklearn画混淆矩阵from sklearn.metrics import confusion_matrix, classification_report import numpy as np # labels_val是真实标签preds_val是模型预测的argmax结果 # 这里假设你已经跑完了验证集的预测 classes [sports, tech, business] print(classification_report(labels_val, preds_val, target_namesclasses)) cm confusion_matrix(labels_val, preds_val) # 按行归一化方便看出“某类被错分到了哪里” cm_norm cm.astype(float) / cm.sum(axis1, keepdimsTrue)看到 “sports” 有30%被分到 “tech”我第一反应不是换模型而是去检查这两个类别的训练语料是否存在大量重复词或者标签本身标注就不干净。很多时候清洗数据带来的提升比换模型大得多。第二个是分层采样与类别权重。我踩过只按文件顺序切分训练集和验证集的坑如果原始数据按类别排序存放验证集里可能完全没有某个类别。正确做法是把样本先按标签分组每组内按比例随机抽到训练集和验证集保证每个类在两边都出现。配合class_weight一起用能让模型在类别不均衡时不至于被多数类带偏。这一步在train.py里写成np.random.RandomState(42).permutation做索引打乱固定随机种子方便复现。第三个是模型导出后的pipeline一致性验证。训练时验证集通过TFRecord喂入但线上预测往往走的是另一条路清洗→分词→词表映射→补pad→模型推理。这两条路只要有一处不一致线上效果就会比测试时低一截。我现在固定用同一条函数处理训练和预测文本也就是predict.py里直接调用data_utils.clean_text和build_vocab不单独写第二套预处理。每次导出模型后我会随机抽20条训练文本用导出模型重新预测并和训练时的真实标签对比全部一致才认为pipeline是通的。从那以后我每次换数据集都会强制走一遍这个流程先跑fasttext.py建立基线再对比text_cnn.py和text_rnn.py最后用混淆矩阵定位难点选择性地尝试HAM.py。宁可多花半小时做验证也不想在错误的数据管线上浪费三天调参。这套源码最值得复用的不是某一个模型而是这条从文本到TFRecord再到预测的完整链路——把这条链路看明白你自己的分类项目就成功了一半。希望这些拆解和踩坑记录帮你在复现时少走弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑