fast.ai · Practical Deep Learning for Coders · Lección 2
Deployment: de un dataset propio a una app desplegada
La lección que cumple la promesa de la Lección 1: armar tu propio dataset desde cero, entrenar, interpretar errores, limpiar datos con ayuda del propio modelo, y desplegar. El proceso completo, de punta a punta.
Antes de leerlección 1
🎯 Al terminar vas a poder
- Desarmar la física de
DataBlock: decodificación y normalización en CPU a tensores homogéneos vs. matrices afines de data augmentation aceleradas en GPU. - Demostrar matemáticamente por qué la pérdida de Cross-Entropy () actúa como un detector automático de anomalías para identificar ruido de etiquetas mediante
plot_top_losses. - Distinguir con rigor formal Covariate Shift ( cambia), Concept Drift ( cambia) y bucles de retroalimentación (Model Collapse en LLMs), implementando rollouts en fase sombra.
- Diseccionar qué serializa
learn.export()(pesos, grafo, estadísticas de normalización y vocabulario) y explicar por qué en inferencia con la CPU supera en latencia y costo a una GPU.
1. El pipeline completo: de la búsqueda de imágenes al modelo
El ejemplo del libro es un clasificador de osos (grizzly / negro / teddy), armado con imágenes buscadas y descargadas por código — no un dataset ya empaquetado y limpio como el de la Lección 1. El pipeline:
from fastai.vision.all import *
from fastbook import search_images_ddg # pip install fastbook
bear_types = 'grizzly', 'black', 'teddy'
path = Path('bears')
# 1. Buscar y descargar (150 imágenes por clase alcanza), una carpeta por clase
for o in bear_types:
dest = path/o
dest.mkdir(exist_ok=True, parents=True)
urls = search_images_ddg(f'{o} bear', max_images=150)
download_images(dest, urls=urls)
# 2. Verificar y descartar archivos corruptos
fns = get_image_files(path)
failed = verify_images(fns)
failed.map(Path.unlink)
print(f'{len(failed)} archivos rotos borrados')
🟢 Actualización 2026: DuckDuckGo en vez de Bing
El libro impreso usa search_images_bing(key, 'grizzly bear') con una key de Azure
Cognitive Services. Esa API fue retirada por Microsoft (el retiro de las Bing Search APIs se
concretó en 2025), así que el código original directamente no corre. El curso vigente (2022) y la
versión actual de los notebooks de fastbook usan DuckDuckGo, que no pide key:
search_images_ddg del paquete fastbook, o directamente el paquete
duckduckgo_search como en el notebook "Is it a bird?" de Kaggle:
from duckduckgo_search import DDGS # pip install duckduckgo_search
def search_images(term, max_images=200):
print(f"Buscando '{term}'")
with DDGS() as ddgs:
return L(ddgs.images(term, max_results=max_images)).itemgot('image')
urls = search_images('grizzly bear photos', max_images=150)
Ese paquete cambió de API más de una vez (la función ddg_images del notebook original fue
reemplazada por DDGS().images, y más tarde el proyecto pasó a publicarse con el nombre
ddgs). Si el import falla, mirá el README del paquete instalado: la idea es la misma, una
lista de URLs de imágenes a partir de un término de búsqueda.
Con menos de 150 imágenes por clase, sin dataset curado de antemano, ya alcanza para entrenar algo útil — el punto es que armar tu propio dataset desde cero no es el cuello de botella que parece. Un consejo del propio libro que sigue vigente: buscá términos variados por clase ("grizzly bear", "grizzly bear shadow", "grizzly bear sun") para que el dataset tenga la variedad que después va a aparecer en producción.
2. La DataBlock API, pieza por pieza
DataBlock es la pieza central de fastai: una plantilla que obliga a responder cuatro
preguntas explícitas sobre cualquier dataset, en vez de escribir un Dataset/DataLoader
de PyTorch a mano cada vez:
bears = DataBlock(
blocks=(ImageBlock, CategoryBlock),
get_items=get_image_files,
splitter=RandomSplitter(valid_pct=0.2, seed=42),
get_y=parent_label,
item_tfms=Resize(128))
dls = bears.dataloaders(path)
dls.valid.show_batch(max_n=4, nrows=1)
blocks: define los tipos tensoriales de la variable independiente y dependiente (aquí,ImageBlockproduce tensores de imagen flotantes,CategoryBlockproduce enteros codificados One-Hot o Cross-Entropy targets).get_items: la función de pipeline para indexar los archivos en disco.splitter: separación determinista de Train/Validation fijando una semilla (seed) para garantizar reproducibilidad exacta.get_y: la función de extracción de etiquetas (por ejemplo, el nombre de la carpeta contenedora conparent_label).
La física del pipeline: CPU vs. GPU
El diseño de DataBlock separa estrictamente dos etapas de cómputo con características de hardware opuestas:
-
Etapa CPU (Ítem por ítem —
item_tfms):
En el disco, los archivos JPEG/PNG son secuencias comprimidas de bytes con dimensiones heterogéneas (, , etc.). La GPU no puede procesar tensores con formas variables: su arquitectura SIMD (Single Instruction, Multiple Data) requiere que todos los elementos del lote compartan exactamente las mismas dimensiones de memoria. Por eso, la CPU decodifica cada archivo comprimido, lo convierte en un arreglo RGB y ejecutaResizeoRandomResizedCrop(224). Recién cuando todas las matrices tienen tamaño uniforme , se apilan en memoria RAM fijada (*pinned host memory*) para formar el tensor homogéneo . -
Etapa GPU (Lote completo —
batch_tfms):
Una vez transferido el tensor a la VRAM mediante DMA, se ejecuta el Data Augmentation (rotación, traslación, escala, perspectiva, brillo). En vez de procesar píxel por píxel con loops de CPU, la GPU formula cada transformación como una matriz afín tridimensional . Mediante interpolación bilineal paralelizada en hardware, los núcleos tensoriales recalculan las coordenadas de los millones de píxeles del batch en una fracción de milisegundo. Por esta razón física, hacer data augmentation en GPU es computacionalmente gratuito y no frena el training loop.
bears = bears.new(
item_tfms=RandomResizedCrop(224, min_scale=0.5),
batch_tfms=aug_transforms())
dls = bears.dataloaders(path)
dls.train.show_batch(max_n=8, nrows=2, unique=True) # la misma imagen, 8 augmentations
DataBlock es una plantilla que obliga a contestar cuatro preguntas y después arma el
pipeline sola. El orden importa: las transformaciones por ítem corren en CPU para poder apilar un
batch rectangular; las del batch corren en GPU sobre el tensor entero, por eso la augmentation
casi no cuesta.
3. Limpiar datos usando el propio modelo
La secuencia del libro invierte el orden intuitivo: en vez de limpiar todos los datos a mano antes de entrenar, entrená un modelo simple primero y usalo para encontrar los problemas del dataset.
learn = vision_learner(dls, resnet18, metrics=error_rate)
learn.fine_tune(4)
interp = ClassificationInterpretation.from_learner(learn)
interp.plot_confusion_matrix()
interp.plot_top_losses(5, nrows=1)
from fastai.vision.widgets import *
cleaner = ImageClassifierCleaner(learn)
cleaner # el widget: elegís categoría y split, marcás cada imagen
# después de marcar: aplicar lo decidido
for idx in cleaner.delete(): cleaner.fns[idx].unlink()
for idx, cat in cleaner.change(): shutil.move(str(cleaner.fns[idx]), path/cat)
La matemática del Loss como detector de anomalías
¿Por qué un modelo todavía imperfecto es el mejor inspector de datos del mundo? La respuesta está en la función de pérdida de Cross-Entropy Loss:
Supongamos que en el dataset descargado de internet, una foto de un oso grizzly salvaje en un río fue etiquetada por error
como teddy (oso de peluche). Durante el forward pass, el backbone convolucional extrae bordes de garras, textura de pelaje mojado
y colmillos; la cabeza proyecta esos rasgos y el Softmax asigna a la clase real asignada una probabilidad ínfima:
.
Al calcular la pérdida:
Una imagen normal bien clasificada tiene .
¡El ejemplo mal etiquetado genera una pérdida 400 veces mayor! Como el optimizador busca minimizar el error promedio,
el modelo no memoriza casos aislados contradictorios si la mayoría de los datos son coherentes; en cambio, los "escupe" con un loss descomunal.
interp.plot_top_losses() simplemente ordena la matriz de validación por descendente,
haciendo que el ruido de etiquetas y las imágenes corruptas floten instantáneamente a la superficie.
4. Out-of-domain data, domain shift y feedback loops
Una sección entera del capítulo se llama "How to Avoid Disaster", y su argumento es una verdad física ineludible: ningún modelo generaliza a distribuciones que violan las hipótesis estadísticas de su entrenamiento. En estadística y machine learning existen tres patologías formales que debés saber distinguir:
-
Covariate Shift (Cambio en las covariables):
Ocurre cuando la distribución de las entradas cambia, pero la función objetivo subyacente se mantiene: , pero es idéntico. Por ejemplo: entrenaste clasificando fotos de stock nítidas y centradas de día, y en producción la cámara de seguridad envía video infrarrojo granulado de noche. Un oso sigue siendo un oso, pero la variedad geométrica y los valores de píxeles caen fuera del soporte sobre el cual se calcularon las convoluciones. -
Concept Drift (Deriva del concepto):
La relación causal entre la entrada y la salida cambia con el tiempo: . Por ejemplo: un clasificador de transacciones fraudulentas o un modelo de scoring financiero. Los patrones que hace un año indicaban solvencia o fraude hoy son usados deliberadamente por actores adversarios para evadir la detección. -
Feedback Loops (Bucles de retroalimentación autodestructivos):
Cuando las salidas emitidas por el modelo en el tiempo condicionan o contaminan los datos que recolectás en el tiempo . En LLMs, esto se conoce hoy como Model Collapse: si entrenás un modelo usando texto generado sintéticamente por otros LLMs, la cola de la distribución de probabilidad se recorta, la varianza de la atención colapsa y el modelo degenera en textos repetitivos e incoherentes.
La receta de mitigación en producción es siempre el Rollout Gradual:
- Fase Sombra (Shadow Deployment): El modelo corre en paralelo sobre tráfico real pero sus predicciones no ejecutan acciones; solo se registran métricas.
- Inspección Humana (Human-in-the-loop): Un operador revisa las predicciones donde la confianza del Softmax sea limítrofe ().
- Monitoreo de Deriva: Alarmas automáticas que miden la divergencia de Kullback-Leibler (KL) o la distancia de Wasserstein entre los embeddings de producción y los del dataset de validación.
🔧 En tu proyecto
Tu gate anti-alucinación para artículos financieros es un caso de libro de domain shift: el lenguaje de los artículos, los nombres de empresas y los eventos macroeconómicos cambian mes a mes. Un umbral de similitud o perplejidad calibrado en el primer trimestre queda desfasado al siguiente. El monitoreo continuo de la perplejidad promedio de entrada es el sensor que te avisa cuándo reentrenar.
5. Cómo se servía en 2020 — y cómo se sirve hoy
El libro exporta el modelo completo (arquitectura + pesos + la definición del DataLoaders,
incluidas las transforms) a un único archivo con learn.export(), y para inferencia usa
load_learner(...) + predict. Hasta ahí, el flujo sigue vigente sin cambios:
# en el notebook de entrenamiento
learn.export('export.pkl') # pickle con modelo + DataLoaders vacíos + transforms
# en otro proceso, sin GPU, sin el dataset
from fastai.vision.all import *
learn_inf = load_learner('export.pkl')
learn_inf.predict('images/grizzly.jpg')
learn_inf.dls.vocab # ['black', 'grizzly', 'teddy']
('grizzly', tensor(1), tensor([1.0e-06, 1.0e+00, 1.5e-07]))Qué empaca realmente learn.export()
El archivo export.pkl no es un simple volcado de pesos; es un paquete autocontenido que resuelve
todo el contrato de inferencia:
- State Dict de Parámetros: Las matrices de pesos y sesgos aprendidas durante el fine-tuning.
- Grafo de la Arquitectura: La definición de capas de PyTorch (Backbone ResNet + Head lineal adaptado).
- Estadísticas de Normalización: Los vectores de media y desviación estándar () con los que se normalizan los canales RGB: . Si un modelo recibiera píxeles en sin esta normalización exacta, los logits saldrían completamente degradados.
- Vocabulario (
dls.vocab): La lista ordenada de strings que mapea el índice entero delargmaxa la etiqueta legible.
La física de la inferencia: por qué CPU supera a GPU con batch=1
Existe la creencia errónea de que producción siempre requiere GPUs costosas. La física de hardware dicta lo contrario para servicios de usuario:
- Entrenamiento (): La GPU domina porque miles de núcleos ALUs multiplican matrices en paralelo saturando los TFLOPs de la tarjeta.
- Inferencia individual (): Cada llamada a la GPU paga un peaje de latencia fija:
- Copia de memoria Host-to-Device (RAM a VRAM vía bus PCIe: ~1–3 ms).
- Overhead de lanzamiento de kernels CUDA en el driver (~5–10 por cada una de las 50 capas).
- Sincronización y copia Device-to-Host para devolver el resultado a Python.
Un procesador moderno (x86 con instrucciones AVX-512/AMX o ARM NEON) procesa un ResNet-18 para una sola imagen en 5 a 12 milisegundos directamente en memoria caché L2/L3, sin peaje PCIe ni reserva de VRAM. Para clasificaciones visuales o extracción de features con tráfico moderado, servir en CPU es órdenes de magnitud más económico y ofrece menor latencia de cola (P99).
🟢 De fastai a producción extrema: TorchScript y ONNX Runtime
En sistemas industriales con miles de requests por segundo, se elimina el runtime de Python por completo:
- TorchScript (
torch.jit.trace): Convierte el modelo de PyTorch a un grafo C++ estático sin dependencias de Python. - ONNX Runtime: Formato abierto de intercambio que compila las operaciones matriciales a instrucciones nativas de hardware optimizadas para cada CPU/NPU.
6. El app.py de Gradio para Hugging Face Spaces
Un Space de Gradio es un repositorio git con tres archivos: el modelo exportado, un
requirements.txt y un app.py. Este es el app.py completo para el
clasificador de la Lección 1 (gato/perro), con la función custom incluida por lo que dice el callout de
arriba:
# app.py
from fastai.vision.all import *
import gradio as gr
def is_cat(x): return x[0].isupper() # la misma función que usó el DataBlock al exportar
learn = load_learner('export.pkl')
labels = learn.dls.vocab # [False, True] → los mostramos con nombre
names = {False: 'perro', True: 'gato'}
def predict(img):
pred, idx, probs = learn.predict(PILImage.create(img))
return dict(zip([names[l] for l in labels], map(float, probs)))
demo = gr.Interface(
fn=predict,
inputs=gr.Image(type='pil'),
outputs=gr.Label(num_top_classes=2),
examples=['gato.jpg', 'perro.jpg'],
title='¿Gato o perro?')
demo.launch()
# requirements.txt (gradio ya viene con el SDK del Space; fastai trae torch)
fastai
Tres detalles que explican por qué está escrito así. gr.Label espera un diccionario
{etiqueta: probabilidad} con floats de Python — por eso el
dict(zip(labels, map(float, probs))): probs es un tensor y hay que convertir cada
elemento. gr.Image(type='pil') le entrega a tu función una imagen PIL, que
PILImage.create convierte al tipo que fastai espera. Y demo.launch() en un Space
levanta el servidor en el puerto que HF espera; en tu máquina abre http://127.0.0.1:7860.
Para publicar: creá el Space (SDK: Gradio), cloná el repo, copiá export.pkl,
app.py, requirements.txt y las imágenes de ejemplo, y hacé push — el Space
instala y arranca solo. Si export.pkl pesa más de unos MB, activá git-lfs para ese archivo
(git lfs track "*.pkl").
⚠️ Versiones
El pickle guarda referencias a clases de fastai y de PyTorch; si el Space instala una versión muy
distinta de la que usaste al exportar, la carga puede fallar. Cuando pase, fijá en
requirements.txt las versiones que imprimió fastai.__version__ /
torch.__version__ en el notebook donde entrenaste. Y como el Space es CPU, el
load_learner ya deja el modelo en CPU por defecto (cpu=True): no hace falta
ningún .cuda(), y si lo agregás, rompe.
Lo que el libro proponía con ipywidgets — un botón de upload, una salida con la predicción — es
exactamente lo que gr.Interface genera solo a partir de la firma de predict. La
API de Gradio de esta página es la de las versiones 4 y 5 (gr.Image, gr.Label
como componentes de primer nivel); si ves gr.inputs.Image(shape=...) en algún tutorial viejo,
es la API anterior a 2023.
7. Errores comunes
Los problemas de esta lección casi nunca aparecen en el entrenamiento — aparecen en los datos, o recién cuando el modelo sale del notebook.
| Síntoma | Causa probable | Qué hacer |
|---|---|---|
dataloaders() o la primera época explotan con UnidentifiedImageError / OSError: image file is truncated. | Descargaste con download_images y no corriste verify_images: hay archivos vacíos, HTML disfrazado de jpg, o descargas cortadas. | failed = verify_images(get_image_files(path)); failed.map(Path.unlink) siempre, justo después de descargar. |
| Accuracy alta pero la matriz de confusión muestra que una clase casi nunca se predice. | Clases desbalanceadas: 150 grizzlies, 150 negros, 30 teddies. El modelo aprende que "casi nunca es teddy" y la accuracy global lo premia. | Mirá dls.train.items por clase antes de entrenar. Bajá a cantidades parecidas, buscá más imágenes de la clase chica, o mirá precision/recall por clase con interp.plot_confusion_matrix() en vez de la accuracy global. |
| En el notebook anda perfecto; con fotos del celular de un usuario real se equivoca seguido. | Out-of-domain data: entrenaste con fotos de stock bien iluminadas y centradas; producción trae ángulos raros, noche, fondo distinto. | Variá los términos de búsqueda al armar el dataset, agregá augmentations, y sobre todo: rollout gradual con humano revisando (§4). Recolectá los fallos reales y sumalos al dataset. |
load_learner falla con AttributeError: Can't get attribute 'is_cat'. | Exportaste con una función de etiquetado custom y el script de inferencia no la define. | Definí la función con el mismo nombre en el script que carga (ver §5). O usá parent_label y organizá el dataset por carpetas. |
| Un Space o servidor de inferencia con GPU que cuesta plata y está el 99% del tiempo ocioso. | Reflejo de "deep learning = GPU". Para predecir una imagen a la vez, la GPU no suma nada. | Serví en CPU. La GPU se justifica en entrenamiento (paralelismo sobre batches grandes) o en inferencia con mucho throughput, no para un demo. |
| El Space arranca y muere con un error de deserialización del pickle. | Versión de fastai/torch en el Space distinta de la del notebook donde exportaste. | Fijá versiones en requirements.txt (§6). Si el modelo es viejo, reexportalo desde un entorno actual. |
| Las augmentations tardan una eternidad por época. | Pusiste aug_transforms() en item_tfms (CPU, imagen por imagen) en vez de batch_tfms (GPU, batch entero). | item_tfms solo para llevar todo al mismo tamaño; las augmentations van en batch_tfms. |
8. Resumen
- Armar tu propio dataset desde cero (buscar con DuckDuckGo, descargar, verificar) no es el cuello de botella que parece — 150 imágenes por clase ya alcanzan para un modelo útil. La API de Bing del libro ya no existe.
- DataBlock formaliza cuatro preguntas (blocks, get_items, splitter, get_y) en vez de escribir Dataset/DataLoader a mano;
item_tfmsunifica tamaños en CPU,batch_tfmshace augmentation en GPU. - La secuencia correcta es entrenar primero, después usar
plot_top_lossesy el cleaner para encontrar problemas en los datos — no limpiar todo a ciegas antes. - Out-of-domain data, domain shift y feedback loops son la razón por la que ningún modelo se despliega de golpe a producción completa — rollout gradual con supervisión humana.
learn.export()+load_learnersiguen siendo el flujo de inferencia; el pickle no guarda tus funciones custom, así que el proceso que carga tiene que definirlas.- El stack de deployment del libro (Voilà/Binder) es historia; hoy es Hugging Face Spaces + Gradio con un
app.pyde 15 líneas, servido en CPU, exactamente lo que ya viste en el Capítulo 9 de HF.
9. Ponete a prueba
Respondé antes de abrir cada respuesta. Si fallás una, volvé a la sección indicada.
1. ¿Cuáles son las cuatro preguntas que responde un DataBlock, y con qué argumento cada una?
Qué tipos tienen entrada y salida (blocks), cómo listar los ítems (get_items), cómo separar train/valid (splitter) y cómo etiquetar cada ítem (get_y). Las transforms (item_tfms, batch_tfms) son extra, no una de las cuatro preguntas. (§2)
2. ¿Por qué el libro entrena antes de limpiar el dataset?
Porque un modelo entrenado con datos mayormente correctos aprende lo común y deja con loss alto a los ejemplos mal etiquetados o fuera de lugar: plot_top_losses y el cleaner los muestran directamente. Limpiar a mano 450 imágenes sin esa ayuda es más lento y menos sistemático. (§3)
3. ¿Qué diferencia hay entre out-of-domain data y domain shift?
Out-of-domain: datos de un tipo que el modelo nunca vio (osos de noche, en video, de espaldas). Domain shift: la distribución de producción cambia con el tiempo y va dejando obsoleto el training set (los clientes de la aseguradora). Ninguno se detecta de antemano con certeza; por eso el rollout gradual. (§4)
4. Exportaste con get_y=is_cat. En un script nuevo hacés load_learner('export.pkl') sin definir nada más. ¿Qué pasa?
AttributeError: Can't get attribute 'is_cat': pickle guardó el nombre y módulo de la función, no su código. Hay que definir is_cat en el script que carga, con el mismo nombre. (§5)
5. ¿Por qué predict en el app.py devuelve dict(zip(labels, map(float, probs))) y no probs directamente?
Porque gr.Label espera un diccionario etiqueta → probabilidad con floats de Python, y probs es un tensor de PyTorch. El map(float, ...) convierte cada elemento; el zip con el vocab le pone nombre a cada probabilidad. (§6)
6. ¿Qué pasa si ponés aug_transforms() en item_tfms en vez de batch_tfms?
Funciona, pero corre en CPU imagen por imagen, mucho más lento. batch_tfms aplica las augmentations sobre el batch entero en GPU, que es lo que las hace casi gratis. (§2, §7)
10. Para practicar
✏️ Ejercicio 1 — Tu propio dataset de tres clases
Elegí tres cosas que se confundan de verdad (tres razas de perro parecidas, tres tipos de hoja, tres
modelos de auto) y armá el pipeline completo de §1 a §3. Mirá la matriz de confusión y los top losses
antes y después de limpiar. Necesita internet para la búsqueda; entrenar con resnet18 y
Resize(128) es viable en CPU con ~100 imágenes por clase, aunque lento.
from fastai.vision.all import *
from duckduckgo_search import DDGS
def search_images(term, max_images=100):
with DDGS() as ddgs:
return L(ddgs.images(term, max_results=max_images)).itemgot('image')
path = Path('mi_dataset')
for o in ['husky', 'malamute', 'samoyed']:
(path/o).mkdir(parents=True, exist_ok=True)
download_images(path/o, urls=search_images(f'{o} dog photo'))
failed = verify_images(get_image_files(path)); failed.map(Path.unlink)
dls = DataBlock(blocks=(ImageBlock, CategoryBlock), get_items=get_image_files,
splitter=RandomSplitter(0.2, seed=42), get_y=parent_label,
item_tfms=RandomResizedCrop(128, min_scale=0.5),
batch_tfms=aug_transforms()).dataloaders(path, bs=32)
print({c: len((path/c).ls()) for c in dls.vocab}) # balance de clases
learn = vision_learner(dls, resnet18, metrics=error_rate)
learn.fine_tune(3)
interp = ClassificationInterpretation.from_learner(learn)
interp.plot_confusion_matrix(); interp.plot_top_losses(6, nrows=2)
Pista / qué esperar
Error_rate bastante peor que en gatos/perros (clases parecidas + dataset chico + imágenes de búsqueda con ruido). En los top losses vas a encontrar imágenes que no son perros, o que son de otra raza: eso es el dataset, no el modelo.
✏️ Ejercicio 2 — Romper y arreglar load_learner
Exportá cualquier learner entrenado con una función custom de etiquetado y cargalo desde un script separado, primero sin definir la función y después definiéndola. Corre en CPU en segundos.
# en el notebook, después de entrenar con get_y=is_cat (Lección 1)
learn.export('export.pkl')
# cargar.py — versión 1: sin is_cat
from fastai.vision.all import *
learn = load_learner('export.pkl') # → AttributeError: Can't get attribute 'is_cat'
# cargar.py — versión 2: con is_cat definida antes
from fastai.vision.all import *
def is_cat(x): return x[0].isupper()
learn = load_learner('export.pkl')
print(learn.dls.vocab, next(learn.model.parameters()).device)
print(learn.predict('gato.jpg'))
Pista / qué esperar
La versión 2 imprime el vocab, device(type='cpu') (aunque hayas entrenado en GPU) y la predicción. Ese es el script que después se convierte en app.py.
✏️ Ejercicio 3 — El app.py en local antes que en el Space
Con pip install gradio, guardá el app.py de §6 junto a tu export.pkl
y corrélo con python app.py. Abrí la URL local, subí una foto, y después probá una imagen
claramente fuera de dominio (un dibujo, una captura de pantalla). Recién cuando funcione en local,
subilo a un Space.
Pista / qué esperar
La interfaz muestra las dos barras de probabilidad. Con la imagen fuera de dominio vas a ver que el modelo igual se decide, a veces con alta confianza — ese es el argumento de §4 hecho visible, y una buena razón para agregar un umbral de confianza en predict antes de mostrar el resultado.
11. Fuentes y para seguir
- Practical Deep Learning for Coders — Lesson 2: Deployment — la lección en video (2022) donde Jeremy arma el dataset con DuckDuckGo y despliega en HF Spaces con Gradio; la versión vigente de este capítulo.
- fastbook — 02_production.ipynb — el Capítulo 2 como notebook, ya actualizado a
search_images_ddg; fuente del pipeline, DataBlock, cleaner y "How to Avoid Disaster". - Is it a bird? Creating a model from your own data — el notebook de Kaggle de la lección, con la función
search_imagessobreduckduckgo_searchlista para copiar. - Gradio documentation — referencia de
gr.Interface,gr.Image,gr.Label; leela cuando quieras pasar deInterfaceaBlocks. - Hugging Face Hub — Spaces — cómo crear un Space con SDK Gradio, hardware, secretos, git-lfs para archivos grandes.
- fastai — Learner (export y load_learner) — la documentación oficial de
Learner.exportyload_learner, incluido el parámetrocpu.