简介基于Word2Vec与TextCNN的文本分类实战资源包面向NLP初学者、算法工程师以及有文本分类任务的学生和开发者适用情感分析、新闻分类、社交媒体内容分拣等场景。压缩包共13个文件整体大小53.6MB核心内容包括Python模型脚本、Word2Vec词向量训练文本、词汇表文件、约10万条带标签的训练集CSV与1万条测试集CSV以及训练完成的model权重文件另附项目配置与Git忽略规则等辅助文件方便复现代码并继续调优。当前已有749人学习下载。资源内数据划分清晰测试集与训练集分离能够支撑从数据预处理、分词、词向量构建、TextCNN卷积、池化到全连接分类输出的完整流程。学习者可借助训练好的词向量和模型权重降低重复训练成本也可以调整卷积核数量、过滤器大小等超参数将方案迁移至其他文本分类任务是一份实操性较强的动手学习素材。1. 开篇为什么我把Word2VecTextCNN当作文本分类的默认方案文本分类这个需求从舆情监控到新闻聚合从客服工单分拣到评论情感判断几乎每个做 NLP 的团队都会碰到。我最早接触这类任务时习惯直接上 BERT效果确实好但 GPU 显存和推理延迟在不少业务场景里根本扛不住。后来我在这份资源里看到 Word2Vec TextCNN 的组合重新审视了这个方案不需要 GPU 也能训练单机 CPU 跑 10 万条样本只要几十分钟推理速度是 BERT 的几十倍而且在小数据集上精度并不差太多。对于大部分中小团队和入门者这可能是性价比最高的文本分类基线。这份资源包含完整可跑的代码word2vectextcnn.py、10 万条带标签的新冠舆情训练集、1 万条测试集、已经训练好的test.model模型文件以及vocab.txt词表和word2vec_txt.txt语料。适合正在学 NLP 的开发者、想快速落地分类功能的工程师以及需要给领导做 demo 但又没有 GPU 的同学。我带过不少实习生用这份资源入门从跑通到调参大概一两天就能上手。接下来的内容按照我从数据处理到模型部署的完整链路拆解每个步骤都给出可直接复现的操作。2. 数据集与词表构建从 CSV 到 vocab.txt 的完整流程2.1 原始数据长什么样先看训练集nCoV_100k_train.labled.csv。这个文件 10 万行每一行是一条文本样本加一个标签列。这类语料一般来自社交媒体内容里既有中文也有英文还夹杂着 URL、 用户名、emoji 等噪声。我第一次打开这个 CSV 的时候第一反应是先跑一下head看看字段分隔符和编码因为很多 CSV 看起来是逗号分隔实际是 tab 分隔或者带了 BOM 头。常见做法是先用 Pandas 读进来设置sep,再打印前几行确认列名。这里给出一段代码import pandas as pd df_train pd.read_csv(nCoV_100k_train.labled.csv, sep,, encodingutf-8) print(df_train.columns.tolist()) print(df_train.head(10)) print(df_train[label].value_counts())这段代码做了三件事读取训练集、打印列名、统计标签分布。运行之后你会发现列名一般类似text和label如果是label和text的顺序不同也没关系后续按列名取就行。如果标签分布极不均衡比如某类占比 95%那训练出来的模型大概率只会预测那一类后面做评估时要格外小心。数据清洗我一般会做两步第一步把空行和重复行去掉因为重复样本会让模型在训练时反复看到同样的内容导致验证集指标虚高第二步把文本里的 URL、 用户名、多余空格清理掉。注意不要过度清洗比如不要把所有标点都删掉因为 TextCNN 的 n-gram 特征依赖标点来区分语义边界。2.2 分词中英文分词策略这份语料同时包含中英文分词策略要分开处理。英文用空格切分基本够用中文需要一个分词器。在word2vectextcnn.py对应的环境里我一般用 jieba 处理中文因为它安装方便、不需要额外训练模型而且在社交媒体文本上的表现还能接受。如果你用的是 jieba注意两个细节一是要关闭HMM开关还是保持默认取决于你对新词识别的需求二是自定义词典的问题。对于新冠舆情这类包含大量专有名词的语料默认词典很可能把“新型冠状病毒”切成奇怪的片段所以我会提前把专有名词加入jieba.add_word()。分词结果需要以空格分隔保存到word2vec_txt.txt这是因为后面训练 Word2Vec 要按空格切分 token。import jieba import re def clean_text(raw): text re.sub(rhttps?://\S, , raw) text re.sub(r\S, , text) text re.sub(r\s, , text) return text.strip() def tokenize(text): cleaned clean_text(text) return .join(jieba.cut(cleaned)) df_train[tokenized] df_train[text].apply(tokenize) df_train[tokenized].to_csv(word2vec_txt.txt, indexFalse, headerFalse)这段代码把清洗和分词合在了一起。第 1 行定义一个清理函数去除 URL 和 用户名第 5 行用 jieba 分词并用空格连接最后写入word2vec_txt.txt作为 Word2Vec 的训练语料。注意如果你后续要重新训练 Word2Vec这里的word2vec_txt.txt就是模型训练的直接输入如果直接用资源里自带的test.model这一步则可以跳过。2.3 词表构建vocab.txt 到底怎么生成vocab.txt是 Word2Vec 训练过程中产生的词汇表每一行是一个词或者带一个频次模型通过它把 token 映射到索引。把词表单独存成一个文件的目的是在 TextCNN 的 embedding 层我们需要知道词表大小才能初始化 Embedding 矩阵。所以 vocab.txt 本质上是 Word2Vec 和 TextCNN 之间的桥梁。from gensim.models import Word2Vec sentences [] with open(word2vec_txt.txt, r, encodingutf-8) as f: for line in f: sentences.append(line.strip().split( )) w2v_model Word2Vec(sentences, vector_size200, window5, min_count2, sg0, workers4, epochs10) vocab list(w2v_model.wv.key_to_index.keys()) with open(vocab.txt, w, encodingutf-8) as f: for word in vocab: f.write(word \n)这里的关键参数vector_size200表示每个词向量的维度这个值同时决定了后续 TextCNN 的 embedding 输出维度window5是上下文窗口大小对于中短文本5 是一个不错的默认值min_count2表示出现次数少于 2 次的词不进入词表这能过滤大部分噪声sg0选择 CBOW 模型sg1则是 Skip-gram。我自己的习惯是样本量 10 万级别、文本较短时CBOW 训练更快Skip-gram 在语料足够大时精度略高。3. 训练 Word2Vec 词向量模型参数、迭代次数与文本格式的坑3.1 为什么不用随机初始化的 embedding在文本分类中embedding 层有两种初始化方式一种是随机初始化然后跟着任务训练另一种是用预训练词向量初始化。Word2Vec TextCNN 组合采用的就是后者。随机初始化的 embedding 相当于把每个词当成一个独立符号完全没有语义相似性的概念——疫情和抗疫在向量空间里可能相距很远模型必须从零学习它们的关系这在数据量不够大时很难学好。而 Word2Vec 训练出的词向量语义相近的词在向量空间中距离也近这给 TextCNN 提供了先验知识。比如病毒和传染的向量距离比较近卷积核就能更容易捕捉到这两个词同时出现时对应的类别模式。这正是这份资源把 Word2Vec 和 TextCNN 串联起来的核心理由词向量给文本分类提供了语义基础TextCNN 负责在此基础上提取局部特征。3.2 模型参数详细说明在上一章的代码里我已经写了 Word2Vec 的基本参数这里重点讲几个容易踩坑的参数。window参数控制上下文窗口窗口越大词向量越偏向主题层面的共现关系窗口越小越偏向语法层面的搭配关系。对于短文本分类建议window在 3 到 5 之间不要超过 10。min_count越低词表越大低频词保留越多但也会引入噪声我见过有人设成 1结果词表里有几百个只出现一次的乱码词直接拉低了分类精度。epochs迭代次数也很关键默认 5 次肯定不够10 次是底线超过 20 次收益就不明显了。这里给出一个我调参时常用的对照方式参数推荐值影响vector_size100 - 200维度太低语义信息不足太高则计算量大且数据稀疏问题加剧window3 - 5分类任务建议偏小主题相似度任务偏大min_count2 - 3过滤低频噪声太小词表膨胀太大丢失语义sg0CBOW小数据集 CBOW 更稳大数据集 Skip-gram 更优epochs10 - 15低于 5 欠拟合高于 20 边际收益极低3.3 文本格式的坑一行一个句子还是空格分隔训练 Word2Vec 时输入格式是 List[List[str]]也就是一个由 token 列表组成的列表。很多人第一次跑的时候直接把原始 CSV 的一整行文本丢进去导致 Word2Vec 把它当成一个超长 tokenvocab.txt 里全是这种长字符串训练出来的词向量毫无语义。我调 Word2Vec 的经验是先按行读文件每行按空格 split得到 token 列表再拼成一个大的训练语料列表。另外如果你保存的分词语料里混入了空行比如 CSV 最后一行的换行符gensim 的 LineSentence 加载方式可能会报错或者跳过所以写完word2vec_txt.txt之后我建议先数一下行数确认和原始文本条数对得上再继续。血泪经验我有一次就是这里少了 200 行vocab.txt 里对应的词表也少了词导致后面的 TextCNN 模型 embedding 矩阵索引越界排查了半小时。3.4 保存和加载 Word2Vec 模型Word2Vec 训练完成后模型需要保存。test.model就是这份资源里已经训练好的模型文件。gensim 保存模型有两种格式model.save()保存的是完整对象加载后可以继续训练model.wv.save_word2vec_format()保存的是纯词向量文本格式体积更小但加载后不能再训练。对于只用来给 TextCNN 提供 embedding 的场景用 word2vec 格式就够了。from gensim.models import Word2Vec # 加载已经训练好的模型 w2v_model Word2Vec.load(test.model) # 或者按 word2vec 格式加载 from gensim.models import KeyedVectors kv KeyedVectors.load_word2vec_format(test.model.bin, binaryTrue)这里有一个关键点test.model如果直接用Word2Vec.load()加载会得到完整的模型对象需要通过w2v_model.wv访问词向量。我一般在代码开头加载模型后先打印一下词表数量和任意一个词的向量长度确认加载成功再继续构建 embedding 矩阵。加载失败最常见的报错是FileNotFoundError通常是因为路径不对或者文件名的大小写拼错了。4. TextCNN 模型实现从 embedding 到多尺寸卷积核4.1 TextCNN 的原理CNN 怎么处理文本TextCNN 的思想并不复杂把文本看成一个二维矩阵行是词向量列是词向量维度卷积核在这个矩阵上滑动提取局部 n-gram 特征。比如说一个卷积核的高度是 3就相当于每次看连续的 3 个词向量这对应了三元组特征trigram。多个不同高度的卷积核并行就能同时捕获 bigram、trigram、4-gram 等不同粒度的局部特征。在图像 CNN 里卷积核通常是在二维平面上滑动的在 TextCNN 里卷积核的宽度一般是词向量维度只在词序方向即句子的长度方向上滑动。这样做的好处是每个卷积核天然覆盖完整的一个词向量不需要跨维度卷积。池化层通常用 max pooling对每个卷积核输出的特征图取最大值保留最显著的特征。最后把所有池化结果拼接起来过一层全连接层输出到类别维度上。4.2 构建 embedding 矩阵的细节真正写代码的时候第一步是把 Word2Vec 词向量转成 PyTorch 或 TensorFlow 的 embedding 矩阵。这个矩阵的形状是vocab_size x embedding_dim每行对应词表中的一个词行号就是该词在 vocab 中的索引。注意词表和 embedding 矩阵的行号必须对齐否则模型查出来的向量就是错的。我在word2vectextcnn.py里看到的核心实现思路是这样先读vocab.txt生成word2idx字典然后把 Word2Vec 模型里的向量按word2idx顺序填入矩阵遇到词表里有但 Word2Vec 里没有的词比如未登录词用随机小值初始化。import numpy as np from gensim.models import KeyedVectors # 加载词向量 kv KeyedVectors.load_word2vec_format(word2vec_txt.vector, binaryFalse) # 读取词表构建索引 word2idx {} with open(vocab.txt, r, encodingutf-8) as f: for idx, line in enumerate(f): word line.strip() word2idx[word] idx vocab_size len(word2idx) embedding_dim kv.vector_size embedding_matrix np.random.uniform(-0.25, 0.25, (vocab_size, embedding_dim)) # 填充预训练向量 for word, idx in word2idx.items(): if word in kv.key_to_index: embedding_matrix[idx] kv[word] print(embedding_matrix shape:, embedding_matrix.shape)这段代码的关键在于第 7 行没有用kv[word]而是加了判断避免 KeyError 中断。第 10 行的np.random.uniform是给未登录词兜底的范围设置成 -0.25 到 0.25 是为了和 Word2Vec 的训练值域保持一致。如果你直接全部用随机初始化那 Word2Vec 就白训练了模型会退化成普通的随机 embedding 文本分类精度会明显下降。4.3 完整模型定义TextCNN 模型本身在 PyTorch 里很好写。先用nn.Embedding层加载预训练权重然后定义多个不同高度的卷积核接着是 max pooling、全连接层。下面给出一个可以直接照着跑的模型定义import torch import torch.nn as nn class TextCNN(nn.Module): def __init__(self, vocab_size, embedding_dim, num_filters, filter_sizes, num_classes, pretrained_embeddingNone): super().__init__() self.embedding nn.Embedding(vocab_size, embedding_dim) if pretrained_embedding is not None: self.embedding.weight.data.copy_(torch.from_numpy(pretrained_embedding)) self.convs nn.ModuleList([ nn.Conv2d(1, num_filters, (fs, embedding_dim)) for fs in filter_sizes ]) self.dropout nn.Dropout(0.5) self.fc nn.Linear(num_filters * len(filter_sizes), num_classes) def forward(self, x): # x shape: (batch_size, seq_len) emb self.embedding(x).unsqueeze(1) # (batch_size, 1, seq_len, embedding_dim) conv_outs [] for conv in self.convs: conv_out torch.relu(conv(emb)).squeeze(3) # (batch_size, num_filters, seq_len - fs 1) pooled nn.functional.max_pool1d(conv_out, conv_out.size(2)).squeeze(2) conv_outs.append(pooled) combined torch.cat(conv_outs, dim1) return self.fc(self.dropout(combined))这段代码的核心参数filter_sizes常用[3, 4, 5]代表分别提取 trigram、4-gram 和 5-gram 特征num_filters每个卷积核的个数一般 100 到 256 之间太小特征不足太大容易过拟合。注意Conv2d(1, num_filters, (fs, embedding_dim))里的kernel_size是一个二维元组第一个维度是词窗口高度第二个维度是词向量维度必须和 embedding_dim 一致。如果这两个维度对不上运行时会直接报维度错误。这个错很常见遇到的话先检查filter_sizes里的值和 embedding_dim 是否匹配。4.4 文本到序列的转换与 padding模型输入需要固定长度的序列。把每个样本分词后查vocab.txt得到词索引然后统一截断到max_seq_len不足的补 00 一般对应pad标记。如果你用的词表里没有pad和unk需要手动在 vocab.txt 开头预留这两个位置。因为 embedding 矩阵的行号要和 word2idx 对齐所以我建议先构建词表再生成 embedding 矩阵。max_seq_len 100 def text_to_sequence(text, word2idx): tokens tokenize(text).split( ) seq [word2idx.get(t, word2idx.get(unk, 0)) for t in tokens[:max_seq_len]] if len(seq) max_seq_len: seq [word2idx.get(pad, 0)] * (max_seq_len - len(seq)) return seqmax_seq_len设置太短会截掉关键信息太长则让卷积核滑动范围变大、特征稀疏。我建议先统计一下语料里 95% 分位的序列长度用这个值作为max_seq_len。比如统计结果是 80那就设 100留点余量。word2idx.get(t, 0)是处理未登录词的兜底0 如果对应unk就传给模型一个统一的未知词向量模型仍然能学习这个位置的特征。5. 避坑与排查训练过拟合、维度不匹配、数据泄漏5.1 训练时 loss 不下降模型根本没在学习现象训练迭代了十几个 epochloss 一直维持在初始水平附近分类准确率在 50% 左右停住不上涨。原因最常见的是 embedding 层没有加载预训练权重或者权重加载了但没有设置requires_gradTrue。还有一个原因是学习率设得太小PyTorch 默认的 Adam 学习率是 0.001但 Word2Vec 词向量的值域通常比较小如果学习率太低全连接层和卷积层的梯度更新太慢。解决先打印模型的 embedding 权重确认前几行是否和 word2vec 词向量一致。然后检查学习率如果用 Adam0.001 是起步值如果 loss 几个 epoch 不动试着提到 0.005。用 PyTorch 训练时可以在代码里打印每个 epoch 的 loss观察到 loss 在最初几个 step 有一个明显的下降才算正常。5.2 训练集准确率很高测试集一塌糊涂现象训练集准确率 98%测试集只有 70%明显过拟合。原因这份资源的训练集 10 万条TextCNN 模型本身不算大但如果 embedding_dim 设得过高、卷积核数量过多、dropout 太低很容易把训练集中的噪声学进去。我自己调过一次把 filter_sizes 从[3,4,5]换成[2,3,4]并且把 num_filters 从 256 降到 128过拟合明显缓解。解决先把nn.Dropout的值从 0.3 调到 0.5这是最直接的手段。然后减少 filter 数量从 256 降到 128 甚至 100。如果还不行把 max_seq_len 缩短因为长的序列意味着更多参数参与计算模型容量更大。数据集小的时候还可以对训练集做同义词替换或文本增强但这个数据量 10 万条其实不太需要。5.3 维度不匹配的报错现象跑模型前向传播时报错size mismatch for embedding.weight: copying a param with shape torch.Size([30000, 200]) from checkpoint, the shape in current model is torch.Size([30000, 100])。原因vocab.txt 里词的总数和 embedding_matrix 的行数对不上。可能是在构建 embedding 矩阵时vocab 里有些词在 word2vec 模型里不存在填充了随机向量但行数漏算了。另一个常见原因是加载test.model时用的向量维度是 200但模型定义里的 embedding_dim 写成了 100。解决在初始化 TextCNN 之前打印一下len(word2idx)和embedding_matrix.shape[0]确认两个数字一致。如果test.model的向量维度是 200模型定义里一定要写embedding_dim200。我一般都会在代码里写一个断言assert embedding_matrix.shape[0] vocab_size assert embedding_matrix.shape[1] embedding_dim这两个断言能挡掉 90% 以上的初始化错误。如果你用的不是这份资源自带的test.model而是自己重新训练的 Word2Vec注意 train 和 test 数据的词表必须要一致否则测试集里会出现大量unk词分类效果会明显下降。5.4 数据泄漏:测试集里混入训练样本现象测试集准确率比训练集还高这显然不合理。原因nCoV_100k_train.labled.csv和nCov_10k_test.csv可能来自同一批数据源如果你在清洗时把测试集的文本也加入了 Word2Vec 的训练语料word2vec_txt.txt就构成了数据泄漏。Word2Vec 是无监督训练理论上它不直接看到标签但如果训练语料里包含了测试集的文本词向量就多多少少携带了测试集的信息。解决严格区分两个文件。word2vec_txt.txt只能从nCoV_100k_train.labled.csv这一份里生成nCov_10k_test.csv只用于最后的模型评估任何预处理阶段都不能碰。另外在构建词表时也不要拿测试集来统计词频否则同样有词表泄漏的风险。5.5 训练时间过长或内存溢出现象训练 10 万条数据时内存占用飙升或者训练一个 epoch 要好几分钟。原因如果vector_size设成 300 甚至更高且vocab_size达到 5 万以上embedding 矩阵本身已经很大卷积层在 forward 时还会产生中间特征图内存消耗会叠加。还有一个常见问题是batch_size设得过大比如 512 或 1024在 8GB 显存的 GPU 上直接 OOM。解决batch_size从 64 试起观察显存占用再往上加。如果 CPU 训练把workers设成 4 到 8让数据加载和计算并行。我自己跑这份资源的时候直接在 CPU 上用batch_size64训练了 15 个 epoch大概一个多小时跑完精度已经能到 85% 左右。6. 验证与进阶用测试集评估分类效果并调整超参6.1 用 test.model 快速验证分类效果如果你不想重新训练 Word2Vec 和 TextCNN资源里自带的test.model可以直接用来评估。加载方式有两种如果test.model是 gensim 的完整模型直接用Word2Vec.load(test.model)如果是纯词向量格式用KeyedVectors.load_word2vec_format()。加载后我来验证一下它能不能正确输出分类结果。from gensim.models import KeyedVectors import numpy as np kv KeyedVectors.load_word2vec_format(test.model, binaryFalse) test_vector np.mean([kv[w] for w in 疫情 防控 重要 措施.split( ) if w in kv.key_to_index], axis0) print(vector shape:, test_vector.shape) print(top similar words:, kv.most_similar(疫情, topn5))这个验证的核心是看词向量的质量。如果most_similar(疫情)返回的是“病毒、口罩、防护、感染、确诊”这类的词说明词向量训练得不错可以继续加载到 TextCNN。如果返回的是乱七八糟的词那大概率是 Word2Vec 的训练格式有问题词向量本身就是噪声。6.2 完整训练流程与评估指标跑通了模型之后我建议你把训练流程固化下来加载数据 → 分词 → 训练 Word2Vec → 构建词表 → 生成 embedding 矩阵 → 训练 TextCNN → 测试集评估。每一步都在关键节点打印形状信息和 loss方便回溯。下面给出一个评估测试集的完整代码片段model.eval() correct, total 0, 0 with torch.no_grad(): for batch_texts, batch_labels in test_loader: seqs torch.tensor([text_to_sequence(t, word2idx) for t in batch_texts]) labels torch.tensor(batch_labels) outputs model(seqs) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() print(test accuracy:, correct / total)代码里torch.no_grad()是推理模式的关键不需要计算梯度能显著减少显存占用。model.eval()会把 dropout 关掉避免推理时随机失活影响结果。text_to_sequence这一步对每条测试样本做一次分词和序列转换如果测试集有 1 万条建议提前把所有样本转换成序列并保存避免在评估时重复分词浪费时间。6.3 调参策略先保证 pipeline 通再谈精度这份资源的整体流程跑通之后我的调参顺序是先调 TextCNN 的超参再回头调 Word2Vec。因为 TextCNN 的卷积核数量、尺寸、dropout 对分类结果的直接影响更大而 Word2Vec 更多是提供初始化的语义底座。具体来说我会固定 Word2Vec 参数vector_size200,window5然后跑一组 TextCNN 参数组合filter_sizes 分别试[3,4,5]和[2,3,4]num_filters 试 100 和 200dropout 试 0.3 和 0.5。这样一共 8 组实验每组大概 15 分钟能跑完观察测试集准确率的差异。另外如果分类效果不理想先别急着调模型检查一下训练集和测试集的标签分布是否一致。如果训练集里某类占 60%测试集里只占 20%模型精度差是正常的。这种时候先做分层采样或类别加权再回来调模型。从那以后我做文本分类前都强制走一遍标签分布统计、序列长度分位统计、词表覆盖率这三板斧。这套流程帮我省了大量排错时间希望帮到你。本文还有配套的精品资源点击获取