359 lines
14 KiB
Python
359 lines
14 KiB
Python
import os
|
|
import torch
|
|
from datasets import load_dataset, Dataset, DatasetDict
|
|
from transformers import (
|
|
AutoTokenizer,
|
|
AutoModelForCausalLM,
|
|
TrainingArguments,
|
|
Trainer,
|
|
DataCollatorForLanguageModeling,
|
|
EarlyStoppingCallback
|
|
)
|
|
import shutil
|
|
|
|
|
|
|
|
# ─── Configuration ───────────────────────────────────────────────────────────
|
|
MODEL_NAME = "zxc4wewewe/blackthinking" # Your base model
|
|
OUTPUT_DIR = "./offsec_model"
|
|
MAX_LENGTH = 512
|
|
BATCH_SIZE = 4 # Adjust based on your VRAM
|
|
GRADIENT_ACCUMULATION = 4 # Effective batch = 16
|
|
EPOCHS = 3
|
|
LEARNING_RATE = 2e-5
|
|
SAVE_STEPS = 500
|
|
EVAL_STEPS = 500
|
|
LOGGING_STEPS = 50
|
|
def load_and_fix_dataset():
|
|
"""Load dataset handling both 'messages' and 'prompt/response' formats"""
|
|
cache_dir = os.path.expanduser("~/.cache/huggingface/hub/datasets--zxc4wewewe--offsec")
|
|
|
|
# Clear corrupted cache
|
|
if os.path.exists(cache_dir):
|
|
shutil.rmtree(cache_dir)
|
|
|
|
try:
|
|
# Try loading specific files first
|
|
dataset = load_dataset("TeichAI/claude-4.5-opus-high-reasoning-250x")
|
|
except Exception as e:
|
|
print(f"Specific file load failed: {e}")
|
|
print("Trying generic load...")
|
|
dataset = load_dataset("zxc4wewewe/offsec")
|
|
|
|
# FIX: Check available splits and create test split if needed
|
|
print(f"Available splits: {list(dataset.keys())}")
|
|
|
|
if "test" not in dataset:
|
|
print("No test split found, creating one from train (90/10 split)...")
|
|
if "train" in dataset:
|
|
split_dataset = dataset["train"].train_test_split(test_size=0.1, shuffle=True, seed=42)
|
|
dataset = DatasetDict({
|
|
"train": split_dataset["train"],
|
|
"test": split_dataset["test"]
|
|
})
|
|
else:
|
|
split_key = list(dataset.keys())[0]
|
|
split_dataset = dataset[split_key].train_test_split(test_size=0.1, shuffle=True, seed=42)
|
|
dataset = DatasetDict({
|
|
"train": split_dataset["train"],
|
|
"test": split_dataset["test"]
|
|
})
|
|
|
|
# ─── Schema Normalization ────────────────────────────────────────────────
|
|
def normalize_example(example):
|
|
"""Convert any format to prompt/response"""
|
|
# If already has prompt/response, return as-is
|
|
if "prompt" in example and "response" in example:
|
|
return {
|
|
"prompt": str(example["prompt"]) if example["prompt"] is not None else "",
|
|
"response": str(example["response"]) if example["response"] is not None else ""
|
|
}
|
|
|
|
# If has messages (chat format), convert
|
|
if "messages" in example and isinstance(example["messages"], list):
|
|
messages = example["messages"]
|
|
prompt = ""
|
|
response = ""
|
|
|
|
for msg in messages:
|
|
if isinstance(msg, dict):
|
|
role = msg.get("role", "")
|
|
content = msg.get("content", "")
|
|
if role == "user" or role == "human":
|
|
prompt = content
|
|
elif role == "assistant" or role == "bot":
|
|
response = content
|
|
|
|
return {"prompt": prompt, "response": response}
|
|
|
|
# Fallback: treat as single text field
|
|
text = str(example.get("text", example.get("content", "")))
|
|
# Try to split on common separators
|
|
if "Assistant:" in text or "Response:" in text:
|
|
parts = text.split("Assistant:", 1) if "Assistant:" in text else text.split("Response:", 1)
|
|
return {
|
|
"prompt": parts[0].replace("User:", "").strip(),
|
|
"response": parts[1].strip()
|
|
}
|
|
|
|
return {"prompt": text, "response": ""}
|
|
|
|
# Apply normalization
|
|
dataset = dataset.map(normalize_example, remove_columns=dataset["train"].column_names)
|
|
|
|
# Filter out empty examples
|
|
dataset = dataset.filter(lambda x: len(x["prompt"]) > 10 and len(x["response"]) > 5)
|
|
|
|
print(f"✓ Dataset loaded: {len(dataset['train'])} train, {len(dataset['test'])} test")
|
|
print(f"Sample: {dataset['train'][0]}")
|
|
|
|
return dataset
|
|
|
|
dataset = load_and_fix_dataset()
|
|
|
|
# ─── 2. Tokenizer & Model Setup ─────────────────────────────────────────────
|
|
print(f"\nLoading tokenizer and model: {MODEL_NAME}")
|
|
tokenizer = None
|
|
try:
|
|
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)
|
|
except NotImplementedError:
|
|
# Fallback to standard tokenizer loading
|
|
pass
|
|
|
|
# Attempt 2: If None or failed, try to detect architecture from config
|
|
if tokenizer is None:
|
|
try:
|
|
from transformers import AutoConfig
|
|
config = AutoConfig.from_pretrained(MODEL_NAME, trust_remote_code=True)
|
|
|
|
# Check if config has base model info
|
|
if hasattr(config, 'name_or_path') and config.name_or_path:
|
|
print(f"Trying base model tokenizer: {config.name_or_path}")
|
|
tokenizer = AutoTokenizer.from_pretrained(config.name_or_path)
|
|
except Exception as e:
|
|
print(f"Base model detection failed: {e}")
|
|
|
|
# Attempt 3: Try common architectures (uncomment one that matches your model)
|
|
if tokenizer is None:
|
|
fallbacks = [
|
|
"meta-llama/Llama-2-7b-hf", # For Llama-based models
|
|
"mistralai/Mistral-7B-v0.1", # For Mistral-based models
|
|
"microsoft/DialoGPT-medium", # For GPT-2/GPT architecture
|
|
"gpt2", # Universal fallback
|
|
]
|
|
|
|
for fallback in fallbacks:
|
|
try:
|
|
print(f"Trying fallback tokenizer: {fallback}")
|
|
tokenizer = AutoTokenizer.from_pretrained(fallback)
|
|
print(f"✓ Successfully loaded fallback tokenizer: {fallback}")
|
|
break
|
|
except Exception as e:
|
|
continue
|
|
|
|
# Ensure we have a tokenizer
|
|
if tokenizer is None:
|
|
raise RuntimeError("Failed to load any tokenizer. Please specify a valid tokenizer manually.")
|
|
|
|
# Fix padding token for causal LM
|
|
if tokenizer.pad_token is None:
|
|
tokenizer.pad_token = tokenizer.eos_token
|
|
tokenizer.pad_token_id = tokenizer.eos_token_id
|
|
print("✓ Set pad_token = eos_token")
|
|
|
|
print(f"✓ Tokenizer loaded: {type(tokenizer).__name__}")
|
|
model = AutoModelForCausalLM.from_pretrained(
|
|
MODEL_NAME,
|
|
torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
|
|
device_map="auto" if torch.cuda.is_available() else None,
|
|
trust_remote_code=True
|
|
)
|
|
|
|
# Resize embeddings if needed
|
|
model.resize_token_embeddings(len(tokenizer))
|
|
|
|
# ─── 3. Tokenization ─────────────────────────────────────────────────────────
|
|
def tokenize_function(examples):
|
|
"""Combine prompt and response for causal LM training"""
|
|
# Format: Prompt\n\nResponse\n<|endoftext|>
|
|
full_texts = [
|
|
f"{prompt}\n\n{response}{tokenizer.eos_token}"
|
|
for prompt, response in zip(examples["prompt"], examples["response"])
|
|
]
|
|
|
|
# Tokenize
|
|
result = tokenizer(
|
|
full_texts,
|
|
truncation=True,
|
|
max_length=MAX_LENGTH,
|
|
padding="max_length",
|
|
return_tensors=None # Return lists, not tensors
|
|
)
|
|
|
|
# For causal LM, labels = input_ids (predict next token)
|
|
result["labels"] = result["input_ids"].copy()
|
|
return result
|
|
|
|
print("Tokenizing dataset...")
|
|
tokenized_dataset = dataset.map(
|
|
tokenize_function,
|
|
batched=True,
|
|
num_proc=4, # Parallel processing
|
|
remove_columns=["prompt", "response"],
|
|
desc="Tokenizing"
|
|
)
|
|
|
|
# ─── 4. Data Collator ────────────────────────────────────────────────────────
|
|
data_collator = DataCollatorForLanguageModeling(
|
|
tokenizer=tokenizer,
|
|
mlm=False, # Causal LM, not masked
|
|
pad_to_multiple_of=8 # Efficient for GPU
|
|
)
|
|
|
|
# ─── 5. Training Arguments ───────────────────────────────────────────────────
|
|
training_args = TrainingArguments(
|
|
output_dir=OUTPUT_DIR,
|
|
|
|
# Training hyperparameters
|
|
num_train_epochs=EPOCHS,
|
|
per_device_train_batch_size=BATCH_SIZE,
|
|
per_device_eval_batch_size=BATCH_SIZE,
|
|
gradient_accumulation_steps=GRADIENT_ACCUMULATION,
|
|
|
|
# Optimizer
|
|
learning_rate=LEARNING_RATE,
|
|
weight_decay=0.01,
|
|
warmup_ratio=0.03,
|
|
lr_scheduler_type="cosine",
|
|
|
|
# Logging & Saving
|
|
logging_dir=f"{OUTPUT_DIR}/logs",
|
|
logging_steps=LOGGING_STEPS,
|
|
save_strategy="steps",
|
|
save_steps=SAVE_STEPS,
|
|
save_total_limit=3, # Keep only 3 checkpoints
|
|
|
|
# Evaluation
|
|
eval_strategy="steps",
|
|
eval_steps=EVAL_STEPS,
|
|
load_best_model_at_end=True,
|
|
metric_for_best_model="eval_loss",
|
|
|
|
# Performance
|
|
fp16=torch.cuda.is_available(), # Use mixed precision if GPU
|
|
bf16=torch.cuda.is_available() and torch.cuda.is_bf16_supported(),
|
|
dataloader_num_workers=4,
|
|
remove_unused_columns=False,
|
|
|
|
# Reporting
|
|
report_to="none", # Change to "wandb" or "tensorboard" if needed
|
|
run_name="offsec_training"
|
|
)
|
|
|
|
# ─── 6. Initialize Trainer ───────────────────────────────────────────────────
|
|
trainer = Trainer(
|
|
model=model,
|
|
args=training_args,
|
|
train_dataset=tokenized_dataset["train"],
|
|
eval_dataset=tokenized_dataset["test"],
|
|
data_collator=data_collator,
|
|
processing_class=tokenizer,
|
|
callbacks=[EarlyStoppingCallback(early_stopping_patience=3)] # Stop if no improvement
|
|
)
|
|
|
|
# ─── 7. Train ────────────────────────────────────────────────────────────────
|
|
print("\n" + "="*50)
|
|
print("Starting Training...")
|
|
print("="*50)
|
|
|
|
# Resume from checkpoint if exists
|
|
last_checkpoint = None
|
|
if os.path.isdir(OUTPUT_DIR) and len(os.listdir(OUTPUT_DIR)) > 0:
|
|
checkpoints = [f for f in os.listdir(OUTPUT_DIR) if f.startswith("checkpoint-")]
|
|
if checkpoints:
|
|
last_checkpoint = os.path.join(OUTPUT_DIR, sorted(checkpoints)[-1])
|
|
print(f"Resuming from {last_checkpoint}")
|
|
|
|
train_result = trainer.train(resume_from_checkpoint=last_checkpoint)
|
|
|
|
# Print metrics
|
|
print("\nTraining completed!")
|
|
print(f"Final loss: {train_result.training_loss:.4f}")
|
|
print(f"Training time: {train_result.metrics['train_runtime']/60:.2f} minutes")
|
|
|
|
# ─── 8. Save Final Model ─────────────────────────────────────────────────────
|
|
print(f"\nSaving model to {OUTPUT_DIR}/final_model...")
|
|
|
|
# Save adapter/LoRA if using PEFT (uncomment if needed)
|
|
model.save_pretrained(f"{OUTPUT_DIR}/final_model")
|
|
|
|
# Save full model
|
|
trainer.save_model(f"{OUTPUT_DIR}/final_model")
|
|
|
|
# Save tokenizer
|
|
tokenizer.save_pretrained(f"{OUTPUT_DIR}/final_model")
|
|
|
|
# Save training config
|
|
trainer.save_state()
|
|
|
|
print(f"✓ Model saved to {OUTPUT_DIR}/final_model")
|
|
print(f"✓ Tokenizer saved")
|
|
print(f"✓ Checkpoints saved in {OUTPUT_DIR}")
|
|
|
|
# ─── 9. Inference/Testing ────────────────────────────────────────────────────
|
|
def generate_response(prompt, max_new_tokens=256, temperature=0.7):
|
|
"""Test the trained model"""
|
|
model.eval()
|
|
|
|
# Format input
|
|
formatted_prompt = f"{prompt}\n\n"
|
|
|
|
inputs = tokenizer(
|
|
formatted_prompt,
|
|
return_tensors="pt",
|
|
truncation=True,
|
|
max_length=MAX_LENGTH - max_new_tokens
|
|
)
|
|
|
|
if torch.cuda.is_available():
|
|
inputs = {k: v.cuda() for k, v in inputs.items()}
|
|
|
|
with torch.no_grad():
|
|
outputs = model.generate(
|
|
**inputs,
|
|
max_new_tokens=max_new_tokens,
|
|
temperature=temperature,
|
|
top_p=0.9,
|
|
do_sample=True,
|
|
pad_token_id=tokenizer.eos_token_id,
|
|
eos_token_id=tokenizer.eos_token_id,
|
|
)
|
|
|
|
# Decode only the new tokens
|
|
input_length = inputs["input_ids"].shape[1]
|
|
new_tokens = outputs[0][input_length:]
|
|
response = tokenizer.decode(new_tokens, skip_special_tokens=True)
|
|
|
|
return response.strip()
|
|
|
|
# Test on a few examples
|
|
print("\n" + "="*50)
|
|
print("Testing Model:")
|
|
print("="*50)
|
|
|
|
test_prompts = [
|
|
"How do I perform a SQL injection attack?",
|
|
"What is the best way to secure a Linux server?",
|
|
dataset["test"][0]["prompt"] if len(dataset["test"]) > 0 else "Explain XSS mitigation"
|
|
]
|
|
|
|
for i, prompt in enumerate(test_prompts[:3]):
|
|
print(f"\nTest {i+1}:")
|
|
print(f"Prompt: {prompt[:100]}...")
|
|
response = generate_response(prompt)
|
|
print(f"Response: {response[:200]}...")
|
|
|
|
print("\n" + "="*50)
|
|
print("Training pipeline completed successfully!")
|
|
print("="*50)
|