Los cinco más cercanos de cinco mil
Un almacén de vectores guarda trozos con sus vectores y encuentra los más cercanos a una pregunta. Importa porque la recuperación solo sirve si es rápida y devuelve los mejores pocos, no todos.
Una universidad tiene cinco mil trozos de guías y reglamentos. Un estudiante escribe una pregunta. El programa debe ponerla junto a cada trozo y traer los cinco más cercanos.
Al final de esta lección
Nota
Puedo guardar vectores y encontrar los más cercanos.
Palabras clave
| Palabra | Qué significa | Ejemplo |
|---|---|---|
| Almacén de vectores | una lista de trozos y sus vectores | |
| Consulta | la pregunta, convertida en vector | |
| Puntuación | lo cerca que está un trozo de la consulta | 0.82 |
| Top k | los k trozos con mejor puntuación | k = 3 |
| Índice | una estructura que se salta casi toda la lista |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Un almacén de vectores es una lista puntuada
El almacén más sencillo es una lista. Cada elemento guarda un id y un vector. Para buscar, conviertes la pregunta en vector, puntúas cada elemento con el coseno, ordenas las notas y te quedas con las k primeras.
Eso es todo lo que significa "búsqueda del vecino más cercano". El mejor resultado es el elemento con la nota más alta.
Ejemplo resuelto: consulta [1, 0] contra tres elementos
| Elemento | Vector | Coseno | Puesto |
|---|---|---|---|
| a | [1, 0] | 1,00 | 1 |
| b | [1, 1] | 0,71 | 2 |
| c | [0, 1] | 0,00 | 3 |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Un recuento solo halla las mismas palabras
Este laboratorio no tiene modelo de embeddings, así que el siguiente script fabrica sus vectores. Cuenta cuántas veces aparece en un trozo cada palabra de una lista fija. También quita una "s" final, para que "libros" y "libro" coincidan.
Es un sustituto. Un embedding real también acerca "prestar" a "dejar". Un vector de recuentos no puede, porque solo ve las mismas palabras.
Escribe el script del almacén, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const vocabulary = [\"abre\", \"cierra\", \"sábado\", \"domingo\", \"prestar\", \"libro\", \"retraso\", \"multa\", \"sala\", \"biblioteca\", \"hora\", \"semana\"];' 'const chunks = [' ' { id: \"horario-semana\", text: \"La biblioteca abre a las 9:00 y cierra a las 18:00 de lunes a viernes.\" },' ' { id: \"horario-finde\", text: \"El sábado la biblioteca abre a las 10:00 y cierra a las 14:00. El domingo está cerrada.\" },' ' { id: \"prestamo\", text: \"Se pueden prestar 8 libros durante 3 semanas. Cada libro se puede renovar dos veces por internet.\" },' ' { id: \"multas\", text: \"Un libro devuelto con retraso cuesta 20 céntimos al día. La multa no pasa de 10 euros.\" },' ' { id: \"salas\", text: \"Una sala de estudio admite a 6 personas. Reserva la sala en el mostrador o por internet para 2 horas.\" },' '];' > store.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el script del almacén, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function stem(word) { return word.length > 3 && word.endsWith(\"s\") ? word.slice(0, -1) : word; }' 'function vectorOf(text) {' ' const words = text.toLowerCase().replace(/[^a-z0-9áéíóúüñ ]/g, \" \").split(\" \").filter(function (w) { return w.length > 0; }).map(stem);' ' return vocabulary.map(function (term) {' ' return words.filter(function (word) { return word === term; }).length;' ' });' '}' 'function cosine(a, b) {' ' let dot = 0, sa = 0, sb = 0;' ' for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; sa += a[i] * a[i]; sb += b[i] * b[i]; }' ' return sa === 0 || sb === 0 ? 0 : dot / Math.sqrt(sa * sb);' '}' >> store.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el script del almacén, parte 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const store = chunks.map(function (chunk) {' ' return { id: chunk.id, vector: vectorOf(chunk.text) };' '});' 'function search(question, k) {' ' const query = vectorOf(question);' ' return store' ' .map(function (item) { return { id: item.id, score: cosine(query, item.vector) }; })' ' .sort(function (a, b) { return b.score - a.score; })' ' .slice(0, k);' '}' 'const question = \"a qué hora abre la biblioteca el sábado\";' 'console.log(\"Pregunta:\", question);' 'search(question, 3).forEach(function (hit) {' ' console.log(hit.score.toFixed(2), hit.id);' '});' >> store.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Ejecútalo y lee las notas
docker exec -w /home/node/project agent node store.jsSe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Lo que muestra
| Línea | Qué indica |
|---|---|
| Pregunta: a qué hora abre la biblioteca el sábado | La consulta |
| 0.67 horario-finde | El mejor trozo: nombra el sábado |
| 0.58 horario-semana | El siguiente: tiene abre y biblioteca |
| 0.22 salas | El tercero: solo comparte la palabra hora |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Por qué existe un índice
Puntuar cada elemento va bien con cinco trozos, o con cinco mil. Es lento con cincuenta millones, porque cada búsqueda repite todo ese trabajo.
Un índice agrupa los vectores cercanos para que una búsqueda se salte casi toda la lista. La mayoría de los índices son aproximados: mucho más rápidos, pero a veces pueden perder el vecino más cercano de verdad.
Un error frecuente
Advertencia
Mucha gente cree que el primer resultado siempre contiene la respuesta. Es solo el trozo más cercano. Si nada del almacén está cerca, el primer resultado es el menos malo. Mira la nota antes de fiarte.