""" Detector de Imágenes IA vs. Reales Pontificia Universidad Javeriana — Técnicas de Aprendizaje de Máquina Proyecto de Aplicación 2 — HuggingFace Spaces """ import os import numpy as np from PIL import Image import tensorflow as tf import gradio as gr # ── Configuración ────────────────────────────────────────────────────────── IMG_SIZE = 64 MODEL_PATH = "mejor_modelo_ai_vs_real.keras" # ── Cargar modelo ────────────────────────────────────────────────────────── try: modelo = tf.keras.models.load_model(MODEL_PATH) MODEL_OK = True except Exception as e: print(f"[ERROR] {e}") modelo = None MODEL_OK = False # ── Predicción ───────────────────────────────────────────────────────────── def predecir(imagen_np): if not MODEL_OK or modelo is None: return {"Error: modelo no disponible": 1.0}, "Modelo no cargado", "" if imagen_np is None: return {"Sin imagen": 1.0}, "Sube una imagen primero", "" img = Image.fromarray(imagen_np.astype("uint8")).convert("RGB") img = img.resize((IMG_SIZE, IMG_SIZE), Image.LANCZOS) arr = np.array(img, dtype=np.float32) / 255.0 arr = np.expand_dims(arr, axis=0) prob_real = float(modelo.predict(arr, verbose=0)[0][0]) prob_ai = 1.0 - prob_real confianza = max(prob_ai, prob_real) * 100 probs = { "IA-Generada": round(prob_ai, 4), "Real": round(prob_real, 4), } if prob_ai >= 0.80: veredicto = f"Imagen generada por IA — confianza {confianza:.1f}%" elif prob_ai >= 0.60: veredicto = f"Probablemente generada por IA — confianza {confianza:.1f}%" elif prob_ai >= 0.40: veredicto = f"Resultado incierto — zona de borde de decisión" elif prob_real >= 0.60: veredicto = f"Probablemente real — confianza {confianza:.1f}%" else: veredicto = f"Imagen real — confianza {confianza:.1f}%" detalle = ( f"P(IA-Generada) = {prob_ai*100:.2f}%\n" f"P(Real) = {prob_real*100:.2f}%\n" f"Confianza = {confianza:.1f}%\n" f"Resolución = {IMG_SIZE}×{IMG_SIZE} px" ) return probs, veredicto, detalle # ── CSS ──────────────────────────────────────────────────────────────────── css = """ @import url('https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=DM+Mono:wght@300;400;500&family=DM+Sans:wght@300;400;500&display=swap'); * { box-sizing: border-box; } body, .gradio-container { font-family: 'DM Sans', sans-serif !important; background: #0a0a0f !important; } gradio-app { background: #0a0a0f !important; } .gradio-container { max-width: 1100px !important; margin: 0 auto !important; padding: 0 24px !important; } /* Header */ .site-header { border-bottom: 1px solid #1e1e2e; padding: 32px 0 28px; margin-bottom: 36px; } .site-header h1 { font-family: 'DM Serif Display', serif; font-size: 2.1rem; color: #f0f0f5; margin: 0 0 6px; letter-spacing: -0.02em; font-weight: 400; } .site-header p { font-family: 'DM Mono', monospace; font-size: 0.72rem; color: #4a4a6a; margin: 0; letter-spacing: 0.08em; text-transform: uppercase; } /* Tabs */ .tab-nav { border-bottom: 1px solid #1e1e2e !important; background: transparent !important; margin-bottom: 28px; } .tab-nav button { font-family: 'DM Mono', monospace !important; font-size: 0.72rem !important; letter-spacing: 0.06em !important; text-transform: uppercase !important; color: #4a4a6a !important; background: transparent !important; border: none !important; border-bottom: 2px solid transparent !important; padding: 10px 18px !important; margin-bottom: -1px !important; transition: color 0.2s, border-color 0.2s !important; } .tab-nav button.selected { color: #c8c8e8 !important; border-bottom-color: #6060c0 !important; } .tab-nav button:hover { color: #a0a0c0 !important; } /* Inputs & outputs */ .gr-input, .gr-output, input, textarea, .gr-box { background: #111120 !important; border: 1px solid #1e1e2e !important; border-radius: 8px !important; color: #d0d0e8 !important; font-family: 'DM Sans', sans-serif !important; } label span, .gr-label { font-family: 'DM Mono', monospace !important; font-size: 0.7rem !important; letter-spacing: 0.07em !important; text-transform: uppercase !important; color: #4a4a6a !important; } /* Button */ button.primary { background: #6060c0 !important; border: none !important; border-radius: 6px !important; font-family: 'DM Mono', monospace !important; font-size: 0.75rem !important; letter-spacing: 0.08em !important; text-transform: uppercase !important; color: #fff !important; padding: 10px 24px !important; transition: background 0.2s !important; cursor: pointer !important; } button.primary:hover { background: #7070d8 !important; } /* Section titles */ .section-title { font-family: 'DM Serif Display', serif; font-size: 1.15rem; color: #c8c8e8; margin: 0 0 16px; font-weight: 400; letter-spacing: -0.01em; } /* Cards */ .card { background: #111120; border: 1px solid #1e1e2e; border-radius: 10px; padding: 20px 24px; margin-bottom: 16px; } /* Tables */ .data-table { width: 100%; border-collapse: collapse; font-size: 0.83rem; color: #c0c0d8; } .data-table th { font-family: 'DM Mono', monospace; font-size: 0.68rem; letter-spacing: 0.07em; text-transform: uppercase; color: #4a4a6a; padding: 8px 12px; text-align: left; border-bottom: 1px solid #1e1e2e; font-weight: 400; } .data-table td { padding: 10px 12px; border-bottom: 1px solid #16162a; vertical-align: top; line-height: 1.5; } .data-table tr:last-child td { border-bottom: none; } .data-table tr:hover td { background: #16162a; } .winner-row td { background: rgba(96, 96, 192, 0.08) !important; } .badge { display: inline-block; font-family: 'DM Mono', monospace; font-size: 0.62rem; letter-spacing: 0.06em; text-transform: uppercase; padding: 2px 8px; border-radius: 4px; background: rgba(96, 96, 192, 0.2); color: #9090d8; border: 1px solid rgba(96, 96, 192, 0.3); margin-left: 8px; vertical-align: middle; } /* Variable grid */ .var-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-top: 4px; } .var-card { background: #0d0d1a; border: 1px solid #1e1e2e; border-radius: 8px; padding: 14px 12px; text-align: center; transition: border-color 0.2s; } .var-card:hover { border-color: #3a3a6a; } .var-val { font-family: 'DM Mono', monospace; font-size: 1.25rem; font-weight: 500; color: #e0e0f5; letter-spacing: -0.01em; } .var-lbl { font-family: 'DM Mono', monospace; font-size: 0.62rem; letter-spacing: 0.07em; text-transform: uppercase; color: #3a3a5a; margin-top: 5px; } .var-card.accent-blue .var-val { color: #7878e0; } .var-card.accent-violet .var-val { color: #a060c8; } .var-card.accent-green .var-val { color: #40b080; } /* Category labels */ .cat-label { font-family: 'DM Mono', monospace; font-size: 0.65rem; letter-spacing: 0.08em; text-transform: uppercase; color: #3a3a5a; margin: 18px 0 8px; } /* Verdict output */ .verdict-box textarea { font-family: 'DM Serif Display', serif !important; font-size: 1.05rem !important; color: #d0d0f0 !important; font-style: italic !important; letter-spacing: 0.01em !important; } /* Info note */ .note { font-family: 'DM Mono', monospace; font-size: 0.7rem; color: #3a3a5a; letter-spacing: 0.04em; margin-top: 8px; } /* Divider */ hr { border: none; border-top: 1px solid #1e1e2e; margin: 28px 0; } footer { display: none !important; } """ # ── HTML blocks ──────────────────────────────────────────────────────────── header_html = """
Pontificia Universidad Javeriana · Técnicas de Aprendizaje de Máquina · 2026
Arquitecturas Comparadas
| Modelo | Tipo | Parámetros | Características clave |
|---|---|---|---|
| CNN Scratch | Capas Convolucionales + Densas | ~800 K | 3 bloques Conv → BN → MaxPool → Dropout, cabeza Dense(256) |
| EfficientNetB0mejor modelo | Transfer Learning — ImageNet | ~5.3 M | Base congelada fase 1 → fine-tuning últimas 20 capas fase 2 |
| ViT Small | Vision Transformer | ~2.5 M | Patch 8×8, 64 patches, 4 bloques Transformer, EMBED_DIM 128 |
Resultados — Test Set
Actualizar con los valores reales tras ejecutar el notebook en Colab.
| Modelo | Accuracy | Precision | Recall | F1-Score | AUC-ROC |
|---|---|---|---|---|---|
| CNN Scratch | — | — | — | — | — |
| EfficientNetB0mejor | — | — | — | — | — |
| ViT Small | — | — | — | — | — |
Hiperparámetros de Entrenamiento
| Parámetro | CNN Scratch | EfficientNetB0 | ViT Small |
|---|---|---|---|
| Optimizador | Adam | Adam | Adam |
| Learning Rate | 1e-3 | 1e-4 → 1e-5 | 1e-4 |
| Batch Size | 32 | 32 | 32 |
| Épocas máx. | 30 | 10 + 10 (fases) | 30 |
| Loss | Binary Cross-Entropy | Binary Cross-Entropy | Binary Cross-Entropy |
| Dropout | 0.25 – 0.40 | 0.30 | 0.10 – 0.30 |
| EarlyStopping | patience=5, monitor=val_loss, restore_best_weights=True | ||
| ReduceLROnPlateau | factor=0.5, patience=3, min_lr=1e-6 | ||
| Class weights | Balanceado via sklearn.compute_class_weight | ||
Dataset
| Fuente | Parveshiiii/AI-vs-Real — HuggingFace Datasets |
| Total imágenes | 13 999 |
| Distribución | ~75% IA-Generada / ~25% Real — desbalanceado |
| División | 80% train+val / 20% test — estratificado |
| Resolución entrada | 64×64 px — normalizado [0, 1] |
| Data augmentation | Flip horizontal, rotación ±10%, zoom ±10% |
| Pipeline | tf.data.Dataset con prefetch y augmentation en GPU |
Variables del Sistema
Generales
Vision Transformer
Callbacks
Sobre el Proyecto
| Problema | Clasificación binaria supervisada: imagen IA-Generada vs. imagen Real |
| Motivación | Los generadores de imagen por IA producen imágenes indistinguibles para el ojo humano, generando riesgos de desinformación, deepfakes y fraude digital |
| Autores | Javier Felipe Aldana Jaramillo · Integrante 2 · Integrante 3 · Integrante 4 |
| Curso | Técnicas de Aprendizaje de Máquina — PUJ Bogotá · 2026 |