-
Notifications
You must be signed in to change notification settings - Fork 18
Expand file tree
/
Copy pathprocess_data.py
More file actions
4834 lines (3928 loc) · 213 KB
/
Copy pathprocess_data.py
File metadata and controls
4834 lines (3928 loc) · 213 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Standard library imports
import argparse
import json
import multiprocessing as mp
import os
import random
import re
import sys
import time
from collections import defaultdict
from datetime import datetime, timedelta
from functools import partial
from typing import Dict, List, Any, Tuple
# Third-party imports
import dotenv
import nltk
import numpy as np
import pandas as pd
import tiktoken
from datasets import load_dataset
from openai import AzureOpenAI
from tqdm import tqdm
# Load environment variables
dotenv.load_dotenv()
# Load the Qwen tokenizer for accurate token counting
from transformers import AutoTokenizer
qwen_tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-32B", trust_remote_code=True)
def count_tokens_qwen(text):
"""Count tokens using Qwen3-32B tokenizer"""
return len(qwen_tokenizer.encode(text))
# # Azure OpenAI configuration
# api_key = os.getenv("AZURE_OPENAI_API_KEY")
# client = AzureOpenAI(
# api_key=api_key,
# api_version="2025-01-01-preview",
# azure_endpoint="https://jplml-resource.cognitiveservices.azure.com"
# )
qwen32b_server_url = os.getenv("QWEN_URL")
# User message templates for conversational format
USER_MESSAGE_TEMPLATES = [
"Here are some new facts I learnt just now:",
"Here are some news I learnt earlier:",
"The following are some more news I learnt:",
"The following are some new knowledge:",
"I just discovered some interesting information:",
"Let me share some additional facts I found:",
"Here's some more information I came across:",
"I want to tell you about some new discoveries:",
"There are some important details I learned:",
"I have some fresh insights to share:",
"I'd like to tell you about some recent findings:",
"Let me update you with some new information:",
"I've gathered some additional data to share:",
"Here's some valuable information I collected:",
"I want to share some knowledge I just acquired:",
"Let me provide you with some new details:",
"I have some interesting updates for you:",
"Here are some facts I recently discovered:",
"I'd like to share some important information:",
"Let me tell you about some new developments:"
]
# User message templates for classification tasks
CLASSIFICATION_USER_TEMPLATES = [
"Here are some classification examples to learn from. Please pay attention to the labels:",
"I have some labeled classification examples for you to study:",
"The following are classification examples with their corresponding labels:",
"Please observe these classification examples and their associated labels:",
"Here are training examples for classification. Note the labels carefully:",
"I'm sharing some classification data with labels for you to learn:",
"These are labeled examples for classification tasks:",
"Please study these classification instances and their labels:",
"Here are some examples with classification labels to remember:",
"The following classification examples include important label information:",
"I want you to learn from these classified examples:",
"Here are categorized examples with their respective labels:",
"Please memorize these classification examples and their labels:",
"These labeled training examples are for classification:",
"I'm providing classification data with labels for your reference:",
"Study these classification examples and pay attention to the categories:",
"Here are some annotated classification examples:",
"Please learn from these labeled classification instances:",
"The following are classification training examples with labels:",
"These examples show different classes - please note the labels:"
]
# Assistant response templates
ASSISTANT_RESPONSE_TEMPLATES = [
"Sure I will remember them.",
"Got it. I will remember them.",
"Thank you for sharing. I've noted this information.",
"I understand. I'll keep this in mind.",
"Thanks for the update. I've recorded these facts.",
"Received. I'll store this information.",
"Noted. I'll remember these details.",
"I've processed this information and will remember it.",
"Thanks for letting me know. I'll keep track of this.",
"Perfect. I've stored this information in my memory.",
"Understood. I'll keep these facts for future reference.",
"Excellent. I've documented all of this information.",
"Thanks for the information. I've saved it.",
"Appreciated. I'll retain these important details.",
"Great! I've added this to my knowledge base.",
"I've successfully recorded all of this data.",
"Wonderful. I'll remember these key points.",
"Thanks for sharing. I've committed this to memory.",
"I've captured all of this valuable information."
]
ACCURACY_PROMPT = """
Your task is to label an answer to a question as 'CORRECT' or 'WRONG'. You will be given the following data:
(1) a question (posed by one user to another user),
(2) a 'gold' (ground truth) answer,
(3) a generated answer
which you will score as CORRECT/WRONG.
The input format is:
Question: {question}
Gold answer: {gold_answer}
Generated answer: {generated_answer}
First, provide a short (one sentence) explanation of your reasoning, then finish with CORRECT or WRONG.
Do NOT include both CORRECT and WRONG in your response, or it will break the evaluation script.
Just return the label CORRECT or WRONG in a json format with the key as "label".
"""
def judge_answer_with_token_logic(ground_truth_answer, predicted_answer, debug=False):
"""
Judge answer based on token count and string containment logic
Args:
ground_truth_answer: The expected correct answer
predicted_answer: The model's predicted answer
debug: If True, print debug information
Returns:
int: 0 if we should keep the example (judge as incorrect), 1 if we should remove it
"""
# Convert to strings and handle edge cases
ground_truth = str(ground_truth_answer).strip()
predicted = str(predicted_answer).strip()
if not ground_truth or not predicted:
if debug:
print(f"Empty answer detected - keeping example (GT: '{ground_truth}', Pred: '{predicted}')")
return 0 # Keep examples with empty answers for safety
# Count tokens in ground truth answer
ground_truth_tokens = count_tokens_qwen(ground_truth)
# Check condition (1): ground truth answer is less than 5 tokens
condition_1 = ground_truth_tokens < 5
# Check condition (2): ground truth answer (lowercase) is NOT in predicted answer (lowercase)
condition_2 = ground_truth.lower() not in predicted.lower()
# If both conditions are satisfied, set judge as 0 (keep the example)
if condition_1 and condition_2:
if debug:
print(f"KEEP: Short answer ({ground_truth_tokens} tokens) not found in prediction")
print(f" GT: '{ground_truth}'")
print(f" Pred: '{predicted[:100]}...'")
return 0 # Keep the example
else:
if debug:
reason = []
if not condition_1:
reason.append(f"answer too long ({ground_truth_tokens} >= 5 tokens)")
if not condition_2:
reason.append("answer found in prediction")
print(f"REMOVE: {', '.join(reason)}")
print(f" GT: '{ground_truth}'")
print(f" Pred: '{predicted[:100]}...'")
return 1 # Remove the example
# VARIABLE CHUNK SIZE FEATURE:
# For SQuAD, HotpotQA, WOS46985, PubMed-RCT, ArXiv-Classification, and EurLex datasets,
# chunks now have variable sizes between 100 and 4096 tokens instead of fixed sizes around 2000 tokens.
# This is controlled by the variable_size parameter in chunking functions.
# Booksum dataset is excluded from this feature as requested.
# def count_tokens_qwen(text, model="gpt-4o-mini"):
# """Count tokens using tiktoken"""
# encoding = tiktoken.encoding_for_model(model)
# # Convert input to string if it's not already a string
# if not isinstance(text, str):
# if isinstance(text, list):
# text = " ".join(str(item) for item in text)
# else:
# text = str(text)
# return len(encoding.encode(text))
def create_chunks_use_sent_tokenizer(text, max_tokens=10000):
"""Create chunks from text using sentence tokenization"""
# Make sure we have the punkt tokenizer downloaded
try:
nltk.data.find('tokenizers/punkt')
except LookupError:
nltk.download('punkt')
# Split text into sentences
sentences = nltk.sent_tokenize(text)
chunks = []
current_chunk = ""
current_tokens = 0
for sentence in sentences:
if '<|endoftext|>' in sentence:
sentence = sentence.replace('<|endoftext|>', '\n')
sentence_tokens = count_tokens_qwen(sentence)
# If adding this sentence would exceed max_tokens, start a new chunk
if current_tokens + sentence_tokens > max_tokens and current_chunk:
chunks.append(current_chunk.strip())
current_chunk = sentence
current_tokens = sentence_tokens
else:
if current_chunk:
# Add space between sentences
current_chunk += " " + sentence
current_tokens += sentence_tokens + count_tokens_qwen(" ")
else:
current_chunk = sentence
current_tokens = sentence_tokens
# Add the last chunk if it exists
if current_chunk:
chunks.append(current_chunk.strip())
return chunks
def create_chunks(contexts, max_tokens=2000, min_tokens=None, variable_size=False):
"""Create chunks from contexts, ensuring each chunk is less than max_tokens
Args:
contexts: List of context strings to chunk
max_tokens: Maximum tokens per chunk (default: 2000)
min_tokens: Minimum tokens per chunk (used when variable_size=True, default: max_tokens/20)
variable_size: If True, randomly vary chunk size between min_tokens and max_tokens for each chunk
"""
chunks = []
current_chunk = ""
current_tokens = 0
# Set default min_tokens if not provided and variable_size is enabled
if variable_size and min_tokens is None:
min_tokens = max(100, max_tokens // 20)
# Set target tokens for first chunk
if variable_size:
target_tokens = random.randint(min_tokens, max_tokens)
else:
target_tokens = max_tokens
for context in contexts:
context_tokens = count_tokens_qwen(context)
# If adding this context would exceed target_tokens, start a new chunk
if current_tokens + context_tokens > target_tokens and current_chunk:
chunks.append(current_chunk.strip())
current_chunk = context
current_tokens = context_tokens
# Set new target for next chunk if using variable size
if variable_size:
target_tokens = random.randint(min_tokens, max_tokens)
else:
target_tokens = max_tokens
else:
if current_chunk:
current_chunk += "\n\n" + context
current_tokens += context_tokens + count_tokens_qwen("\n\n")
else:
current_chunk = context
current_tokens = context_tokens
# Add the last chunk if it exists
if current_chunk:
chunks.append(current_chunk.strip())
return chunks
def batch_process_questions_with_qwen32b(questions, batch_size=32, system_prompt=None, model="qwen3-32b", no_thinking=False):
"""
Process a list of questions using Qwen32B model in batches
Args:
questions: List of questions to process
batch_size: Number of questions to process in each batch
model: Qwen model to use
Returns:
List of responses corresponding to each question
"""
# Import and setup Qwen client
from openai import OpenAI
from transformers import AutoTokenizer
import time
# Setup Qwen client
client = OpenAI(
base_url=qwen32b_server_url,
api_key="EMPTY"
)
# Initialize tokenizer for prompt conversion
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-32B", trust_remote_code=True)
print(f"Starting batch processing of {len(questions)} questions with Qwen32B, batch size {batch_size}")
all_responses = []
# Process questions in batches
for i in range(0, len(questions), batch_size):
batch_questions = questions[i:i + batch_size]
batch_num = (i // batch_size) + 1
total_batches = (len(questions) + batch_size - 1) // batch_size
print(f"Processing batch {batch_num}/{total_batches} ({len(batch_questions)} questions)")
# Convert all questions in batch to prompts
batch_prompts = []
for question in batch_questions:
if system_prompt is not None:
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": question}
]
else:
messages = [
{"role": "user", "content": question}
]
# Convert to prompt using tokenizer
prompt = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)
if no_thinking:
prompt += "<think></think>\n\n"
batch_prompts.append(prompt)
# Process the entire batch at once using completions API
response = client.completions.create(
model=model,
prompt=batch_prompts,
max_tokens=1024,
temperature=0.7,
stream=False
)
# Extract responses
batch_responses = [choice.text for choice in response.choices]
if not no_thinking:
# need to remove the <think></think> tags
batch_responses = [(x.split("</think>")[1] if "</think>" in x else x) for x in batch_responses]
all_responses.extend(batch_responses)
print(f"Completed batch {batch_num}/{total_batches}")
# Delay between batches to avoid overloading the server
if i + batch_size < len(questions):
time.sleep(0.5)
print(f"Batch processing complete. Generated {len(all_responses)} responses.")
return all_responses
def process_squad_dataset(split='train', num_chunks=10, max_chunks_allowed=20):
"""Process SQuAD dataset into the desired format"""
if not os.path.exists("./data/squad/raw_instances.json"):
ds = load_dataset("rajpurkar/squad")
context_to_qas = defaultdict(list)
for item in ds[split]:
context = item['context']
qa = {
'question': item['question'],
'answer': item['answers']['text'][0],
}
context_to_qas[context].append(qa)
print(f"Found {len(context_to_qas)} unique contexts")
# Get unique contexts
unique_contexts = list(context_to_qas.keys())
chunks = create_chunks(unique_contexts, max_tokens=2048, min_tokens=100, variable_size=True)
# Print chunk size statistics
chunk_sizes = [count_tokens_qwen(chunk) for chunk in chunks]
print(f"Chunk size statistics - Min: {min(chunk_sizes)}, Max: {max(chunk_sizes)}, Avg: {sum(chunk_sizes)/len(chunk_sizes):.1f}")
# Create data instances with 10 chunks each
print("Creating data instances...")
processed_data = []
# Import datetime for timestamp generation
base_date = datetime(2024, 1, 1)
for i in range(0, len(chunks), num_chunks):
# Get 10 chunks (or remaining chunks if less than 10)
chunk_batch_raw = chunks[i:i+num_chunks]
# Format chunks with conversational templates
chunk_batch = []
for chunk_idx, chunk_content in enumerate(chunk_batch_raw):
# Format using conversational template with random selection
user_template = random.choice(USER_MESSAGE_TEMPLATES)
assistant_template = random.choice(ASSISTANT_RESPONSE_TEMPLATES)
# Create a timestamp for each chunk (incrementing by days)
chunk_date = base_date + timedelta(days=chunk_idx)
timestamp = chunk_date.strftime("%Y-%m-%d %H:%M")
# Format the chunk with conversational template
formatted_chunk = f"[Dialogue between User and Assistant on {timestamp}]\n<User>{user_template}\n{chunk_content}\n<Assistant>{assistant_template}"
chunk_batch.append(formatted_chunk)
# Collect all questions and answers for these chunks
all_qas = []
for chunk_idx, chunk in enumerate(chunk_batch_raw): # Use raw chunks for Q&A matching
# For each chunk, find all contexts within it and collect their Q&As
for context in unique_contexts:
if context in chunk:
cur_qas = context_to_qas[context]
for qa in cur_qas:
qa['evidence_idx'] = chunk_idx
all_qas.extend(cur_qas)
# filter out oversized questions
all_qas = [qa for qa in all_qas if count_tokens_qwen(qa['question']) < 2048]
# Filter out instances with less than 50 questions
if len(all_qas) < 50:
print(f"Skipping instance with only {len(all_qas)} questions (< 50)")
continue
# Create the data instance
data_instance = {
'prompt': 'I will provide you with sequential information chunks. Please analyze each chunk and decide what memory operations to perform to store this information effectively. Use memory_insert, memory_update, or memory_delete operations as needed.',
'chunks': chunk_batch,
'questions_and_answers': all_qas,
'data_source': 'squad',
}
processed_data.append(data_instance)
print(f"Created instance {len(processed_data)} with {len(chunk_batch)} chunks and {len(all_qas)} Q&As")
with open("./data/squad/raw_instances.json", "w", encoding="utf-8") as f:
json.dump(processed_data, f, indent=2, ensure_ascii=False)
else:
with open("./data/squad/raw_instances.json", "r", encoding="utf-8") as f:
processed_data = json.load(f)
question_to_groundtruth_answer = {}
for instance in processed_data:
for qa in instance['questions_and_answers']:
question_to_groundtruth_answer[qa['question']] = qa['answer']
if not os.path.exists("./data/qwen32b-answers/squad_results.json"):
# Filter questions to only process those with short answers (< 5 tokens) to save compute
print("Pre-filtering questions with short answers (< 5 tokens) to save compute...")
all_questions = []
question_to_answer = {}
original_count = 0
for instance in processed_data:
for qa in instance['questions_and_answers']:
original_count += 1
answer_tokens = count_tokens_qwen(str(qa['answer']).strip())
if answer_tokens < 5:
all_questions.append(qa['question'])
question_to_answer[qa['question']] = qa['answer']
print(f"Filtered questions: {original_count} → {len(all_questions)} ({len(all_questions)/original_count*100:.1f}% kept)")
print(f"Saved {original_count - len(all_questions)} API calls by pre-filtering long answers")
# Batch process filtered questions using Qwen32B API
batch_responses = batch_process_questions_with_qwen32b(all_questions,
batch_size=1024,
system_prompt="You are a helpful assistant. Answer the question as accurately as possible. Be brief, only output the answer without any other text.", no_thinking=False)
assert len(batch_responses) == len(all_questions)
# Create results for all questions (including filtered ones)
results = []
processed_questions = set(all_questions)
for instance in processed_data:
for qa in instance['questions_and_answers']:
question = qa['question']
if question in processed_questions:
# Question was processed - use API response
question_idx = all_questions.index(question)
results.append({'question': question, 'answer': batch_responses[question_idx]})
else:
# Question was filtered out - use placeholder to indicate it wasn't processed
results.append({'question': question, 'answer': '[FILTERED_LONG_ANSWER]'})
# Save batch results for analysis
os.makedirs('./data/qwen32b-answers', exist_ok=True)
with open('./data/qwen32b-answers/squad_results.json', 'w', encoding='utf-8') as f:
json.dump(results, f, indent=2, ensure_ascii=False)
print("Batch processing results saved to ./data/qwen32b-answers/squad_results.json")
else:
with open("./data/qwen32b-answers/squad_results.json", "r", encoding="utf-8") as f:
results = json.load(f)
if not os.path.exists("./data/qwen32b-answers/squad_results_with_judge.json"):
# Use token-based logic instead of LLM judge
question_to_predicted_answer = {qa['question']: qa['answer'] for qa in results}
question_to_score = {}
keep_count = 0
remove_count = 0
short_answer_count = 0
print("Applying new token-based filtering logic...")
for q in question_to_groundtruth_answer:
ground_truth = question_to_groundtruth_answer[q]
predicted = question_to_predicted_answer[q]
# Count short answers for statistics
if count_tokens_qwen(str(ground_truth).strip()) < 5:
short_answer_count += 1
# Handle filtered questions (long answers that weren't processed)
if predicted == '[FILTERED_LONG_ANSWER]':
score = 1 # Remove questions with long answers automatically
else:
# Use our new token logic: 0 means keep (incorrect), 1 means remove (correct)
score = judge_answer_with_token_logic(ground_truth, predicted)
question_to_score[q] = score
if score == 0:
keep_count += 1
else:
remove_count += 1
total_questions = len(question_to_groundtruth_answer)
filtered_questions = len([q for q in question_to_predicted_answer.values() if q == '[FILTERED_LONG_ANSWER]'])
print(f"Token-based filtering results for SQuAD:")
print(f" Total questions: {total_questions}")
print(f" Pre-filtered (long answers): {filtered_questions} ({filtered_questions/total_questions*100:.1f}%)")
print(f" Short answers (< 5 tokens): {short_answer_count} ({short_answer_count/total_questions*100:.1f}%)")
print(f" Questions to keep: {keep_count} ({keep_count/total_questions*100:.1f}%)")
print(f" Questions to remove: {remove_count} ({remove_count/total_questions*100:.1f}%)")
# Save the predicted results to "results"
for idx, result in enumerate(results):
result['score'] = question_to_score[result['question']]
# Save the results to "results"
with open('./data/qwen32b-answers/squad_results_with_judge.json', 'w', encoding='utf-8') as f:
json.dump(results, f, indent=2, ensure_ascii=False)
print("Batch processing results saved to ./data/squad_results_with_judge.json")
else:
with open("./data/qwen32b-answers/squad_results_with_judge.json", "r", encoding="utf-8") as f:
results = json.load(f)
# Now start filtering the data
question_to_score = {qa['question']: qa['score'] for qa in results}
for idx, instance in enumerate(processed_data):
original_len = len(instance['questions_and_answers'])
filtered_qas = []
for qa in instance['questions_and_answers']:
# Use .get() to handle missing questions gracefully
if question_to_score.get(qa['question'], 1) == 0: # Default to 1 if question not found
filtered_qas.append(qa)
if len(filtered_qas) > 100:
filtered_qas = random.sample(filtered_qas, 100)
instance['questions_and_answers'] = filtered_qas
print(f"Filtered out {original_len - len(filtered_qas)} out of {original_len} questions for Instance {idx}")
print(f"Obtained {len(processed_data)} instances")
processed_data = [f for f in processed_data if len(f['questions_and_answers']) >= 30]
print(f"Filtered to {len(processed_data)} instances with more than 30 questions")
# Filter out instances with too many chunks (over 20)
max_chunks_allowed = max_chunks_allowed
filtered_instances = []
instances_with_too_many_chunks = 0
for instance in processed_data:
num_chunks = len(instance['chunks'])
if num_chunks <= max_chunks_allowed:
filtered_instances.append(instance)
else:
instances_with_too_many_chunks += 1
print(f"Removing SQuAD instance with {num_chunks} chunks (> {max_chunks_allowed})")
print(f"\n📊 SQuAD Chunk Filtering Summary:")
print(f" • Removed {instances_with_too_many_chunks} instances with > {max_chunks_allowed} chunks")
if processed_data:
print(f" • Instances: {len(processed_data):,} → {len(filtered_instances):,} (kept {len(filtered_instances)/len(processed_data)*100:.1f}%)")
else:
print(f" • Instances: 0 → 0")
return filtered_instances
def process_hotpotqa_dataset():
"""Process HotpotQA dataset into the desired format"""
os.makedirs("./data/hotpotqa", exist_ok=True)
if not os.path.exists("./data/hotpotqa/processed_data.json"):
ds = load_dataset("hotpotqa/hotpot_qa", "fullwiki")
print(f"HotpotQA dataset loaded with {len(ds['train'])} training examples")
unique_articles = {} # title -> full_text
questions_data = []
for item in ds['train']:
# Extract context information
titles = item['context']['title']
sentences_lists = item['context']['sentences']
# Create unique articles
for title, sentences in zip(titles, sentences_lists):
if title not in unique_articles:
# Combine all sentences for this title into one article
full_text = ' '.join(sentences)
unique_articles[title] = full_text
# Extract question with evidence requirements
question_data = {
'id': item['id'],
'question': item['question'],
'answer': item['answer'],
'evidence_requirements': [] # list of (title, sentence_text) tuples
}
# Extract supporting facts (evidence)
for support_title, sent_id in zip(item['supporting_facts']['title'], item['supporting_facts']['sent_id']):
# Find the sentence text
title_idx = titles.index(support_title) if support_title in titles else -1
if title_idx >= 0 and sent_id < len(sentences_lists[title_idx]):
sentence_text = sentences_lists[title_idx][sent_id]
question_data['evidence_requirements'].append((support_title, sentence_text))
questions_data.append(question_data)
print(f"Found {len(unique_articles)} unique articles")
print(f"Found {len(questions_data)} questions")
print("Calculating article statistics...")
article_tokens = []
for title, text in unique_articles.items():
tokens = count_tokens_qwen(f"Title: {title}\n{text}")
article_tokens.append(tokens)
avg_article_tokens = sum(article_tokens) / len(article_tokens)
print(f"Average article length: {avg_article_tokens:.1f} tokens")
# Target: ~20k tokens per data instance, with chunks of ~2k tokens each (so ~10 chunks)
# Calculate how many articles we can fit in one data instance
target_tokens_per_instance = 10000
k = max(1, int(target_tokens_per_instance / avg_article_tokens))
print(f"Target articles per data instance: {k}")
# Step 3: Process questions sequentially and group articles
print("Processing questions and grouping articles...")
processed_data = []
current_articles = [] # list of titles
current_questions = [] # list of question_data
for question in questions_data:
# Extract evidence titles for this question
evidence_titles = set(title for title, _ in question['evidence_requirements'])
# Check if we need to add new articles
new_articles_needed = evidence_titles - set(current_articles)
# If adding new articles would exceed k, process current batch
if current_articles and len(current_articles) + len(new_articles_needed) > k:
# Process current batch
if current_articles and current_questions: # Make sure we have both articles and questions
data_instance = create_data_instance(current_articles, current_questions, unique_articles)
if data_instance: # Only add if successfully created
processed_data.append(data_instance)
print(f"Created instance {len(processed_data)} with {len(current_articles)} articles and {len(current_questions)} questions")
# Start new batch with current question's requirements
current_articles = list(evidence_titles)
current_questions = [question]
else:
# Add new articles to current batch
current_articles.extend(new_articles_needed)
current_questions.append(question)
# Process the last batch
if current_articles and current_questions:
data_instance = create_data_instance(current_articles, current_questions, unique_articles)
if data_instance:
processed_data.append(data_instance)
print(f"Created instance {len(processed_data)} with {len(current_articles)} articles and {len(current_questions)} questions")
with open("./data/hotpotqa/processed_data.json", "w", encoding="utf-8") as f:
json.dump(processed_data, f, indent=2, ensure_ascii=False)
else:
with open("./data/hotpotqa/processed_data.json", "r", encoding="utf-8") as f:
processed_data = json.load(f)
print(f"Obtained {len(processed_data)} instances")
processed_data = [f for f in processed_data if len(f['questions_and_answers']) >= 30]
print(f"Filtered to {len(processed_data)} instances with more than 30 questions")
question_to_groundtruth_answer = {}
for instance in processed_data:
for qa in instance['questions_and_answers']:
question_to_groundtruth_answer[qa['question']] = qa['answer']
if not os.path.exists("./data/qwen32b-answers/hotpotqa_results.json"):
# Filter questions to only process those with short answers (< 5 tokens) to save compute
print("Pre-filtering questions with short answers (< 5 tokens) to save compute...")
all_questions = []
question_to_answer = {}
original_count = 0
for instance in processed_data:
for qa in instance['questions_and_answers']:
original_count += 1
answer_tokens = count_tokens_qwen(str(qa['answer']).strip())
if answer_tokens < 5:
all_questions.append(qa['question'])
question_to_answer[qa['question']] = qa['answer']
print(f"Filtered questions: {original_count} → {len(all_questions)} ({len(all_questions)/original_count*100:.1f}% kept)")
print(f"Saved {original_count - len(all_questions)} API calls by pre-filtering long answers")
batch_responses = batch_process_questions_with_qwen32b(all_questions, batch_size=1024, system_prompt="You are a helpful assistant. Answer the question as accurately as possible. Be brief, only output the answer without any other text.", no_thinking=False)
# Create results for all questions (including filtered ones)
results = []
processed_questions = set(all_questions)
for instance in processed_data:
for qa in instance['questions_and_answers']:
question = qa['question']
if question in processed_questions:
# Question was processed - use API response
question_idx = all_questions.index(question)
results.append({'question': question, 'answer': batch_responses[question_idx]})
else:
# Question was filtered out - use placeholder to indicate it wasn't processed
results.append({'question': question, 'answer': '[FILTERED_LONG_ANSWER]'})
with open("./data/qwen32b-answers/hotpotqa_results.json", "w", encoding="utf-8") as f:
json.dump(results, f, indent=2, ensure_ascii=False)
print("Batch processing results saved to ./data/qwen32b-answers/hotpotqa_results.json")
else:
with open("./data/qwen32b-answers/hotpotqa_results.json", "r", encoding="utf-8") as f:
results = json.load(f)
if not os.path.exists("./data/qwen32b-answers/hotpotqa_results_with_judge.json"):
# Use token-based logic instead of LLM judge
question_to_predicted_answer = {qa['question']: qa['answer'] for qa in results}
question_to_score = {}
keep_count = 0
remove_count = 0
short_answer_count = 0
print("Applying new token-based filtering logic...")
for q in question_to_groundtruth_answer:
ground_truth = question_to_groundtruth_answer[q]
predicted = question_to_predicted_answer[q]
# Count short answers for statistics
if count_tokens_qwen(str(ground_truth).strip()) < 5:
short_answer_count += 1
# Handle filtered questions (long answers that weren't processed)
if predicted == '[FILTERED_LONG_ANSWER]':
score = 1 # Remove questions with long answers automatically
else:
# Use our new token logic: 0 means keep (incorrect), 1 means remove (correct)
score = judge_answer_with_token_logic(ground_truth, predicted)
question_to_score[q] = score
if score == 0:
keep_count += 1
else:
remove_count += 1
total_questions = len(question_to_groundtruth_answer)
filtered_questions = len([q for q in question_to_predicted_answer.values() if q == '[FILTERED_LONG_ANSWER]'])
print(f"Token-based filtering results for HotpotQA:")
print(f" Total questions: {total_questions}")
print(f" Pre-filtered (long answers): {filtered_questions} ({filtered_questions/total_questions*100:.1f}%)")
print(f" Short answers (< 5 tokens): {short_answer_count} ({short_answer_count/total_questions*100:.1f}%)")
print(f" Questions to keep: {keep_count} ({keep_count/total_questions*100:.1f}%)")
print(f" Questions to remove: {remove_count} ({remove_count/total_questions*100:.1f}%)")
for idx, result in enumerate(results):
result['score'] = question_to_score[result['question']]
with open("./data/qwen32b-answers/hotpotqa_results_with_judge.json", "w", encoding="utf-8") as f:
json.dump(results, f, indent=2, ensure_ascii=False)
print("Batch processing results saved to ./data/hotpotqa_results_with_judge.json")
else:
with open("./data/qwen32b-answers/hotpotqa_results_with_judge.json", "r", encoding="utf-8") as f:
results = json.load(f)
# Now start filtering the data
question_to_score = {qa['question']: qa['score'] for qa in results}
for idx, instance in enumerate(processed_data):
filtered_qas = []
original_len = len(instance['questions_and_answers'])
for qa in instance['questions_and_answers']:
if question_to_score[qa['question']] == 0:
filtered_qas.append(qa)
if len(filtered_qas) > 100:
filtered_qas = random.sample(filtered_qas, 100)
instance['questions_and_answers'] = filtered_qas
print(f"Filtered out {original_len - len(filtered_qas)} out of {original_len} questions for Instance {idx}")
print(f"Obtained {len(processed_data)} instances")
processed_data = [f for f in processed_data if len(f['questions_and_answers']) >= 10 and len(f['questions_and_answers']) <= 200]
print(f"Filtered to {len(processed_data)} instances with 10-200 questions")
# Filter out instances with too many chunks (over 20)
max_chunks_allowed = 20
filtered_instances = []
instances_with_too_many_chunks = 0
for instance in processed_data:
num_chunks = len(instance['chunks'])
if num_chunks <= max_chunks_allowed:
filtered_instances.append(instance)
else:
instances_with_too_many_chunks += 1
print(f"Removing HotpotQA instance with {num_chunks} chunks (> {max_chunks_allowed})")
print(f"\n📊 HotpotQA Chunk Filtering Summary:")
print(f" • Removed {instances_with_too_many_chunks} instances with > {max_chunks_allowed} chunks")
if processed_data:
print(f" • Instances: {len(processed_data):,} → {len(filtered_instances):,} (kept {len(filtered_instances)/len(processed_data)*100:.1f}%)")
else:
print(f" • Instances: 0 → 0")
return filtered_instances
def create_data_instance(article_titles, questions, unique_articles):
"""Create a data instance from a list of article titles and questions"""
# Get article texts
article_texts = []
for title in article_titles:
if title in unique_articles:
article_texts.append(unique_articles[title])
else:
print(f"Warning: Article '{title}' not found in unique_articles")
if not article_texts:
return None
# Create chunks from articles with variable sizes between 100 and 4k tokens
chunks_with_titles = create_chunks_with_titles(article_titles, article_texts, max_tokens=2048, min_tokens=100, variable_size=True)
if not chunks_with_titles:
return None
# Import datetime for timestamp generation
base_date = datetime(2024, 1, 1)
# Extract chunk texts and format with conversational templates
chunk_texts = []
chunk_title_mapping = [] # List of lists, each containing titles in that chunk
for chunk_idx, (chunk_text, titles_in_chunk) in enumerate(chunks_with_titles):
# Format using conversational template with random selection
user_template = random.choice(USER_MESSAGE_TEMPLATES)
assistant_template = random.choice(ASSISTANT_RESPONSE_TEMPLATES)
# Create a timestamp for each chunk (incrementing by days)
chunk_date = base_date + timedelta(days=chunk_idx)
timestamp = chunk_date.strftime("%Y-%m-%d %H:%M")
# Format the chunk with conversational template
formatted_chunk = f"[Dialogue between User and Assistant on {timestamp}]\n<User>{user_template}\n{chunk_text}\n<Assistant>{assistant_template}"
chunk_texts.append(formatted_chunk)
chunk_title_mapping.append(titles_in_chunk)
# Print chunk size statistics for this instance
chunk_sizes = [count_tokens_qwen(chunk_text) for chunk_text in chunk_texts]
print(f"Instance chunks - Count: {len(chunk_texts)}, Sizes: Min={min(chunk_sizes)}, Max={max(chunk_sizes)}, Avg={sum(chunk_sizes)/len(chunk_sizes):.1f}")
# Format questions and answers with evidence_idx
formatted_qas = []
for q in questions:
# Find which chunk(s) contain the evidence for this question
evidence_chunk_indices = set()
for evidence_title, evidence_sentence in q['evidence_requirements']:
# Find which chunk contains this evidence title
for chunk_idx, titles_in_chunk in enumerate(chunk_title_mapping):
if evidence_title in titles_in_chunk:
evidence_chunk_indices.add(chunk_idx)
break
# Convert to sorted list for consistency
evidence_idx = sorted(list(evidence_chunk_indices)) if evidence_chunk_indices else [0] # Default to first chunk if no evidence found
qa = {
'question': q['question'],
'answer': q['answer'],
'evidence': q['evidence_requirements'],
'evidence_idx': evidence_idx # Add evidence chunk indices
}
formatted_qas.append(qa)
# Create the data instance
data_instance = {
'prompt': 'I will provide you with sequential information chunks. Please analyze each chunk and decide what memory operations to perform to store this information effectively. Use memory_insert, memory_update, or memory_delete operations as needed.',
'chunks': chunk_texts,
'questions_and_answers': formatted_qas
}
return data_instance
def create_chunks_with_titles(titles, texts, max_tokens=2000, min_tokens=None, variable_size=False):
"""Create chunks from articles, keeping track of which titles are in each chunk
Args:
titles: List of article titles
texts: List of article texts
max_tokens: Maximum tokens per chunk (default: 2000)
min_tokens: Minimum tokens per chunk (used when variable_size=True, default: max_tokens/20)
variable_size: If True, randomly vary chunk size between min_tokens and max_tokens for each chunk
"""
chunks_with_titles = [] # list of (chunk_text, [titles_in_chunk])
current_chunk = ""
current_titles = []
current_tokens = 0
# Set default min_tokens if not provided and variable_size is enabled
if variable_size and min_tokens is None:
min_tokens = max(100, max_tokens // 20)
# Set target tokens for first chunk
if variable_size:
target_tokens = random.randint(min_tokens, max_tokens)
else:
target_tokens = max_tokens
for title, text in zip(titles, texts):
text_with_title = f"Title: {title}\n{text}"
text_tokens = count_tokens_qwen(text_with_title)
# CRITICAL FIX: Truncate oversized articles to prevent massive chunks
if text_tokens > target_tokens:
# Truncate text to fit within target_tokens (reserve ~100 tokens for title)
max_text_chars = min(len(text), (target_tokens - 100) * 4) # ~4 chars per token estimate
truncated_text = text[:max_text_chars]
text_with_title = f"Title: {title}\n{truncated_text}"
text_tokens = count_tokens_qwen(text_with_title)
# If still too large after truncation, skip this article
if text_tokens > target_tokens:
print(f" Warning: Skipping oversized article '{title}' ({text_tokens} tokens > {target_tokens} target)")
continue
# If adding this article would exceed target_tokens, start a new chunk
if current_tokens + text_tokens > target_tokens and current_chunk:
chunks_with_titles.append((current_chunk.strip(), current_titles.copy()))
current_chunk = text_with_title
current_titles = [title]
current_tokens = text_tokens
# Set new target for next chunk if using variable size
if variable_size:
target_tokens = random.randint(min_tokens, max_tokens)