Downloads · 30 days
30
52% of all-time downloads
susurofu/literary-bert-sentiment-eng
literary-bert-sentiment-eng is a text classification model from susurofu. Use it when you need a label for a piece of text. The card lists the license as apache-2.0.
NOTICE: This model was updated here: BERT-large: susurofu/literary-bert-en-large (340M) I recommend using this model instead.
Downloads · 30 days
30
52% of all-time downloads
All-time downloads
58
Public
Parameters
335M
1.3 GB on disk
Likes
0
Public
Click a slice to open those files.
.safetensors1.3 GB · 100%
From the Hugging Face model README
NOTICE: This model was updated here:
BERT-large: susurofu/literary-bert-en-large (340M)
I recommend using this model instead.
This is a fine-tuned version of google-bert/bert-large-uncased, designed specifically for literary sentiment analysis.
This early but fully operational version was fine-tuned on 10,889 samples selected from the Project Gutenberg collection.
Unlike most BERT-based sentiment analysis models, it features:
The model's perfomance was evaluated on marked up by human annotators the text of E. Hemingway's The Old Man and the Sea (dataset can be found in Bizzoni & Feldkamp (2024)). It was compared against cardiffnlp/twitter-roberta-base-sentiment-lates, SOTA model used in Digital Humanities and literary sentiment analysis.
| Metric | This Model | RoBERTa |
|---|---|---|
| MAE | 0.72 | 0.82 |
| Accuracy (within 1 class error) | 0.75 | 0.70 |
| Precision (within 1 class error) | 0.75 | 0.18 |
| Kendall's Correlation | 0.57 | 0.50 |
To run this model you need the class for model and the class to run it. Ensure you have them both in your script.
Model Class
import torch
import numpy as np
import torch.nn as nn
import torch.nn.functional as F
from transformers import (
AutoTokenizer,
BertModel,
BertPreTrainedModel,
)
from transformers.modeling_outputs import SequenceClassifierOutput
class BertForOrdinalRegression(BertPreTrainedModel):
def __init__(self, config):
super().__init__(config)
self.num_classes = config.num_labels
self.bert = BertModel(config)
dropout_probability = (
config.classifier_dropout
if config.classifier_dropout is not None
else config.hidden_dropout_prob
)
self.dropout = nn.Dropout(dropout_probability)
self.sentiment_score = nn.Linear(
config.hidden_size,
1,
)
self.first_threshold = nn.Parameter(
torch.tensor(-2.0)
)
self.threshold_deltas = nn.Parameter(
torch.zeros(self.num_classes - 2)
)
self.post_init()
def get_ordered_thresholds(self):
if self.num_classes == 2:
return self.first_threshold.reshape(1)
positive_deltas = F.softplus(
self.threshold_deltas
)
remaining_thresholds = (
self.first_threshold
+ torch.cumsum(
positive_deltas,
dim=0
)
)
return torch.cat([
self.first_threshold.reshape(1),
remaining_thresholds,
])
def forward(
self,
input_ids=None,
attention_mask=None,
token_type_ids=None,
labels=None,
**kwargs,
):
outputs = self.bert(
input_ids=input_ids,
attention_mask=attention_mask,
token_type_ids=token_type_ids,
return_dict=True,
)
pooled_output = self.dropout(
outputs.pooler_output
)
latent_score = self.sentiment_score(
pooled_output
)
thresholds = self.get_ordered_thresholds()
logits = (
latent_score
- thresholds.unsqueeze(0)
)
loss = None
if labels is not None:
labels = labels.long()
levels = torch.arange(
1,
self.num_classes,
device=labels.device,
).unsqueeze(0)
ordinal_targets = (
labels.unsqueeze(1) > levels
).float()
loss = F.binary_cross_entropy_with_logits(
logits,
ordinal_targets,
)
return SequenceClassifierOutput(
loss=loss,
logits=logits,
)
Analyzer Class
class SentimentAnalyzer:
def __init__(
self,
model_path,
device=None,
max_length=256,
batch_size=32,
):
self.model_path = model_path
self.max_length = max_length
self.batch_size = batch_size
if device is None:
if torch.cuda.is_available():
device = "cuda"
elif (
hasattr(torch.backends, "mps")
and torch.backends.mps.is_available()
):
device = "mps"
else:
device = "cpu"
self.device = torch.device(device)
self.tokenizer = AutoTokenizer.from_pretrained(
model_path
)
self.model = (
BertForOrdinalRegression.from_pretrained(
model_path
)
)
self.model.to(self.device)
self.model.eval()
self.num_classes = self.model.config.num_labels
print(
f"Model loaded from: {model_path}"
)
print(
f"Device: {self.device}"
)
print(
f"Sentiment scale: 1-{self.num_classes}"
)
def _predict_batch(self, texts):
inputs = self.tokenizer(
texts,
return_tensors="pt",
padding=True,
truncation=True,
max_length=self.max_length,
)
inputs = {
key: value.to(self.device)
for key, value in inputs.items()
}
with torch.inference_mode():
outputs = self.model(
**inputs
)
# P(y > k)
cumulative_probabilities = torch.sigmoid(
outputs.logits
)
# Expected ordinal sentiment:
# E[Y] = 1 + sum(P(Y > k))
scores = (
1.0
+ cumulative_probabilities.sum(dim=1)
)
scores = (
scores
.detach()
.cpu()
.numpy()
)
return scores
def predict(self, sentence):
# if you need to predict only for a single sentence
if not isinstance(sentence, str):
raise TypeError(
"sentence must be a string"
)
scores = self._predict_batch(
[sentence]
)
return float(scores[0])
def predict_list(self, sentences):
# predictions for a list of sentences
if not isinstance(sentences, list):
raise TypeError(
"sentences must be a list of strings"
)
if not all(
isinstance(sentence, str)
for sentence in sentences
):
raise TypeError(
"Every element must be a string"
)
if len(sentences) == 0:
return []
all_scores = []
for start in range(
0,
len(sentences),
self.batch_size,
):
batch = sentences[
start:start + self.batch_size
]
scores = self._predict_batch(
batch
)
all_scores.extend(
scores.tolist()
)
return all_scores
Finally, run it for sentiment analysis
sentiment_model = SentimentAnalyzer(
model_path="susurofu/literary-bert-sentiment-eng"
)
sentences = [
"In the first forty days a boy had been with him.",
"They were as old as erosions in a fishless desert.",
"The old man had taught the boy to fish and the boy loved him.",
]
scores = sentiment_model.predict_list(sentences)
print(scores)
# Outputs:
# [4.938718795776367, 4.118151664733887, 7.0]
The dataset was taken from: Bizzoni, Yuri, and Pascale Feldkamp. “Sentiment Analysis for Literary Texts: Hemingway as a Case-Study.” Journal of Data Mining & Digital Humanities, vol. NLP4DH, Apr. 2024, pp. 1–20, https://doi.org/10.46298/jdmdh.13155.
@misc{suslov2026distilling,
author = {Suslov, Artem and Hashimoto, Takehiro},
title = {Distilling Literary Sentiment from LLM Annotations into an Ordinal BERT Model},
year = {2026},
publisher = {Zenodo},
doi = {10.5281/zenodo.23554553},
url = {https://doi.org/10.5281/zenodo.23554553}
}