Initial upload: sigmoid QE head for TowerInstruct-7B-v0.2
Browse files- README.md +197 -0
- config.json +9 -0
- model.safetensors +3 -0
- 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))
|