基于方面的情感分析(Aspect Based Sentiment Analysis,
ABSA)是一种细粒度的情感分析任务,即,对于给定的一段文本,识别出该文本针对文中指定的某一方面的情感极性。
2025年11月:更新了基于CausalLM的方法
本文将介绍如何在本机上部署并使用预训练的DeBERTa
V3 模型对文本中的某一特定方面进行情感分析,从而实现观点挖掘相关的工作。当然,由于本人没有涉足过NLP的具体研究,可能很多表述并不严谨,敬请批评指正。
前言
这两天接触了一下情感分析相关的工作,大致是需要确定一些文本对某些个体的情感倾向。经过搜索,这属于情感分析(Sentiment
Analysis)的任务范畴,而更具体地,实际上是基于方面的情感分析(Aspect
Based Sentiment Analysis, ABSA) 。
所谓基于方面的情感分析,我们用一个例子来说明就可以。
The food here is delicious , but the serivce is
terrible .
如果上面这句话是某个顾客对某家餐馆的评价,那么我们应该如何精确地描述这名顾客对餐馆的观点呢?
一个很显然的结论是,恐怕我们不能简单地将 delicious 或
terrible
作为顾客的态度代表,也不能简单地因为文本中出现了一个积极的词语和一个消极的词语,分别赋分1和-1,然后加起来得分为0,结论为情感中性。
但如果现在有一位饥肠辘辘的游人来到了这座城市,他更在乎食物的口味,但却对餐馆的服务态度毫不在意 。现在,他正在手机上搜寻附近口味出众的餐馆,根据上述顾客的评论,我们能否将这家餐馆推荐给他呢?
通过上述这个案例,我们会发现,在现实中,我们面对的大量文本可能会同时描述多个实体(Entity),例如上面出现在同一句话中的
food 和 serivce
,并表示出出截然相反的态度。然而,传统的方法无法准确地判别出隐藏在文本中的上述情感差异。
而通过对文本中不同的实体进行情感分析,就有可能发现人们在观点/意见(Opinion)上的差异。在上述例子中,顾客的评价是对餐馆不同方面/实体的观点存在差异,现实中包括新闻、媒体、文章等,可能都会对不同的事物存在不同的态度。通过发掘,可能发现上述差异,从而辅助决策。
(2025年11月更新)
基于CausalLM的实现方法
在CausalLM时代
两年前写下这篇文章时,GPT还是个新鲜事物,宛若天顶星科技一般,或许有人想过用GPT的API完成本文介绍的情感分析任务,但看在API价格的份上也还得慎重考虑。
两年后的今天,各种Decoder-Only架构的CausalLM获得了极其亮眼的进步,大模型也不再是少数公司的专利,开放权重的模型可以轻松获取,而LLM
API的价格已经足够便宜。
在各种模型一路高歌猛进,卷着Agentic能力、Coding能力、Novelty的路上,本文所介绍的ABSA任务,被
“轻松地”
解决了,以更便宜更好的效果,像马儿扬鞭飞驰而去,徒留下一阵尘土。
回头来看,只是不由得感慨,这样的进步不可谓不日新月异哇。
实现案例
时至今日,这样的任务简单到只要是一个训练正常的CausalLM,哪怕参数量只有0.5B都能轻松解决,不仅分类效果好,支持多语言,对输出分类也可以极其轻松地自定义。
写了一个使用Qwen2.5-0.5B的示例,直接上代码和提示词:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 from transformers import AutoTokenizer, AutoModelForCausalLMmodel_id = "./Qwen2.5-0.5B-Instruct" tokenizer = AutoTokenizer.from_pretrained(model_id) model = AutoModelForCausalLM.from_pretrained(model_id, dtype="auto" , device_map="auto" ) sentence = "The food here is delicious but the service is awful." entity = "service" prompt = f""" Analyze the sentiment expressed toward the specified entity in the sentence. Classify it as exactly ONE of these categories: Happy, Sad, Angry, Surprise. Output only the category name. Sentence: {sentence} Entity: {entity} """ messages = [ { "role" : "system" , "content" : "You are Qwen, created by Alibaba Cloud. You are a helpful assistant." , }, { "role" : "user" , "content" : prompt }, ] text = tokenizer.apply_chat_template( messages, tokenize=False , add_generation_prompt=True ) model_inputs = tokenizer([text], return_tensors="pt" ).to(model.device) generated_ids = model.generate(**model_inputs, max_new_tokens=512 ) generated_ids = [ output_ids[len (input_ids) :] for input_ids, output_ids in zip (model_inputs.input_ids, generated_ids) ] response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True )[0 ] print (response)
事已至此,微调什么的都不需要了(笑)。
实现方法
搜索ABSA相关的应用方法,首先发现的是Azure提供的Sentiment
analysis and opinion mining 服务,刚好免费申请的Azure for
Students权益包里似乎也能用这个API,只要把需要分析的文本上传,就可以得到分析的结果。
但思来想去,觉得光是调用API就没意思了。
继续搜索,发现HuggingFace上有很多训练好的模型,仔细一看,推理使用到的计算硬件资源也不大,在可以接受的范围内,于是决定试试在自己电脑上部署模型进行推理。
只是奈何自己也不会什么模型,最后还是得用其他人做好的模型来完成这项工作。不过倒也可以之后再改进吧。
这里我们使用的是DeBERTa
V3 模型,HuggingFace上已经有人使用ABSADatasets训练好了可用于ABSA任务的模型,请见yangheng/deberta-v3-large-absa-v1.1 .
可以参考的其他模型
在情感分析方面,当然也有一些其他预训练好的模型可以使用,例如:
部署细节
下面大致介绍一下本地部署模型的相关步骤和可能踩坑的地方。
获取模型
确保Git已经启用LFS扩展,然后执行如下命令:
1 2 3 GIT_LFS_SKIP_SMUDGE=1 git clone https://huggingface.co/yangheng/deberta-v3-large-absa-v1.1 git lfs pull -I "model.safetensors" git lfs pull -I "spm.model"
在我们使用的这一模型仓库里,我们需要拉取的是model.safetensors和spm.model两个文件,其中model.safetensors包含了模型主要的权重,因此不需要 重复下载pytorch_model.bin权重文件。
本机环境
为了在本机上进行推理,需要创建一个Python环境,依赖如下:
这两个就是最简单的,使用CPU进行推理的配置了。稍后我们介绍不同模式时会再介绍其他模式下依赖的Python包。
运行推理
回到上面克隆的模型仓库目录的同一级目录下,创建一个.py文件(或者.ipynb,如果你喜欢),这里取名run.py。大致文件结构如下:
1 2 3 - MyABSA - deberta-v3-large-absa-v1.1/ - run.py
编辑run.py文件,填入以下代码:
1 2 3 4 5 6 7 8 9 10 from transformers import AutoTokenizer, AutoModelForSequenceClassification, pipelinemodel_path = "deberta-v3-large-absa-v1.1" model = AutoModelForSequenceClassification.from_pretrained(model_path) tokenizer = AutoTokenizer.from_pretrained(model_path) recognizer = pipeline("text-classification" , tokenizer=tokenizer, model=model) sentence = "[CLS] The food here is delicious but the service is awful. [SEP] service [SEP]" recognizer(sentence)
这里我们传入模型的输入分为两个部分,前半段即为本文开头的餐馆评价示例,而后面,用[SEP]标志和主要文段内容分割开的,就是我们希望探知的情感的实体。
在上面的输入中,我们希望模型给出,前面语句对于service这个实体的态度。
运行上述代码,或许你会得到类似这样的输出:
1 2 3 [[{'label' : 'Negative' , 'score' : 0.9997419714927673 }, {'label' : 'Neutral' , 'score' : 0.00018644658848643303 }, {'label' : 'Positive' , 'score' : 7.166137220337987e-05 }]]
结果不难解读,模型给出的推理认为,前面的文本对service一词表现出的消极态度占据最主导的地位。
WebAPI化
为了便于对外设计接口,还可以将推理任务设计成WebAPI,对外暴露服务。
测试了一下使用ONNX
Runtime进行的部署,甚至还测试了使用DirectML加速的ONNX
Runtime,但似乎没啥用,测试下来尽管GPU占用是有了,但速度甚至比用numpy在CPU上的推理还要慢了。很大可能是我代码写的不好,数据在CPU和GPU之间反复倒腾了,导致速度上不去。
总之这一部分的代码,首先需要导出模型为ONNX格式,然后再放到这里进行加载。其实这部分内容主要是我想亲自动手看看,在pipeline中间到底发生了什么。实际使用其实并不用这样做的。
使用CPU进行推理的具体代码看下面吧,依赖项在引入部分已经写清楚了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 from fastapi import FastAPI, Depends, Requestfrom pydantic import BaseModelfrom transformers import AutoTokenizerfrom scipy.special import softmaximport jsonimport numpy as npimport onnxruntimeMODEL_DIR = "deberta-v3-large-absa-v1.1_onnx" app = FastAPI() def create_onnxruntime_infersess (model_file: str ): print (f"Infering using {onnxruntime.get_available_providers()} " ) return onnxruntime.InferenceSession(model_file) def create_tokenizer (model_dir: str ): return AutoTokenizer.from_pretrained(model_dir) def create_id2label_dict (model_config: str ): with open (model_config, "r" ) as f: return {int (k): v for k, v in json.load(f)["id2label" ].items()} ort_infersess = create_onnxruntime_infersess(f"{MODEL_DIR} /model.onnx" ) bert_tokenizer = create_tokenizer(MODEL_DIR) id2label_dict = create_id2label_dict(f"{MODEL_DIR} /config.json" ) def get_inference_session (): return ort_infersess def get_tokenizer (): return bert_tokenizer def get_id2label_dict (): return id2label_dict def predict (sentence: str , aspect: str ): infsess = get_inference_session() tokenizer = get_tokenizer() id2label = get_id2label_dict() inputs = tokenizer(sentence, aspect, return_tensors="np" ) outputs = infsess.run( None , { "input_ids" : inputs["input_ids" ].astype(dtype=np.int64), "attention_mask" : inputs["attention_mask" ].astype(dtype=np.int64), }, ) pred_probs = softmax(outputs).flatten() return [(id2label[arg], pred_probs[arg]) for arg in np.flip(pred_probs.argsort())] def get_predict_handler (request: Request ): return predict class SentimentAnalysisRequest (BaseModel ): sentence: str aspect: str @app.post("/analysis" ) async def post_analysis ( request: SentimentAnalysisRequest, predictor=Depends(get_predict_handler ) ): sentence = request.sentence aspect = request.aspect result_list = predictor(sentence, aspect) return { "aspect" : aspect, "mostlike_label" : result_list[0 ][0 ], "mostlike_probability" : f"{result_list[0 ][1 ]:.6 f} " , "full_result" : [{"label" : label, "prob" : f"{prob:.6 f} " } for label, prob in result_list], }
这将创建一个WebAPI,监听POST请求,请求主体是一个JSON格式的输入,包含sentence和aspect两个字段,前者是文本内容,后者就是希望获取态度的Aspect/Entity。
TODO
[]试着在预训练模型上进行fine-tune.
[]试着将推理过程运行在GPU上进行加速