tuanh23 commited on
Commit
e6d6bac
·
verified ·
1 Parent(s): 87ab0d0

Initial upload: sigmoid QE head for TowerInstruct-7B-v0.2

Browse files
Files changed (4) hide show
  1. README.md +197 -0
  2. config.json +9 -0
  3. model.safetensors +3 -0
  4. sigmoid_head.py +44 -0
README.md ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: cc-by-nc-4.0
3
+ library_name: pytorch
4
+ tags:
5
+ - quality-estimation
6
+ - sigmoid-head
7
+ - machine-translation
8
+ - confidence-estimation
9
+ - tower
10
+ base_model: Unbabel/TowerInstruct-7B-v0.2
11
+ ---
12
+
13
+ # Sigmoid Head for TowerInstruct-7B-v0.2
14
+
15
+ This repo hosts a **sigmoid quality-estimation (QE) head** trained on top of
16
+ [`Unbabel/TowerInstruct-7B-v0.2`](https://huggingface.co/Unbabel/TowerInstruct-7B-v0.2).
17
+
18
+ It is the model from the paper *Sigmoid Head for Quality Estimation under
19
+ Language Ambiguity*. Unlike the usual softmax LM head, this head uses a
20
+ **sigmoid activation**, so multiple equally-valid tokens can simultaneously
21
+ receive high scores. This produces a more reliable per-token quality / confidence
22
+ score in settings with language ambiguity (e.g. machine translation).
23
+
24
+ - **Base model:** `Unbabel/TowerInstruct-7B-v0.2` (frozen during training)
25
+ - **Head type:** new unembedding head — a `torch.nn.Embedding(vocab_size, hidden_size)` applied to the last hidden state
26
+ - **Activation:** sigmoid (per-token, not normalized over vocab)
27
+ - **Shape:** `[32007, 4096]`
28
+ - **Trained with:** ambiguity-aware negative sampling
29
+
30
+ ## Files
31
+
32
+ - `model.safetensors` — the trained head weights (single tensor `weight`).
33
+ - `config.json` — `SigmoidHeadConfig` (vocab/hidden sizes + `auto_map`).
34
+ - `sigmoid_head.py` — `SigmoidHead(PreTrainedModel)` definition; auto-loaded by `transformers` via `trust_remote_code=True`.
35
+
36
+ ## Usage
37
+
38
+ The head is loaded with `transformers.AutoModel`. Pass `trust_remote_code=True`
39
+ so `transformers` downloads `sigmoid_head.py` from this repo automatically.
40
+
41
+ ### 1. Score an existing output (teacher forcing)
42
+
43
+ Given a (source-prompt, hypothesis) pair, compute a per-token confidence for the
44
+ hypothesis. Useful for QE on outputs from any MT system.
45
+
46
+ ```python
47
+ import torch
48
+ from transformers import AutoModel, AutoModelForCausalLM, AutoTokenizer
49
+
50
+ BASE = "Unbabel/TowerInstruct-7B-v0.2"
51
+ HEAD = "tuanh23/SigmoidHead-TowerInstruct-7B-v0.2"
52
+ device = "cuda" if torch.cuda.is_available() else "cpu"
53
+
54
+ tokenizer = AutoTokenizer.from_pretrained(BASE)
55
+ base_model = AutoModelForCausalLM.from_pretrained(BASE, torch_dtype=torch.bfloat16).to(device).eval()
56
+ head = AutoModel.from_pretrained(HEAD, trust_remote_code=True).to(device).eval()
57
+
58
+ # Same chat-template format the head was trained on (see prepare_data.py).
59
+ src_lang, tgt_lang = "English", "German"
60
+ src = "The cat sat on the mat."
61
+ hypothesis = "Die Katze saß auf der Matte."
62
+ user_msg = {"role": "user", "content": f"Translate the following text from {src_lang} into {tgt_lang}.\n{src_lang}: {src}.\n{tgt_lang}: "}
63
+ asst_msg = {"role": "assistant", "content": " " + hypothesis}
64
+
65
+ # Full conversation -> input_ids for the model
66
+ input_ids = tokenizer.apply_chat_template(
67
+ [user_msg, asst_msg], tokenize=True, add_generation_prompt=False, return_tensors="pt"
68
+ ).to(device)
69
+ # Same encoding but with the generation prompt added after the user turn -> tells us
70
+ # where the assistant content begins inside `input_ids`.
71
+ prompt_len = tokenizer.apply_chat_template(
72
+ [user_msg], tokenize=True, add_generation_prompt=True, return_tensors="pt"
73
+ ).shape[1]
74
+
75
+ with torch.no_grad():
76
+ out = base_model(input_ids, output_hidden_states=True)
77
+ last_hidden = out.hidden_states[-1].float() # [1, T, hidden]
78
+ conf_full = head.score(last_hidden) # [1, T, vocab] in (0, 1)
79
+
80
+ # Per-token confidence for the actual next token at each position (shifted by 1)
81
+ target_ids = input_ids[:, 1:]
82
+ conf = conf_full[:, :-1, :].gather(-1, target_ids.unsqueeze(-1)).squeeze(-1) # [1, T-1]
83
+
84
+ # Confidence over just the assistant span (hypothesis + closing chat tokens):
85
+ hyp_conf = conf[0, prompt_len - 1:]
86
+ hyp_tokens = tokenizer.convert_ids_to_tokens(input_ids[0, prompt_len:].tolist())
87
+
88
+ print("Hypothesis:", hypothesis)
89
+ for tok, s in zip(hyp_tokens, hyp_conf.tolist()):
90
+ print(f" {tok!r:>20s} conf={s:.4f}")
91
+ print(f"Sentence-level (mean): {hyp_conf.mean().item():.4f}")
92
+
93
+ # Expected output:
94
+ # Hypothesis: Die Katze saß auf der Matte.
95
+ # '▁Die' conf=0.9999
96
+ # '▁Kat' conf=0.9995
97
+ # 'ze' conf=0.9992
98
+ # '▁sa' conf=0.9993
99
+ # 'ß' conf=1.0000
100
+ # '▁auf' conf=1.0000
101
+ # '▁der' conf=0.9983
102
+ # '▁Mat' conf=0.9992
103
+ # 'te' conf=0.9999
104
+ # '.' conf=0.9897
105
+ # '<|im_end|>' conf=1.0000
106
+ # '▁' conf=1.0000
107
+ # '<0x0A>' conf=1.0000
108
+ # Sentence-level (mean): 0.9988
109
+ ```
110
+
111
+ ### 2. Generate and score
112
+
113
+ The sigmoid head only needs the last-layer hidden states, which `transformers.generate`
114
+ already returns when you ask for them. So you can **generate** with the base LM and
115
+ **score** with the sigmoid head in **one forward pass** — no re-decoding.
116
+
117
+ ```python
118
+ import torch
119
+ from transformers import AutoModel, AutoModelForCausalLM, AutoTokenizer
120
+
121
+ BASE = "Unbabel/TowerInstruct-7B-v0.2"
122
+ HEAD = "tuanh23/SigmoidHead-TowerInstruct-7B-v0.2"
123
+ device = "cuda" if torch.cuda.is_available() else "cpu"
124
+
125
+ tokenizer = AutoTokenizer.from_pretrained(BASE)
126
+ base_model = AutoModelForCausalLM.from_pretrained(BASE, torch_dtype=torch.bfloat16).to(device).eval()
127
+ head = AutoModel.from_pretrained(HEAD, trust_remote_code=True).to(device).eval()
128
+
129
+ src_lang, tgt_lang = "English", "German"
130
+ src = "The cat sat on the mat."
131
+ messages = [{"role": "user", "content": f"Translate the following text from {src_lang} into {tgt_lang}.\n{src_lang}: {src}.\n{tgt_lang}: "}]
132
+ input_ids = tokenizer.apply_chat_template(
133
+ messages, tokenize=True, add_generation_prompt=True, return_tensors="pt"
134
+ ).to(device)
135
+
136
+ with torch.no_grad():
137
+ gen = base_model.generate(
138
+ input_ids=input_ids,
139
+ max_new_tokens=64,
140
+ do_sample=False, # greedy
141
+ output_hidden_states=True,
142
+ return_dict_in_generate=True,
143
+ )
144
+ # Stitch together per-step last-layer hidden states into [B, gen_len, hidden].
145
+ # Step 0 returns hidden states for the whole prompt — keep only the last position.
146
+ last_hidden = [step[-1] for step in gen.hidden_states]
147
+ last_hidden[0] = last_hidden[0][:, -1:, :]
148
+ last_hidden = torch.cat(last_hidden, dim=1).float() # [B, gen_len, hidden]
149
+
150
+ gen_ids = gen.sequences[:, input_ids.shape[1]:] # [B, gen_len]
151
+ conf_full = head.score(last_hidden) # [B, gen_len, vocab] in (0, 1)
152
+ conf = conf_full.gather(-1, gen_ids.unsqueeze(-1)).squeeze(-1) # [B, gen_len]
153
+
154
+ translation = tokenizer.decode(gen_ids[0], skip_special_tokens=True)
155
+ print("Translation:", translation)
156
+ for tok, s in zip(tokenizer.convert_ids_to_tokens(gen_ids[0].tolist()), conf[0].tolist()):
157
+ print(f" {tok!r:>20s} conf={s:.4f}")
158
+ print(f"Sentence-level (mean): {conf[0].mean().item():.4f}")
159
+
160
+ # Expected output:
161
+ # Translation: Die Katze saß auf der Matte.
162
+ # '▁Die' conf=0.9999
163
+ # '▁Kat' conf=0.9994
164
+ # 'ze' conf=0.9991
165
+ # '▁sa' conf=0.9993
166
+ # 'ß' conf=1.0000
167
+ # '▁auf' conf=1.0000
168
+ # '▁der' conf=0.9983
169
+ # '▁Mat' conf=0.9992
170
+ # 'te' conf=0.9999
171
+ # '.' conf=0.9900
172
+ # '<|im_end|>' conf=1.0000
173
+ # Sentence-level (mean): 0.9986
174
+ ```
175
+
176
+ ## Why sigmoid?
177
+
178
+ A standard softmax head forces the probability mass to sum to 1 across the vocab,
179
+ so when several outputs are equally valid, the mass is split and valid tokens might look low-confidence. The sigmoid head decouples tokens, so all valid
180
+ options can score high simultaneously — a better proxy for *quality*.
181
+
182
+ ## Citation
183
+
184
+ ```
185
+ @article{dinh2026sigmoid,
186
+ title = {Sigmoid Head for Quality Estimation under Language Ambiguity},
187
+ author = {Dinh, Tu Anh and Niehues, Jan},
188
+ journal = {arXiv preprint arXiv:2601.00680},
189
+ year = {2026}
190
+ }
191
+ ```
192
+
193
+ Accepted to ACL 2026 (Main); proceedings not yet released.
194
+
195
+ ## Code
196
+
197
+ Training and evaluation code: <https://github.com/tuanh23/sigmoid-head-qe>.
config.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "sigmoid_head",
3
+ "vocab_size": 32007,
4
+ "hidden_size": 4096,
5
+ "auto_map": {
6
+ "AutoConfig": "sigmoid_head.SigmoidHeadConfig",
7
+ "AutoModel": "sigmoid_head.SigmoidHead"
8
+ }
9
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:91f536bd78034ff6130444ed7a8bb0529e0d01f7f19755fc3eb76a424ae150cf
3
+ size 524402808
sigmoid_head.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Sigmoid head for token-level Quality Estimation.
2
+
3
+ A new unembedding head that sits on top of the last hidden states of a frozen
4
+ base LM and produces a per-token confidence score via sigmoid (not softmax).
5
+ Multiple equally-valid tokens can simultaneously have high scores under
6
+ language ambiguity.
7
+
8
+ Paper: "Sigmoid Head for Quality Estimation under Language Ambiguity"
9
+ """
10
+
11
+ import torch
12
+ from transformers import PreTrainedModel, PretrainedConfig
13
+
14
+
15
+ class SigmoidHeadConfig(PretrainedConfig):
16
+ model_type = "sigmoid_head"
17
+
18
+ def __init__(self, vocab_size: int = 32007, hidden_size: int = 4096, **kwargs):
19
+ super().__init__(**kwargs)
20
+ self.vocab_size = vocab_size
21
+ self.hidden_size = hidden_size
22
+
23
+
24
+ class SigmoidHead(PreTrainedModel):
25
+ config_class = SigmoidHeadConfig
26
+
27
+ def __init__(self, config: SigmoidHeadConfig):
28
+ super().__init__(config)
29
+ self.weight = torch.nn.Parameter(
30
+ torch.empty(config.vocab_size, config.hidden_size)
31
+ )
32
+ self.post_init()
33
+
34
+ @torch.no_grad()
35
+ def score(self, last_hidden_states: torch.Tensor) -> torch.Tensor:
36
+ """Per-token confidence in (0, 1).
37
+
38
+ Args:
39
+ last_hidden_states: [batch, seq_len, hidden_size]
40
+
41
+ Returns:
42
+ confidence_scores: [batch, seq_len, vocab_size]
43
+ """
44
+ return torch.sigmoid(torch.matmul(last_hidden_states, self.weight.T))