020、文本分类与情感分析项目实战:从数据坑到模型部署的硬核笔记
一、深夜调bug为什么我的准确率卡在50%上周三凌晨两点盯着屏幕上的二分类验证集结果发愣——正负样本各50%模型死活学不进去。第一反应是模型结构有问题换了三个预训练模型还是老样子。直到把训练集的前100条样本打印出来# 错误示范直接读文件不做检查withopen(reviews.txt,r,encodingutf-8)asf:textsf.readlines()labels[int(line.strip().split(\t)[1])forlineintexts]# 这里踩过大坑# 调试时一定要加这个print(标签分布:,Counter(labels))print(前5条样本:,texts[:5])结果发现文件里混进了空行和格式错误的行标签列有的样本跑到文本里去了。文本处理的第一原则永远假设你的数据是脏的。后来改成这样defload_data_with_sanity_check(filepath):texts,labels[],[]withopen(filepath,r,encodingutf-8)asf:fori,lineinenumerate(f):lineline.strip()ifnotline:# 跳过空行continuepartsline.split(\t)iflen(parts)!2:# 格式检查print(f第{i}行格式异常:{line[:50]}...)continuetext,labelpartsiflabelnotin[0,1]:# 标签合法性检查print(f第{i}行标签异常:{label})continuetexts.append(text)labels.append(int(label))returntexts,labels数据清洗占整个项目60%的时间但能避免后面80%的诡异问题。二、别急着上BERT从TF-IDF朴素贝叶斯开始新手最容易犯的错就是直接套BERT结果训练三天发现小样本过拟合。实战中应该建立基线模型阶梯# 第一级词袋模型经典分类器30分钟出结果fromsklearn.feature_extraction.textimportTfidfVectorizerfromsklearn.naive_bayesimportMultinomialNBfromsklearn.pipelineimportmake_pipeline# 关键参数限制特征数量别让维度爆炸vectorizerTfidfVectorizer(max_features5000,ngram_range(1,2),# 加入二元词组stop_wordsenglish)# 英文记得去停用词clfMultinomialNB(alpha0.1)# 平滑参数调一下pipelinemake_pipeline(vectorizer,clf)pipeline.fit(train_texts,train_labels)# 看一眼特征重要性可解释性feature_namesvectorizer.get_feature_names_out()coefsclf.coef_[0]top_10sorted(zip(feature_names,coefs),keylambdax:x[1],reverseTrue)[:10]print(正向重要词:,top_10)这个基线模型在IMDB数据集上能做到85%左右足够验证数据 pipeline 是否正常。如果连朴素贝叶斯都学不会数据大概率有问题。三、Embedding层的选择别盲目用预训练当数据量小于1万条时静态词向量Word2Vec/GloVe往往比微调BERT更稳定# 加载预训练词向量准备一个fallback机制defload_embeddings(embedding_path):embeddings_index{}try:withopen(embedding_path,r,encodingutf-8)asf:forlineinf:valuesline.split()wordvalues[0]try:coefsnp.asarray(values[1:],dtypefloat32)embeddings_index[word]coefsexceptValueError:continue# 跳过格式错误的行exceptFileNotFoundError:print(警告预训练词向量文件不存在使用随机初始化)returnNonereturnembeddings_index# 构建Embedding矩阵embedding_dim300embedding_matrixnp.random.randn(vocab_size,embedding_dim)*0.1# 随机初始化ifembeddings_index:forword,iintokenizer.word_index.items():ifivocab_size:breakembedding_vectorembeddings_index.get(word)ifembedding_vectorisnotNone:embedding_matrix[i]embedding_vectorprint(f覆盖了{np.sum(np.any(embedding_matrix!0,axis1))/vocab_size:.1%}的词汇)实际项目中中文领域很多垂直行业如医疗、金融的术语在通用预训练向量里找不到这时候用领域语料训练一个小的Word2Vec反而更有效。四、LSTM不是万能的试试CNN和注意力文本分类不等于LSTM特别是当句子长度小于100时CNN速度更快效果也不差defbuild_text_cnn(max_len,vocab_size,embedding_dim):inputsInput(shape(max_len,))# Embedding层建议加mask_zeroTruexEmbedding(vocab_size,embedding_dim,embeddings_initializerConstant(embedding_matrix),mask_zeroTrue)(inputs)# 多尺度卷积核抓不同长度的n-gram特征conv_blocks[]forkernel_sizein[3,4,5]:convConv1D(filters128,kernel_sizekernel_size,paddingsame,activationrelu)(x)poolGlobalMaxPooling1D()(conv)# 全局池化替代Flattenconv_blocks.append(pool)xConcatenate()(conv_blocks)iflen(conv_blocks)1elseconv_blocks[0]xDropout(0.5)(x)# 文本任务Dropout要高一点xDense(64,activationrelu)(x)outputsDense(1,activationsigmoid)(x)modelModel(inputsinputs,outputsoutputs)returnmodel经验值短文本如评论、标题用CNN长文本如文档、文章用Transformer编码器序列标注任务才用LSTM/GRU。五、标签噪声处理真实场景的必修课用户标注的情感标签有30%可能是错的比如反讽标注成正面。两种实用方法# 方法1置信度过滤简单有效probasmodel.predict_proba(val_texts)confidence_threshold0.8high_conf_idxnp.where((probasconfidence_threshold)|(probas1-confidence_threshold))[0]clean_texts[val_texts[i]foriinhigh_conf_idx]clean_labels[1ifprobas[i]0.5else0foriinhigh_conf_idx]# 方法2噪声标签学习Co-teaching框架classCoTeachingModel:def__init__(self,model1,model2):self.model1model1 self.model2model2deftrain_step(self,texts,labels,forget_rate0.3):# 两个模型分别预测prob1self.model1.predict(texts)prob2self.model2.predict(texts)# 互相筛选低loss样本给对方训练loss1cross_entropy(labels,prob1)loss2cross_entropy(labels,prob2)idx1np.argsort(loss1)[:int(len(texts)*(1-forget_rate))]idx2np.argsort(loss2)[:int(len(texts)*(1-forget_rate))]# 用筛选后的样本更新self.model1.update(texts[idx2],labels[idx2])self.model2.update(texts[idx1],labels[idx1])生产环境里宁可模型保守一点也要保证高置信度样本的准确性。六、部署时的内存陷阱实验室能跑不等于生产能跑。第一次部署时OOM内存溢出的教训# 错误一次性加载所有数据做预测defpredict_batch(texts):tokenstokenizer(texts,paddingTrue,truncationTrue)# 全部文本同时编码returnmodel(tokens)# 内存爆炸# 正确流式处理defpredict_stream(texts,batch_size32):results[]foriinrange(0,len(texts),batch_size):batchtexts[i:ibatch_size]tokenstokenizer(batch,paddingTrue,truncationTrue,max_length128)# 必须限制长度batch_predmodel(tokens)results.extend(batch_pred)deltokens# 显式释放内存ifi%1000:gc.collect()# 定期垃圾回收returnresults关键参数max_length必须根据业务场景设置微博128够用长文章可能需要512batch_size从32开始试。七、个人经验包数据质量检查清单标签分布、文本长度分布、特殊字符比例、重复样本、类别平衡性。每项不达标都要处理。模型选择路线图样本1000TF-IDF SVM/朴素贝叶斯1000~10000Word2Vec TextCNN10000~50000微调BERT前几层50000完整预训练模型微调调参优先级第一梯队学习率用余弦退火、batch_size影响泛化第二梯队Dropout率、优化器类型第三梯队网络层数、神经元数量最后才动损失函数交叉熵在90%场景够用上线前必须测试极端输入空字符串、超长文本、特殊字符、中英文混合压力测试连续预测1000次的显存/内存占用一致性相同输入多次预测结果是否一致关闭Dropout可解释性不能少哪怕只用LIME对关键样本做解释也比完全黑箱强。产品经理问“为什么判为负面”时你能指出是哪个词触发的。文本分类就像做菜数据是食材模型是厨具。米其林厨具做不出烂食材的好菜但好食材用普通炒锅也能出味。先花时间把数据洗干净比换十个SOTA模型都管用。