Skip to content

Repository files navigation

aspectkit

A configurable, evaluation-centric framework for aspect-based sentiment analysis (ABSA).

aspectkit gives researchers one coherent API over the whole ABSA task family — from aspect term extraction to full (aspect, category, opinion, polarity) quadruples with implicit elements — with swappable model backends, loaders for the standard benchmarks, the exact-match evaluation protocol built in, and the corpus-level opinion summary as a first-class output.

fromaspectkitimportABSAabsa=ABSA(task="acos", backend="llm", model="openai:gpt-4o-mini",
categories=["FOOD#QUALITY", "SERVICE#GENERAL"])
absa.predict("The pasta was great but we waited forever.")
# [SentimentTuple(aspect=Span(text='pasta', start=4, end=9), polarity='positive',# category='FOOD#QUALITY', opinion=Span(text='great', start=14, end=19)),# SentimentTuple(aspect=IMPLICIT, polarity='negative', category='SERVICE#GENERAL',# opinion=Span(text='waited forever', start=27, end=41))]

Why aspectkit

  • One schema, every subtask. ATE, ATSC, ACD, ACSA, E2E, ASTE, TASD, and ACOS/ASQP are all declarative views (which elements are given, which are predicted) over one canonical tuple — so backends, datasets, and metrics compose instead of forking.
  • Implicit aspects and opinions are first-class. Roughly a third of real review sentiment has no explicit target span; IMPLICIT is part of the data model, distinct from "not annotated".
  • Evaluation is the point, not an afterthought. Exact-match tuple P/R/F1 (the SemEval comparability standard) is always computed; a lenient token-overlap mode can be reported alongside it; multi-element tasks get a per-element breakdown showing which element drives the misses; gold labels are stripped before the backend ever sees evaluation inputs.
  • LLM-era backends without lock-in. One small connector interface spans hosted APIs, OpenAI-compatible local servers, and models already loaded in your notebook.
  • Fine-tuning when prompting is not enough. A generative seq2seq backend (the model family holding the published state of the art on exact-match quad extraction) and a trainable ATSC cross-encoder, with plain, transparent training loops.
  • The output researchers actually want.summarize() rolls predictions up into per-aspect sentiment distributions, net scores, and representative quotes — the aspect-based opinion summary ABSA was invented for.

Installation

pip install aspectkit # core: zero dependencies
pip install "aspectkit[openai]"# OpenAI + OpenAI-compatible endpoints
pip install "aspectkit[anthropic]"# Anthropic models
pip install "aspectkit[gemini]"# Google Gemini
pip install "aspectkit[transformers]"# local Hugging Face models (+ torch)
pip install "aspectkit[all]"

Python 3.10+. The core library (schema, loaders, evaluation, aggregation) has no dependencies; provider SDKs are optional extras imported lazily, so you install only what your chosen backend needs.

Connecting a model

Every chat model is addressed the same way — a "provider:model" string or a live object:

SpecConnector
"openai:gpt-4o-mini"OpenAI API (OPENAI_API_KEY)
"anthropic:claude-opus-4-8"Anthropic API (ANTHROPIC_API_KEY)
"gemini:gemini-2.0-flash"Google Gemini (GEMINI_API_KEY)
"deepseek:deepseek-chat"DeepSeek (DEEPSEEK_API_KEY)
"vllm:<served-model>"local vLLM server (http://localhost:8000/v1)
"ollama:llama3.1"local Ollama server
"mistral:...", "together:...", "groq:...", "openrouter:..."other OpenAI-compatible providers
"openai-compatible:<model>" + base_url=...any OpenAI-protocol endpoint
"hf:Qwen/Qwen2.5-7B-Instruct"Hugging Face hub id, loaded as a local pipeline
a transformerspipeline objectused in-process, as-is
a (model, tokenizer)pairused in-process, as-is
any messages -> strcallablecustom gateways, caching layers, test doubles

The notebook workflow needs no ceremony — pass the object you already have:

fromtransformersimportpipelinefromaspectkitimportABSApipe=pipeline("text-generation", model="Qwen/Qwen2.5-7B-Instruct")
absa=ABSA(task="aste", backend="llm", model=pipe)

Connector behaviour worth knowing: generation defaults are deterministic (temperature 0 where the provider allows it), JSON schemas are enforced natively where the provider supports structured output, and protocol-dialect quirks of OpenAI-compatible servers (unsupported response_format, max_completion_tokens, rejected temperature) are detected, downgraded once, and remembered.

For corpus-scale runs against hosted APIs, predict examples in parallel (order is preserved, failures behave per on_error):

absa=ABSA(task="acos", backend="llm", model="openai:gpt-4o-mini",
concurrency=8, on_error="skip")

Tasks

namegivenpredicted
ateaspect
atsc (asc, apc)aspectpolarity
acdcategory
acsacategory, polarity
e2e (atepc)aspect, polarity
asteaspect, opinion, polarity
tasdaspect, category, polarity
acos (asqp, quad)aspect, category, opinion, polarity

Loading data: benchmarks, custom files, frameworks

Every published benchmark format has a loader, verified against the official distributions:

fromaspectkitimportload_examplesrest14=load_examples("Restaurants_Train.xml", "semeval2014") # terms/categories viewsmams=load_examples("train.xml", "mams") # same XML schemarest16=load_examples("ABSA16_Restaurants_Train_SB1.xml", "semeval2016")
quads=load_examples("laptop_quad_train.tsv", "acos") # token-span TSVasqp=load_examples("rest15/train.txt", "asqp") # generative-ABSA txttriples=load_examples("train_triplets.txt", "aste") # ASTE-Data-V2tweets=load_examples("train.raw", "twitter") # Dong et al. $T$ formattagged=load_examples("Laptops_Train.atepc", "atepc") # BIO/CoNLL token files

Each loader converts one published format into the canonical schema — spans become character offsets, NULL/-1,-1 targets become IMPLICIT, integer polarity codes are mapped per each format's own convention (ACOS 0 is negative, Twitter 0 is neutral — the loaders know) — so the dataset-version chaos stays out of your experiment code.

Custom datasets come in through one remappable interface, whatever shape they're in — nested records (one item per text) or flat rows (one opinion per row, the usual CSV/DataFrame layout, grouped by id or text automatically):

fromaspectkit.ioimportfrom_records, from_pandas, from_hf_dataset, read_csv, read_jsonexamples=read_csv("reviews.csv", text="review", aspect="term", polarity="label")
examples=from_pandas(df) # pandas DataFrameexamples=from_hf_dataset(ds["train"]) # Hugging Face datasetsexamples=from_records([{"text": "...", "tuples": [{"aspect": "...", "polarity": "POS"}]}])

And back out for analysis: to_pandas(examples) flattens gold data or predictions into one row per opinion; to_records(...)/read_json round-trip losslessly.

Few-shot, evaluation, and the corpus summary

absa=ABSA(task="acos", backend="llm", model="anthropic:claude-opus-4-8",
categories=CATEGORIES)
absa.fit(train_examples) # seeded few-shot exemplar selection (optional)report=absa.evaluate(test_examples, lenient=True)
print(report)
# EvaluationReport(task=acos, n_examples=583)# exact match P=0.61 R=0.57 F1=0.59 (pred=802, gold=843)# lenient P=0.68 R=0.64 F1=0.66# by element (exact):# aspect P=0.74 R=0.69 F1=0.71# category P=0.81 R=0.76 F1=0.78# opinion P=0.69 R=0.64 F1=0.66# polarity P=0.88 R=0.82 F1=0.85summary=absa.summarize(corpus, by="category", min_mentions=5)
forsinsummary[:3]:
print(s)
# FOOD#QUALITY: n=412, score=+0.55 (negative=71, neutral=44, positive=297)# SERVICE#GENERAL: n=259, score=-0.18 (...)

A methodological note baked into the design: prompted LLMs are convenient and strong at simple polarity, but the benchmark literature consistently shows them trailing fine-tuned models on exact-match tuple extraction. Validate the LLM backend on a labelled sample with evaluate() before trusting corpus-level output — the API makes that the path of least resistance.

For polarity-given-aspect (ATSC), the empirically stronger default is the fine-tuned cross-encoder backend:

absa=ABSA(task="atsc", backend="pair") # yangheng/deberta-v3-base-absa-v1.1

Fine-tuning

When labelled data is available, the fine-tuned route is the strong one. The seq2seq backend trains a T5/BART-family model to generate linearised tuples — either MvP-style element markers ([A] pasta [C] FOOD#QUALITY [O] great [S] positive, any task view) or the ASQP paraphrase template ("FOOD#QUALITY is great because pasta is great", quad view):

absa=ABSA(task="acos", backend="seq2seq", model="t5-base",
categories=CATEGORIES) # style="markers" by defaultabsa.fit(train_examples, epochs=20) # plain AdamW loop, seededprint(absa.backend.history_[-1]) # mean loss of the last epochreport=absa.evaluate(test_examples)
absa.backend.save_pretrained("runs/acos-t5") # reload via model="runs/acos-t5"

A fresh t5-base knows nothing about the templates: fit before predict. The same fit() recipe (epochs, learning rate, seeded shuffling, loss history) fine-tunes the ATSC cross-encoder:

absa=ABSA(task="atsc", backend="pair")
absa.fit(train_examples, epochs=3) # 2e-5 AdamW, the BERT recipe

The data model

fromaspectkitimportABSAExample, SentimentTuple, Span, IMPLICITABSAExample(
text="Would not recommend.",
tuples=[SentimentTuple(aspect=IMPLICIT, # no surface targetcategory="RESTAURANT#GENERAL",
opinion=Span("Would not recommend", 0, 19),
polarity="negative")],
)
  • Spans carry character offsets [start, end); offsets are optional, so generative outputs remain first-class citizens.
  • IMPLICITNone: implicit means expressed without a surface span; None means not part of this task's annotation.
  • An empty tuples list means "no opinions", which is not the same as neutral.
  • Everything round-trips through JSONL (aspectkit.io.write_jsonl / read_jsonl).

Extending

Custom strategies implement the two-method Backend interface (fit, predict) over canonical examples and plug straight into the facade:

fromaspectkitimportABSAfromaspectkit.backendsimportBackendclassMyBackend(Backend):
...
absa=ABSA(backend=MyBackend(...))

Custom chat endpoints implement ChatLLM.complete() — or are just passed as a callable.

License

GPL-3.0-or-later — see LICENSE.

About

A ready-to-use Python framework for automated aspect-based sentiment analysis employing a variety of flexible tools that span from originally proposed ones to those viable in the post-LLM era

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages