summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--finetune.py22
-rw-r--r--inference.py53
-rw-r--r--main.py2
-rw-r--r--reformat.py66
-rw-r--r--split_dataset.py66
6 files changed, 186 insertions, 25 deletions
diff --git a/.gitignore b/.gitignore
index 6ade941..252715e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,7 @@
1venv 1venv
2*~ 2*~
3.*
4*.mp3 3*.mp3
5*.wav 4*.wav
6*.json 5*.json
7*.jsonl 6*.jsonl
7*/
diff --git a/finetune.py b/finetune.py
index bca7ac1..05b558f 100644
--- a/finetune.py
+++ b/finetune.py
@@ -1,15 +1,17 @@
1import torch 1import torch
2from datasets import load_dataset 2from datasets import load_dataset
3from peft import LoraConfig 3from peft import LoraConfig, get_peft_model
4from transformers import AutoModelForCausalLM, AutoModelForSeq2SeqLM, AutoTokenizer 4from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
5from trl import SFTTrainer, SFTConfig 5from trl import SFTTrainer, SFTConfig
6import os 6import os
7 7
8#MODEL_ID = "google/gemma-3-270m-it" 8#MODEL_ID = "google/flan-t5-large"
9MODEL_ID = "google/gemma-3-1b-it" 9#MODEL_ID = "google/t5gemma-b-b-prefixlm-it"
10#MODEL_ID = "google/long-t5-tglobal-base"
11MODEL_ID = "google/t5gemma-s-s-ul2-it"
10DATASET_TRAIN_PATH = "train.jsonl" 12DATASET_TRAIN_PATH = "train.jsonl"
11DATASET_VAL_PATH = "validation.jsonl" 13DATASET_VAL_PATH = "validation.jsonl"
12OUTPUT_DIR = "./gemma3-1b-it-sponsors" 14OUTPUT_DIR = "./t5gemma-s-s-ul2-it-sponsors"
13LORA_R = 16 15LORA_R = 16
14LORA_ALPHA = 32 16LORA_ALPHA = 32
15LORA_DROPOUT = 0.05 17LORA_DROPOUT = 0.05
@@ -24,7 +26,7 @@ def main():
24 if tokenizer.pad_token is None: 26 if tokenizer.pad_token is None:
25 tokenizer.pad_token = tokenizer.eos_token 27 tokenizer.pad_token = tokenizer.eos_token
26 28
27 model = AutoModelForCausalLM.from_pretrained( 29 model = AutoModelForSeq2SeqLM.from_pretrained(
28 MODEL_ID, 30 MODEL_ID,
29 device_map="auto", 31 device_map="auto",
30 torch_dtype=torch.bfloat16, 32 torch_dtype=torch.bfloat16,
@@ -44,9 +46,13 @@ def main():
44 target_modules=LORA_TARGET_MODULES, 46 target_modules=LORA_TARGET_MODULES,
45 lora_dropout=LORA_DROPOUT, 47 lora_dropout=LORA_DROPOUT,
46 bias="none", 48 bias="none",
47 task_type="CAUSAL_LM" 49 task_type="SEQ_2_SEQ_LM"
48 ) 50 )
49 51
52 print("Applying LoRA config to the model...")
53 model = get_peft_model(model, peft_config)
54 model.print_trainable_parameters()
55
50 print("Model and LoRA config loaded. Setting up trainer...") 56 print("Model and LoRA config loaded. Setting up trainer...")
51 57
52 training_args = SFTConfig( 58 training_args = SFTConfig(
@@ -57,7 +63,6 @@ def main():
57 per_device_eval_batch_size=1, 63 per_device_eval_batch_size=1,
58 per_device_train_batch_size=1, 64 per_device_train_batch_size=1,
59 gradient_accumulation_steps=8, 65 gradient_accumulation_steps=8,
60 gradient_checkpointing=True,
61 learning_rate=1e-4, 66 learning_rate=1e-4,
62 lr_scheduler_type="linear", 67 lr_scheduler_type="linear",
63 optim="adamw_torch", 68 optim="adamw_torch",
@@ -83,7 +88,6 @@ def main():
83 model=model, 88 model=model,
84 processing_class=tokenizer, 89 processing_class=tokenizer,
85 args=training_args, 90 args=training_args,
86 peft_config=peft_config,
87 train_dataset=train_dataset, 91 train_dataset=train_dataset,
88 eval_dataset=validation_dataset, 92 eval_dataset=validation_dataset,
89 ) 93 )
diff --git a/inference.py b/inference.py
new file mode 100644
index 0000000..283fa1b
--- /dev/null
+++ b/inference.py
@@ -0,0 +1,53 @@
1import torch
2import re
3from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
4from peft import PeftModel
5
6MODEL_PATH = "./t5gemma-s-s-ul2-it-sponsors/final_checkpoint"
7BASE_MODEL_ID = "google/t5gemma-s-s-ul2-it"
8DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
9torch.set_float32_matmul_precision('high')
10
11print("Loading base model...")
12base_model = AutoModelForSeq2SeqLM.from_pretrained(BASE_MODEL_ID, torch_dtype=torch.bfloat16)
13print("Loading LoRA weights...")
14model = PeftModel.from_pretrained(base_model, MODEL_PATH)
15
16print("Merging LoRA weights into the base model...")
17model = model.merge_and_unload()
18
19model.to(DEVICE)
20model.eval()
21
22tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_ID)
23if tokenizer.pad_token is None:
24 tokenizer.pad_token = tokenizer.eos_token
25
26def predict_timestamp(transcript_text, max_new_tokens=256):
27 input_text = (
28 "Instruction: Identify the timestamp range of the pre-recorded ad in the following transcript. "
29 "Output only the HH:MM:SS.mmm-HH:MM:SS.mmm range.\n\nTranscript:\n"
30 f"{transcript_text}"
31 )
32
33 inputs = tokenizer(input_text, return_tensors="pt").to(DEVICE)
34
35 with torch.no_grad():
36 output_ids = model.generate(
37 **inputs,
38 max_new_tokens=max_new_tokens,
39 do_sample=False,
40 pad_token_id=tokenizer.eos_token_id,
41 )
42
43 input_length = inputs.input_ids.shape[1]
44 newly_generated_ids = output_ids[0][input_length:]
45 output_text = tokenizer.decode(newly_generated_ids, skip_special_tokens=True)
46 return output_text
47
48if __name__ == "__main__":
49 # Single transcript
50 transcript = "[00:18:00.079 --> 00:18:01.119] Points on my bracket.\n[00:18:01.200 --> 00:18:02.640] That's the max score I can get.\n[00:18:02.799 --> 00:18:12.720] His max bracket score right now is 62 points, which is barely enough to compete with the people currently in the lead of our bracket challenge.\n[00:18:12.960 --> 00:18:15.039] So you're saying there's a chance?\n[00:18:15.359 --> 00:18:16.400] That's not what he's saying.\n[00:18:16.480 --> 00:18:17.759] That's not what he's saying at all.\n[00:18:17.920 --> 00:18:19.839] We rewind, we cut to him like actually.\n[00:18:20.160 --> 00:18:29.359] Actually, like mad scientist going through each team's roster, like seeing their potential draft status, and he's like trying to create and he's like, This is gonna be the best bracket we've ever seen.\n[00:18:29.599 --> 00:18:32.799] And he never put more effort to anything in his life, and this is the result.\n[00:18:33.039 --> 00:18:36.160] Well, it doesn't bode well for him that who did he pick to win it?\n[00:18:36.319 --> 00:18:38.319] He picked Oh, he picked the Zags.\n[00:18:38.400 --> 00:18:39.680] He picked on Zaga to win it, which\n[00:18:40.319 --> 00:18:43.839] Is not a completely insane pick, but they were an eight seed.\n[00:18:43.920 --> 00:18:45.440] They're a good tournament team.\n[00:18:45.519 --> 00:18:49.119] Like they're traditionally they're a good tournament team, but what was their seed this year?\n[00:18:49.279 --> 00:18:50.319] They were an eight seed.\n[00:18:50.480 --> 00:18:52.640] It's a bold strategy, but there's a chance.\n[00:18:53.359 --> 00:18:54.640] Mark, how's your bracket?\n[00:18:54.960 --> 00:18:55.680] Oh man.\n[00:18:55.839 --> 00:18:57.440] If I did one this year.\n[00:18:57.599 --> 00:18:59.680] Well, I've done one for the past few years.\n[00:19:00.240 --> 00:19:05.119] And I've done it completely by random, and each time I've beaten Tyler's bracket.\n[00:19:06.960 --> 00:19:09.119] Like not lying multiple times.\n[00:19:09.279 --> 00:19:10.559] I can't remember if it's every time.\n[00:19:10.799 --> 00:19:12.319] I'll bet that goes over well.\n[00:19:12.559 --> 00:19:19.759] But yeah, I just purely by random chance and I I don't just win, I crush, I crush him in points.\n[00:19:20.240 --> 00:19:22.240] Just absolutely destroy.\n[00:19:22.480 --> 00:19:27.359] I'm gonna put down better at sports than Tyler for a point for Mark.\n[00:19:27.599 --> 00:19:29.119] Yep, that's that's true actually.\n[00:19:29.279 --> 00:19:31.440] I'm surprised you guys didn't do one for Go.\n[00:19:31.680 --> 00:19:32.720] He's gone.\n[00:19:34.880 --> 00:19:35.920] Where'd he go?\n[00:19:36.160 --> 00:19:36.880] He's gone.\n[00:19:37.200 --> 00:19:38.000] Honorable.\n[00:19:38.160 --> 00:19:39.920] How many times have I started a rumor that?\n[00:19:40.240 --> 00:19:41.839] Tyler's dead on his podcast.\n[00:19:42.799 --> 00:19:44.079] At least one now.\n[00:19:44.319 --> 00:19:47.440] I forget where this is all a delay because I forgot where he's going.\n[00:19:47.599 --> 00:19:50.240] He told me many times, but he's not here.\n[00:19:50.480 --> 00:19:52.799] I know where he was until the middle of the night last night.\n[00:19:53.039 --> 00:19:54.400] That's very specific.\n[00:19:54.559 --> 00:19:58.079] Uh Bird, the porn guy, if you guys remember the porn guy from the porn episode.\n[00:19:58.240 --> 00:19:59.920] Uh I found out that he had to take Tyler's.\n[00:20:00.240 --> 00:20:01.200] to the airport last night.\n[00:20:01.359 --> 00:20:04.880] That's weird because I know where Bert lives and it's not near where Tyler lives.\n[00:20:05.119 --> 00:20:05.759] It is not.\n[00:20:05.920 --> 00:20:07.680] Meaning that Tyler was in fact gone.\n[00:20:07.920 --> 00:20:10.400] Maybe that should be the the topic of this episode.\n[00:20:10.480 --> 00:20:12.720] We need to get to the bottom of this ASAP.\n[00:20:12.960 --> 00:20:15.839] If we find out where he is by the end of the episode, do we get a point?\n[00:20:16.079 --> 00:20:16.400] Yeah.\n[00:20:16.640 --> 00:20:19.920] You can have two points if you find out where he is by the end of the episode.\n[00:20:20.480 --> 00:20:21.599] Everybody starts texting Tyler.\n[00:20:21.759 --> 00:20:22.640] Hey, where the fuck are you?\n[00:20:22.799 --> 00:20:27.279] I'm debating whether I text Tyler or do I reach for the Girl Scout cookies?\n[00:20:27.519 --> 00:20:29.839] Do I want the points or the sugar?\n[00:20:30.079 --> 00:20:32.400] Alright, let's say we can't just text him directly.\n[00:20:32.559 --> 00:20:34.160] That would be cheating.\n[00:20:34.480 --> 00:20:36.640] This episode is brought to you by Amazon.\n[00:20:36.799 --> 00:20:39.839] This off to college season, save on college, save the everyday.\n[00:20:40.160 --> 00:20:44.640] Literally every supply you need for school, like pens and stuff that you just assume you have.\n[00:20:44.799 --> 00:20:46.319] You know, I remember a story.\n[00:20:46.480 --> 00:20:49.519] My roommate and I decided to bunk our beds.\n[00:20:49.680 --> 00:20:53.119] So we just cut up some plastic hangers and just jammed them in there.\n[00:20:53.279 --> 00:20:56.160] We could have really used some metal pins.\n[00:20:56.400 --> 00:20:58.000] Wonder who that roommate could have been.\n[00:20:58.079 --> 00:20:59.920] So remember with Amazon's low off.\n[00:21:00.079 --> 00:21:02.559] Off to college prices, save on college, save the everyday.\n[00:21:02.720 --> 00:21:04.559] Shop off to college at Amazon.\n[00:21:04.720 --> 00:21:06.880] This episode is brought to you by Mentos Gum.\n[00:21:07.039 --> 00:21:09.119] Keep things fresh, it's important, right?\n[00:21:09.279 --> 00:21:10.799] And I'm not just talking about fresh breath.\n[00:21:10.880 --> 00:21:13.599] It's important to switch up your routine whenever you can.\n[00:21:13.759 --> 00:21:16.480] I just uh I'm the person who can't help but chew.\n[00:21:16.640 --> 00:21:18.640] You put up an event in your mouth, you're supposed to suck on it.\n[00:21:18.720 --> 00:21:18.880] I'm like\n[00:21:20.160 --> 00:21:21.759] Swallow, so I kinda need gum.\n[00:21:22.000 --> 00:21:23.759] You turn into a cartoon dog.\n[00:21:23.839 --> 00:21:24.240] I'm sorry.\n[00:21:25.519 --> 00:21:28.319] Next time we hang out, I'm giving you a mint just to see what happens.\n[00:21:28.480 --> 00:21:34.799] And of course, another way to refresh every day is with Mento's gum, available in a range of fresh flavors like spearmint, fresh mint, and strawberry.\n[00:21:35.039 --> 00:21:39.200] Mentos gum, yes, to fresh.\n[00:21:40.000 --> 00:21:42.400] Uh, should we get into the topic for today's episode?\n[00:21:42.640 --> 00:21:50.880] I gotta be honest, I looked and I'm only mediumly sure we haven't done something that's fairly similar to this, but I just sort of want to talk about it.\n[00:21:51.440 --> 00:21:59.359] I'm calling this episode probably not maybe something like uh Distractable Travel Guide, Cincinnati."
51 print(transcript)
52 predicted_range = predict_timestamp(transcript)
53 print("Predicted ad range:", predicted_range)
diff --git a/main.py b/main.py
index a58e02f..6887ac3 100644
--- a/main.py
+++ b/main.py
@@ -401,7 +401,7 @@ def process_transcript_json(input_json_path: str, output_jsonl_path: str):
401 401
402 final_results.append({ 402 final_results.append({
403 "id": prompt_id, 403 "id": prompt_id,
404 "text": full_prompt_for_finetuning, 404 "input": full_prompt_for_finetuning,
405 "target": response_text.strip() 405 "target": response_text.strip()
406 }) 406 })
407 407
diff --git a/reformat.py b/reformat.py
new file mode 100644
index 0000000..c21056c
--- /dev/null
+++ b/reformat.py
@@ -0,0 +1,66 @@
1import sys
2import json
3from transformers import AutoTokenizer
4from tqdm import tqdm
5
6# IMPORTANT: The tokenizer MUST match the student model you are fine-tuning.
7STUDENT_MODEL_ID = "google/t5gemma-s-s-ul2-it"
8
9def reformat_with_template(input_filepath: str, output_filepath: str):
10 """
11 Reads a JSONL file with 'text' (prompt) and 'target' (completion) fields
12 and reformats it into a single 'text' field using the model's chat template.
13 """
14 print(f"Loading tokenizer for {STUDENT_MODEL_ID}...")
15 tokenizer = AutoTokenizer.from_pretrained(STUDENT_MODEL_ID)
16
17 print(f"Reformatting {input_filepath}...")
18
19 with open(input_filepath, 'r', encoding='utf-8') as infile, \
20 open(output_filepath, 'w', encoding='utf-8') as outfile:
21
22 # Count lines for tqdm progress bar
23 num_lines = sum(1 for line in open(input_filepath, 'r', encoding='utf-8'))
24 infile.seek(0) # Reset file pointer
25
26 for line in tqdm(infile, total=num_lines, desc="Reformatting"):
27 original_entry = json.loads(line)
28
29 # The original prompt is in the 'text' field
30 prompt_text = original_entry.get("text", "")
31 # The verified completion is in the 'target' field
32 completion_text = original_entry.get("target", "")
33
34 # The prompt needs to end with the cue for the model to respond to
35 if "Timestamp:" not in prompt_text:
36 prompt_text += "\n\nTimestamp:\n"
37
38 # Create the message structure that the chat template expects
39 messages = [
40 {"role": "user", "content": prompt_text},
41 {"role": "assistant", "content": completion_text}
42 ]
43
44 # Apply the template to create the new, single text field
45 # add_generation_prompt=False is important for training data
46 formatted_text = tokenizer.apply_chat_template(
47 messages,
48 tokenize=False,
49 add_generation_prompt=False
50 )
51
52 new_entry = {
53 "id": original_entry.get("id"),
54 "text": formatted_text,
55 "target": completion_text # Keep target for reference if needed
56 }
57 outfile.write(json.dumps(new_entry) + "\n")
58
59 print(f"\nSuccessfully reformatted {num_lines} entries. Output saved to {output_filepath}.")
60
61if __name__ == "__main__":
62 if len(sys.argv) < 3:
63 print("Usage: python reformat_with_template.py <input_dataset.jsonl> <output_dataset_templated.jsonl>")
64 sys.exit(1)
65
66 reformat_with_template(sys.argv[1], sys.argv[2])
diff --git a/split_dataset.py b/split_dataset.py
index 9ac7bbf..a161cc1 100644
--- a/split_dataset.py
+++ b/split_dataset.py
@@ -7,10 +7,12 @@ def split_dataset(
7 input_filepath: str, 7 input_filepath: str,
8 train_filepath: str = "train.jsonl", 8 train_filepath: str = "train.jsonl",
9 validation_filepath: str = "validation.jsonl", 9 validation_filepath: str = "validation.jsonl",
10 split_ratio: float = 0.95 10 split_ratio: float = 0.95,
11 balance_ratio: float = 1.0
11): 12):
12 """ 13 """
13 Reads a .jsonl file, shuffles it, and splits it into training and validation files. 14 Reads a .jsonl file, balances the positive/negative examples, shuffles,
15 and splits it into training and validation files.
14 """ 16 """
15 print(f"Loading data from {input_filepath}...") 17 print(f"Loading data from {input_filepath}...")
16 try: 18 try:
@@ -20,23 +22,51 @@ def split_dataset(
20 print(f"Error: Input file not found at {input_filepath}", file=sys.stderr) 22 print(f"Error: Input file not found at {input_filepath}", file=sys.stderr)
21 sys.exit(1) 23 sys.exit(1)
22 24
23 # Shuffle the dataset to ensure random distribution 25 # 1. Separate into positive and negative examples
24 print("Shuffling data...") 26 positive_examples = []
25 random.shuffle(lines) 27 negative_examples = []
28 for line in lines:
29 try:
30 entry = json.loads(line)
31 if entry.get("target", "").strip():
32 positive_examples.append(line)
33 else:
34 negative_examples.append(line)
35 except json.JSONDecodeError:
36 print(f"Warning: Skipping malformed JSON line: {line.strip()}", file=sys.stderr)
26 37
27 # Determine the split point 38 print(f"Found {len(positive_examples)} positive examples and {len(negative_examples)} negative examples.")
28 split_index = int(len(lines) * split_ratio)
29 39
30 # Split the data 40 # 2. Shuffle both lists independently
31 train_lines = lines[:split_index] 41 random.shuffle(positive_examples)
32 validation_lines = lines[split_index:] 42 random.shuffle(negative_examples)
43
44 # 3. Balance the dataset based on the ratio
45 num_positives = len(positive_examples)
46 num_negatives_to_keep = int(num_positives * balance_ratio)
47
48 if len(negative_examples) < num_negatives_to_keep:
49 print(f"Warning: Not enough negative examples to meet a {balance_ratio}:1 ratio. "
50 f"Using all {len(negative_examples)} negative examples.", file=sys.stderr)
51 num_negatives_to_keep = len(negative_examples)
52
53 print(f"Balancing dataset with {num_positives} positive and {num_negatives_to_keep} negative examples.")
54
55 balanced_lines = positive_examples + negative_examples[:num_negatives_to_keep]
56
57 # 4. Final shuffle to mix positive and negative examples
58 print("Shuffling final balanced dataset...")
59 random.shuffle(balanced_lines)
60
61 # 5. Determine the split point and write files
62 split_index = int(len(balanced_lines) * split_ratio)
63 train_lines = balanced_lines[:split_index]
64 validation_lines = balanced_lines[split_index:]
33 65
34 # Write the training file
35 print(f"Writing {len(train_lines)} lines to {train_filepath}...") 66 print(f"Writing {len(train_lines)} lines to {train_filepath}...")
36 with open(train_filepath, 'w', encoding='utf-8') as f: 67 with open(train_filepath, 'w', encoding='utf-8') as f:
37 f.writelines(train_lines) 68 f.writelines(train_lines)
38 69
39 # Write the validation file
40 print(f"Writing {len(validation_lines)} lines to {validation_filepath}...") 70 print(f"Writing {len(validation_lines)} lines to {validation_filepath}...")
41 with open(validation_filepath, 'w', encoding='utf-8') as f: 71 with open(validation_filepath, 'w', encoding='utf-8') as f:
42 f.writelines(validation_lines) 72 f.writelines(validation_lines)
@@ -44,7 +74,9 @@ def split_dataset(
44 print("\nSplitting complete.") 74 print("\nSplitting complete.")
45 75
46if __name__ == "__main__": 76if __name__ == "__main__":
47 parser = argparse.ArgumentParser(description="Split a .jsonl dataset into training and validation sets.") 77 parser = argparse.ArgumentParser(
78 description="Balance and split a .jsonl dataset into training and validation sets."
79 )
48 parser.add_argument("input_file", type=str, help="Path to the input .jsonl file.") 80 parser.add_argument("input_file", type=str, help="Path to the input .jsonl file.")
49 parser.add_argument( 81 parser.add_argument(
50 "--split", 82 "--split",
@@ -52,6 +84,12 @@ if __name__ == "__main__":
52 default=0.95, 84 default=0.95,
53 help="Ratio for the training set split (e.g., 0.95 for a 95/5 split). Default is 0.95." 85 help="Ratio for the training set split (e.g., 0.95 for a 95/5 split). Default is 0.95."
54 ) 86 )
87 parser.add_argument(
88 "--ratio",
89 type=float,
90 default=1.0,
91 help="Ratio of negative to positive examples (e.g., 1.0 for 1:1, 2.0 for 2:1). Default is 1.0."
92 )
55 93
56 args = parser.parse_args() 94 args = parser.parse_args()
57 95
@@ -59,4 +97,4 @@ if __name__ == "__main__":
59 print("Error: Split ratio must be between 0 and 1.", file=sys.stderr) 97 print("Error: Split ratio must be between 0 and 1.", file=sys.stderr)
60 sys.exit(1) 98 sys.exit(1)
61 99
62 split_dataset(args.input_file, split_ratio=args.split) \ No newline at end of file 100 split_dataset(args.input_file, split_ratio=args.split, balance_ratio=args.ratio) \ No newline at end of file