Skip to content

Commit 10ef558

Browse files
authored
Merge pull request #702 from BY-Elysia/main
Fix generated evaluation consistency and preserve locality evidence
2 parents 5bb37e7 + e7f7ec0 commit 10ef558

5 files changed

Lines changed: 147 additions & 65 deletions

File tree

easyeditor/editors/concept_editor.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,6 @@ def edit(self,
237237
f"locality.{locality_key}",
238238
build_locality_metric_meta(locality_key, self.hparams, self.model_name),
239239
)
240-
all_metrics[i]['post']['locality'].pop(f'{locality_key}_output')
241-
all_metrics[i]['pre'].pop('locality')
242240

243241
LOG.info(f"Evaluation took {time() - start}")
244242

easyeditor/editors/editor.py

Lines changed: 56 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,49 @@ def seed_everything(seed):
5151
random.seed(seed)
5252

5353
seed_everything(42)
54+
55+
56+
def finalize_locality_metrics(metric, request, hparams, model_name):
57+
if not request.get("locality"):
58+
return
59+
60+
pre_locality = metric["pre"]["locality"]
61+
post_locality = metric["post"]["locality"]
62+
evaluation_type = getattr(hparams, "evaluation_type", None)
63+
uses_generated_text = evaluation_type in ["LLM-judge", "generate-text"]
64+
65+
for locality_key in request["locality"]:
66+
if uses_generated_text:
67+
output_key = f"{locality_key}_gen_content"
68+
else:
69+
output_key = f"{locality_key}_output"
70+
71+
pre_outputs = pre_locality[output_key]
72+
post_outputs = post_locality[output_key]
73+
if len(pre_outputs) != len(post_outputs):
74+
raise ValueError(
75+
f"Locality output count mismatch for `{locality_key}`: "
76+
f"{len(pre_outputs)} pre-edit outputs and "
77+
f"{len(post_outputs)} post-edit outputs."
78+
)
79+
80+
if uses_generated_text:
81+
locality_result = [
82+
float(pre_output == post_output)
83+
for pre_output, post_output in zip(pre_outputs, post_outputs)
84+
]
85+
else:
86+
locality_result = [
87+
float(np.mean(np.equal(pre_output, post_output)))
88+
for pre_output, post_output in zip(pre_outputs, post_outputs)
89+
]
90+
91+
post_locality[f"{locality_key}_acc"] = locality_result
92+
attach_metric_meta(
93+
metric["post"],
94+
f"locality.{locality_key}",
95+
build_locality_metric_meta(locality_key, hparams, model_name),
96+
)
5497

5598
class BaseEditor:
5699
"""Base editor for all methods"""
@@ -252,22 +295,12 @@ def batch_edit(self,
252295
restore_after_edit(self, edited_model, weights_copy)
253296

254297
for i, request in enumerate(record_chunks):
255-
if 'locality' in chunk_metrics[i]['post'].keys():
256-
for locality_key in request['locality'].keys():
257-
locality_result = []
258-
if hasattr(self.hparams, 'evaluation_type') and self.hparams.evaluation_type == "LLM-judge":
259-
locality_result.append(float(chunk_metrics[i]['post']['locality'][f'{locality_key}_output']==chunk_metrics[i]['pre']['locality'][f'{locality_key}_output']))
260-
else:
261-
for ans, label in zip(chunk_metrics[i]['post']['locality'][f'{locality_key}_output'], chunk_metrics[i]['pre']['locality'][f'{locality_key}_output']):
262-
locality_result.append(np.mean(np.equal(ans, label)))
263-
chunk_metrics[i]['post']['locality'][f'{locality_key}_acc'] = locality_result
264-
attach_metric_meta(
265-
chunk_metrics[i]['post'],
266-
f"locality.{locality_key}",
267-
build_locality_metric_meta(locality_key, self.hparams, self.model_name),
268-
)
269-
chunk_metrics[i]['post']['locality'].pop(f'{locality_key}_output')
270-
chunk_metrics[i]['pre'].pop('locality')
298+
finalize_locality_metrics(
299+
chunk_metrics[i],
300+
request,
301+
self.hparams,
302+
self.model_name,
303+
)
271304

272305
if verbose:
273306
LOG.info(
@@ -356,22 +389,12 @@ def edit_evaluation(all_metrics, request, edited_model, idx, test_generation, ic
356389
})
357390
if "metric_kwargs" in kwargs:
358391
all_metrics[idx].update(compute_sent_metric(self.model, edited_model, self.model_name, self.hparams, self.tok,metric_kwargs=kwargs["metric_kwargs"][idx], device=self.hparams.device))
359-
if 'locality' in all_metrics[idx]['post'].keys() and not hasattr(self.hparams, 'evaluation_type'):
360-
for locality_key in request['locality'].keys():
361-
locality_result = []
362-
if hasattr(self.hparams, 'evaluation_type'):
363-
locality_result.append(float(all_metrics[idx]['post']['locality'][f'{locality_key}_output']==all_metrics[idx]['pre']['locality'][f'{locality_key}_output']))
364-
else:
365-
for ans, label in zip(all_metrics[idx]['post']['locality'][f'{locality_key}_output'], all_metrics[idx]['pre']['locality'][f'{locality_key}_output']):
366-
locality_result.append(np.mean(np.equal(ans, label)))
367-
all_metrics[idx]['post']['locality'][f'{locality_key}_acc'] = locality_result
368-
attach_metric_meta(
369-
all_metrics[idx]['post'],
370-
f"locality.{locality_key}",
371-
build_locality_metric_meta(locality_key, self.hparams, self.model_name),
372-
)
373-
all_metrics[idx]['post']['locality'].pop(f'{locality_key}_output')
374-
all_metrics[idx]['pre'].pop('locality')
392+
finalize_locality_metrics(
393+
all_metrics[idx],
394+
request,
395+
self.hparams,
396+
self.model_name,
397+
)
375398

376399
if verbose:
377400
LOG.info(f"{idx} editing: {request['prompt']} -> {request['target_new']} \n\n {all_metrics[idx]}")
@@ -395,8 +418,7 @@ def edit_evaluation(all_metrics, request, edited_model, idx, test_generation, ic
395418

396419
if isinstance(edited_model, LORA):
397420
edited_model = edited_model.model
398-
if not hasattr(self.hparams, 'evaluation_type') or self.hparams.evaluation_type != "generate-text":
399-
summary_metrics(all_metrics)
421+
summary_metrics(all_metrics)
400422

401423
return all_metrics, edited_model, weights_copy
402424

easyeditor/editors/multimodal_editor.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,42 @@ def make_logs():
5353
LOG.addHandler(s_h)
5454

5555

56+
def get_aligned_topk_token_ids(pre_logits, post_logits, k):
57+
pre_logits = torch.as_tensor(pre_logits)
58+
post_logits = torch.as_tensor(post_logits)
59+
if pre_logits.dim() != 3 or post_logits.dim() != 3:
60+
raise ValueError(
61+
"pre_logits and post_logits must be rank-3 tensors shaped "
62+
"[batch, sequence, vocab]."
63+
)
64+
if pre_logits.shape[0] != post_logits.shape[0]:
65+
raise ValueError(
66+
"pre_logits and post_logits must have the same batch size: "
67+
f"{pre_logits.shape[0]} != {post_logits.shape[0]}"
68+
)
69+
70+
if post_logits.shape[1] > pre_logits.shape[1]:
71+
post_logits = post_logits[:, -pre_logits.shape[1]:, :]
72+
else:
73+
pre_logits = pre_logits[:, -post_logits.shape[1]:, :]
74+
75+
k = min(k, pre_logits.shape[-1], post_logits.shape[-1])
76+
pre_topk = torch.topk(pre_logits, k=k, dim=-1).indices.cpu().tolist()
77+
post_topk = torch.topk(post_logits, k=k, dim=-1).indices.cpu().tolist()
78+
return pre_topk, post_topk
79+
80+
5681
def attach_topk_locality_metrics(metrics):
5782
if 'locality_output' in metrics['post'].keys():
5883
assert len(metrics['post']['locality_output']) == \
5984
len(metrics['pre']['locality_output'])
85+
pre_topk, post_topk = get_aligned_topk_token_ids(
86+
metrics['pre']['locality_output'],
87+
metrics['post']['locality_output'],
88+
k=1,
89+
)
90+
metrics['pre']['locality_topk_tokens'] = pre_topk
91+
metrics['post']['locality_topk_tokens'] = post_topk
6092
metrics['post']['locality_acc'] = topk_token_match(
6193
metrics['pre']['locality_output'],
6294
metrics['post']['locality_output'],
@@ -78,6 +110,13 @@ def attach_topk_locality_metrics(metrics):
78110
if 'multimodal_locality_output' in metrics['post'].keys():
79111
assert len(metrics['post']['multimodal_locality_output']) == \
80112
len(metrics['pre']['multimodal_locality_output'])
113+
pre_topk, post_topk = get_aligned_topk_token_ids(
114+
metrics['pre']['multimodal_locality_output'],
115+
metrics['post']['multimodal_locality_output'],
116+
k=10,
117+
)
118+
metrics['pre']['multimodal_locality_topk_tokens'] = pre_topk
119+
metrics['post']['multimodal_locality_topk_tokens'] = post_topk
81120
metrics['post']['multimodal_locality_acc'] = topk_token_match(
82121
metrics['pre']['multimodal_locality_output'],
83122
metrics['post']['multimodal_locality_output'],

easyeditor/evaluate/evaluate.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from ..util import HyperParams
1717
from ..util.device import normalize_device
1818
from .evaluate_utils import (
19+
generate_texts,
1920
test_seq2seq_batch_prediction_acc,
2021
test_batch_prediction_acc,
2122
test_prediction_acc,
@@ -126,7 +127,7 @@ def compute_rewrite_or_rephrase_quality(
126127
f"{key}_gen_content": gen_content
127128
}
128129
elif hasattr(hparams, 'evaluation_type') and hparams.evaluation_type == "generate-text":
129-
gen_content_model = test_prediction_acc_LLM_judge(model, tok, hparams, prompt, target_new, device, locality=False)
130+
gen_content_model = generate_texts(model, tok, hparams, prompt, device)
130131
ret = {
131132
f"{key}_gen_content": gen_content_model
132133
}
@@ -176,8 +177,13 @@ def compute_locality_quality(
176177
) -> typing.Dict:
177178

178179
# using real-world evaluation: autoregressive decoding, natural stop criteria, LLM-as-a-Judge
179-
if hasattr(hparams, 'evaluation_type'):
180-
loc_tokens = test_prediction_acc_LLM_judge(model, tok, hparams, prompt, locality_ground_truth, device, locality=True)
180+
evaluation_type = getattr(hparams, "evaluation_type", None)
181+
if evaluation_type in ["LLM-judge", "generate-text"]:
182+
return {
183+
f"{locality_key}_gen_content": generate_texts(
184+
model, tok, hparams, prompt, device
185+
)
186+
}
181187
else: # traditional evaluation
182188
if 't5' in model_name.lower():
183189
loc_tokens = test_seq2seq_batch_prediction_acc(model, tok, hparams, prompt, locality_ground_truth, device, locality=True)
@@ -202,10 +208,21 @@ def compute_portability_quality(
202208
device,
203209
) -> typing.Dict:
204210
# using real-world evaluation: autoregressive decoding, natural stop criteria, LLM-as-a-Judge
205-
if hasattr(hparams, 'evaluation_type') and hparams.evaluation_type == "LLM-judge":
206-
portability_correct = test_prediction_acc_LLM_judge(model, tok, hparams, prompt, ground_truth, device, locality=False)
207-
elif hasattr(hparams, 'evaluation_type') and hparams.evaluation_type == "generate-text":
208-
portability_correct = test_prediction_acc_LLM_judge(model, tok, hparams, prompt, ground_truth, device, locality=False)
211+
evaluation_type = getattr(hparams, "evaluation_type", None)
212+
if evaluation_type == "LLM-judge":
213+
portability_correct, gen_content = test_prediction_acc_LLM_judge(
214+
model, tok, hparams, prompt, ground_truth, device, locality=False
215+
)
216+
return {
217+
f"{portability_key}_acc": portability_correct,
218+
f"{portability_key}_gen_content": gen_content,
219+
}
220+
elif evaluation_type == "generate-text":
221+
return {
222+
f"{portability_key}_gen_content": generate_texts(
223+
model, tok, hparams, prompt, device
224+
)
225+
}
209226
else: # traditional evaluation
210227
if 't5' in model_name.lower():
211228
portability_correct = test_seq2seq_batch_prediction_acc(model, tok, hparams, prompt, ground_truth, device)

easyeditor/evaluate/evaluate_utils.py

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,11 @@ def llm_judge(question, ground_truth, prediction, api_key):
101101
time.sleep(1) # avoid high rate of request
102102
return llm_score
103103

104-
def test_prediction_acc_LLM_judge(model, tok, hparams, prompts, targets, device, locality=False):
105-
# generation & truncation
106-
all_score = []
107-
all_response = []
104+
def generate_texts(model, tok, hparams, prompts, device):
108105
if isinstance(prompts, str):
109-
prompts, targets = [prompts, ], [targets, ]
106+
prompts = [prompts]
107+
108+
all_response = []
110109
for prompt in prompts:
111110
messages = [
112111
{"role": "system", "content": "You are a helpful assistant."},
@@ -146,25 +145,32 @@ def test_prediction_acc_LLM_judge(model, tok, hparams, prompts, targets, device,
146145
gen_content = tok.decode(trunc_gen_tokens)
147146
suffixes_to_remove = [".", "\n", tok.eos_token]
148147
for suffix in suffixes_to_remove:
149-
if gen_content.endswith(suffix):
148+
if suffix and gen_content.endswith(suffix):
150149
gen_content = gen_content[:-len(suffix)]
151-
# LLM-as-a-Judge
152-
if hparams.evaluation_type == "generate-text":
153-
all_response.append(gen_content)
154-
elif hparams.evaluation_type == "LLM-judge" and hasattr(hparams, 'api_key') and hparams.api_key:
155-
LLM_Score = llm_judge(prompts, targets, gen_content, hparams.api_key)
156-
all_score.append(LLM_Score)
157-
all_response.append(gen_content)
150+
151+
all_response.append(gen_content)
152+
153+
return all_response
154+
155+
156+
def test_prediction_acc_LLM_judge(model, tok, hparams, prompts, targets, device, locality=False):
157+
if isinstance(prompts, str):
158+
prompts = [prompts]
159+
if isinstance(targets, str):
160+
targets = [targets]
161+
if len(prompts) != len(targets):
162+
raise ValueError("The number of prompts and targets must match.")
163+
164+
all_response = generate_texts(model, tok, hparams, prompts, device)
165+
all_score = []
166+
for prompt, target, gen_content in zip(prompts, targets, all_response):
167+
if hasattr(hparams, 'api_key') and hparams.api_key:
168+
score = llm_judge(prompt, target, gen_content, hparams.api_key)
158169
else:
159-
# the user do not provide api key, using exact match as an alternative
160-
EM_Score = float(exact_match_score(gen_content, targets))
161-
all_score.append(EM_Score)
162-
all_response.append(gen_content)
163-
164-
if len(all_score) > 0:
165-
return all_score, all_response
166-
else:
167-
return all_response
170+
score = float(exact_match_score(gen_content, target))
171+
all_score.append(score)
172+
173+
return all_score, all_response
168174

169175
def test_batch_prediction_acc(model, tok, hparams, prompts, target, device, locality=False):
170176
prompt_tok = tok(

0 commit comments

Comments
 (0)