shawhed commited on
Commit
a4bf0c5
Β·
verified Β·
1 Parent(s): df10fc7

Report measured control fidelity and story->review domain transfer

Browse files
Files changed (1) hide show
  1. README.md +70 -23
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 from βˆ’1.0 to +1.0. Sweeping the target over one sentence
38
- of the example below, holding everything else fixed:
39
 
40
  | target | achieved | rewritten sentence |
41
  | ---: | ---: | --- |
42
- | βˆ’1.0 | βˆ’0.5 | He had a terrible job to do. |
43
- | βˆ’0.9 | βˆ’0.8 | He was very angry and very sad. |
44
- | βˆ’0.6 | βˆ’0.6 | We were so tired from the trip. |
45
- | βˆ’0.3 | +0.0 | He was very polite and very slow. |
46
- | +0.0 | +0.0 | He brought us a bottle of wine. |
47
- | +0.3 | +0.5 | He said Welcome to the hotel. |
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-large")
86
 
87
  from senseshift import SenseShift
88
 
89
- shifter = SenseShift.from_pretrained("SenseShift-large")
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 narrative and review-style text.
 
156
  - VADER supplies the sentiment labels, so the model inherits its lexicon-based
157
- view of sentiment. `achieved_sentiment` usually lands within 0.1–0.2 of the
158
- target, but individual targets can miss outright β€” see the sweep above. The
159
- saturated ends (`Β±1.0`) are the least reliable.
 
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