Enseña a tu navegador a ver
Un modelo de 8,8 MB convierte cada foto en 1.280 números y tu vecino más cercano decide si es naranja, limón o aguacate. Sin entrenar nada.
DATA.examples trae 18 fotos de la cinta de un almacén de fruta, cada una como { label, name, image, vec }, con vec los 1.280 números que MobileNetV2 calculó para ella; DATA.query es una foto más, sin etiqueta. Escribe cinco funciones:
meanVector(vectors): la media posición a posición. cosine(a, b): producto escalar entre el producto de las longitudes; 0 si alguna longitud es 0.
nearest(examples, vec, k): los k ejemplos más parecidos a vec como [{ label, name, sim }], de mayor a menor sim; a igual sim, en el orden en que venían.
vote(neighbours): la etiqueta con más vecinos; si empatan, la de mayor suma de sim; si siguen empatadas, la primera en orden alfabético.
leaveOneOut(examples, k, centre): clasifica cada ejemplo con todos los demás (vote(nearest(...))) y devuelve la fracción de aciertos. Con centre, antes resta la media de los demás a sus vectores y al del ejemplo apartado. La vista previa enseña los ejemplos, los aciertos y la foto a clasificar.
Retos 0/5
- La media de varios vectores y el coseno entre dos
- Los vecinos más cercanos, en orden y con los empates en su sitio
- La votación: mayoría, luego la suma de similitudes, luego el alfabeto
- Las 18 fotos de la cinta, dejando una fuera: 15 sin centrar, 17 centrando
- Con los vectores del modelo (tus fotos) o la muestra: una clase conocida para la foto nueva
function meanVector(vectors) {
// la media de arrays de igual longitud, posición a posición
return [];
}
function cosine(a, b) {
// producto escalar / (longitud de a * longitud de b); 0 si alguna longitud es 0
return 0;
}
function nearest(examples, vec, k) {
// los k ejemplos más parecidos a vec como [{ label, name, sim }], de mayor a menor sim; los empates mantienen el orden de entrada
return [];
}
function vote(neighbours) {
// la etiqueta con más vecinos; si empatan, la de mayor suma de sim; después, orden alfabético
return '';
}
function leaveOneOut(examples, k, centre) {
// clasifica cada ejemplo con todos los demás; si centre, resta antes la media de los demás a cada vector
// devuelve la fracción de aciertos
return 0;
}
console.log(leaveOneOut(DATA.examples, 1, false), leaveOneOut(DATA.examples, 1, true));La idea
Para que un programa distinga tus piezas buenas de las defectuosas no siempre hace falta entrenar una red. Un modelo ya entrenado con millones de fotos, cortado antes de su última capa, convierte cada imagen en un vector: aquí, 1.280 números de MobileNetV2. Dos fotos parecidas dan vectores que apuntan hacia el mismo lado, y eso lo mide el coseno. Con unos pocos ejemplos etiquetados, la foto nueva se lleva la etiqueta de sus vecinos más cercanos (k-NN). Enseñar es añadir ejemplos; no hay entrenamiento.
Un detalle que marca la diferencia: los vectores de un mismo modelo se parecen todos un poco. En nuestras fotos, el coseno medio entre dos frutas de la misma clase es 0,79 y entre clases distintas, 0,71. Restar la media de los ejemplos (centrar) quita lo que todas comparten y deja lo que distingue a cada una. Con las 18 fotos de la cinta, dejando una fuera cada vez, el vecino más cercano acierta 15 sin centrar y 17 centrando.
Lo que medimos el 19-10-2026 en el navegador del DGX Spark (ONNX Runtime Web, WebAssembly, un hilo): 18 a 19 ms por foto, con un modelo de 8,8 MB. Y dos avisos. Primero, el fondo pesa: las mismas tres frutas fotografiadas sobre cinco fondos distintos (caja, encimera, mano, mercado y plato) solo aciertan 8 de 15, porque el modelo agrupa por fondo antes que por fruta. Fotografía tus ejemplos igual que vas a fotografiar lo que quieres clasificar. Segundo, seis ejemplos por clase son pocos: nuestra foto a clasificar, un limón con una hoja, sale bien con k = 1 centrando y mal con k = 3, porque ningún ejemplo tiene hoja. La solución no es otro modelo; son dos fotos más.
Todo ocurre en tu pestaña: las fotos que añadas para enseñarle no salen de tu navegador.
Hazlo en tu máquina
Con Python y NumPy, sobre los vectores de este ejercicio y los de las fotos con cinco fondos (CC BY 4.0):
curl -O https://vorluxai.com/data/labs/fruta-vectores.json
python3 -c "
import json, base64, numpy as np
d = json.load(open('fruta-vectores.json'))
def vecs(items): return np.array([np.frombuffer(base64.b64decode(e['q']), np.uint8) / 255 * e['s'] for e in items]), [e['label'] for e in items]
def loo(X, y, centre):
right = 0
for i in range(len(X)):
T, q, ty = np.delete(X, i, 0), X[i], y[:i] + y[i + 1:]
if centre: m = T.mean(0); T, q = T - m, q - m
sim = T @ q / (np.linalg.norm(T, axis=1) * np.linalg.norm(q))
right += ty[int(sim.argmax())] == y[i]
return f'{right}/{len(X)}'
for name in ('examples', 'fondos'):
X, y = vecs(d[name])
print(name, 'sin centrar', loo(X, y, False), 'centrando', loo(X, y, True))
" Para profundizar: la referencia de GitHub (en inglés) →
¿Esto en producción, con tus datos? Hablemos 15 minutos →