
简介方面级别情感分析是自然语言处理课程设计中常见的进阶任务这份压缩包正是一套可运行的Python实现方案面向需要完成NLP课程作业或初步接触情感分析的学生。包体共7个文件包含2个Python脚本、1个Shell脚本、1个依赖说明txt、1个使用说明markdown文件以及gitignore和开源许可证文件整体仅14KB结构精简、易于上手。方案支持调用预训练语言模型脚本覆盖数据预处理与模型微调预测等关键环节配合运行脚本和说明文档能帮助使用者快速搭建实验环境并理解从数据清洗到情感极性预测的完整流程。压缩包内还提供了规范的目录组织与基础配置便于在此基础上替换数据集或调整模型参数适合作为课程设计参考、实验复现模板或NLP实践入门素材。资源自发布以来已有368人学习尤其适合具备一定Python基础、希望以短平快方式完成方面级情感分析任务的学生。1. 方面级别情感分析一个NLP课程作业为什么值得动手做拿到一份「基于Python的方面级别情感分析NLP课程作业.zip」很多人第一反应是“又是个交差用的模板”。但方面级别情感分析Aspect-Based Sentiment AnalysisABSA恰恰是NLP里最适合练手的硬骨头。普通情感分析只回答“这条评论是好评还是差评”ABSA要回答“评论里提到的每个方面分别是什么情绪”——比如“屏幕大但电池不耐用”好和不好同时存在。这篇笔记会从数据标注、规则基线、BERT微调到部署演示把完整落地路径拆开讲。适合正在做NLP课程作业的学生也适合想快速入坑ABSA、不想只交一个黑匣子模型的工程师。2. 把评论变成ABSA训练样本基于Python的数据标注与预处理2.1 先理解ABSA的任务定义一个句子可能有好几个“看法”ABSA把一条文本拆成若干个“方面-情感”对最常见的是方面词情感极性。例如“这台手机屏幕清晰但续航崩了”方面词是“屏幕”和“续航”情感极性分别是正向和负向。显式ABSA只需要抽取方面词并分类情感隐式ABSA还会处理类似“价格虽然贵但值”里“价格”这种没有直接出现的方面。课程作业一般做显式就够因为好评估、好做消融实验。拿到新数据先回答一个问题你手里是原始评论还是已经标注好的格式。教材里常引用的SemEval-2014 Task 4的Laptop和Restaurant数据集就是经典ABSA数据句子级带方面词和极性。课程作业如果只给你原始评论那第一步就得做标注。常见做法是维护一个CSV文件一个句子可以对应多条记录每条记录至少四列sentence_id、aspect_term、polarity、sentence。为什么要把“一个句子可以对应多条记录”挂在嘴边因为很多新手把句子当标签单元用整句极性去训练等于把ABSA降级成普通情感分析任务作业评分一眼就能看穿。比如“屏幕清晰但续航崩了”整句情感难说好还是坏但拆成“屏幕→正向”和“续航→负向”之后任务定义才算立住。2.2 用Python把原始评论清洗成训练样本环境准备阶段我一般建议先装好Python 3.9并顺手用vscode配置好python环境避免后面在命令行里反复折腾包路径。清洗数据时用pandas和re就够不需要上太重的框架。import pandas as pd import re df pd.read_csv(reviews_raw.csv, encodingutf-8) df df[[sentence, aspect, polarity]].copy() df df.dropna(subset[sentence, aspect, polarity]) def clean_text(text): text re.sub(r[^], , text) # 去HTML标签 text re.sub(r[^0-9A-Za-z\u4e00-\u9fff.?!。], , text) # 去特殊符号 text re.sub(r\s, , text).strip() return text df[sentence] df[sentence].astype(str).map(clean_text) df[aspect] df[aspect].astype(str).map(lambda x: x.strip()) # 同一个句子不同方面保留多行但重复行要删掉 df df.drop_duplicates(subset[sentence, aspect]) print(df.groupby(polarity).size())逻辑说明先加载原始评论文件去掉缺失行然后用正则做轻度清洗。re.sub(r[^], , text)处理爬虫带出来的HTML标签[^0-9A-Za-z\u4e00-\u9fff.?!。]把表情和特殊符号替换成空格但保留中文、英文、数字和常见标点避免把“4.5分”里的数字和点拆没。参数说明如果数据是中文评论分词在下一步做清洗阶段不要急着把所有标点删光因为后面的分句和方面词匹配还要用。drop_duplicates(subset[sentence, aspect])防止重复样本导致训练集和测试集信息泄露——你在数据层面多留一分后面模型的玄学问题就少一分。原始数据如果是Excel直接用pd.read_excel就行列名规范好后面能省两小时。2.3 中文分词与英文词元化边缘情况怎么处理中文评论必须分词英文可以按空格做词元化但还得处理大小写和词形还原。课程作业用jieba分词是最常见的选择但jieba对领域词经常切不好比如“散热器”会被切成“散热”和“器”这对方面词抽取影响很大。import jieba.posseg as pseg def tokenize_zh(text): words [] for word, flag in pseg.cut(text): if word.strip(): words.append((word, flag)) return words # 示例看词性和词 for word, flag in tokenize_zh(屏幕清晰续航崩了): print(word, flag)逻辑说明用词性标注模式jieba.posseg.cut返回词语和词性标签。对ABSA来说名词n、动词v、形容词a是最值得保留的因为方面词基本都是名词短语情感词大多是形容词。flag可以在后面的规则抽取里直接过滤。参数说明jieba有两个常用方法——jieba.cut返回纯词序列jieba.posseg.cut返回带词性的元组。如果需要自定义词典用jieba.load_userdict(domain_words.txt)把“散热器 3 n”这样的行放进去能避免漏切。英文场景用spaCy的en_core_web_sm模型更稳但要知道spaCy会把“doesnt”切成“does”和“nt”建议保留原词text而不是lemma_。2.4 把预处理结果保存成模型可读的格式预处理完的数据要落盘后续反复加载才能省时间。常见做法是存成JSON Lines每一行一个样本。import json with open(train.jsonl, w, encodingutf-8) as f: for idx, row in df.iterrows(): line json.dumps({ sentence: row[sentence], aspect: row[aspect], polarity: row[polarity] }, ensure_asciiFalse) f.write(line \n)逻辑说明JSON Lines格式一行一条记录方便流式加载也方便BERT输入再造。ensure_asciiFalse保证中文以原文写入不然全部变成\uXXXX调试时看着头疼。参数说明如果打算先用规则基线跑一版建议把tokenize_zh的结果也存成一列避免重复分词。文件命名建议带版本号比如train_v1.jsonl因为标注经常会改命好名就是给自己留一条回退的路。3. 用Python做ABSA基线模型从规则匹配到scikit-learn情感分类3.1 规则基线用依存句法抽取方面词和情感词规则基线不是为了拿高分是为了给自己一个“锚点”。常见做法是先用词性过滤候选词再用依存句法找到修饰关系比如形容词修饰名词的amod、名词主语nsubj把情感词和方面词配对。以spaCy的英文模型为例import spacy nlp spacy.load(en_core_web_sm) def extract_aspect_polarity(text): doc nlp(text) for token in doc: if token.dep_ nsubj and token.head.pos_ ADJ: aspect token.text sentiment_word token.head.text # 简单词典判定极性这里只做示意 polarity POS if sentiment_word in {good, clear, great} else NEG yield aspect, polarity for aspect, polarity in extract_aspect_polarity(Battery life is great): print(aspect, polarity)逻辑说明token.dep_是依存关系标签nsubj表示名词主语它的父节点head如果是形容词说明这个名词被形容词修饰。yield改成生成器方便后面一次性处理多条评论。规则基线的准确率通常不高但它的输出可以作为弱监督标注或者用来生成候选方面词给后面的分类器当特征。参数说明spaCy的英文小模型en_core_web_sm只有12MB左右适合课程作业如果追求句法准确率可以换en_core_web_trf但推理速度慢一个量级。中文场景用spaCy需要zh_core_web_sm不过相比jieba依存句法中文ABSA用规则效果更依赖词典覆盖。注意先python -m spacy download en_core_web_sm忘了装就会出现ModuleNotFoundError这是最常见的起步翻车点。3.2 用scikit-learn训练方面情感分类器特征与参数规则基线局限于词典换成机器学习后就可以用上下文信息。常见做法是把句子方面改造成分类任务给定一个句子和一个方面词预测情感极性。特征工程分为三块句子TF-IDF向量、方面词本身、方面词在句子中的位置窗口。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline df[sentence_with_aspect] df[sentence] [SEP] df[aspect] tfidf TfidfVectorizer(max_features20000, ngram_range(1, 2)) clf LogisticRegression(C1.0, class_weightbalanced, max_iter500) pipeline Pipeline([(tfidf, tfidf), (clf, clf)]) pipeline.fit(df[sentence_with_aspect], df[polarity]) print(pipeline.score(test_df[sentence_with_aspect], test_df[polarity]))逻辑说明把方面词和句子拼到一起麻烦但也有效。TF-IDF会学习到“句子里的情感词”和“方面词本身”的统计关联。class_weightbalanced让少数类极性比如负向样本只有10%在loss里获得更高权重避免模型全预测成多数类的“正向”这在餐饮和商品评论里特别常见。参数说明C1.0是逻辑回归正则化强度的倒数C越小正则越强防止过拟合。训练样本只有几百条时建议C0.1~0.5能明显提高泛化。max_features20000限制字典规模中文文本20k通常够用ngram_range(1,2)同时考虑单个词和相邻两个词的共现能捕捉“不清晰”这类否定结构。这里先不拼自定义情感词典基线模型如果F1有70%以上后面BERT调到80%才体现得出价值。3.3 评估指标不要只看准确率课程作业经常出现准确率95%但模型完全没用的情况因为测试集里90%是正向样本。ABSA的标准做法是看每类极性的precision、recall和macro F1用scikit-learn一行就能算。from sklearn.metrics import classification_report y_pred pipeline.predict(test_df[sentence_with_aspect]) print(classification_report(test_df[polarity], y_pred, digits3))逻辑说明classification_report输出每一类的精确率、召回率、F1还有micro和macro平均。判断模型好坏重点看macro F1而不是准确率准确率只反映多数类。如果你的三个极性是POS/NEG/NEUNEU类中性的样本最容易混淆因为很多标注者把微弱情绪都标成中性。参数说明如果标签顺序乱可以在classification_report里指定labels[POS, NEG, NEU]。报告里如果NEG的F1只有30%优先要补的是负向样本的数量和特征而不是调分类器参数。评估集要按句子划分不是按样本划分——同一个句子下不同方面在训练和测试里都出现这不算泄露但如果你用整句级划分就会低估模型效果。3.4 为什么基线模型要留一套“能跑通”的副本进入BERT微调前把这个规则机器学习的管线完整保存下来包括joblib.dump(pipeline, baseline.joblib)。好处有三个实验报告可以画对比表BERT训练失败的时候你有退路答辩时你可以解释“规则、机器学习、预训练模型”三层递进而不只是拿一个黑匣子。保存的代码很简单import joblib joblib.dump(pipeline, baseline.joblib)逻辑说明模型文件里同时包含TF-IDF词表和逻辑回归权重后续预测时不会再依赖训练数据。课程作业如果要求“不能使用测试集调参”这一份模型文件就是你跑测试集前的最终版本。保存前记得把训练时的random_state也写进备注不然下次复现报告时又是一场恶战。4. 用BERT微调方面级别情感分析训练脚本与参数设置4.1 为什么课程作业里BERT是性价比最高的选择在NLP课程作业里选模型唯一标准是“在有限算力下拿到尽可能高的分数”。BERT以及它的一系列中文模型通过预训练学会了通用语义微调阶段只需要一个小型分类头就能胜任ABSA。相比LSTMAttention需要自己训练词向量和位置编码BERT把最困难的语言表征问题外包给了预训练阶段课程作业的侧重点可以放在任务构造和参数调试上这也是“基于Python”作业里最常见的进阶路径。ABSA用BERT有两种常见构造第一种是“方面词句子”做句子对分类例如用[CLS] aspect [SEP] sentence的方式把方面词当作第一段第二种是在每个词上做序列标注抽取方面词的同时判定情感。课程作业如果只要求情感极性分类用句子对分类最简单收敛更快如果要求同时抽取方面词和情感词再做序列标注。我一般建议先做句子对分类拿到能看的F1再考虑序列标注因为序列标注的BIO标签对齐里坑太多。4.2 把ABSA构造成句子对分类输入格式与Tokenizer用Transformers库的AutoTokenizer加载预训练模型。中文场景常用bert-base-chinese英文常用bert-base-uncased。注意中文模型会把每个汉字当成一个token“屏幕清晰”在BERT里没有jieba分词的概念但它的词表中仍然有“屏”“幕”“清”“晰”这样的字token对ABSA足够用了。from transformers import AutoTokenizer MODEL_NAME bert-base-chinese tokenizer AutoTokenizer.from_pretrained(MODEL_NAME) def encode_ab(sentence, aspect, max_len128): encoded tokenizer( aspect, sentence, paddingmax_length, truncationTrue, max_lengthmax_len, return_tensorspt, ) return encoded sample encode_ab(屏幕清晰续航崩了, 续航) print(sample[input_ids].shape) print(tokenizer.decode(sample[input_ids][0]))逻辑说明tokenizer接受两个文本参数第一个是方面词第二个是原句。预训练模型的[SEP]会把两个文本隔开[CLS]位置的输出向量最后会被拿去分类。paddingmax_length会把短样本补到128位truncationTrue把超长部分截断。decode之后可以看到[CLS]、[SEP]都变成了特殊token说明句子对输入构造成功。参数说明max_length对ABSA非常关键。评论文本一般不超128个token但如果你把句子切得很长128可能不够。显存足够的话设256更稳如果测试集里偶尔有超过256的句子截断会丢掉尾部情感词模型只能学到“开头论”。后续训练数据加载时要用DataLoader和collator如果自己写collate_fn记得把所有样本pad到同batch最大长度。4.3 微调训练脚本关键参数怎么设微调用HuggingFace的Trainer最省心它封装了梯度累积、学习率调度和保存逻辑。你需要先把数据集转成Dataset对象再定义compute_metrics。from sklearn.metrics import f1_score, accuracy_score def compute_metrics(eval_pred): logits, labels eval_pred preds logits.argmax(axis-1) return { accuracy: accuracy_score(labels, preds), f1_macro: f1_score(labels, preds, averagemacro), }逻辑说明eval_pred是Trainer传入的元组第一个元素是模型logits第二个是真实标签。argmax取概率最大的类作为预测返回字典供Trainer记录。f1_macro是ABSA报告里最常出现的指标因为它同时惩罚了三类极性的不均衡。接下来是训练参数from transformers import BertForSequenceClassification, Trainer, TrainingArguments model BertForSequenceClassification.from_pretrained(MODEL_NAME, num_labels3) training_args TrainingArguments( output_dir./absa_bert, learning_rate2e-5, per_device_train_batch_size16, per_device_eval_batch_size32, num_train_epochs3, weight_decay0.01, evaluation_strategyepoch, save_strategyepoch, load_best_model_at_endTrue, metric_for_best_modelf1_macro, logging_steps50, ) trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, eval_datasettest_dataset, compute_metricscompute_metrics, ) trainer.train()逻辑说明BertForSequenceClassification会自动在[CLS]向量后面接一个线性分类层num_labels3对应POS/NEG/NEU。Trainer在epoch结束时自动做评估并把验证集F1最高的模型保留下来。metric_for_best_model必须指向compute_metrics返回的字段名否则load_best_model_at_endTrue找不到指标值。参数说明学习率2e-5是BERT微调的标准起点别像调普通模型那样设0.01。num_train_epochs3对ABSA这种小数据集够用训练超过5轮通常会在验证集上下降。per_device_train_batch_size16在8GB显存下配合max_length128没问题显存不够时先减batch_size不要动学习率。weight_decay0.01只在非偏置和非LayerNorm参数上生效是Transformer微调里的常见默认值。4.4 中文模型还是英文模型怎么选课程作业的数据决定预训练模型。中文评论用bert-base-chinese如果数据是中文但偏口语可以考虑chinese-roberta-wwm-ext。英文数据别用中文模型词表不覆盖效果会很差。另有一个通用技巧用AutoModelForSequenceClassification配合bert-base-multilingual-cased同时处理中英文但课程作业里没必要因为微调数据量小单语模型更稳。5. 基于Python的ABSA作业避坑指南5个让模型翻车的常见问题课程作业里模型的翻车往往不在模型本身而在数据处理和环境配置。下面的坑按发生频率排序每一条都是“现象→原因→解决”的结构遇到类似问题直接对照排查。5.1 测试集指标虚高把整句极性当成了样本标签现象模型在测试集上准确率85%但人工抽看预测结果时全部预测都偏向正向类。原因数据预处理时你按“句子”而不是“句子方面”去划分训练集和测试集。同一个句子里的“屏幕”和“续航”被打到了两个集合里模型实际只记住了句子整体的情感词没有为方面建模。解决严格按sentence_id划分数据源保证同一个句子不管包含几个方面都只出现在训练集或测试集其中一个。划分代码很简单from sklearn.model_selection import train_test_split unique_sentences df[sentence].unique() trn_sids, val_sids train_test_split(unique_sentences, test_size0.2, random_state42) trn df[df[sentence].isin(trn_sids)] val df[df[sentence].isin(val_sids)]逻辑说明先对句子分组再按句子级切分避免相同词在不同极性里重复出现。random_state固定下来方便你复现实验报告里的数字。5.2 中文分句把“虽然…但是…”切碎了现象一条评论“虽然屏幕分辨率不高但是色彩很准”被正则按逗号切成了两个独立样本模型把“屏幕”预测成负向把“色彩”预测成正向可实际上第一个分句带有转折关系屏幕是负向、色彩是正向。另一种情况“虽然价格小贵但是性能很强”按逗号切成两段后后面那句没有主语方面词“价格”就丢了。原因简单按标点分句破坏了完整的转折结构把“虽然”从句和主句割裂。ABSA需要保留转折词前后的信息。解决不要在预处理阶段盲目分句。正确的做法是把整句喂给模型让模型自己学习“虽然……但是……”结构如果必须分句也要让每个分句带上原句的全局信息比如在原句后面拼接“该评论的其他观点剩余分句”。课程作业里最简单的方案是保留完整句只切分方面元组。5.3 显存溢出batch size和max_length打架现象训练刚开始就报CUDA out of memory第一反应是把batch_size调到4但还是溢出。原因max_length512加上batch_size16会导致激活值暴涨另一方面Tokenizer里如果设置了paddingmax_length每个样本都是512个token等于浪费了大半显存。解决max_length降到128或256同时用动态padding。做法是在collator里按当前batch的最大长度padding而不是固定长度。代码片段from transformers import DataCollatorWithPadding data_collator DataCollatorWithPadding(tokenizertokenizer, paddingTrue) trainer Trainer( ..., data_collatordata_collator, )逻辑说明DataCollatorWithPadding会在每个batch内部按最长样本padding短评论不会白白占满512位显存占用下降明显per_device_train_batch_size也可以回到16。5.4 标注不一致同一个方面词在不同句子里的极性分布混乱现象验证集F1在55%上下浮动训练曲线一直震荡模型似乎学过又似乎没学过。原因标注时把“价格虽然贵但服务好”里的价格标成“中性”另一个人标“负向”模型在同样的上下文里看到两个相反标签只能学出概率接近0.5的随机答案。解决标注阶段必须提前写标注规范。常见做法是给每个方面词单独维护一个“极性字典”比如“价格贵负向”“送货快正向”。如果出现分歧以多数票或按作业要求定标准。写完数据后跑一个快速交叉验证如果同一句话重复标注的一致性低于80%先修数据再调模型。5.5 报告里的评估分数和代码跑出来的对不上现象实验报告写macro F182%但评审运行代码时只得到75%被当成造假。原因可能用了不同切分报告里用随机切分代码里按句子切分或者compute_metrics和报告里的指标定义不一样比如报告里是“平均F1”代码里用的是“准确率”。解决把你用的random_state、数据文件名、模型版本全部写进实验报告附录。更稳妥的做法是把测试集单独存成文件不让模型喂过训练。跑实验前用git或复制一份data_final目录这就是你给最终分数上的保险。“可复现”比“分数高”更能拿分。6. 把ABSA模型变成可演示的成果模型保存、API化与可视化验证答辩时光有终端输出的准确率和F1不够评委想看的是“输入一句话立刻知道每个方面什么情感”。用Gradio做一个小demo是常见的高性价比做法代码量不大效果直观。import gradio as gr from transformers import pipeline pipe pipeline(text-classification, model./absa_bert, tokenizerbert-base-chinese) def predict(sentence, aspect): result pipe(f{aspect}[SEP]{sentence})[0] return result[label], result[score] gr.Interface(fnpredict, inputs[text, text], outputs[text, label], title方面级情感分析演示).launch()逻辑说明加载第4章保存的模型目录用pipeline封装成输入句子和方面词、输出情感极性和置信度的函数。[SEP]拼接的方式要和训练时完全一致这一点是多数演示失败的原因。launch()默认在本机启动一个网页服务答辩的时候直接浏览器打开端口。验证技巧演示前准备好三组测试样例一组是“屏幕清晰但续航崩了”这种明显双方面的句子一组是“一般吧”这种中性一组是含否定词“不怎么样”的句子。模型如果能把“续航”判负、“屏幕”判正说明它真在按方面建模。如果“不怎么样”被预测成正向去检查训练数据里的否定句覆盖率多数情况是负向样本太少。我自己的一个习惯是提交前把训练脚本、评估脚本、demo脚本分开命名数据文件单独放目录再写两行README记录Python版本和依赖。因为课程作业翻车最多的不是模型而是“代码能跑起来”这一关。上次帮学生排查发现他在另一台电脑上没装transformers排查一圈全是环境问题。希望帮到你。本文还有配套的精品资源点击获取