Qué queremos predecir, por qué importa y qué hay dentro de una resonancia
Universidad Alfonso X el Sabio · Tema 2
Antes de construir ninguna red neuronal hay que entender el problema médico. Este cuaderno es la introducción al caso y va de lo clínico a lo técnico:
| Parte | Qué vas a entender | Animación |
|---|---|---|
| 0 | El caso entero, de un vistazo | 🎬 de la paciente a la red, y de vuelta a la patología |
| 1 | Qué es el cáncer de mama y cómo se diagnostica | — |
| 2 | La quimioterapia antes de operar y qué es la pCR | 🎬 dos pacientes, mismo tratamiento |
| 3 | Por qué la pCR importa y qué dicen nuestros datos | 🎬 la pCR por subtipo |
| 4 | Cómo se lee una resonancia de mama y dónde está el tumor | 🎬 leer un corte, paso a paso |
| 5 | Qué son la intensidad y el tiempo en la imagen | 🎬 un píxel del tumor y uno normal; curvas |
| 6 | Qué es un corte y por qué hay muchos por paciente | 🎬 la mama vista de lado y sus cortes |
| 7 | El tratamiento de la imagen antes de la red | 🎬 del PNG al tensor |
| 8 | Los kernels sobre la resonancia | 🎬 kernels 3×3; 🎬 cómo 3 imágenes se convierten en 1 |
| 9 | Del bloque 1 al bloque 2 | 🎬 paso a paso; 🎬 qué pasa si cambia un filtro |
Cómo ver las animaciones 🎬: pulsa ▶, o usa los botones de paso para ir fotograma a fotograma. Cada una lleva antes una explicación de qué vas a ver y después un resumen de lo que hay que llevarse.
⚠️ Uso educativo. Este material explica un problema de investigación. No sirve para diagnosticar ni para decidir un tratamiento. Los esquemas clínicos están simplificados.
# Si falta algo, descomenta, ejecuta una vez y reinicia el kernel:
# %pip install numpy pandas matplotlib pillow scikit-learn scipy torch
# Datos (38.109 PNG, ~1,4 GB): !python ../alumnos/breastdcedl/descargar_datos.py
from pathlib import Path
import sys
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display, Image as ImagenArchivo
from PIL import Image
import torch
import torch.nn.functional as F
AQUI = next((p.resolve() for p in [Path.cwd(), Path.cwd() / 'ejemplo_clase',
Path.cwd() / 'breastdcedl-usecase/ejemplo_clase']
if (p / 'animaciones_clinica.py').exists()), None)
if AQUI is None:
raise FileNotFoundError('Abre el cuaderno desde la carpeta ejemplo_clase.')
SOL, RAIZ = AQUI.parent / 'solution', AQUI.parent / 'alumnos/breastdcedl'
for carpeta in (SOL, AQUI):
sys.path.insert(0, str(carpeta))
if not (RAIZ / 'metadata/samples.csv').exists():
raise FileNotFoundError(f'No encuentro los datos en {RAIZ}. Descárgalos antes de seguir.')
from datos import leer_indice, CortesMama
from dibujos import estilo
import animaciones as anim
import animaciones_clinica as clin
estilo()
print('Datos:', RAIZ)
Datos: …/breastdcedl-usecase\alumnos\breastdcedl
Para explicar la imagen usamos una paciente real del dataset en la que el tumor se ve con claridad: la hemos elegido por eso, para enseñar, no al azar. Leemos sus cortes y, en cada uno, localizamos de forma aproximada la zona que más capta contraste dentro de la mama (el dataset no trae el contorno exacto del tumor).
indice = leer_indice(RAIZ)
pacientes = pd.read_csv(RAIZ / 'metadata/patients.csv')
PACIENTE = 'ACRIN-6698-218503' # elegida porque el tumor se ve claro
cortes_p = indice[indice.patient_id == PACIENTE].sort_values('slice_index').reset_index(drop=True)
volumen = torch.stack([x for x, _ in CortesMama(cortes_p, RAIZ)]).numpy() # (N, 3, 256, 256)
alturas = cortes_p.slice_index.tolist()
medio = len(cortes_p) // 2
corte = volumen[medio] # (3, 256, 256): PRE, EARLY, LATE
info = clin.localizar_mama(corte)
zonas = [clin.localizar_mama(v)['zona'] for v in volumen]
ficha = pacientes.set_index('pid').loc[PACIENTE]
print(f'Paciente {PACIENTE}')
print(f' cohorte: {ficha.dataset} · edad: {ficha.age:.0f} · subtipo: {ficha.HR_HER2_STATUS} · pCR: {int(ficha.pCR)}')
print(f' {len(cortes_p)} cortes (alturas {alturas[0]} a {alturas[-1]}); cada corte = 3 imágenes PNG de 256×256')
Paciente ACRIN-6698-218503 cohorte: spy2 · edad: 52 · subtipo: HRposHER2neg · pCR: 0 10 cortes (alturas 5 a 14); cada corte = 3 imágenes PNG de 256×256
Esta ilustración, de la documentación del caso, resume la historia de dos pacientes: la resonancia del principio, la quimioterapia, la pieza quirúrgica analizada al microscopio y el resultado: respuesta completa (✔) o enfermedad residual (✘).
figura = RAIZ / 'documentation/figures/pcr_two_patient_paths.png'
if figura.exists():
display(ImagenArchivo(filename=str(figura), width=900))

Pulsa ▶. Cada viñeta se enciende con una frase arriba. Fíjate en la flecha verde del final: el reto es usar la imagen de antes (viñeta 2) para anticipar un resultado que solo se conoce meses después (viñeta 7).
clin.animar_mapa_del_caso(volumen, info)
La mama está formada sobre todo por grasa y por tejido fibroglandular: las glándulas que producen leche (lobulillos) y los conductos que la llevan al pezón. El cáncer de mama aparece cuando células de esos conductos o lobulillos crecen sin control. Si rompen la pared del conducto e invaden el tejido de alrededor, se habla de carcinoma invasivo; si siguen dentro, de carcinoma in situ.
Es el cáncer más frecuente en mujeres: aproximadamente 1 de cada 8 lo tendrá a lo largo de su vida.
| Paso | Qué se hace | Qué aporta |
|---|---|---|
| 1. Sospecha | mamografía de cribado, o un bulto que nota la paciente | una zona sospechosa |
| 2. Ecografía | se mira la zona con ultrasonidos | forma y tamaño; guía la aguja |
| 3. Biopsia | con una aguja se saca un cilindro de tejido y lo analiza el patólogo | confirma que es cáncer, su tipo y su subtipo |
| 4. Resonancia magnética | imágenes de las dos mamas con contraste | tamaño real, si hay más de un foco, cómo capta el contraste |
| 5. Estudio de extensión | otras pruebas, según el caso | si se ha extendido a otros órganos |
| 6. Comité | oncólogos, cirujanos, radiólogos, patólogos | deciden el tratamiento |
En la biopsia se mira si las células tienen ciertas «antenas» (receptores) en su superficie. Eso define el subtipo, que cambia el tratamiento y, como veremos, la probabilidad de responder:
| Subtipo | Qué significa | Tratamiento dirigido |
|---|---|---|
| HR+ / HER2− | tiene receptores hormonales (estrógeno, progesterona): crece con las hormonas | tratamiento hormonal |
| HER2+ | tiene muchos receptores HER2: crece deprisa | fármacos anti-HER2 |
| Triple negativo | no tiene ni receptores hormonales ni HER2 | no hay diana: quimioterapia |
En patients.csv están el subtipo de cada paciente (HR_HER2_STATUS) y su edad.
En muchas pacientes, sobre todo con tumores grandes o de subtipos agresivos (triple negativo, HER2+), el tratamiento no empieza por la cirugía, sino por la quimioterapia neoadyuvante: medicación antes de operar, durante varios meses. Tiene dos ventajas: puede reducir el tumor (y permitir una cirugía menor) y deja ver cómo responde el tumor al tratamiento.
Después se opera, y el patólogo analiza al microscopio todo el tejido extirpado, de la mama y de los ganglios de la axila. Ahí nace la etiqueta de nuestro problema:
pCR (respuesta patológica completa): tras la quimioterapia neoadyuvante, no queda carcinoma invasivo ni en la mama ni en los ganglios axilares (en nomenclatura patológica, ypT0/is ypN0; puede quedar carcinoma in situ).
Qué vas a ver: dos líneas de tiempo. El círculo oscuro dentro de la mama es el tumor. Durante los ciclos de quimioterapia encoge: en la paciente A desaparece; en la B no del todo. La flecha naranja marca cuándo queremos predecir (al diagnóstico) y la verde, cuándo se conoce la respuesta (tras la cirugía).
clin.animar_recorrido_paciente()
Lo que hay que llevarse: la pCR no se ve en la imagen del principio: es un resultado que se conoce meses después, al microscopio. Nuestra red intentará anticiparlo con la resonancia tomada antes del tratamiento.
1. Dice mucho del pronóstico. Las pacientes que alcanzan pCR tienen, en conjunto, mejor supervivencia libre de enfermedad y mejor supervivencia global que las que tienen enfermedad residual. La asociación es especialmente fuerte en los tumores triple negativos y HER2+, y más débil en los HR+/HER2− (análisis agrupado CTNeoBC, Cortazar et al., The Lancet, 2014).
2. Se usa para evaluar tratamientos. Se conoce en meses, no en años, así que se usa como objetivo en ensayos clínicos. I-SPY 2, de donde vienen muchas imágenes de este dataset, es un ensayo que usa la pCR para decidir qué fármacos avanzan.
3. Cambia lo que se hace después. Si queda enfermedad residual, en algunos subtipos se añade tratamiento tras la cirugía (por ejemplo, en los ensayos CREATE-X, con tumores HER2−, y KATHERINE, con HER2+, eso mejoró los resultados).
4. Por eso interesaría predecirla antes. Saber pronto qué paciente probablemente no responderá permitiría, en teoría, cambiar de estrategia a tiempo. Hoy es un problema de investigación: ningún modelo sustituye a la anatomía patológica.
Qué vas a ver: a la izquierda, 100 cuadraditos que representan a 100
pacientes; los verdes son las que alcanzan pCR. A la derecha, la proporción de
pCR por subtipo. Las cifras salen del dataset (patients.csv, parte de
entrenamiento), no de un libro.
train = pacientes[pacientes.split == 'train']
nombres = {'HER2pos': 'HER2 positivo', 'TripleNeg': 'Triple negativo', 'HRposHER2neg': 'HR+ / HER2−'}
por_subtipo = (train.groupby('HR_HER2_STATUS').pCR.agg(['count', 'mean']).reindex(list(nombres))
.rename(index=nombres))
display(por_subtipo.rename(columns={'count': 'pacientes', 'mean': 'proporción con pCR'}).round(3))
display(train.groupby('dataset').pCR.agg(['count', 'mean']).rename(
columns={'count': 'pacientes', 'mean': 'proporción con pCR'}).round(3))
clin.animar_pcr_en_datos(len(train), int(train.pCR.sum()),
[(n, int(f['count']), float(f['mean'])) for n, f in por_subtipo.iterrows()])
| pacientes | proporción con pCR | |
|---|---|---|
| HR_HER2_STATUS | ||
| HER2 positivo | 287 | 0.429 |
| Triple negativo | 354 | 0.379 |
| HR+ / HER2− | 453 | 0.141 |
| pacientes | proporción con pCR | |
|---|---|---|
| dataset | ||
| duke | 209 | 0.211 |
| spy1 | 104 | 0.250 |
| spy2 | 784 | 0.321 |
Lo que hay que llevarse:
spy1, spy2, duke) tienen proporciones distintas:
es un posible sesgo que habrá que vigilar.Antes de pedirle nada a una red, hay que saber qué hay en la imagen.
Qué vas a ver: el corte central de nuestra paciente y, a la derecha, una explicación que cambia en cada fotograma. Recorremos, en orden: qué tipo de imagen es, el aire, la piel, el lado del tórax, el interior de la mama, qué pasa al inyectar el contraste, dónde está el tumor y por qué no todo lo que brilla es tumor.
Es mejor verla fotograma a fotograma con los botones de paso.
clin.animar_leer_resonancia(corte, info)
Lo que hay que llevarse:
Intensidad es, simplemente, lo brillante que se ve un píxel. En nuestras imágenes es un número entre 0 (negro) y 1 (blanco). En resonancia, un tejido brilla más o menos según su composición y, sobre todo aquí, según cuánto contraste ha llegado a él.
La resonancia de mama con contraste (DCE-RM, dynamic contrast-enhanced) no es una foto: son varias fotos seguidas del mismo sitio.
Así que PRE, EARLY y LATE son el mismo corte en tres momentos. Lo que cambia de una a otra es cuánto brilla cada píxel con el paso del tiempo.
Qué vas a ver:
clin.animar_intensidad_tiempo(corte, info)
Lo que hay que llevarse: el tumor (A) sube mucho al llegar el contraste; el tejido normal (B) casi no cambia. Esa diferencia en el tiempo es la principal pista de la imagen. Por eso a la red le daremos las tres fases juntas.
(Si ves que A llega a 1,00, es porque la escala del dataset satura: 1 es el máximo brillo representable.)
Los radiólogos se fijan en cómo evoluciona la intensidad de una lesión. Hay tres formas típicas:
Qué vas a ver: a la izquierda, un esquema de las tres curvas dibujándose (no son datos de pacientes). A la derecha, lo que tenemos de verdad en el dataset para los puntos A y B: solo tres valores, PRE, EARLY y LATE. Con tres puntos no se dibuja la curva entera, pero sí se ve si sube, se mantiene o baja.
suave = np.stack([clin.ndi.uniform_filter(corte[j], 5) for j in range(3)])
reales = {'A · tumor': suave[:, info['centro'][0], info['centro'][1]],
'B · tejido normal': suave[:, info['normal'][0], info['normal'][1]]}
clin.animar_curvas_cineticas(reales)
La resonancia no es una imagen plana: es un volumen en 3D. El ordenador lo guarda como una pila de rebanadas horizontales, igual que un pan de molde cortado. Cada rebanada es un corte, y su posición es la altura z.
Qué vas a ver:
Mira cómo el tumor cambia de tamaño y de forma al subir y bajar.
clin.animar_volumen_cortes(volumen, alturas, zonas)
Lo que hay que llevarse: la red verá cortes sueltos, cada uno con sus 3 fases, pero la decisión final es por paciente. Por eso, más adelante, habrá que juntar las predicciones de todos sus cortes, y separar los datos por paciente, nunca por corte.
Las resonancias originales son volúmenes médicos con tamaños, resoluciones y escalas distintas según el hospital. Para este caso ya se han preparado así:
| Paso | Qué se hace | Por qué |
|---|---|---|
| 1 | leer los 3 PNG | vienen como números enteros de 0 a 255 |
| 2 | dividir entre 255 | números entre 0 y 1: la escala que prefiere la red |
| 3 | apilar las 3 fases | un tensor (3, 256, 256), siempre en el orden PRE, EARLY, LATE |
| 4 y 5 | mirar las restas | EARLY − PRE (realce) y EARLY − LATE (lavado): la información está ahí |
| 6 | voltear las 3 fases juntas | aumento de datos correcto |
| 7 | voltear solo una | el error típico: rompe la resta |
| 8 | reducir el tamaño | más rápido, pero se pierde detalle |
Qué vas a ver: 8 fotogramas, uno por paso de la tabla, con las imágenes antes y después y, en algunos, el histograma de valores. Fíjate sobre todo en el paso 7: aparecen «realces» fantasma que no existen.
fila = cortes_p.iloc[medio]
crudo = {f: np.asarray(Image.open(RAIZ / fila[f'path_{f.lower()}'])) for f in ['PRE', 'EARLY', 'LATE']}
pre, early, late = corte
realce_img, lavado_img = early - pre, early - late
early_128 = F.avg_pool2d(torch.tensor(early)[None, None], 2)[0, 0].numpy()
early_64 = F.avg_pool2d(torch.tensor(early)[None, None], 4)[0, 0].numpy()
pasos = [
dict(titulo='el PNG tal cual', hist=(crudo['EARLY'], 'valor del píxel en EARLY (0 a 255)'),
paneles=[(crudo[f], 'gray', 0, 255, f'{f}: enteros de 0 a 255') for f in ['PRE', 'EARLY', 'LATE']],
texto='Tres PNG en gris de 8 bits. Misma escala de brillo en las tres: se pueden comparar.'),
dict(titulo='dividir entre 255', hist=(early, 'valor del píxel en EARLY (0 a 1)'),
paneles=[(v, 'gray', 0, 1, f'{f}: de 0 a 1') for f, v in zip(['PRE', 'EARLY', 'LATE'], [pre, early, late])],
texto='Números pequeños y en la misma escala. A la vista la imagen no cambia: cambian los números.'),
dict(titulo='apilar las 3 fases: tensor (3, 256, 256)',
paneles=[(pre, 'gray', 0, 1, 'canal 0: PRE'), (early, 'gray', 0, 1, 'canal 1: EARLY'),
(late, 'gray', 0, 1, 'canal 2: LATE'), (corte.transpose(1, 2, 0), None, None, None, 'las 3 pintadas como R, G, B')],
texto='Canal = momento. El orden (PRE, EARLY, LATE) tiene que ser SIEMPRE el mismo en todas las pacientes.'),
dict(titulo='la resta EARLY − PRE: el realce', hist=(realce_img, 'EARLY − PRE'),
paneles=[(pre, 'gray', 0, 1, 'PRE'), (early, 'gray', 0, 1, 'EARLY'),
(realce_img, 'RdBu_r', -.6, .6, 'EARLY − PRE (rojo = sube)')],
texto='Lo que se ilumina al llegar el contraste: el tumor, y también el tórax.'),
dict(titulo='la resta EARLY − LATE: el lavado', hist=(lavado_img, 'EARLY − LATE'),
paneles=[(early, 'gray', 0, 1, 'EARLY'), (late, 'gray', 0, 1, 'LATE'),
(lavado_img, 'RdBu_r', -.4, .4, 'EARLY − LATE (rojo = baja después)')],
texto='Dónde la señal baja de EARLY a LATE: el patrón de lavado de la curva de tipo III.'),
dict(titulo='aumento de datos BIEN hecho: voltear las 3 fases juntas',
paneles=[(realce_img, 'RdBu_r', -.6, .6, 'realce original'),
(early[:, ::-1], 'gray', 0, 1, 'EARLY volteada'),
(early[:, ::-1] - pre[:, ::-1], 'RdBu_r', -.6, .6, 'realce con las 3 volteadas')],
texto='Una mama volteada sigue siendo una mama válida, y la resta sigue comparando el mismo tejido.'),
dict(titulo='aumento de datos MAL hecho: voltear solo EARLY',
paneles=[(early[:, ::-1], 'gray', 0, 1, 'EARLY volteada'), (pre, 'gray', 0, 1, 'PRE sin voltear'),
(early[:, ::-1] - pre, 'RdBu_r', -.6, .6, 'EARLY − PRE: ¡rota!')],
texto='Se restan tejidos distintos: aparecen realces fantasma. Por eso datos.py voltea las tres fases a la vez.'),
dict(titulo='reducir el tamaño: 256 → 128 → 64',
paneles=[(early, 'gray', 0, 1, '256×256'), (early_128, 'gray', 0, 1, '128×128'),
(early_64, 'gray', 0, 1, '64×64')],
texto='Entrena mucho más rápido, pero se pierde detalle: un tumor pequeño queda en muy pocos píxeles.'),
]
clin.animar_pasos(pasos)
Un kernel (o filtro) es una tablita de números, por ejemplo de 3×3. La convolución lo va poniendo encima de cada píxel de la imagen, multiplica cada número del kernel por el píxel que tiene debajo, suma los 9 productos y escribe el resultado en ese píxel de una imagen nueva.
El cálculo es siempre el mismo. Lo que cambia es el kernel, y con él lo que se resalta. Una red neuronal convolucional es, en el fondo, muchos kernels cuyos números aprende sola.
Qué vas a ver:
Pista para leerlo: si los 9 números suman 0, el kernel ignora el brillo y solo ve cambios (bordes), en rojo y azul según el signo. Si suman 1, la imagen se sigue pareciendo a la original.
FILTROS = {
'Identidad': ([[0, 0, 0], [0, 1, 0], [0, 0, 0]],
'Un 1 en el centro: cada píxel\nse queda igual. La imagen no cambia.'),
'Difuminar': ([[1 / 9] * 3] * 3,
'La media de 9 píxeles vecinos:\nmenos ruido, pero bordes borrosos.'),
'Bordes verticales': ([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]],
'Resta la izquierda y suma la derecha:\nmarca el contorno de la piel\ny del tumor en vertical.'),
'Bordes horizontales': ([[-1, -2, -1], [0, 0, 0], [1, 2, 1]],
'Resta arriba y suma abajo:\ncontornos en horizontal.'),
'Todos los bordes': ([[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]],
'El centro contra sus 8 vecinos:\ncualquier contorno. También realza\nel ruido.'),
'Realzar': ([[0, -1, 0], [-1, 5, -1], [0, -1, 0]],
'La imagen más sus bordes:\nmás nitidez.'),
}
anim.animar_filtros_foto(early, FILTROS)
En la red, la primera convolución no recibe una imagen, sino las 3 fases a la vez. ¿Cómo sale una imagen de tres? Así:
Por ejemplo, con −1 en la capa PRE, +1 en la capa EARLY y 0 en la capa LATE, el resultado es EARLY − PRE: el kernel ha aprendido a medir el cambio en el tiempo.
Qué vas a ver: el primer kernel se construye en 4 pasos (las fases, las capas, las 3 aportaciones y la suma), con las flechas y los signos + y =. Después pasan otros cinco kernels de golpe, con su explicación debajo.
def capas(pre=None, early=None, late=None):
"""Un kernel (3, 3, 3) a partir de una tabla 3×3 por fase (None = todo ceros)."""
cero = np.zeros((3, 3))
return np.stack([np.asarray(c if c is not None else cero, dtype=np.float32) for c in (pre, early, late)])
centro = np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]], dtype=np.float32)
media = np.ones((3, 3), dtype=np.float32) / 9
laplace = np.array([[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]], dtype=np.float32) / 8
FILTROS_3FASES = {
'realce: EARLY − PRE': (capas(pre=-centro, early=centro),
'Resta el antes del después: rojo donde la señal SUBE al llegar el contraste.'),
'solo PRE': (capas(pre=centro),
'Copia la fase PRE (las otras capas son ceros): la anatomía antes del contraste.'),
'media de las 3 fases': (capas(centro / 3, centro / 3, centro / 3),
'Promedia los tres momentos: se ve la mama, pero SE PIERDE el tiempo.'),
'realce suavizado': (capas(pre=-media, early=media),
'La misma resta, promediada en 3×3: menos ruido.'),
'lavado: EARLY − LATE': (capas(early=centro, late=-centro),
'Rojo donde la señal BAJA de EARLY a LATE: el patrón de lavado.'),
'bordes del realce': (capas(pre=-laplace, early=laplace),
'Bordes de la imagen EARLY − PRE: el contorno de lo que capta contraste.'),
}
clin.animar_filtros_3fases(corte, FILTROS_3FASES)
Lo que hay que llevarse: un kernel de la primera capa mezcla las tres fases en una imagen. Según sus 27 números puede copiar una fase, promediarlas (y perder el tiempo) o restarlas (y medir el cambio). Nadie elige esos números en la red de verdad: los aprende al entrenar.
Una red convolucional se organiza en bloques. Cada bloque tiene varios kernels, y cada kernel produce un mapa (una imagen que dice dónde encontró «lo suyo»). Lo importante:
El bloque 2 no ve la resonancia. Ve los mapas que salen del bloque 1, y los combina.
Para entenderlo sin perderse, construimos una red de juguete con kernels que ya conocemos:
| Bloque 1 | Qué detecta |
|---|---|
| filtro 0: realce EARLY − PRE | dónde sube la señal al llegar el contraste |
| filtro 1: bordes en EARLY | los contornos |
| filtro 2: lavado EARLY − LATE | dónde la señal baja después |
| filtro 3: brillo en EARLY | lo que es brillante, capte contraste o no |
Bloque 2: un solo filtro que suma los 4 mapas con estos pesos: +1,0 (realce), +0,6 (bordes), +0,8 (lavado) y −0,4 (brillo). En palabras: «zona que capta contraste, con borde y con lavado, que no sea simplemente brillante».
Qué vas a ver, en 8 fotogramas:
FILTROS_BLOQUE1 = [
('realce EARLY − PRE', capas(pre=-media, early=media), 'Positivo donde la señal SUBE al llegar el contraste.'),
('bordes en EARLY', capas(early=laplace), 'Positivo en los contornos, donde el brillo cambia de golpe.'),
('lavado EARLY − LATE', capas(early=media, late=-media), 'Positivo donde la señal BAJA de EARLY a LATE.'),
('brillo en EARLY', capas(early=media), 'Positivo donde EARLY es brillante, capte contraste o no.'),
]
PESOS_BLOQUE2 = [1.0, 0.6, 0.8, -0.4]
clin.animar_bloque1_a_bloque2(corte, FILTROS_BLOQUE1, PESOS_BLOQUE2)
Lo que hay que llevarse: la salida del bloque 2 brilla donde coinciden los mapas con peso positivo. Es una idea más compleja que la de cualquier filtro del bloque 1 por separado. Así es como una red construye, bloque a bloque, detectores cada vez más elaborados.
Ahora dejamos el bloque 2 igual y cambiamos solo el filtro 0 del bloque 1. Si el bloque 2 depende de lo que le llega, su salida debería cambiar.
Qué vas a ver:
✋ Predice antes de pulsar: si el filtro 0 deja de calcular el realce y pasa a difuminar la imagen, ¿qué le pasará a la zona del tumor en el bloque 2?
fijos = [(n, k) for n, k, _ in FILTROS_BLOQUE1[1:]]
sobel_h = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=np.float32) / 4
opciones = [
('realce: EARLY − PRE', capas(pre=-media, early=media),
'Referencia: el bloque 2 marca lo que capta\ncontraste y además tiene borde o lavado.'),
('difuminar EARLY', capas(early=media),
'Ya no mira el tiempo, solo el brillo: entran\nzonas brillantes que no captan contraste.'),
('bordes horizontales en EARLY', capas(early=sobel_h),
'Solo contornos: el bloque 2 pierde la\ninformación del contraste en el tumor.'),
('realce tardío: LATE − PRE', capas(pre=-media, late=media),
'Parecido al realce: el bloque 2 se parece\nmucho a la referencia.'),
('todo a cero', capas(),
'El filtro 0 no aporta nada: el bloque 2 solo\npuede usar los otros tres mapas.'),
]
clin.animar_efecto_bloque2(corte, fijos, opciones, PESOS_BLOQUE2)
Lo que hay que llevarse:
🧪 Prueba tú: cambia PESOS_BLOQUE2 (por ejemplo, el del lavado a 0 o el del
brillo a +1) y vuelve a ejecutar las dos celdas de animación.
| Idea | En una frase |
|---|---|
| Diagnóstico | sospecha → ecografía → biopsia (confirma y da el subtipo) → resonancia (tamaño y captación) |
| Neoadyuvancia | quimioterapia antes de operar, durante meses |
| pCR | tras ella, no queda carcinoma invasivo en mama ni ganglios; se sabe tras la cirugía |
| Por qué importa | mejor pronóstico (sobre todo TN y HER2+), objetivo de ensayos, cambia el tratamiento posterior |
| Nuestros datos | 29 % con pCR; 43 % HER2+, 38 % triple negativo, 14 % HR+/HER2− |
| La imagen | rebanadas de la mama; el tumor se ilumina con el contraste, pero no todo lo que brilla es tumor |
| Intensidad | el brillo de un píxel: un número de 0 a 1 |
| Tiempo | PRE, EARLY y LATE son el mismo corte en tres momentos; la pista está en cómo cambia |
| Cortes | ~10 por paciente, todos con la etiqueta de la paciente |
| Kernels | tablitas de números; en 3 fases, 3 capas que se suman en una imagen |
| Bloques | el bloque 2 combina los mapas del bloque 1; si cambia el bloque 1, cambia todo lo demás |
patients.csv, pero el enunciado pide trabajar con la imagen.Siguientes cuadernos: CNN_2_resonancias_mama.ipynb (la red recorriendo una
resonancia) y CNN_3_resonancias_arquitectura.ipynb (3 canales, N cortes por
paciente y la arquitectura línea a línea).