ARTICLE DETAIL

资讯详情

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

中文BERT情感分类全链路工程实践:从Tokenization到Docker部署

中文BERT情感分类全链路工程实践:从Tokenization到Docker部署 简介本资源是一套面向自然语言处理初学者与进阶实践者的中文情感分类完整实验方案聚焦BERT模型在真实中文文本场景下的落地应用。项目以情感分析为任务主线提供从数据预处理、模型微调、特征提取到预测部署的全流程Python实现覆盖NLP核心技能链特别适合课程设计、科研复现及工程化入门学习。压缩包共22个文件含11个关键Python脚本如run_classifier.py、predict.py、tokenization.py等、2个CSV数据集、2个Shell执行脚本、3个说明类文本及Markdown文档整体4.87MB结构清晰、模块解耦便于按训练/预测/数据/配置分层理解。已有312人学习下载读者可直接复现实验结果获取可调试的BERT中文分类源码、标准化数据处理流程、详尽的实验记录与环境依赖说明requirements.txt并参考readme.txt和multilingual.md快速掌握项目架构与扩展路径。1. 这不是调个 pretrain 模型就完事的“情感分类”一个真实跑通中文 BERT 分类链路的完整工程切片你手头刚拿到一份标着“BERT 中文情感分类”的压缩包解压后看到run_classifier.py、train.csv、predict.py第一反应可能是“哦改下路径pip install -r requirements.txtpython run_classifier.py —data_dir ./data —bert_model bert-base-chinese … 然后坐等 acc 上 92%”现实是你大概率会在第 3 步卡住——tokenization.py 报KeyError: ##好第 5 步崩溃——ValueError: Input mask has wrong shape第 7 步怀疑人生——预测结果全是中性连“太差了”和“绝了”都分不清。这不是模型不行而是这份资源根本不是“玩具 demo”而是一份从预训练语料清洗、中文子词切分适配、下游任务微调结构定义、到推理服务封装全链路可复现的工程切片。它包含 11 个 Python 脚本含modeling.py的自定义层、tokenization.py的中文 WordPiece 实现、2 个 Shell 脚本train.sh和predict.sh封装了真实生产环境的启动逻辑、以及dev.csv/train_sentiment.txt这类带原始标注噪声的中文数据——它们不是 Kaggle 那种干净 CSV而是从电商评论、微博短文本里爬下来、带 emoji、带错别字、带中英混排的真实语料。适合谁适合正在写毕设却卡在“BERT 怎么真正训中文”的研究生适合想把 NLP 模型落地到客服工单情绪识别但被 tokenization 坑过三次的算法工程师也适合刚学完《动手学深度学习》、想亲手拆开 BERT 微调黑匣子的 Python 开发者。它不教你 Attention 是什么但它会告诉你为什么max_seq_length128在中文场景下必须砍到 64 才不 OOM为什么train_sentiment.txt里的“一般般”要被 tokenizer 切成[一, 般, 般]而不是[一般, 般]以及predict.sh里那行--do_lower_caseFalse是救你于中文大小写误判的后悔药。2. 从train.sh到run_classifier.py一条不能跳过的微调执行链2.1train.sh不只是 shell 脚本它是环境与参数的契约train.sh是整个训练流程的入口它不是简单地调用python run_classifier.py而是通过显式声明所有关键参数强制你在复现前直面每一个决策点。打开它你会看到#!/bin/bash export CUDA_VISIBLE_DEVICES0 python run_classifier.py \ --task_namesentiment \ --do_traintrue \ --do_evaltrue \ --data_dir./data \ --vocab_file./chinese_L-12_H-768_A-12/vocab.txt \ --bert_config_file./chinese_L-12_H-768_A-12/bert_config.json \ --init_checkpoint./chinese_L-12_H-768_A-12/pytorch_model.bin \ --max_seq_length64 \ --train_batch_size32 \ --learning_rate2e-5 \ --num_train_epochs3.0 \ --output_dir./output/sentiment_model/注意--vocab_file指向的是chinese_L-12_H-768_A-12/vocab.txt这是 Google 官方发布的中文 BERT Base 模型词表而非英文版。如果你直接用bert-base-uncased的 vocabtokenization.py会在遇到“的”“了”等高频字时抛出KeyError——因为英文 vocab 根本没有这些 token。--max_seq_length64是硬性要求中文平均字数密度远高于英文128在 batch_size32 下极易触发 CUDA out of memory这是实测血泪经验不是拍脑袋。这个脚本的核心价值在于固化环境变量CUDA_VISIBLE_DEVICES和所有超参。它告诉你这不是一个“改改路径就能跑”的 demo而是一个需要你明确指定 GPU 设备、词表路径、配置文件、初始化权重的完整训练契约。删掉export CUDA_VISIBLE_DEVICES0你的训练可能静默占用所有卡漏掉--do_evaltrue你将永远看不到验证集 loss 曲线无法判断是否过拟合。2.2run_classifier.py微调任务的骨架Processor是灵魂run_classifier.py是 BERT 官方run_pretraining.py的下游任务适配器。它的核心逻辑是加载 BERT 模型 → 构建分类 head → 读取数据 → 训练/评估。但真正决定你能否跑通的是它依赖的DataProcessor子类。项目里没有明说但根据train_sentiment.txt和dev.csv的格式sentimenttask 对应的 processor 必然在run_classifier.py内部或intent.py中定义。我们来还原它# intent.py (推断存在因项目正文列出 intent.py) class SentimentProcessor(DataProcessor): Processor for the Chinese sentiment classification dataset. def get_train_examples(self, data_dir): # 读取 train_sentiment.txt每行格式label\ttext # 注意label 可能是 0/1/2 或 negative/neutral/positive return self._create_examples( self._read_tsv(os.path.join(data_dir, train_sentiment.txt)), train) def get_dev_examples(self, data_dir): # 读取 dev.csvpandas 读取后需处理 header 和 label 列名 df pd.read_csv(os.path.join(data_dir, dev.csv)) # 假设列名为 text 和 label且 label 是 int 类型 examples [] for i, row in df.iterrows(): examples.append( InputExample(guidfdev-{i}, text_astr(row[text]), labelstr(row[label]))) return examples def get_labels(self): # 必须返回字符串列表BERT classifier head 依赖此定义输出维度 return [0, 1, 2] # 或 [negative, neutral, positive]逻辑说明get_labels()返回值直接决定分类头的num_labels。如果train_sentiment.txt里 label 是0/1/2但get_labels()返回[negative,positive]模型最后一层会报维度不匹配。_create_examples()中text_astr(row[text])强制转字符串是为了防止 CSV 中空单元格读成nan导致 tokenizer 崩溃。InputExample的guid字段虽不参与计算但在 debug 时可通过它定位出错的具体样本行号。2.3modeling.py不是黑盒是可调试的模型定义modeling.py是整个项目的基石。它不是简单import transformers而是实现了 BERT 的核心结构BertModel、BertConfig、BertLayer。重点看BertForSequenceClassification类# modeling.py class BertForSequenceClassification(BertPreTrainedModel): def __init__(self, config, num_labels3): super(BertForSequenceClassification, self).__init__(config) self.num_labels num_labels self.bert BertModel(config) # 加载预训练 BERT encoder self.dropout nn.Dropout(config.hidden_dropout_prob) self.classifier nn.Linear(config.hidden_size, num_labels) # 自定义分类头 self.apply(self.init_weights) # 权重初始化 def forward(self, input_ids, token_type_idsNone, attention_maskNone, labelsNone): # 1. BERT encoder 输出 [batch, seq_len, hidden_size] outputs self.bert(input_ids, token_type_ids, attention_mask) pooled_output outputs[1] # 取 [CLS] token 的 pooled output # 2. dropout classifier pooled_output self.dropout(pooled_output) logits self.classifier(pooled_output) if labels is not None: loss_fct CrossEntropyLoss() loss loss_fct(logits.view(-1, self.num_labels), labels.view(-1)) return loss, logits else: return logits参数说明num_labels3必须与SentimentProcessor.get_labels()返回长度一致。pooled_output outputs[1]是关键——outputs[0]是所有 token 的 hidden statesoutputs[1]是[CLS]token 经过 pooling layer 后的向量这才是分类任务的标准输入。如果你误用outputs[0][:, 0, :]取第一个 token在某些 BERT 版本下会因维度对齐问题报错。self.apply(self.init_weights)确保新添加的classifier层使用正确的 Xavier 初始化否则新层权重为零会导致训练初期 loss 不下降。3.tokenization.py与extract_features.py中文文本的生死线3.1tokenization.pyWordPiece 切分不是魔法是查表规则BERT 的中文能力90% 依赖tokenization.py。它不是调用 jieba而是实现 Google 的 WordPiece 算法。核心函数convert_to_unicode()和FullTokenizer# tokenization.py def convert_to_unicode(text): Converts text to Unicode (if its not already), assuming utf-8 input. if isinstance(text, str): return text elif isinstance(text, bytes): return text.decode(utf-8, ignore) else: raise ValueError(Unsupported string type: %s % (type(text))) class FullTokenizer(object): def __init__(self, vocab_file, do_lower_caseTrue): self.vocab load_vocab(vocab_file) # 加载 vocab.txt 到 dict: {token: id} self.inv_vocab {v: k for k, v in self.vocab.items()} self.basic_tokenizer BasicTokenizer(do_lower_casedo_lower_case) self.wordpiece_tokenizer WordpieceTokenizer(vocabself.vocab) def tokenize(self, text): split_tokens [] for token in self.basic_tokenizer.tokenize(text): # basic_tokenizer 先做基础清洗去空格、标点归一化、中文字符保留 for sub_token in self.wordpiece_tokenizer.tokenize(token): # wordpiece_tokenizer 对每个 token 做子词切分 split_tokens.append(sub_token) return split_tokens逻辑说明convert_to_unicode()是防御性编程——确保输入是 str避免bytes导致后续.split()失败。BasicTokenizer对中文的作用是保留所有汉字、数字、字母将标点如“”“。”单独切分为 token不合并。WordpieceTokenizer才是核心它查vocab.txt优先匹配最长子串。例如“非常好”在 vocab 中有[非常, 好]则切分为[非常, 好]若只有[非, 常, 好]则切为[非, 常, 好]。vocab.txt里##好这种形式是 WordPiece 的子词标记表示“好”作为后缀KeyError: ##好的本质是你的vocab.txt文件损坏或路径错误导致load_vocab()返回空 dict。3.2extract_features.py特征提取不是可选步骤是推理前置extract_features.py的作用是对原始文本输出 BERT 的各层 hidden states供下游任务如相似度计算、特征聚类使用。它与run_classifier.py的区别在于不训练只前向传播。# extract_features.py def main(_): tokenizer tokenization.FullTokenizer( vocab_fileFLAGS.vocab_file, do_lower_caseFLAGS.do_lower_case) examples read_examples(FLAGS.input_file) # 读取 .txt每行一个文本 features convert_examples_to_features( examplesexamples, tokenizertokenizer, max_seq_lengthFLAGS.max_seq_length, doc_strideFLAGS.doc_stride) model_fn model_fn_builder( bert_configmodeling.BertConfig.from_json_file(FLAGS.bert_config_file), init_checkpointFLAGS.init_checkpoint, layer_indexes[-1, -2, -3, -4], # 提取最后4层 use_tpuFalse, use_one_hot_embeddingsFalse) # ... 构建 estimator 并 predict参数说明layer_indexes[-1, -2, -3, -4]表示提取倒数四层的 hidden states即第12、11、10、9层。这是经验选择底层捕获字形/语法高层捕获语义情感分类通常用高层特征。doc_stride用于长文本分段滑动如一篇 500 字评论但中文情感文本普遍 100 字此处doc_stride可设为 0。convert_examples_to_features()会调用tokenizer.tokenize()并生成input_ids、input_mask、segment_ids三元组——这正是run_classifier.py训练时喂给模型的数据格式。如果你的predict.py报Input mask has wrong shape90% 是因为predict.py里max_seq_length与train.sh中不一致导致 padding 长度不同。3.3predict.py如何让训练好的模型真正“说话”predict.py是推理脚本它加载./output/sentiment_model/下的 checkpoint并对新文本做预测# predict.py def main(_): tokenizer tokenization.FullTokenizer( vocab_fileFLAGS.vocab_file, do_lower_caseFLAGS.do_lower_case) # 1. 加载模型 model modeling.BertForSequenceClassification.from_pretrained( FLAGS.init_checkpoint, num_labelslen(processor.get_labels())) # 2. 对输入文本预处理 text 这个手机太卡了完全不想再用了 tokens tokenizer.tokenize(text) # 截断 添加 [CLS] [SEP] if len(tokens) FLAGS.max_seq_length - 2: tokens tokens[0:(FLAGS.max_seq_length - 2)] tokens [[CLS]] tokens [[SEP]] input_ids tokenizer.convert_tokens_to_ids(tokens) input_mask [1] * len(input_ids) segment_ids [0] * len(input_ids) # 3. padding while len(input_ids) FLAGS.max_seq_length: input_ids.append(0) input_mask.append(0) segment_ids.append(0) # 4. 转 tensor 并预测 input_ids torch.tensor([input_ids], dtypetorch.long) input_mask torch.tensor([input_mask], dtypetorch.long) segment_ids torch.tensor([segment_ids], dtypetorch.long) with torch.no_grad(): logits model(input_ids, segment_ids, input_mask) predictions torch.nn.functional.softmax(logits, dim-1) predicted_class torch.argmax(predictions, dim-1).item() print(fText: {text}) print(fPredicted class: {predicted_class}, Prob: {predictions[0][predicted_class].item():.4f})关键点tokenizer.convert_tokens_to_ids(tokens)将[[CLS], 这, 个, 手, 机, 太, 卡, 了, ...]转为[101, 2769, 1581, 3221, 776, 1745, 1372, 1110, ...]ID 101 是[CLS]的固定 ID。input_mask全 1 表示有效 tokenpadding 位置为 0。segment_ids全 0 因为单句任务无需区分 A/B 句。这里max_seq_length必须与train.sh中完全一致否则input_ids长度不匹配model()调用直接报错。4. 避坑那些让你在凌晨三点对着 terminal 咬牙切齿的 4 个真实翻车现场4.1 现象tokenization.py报KeyError: ##好原因vocab.txt文件缺失、路径错误或你误用了英文 BERT 的 vocabbert-base-uncased/vocab.txt。中文 vocab 中##好是合法子词英文 vocab 中不存在。解决确认--vocab_file参数指向chinese_L-12_H-768_A-12/vocab.txt用head -n 5 chinese_L-12_H-768_A-12/vocab.txt查看前几行应看到[PAD]、[UNK]、[CLS]、[SEP]、[MASK]然后是大量中文字符和##开头的子词。若看到the、and、of说明你用了英文 vocab。4.2 现象run_classifier.py启动后立即 OOMCUDA out of memory原因--train_batch_size32和--max_seq_length128的组合在单张 11G V100 上必然爆显存。中文文本平均长度接近英文 1.5 倍128实际占用显存是英文的 2 倍以上。解决按梯度下降法调整先将--max_seq_length降至64再试--train_batch_size16若仍 OOM再降batch_size到8。记住中文场景下max_seq_length64是安全起点128是奢侈配置。4.3 现象训练 loss 从 1.1 降到 0.01 后突然飙升到 2.5验证 acc 波动剧烈原因train_sentiment.txt和dev.csv的 label 映射不一致。例如train_sentiment.txt用0negative, 1neutral, 2positive但dev.csv的label列是1,2,3或negative,positive,neutral导致get_dev_examples()读入的 label 与get_labels()定义的顺序错位。解决用 pandas 直接检查数据import pandas as pd train_df pd.read_csv(./data/dev.csv) print(train_df[label].value_counts()) # 看实际分布 print(set(train_df[label].astype(str))) # 看字符串集合然后修改SentimentProcessor.get_labels()为[0, 1, 2]或[negative, neutral, positive]确保与数据文件中的字符串完全一致包括大小写、空格。4.4 现象predict.py输出Predicted class: 1, Prob: 0.9999但输入是“垃圾产品退货”原因predict.py中do_lower_case参数与训练时不一致。中文虽无大小写但do_lower_caseFalse是官方中文 BERT 的默认设置若你在train.sh中没显式写--do_lower_caseFalse而predict.py里 tokenizer 初始化用了do_lower_caseTrue则 tokenizer 会把“退货”转为“退货”但 vocab 中存的是“退货”导致convert_tokens_to_ids()返回[0]UNK ID模型输入全为 UNK输出退化为先验分布即多数类。解决统一在所有脚本中显式声明do_lower_caseFalse。在predict.py的FullTokenizer初始化处加tokenizer tokenization.FullTokenizer( vocab_fileFLAGS.vocab_file, do_lower_caseFalse) # 强制 False5.predict.sh与requirements.txt从单次预测到可交付服务的临门一脚5.1predict.sh把模型变成一行命令的生产力predict.sh是predict.py的生产级封装它解决了三个实际问题输入标准化、输出结构化、错误兜底。#!/bin/bash # predict.sh INPUT_FILE${1:-./data/test_input.txt} # 第一个参数为输入文件缺省为 test_input.txt OUTPUT_FILE${2:-./output/predictions.json} # 第二个参数为输出文件 if [ ! -f $INPUT_FILE ]; then echo Error: Input file $INPUT_FILE not found! exit 1 fi # 1. 检查模型目录是否存在 if [ ! -d ./output/sentiment_model/ ]; then echo Error: Model directory ./output/sentiment_model/ not found! Run train.sh first. exit 1 fi # 2. 执行预测捕获 stderr 到 log python predict.py \ --input_file$INPUT_FILE \ --output_file$OUTPUT_FILE \ --vocab_file./chinese_L-12_H-768_A-12/vocab.txt \ --bert_config_file./chinese_L-12_H-768_A-12/bert_config.json \ --init_checkpoint./output/sentiment_model/model.ckpt-1000 \ --max_seq_length64 \ --do_lower_caseFalse \ 2 ./output/predict_error.log # 3. 检查预测是否成功 if [ $? -eq 0 ]; then echo Prediction completed. Results saved to $OUTPUT_FILE # 附加统计各类别数量 python -c import json; with open($OUTPUT_FILE) as f: data json.load(f); from collections import Counter; print(Class distribution:, Counter([x[label] for x in data])) else echo Prediction failed. Check ./output/predict_error.log exit 1 fi价值点INPUT_FILE${1:-./data/test_input.txt}支持命令行传参./predict.sh ./my_comments.txt ./results.json即可批量预测。2 ./output/predict_error.log将所有报错重定向到日志避免 terminal 刷屏丢失关键信息。最后的python -c用一行命令统计预测结果分布让你一眼看出模型是否严重偏向某类如全预测为中性。这已经不是实验脚本而是可嵌入 CI/CD 流程的交付物。5.2requirements.txt版本锁死是复现的唯一护栏requirements.txt内容看似平淡却是跨环境复现的生命线numpy1.19.5 torch1.7.1 tensorflow1.15.0 pandas1.1.5 scikit-learn0.24.1为什么锁死torch1.7.1是关键BERT PyTorch 实现大量使用torch.jit.trace和nn.MultiheadAttention的旧 APItorch1.8会因attn_mask参数变更导致modeling.py中 attention 计算报错。tensorflow1.15.0是为了兼容run_squad.pySQuAD 问答任务该脚本依赖 TF 1.x 的 graph modeTF2.x会直接 import 失败。pandas1.1.5解决read_csv在dev.csv有空行时的解析 bug新版 pandas 默认skip_blank_linesTrue旧版需显式指定。不要pip install -r requirements.txt后就以为万事大吉——用pip list | grep torch确认版本用python -c import torch; print(torch.__version__)双重校验。5.3 从train.sh到 Dockerfile一个可交付的最小服务镜像既然train.sh和predict.sh已封装好下一步就是容器化。一个极简但生产可用的DockerfileFROM nvidia/cuda:11.0-cudnn8-runtime-ubuntu18.04 # 安装系统依赖 RUN apt-get update apt-get install -y python3-pip python3-dev \ rm -rf /var/lib/apt/lists/* # 复制项目 COPY . /app WORKDIR /app # 创建虚拟环境避免污染系统 python RUN python3 -m pip install --upgrade pip RUN python3 -m pip install virtualenv RUN python3 -m virtualenv venv RUN source venv/bin/activate pip install -r requirements.txt # 复制预训练模型 RUN mkdir -p /app/chinese_L-12_H-768_A-12 # 此处需手动下载 google 官方中文 BERT 并解压到 /app/chinese_L-12_H-768_A-12/ # 暴露端口为后续 API 化预留 EXPOSE 5000 # 启动脚本 COPY entrypoint.sh /app/entrypoint.sh RUN chmod x /app/entrypoint.sh ENTRYPOINT [/app/entrypoint.sh]entrypoint.sh内容#!/bin/bash # entrypoint.sh source /app/venv/bin/activate if [ $1 train ]; then bash train.sh elif [ $1 predict ]; then bash predict.sh ${2:-./data/test_input.txt} ${3:-./output/predictions.json} else echo Usage: docker run image [train|predict] [input_file] [output_file] exit 1 fi交付价值docker build -t bert-sentiment .后docker run bert-sentiment train即启动训练docker run -v $(pwd)/my_data:/app/data bert-sentiment predict /app/data/comments.txt /app/output/res.json即完成预测。所有环境、依赖、模型、代码全部打包进一个镜像这才是真正的“可复现”。你不再需要向同事解释“你得装 CUDA 11.0然后 pip install 特定版本 torch”只需docker pull和docker run。6. 验证用dev.csv的混淆矩阵说话而不是只信accuracy0.896.1 为什么accuracy是危险的指标假设你的dev.csv有 1000 条样本其中 700 条是中性label1200 条负面0100 条正面2。一个永远预测“中性”的模型accuracy70%。而你的模型accuracy89%听起来不错——但如果它把所有正面都判为中性所有负面都判为中性accuracy依然可以很高。情感分析的核心挑战是识别“极端情绪”不是区分中性。6.2 用classification_report看清真相在run_classifier.py的main()函数末尾添加验证集预测结果的详细报告# 在 eval 阶段结束后添加 from sklearn.metrics import classification_report, confusion_matrix import numpy as np # 假设 eval_results 是一个 list of (true_label, pred_label) true_labels [x[0] for x in eval_results] pred_labels [x[1] for x in eval_results] print(\n Detailed Evaluation Report ) print(classification_report(true_labels, pred_labels, target_names[negative, neutral, positive], digits4)) print(\n Confusion Matrix ) cm confusion_matrix(true_labels, pred_labels) print(cm)输出解读classification_report会给出每个类别的precision查准率、recall查全率、f1-score。重点关注negative和positive的recall如果negative recall0.32意味着 100 个负面评论模型只找出了 32 个漏掉了 68 个——这对客服系统是灾难性的。confusion_matrix的对角线是正确预测数非对角线是混淆数。如果cm[0][1]负面→中性很大说明模型把负面当成了中性根源可能是训练数据中负面样本太少或train_sentiment.txt里负面表达太隐晦如“还行吧”被标为负面。6.3 人工抽检5 个最不确定的预测样本predict.py可以增强为输出每个样本的 top-3 预测概率# predict.py 中修改输出逻辑 probabilities torch.nn.functional.softmax(logits, dim-1)[0] top3_probs, top3_indices torch.topk(probabilities, k3) for i, (prob, idx) in enumerate(zip(top3_probs, top3_indices)): label_name processor.get_labels()[idx.item()] print(f Top-{i1}: {label_name} ({prob.item():.4f}))操作运行./predict.sh ./data/dev_sample.txt取dev_sample.txt中 5 个Top-1和Top-2概率接近如0.48 vs 0.45的样本人工判断哪个更合理。你会发现模型在“一般般”上纠结是因为训练数据里这个词既标过中性也标过负面在“绝了”上犹豫是因为vocab.txt里没有“绝了”这个子词被切成了[绝, 了]而[绝]在 vocab 中是生僻字embedding 质量差。这些样本就是你下一轮数据清洗和增强的靶子。从那以后我每次跑完run_classifier.py都强制走一遍python -m sklearn.metrics classification_report ...再抽 5 个低置信度样本人工过一遍。不是为了追求报表上的数字好看而是为了确认模型真的“懂”了中文情绪的微妙——比如知道“还行”和“还行吧”的差别“差”和“太差了”的强度差异。这些细节藏在train_sentiment.txt的每一行里藏在tokenization.py的每一个##前缀里也藏在你predict.sh输出的每一行 JSON 中。希望帮到你。本文还有配套的精品资源点击获取
返回列表