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
Report measured control fidelity and story->review domain transfer
Browse files
README.md
CHANGED
|
@@ -34,26 +34,16 @@ 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
|
| 38 |
-
of the example below, holding everything else fixed:
|
| 39 |
|
| 40 |
| target | achieved | rewritten sentence |
|
| 41 |
| ---: | ---: | --- |
|
| 42 |
-
| β
|
| 43 |
-
| β0.
|
| 44 |
-
|
|
| 45 |
-
|
|
| 46 |
-
| +0.
|
| 47 |
-
|
| 48 |
-
| +0.6 | +0.6 | He was very kind and very polite. |
|
| 49 |
-
| +0.9 | +0.7 | He smiled and said Thank you. |
|
| 50 |
-
| +1.0 | +0.9 | We were so happy and felt successful. |
|
| 51 |
-
|
| 52 |
-
The trend is monotone and most targets land within 0.1β0.2, but the control is
|
| 53 |
-
not exact: `β1.0` undershot to `β0.5` and `β0.3` came out neutral. Treat the
|
| 54 |
-
dial as a strong steer, not a guarantee β and check `out.achieved_sentiment`
|
| 55 |
-
if you need one. [SenseShift-large](https://huggingface.co/shawhed/SenseShift-large)
|
| 56 |
-
tracks the target more tightly.
|
| 57 |
- **The generator is an encoder.** Text generation is nearly always
|
| 58 |
autoregressive decoding. SenseShift generates by iteratively filling masked
|
| 59 |
positions in a bidirectional ModernBERT, so every token it writes is
|
|
@@ -82,11 +72,11 @@ git clone https://huggingface.co/shawhed/SenseShift-base
|
|
| 82 |
|
| 83 |
```python
|
| 84 |
import sys
|
| 85 |
-
sys.path.insert(0, "SenseShift-
|
| 86 |
|
| 87 |
from senseshift import SenseShift
|
| 88 |
|
| 89 |
-
shifter = SenseShift.from_pretrained("SenseShift-
|
| 90 |
|
| 91 |
text = ("The waiter greeted us at the door. "
|
| 92 |
"The food arrived quickly and was still hot. "
|
|
@@ -150,13 +140,70 @@ Steps 1β4 live in the `senseshift` package, not in the weights β the checkpo
|
|
| 150 |
itself is a stock `ModernBertForMaskedLM` and can be loaded with
|
| 151 |
`AutoModelForMaskedLM` if you want to build your own decoding loop.
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
## Limitations
|
| 154 |
|
| 155 |
-
- English only; trained on short
|
|
|
|
| 156 |
- VADER supplies the sentiment labels, so the model inherits its lexicon-based
|
| 157 |
-
view of sentiment.
|
| 158 |
-
|
| 159 |
-
|
|
|
|
| 160 |
- The rewrite is length-bounded by `num_masks`, so very long sentences are
|
| 161 |
usually replaced by something shorter.
|
| 162 |
- Because the whole passage is re-encoded per mask fill, generation cost grows
|
|
|
|
| 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
|
|
|
|
| 72 |
|
| 73 |
```python
|
| 74 |
import sys
|
| 75 |
+
sys.path.insert(0, "SenseShift-base")
|
| 76 |
|
| 77 |
from senseshift import SenseShift
|
| 78 |
|
| 79 |
+
shifter = SenseShift.from_pretrained("SenseShift-base")
|
| 80 |
|
| 81 |
text = ("The waiter greeted us at the door. "
|
| 82 |
"The food arrived quickly and was still hot. "
|
|
|
|
| 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
|