Text Generation
Transformers
Safetensors
English
modernbert
fill-mask
sentiment-control
continuous-control
controllable-text-generation
encoder-generation
non-autoregressive
masked-language-model
text-style-transfer
data-augmentation
emnlp2026
Instructions to use shawhed/SenseShift-base with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use shawhed/SenseShift-base with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="shawhed/SenseShift-base")# Load model directly from transformers import AutoTokenizer, AutoModelForMaskedLM tokenizer = AutoTokenizer.from_pretrained("shawhed/SenseShift-base") model = AutoModelForMaskedLM.from_pretrained("shawhed/SenseShift-base", device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use shawhed/SenseShift-base with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "shawhed/SenseShift-base" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "shawhed/SenseShift-base", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/shawhed/SenseShift-base
- SGLang
How to use shawhed/SenseShift-base with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "shawhed/SenseShift-base" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "shawhed/SenseShift-base", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "shawhed/SenseShift-base" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "shawhed/SenseShift-base", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use shawhed/SenseShift-base with Docker Model Runner:
docker model run hf.co/shawhed/SenseShift-base
Focused model card; explicit generation parameters
Browse files- README.md +114 -156
- senseshift/__pycache__/__init__.cpython-311.pyc +0 -0
- senseshift/__pycache__/decoding.cpython-311.pyc +0 -0
- senseshift/__pycache__/masking.cpython-311.pyc +0 -0
- senseshift/__pycache__/pipeline.cpython-311.pyc +0 -0
- senseshift/__pycache__/text_utils.cpython-311.pyc +0 -0
- senseshift/pipeline.py +38 -7
README.md
CHANGED
|
@@ -12,8 +12,7 @@ tags:
|
|
| 12 |
- non-autoregressive
|
| 13 |
- masked-language-model
|
| 14 |
- text-style-transfer
|
| 15 |
-
-
|
| 16 |
-
- text-rewriting
|
| 17 |
- modernbert
|
| 18 |
- emnlp2026
|
| 19 |
language:
|
|
@@ -22,49 +21,79 @@ language:
|
|
| 22 |
|
| 23 |
# SenseShift-base
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
> that *writes* text at a sentiment you dial in.
|
| 28 |
-
|
| 29 |
-
SenseShift rewrites a sentence — or writes a new one — at **any sentiment you ask
|
| 30 |
-
for on a continuous −1.0 to +1.0 scale**, while keeping it consistent with the
|
| 31 |
-
surrounding text.
|
| 32 |
-
|
| 33 |
-
Two things make it unusual:
|
| 34 |
-
|
| 35 |
-
- **Continuous, not categorical.** Most sentiment-controlled generation offers
|
| 36 |
-
you `positive` / `negative` / `neutral`. SenseShift takes a *number* — 21
|
| 37 |
-
points on a 0.1 grid. Sweeping one sentence of a story, everything else fixed:
|
| 38 |
-
|
| 39 |
-
| target | achieved | rewritten sentence |
|
| 40 |
-
| ---: | ---: | --- |
|
| 41 |
-
| −0.9 | −0.7 | The water was bitter and made him sick. |
|
| 42 |
-
| −0.6 | −0.4 | The water was bitter and made him cough. |
|
| 43 |
-
| +0.0 | +0.0 | The water was cold and made him wet. |
|
| 44 |
-
| +0.6 | +0.7 | The water was clear and tasted very good. |
|
| 45 |
-
| +0.9 | +0.8 | His friends laughed and played games with him. |
|
| 46 |
-
|
| 47 |
-
- **The generator is an encoder.** Text generation is nearly always
|
| 48 |
-
autoregressive decoding. SenseShift generates by iteratively filling masked
|
| 49 |
-
positions in a bidirectional ModernBERT, so every token it writes is
|
| 50 |
-
conditioned on the text on *both* sides. That is what lets it drop a
|
| 51 |
-
replacement sentence into the middle of a passage and have it fit.
|
| 52 |
-
|
| 53 |
-
It is a masked language model fine-tuned from ModernBERT-base with an explicit
|
| 54 |
-
**control vocabulary**: 21 special tokens `[-1.0] … [1.0]` on a 0.1 grid. At
|
| 55 |
-
training time every sentence is prefixed with its own VADER sentiment token and
|
| 56 |
-
one sentence is masked out, so the model learns to write a replacement that
|
| 57 |
-
realises the requested sentiment in context.
|
| 58 |
|
| 59 |
| Model | Base | Params |
|
| 60 |
| --- | --- | --- |
|
| 61 |
| **SenseShift-base** (this model) | ModernBERT-base | 150M |
|
| 62 |
-
| [SenseShift-large](https://huggingface.co/shawhed/SenseShift-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
## Usage
|
| 65 |
|
| 66 |
-
No install needed — the inference code ships
|
| 67 |
-
have the weights and the code together:
|
| 68 |
|
| 69 |
```bash
|
| 70 |
git clone https://huggingface.co/shawhed/SenseShift-base
|
|
@@ -78,138 +107,67 @@ from senseshift import SenseShift
|
|
| 78 |
|
| 79 |
shifter = SenseShift.from_pretrained("SenseShift-base")
|
| 80 |
|
| 81 |
-
text = ("
|
| 82 |
-
"
|
| 83 |
-
"
|
| 84 |
|
| 85 |
-
# Rewrite sentence
|
| 86 |
-
out = shifter.generate(text, generation_mode="rewrite", sentence_index=1, sentiment=-0.
|
| 87 |
-
print(out.sentence)
|
| 88 |
-
print(out.text)
|
| 89 |
|
| 90 |
-
#
|
| 91 |
-
out = shifter.generate(text, generation_mode="add", sentiment=0.
|
| 92 |
print(out.text)
|
| 93 |
```
|
| 94 |
|
| 95 |
-
|
|
|
|
| 96 |
|
| 97 |
-
``
|
| 98 |
-
import sys
|
| 99 |
-
from huggingface_hub import snapshot_download
|
| 100 |
-
|
| 101 |
-
path = snapshot_download("shawhed/SenseShift-base")
|
| 102 |
-
sys.path.insert(0, path)
|
| 103 |
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
``
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
-
|
| 109 |
-
`requirements.txt`). The VADER lexicon downloads itself on first use.
|
| 110 |
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
| `generation_mode` | `"rewrite"` replaces the sentence at `sentence_index`; `"add"` inserts a new sentence right after it. |
|
| 117 |
-
| `sentiment` | `None` → keep the sentiment already there. `"random"` → a random grid value, excluding the current one. A number in `[-1, 1]` → that value, snapped to the nearest 0.1. |
|
| 118 |
-
| `sentence_index` | Which sentence to act on. Defaults to a random sentence (`rewrite`) or the last sentence (`add`). Negative indices count from the end. |
|
| 119 |
-
| `num_masks` | How many mask slots the model gets, i.e. roughly how long the new sentence is. Defaults to the replaced sentence's word count (`rewrite`) or `12` (`add`). |
|
| 120 |
-
| `seed` | Seeds the random index / sentiment draws. |
|
| 121 |
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
`
|
|
|
|
| 125 |
|
| 126 |
-
|
| 127 |
-
`.target_sentiment`, `.achieved_sentiment` (VADER of what was actually written),
|
| 128 |
-
`.beams`, and friends. `str(out)` gives the edited passage.
|
| 129 |
|
| 130 |
-
|
|
|
|
|
|
|
| 131 |
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
normalisation and a diversity penalty, stopping at terminal punctuation.
|
| 137 |
-
4. Splice the decoded sentence back into the passage.
|
| 138 |
-
|
| 139 |
-
Steps 1–4 live in the `senseshift` package, not in the weights — the checkpoint
|
| 140 |
-
itself is a stock `ModernBertForMaskedLM` and can be loaded with
|
| 141 |
-
`AutoModelForMaskedLM` if you want to build your own decoding loop.
|
| 142 |
-
|
| 143 |
-
## Control fidelity, and how far it transfers
|
| 144 |
-
|
| 145 |
-
Measured over 50 held-out passages per domain, 9 targets each, rewriting one
|
| 146 |
-
randomly chosen sentence. "Achieved" is VADER on the sentence the model wrote.
|
| 147 |
-
|
| 148 |
-
| target | achieved — stories *(in-domain)* | achieved — reviews *(out-of-domain)* |
|
| 149 |
-
| ---: | ---: | ---: |
|
| 150 |
-
| −0.9 | −0.40 | −0.44 |
|
| 151 |
-
| −0.6 | −0.23 | −0.25 |
|
| 152 |
-
| −0.3 | −0.14 | −0.04 |
|
| 153 |
-
| +0.0 | +0.09 | +0.01 |
|
| 154 |
-
| +0.3 | +0.22 | +0.14 |
|
| 155 |
-
| +0.6 | +0.53 | +0.47 |
|
| 156 |
-
| +0.9 | +0.75 | +0.73 |
|
| 157 |
-
| **MAE** | **0.33** | **0.33** |
|
| 158 |
-
|
| 159 |
-
Two things to read off this.
|
| 160 |
-
|
| 161 |
-
**The response is monotone and the positive half is well calibrated** — targets
|
| 162 |
-
of +0.6 and +0.9 land within about 0.15. The negative half is compressed: ask
|
| 163 |
-
for −0.9 and you reliably get *more negative*, but around −0.4 rather than −0.9.
|
| 164 |
-
|
| 165 |
-
**Domain transfer is essentially free.** SenseShift was trained only on
|
| 166 |
-
short children's stories, yet product reviews — a domain it never saw, with
|
| 167 |
-
different vocabulary, register and length — come out at an identical MAE of
|
| 168 |
-
0.33. The per-target curve is the same shape. Use it outside the story domain
|
| 169 |
-
with reasonable confidence.
|
| 170 |
-
|
| 171 |
-
## Training data and usable range
|
| 172 |
-
|
| 173 |
-
Trained on TinyStories-style children's stories, labelled per sentence with
|
| 174 |
-
VADER. That corpus is lopsided, which directly shapes what the model can do:
|
| 175 |
-
|
| 176 |
-
| sentiment of training sentences | share |
|
| 177 |
-
| --- | ---: |
|
| 178 |
-
| negative (< 0) | 15.3% |
|
| 179 |
-
| neutral (= 0) | 37.8% |
|
| 180 |
-
| positive (> 0) | 47.0% |
|
| 181 |
-
| exactly −1.0 | **0.00%** |
|
| 182 |
-
| exactly +1.0 | 0.10% |
|
| 183 |
-
|
| 184 |
-
So:
|
| 185 |
-
|
| 186 |
-
- **Use targets in −0.8 … +0.9.** The `[-1.0]` control token appears *zero*
|
| 187 |
-
times in training and `[1.0]` almost never; asking for ±1.0 gives an
|
| 188 |
-
undertrained token and unpredictable output. `-1.0` in particular comes back
|
| 189 |
-
near-neutral.
|
| 190 |
-
- **Negative control is looser than positive**, because negative sentences are
|
| 191 |
-
a third as common in the corpus. For strongly negative text, ask for −0.9 and
|
| 192 |
-
expect roughly −0.4, or resample.
|
| 193 |
-
|
| 194 |
-
The fidelity numbers above were measured on
|
| 195 |
-
[SenseShift-large](https://huggingface.co/shawhed/SenseShift-large); this 150M
|
| 196 |
-
variant follows the same shape but tracks the target less tightly.
|
| 197 |
-
|
| 198 |
-
## Limitations
|
| 199 |
-
|
| 200 |
-
- English only; trained on short children's stories, though it transfers to
|
| 201 |
-
other domains at no measured cost (see above).
|
| 202 |
-
- VADER supplies the sentiment labels, so the model inherits its lexicon-based
|
| 203 |
-
view of sentiment.
|
| 204 |
-
- Control is a steer, not a guarantee: MAE 0.33 over the grid, with the negative
|
| 205 |
-
half compressed and `±1.0` unusable. Single generations are noisy — check
|
| 206 |
-
`out.achieved_sentiment` and resample if you need a specific value.
|
| 207 |
-
- The rewrite is length-bounded by `num_masks`, so very long sentences are
|
| 208 |
-
usually replaced by something shorter.
|
| 209 |
-
- Because the whole passage is re-encoded per mask fill, generation cost grows
|
| 210 |
-
with `num_masks × beam_size`.
|
| 211 |
|
| 212 |
## Citation
|
| 213 |
|
| 214 |
-
Accepted to
|
| 215 |
-
|
|
|
|
| 12 |
- non-autoregressive
|
| 13 |
- masked-language-model
|
| 14 |
- text-style-transfer
|
| 15 |
+
- data-augmentation
|
|
|
|
| 16 |
- modernbert
|
| 17 |
- emnlp2026
|
| 18 |
language:
|
|
|
|
| 21 |
|
| 22 |
# SenseShift-base
|
| 23 |
|
| 24 |
+
Rewrite any sentence in a passage — or write a new one — at a sentiment you
|
| 25 |
+
choose on a continuous scale from **−1.0 to +1.0**, in context.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
| Model | Base | Params |
|
| 28 |
| --- | --- | --- |
|
| 29 |
| **SenseShift-base** (this model) | ModernBERT-base | 150M |
|
| 30 |
+
| [SenseShift-large](https://huggingface.co/shawhed/SenseShift-base) | ModernBERT-large | 396M |
|
| 31 |
+
|
| 32 |
+
Accepted to EMNLP 2026.
|
| 33 |
+
|
| 34 |
+
## How it works
|
| 35 |
+
|
| 36 |
+
Sentiment is a **control vocabulary**: 21 special tokens `[-1.0] … [1.0]` on a
|
| 37 |
+
0.1 grid, added to the ModernBERT tokenizer.
|
| 38 |
+
|
| 39 |
+
Training: every sentence in a passage is scored with VADER and prefixed with its
|
| 40 |
+
own sentiment token. One sentence is then masked out. The model must reconstruct
|
| 41 |
+
it from the token it was given and the text on both sides — so it learns to
|
| 42 |
+
write a sentence that hits a requested sentiment *and* fits its neighbours.
|
| 43 |
+
|
| 44 |
+
Generation reverses this. Prefix each sentence with its current sentiment, give
|
| 45 |
+
the target sentence the sentiment you *want*, mask its words, and fill the masks
|
| 46 |
+
with beam search. Because the generator is a bidirectional encoder rather than a
|
| 47 |
+
decoder, every token it writes is conditioned on the text before **and** after —
|
| 48 |
+
which is what lets it replace a sentence mid-passage.
|
| 49 |
+
|
| 50 |
+
## Examples
|
| 51 |
+
|
| 52 |
+
Rewriting sentence `[3]` at **−0.8**. The model picks up "tunnel" from the
|
| 53 |
+
preceding sentence:
|
| 54 |
+
|
| 55 |
+
> Once upon a time, there was a little boy named Timmy. One day, Timmy went to a
|
| 56 |
+
> park with his mommy. At the park, there was a big tunnel that Timmy wanted to
|
| 57 |
+
> explore. **~~Timmy's mommy said he could choose whether or not to go through
|
| 58 |
+
> the tunnel.~~ → But Timmy's mommy said no because it was too dangerous.**
|
| 59 |
+
> Timmy was brave and decided to go through the tunnel. As Timmy crawled through
|
| 60 |
+
> the tunnel, he felt mighty and strong. When he came out the other side, he was
|
| 61 |
+
> so happy and proud of himself. From that day on, Timmy loved going on
|
| 62 |
+
> adventures and choosing to be brave.
|
| 63 |
+
|
| 64 |
+
Rewriting sentence `[2]` of a restaurant review at **+0.8**. The model carries
|
| 65 |
+
the business name "Lotus 2" in from context:
|
| 66 |
+
|
| 67 |
+
> I ordered delivery this evening and I must say that I was pleasantly surprised
|
| 68 |
+
> at the level of customer service on the phone and at my door. I usually get my
|
| 69 |
+
> Chinese take out from best wok 2 and they pale in comparison to lotus 2.
|
| 70 |
+
> **~~I ordered the exact same things that I usually order from best wok and
|
| 71 |
+
> everything tasted better and was of better quality and was even a little less
|
| 72 |
+
> expensive.~~ → Lotus 2 are very friendly and always make sure that I get the
|
| 73 |
+
> best quality of food and I get a lot of value for my money.** I am so happy
|
| 74 |
+
> that I've found a new Chinese take out. And they deliver! Of course its not
|
| 75 |
+
> P.F. Changs. But its well worth what you pay.
|
| 76 |
+
|
| 77 |
+
## Use cases
|
| 78 |
+
|
| 79 |
+
- **Writing assistant for fiction.** Adjust the emotional arc of a draft one
|
| 80 |
+
sentence at a time — darken a turning point, soften an ending — without
|
| 81 |
+
rewriting the surrounding prose.
|
| 82 |
+
- **Review and copy editing.** Retune the tone of a testimonial, product
|
| 83 |
+
description or release note while keeping the concrete details intact.
|
| 84 |
+
- **Data augmentation.** Generate sentiment-varied paraphrases of a corpus at
|
| 85 |
+
known target values: balance a skewed sentiment dataset, or produce minimal
|
| 86 |
+
pairs that differ in sentiment but share context.
|
| 87 |
+
- **Counterfactual and robustness testing.** Probe a downstream classifier with
|
| 88 |
+
inputs where exactly one sentence's sentiment moved, holding everything else
|
| 89 |
+
fixed.
|
| 90 |
+
- **Controllable-generation research.** A non-autoregressive baseline for
|
| 91 |
+
continuous attribute control, and a testbed for how far a control signal can
|
| 92 |
+
be pushed at inference time.
|
| 93 |
|
| 94 |
## Usage
|
| 95 |
|
| 96 |
+
No install needed — the inference code ships in this repo.
|
|
|
|
| 97 |
|
| 98 |
```bash
|
| 99 |
git clone https://huggingface.co/shawhed/SenseShift-base
|
|
|
|
| 107 |
|
| 108 |
shifter = SenseShift.from_pretrained("SenseShift-base")
|
| 109 |
|
| 110 |
+
text = ("At the park, there was a big tunnel that Timmy wanted to explore. "
|
| 111 |
+
"Timmy's mommy said he could choose whether or not to go through the tunnel. "
|
| 112 |
+
"Timmy was brave and decided to go through the tunnel.")
|
| 113 |
|
| 114 |
+
# Rewrite a sentence at a target sentiment
|
| 115 |
+
out = shifter.generate(text, generation_mode="rewrite", sentence_index=1, sentiment=-0.8)
|
| 116 |
+
print(out.sentence)
|
| 117 |
+
print(out.text)
|
| 118 |
|
| 119 |
+
# Write a new sentence and splice it in after sentence 2
|
| 120 |
+
out = shifter.generate(text, generation_mode="add", sentence_index=2, sentiment=0.9)
|
| 121 |
print(out.text)
|
| 122 |
```
|
| 123 |
|
| 124 |
+
Requires `torch`, `transformers`, `huggingface-hub`, `nltk`. The VADER lexicon
|
| 125 |
+
downloads itself on first use.
|
| 126 |
|
| 127 |
+
### `generate` arguments
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
+
| Argument | Default | Meaning |
|
| 130 |
+
| --- | --- | --- |
|
| 131 |
+
| `text` | — | The passage to edit. |
|
| 132 |
+
| `generation_mode` | `"rewrite"` | `"rewrite"` replaces the sentence at `sentence_index`; `"add"` inserts a new sentence after it. |
|
| 133 |
+
| `sentiment` | `None` | `None` keeps the current sentiment; `"random"` draws from the grid; a number in `[-1, 1]` is snapped to the nearest 0.1. |
|
| 134 |
+
| `sentence_index` | `None` | Which sentence to act on. Defaults to a random sentence (`rewrite`) or the last one (`add`). Negative indices count from the end. |
|
| 135 |
+
| `num_masks` | `None` | Mask slots given to the model, i.e. roughly how long the new sentence is. Defaults to the replaced sentence's word count, or 12 for `add`. |
|
| 136 |
+
| `seed` | `None` | Seeds the random index / sentiment draws. |
|
| 137 |
|
| 138 |
+
Decoding parameters — leave any at `None` to use the model's defaults:
|
|
|
|
| 139 |
|
| 140 |
+
| Argument | Default | Meaning |
|
| 141 |
+
| --- | --- | --- |
|
| 142 |
+
| `beam_size` | `2` | Hypotheses kept alive. Higher is slower and usually more fluent. `num_beams` works as an alias. |
|
| 143 |
+
| `top_k` | `40` | Candidate tokens considered per masked position. |
|
| 144 |
+
| `temperature` | `0.8` | Below 1.0 is more conservative, above 1.0 more varied. |
|
| 145 |
+
| `alpha` | `0.7` | Length-normalisation exponent; higher tolerates longer output. |
|
| 146 |
+
| `gamma` | `0.05` | Diversity penalty on beams reusing the same token. |
|
| 147 |
+
| `max_iters` | `30` | Cap on mask-filling steps. |
|
| 148 |
+
| `min_words` | `3` | Tokens filled before the model may stop at punctuation. Raise to avoid very short rewrites. |
|
| 149 |
|
| 150 |
+
```python
|
| 151 |
+
out = shifter.generate(text, sentence_index=1, sentiment=-0.8, beam_size=8, top_k=60, temperature=0.9)
|
| 152 |
+
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
|
| 154 |
+
`generate` returns a `SenseShiftOutput` with `.text` (the edited passage),
|
| 155 |
+
`.sentence` (what was written), `.original_sentence`, `.target_sentiment`,
|
| 156 |
+
`.achieved_sentiment` (VADER of the result) and `.beams`. `str(out)` gives the
|
| 157 |
+
passage.
|
| 158 |
|
| 159 |
+
## Training data
|
|
|
|
|
|
|
| 160 |
|
| 161 |
+
TinyStories-style short children's stories, labelled per sentence with VADER.
|
| 162 |
+
Despite the narrow training domain it transfers to other English prose — the
|
| 163 |
+
review example above is out-of-domain.
|
| 164 |
|
| 165 |
+
Negative sentiment is under-represented in that corpus, so the negative half of
|
| 166 |
+
the range is a looser steer than the positive half, and the extremes `±1.0` are
|
| 167 |
+
effectively untrained. **Use targets in −0.9 … +0.9**, and read
|
| 168 |
+
`out.achieved_sentiment` if you need a specific value.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
## Citation
|
| 171 |
|
| 172 |
+
Accepted to EMNLP 2026. A preprint and BibTeX entry will be linked here once
|
| 173 |
+
they are public.
|
senseshift/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (601 Bytes). View file
|
|
|
senseshift/__pycache__/decoding.cpython-311.pyc
ADDED
|
Binary file (8.27 kB). View file
|
|
|
senseshift/__pycache__/masking.cpython-311.pyc
ADDED
|
Binary file (5.33 kB). View file
|
|
|
senseshift/__pycache__/pipeline.cpython-311.pyc
ADDED
|
Binary file (15.6 kB). View file
|
|
|
senseshift/__pycache__/text_utils.cpython-311.pyc
ADDED
|
Binary file (7.1 kB). View file
|
|
|
senseshift/pipeline.py
CHANGED
|
@@ -121,7 +121,14 @@ class SenseShift:
|
|
| 121 |
sentence_index: Optional[int] = None,
|
| 122 |
num_masks: Optional[int] = None,
|
| 123 |
seed: Optional[int] = None,
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
) -> SenseShiftOutput:
|
| 126 |
"""Rewrite one sentence, or add a new one, at a controlled sentiment.
|
| 127 |
|
|
@@ -140,8 +147,19 @@ class SenseShift:
|
|
| 140 |
length of what it writes. Defaults to the replaced sentence's
|
| 141 |
word count (``"rewrite"``) or ``add_num_masks`` (``"add"``).
|
| 142 |
seed: Seed for the sentence/sentiment draws, for reproducibility.
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
Returns:
|
| 147 |
A :class:`SenseShiftOutput`; ``str(out)`` is the edited passage.
|
|
@@ -151,10 +169,23 @@ class SenseShift:
|
|
| 151 |
f"generation_mode must be 'rewrite' or 'add', got {generation_mode!r}"
|
| 152 |
)
|
| 153 |
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
rng = _random.Random(seed) if seed is not None else _random
|
| 160 |
|
|
|
|
| 121 |
sentence_index: Optional[int] = None,
|
| 122 |
num_masks: Optional[int] = None,
|
| 123 |
seed: Optional[int] = None,
|
| 124 |
+
beam_size: Optional[int] = None,
|
| 125 |
+
top_k: Optional[int] = None,
|
| 126 |
+
temperature: Optional[float] = None,
|
| 127 |
+
alpha: Optional[float] = None,
|
| 128 |
+
gamma: Optional[float] = None,
|
| 129 |
+
max_iters: Optional[int] = None,
|
| 130 |
+
min_words: Optional[int] = None,
|
| 131 |
+
num_beams: Optional[int] = None,
|
| 132 |
) -> SenseShiftOutput:
|
| 133 |
"""Rewrite one sentence, or add a new one, at a controlled sentiment.
|
| 134 |
|
|
|
|
| 147 |
length of what it writes. Defaults to the replaced sentence's
|
| 148 |
word count (``"rewrite"``) or ``add_num_masks`` (``"add"``).
|
| 149 |
seed: Seed for the sentence/sentiment draws, for reproducibility.
|
| 150 |
+
beam_size: Hypotheses kept alive during decoding. Higher is slower
|
| 151 |
+
and usually more fluent. ``num_beams`` is accepted as an alias.
|
| 152 |
+
top_k: Candidate tokens considered at each masked position.
|
| 153 |
+
temperature: Softmax temperature. Below 1.0 is more conservative,
|
| 154 |
+
above 1.0 more varied.
|
| 155 |
+
alpha: Length-normalisation exponent; higher tolerates longer output.
|
| 156 |
+
gamma: Diversity penalty on beams reusing the same token.
|
| 157 |
+
max_iters: Cap on mask-filling steps.
|
| 158 |
+
min_words: Tokens that must be filled before the model may stop at
|
| 159 |
+
terminal punctuation. Raise it to prevent very short rewrites.
|
| 160 |
+
|
| 161 |
+
Any decoding argument left as ``None`` falls back to the model's
|
| 162 |
+
defaults from ``senseshift_config.json``.
|
| 163 |
|
| 164 |
Returns:
|
| 165 |
A :class:`SenseShiftOutput`; ``str(out)`` is the edited passage.
|
|
|
|
| 169 |
f"generation_mode must be 'rewrite' or 'add', got {generation_mode!r}"
|
| 170 |
)
|
| 171 |
|
| 172 |
+
if num_beams is not None:
|
| 173 |
+
if beam_size is not None and beam_size != num_beams:
|
| 174 |
+
raise TypeError("Pass either beam_size or num_beams, not both.")
|
| 175 |
+
beam_size = num_beams
|
| 176 |
+
|
| 177 |
+
overrides = {
|
| 178 |
+
"beam_size": beam_size, "top_k": top_k, "temperature": temperature,
|
| 179 |
+
"alpha": alpha, "gamma": gamma, "max_iters": max_iters,
|
| 180 |
+
"min_words": min_words,
|
| 181 |
+
}
|
| 182 |
+
params = {**self.generation_defaults,
|
| 183 |
+
**{k: v for k, v in overrides.items() if v is not None}}
|
| 184 |
+
|
| 185 |
+
if params["beam_size"] < 1 or params["top_k"] < 1:
|
| 186 |
+
raise ValueError("beam_size and top_k must be >= 1.")
|
| 187 |
+
if params["temperature"] <= 0:
|
| 188 |
+
raise ValueError("temperature must be > 0.")
|
| 189 |
|
| 190 |
rng = _random.Random(seed) if seed is not None else _random
|
| 191 |
|