ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

词向量与CNN结合的短文本分类:从word2vec训练到TextCNN模型实践

词向量与CNN结合的短文本分类:从word2vec训练到TextCNN模型实践 简介面向自然语言处理与生物信息学交叉领域的学习者这份资源以CNN结合word2vec完成基因序列分类任务整合CBOW与Skip-gram词向量训练、多尺寸卷积核局部特征提取覆盖从原始序列处理、词嵌入到池化、全连接分类的完整技术链路。压缩包共5个文件约174KB包含Python脚本、自定义词表、FASTA格式的Rice_880序列数据以及模型架构图和损失曲线图虽然体积不大但代码、数据、可视化一应俱全脚本负责模型构建与训练词表提供词汇映射FASTA为输入序列图片则分别用于结构说明与训练监控。已有168人浏览学习适合作为NLP与生物信息学交叉课题的入门参考。读者可对照脚本代码理解数据预处理、word2vec词向量生成、卷积与池化参数设置等关键环节借助模型架构图梳理网络层次利用损失曲线判断训练收敛情况从而快速复现一个可运行的序列分类基线系统也可将思路迁移到情感分析、文本主题识别等序列分类场景。1. 序列分类这道题为什么我选了 CNN 加 word2vec做序列分类大部分人第一反应是 LSTM 或者 Transformer。但如果你手里的任务是短文本、定长日志、工单意图或者评论打标数据量又只有几万条那我更推荐先用 CNN 加 word2vec 把基线跑起来。原因很简单它训练快、参数少、对硬件要求低而且在短序列上效果往往不比循环网络差。这个方向近几年的热度一直没降过核心就是先离线训好 word2vec 词向量再用卷积神经网络去捕捉序列里的局部特征后续甚至能扩展成 TextCNN、双通道 CNN 等结构。这篇笔记会带你过一遍完整流程从词向量训练到模型落地代码可以直接抄。2. 先把词向量搞好word2vec 训练与选型细节2.1 词向量从哪来自己训还是拿现成的在把文本交给 CNN 之前得先想清楚怎么把词语变成向量。常见做法有两种一是直接用预训练好的词向量文件比如 Tencent AI Lab Embedding 或者百度百科向量二是用自己的语料离线训一套 word2vec。我一般只在两种情况下用现成的手里连原始文本都没有或者只是想两小时跑通一个 demo。一旦你手里有行业语料比如客服对话、医疗报告、代码日志自己训几乎总是更好因为领域词、错别字、英文缩写这些在通用词向量里往往没有靠谱的表达。自己训的好处在于可控比如你可以决定向量维度、上下文窗口大小还能把未登录词的覆盖率压得很低。坏处是语料不够的时候词向量质量会比通用预训练差这个没什么玄学数据量不够就是不够。这里有个可行的折中方案用现成词向量做初始化再在自己的语料上做增量训练这样既拿到通用语义又能吸收领域词汇。用 gensim 就能实现加载预训练模型后继续训练但要控制学习率这个稍后再展开。2.2 训练 word2vec 的参数怎么定训练 word2vec 时参数选择直接决定向量质量。下面的代码是一个最基础的训练脚本语料是一行一条的分好词的文本文件每一行是空格分隔的 token 序列。import jieba from gensim.models import Word2Vec from gensim.models.word2vec import LineSentence # 原始语料分词输出到文件 with open(corpus.txt, r, encodingutf-8) as f_in, \ open(corpus_seg.txt, w, encodingutf-8) as f_out: for line in f_in: seg_list jieba.cut(line.strip()) f_out.write( .join(seg_list) \n) # 训练词向量 model Word2Vec( LineSentence(corpus_seg.txt), vector_size200, # 向量维度常见 100~300 window5, # 上下文窗口 min_count5, # 过滤低频词 sg1, # 1 用 skip-gram0 用 CBOW epochs10, # 训练轮数 workers4, # 并行线程 negative10, # 负采样数量 sample1e-5, # 高频词下采样阈值 ) model.save(w2v.model)参数选择逻辑是这样的sg 选 skip-gram 适合中小规模语料它对低频词的表达更友好在大语料上才会考虑换 CBOW 追求速度。vector_size 我一般先拉 200小数据集用 100 也行但要记住维度太低表达不了复杂语义太高在小语料上又容易过拟合。window 设 5 对短文本合适如果句子里存在长距离依赖可以调到 8但 CNN 本身是局部建模窗口太长了意义有限。min_count 设 5 是为了过滤拼写错误和只出现一两次的生僻词避免它们拉低向量质量。训练完成后建议先在验证集上做一次相似词检查比如查“退货”的 top10 相似词看结果是否符合同义、近义、相关词的预期。这一步比看 loss 曲线有用得多词向量是下游任务的上游这里翻车后面全白搭。3. 把词向量接到 CNNEmbedding、卷积与池化的落地做法3.1 输入侧处理分词、词典、定长序列词向量训好之后下一步是构建模型输入。CNN 不像 LSTM 那样能直接吃变长序列必须把每个句子截断或者补齐成同一个长度。这一步很关键因为 CNN 的卷积核是在空间维度上滑动的序列长度不一致会导致 batch 内张量形状冲突。import numpy as np from tensorflow.keras.preprocessing.sequence import pad_sequences # 构建 词-id 映射 word2idx {word: idx 2 for idx, word in enumerate(model.wv.index_to_key)} word2idx[PAD] 0 word2idx[UNK] 1 def encode_sentence(sentence, max_len50): # 分词后映射为 id 序列 tokens jieba.lcut(sentence.strip()) ids [word2idx.get(w, word2idx[UNK]) for w in tokens] # 截断 paddingpad 位置放在后面 ids ids[:max_len] ids ids [word2idx[PAD]] * (max_len - len(ids)) return np.array(ids, dtypenp.int32) # 示例 sample_ids encode_sentence(这件衣服质量不错就是物流慢, max_len50) print(sample_ids[:10])这里要提两个细节。第一UNK必须有因为线上预测时大概率会遇到训练集没见过的词没有这个映射就只能报错。第二padding 的方向值得斟酌我习惯把 pad 加在后面即 post-padding这样卷积核在滑动时先看到真实词再看到 pad 位对短文本更友好有些场景 pre-padding 也能用但会让 CNN 前几个卷积步长落在空位上等于白算。max_len 的选取要看你数据里句子长度的分布先统计一下 p95 的长度再往上留一点余量比如 95% 的样本不足 60 个 token那就设 64不要盲目设 200。3.2 构建 CNN 分类网络从 Embedding 层到输出层词向量接 CNN 的核心思路Embedding 层把 token id 映射成稠密向量然后多组不同宽度的卷积核并行扫描句子抓取 n-gram 级别的局部特征再用全局池化把每个卷积核产生的特征向量压成一个标量最后拼起来过全连接层。这里的直觉是2-gram、3-gram、4-gram 的卷积核分别对应词组、短语和短句模式比单一宽度灵话。import tensorflow as tf from tensorflow.keras import Input, Model from tensorflow.keras.layers import Embedding, Conv1D, GlobalMaxPooling1D, Concatenate, Dense, Dropout embedding_dim 200 vocab_size len(word2idx) num_filters 128 filter_sizes [2, 3, 4] max_len 50 # 输入是 padding 后的序列 inputs Input(shape(max_len,), dtypeint32) # 加载 word2vec 向量作为初始权重 embedding_matrix np.zeros((vocab_size, embedding_dim)) for word, idx in word2idx.items(): if word in model.wv: embedding_matrix[idx] model.wv[word] embedding Embedding( input_dimvocab_size, output_dimembedding_dim, weights[embedding_matrix], trainableTrue, # 置 True 表示训练中微调词向量 mask_zeroTrue, # 跳过 padding 位置的计算 )(inputs) # 多宽度卷积 全局最大池化 conv_blocks [] for fsize in filter_sizes: conv Conv1D(filtersnum_filters, kernel_sizefsize, activationrelu)(embedding) pooled GlobalMaxPooling1D()(conv) conv_blocks.append(pooled) # 拼接所有池化结果 merged Concatenate()(conv_blocks if len(conv_blocks) 1 else conv_blocks[0]) merged Dropout(0.5)(merged) outputs Dense(1, activationsigmoid)(merged) # 二分类场景多分类改用 softmax model Model(inputs, outputs) model.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy]) model.summary()这段代码里有几个参数值得说明。第一trainableTrue我建议默认开着因为在训练过程中让词向量微调往往能涨两三个点只有数据量极其稀少时才冻结 Embedding 层防止灾难性遗忘。第二mask_zeroTrue配合 max-pooling能让 padding 位置的卷积结果不被池化选中这是 TextCNN 里容易被忽略的细节。第三num_filters设 128 对大多数任务够用数据量上去再调大小数据设太大只会增加过拟合风险。relu 激活是标配换别的非线性对结果影响不大。3.3 训练与评估先看 loss 再看准确率训练时我习惯把数据先切出 10% 做验证集而且要保证类别分布大致一致这里用sklearn的train_test_split加上stratify参数就能搞定。下面这段是训练与验证的最小闭环。from sklearn.model_selection import train_test_split X_train, X_val, y_train, y_val train_test_split( X_ids, y_labels, test_size0.1, stratifyy_labels, random_state42 ) history model.fit( X_train, y_train, validation_data(X_val, y_val), batch_size64, epochs20, callbacks[ tf.keras.callbacks.EarlyStopping(patience3, restore_best_weightsTrue), tf.keras.callbacks.ReduceLROnPlateau(factor0.5, patience2, verbose1), ] )batch_size 在序列长度 50、向量维度 200 的条件下设 64 比较稳显存紧张就降到 32。EarlyStopping 的 patience 设 3意思是验证集 loss 连续 3 个 epoch 不降就停。ReduceLROnPlateau 则是当训练进入平台期时把学习率砍半这个操作比手动调学习率省事得多实测中经常能在第 10 个 epoch 后让 loss 再降一截。训练完成后别急着看准确率先对比训练集和验证集的 loss如果训练 loss 远低于验证 loss说明过拟了如果两个都高先查输入侧的编码是否有 bug再考虑调参。4. 序列分类避坑五个真实踩坑记录与修复4.1 现象验证集准确率高达 92%线上 A 榜直接崩到 60%原因分析训练集和测试集来自不同时间段线上文本里有大量训练集没出现的新词、新句式。当时我没有把训练语料里低频词过滤干净也没有考虑领域迁移导致模型在已知分布上过拟合碰到新分布就失效。解决在训练词向量时就按 min_count 过滤掉低频词同时显式加上UNK映射线上预测时所有未登录词一律落到UNK不要硬编码一个随机向量。4.2 现象Embedding 层加mask_zeroTrue后 loss 变成 NaN原因分析mask_zeroTrue在某些 GPU 和 tf.keras 版本组合下与后续的 Conv1D 存在兼容性问题特别是卷积核宽度大于 1 且 padding 方式为valid时mask 信息不能正确传导到池化层。解决要么把mask_zero关掉通过传入无 padding 的原始序列并手动做 mask 计算要么升级 TF 版本。更稳妥的做法是在 Embedding 层不启用 mask直接用 GlobalMaxPooling1D因为 pad 位置的词向量恰好是零向量经过卷积和 relu 之后池化层天然会忽略全零区域。4.3 现象验证集准确率稳定但类别少的样本几乎全错原因分析典型的不均衡分类问题。比如工单数据里“投诉”只占 1%二分类准确率再高都是被多数类带跑的。准确率这个指标在这种场景下没有参考意义。解决评估指标换成 F1-score、recall 和 confusion matrix训练时给少数类加class_weight。下面是 Keras 里设置类别权重的方式from sklearn.utils.class_weight import compute_class_weight class_weights compute_class_weight( class_weightbalanced, classesnp.unique(y_train), yy_train ) class_weight_dict {i: w for i, w in zip(np.unique(y_train), class_weights)} model.fit(X_train, y_train, class_weightclass_weight_dict, validation_data(X_val, y_val))compute_class_weight会自动按类别频率倒数折算权重少数类样本在计算 loss 时获得更大惩罚梯度更新时会更偏向把它们分对。这个方法比人工指定权重省心但注意权重过大反而会让模型震荡我一般限制权重最大值不超过 10。4.4 现象同样的代码别人跑出 88%我跑出 76%原因分析嵌入层的初始化权重不同。np.zeros初始化会破坏词向量语义别人可能用了model.wv转换时保持向量不变而我用零矩阵然后把PAD和UNK置零导致所有未知词在初始化时都是全零向量相当于给模型注入大量噪音。解决初始化矩阵时对不在词表中的词用很小的随机值初始化而对PAD才用零向量。修改方法很简单embedding_matrix np.random.uniform(-0.05, 0.05, (vocab_size, embedding_dim)) embedding_matrix[word2idx[PAD]] 0.0 for word, idx in word2idx.items(): if word in model.wv: embedding_matrix[idx] model.wv[word]4.5 现象训练一轮非常慢batch 跑完 GPU 利用率只有 30%原因分析问题不在模型而在数据处理管线。每个 epoch 都在重新调用jieba.cut做分词还把分词结果反复转成 idCPU 成为瓶颈GPU 一直空转。解决把分词、编码一次性离线处理完成存成.npy或者 TFRecord训练时直接从磁盘读整数序列。我第一次跑这个模型时也在循环里调分词接口一轮训练下来 90% 的时间都在等 CPU改成离线预处理后速度提升超过 6 倍。5. 进阶验证与调优用混淆矩阵和坏例分析指导下一步模型训练完只是第一步真正决定上线质量的是验证方法。我的固定习惯是先输出混淆矩阵再逐个看坏例最后再决定是加数据、调结构还是换模型。下面的代码输出混淆矩阵和部分预测错误的样本from sklearn.metrics import confusion_matrix, classification_report y_pred (model.predict(X_val) 0.5).astype(int) print(classification_report(y_val, y_pred)) # 输出错误样本 errors np.where(y_pred.flatten() ! y_val.flatten())[0] for i in errors[:10]: sent id2sentence(X_val[i], word2idx) # 从 id 还原回文本 print(flabel{y_val[i]}, pred{y_pred[i][0]}, text{sent})坏例分析有几个高频方向如果错例集中在同一类说明特征不够需要补充该类别的训练数据如果错例都是相似句式但语义相反比如“不是不好”和“不好”说明模型没学到否定结构这时可以考虑把 2-gram 之外的 3-gram、4-gram 卷积核加宽或者干脆把句子里的否定词和程度副词标出来做特征拼接。还有一个习惯值得一提不要只盯着准确率在样本不均衡时用 PR 曲线的面积和 F1 做调参指标会比准确率更早暴露问题。调参顺序上我建议先动 Embedding 层的trainable开关、滤波器数量和卷积核宽度再动 dropout 和 batch size。学习率用 Adam 默认的 0.001 起步不要一上来就动它。如果发现验证集准确率在 20 个 epoch 后仍然爬升很慢先检查是不是词向量本身质量差这一步可以用model.wv.similarity(好评, 满意)这类内积去验证。从做这个项目开始我养成了每改一次参数就单独存一份实验记录的习惯包含词向量参数、CNN 结构、训练超参数、验证集 F1以及坏例截图。这样每次复盘都有据可查不会陷入“感觉自己加了层但是忘了改了什么参数”的状态。现在你手里的这份流程从我第一次跑通到现在过了两年仍然是我做短文本分类的默认起点。希望帮到你落地过程遇到问题可以顺着这篇笔记里的细节倒查大多数坑都写在上面的避坑章节里了。本文还有配套的精品资源点击获取
返回列表