skboy commited on
Commit
cc9641d
·
verified ·
1 Parent(s): 197d8ec

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +39 -3
  2. et_predictor2_seed123.safetensors +3 -0
  3. model.py +245 -0
README.md CHANGED
@@ -1,3 +1,39 @@
1
- ---
2
- license: cc-by-nc-sa-4.0
3
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # RoBERTa-based Eye-Tracking (ET) Feature Generator
2
+
3
+ ## Overview
4
+ This repository contains the weights and architecture for a custom regression model based on `roberta-base`. It is designed to predict 5 distinct eye-tracking (ET) features directly from text inputs. This model was trained to serve as the ET generator component required to replicate and extend the GazeReward framework.
5
+
6
+ ## Reference
7
+ This model replicates the ET generator mentioned in the following work:
8
+ > "Through ablation studies we test our framework with different integration methods, LLMs, and ET generator models..."
9
+ > (Lopez-Cardona et al., "SEEING EYE TO AI: HUMAN ALIGNMENT VIA GAZE-BASED RESPONSE REWARDS FOR LARGE LANGUAGE MODELS")
10
+
11
+ ## Model Architecture
12
+ - **Base Model:** `roberta-base`
13
+ - **Custom Head:** A linear layer that outputs 5 continuous ET features.
14
+ - **Implementation:** The exact architecture is defined in the accompanying `model.py` file.
15
+
16
+ ## Training Data
17
+ The model was fine-tuned using eye-tracking data from:
18
+ - ZuCo 2.0 Dataset (CC BY-NC 4.0)
19
+ - Provo Corpus
20
+
21
+ ## How to Use
22
+ To load this model, make sure you download both the weights (`.safetensors` or `.pt`) and the custom architecture script (`model.py`) into your environment.
23
+
24
+ ```python
25
+ # File: load_model.py
26
+ # Loads the custom ET generator model and its weights from the Hugging Face Hub.
27
+
28
+ import torch
29
+ from huggingface_hub import hf_hub_download
30
+ from model import RobertaRegressionModel
31
+
32
+ def load_et_generator(repo_id="your-username/your-model-name"):
33
+ weights_path = hf_hub_download(repo_id=repo_id, filename="model.safetensors")
34
+
35
+ model = RobertaRegressionModel()
36
+ model.load_state_dict(torch.load(weights_path))
37
+ model.eval()
38
+
39
+ return model
et_predictor2_seed123.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1a70c01f6a37e897fec8cf0d39ccba8a50ad144f076545cc4f0d8b7d67bf2b40
3
+ size 498621996
model.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import transformers
3
+ import numpy as np
4
+
5
+
6
+ FEATURE_NAMES = ['nFix', 'FFD', 'GPT', 'TRT', 'fixProp']
7
+ WINDOW_SIZE = 512
8
+ OVERLAP = 50
9
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
10
+ try:
11
+ from safetensors.torch import load_file as st_load_file
12
+ HAS_SAFETENSORS = True
13
+ except ImportError:
14
+ HAS_SAFETENSORS = False
15
+
16
+ class RobertaRegressionModel(torch.nn.Module):
17
+ def __init__(self, model_name='roberta-base'):
18
+ super().__init__()
19
+ self.roberta = transformers.RobertaModel.from_pretrained(model_name)
20
+ embed_size = 1024 if 'large' in model_name else 768
21
+ self.decoder = torch.nn.Linear(embed_size, 5)
22
+
23
+ def forward(self, input_ids, attention_mask, predict_mask):
24
+ hidden = self.roberta(input_ids, attention_mask=attention_mask).last_hidden_state
25
+ Y_pred = self.decoder(hidden)
26
+ mask = (predict_mask == 0).unsqueeze(-1).expand_as(Y_pred).to(Y_pred.device)
27
+ Y_pred = Y_pred.masked_fill(mask, -1.0)
28
+ return Y_pred
29
+
30
+ class FixationsPredictor2:
31
+
32
+ def __init__(self, checkpoint_path, model_name='roberta-base'):
33
+ self.model_name = model_name
34
+ self.tokenizer = transformers.RobertaTokenizer.from_pretrained(
35
+ model_name, add_prefix_space=True
36
+ )
37
+ self.model = RobertaRegressionModel(model_name).to(device)
38
+ self._load_checkpoint(checkpoint_path)
39
+ self.model.eval()
40
+
41
+ def _load_checkpoint(self, path):
42
+ import os
43
+ if path.endswith('.safetensors'):
44
+ if not HAS_SAFETENSORS:
45
+ raise ImportError('pip install safetensors')
46
+ self.model.load_state_dict(st_load_file(path, device=str(device)))
47
+ elif path.endswith('.pt') or path.endswith('.bin'):
48
+ self.model.load_state_dict(torch.load(path, map_location=device))
49
+ else:
50
+ for ext in ['.safetensors', '.pt']:
51
+ if os.path.exists(path + ext):
52
+ self._load_checkpoint(path + ext)
53
+ return
54
+ raise FileNotFoundError(f'체크포인트 없음: {path}')
55
+
56
+ def _predict_with_sliding_window(self, input_ids_full, attention_mask_full):
57
+ seq_len = input_ids_full.shape[1]
58
+
59
+ if seq_len <= WINDOW_SIZE:
60
+ predict_mask = attention_mask_full.clone()
61
+ with torch.no_grad():
62
+ pred = self.model(input_ids_full, attention_mask_full, predict_mask)
63
+ return pred.squeeze(0).cpu().numpy()
64
+
65
+ predictions = np.zeros((seq_len, 5), dtype=np.float32)
66
+ weights = np.zeros(seq_len, dtype=np.float32)
67
+ stride = WINDOW_SIZE - OVERLAP
68
+
69
+ start = 0
70
+ while start < seq_len:
71
+ end = min(start + WINDOW_SIZE, seq_len)
72
+ ids_win = input_ids_full[:, start:end]
73
+ mask_win = attention_mask_full[:, start:end]
74
+ predict_mask = mask_win.clone()
75
+
76
+ with torch.no_grad():
77
+ pred_win = self.model(ids_win, mask_win, predict_mask)
78
+ pred_np = pred_win.squeeze(0).cpu().numpy()
79
+
80
+ win_len = end - start
81
+ linear_w = np.ones(win_len, dtype=np.float32)
82
+ if start > 0:
83
+ ramp_len = min(OVERLAP, win_len)
84
+ linear_w[:ramp_len] = np.linspace(0, 1, ramp_len)
85
+ if end < seq_len:
86
+ ramp_len = min(OVERLAP, win_len)
87
+ linear_w[-ramp_len:] = np.linspace(1, 0, ramp_len)
88
+
89
+ for feat_i in range(5):
90
+ predictions[start:end, feat_i] += pred_np[:, feat_i] * linear_w
91
+ weights[start:end] += linear_w
92
+
93
+ if end == seq_len:
94
+ break
95
+ start += stride
96
+
97
+ nonzero = weights > 0
98
+ predictions[nonzero] /= weights[nonzero, None]
99
+ return predictions
100
+
101
+ def _get_word_boundaries(self, input_ids):
102
+ tokens = [self.tokenizer.convert_ids_to_tokens(i) for i in input_ids]
103
+ words = []
104
+ current_word_tokens = []
105
+ current_indices = []
106
+
107
+ for i, tok in enumerate(tokens):
108
+ if tok in ('<s>', '</s>', '<pad>'):
109
+ if current_word_tokens:
110
+ words.append(current_indices)
111
+ current_word_tokens = []
112
+ current_indices = []
113
+ continue
114
+
115
+ if tok.startswith('Ġ') or not current_word_tokens:
116
+ if current_word_tokens:
117
+ words.append(current_indices)
118
+ current_word_tokens = [tok]
119
+ current_indices = [i]
120
+ else:
121
+ current_word_tokens.append(tok)
122
+ current_indices.append(i)
123
+
124
+ if current_word_tokens:
125
+ words.append(current_indices)
126
+
127
+ return words
128
+
129
+ def predict_raw_text(self, text):
130
+ words_list = text.strip().split()
131
+ encoding = self.tokenizer(
132
+ [words_list],
133
+ is_split_into_words=True,
134
+ return_tensors='pt',
135
+ truncation=False,
136
+ padding=False,
137
+ )
138
+ input_ids = encoding['input_ids'].to(device)
139
+ attention_mask = encoding['attention_mask'].to(device)
140
+
141
+ token_preds = self._predict_with_sliding_window(input_ids, attention_mask)
142
+
143
+ word_boundaries = self._get_word_boundaries(input_ids.squeeze(0).cpu().tolist())
144
+
145
+ word_features = np.zeros((len(word_boundaries), 5), dtype=np.float32)
146
+ for w_idx, token_indices in enumerate(word_boundaries):
147
+ first_tok = token_indices[0]
148
+ pred = token_preds[first_tok]
149
+ pred = np.clip(pred, 0, None)
150
+ word_features[w_idx] = pred
151
+
152
+ return word_features, words_list
153
+
154
+ def predict_and_remap_to_tokenizer(self, input_ids_rm, attention_mask_rm, rm_tokenizer):
155
+ batch_size = input_ids_rm.shape[0]
156
+ seq_len_rm = input_ids_rm.shape[1]
157
+
158
+ fixations_batch = []
159
+ masks_batch = []
160
+
161
+ for b in range(batch_size):
162
+ ids = input_ids_rm[b].cpu().tolist()
163
+ mask = attention_mask_rm[b].cpu().tolist()
164
+
165
+ pad_id = rm_tokenizer.pad_token_id
166
+ ids_no_pad = [i for i, m in zip(ids, mask) if m == 1 and i != pad_id]
167
+ text = rm_tokenizer.decode(ids_no_pad, skip_special_tokens=True)
168
+
169
+ word_features, _ = self.predict_raw_text(text)
170
+
171
+ remapped = self._remap_features_to_rm_tokens(
172
+ word_features, text, ids, mask, rm_tokenizer
173
+ )
174
+
175
+ fixations_batch.append(remapped)
176
+ masks_batch.append(torch.tensor(mask, dtype=torch.long))
177
+
178
+ fixations = torch.stack(fixations_batch)
179
+ fixations_attention_mask = torch.stack(masks_batch)
180
+
181
+ return fixations, fixations_attention_mask
182
+
183
+ def _compute_mapped_fixations(self, input_ids_rm, attention_mask_rm=None):
184
+ # gaze_reward reward_model_base.py의 fixations_model_version=2 호환 인터페이스
185
+ if attention_mask_rm is None:
186
+ attention_mask_rm = torch.ones_like(input_ids_rm)
187
+ ids = input_ids_rm[0].cpu().tolist()
188
+ mask = attention_mask_rm[0].cpu().tolist()
189
+ pad_id = self.tokenizer.pad_token_id if self.tokenizer.pad_token_id else 1
190
+ ids_no_pad = [i for i, m in zip(ids, mask) if m == 1 and i != pad_id]
191
+ text = self.tokenizer.decode(ids_no_pad, skip_special_tokens=True)
192
+ word_features, _ = self.predict_raw_text(text)
193
+ remapped = self._remap_features_to_rm_tokens(
194
+ word_features, text, ids, mask, self.tokenizer
195
+ )
196
+ fixations = remapped.unsqueeze(0)
197
+ fix_attn = torch.tensor(mask, dtype=torch.long).unsqueeze(0)
198
+ return fixations, fix_attn, None, None, None, None
199
+
200
+ def _remap_features_to_rm_tokens(self, word_features, text, rm_input_ids, rm_mask, rm_tokenizer):
201
+ words = text.strip().split()
202
+ seq_len = len(rm_input_ids)
203
+ output = torch.zeros(seq_len, 5, dtype=torch.float32)
204
+
205
+ rm_tokens = rm_tokenizer.convert_ids_to_tokens(rm_input_ids)
206
+
207
+ word_to_rm_indices = _align_words_to_rm_tokens(words, rm_tokens, rm_tokenizer)
208
+
209
+ n_words = min(len(words), len(word_features))
210
+ for w_idx in range(n_words):
211
+ if w_idx >= len(word_to_rm_indices):
212
+ break
213
+ indices = word_to_rm_indices[w_idx]
214
+ if not indices:
215
+ continue
216
+ feat = torch.tensor(word_features[w_idx], dtype=torch.float32)
217
+ if indices[0] < seq_len and rm_mask[indices[0]] == 1:
218
+ output[indices[0]] = feat
219
+ return output
220
+
221
+
222
+ def _align_words_to_rm_tokens(words, rm_tokens, rm_tokenizer):
223
+ special_ids = set(rm_tokenizer.all_special_ids)
224
+ word_to_indices = []
225
+ tok_idx = 0
226
+
227
+ for word in words:
228
+ indices = []
229
+ chars_remaining = len(word)
230
+
231
+ while tok_idx < len(rm_tokens) and chars_remaining > 0:
232
+ tok = rm_tokens[tok_idx]
233
+ tok_id = rm_tokenizer.convert_tokens_to_ids(tok)
234
+ if tok_id in special_ids:
235
+ tok_idx += 1
236
+ continue
237
+
238
+ tok_clean = tok.lstrip('Ġ▁ ')
239
+ indices.append(tok_idx)
240
+ chars_remaining -= len(tok_clean)
241
+ tok_idx += 1
242
+
243
+ word_to_indices.append(indices)
244
+
245
+ return word_to_indices