Spaces:
Runtime error
Runtime error
File size: 11,448 Bytes
e55f958 a59479e e55f958 a59479e e55f958 a59479e e55f958 a59479e e55f958 a59479e e55f958 a59479e e55f958 | 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 | import torch
import torch.nn as nn
from transformers import (
GPT2LMHeadModel,
AutoTokenizer,
RobertaForSequenceClassification,
GPT2Config,
RobertaConfig,
DataCollatorForLanguageModeling
)
from datasets import load_dataset
from torch.utils.data import DataLoader
from accelerate import Accelerator
import logging
# --- Configuration ---
# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Constants
GPT2_MODEL_NAME = "gpt2"
ROBERTA_MODEL_NAME = "roberta-base"
SEQUENCE_LENGTH = 128
BATCH_SIZE = 8
NUM_EPOCHS = 3
LEARNING_RATE_G = 1e-5 # Lower LR for generation models
LEARNING_RATE_D = 5e-5 # Higher LR for classification models
D_STEPS = 1 # Number of discriminator updates per generator update
G_STEPS = 1 # Number of generator updates per batch
# --- 1. Discriminator Wrapper Class ---
# We wrap RoBERTa to make it function as a binary classifier (0: Fake, 1: Real)
class Discriminator(nn.Module):
def __init__(self, model_name):
super().__init__()
# RoBERTa is loaded for sequence classification with 2 labels (real/fake)
self.roberta = RobertaForSequenceClassification.from_pretrained(model_name, num_labels=2)
def forward(self, input_ids, attention_mask=None, labels=None):
# The RoBERTa model outputs a SequenceClassifierOutput
output = self.roberta(
input_ids=input_ids,
attention_mask=attention_mask,
labels=labels
)
# We only need the logits for the GAN loss calculation
return output.logits
# --- 2. Tokenizer Initialization (Kept Global for Data Preprocessing) ---
# Initialize the tokenizer
tokenizer = AutoTokenizer.from_pretrained(GPT2_MODEL_NAME)
# GPT-2 does not have a native padding token, so we set the EOS token as the pad token
# This is crucial for batching and RoBERTa's input structure
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
# --- 3. Data Preprocessing ---
# This function requires the tokenizer to be available globally
def preprocess_function(examples):
# Tokenize the dataset
return tokenizer(examples["text"], max_length=SEQUENCE_LENGTH, truncation=True, padding="max_length")
def load_and_prepare_data():
# Load a dataset of real text (e.g., IMDB reviews)
raw_datasets = load_dataset("imdb", split="train[:5%]")
# Select only the 'text' column for language modeling
processed_datasets = raw_datasets.map(
preprocess_function,
batched=True,
remove_columns=raw_datasets.column_names,
)
# Convert to PyTorch tensors and prepare for DataLoader
processed_datasets.set_format(type="torch", columns=["input_ids", "attention_mask"])
# Simple data collator for padding
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
# Create DataLoader
dataloader = DataLoader(
processed_datasets,
shuffle=True,
collate_fn=data_collator, # The data collator will handle batching and masking
batch_size=BATCH_SIZE
)
return dataloader
# --- 4. Adversarial Training Function ---
def train_gan():
# Initialize Generator and Discriminator models LOCALLY to ensure correct scope
logger.info(f"Loading Generator ({GPT2_MODEL_NAME}) and Discriminator ({ROBERTA_MODEL_NAME}) inside train_gan...")
generator = GPT2LMHeadModel.from_pretrained(GPT2_MODEL_NAME)
discriminator = Discriminator(ROBERTA_MODEL_NAME)
# Initialize Accelerator for mixed-precision and distributed training handling
accelerator = Accelerator()
dataloader = load_and_prepare_data()
# Define optimizers
# These references now correctly point to the locally defined 'generator' and 'discriminator'
optimizer_g = torch.optim.AdamW(generator.parameters(), lr=LEARNING_RATE_G)
optimizer_d = torch.optim.AdamW(discriminator.parameters(), lr=LEARNING_RATE_D)
# Move models and optimizers to the appropriate device
generator, optimizer_g, discriminator, optimizer_d, dataloader = accelerator.prepare(
generator, optimizer_g, discriminator, optimizer_d, dataloader
)
# Define Loss Function: Binary Cross-Entropy with Logits
# Since RoBERTa is outputting logits (unscaled scores), BCEWithLogitsLoss is the correct, stable choice.
loss_fn = nn.BCEWithLogitsLoss()
logger.info("Starting adversarial training loop...")
# Set models to training mode
generator.train()
discriminator.train()
for epoch in range(NUM_EPOCHS):
for step, batch in enumerate(dataloader):
# --- DISCRIMINATOR TRAINING STEP (D_STEPS times) ---
for _ in range(D_STEPS):
optimizer_d.zero_grad()
# 1. Process REAL Data
real_input_ids = batch['input_ids']
real_attention_mask = batch['attention_mask']
# Target: 1 (Real)
real_labels = torch.ones(real_input_ids.size(0), 1).to(accelerator.device)
# Get discriminator prediction for real data
# We classify the full sequence (CLS token's output is used by RoBERTa's classification head)
real_logits = discriminator(real_input_ids, attention_mask=real_attention_mask)
real_loss = loss_fn(real_logits[:, 1].unsqueeze(-1), real_labels) # Use logit for label 1 (Real)
# 2. Process FAKE (Generated) Data
# Generate text using GPT-2. We use 'no_grad' since we don't want to calculate
# gradients for the generator during the D step.
with torch.no_grad():
# Generate text. 'max_length' ensures the generated text is the same size as real data.
generated_ids = generator.generate(
real_input_ids,
max_length=SEQUENCE_LENGTH,
do_sample=True,
top_k=50,
top_p=0.95,
pad_token_id=tokenizer.eos_token_id
)
# Get generated text and attention mask
fake_input_ids = generated_ids
# RoBERTa's tokenizer automatically handles attention masking based on the pad token ID
fake_attention_mask = (fake_input_ids != tokenizer.pad_token_id).int()
# Target: 0 (Fake)
fake_labels = torch.zeros(fake_input_ids.size(0), 1).to(accelerator.device)
# Get discriminator prediction for fake data
fake_logits = discriminator(fake_input_ids, attention_mask=fake_attention_mask)
# Use logit for label 1 (Real) but target is 0 (Fake).
fake_loss = loss_fn(fake_logits[:, 1].unsqueeze(-1), fake_labels)
# 3. Total Discriminator Loss and Update
d_loss = real_loss + fake_loss
# Backpropagate and update
accelerator.backward(d_loss)
optimizer_d.step()
# --- GENERATOR TRAINING STEP (G_STEPS times) ---
# Generator aims to make D classify its output as REAL (target 1)
for _ in range(G_STEPS):
optimizer_g.zero_grad()
# Generate new fake data for the G step
# We need gradients for this step, so no 'no_grad()'
generated_ids = generator.generate(
real_input_ids,
max_length=SEQUENCE_LENGTH,
do_sample=True,
top_k=50,
top_p=0.95,
pad_token_id=tokenizer.eos_token_id
)
fake_input_ids = generated_ids
fake_attention_mask = (fake_input_ids != tokenizer.pad_token_id).int()
# Target for Generator: 1 (it wants the discriminator to think the text is Real)
generator_target_labels = torch.ones(fake_input_ids.size(0), 1).to(accelerator.device)
# Get discriminator prediction for the generated text
# We detach the discriminator's forward pass to prevent gradient updates to D during G step
discriminator_logits = discriminator(fake_input_ids.detach(), attention_mask=fake_attention_mask.detach())
# Generator Loss: BCE loss where the target is 1 (Real)
# The generator is being updated to minimize this loss, meaning its output
# should drive the discriminator's output closer to 1.
g_loss = loss_fn(discriminator_logits[:, 1].unsqueeze(-1), generator_target_labels)
# Backpropagate and update
accelerator.backward(g_loss)
optimizer_g.step()
# --- Logging and Reporting ---
if (step + 1) % 50 == 0:
# Calculate Discriminator Accuracy for monitoring
# Predictions are based on which logit is higher (0 or 1)
d_real_preds = (real_logits[:, 1] > real_logits[:, 0]).float().mean()
d_fake_preds = (fake_logits[:, 1] < fake_logits[:, 0]).float().mean()
d_accuracy = (d_real_preds + d_fake_preds) / 2
# G's success (how often D thinks the fake is real)
g_success_rate = (discriminator_logits[:, 1] > discriminator_logits[:, 0]).float().mean()
logger.info(
f"Epoch {epoch+1}/{NUM_EPOCHS}, Step {step+1}/{len(dataloader)} | "
f"D Loss: {d_loss.item():.4f}, G Loss: {g_loss.item():.4f} | "
f"D Acc: {d_accuracy.item():.2f} | G Success: {g_success_rate.item():.2f}"
)
# --- End of Epoch ---
logger.info(f"--- Epoch {epoch+1} finished. Generating sample text. ---")
# Simple evaluation by generating text
generator.eval()
prompt = "Finetuning large language models in an adversarial setting is"
input_ids = tokenizer.encode(prompt, return_tensors="pt").to(accelerator.device)
sample_output = generator.generate(
input_ids,
max_length=50,
num_return_sequences=1,
do_sample=True,
top_k=50,
top_p=0.95,
pad_token_id=tokenizer.eos_token_id
)
decoded_output = tokenizer.decode(sample_output[0], skip_special_tokens=True)
logger.info(f"Sample Output: {decoded_output}")
generator.train()
# Save the fine-tuned Generator model
accelerator.wait_for_everyone()
unwrapped_generator = accelerator.unwrap_model(generator)
unwrapped_generator.save_pretrained("./finetuned_gpt2_gan_generator")
logger.info("Fine-tuning complete. Generator saved to ./finetuned_gpt2_gan_generator")
if __name__ == "__main__":
# Note: To run this script, you typically need to use the 'accelerate launch' command:
# accelerate launch your_script_name.py
# Since this is a self-contained script in this environment, we call the function directly.
train_gan() |