import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from datasets import load_dataset import open_clip from tqdm.auto import tqdm import os import random from PIL import Image from huggingface_hub import login from kaggle_secrets import UserSecretsClient try: user_secrets = UserSecretsClient() hf_token = user_secrets.get_secret("HF_TOKEN") login(token=hf_token) print("Successfully logged into Hugging Face") except Exception as e: print("Warning: Could not find HF_TOKEN in Kaggle Secrets. Proceeding anonymously") # config KAGLE_REAL_PATH = "/kaggle/input/datasets/matthewjansen/unsplash-lite-5k-colorization/train/color" HF_AI_DATASET = "Rapidata/Flux_SD3_MJ_Dalle_Human_Alignment_Dataset" SAVE_PATH = "/kaggle/working/openclip_forensic_head.pth" TARGET_SHARDS = ["train_0001", "train_0002", "train_0003", "train_0004"] # params BATCH_SIZE = 16 EPOCHS = 5 LR = 1e-4 DEVICE = "cuda" if torch.cuda.is_available() else "cpu" IMG_SIZE = (224, 224) # data loading with streaming print(f"Streaming {len(TARGET_SHARDS)} shards from Hugging Face") final_data = [] for shard in TARGET_SHARDS: print(f"Opening stream for {shard}") shard_stream = load_dataset(HF_AI_DATASET, split=shard, streaming=True) for item in tqdm(shard_stream, total=1000, desc=f"Streaming {shard}"): # Resize immediately to keep RAM usage low img = item["image1"].convert("RGB").resize(IMG_SIZE) final_data.append({ "image": img, "label": 1 }) num_ai_images = len(final_data) print(f"Total AI images collected: {num_ai_images}") print("Loading Real Images from Kaggle") real_images_list = [os.path.join(KAGLE_REAL_PATH, f) for f in os.listdir(KAGLE_REAL_PATH) if f.endswith(('.jpg', '.jpeg', '.png'))] random.shuffle(real_images_list) print(f"Balancing dataset with {num_ai_images} Real images") for i in tqdm(range(min(num_ai_images, len(real_images_list))), desc="Processing Real Images"): path = real_images_list[i] try: img = Image.open(path).convert("RGB").resize(IMG_SIZE) final_data.append({ "image": img, "label": 0 }) except Exception as e: continue # shuffle split random.seed(42) random.shuffle(final_data) split_idx = int(len(final_data) * 0.85) train_list = final_data[:split_idx] val_list = final_data[split_idx:] print(f"Dataset prepared: Train size = {len(train_list)}, Val size = {len(val_list)}") # model init print(f"Initializing ViT-L-14 on {DEVICE}") model, _, preprocess_val = open_clip.create_model_and_transforms( 'ViT-L-14', pretrained='datacomp_xl_s13b_b90k' ) model = model.to(DEVICE) # freeze backbone for param in model.parameters(): param.requires_grad = False print("Detecting feature dimensions...") with torch.no_grad(): dummy_input = torch.randn(1, 3, 224, 224).to(DEVICE) dummy_feature = model.encode_image(dummy_input) detected_dim = dummy_feature.shape[1] print(f"Backbone output dimension: {detected_dim}") class ForensicHead(nn.Module): def __init__(self, input_dim): super().__init__() self.net = nn.Sequential( nn.Linear(input_dim, 512), nn.ReLU(), nn.Dropout(0.3), nn.Linear(512, 1), nn.Sigmoid() ) def forward(self, x): return self.net(x) # Initialize head with detected dimension (768 for DataComp ViT-L-14) head = ForensicHead(input_dim=detected_dim).to(DEVICE) def collate_fn(batch): images = [preprocess_val(item['image']) for item in batch] labels = [item['label'] for item in batch] return torch.stack(images), torch.tensor(labels).float().view(-1, 1) train_loader = DataLoader(train_list, batch_size=BATCH_SIZE, shuffle=True, collate_fn=collate_fn) val_loader = DataLoader(val_list, batch_size=BATCH_SIZE, collate_fn=collate_fn) # training loop optimizer = optim.Adam(head.parameters(), lr=LR) criterion = nn.BCELoss() best_acc = 0.0 print(f"Starting training on {len(train_list)} images") for epoch in range(EPOCHS): head.train() train_pbar = tqdm(train_loader, desc=f"Epoch {epoch + 1}/{EPOCHS} [Train]") epoch_loss = 0 for imgs, lbls in train_pbar: imgs, lbls = imgs.to(DEVICE), lbls.to(DEVICE) with torch.no_grad(): features = model.encode_image(imgs) features /= features.norm(dim=-1, keepdim=True) optimizer.zero_grad() outputs = head(features) loss = criterion(outputs, lbls) loss.backward() optimizer.step() epoch_loss += loss.item() train_pbar.set_postfix(loss=f"{loss.item():.4f}") # validation head.eval() val_correct = 0 val_pbar = tqdm(val_loader, desc=f"Epoch {epoch + 1}/{EPOCHS} [Val]") with torch.no_grad(): for imgs, lbls in val_pbar: imgs, lbls = imgs.to(DEVICE), lbls.to(DEVICE) feat = model.encode_image(imgs) feat /= feat.norm(dim=-1, keepdim=True) preds = (head(feat) > 0.5).float() val_correct += (preds == lbls).sum().item() val_acc = val_correct / len(val_list) print(f"Epoch {epoch + 1} Results | Loss: {epoch_loss / len(train_loader):.4f} | Val Acc: {val_acc:.4f}") if val_acc > best_acc: best_acc = val_acc torch.save(head.state_dict(), SAVE_PATH) print(f"New best model saved with {val_acc:.4f} accuracy") print(f"Training complete. Model saved in: {SAVE_PATH}")