0
# Aspect-Based Sentiment Analysis (ABSA)
1
2
Specialized models and trainers for aspect-based sentiment analysis tasks with span-level predictions. ABSA enables fine-grained sentiment analysis by identifying specific aspects mentioned in text and determining the sentiment toward each aspect.
3
4
## Capabilities
5
6
### ABSA Model
7
8
Combined model that performs both aspect extraction and polarity classification for aspect-based sentiment analysis.
9
10
```python { .api }
11
class AbsaModel:
12
def __init__(
13
self,
14
aspect_model: Optional["AspectModel"] = None,
15
polarity_model: Optional["PolarityModel"] = None
16
):
17
"""
18
Initialize an ABSA model with aspect extraction and polarity classification.
19
20
Parameters:
21
- aspect_model: Model for extracting aspects from text
22
- polarity_model: Model for classifying sentiment polarity of aspects
23
"""
24
25
def predict(self, inputs: Union[str, List[str]]) -> List[Dict]:
26
"""
27
Perform aspect-based sentiment analysis on input texts.
28
29
Parameters:
30
- inputs: Input text(s) to analyze
31
32
Returns:
33
List of dictionaries containing aspect-polarity pairs for each input
34
"""
35
36
def __call__(self, inputs: Union[str, List[str]]) -> List[Dict]:
37
"""
38
Callable interface for prediction.
39
40
Parameters:
41
- inputs: Input text(s) to analyze
42
43
Returns:
44
ABSA predictions with aspects and their sentiment polarities
45
"""
46
```
47
48
### Aspect Model
49
50
Model specialized for extracting aspect terms from text using span-based classification.
51
52
```python { .api }
53
class AspectModel:
54
def __init__(
55
self,
56
model_body: Optional[SentenceTransformer] = None,
57
model_head: Optional[Union[SetFitHead, LogisticRegression]] = None,
58
spacy_model: str = "en_core_web_sm",
59
span_context: int = 0,
60
normalize_embeddings: bool = True,
61
labels: Optional[List[str]] = None,
62
model_card_data: Optional[SetFitModelCardData] = None
63
):
64
"""
65
Initialize an aspect extraction model.
66
67
Parameters:
68
- model_body: Sentence transformer for generating embeddings
69
- model_head: Classification head for aspect/non-aspect prediction
70
- spacy_model: SpaCy model name for tokenization and NER
71
- span_context: Number of context tokens around spans
72
- normalize_embeddings: Whether to normalize embeddings
73
- labels: Label names for aspect categories
74
- model_card_data: Metadata for model card generation
75
"""
76
77
def predict(self, inputs: Union[str, List[str]]) -> List[List[str]]:
78
"""
79
Extract aspect terms from input texts.
80
81
Parameters:
82
- inputs: Input text(s) to extract aspects from
83
84
Returns:
85
List of aspect terms for each input text
86
"""
87
88
def prepend_aspects(
89
self,
90
sentences: List[str],
91
aspects: List[List[str]]
92
) -> List[str]:
93
"""
94
Prepend aspect terms to sentences for polarity classification.
95
96
Parameters:
97
- sentences: Original sentences
98
- aspects: Extracted aspects for each sentence
99
100
Returns:
101
Modified sentences with prepended aspects
102
"""
103
```
104
105
### Polarity Model
106
107
Model specialized for classifying sentiment polarity of extracted aspects.
108
109
```python { .api }
110
class PolarityModel:
111
def __init__(
112
self,
113
model_body: Optional[SentenceTransformer] = None,
114
model_head: Optional[Union[SetFitHead, LogisticRegression]] = None,
115
spacy_model: str = "en_core_web_sm",
116
span_context: int = 0,
117
normalize_embeddings: bool = True,
118
labels: Optional[List[str]] = None,
119
model_card_data: Optional[SetFitModelCardData] = None
120
):
121
"""
122
Initialize a polarity classification model for aspects.
123
124
Parameters:
125
- model_body: Sentence transformer for generating embeddings
126
- model_head: Classification head for polarity prediction
127
- spacy_model: SpaCy model name for tokenization
128
- span_context: Number of context tokens around aspects
129
- normalize_embeddings: Whether to normalize embeddings
130
- labels: Label names for polarity classes (e.g., ["negative", "neutral", "positive"])
131
- model_card_data: Metadata for model card generation
132
"""
133
134
def predict(self, inputs: Union[str, List[str]]) -> List[int]:
135
"""
136
Classify sentiment polarity of aspects in input texts.
137
138
Parameters:
139
- inputs: Input text(s) with aspects to classify
140
141
Returns:
142
Polarity class predictions for each input
143
"""
144
```
145
146
### Span SetFit Model
147
148
Base class for span-based SetFit models that work with text spans and context.
149
150
```python { .api }
151
class SpanSetFitModel:
152
def __init__(
153
self,
154
model_body: Optional[SentenceTransformer] = None,
155
model_head: Optional[Union[SetFitHead, LogisticRegression]] = None,
156
spacy_model: str = "en_core_web_sm",
157
span_context: int = 0,
158
multi_target_strategy: Optional[str] = None,
159
normalize_embeddings: bool = True,
160
labels: Optional[List[str]] = None,
161
model_card_data: Optional[SetFitModelCardData] = None
162
):
163
"""
164
Initialize a span-based SetFit model.
165
166
Parameters:
167
- model_body: Sentence transformer for embeddings
168
- model_head: Classification head
169
- spacy_model: SpaCy model for text processing
170
- span_context: Number of context tokens around spans
171
- multi_target_strategy: Strategy for multi-target classification
172
- normalize_embeddings: Whether to normalize embeddings
173
- labels: Label names
174
- model_card_data: Model metadata
175
"""
176
177
def prepend_aspects(
178
self,
179
sentences: List[str],
180
aspects: List[List[str]]
181
) -> List[str]:
182
"""
183
Prepend aspect information to sentences.
184
185
Parameters:
186
- sentences: Input sentences
187
- aspects: Aspect terms for each sentence
188
189
Returns:
190
Sentences with prepended aspect information
191
"""
192
193
def __call__(self, inputs: Union[str, List[str]]) -> List:
194
"""
195
Process inputs through the span-based model.
196
197
Parameters:
198
- inputs: Input texts or spans
199
200
Returns:
201
Model predictions for the spans
202
"""
203
```
204
205
### ABSA Trainer
206
207
Specialized trainer for training ABSA models with span-level supervision.
208
209
```python { .api }
210
class AbsaTrainer:
211
def __init__(
212
self,
213
model: Optional[Union[AspectModel, PolarityModel]] = None,
214
args: Optional[TrainingArguments] = None,
215
train_dataset: Optional[Dataset] = None,
216
eval_dataset: Optional[Dataset] = None,
217
compute_metrics: Optional[Callable] = None,
218
callbacks: Optional[List] = None,
219
column_mapping: Optional[Dict[str, str]] = None
220
):
221
"""
222
Initialize an ABSA trainer.
223
224
Parameters:
225
- model: ABSA model to train (aspect or polarity model)
226
- args: Training arguments
227
- train_dataset: Training dataset with span annotations
228
- eval_dataset: Evaluation dataset
229
- compute_metrics: Function to compute evaluation metrics
230
- callbacks: Training callbacks
231
- column_mapping: Mapping of dataset columns
232
"""
233
234
def train(self) -> None:
235
"""
236
Train the ABSA model on span-level data.
237
"""
238
239
def evaluate(self, eval_dataset: Optional[Dataset] = None) -> Dict[str, float]:
240
"""
241
Evaluate the ABSA model.
242
243
Parameters:
244
- eval_dataset: Evaluation dataset
245
246
Returns:
247
Evaluation metrics
248
"""
249
```
250
251
### Aspect Extractor
252
253
Utility class for extracting aspect terms from text using various strategies.
254
255
```python { .api }
256
class AspectExtractor:
257
def __init__(
258
self,
259
spacy_model: str = "en_core_web_sm",
260
aspect_model: Optional[AspectModel] = None
261
):
262
"""
263
Initialize aspect extractor.
264
265
Parameters:
266
- spacy_model: SpaCy model for NLP processing
267
- aspect_model: Trained aspect model for ML-based extraction
268
"""
269
270
def extract_aspects(
271
self,
272
text: str,
273
method: str = "model"
274
) -> List[str]:
275
"""
276
Extract aspect terms from text.
277
278
Parameters:
279
- text: Input text to extract aspects from
280
- method: Extraction method ("model", "pos", "ner", "hybrid")
281
282
Returns:
283
List of extracted aspect terms
284
"""
285
286
def extract_aspects_batch(
287
self,
288
texts: List[str],
289
method: str = "model"
290
) -> List[List[str]]:
291
"""
292
Extract aspects from multiple texts.
293
294
Parameters:
295
- texts: List of input texts
296
- method: Extraction method
297
298
Returns:
299
List of aspect lists for each text
300
"""
301
```
302
303
## Usage Examples
304
305
### Basic ABSA Pipeline
306
307
```python
308
from setfit import AbsaModel, AspectModel, PolarityModel, SetFitModel
309
from datasets import Dataset
310
311
# Prepare ABSA training data
312
absa_data = {
313
"text": [
314
"The food was great but the service was slow.",
315
"Amazing pizza, terrible atmosphere.",
316
"Good value for money, poor location."
317
],
318
"aspects": [
319
["food", "service"],
320
["pizza", "atmosphere"],
321
["value", "location"]
322
],
323
"polarities": [
324
[1, 0], # food: positive, service: negative
325
[1, 0], # pizza: positive, atmosphere: negative
326
[1, 0] # value: positive, location: negative
327
]
328
}
329
330
# Create datasets for aspect and polarity training
331
aspect_train_data = []
332
polarity_train_data = []
333
334
for text, aspects, polarities in zip(absa_data["text"], absa_data["aspects"], absa_data["polarities"]):
335
# Create aspect extraction examples (binary classification per span)
336
# This would need proper span annotation in practice
337
338
# Create polarity classification examples
339
for aspect, polarity in zip(aspects, polarities):
340
aspect_text = f"{aspect}: {text}"
341
polarity_train_data.append({
342
"text": aspect_text,
343
"label": polarity
344
})
345
346
polarity_dataset = Dataset.from_list(polarity_train_data)
347
348
# Train aspect model (simplified example)
349
aspect_model = AspectModel.from_pretrained("sentence-transformers/all-MiniLM-L6-v2")
350
351
# Train polarity model
352
polarity_model = PolarityModel.from_pretrained("sentence-transformers/all-MiniLM-L6-v2")
353
354
from setfit import AbsaTrainer, TrainingArguments
355
356
polarity_trainer = AbsaTrainer(
357
model=polarity_model,
358
train_dataset=polarity_dataset,
359
args=TrainingArguments(
360
batch_size=16,
361
num_epochs=4,
362
learning_rate=2e-5
363
),
364
column_mapping={"text": "text", "label": "label"}
365
)
366
367
polarity_trainer.train()
368
369
# Create combined ABSA model
370
absa_model = AbsaModel(
371
aspect_model=aspect_model,
372
polarity_model=polarity_model
373
)
374
375
# Perform ABSA
376
test_texts = [
377
"The pasta was delicious but the service was really poor.",
378
"Great ambiance, overpriced drinks."
379
]
380
381
results = absa_model.predict(test_texts)
382
for i, result in enumerate(results):
383
print(f"Text {i+1}: {test_texts[i]}")
384
print(f"ABSA Results: {result}")
385
print()
386
```
387
388
### Training Aspect Extraction Model
389
390
```python
391
from setfit import AspectModel, AbsaTrainer, TrainingArguments
392
from datasets import Dataset
393
import spacy
394
395
# Load SpaCy model for tokenization
396
nlp = spacy.load("en_core_web_sm")
397
398
def create_aspect_training_data(texts, aspect_annotations):
399
"""
400
Create training data for aspect extraction from span annotations.
401
402
Parameters:
403
- texts: List of input texts
404
- aspect_annotations: List of (start, end, label) tuples for each text
405
406
Returns:
407
Dataset with span-level labels
408
"""
409
training_examples = []
410
411
for text, annotations in zip(texts, aspect_annotations):
412
doc = nlp(text)
413
414
# Create binary labels for each token/span
415
for token in doc:
416
is_aspect = False
417
aspect_label = "O" # Outside
418
419
# Check if token is part of an aspect span
420
for start, end, label in annotations:
421
if start <= token.idx < end:
422
is_aspect = True
423
aspect_label = label
424
break
425
426
training_examples.append({
427
"text": token.text,
428
"context": text,
429
"label": 1 if is_aspect else 0,
430
"aspect_type": aspect_label
431
})
432
433
return Dataset.from_list(training_examples)
434
435
# Example aspect annotations (start_char, end_char, aspect_type)
436
texts = [
437
"The pizza was amazing but the service was slow.",
438
"Great atmosphere, terrible food quality."
439
]
440
441
annotations = [
442
[(4, 9, "food"), (33, 40, "service")], # "pizza", "service"
443
[(6, 16, "ambiance"), (27, 39, "food")] # "atmosphere", "food quality"
444
]
445
446
# Create training dataset
447
aspect_dataset = create_aspect_training_data(texts, annotations)
448
449
# Initialize and train aspect model
450
aspect_model = AspectModel(
451
spacy_model="en_core_web_sm",
452
span_context=2 # Include 2 tokens of context around spans
453
)
454
455
aspect_trainer = AbsaTrainer(
456
model=aspect_model,
457
train_dataset=aspect_dataset,
458
args=TrainingArguments(
459
batch_size=32,
460
num_epochs=5,
461
learning_rate=2e-5,
462
eval_strategy="epoch"
463
)
464
)
465
466
aspect_trainer.train()
467
468
# Test aspect extraction
469
test_sentences = [
470
"The burger was tasty but the fries were cold.",
471
"Excellent wine selection, poor table service."
472
]
473
474
extracted_aspects = aspect_model.predict(test_sentences)
475
for sentence, aspects in zip(test_sentences, extracted_aspects):
476
print(f"Sentence: {sentence}")
477
print(f"Extracted aspects: {aspects}")
478
```
479
480
### Fine-grained ABSA with Multiple Aspects
481
482
```python
483
from setfit import AbsaModel, AspectModel, PolarityModel
484
import json
485
486
# Define aspect categories for restaurant reviews
487
ASPECT_CATEGORIES = {
488
"food": ["taste", "quality", "freshness", "variety"],
489
"service": ["speed", "friendliness", "attentiveness", "professionalism"],
490
"ambiance": ["atmosphere", "noise", "lighting", "decor"],
491
"value": ["price", "portion", "worth", "deals"]
492
}
493
494
class DetailedAbsaModel:
495
def __init__(self, aspect_model, polarity_model, aspect_categories):
496
self.aspect_model = aspect_model
497
self.polarity_model = polarity_model
498
self.aspect_categories = aspect_categories
499
500
def predict_detailed(self, text):
501
"""Perform detailed ABSA with aspect categorization."""
502
# Extract aspects
503
aspects = self.aspect_model.predict([text])[0]
504
505
# Categorize aspects
506
categorized_aspects = {}
507
for aspect in aspects:
508
for category, subcategories in self.aspect_categories.items():
509
if any(sub in aspect.lower() for sub in subcategories):
510
if category not in categorized_aspects:
511
categorized_aspects[category] = []
512
categorized_aspects[category].append(aspect)
513
break
514
else:
515
# Uncategorized aspect
516
if "other" not in categorized_aspects:
517
categorized_aspects["other"] = []
518
categorized_aspects["other"].append(aspect)
519
520
# Get sentiment for each aspect
521
results = {}
522
for category, category_aspects in categorized_aspects.items():
523
category_results = []
524
for aspect in category_aspects:
525
aspect_text = f"{aspect}: {text}"
526
polarity_probs = self.polarity_model.predict_proba([aspect_text])[0]
527
sentiment_score = polarity_probs[1] - polarity_probs[0] # positive - negative
528
529
category_results.append({
530
"aspect": aspect,
531
"sentiment_score": float(sentiment_score),
532
"sentiment": "positive" if sentiment_score > 0.1 else "negative" if sentiment_score < -0.1 else "neutral",
533
"confidence": float(max(polarity_probs))
534
})
535
536
results[category] = category_results
537
538
return results
539
540
# Initialize detailed ABSA model
541
detailed_model = DetailedAbsaModel(
542
aspect_model=aspect_model,
543
polarity_model=polarity_model,
544
aspect_categories=ASPECT_CATEGORIES
545
)
546
547
# Analyze restaurant review
548
review = """
549
The seafood pasta was absolutely delicious and the portion size was generous.
550
However, our waiter was quite rude and it took forever to get our drinks.
551
The restaurant is beautifully decorated but it was way too noisy to have a conversation.
552
For the price we paid, I expected much better service.
553
"""
554
555
detailed_results = detailed_model.predict_detailed(review)
556
557
print("Detailed ABSA Results:")
558
print(json.dumps(detailed_results, indent=2))
559
```
560
561
### ABSA Model Evaluation
562
563
```python
564
from setfit import AbsaModel
565
from sklearn.metrics import classification_report, accuracy_score
566
import numpy as np
567
568
def evaluate_absa_model(absa_model, test_dataset):
569
"""
570
Comprehensive evaluation of ABSA model performance.
571
572
Parameters:
573
- absa_model: Trained ABSA model
574
- test_dataset: Test dataset with ground truth aspects and sentiments
575
576
Returns:
577
Dictionary with evaluation metrics
578
"""
579
580
# Prepare test data
581
test_texts = test_dataset["text"]
582
true_aspects = test_dataset["aspects"] # List of lists
583
true_sentiments = test_dataset["sentiments"] # List of lists
584
585
# Get predictions
586
predictions = absa_model.predict(test_texts)
587
588
# Extract predicted aspects and sentiments
589
pred_aspects = []
590
pred_sentiments = []
591
592
for pred in predictions:
593
aspects = list(pred.keys())
594
sentiments = [pred[aspect]["sentiment"] for aspect in aspects]
595
pred_aspects.append(aspects)
596
pred_sentiments.append(sentiments)
597
598
# Aspect extraction evaluation
599
aspect_precision_scores = []
600
aspect_recall_scores = []
601
aspect_f1_scores = []
602
603
for true_asp, pred_asp in zip(true_aspects, pred_aspects):
604
true_set = set(true_asp)
605
pred_set = set(pred_asp)
606
607
if len(pred_set) > 0:
608
precision = len(true_set & pred_set) / len(pred_set)
609
else:
610
precision = 0.0
611
612
if len(true_set) > 0:
613
recall = len(true_set & pred_set) / len(true_set)
614
else:
615
recall = 1.0 if len(pred_set) == 0 else 0.0
616
617
if precision + recall > 0:
618
f1 = 2 * precision * recall / (precision + recall)
619
else:
620
f1 = 0.0
621
622
aspect_precision_scores.append(precision)
623
aspect_recall_scores.append(recall)
624
aspect_f1_scores.append(f1)
625
626
# Sentiment classification evaluation (for correctly identified aspects)
627
sentiment_true = []
628
sentiment_pred = []
629
630
for i, (true_asp, pred_asp, true_sent, pred_sent) in enumerate(
631
zip(true_aspects, pred_aspects, true_sentiments, pred_sentiments)
632
):
633
# Only evaluate sentiment for aspects that were correctly identified
634
for j, aspect in enumerate(true_asp):
635
if aspect in pred_asp:
636
aspect_idx = pred_asp.index(aspect)
637
sentiment_true.append(true_sent[j])
638
sentiment_pred.append(pred_sent[aspect_idx])
639
640
# Calculate metrics
641
results = {
642
"aspect_extraction": {
643
"precision": np.mean(aspect_precision_scores),
644
"recall": np.mean(aspect_recall_scores),
645
"f1": np.mean(aspect_f1_scores)
646
},
647
"sentiment_classification": {
648
"accuracy": accuracy_score(sentiment_true, sentiment_pred) if sentiment_true else 0.0,
649
"classification_report": classification_report(sentiment_true, sentiment_pred, output_dict=True) if sentiment_true else {}
650
},
651
"overall_scores": {
652
"num_samples": len(test_texts),
653
"avg_aspects_per_sample": np.mean([len(aspects) for aspects in true_aspects]),
654
"aspect_coverage": len(sentiment_true) / sum(len(aspects) for aspects in true_aspects) if true_aspects else 0.0
655
}
656
}
657
658
return results
659
660
# Example evaluation
661
test_data = {
662
"text": [
663
"Great food, terrible service.",
664
"The ambiance was nice but the food was cold."
665
],
666
"aspects": [
667
["food", "service"],
668
["ambiance", "food"]
669
],
670
"sentiments": [
671
["positive", "negative"],
672
["positive", "negative"]
673
]
674
}
675
676
test_dataset = Dataset.from_dict(test_data)
677
evaluation_results = evaluate_absa_model(absa_model, test_dataset)
678
679
print("ABSA Model Evaluation Results:")
680
print(f"Aspect Extraction F1: {evaluation_results['aspect_extraction']['f1']:.3f}")
681
print(f"Sentiment Classification Accuracy: {evaluation_results['sentiment_classification']['accuracy']:.3f}")
682
print(f"Aspect Coverage: {evaluation_results['overall_scores']['aspect_coverage']:.3f}")
683
```