Skip to content

feat: replace the handshape table with a model trained on real fingerspelling - #78

Merged
Endika merged 2 commits into
mainfrom
feat/alphabet-ctc
Aug 26, 2026
Merged

feat: replace the handshape table with a model trained on real fingerspelling#78
Endika merged 2 commits into
mainfrom
feat/alphabet-ctc

Conversation

@Endika

@Endika Endika commented Aug 26, 2026

Copy link
Copy Markdown
Owner

La tabla de handshapes escribía el 17% de las letras y deletreaba 0 de 456 palabras. La sustituye una GRU causal entrenada con CTC sobre LSE-FS-UVigo.

El resultado, sobre el mismo test y medido una sola vez

Cada motor va con la regla de su propia versión de la app: la tabla parpadeaba y pedía tres frames de acuerdo, la cabeza CTC pica y pide uno. Medir ambos con una sola regla favorecería a aquel para el que se eligió.

palabras exactas CER letras escritas acierto entre ellas
tabla geométrica 0/456 0,919 742 de 4.321 47,7%
GRU + CTC 75/456 0,354 3.290 de 4.321 88,4%

4,4 veces más letras escritas, y acierta nueve de cada diez en vez de menos de cinco. Gana en las 14 letras que la tabla intentaba, sin una sola excepción — cinco de ellas la tabla las tenía a 0%. Por eso no hay motor híbrido: no hay nada que conservar.

Validation: CER 0,255, sd 0,005 sobre semillas 7/13/29/41. Modelo de 0,71 MB, el 27% de lo que pesa el de vocabulario.

Por qué CTC, y por qué el blank es lo importante

LSE-FS etiqueta la palabra deletreada y nunca dice cuándo ocurre cada letra, así que CTC es lo que hace usable el corpus. Pero su clase blank es además un «aquí no hay letra» entrenado, que una tabla de similitud no puede expresar con ningún umbral. Son los negativos que health_dataset.py no pudo construir para el vocabulario, y aquí salen gratis del método.

Causal no es preferencia: la app clasifica frame a frame en vivo, así que la GRU bidireccional que pediría CTC es inviable.

El único cambio en la app, y por qué era obligatorio

Los posteriores de CTC son picudos por construcción — la pérdida es invariante a cuánto dura una letra. Medido: las rachas no-blank son 26% de un frame y solo el 23% llega a tres. Con la regla vieja este modelo escribe 550 letras de 3.758 y ni una palabra entera; con una, escribe 2.981 y 53 palabras.

Así que el motor de alfabeto usa CandidateStabilizer(1, 0.5) y se suelta el latch cuando el motor se abstiene. Con esa suelta, un frame de acuerdo más el latch de no-repetir es el colapso greedy de CTC: la regla ya estaba, con la constante equivocada.

Fidelidad de puerto

gruStep sale del cuerpo de gruPass, no es un GRU nuevo — dos copias de esas seis líneas coincidirían el día que se escriben y divergirían después. Un test comprueba que N pasos encadenados igualan un gruPass.

Y la fixture de paridad reconstruye el modelo desde el blob exportado, no desde el checkpoint, así que prueba también el orden de tensores del .bin. Seis frames × 28 clases a 5 decimales.

Aumentación: qué compró y qué descarté

El cuello son los datos, no la capacidad: 18.887 letras para 27 clases, y el CER toca suelo y empeora. Una semilla cada una: ninguna 0,349 · temporal 0,309 · rotación+ruido+caída 0,272 · todas 0,262.

Dos que no están, y el motivo importa. El espejado parecía gratis y es dañino: normalize_hand ya pliega las izquierdas a espacio derecho, así que negar la x fabrica formas que la app no produce. El jitter de escala es inútil por lo mismo.

Aviso de método: ordenar por longitud para matar el relleno aceleró 2,4x y costó 0,036 de CER, porque la longitud correlaciona con la de la palabra y los batches dejan de ser diversos. Barajar dentro de ventanas de ocho batches devuelve la precisión.

Licencias

Los pesos del alfabeto son CC BY 4.0, sin la cláusula NC del vocabulario, porque LSE-FS no la tiene. Estar en la misma carpeta no contagia y LICENSE.md ahora lo dice explícito: un fork comercial puede llevarse lse-alphabet.* y no lse-vocabulary.*.

Honestidad de cara al usuario

La UI listaba 12 letras «que no distingue». Ahora las intenta todas, pero seis salen bien menos de una de cada tres veces (J 5%, W 20%, Y 22%, Ñ 25%, Q 27%, Z 29%), y son exactamente las que el corpus apenas contiene. Se avisa de esas seis, con la nota de que es una medición y hay que re-derivarla al reentrenar.

HandshapeAlphabetClassifier se queda en el repo aunque la app ya no lo use: el banco lo necesita para la línea base.

Lint, typecheck, 194 tests y build en verde.

@Endika
Endika merged commit 99f5aba into main Aug 26, 2026
8 checks passed
@Endika
Endika deleted the feat/alphabet-ctc branch August 26, 2026 18:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant