Un ayudante de tienda que responde preguntas
Una tienda de alquiler de bicis crea un ayudante. Un cliente escribe una pregunta. La app busca el mejor párrafo entre las respuestas de la tienda y se lo da a un modelo de lenguaje, que redacta la respuesta.
Si la app encuentra el párrafo equivocado, el modelo redacta con mucha educación una respuesta equivocada. Esta lección prueba la búsqueda. Es código sencillo: se puede puntuar sin llamar a ningún modelo.
Al final de esta lección
Nota
Puedo probar una app que responde preguntas con un pequeño conjunto de preguntas, cada una con el dato que la respuesta debe contener.
Palabras clave
| Palabra | Qué significa | Ejemplo |
|---|---|---|
| Búsqueda | encontrar el texto que debe ayudar a responder una pregunta | |
| Dato esperado | un trozo corto de texto que una buena respuesta debe contener | doce euros |
| Tasa de aciertos | la parte de las preguntas en las que se halló el texto correcto | |
| Palabra vacía | una palabra muy común, como «el», que dice poco | |
| Respuesta equivocada | se halló un texto, pero no contiene el dato | |
| Sin respuesta | nada coincidió, así que la app dice que no lo sabe |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Dos pasos, y solo uno se prueba bien
Muchas apps responden en dos pasos. Primero la búsqueda: elegir el párrafo que encaja con la pregunta. Después un modelo de lenguaje redacta la respuesta a partir de ese párrafo.
La respuesta de un modelo cambia de una ejecución a otra: se puntúa mal. La búsqueda da lo mismo cada vez: se puntúa bien. Si la búsqueda falla, nada después puede estar bien.
Contar las palabras en común
Aquí la coincidencia es tan sencilla como se puede. Parte la pregunta en palabras. Para cada párrafo, cuenta cuántas de esas palabras están también en él. Gana el párrafo con la cuenta más alta.
Compara palabras enteras: «abre» no coincide con «abrir». Ejecuta el código y lee las cuentas.
Contar las palabras en común de una pregunta
def words(text):
for mark in ".,?¿":
text = text.replace(mark, "")
return text.lower().split()
docs = [
"La tienda abre a las nueve y cierra a las seis de lunes a sábado.",
"Una bici cuesta doce euros por un día y cinco euros por una hora.",
"Puedes devolver una bici en cualquiera de las tres estaciones de la ciudad.",
"Si una bici se estropea, llama al número que está en el cuadro y vamos a repararla.",
"Los cascos son gratis con cada alquiler, pero hay que pedirlos en el mostrador.",
]
question = "¿Cuál es el precio de las bicis de un día?"
for text in docs:
shared = []
for word in words(question):
if word in words(text):
shared.append(word)
print(len(shared), shared, "<-", text[:28])
# Cambia la pregunta por "¿Me dan un casco?" y vuelve a ejecutar.
Un conjunto de prueba: preguntas y datos
Para puntuar la app necesitas un pequeño conjunto de prueba. Contiene preguntas que un cliente podría hacer. Cada una tiene el dato esperado que contiene el párrafo correcto.
Una pregunta es un acierto si el párrafo hallado contiene su dato. Es un fallo si se halló un párrafo sin él. Queda sin respuesta si nada coincidió.
Escribir las cinco respuestas de la tienda
docker exec jupyter sh -c "printf '%s\n' 'd1|La tienda abre a las nueve y cierra a las seis de lunes a sábado.' 'd2|Una bici cuesta doce euros por un día y cinco euros por una hora.' 'd3|Puedes devolver una bici en cualquiera de las tres estaciones de la ciudad.' 'd4|Si una bici se estropea, llama al número que está en el cuadro y vamos a repararla.' 'd5|Los cascos son gratis con cada alquiler, pero hay que pedirlos en el mostrador.' > faq.txt"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribir las seis preguntas de prueba
docker exec jupyter sh -c "printf '%s\n' '¿Cuándo abre la tienda?|las nueve' '¿Cuál es el precio de las bicis de un día?|doce euros' '¿Dónde puedo devolver una bici?|tres estaciones' '¿Qué hago si se estropea mi bici?|llama al número' '¿Me dan un casco?|gratis' '¿Qué días cierran?|sábado' > preguntas.txt"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribir el script de puntuación
docker exec jupyter sh -c "printf '%s\n' 'import sys' '' 'SKIP = [\"el\", \"la\", \"los\", \"las\", \"un\", \"una\", \"de\", \"del\", \"es\", \"cuál\", \"cuánto\", \"qué\", \"cuándo\", \"dónde\", \"por\", \"a\", \"en\", \"si\", \"se\", \"y\", \"mi\", \"me\", \"puedo\"]' '' '' 'def words(text):' ' for mark in \".,?¿\":' ' text = text.replace(mark, \"\")' ' return text.lower().split()' '' '' 'docs = []' 'for line in open(\"faq.txt\", encoding=\"utf-8\"):' ' docs.append(line.strip().split(\"|\")[1])' 'skip = []' 'if sys.argv[1] == \"vacias\":' ' skip = SKIP' 'hit = 0' 'wrong = 0' 'none = 0' 'for line in open(\"preguntas.txt\", encoding=\"utf-8\"):' ' question, fact = line.strip().split(\"|\")' ' asked = []' ' for word in words(question):' > steps/ask.py"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribir el resto del script de puntuación
docker exec jupyter sh -c "printf '%s\n' ' if word not in skip:' ' asked.append(word)' ' best = \"\"' ' best_shared = 0' ' for text in docs:' ' shared = 0' ' for word in asked:' ' if word in words(text):' ' shared = shared + 1' ' if shared > best_shared:' ' best = text' ' best_shared = shared' ' if best == \"\":' ' none = none + 1' ' print(\"nada:\", question)' ' elif fact.lower() in best.lower():' ' hit = hit + 1' ' print(\"acierto:\", question)' ' else:' ' wrong = wrong + 1' ' print(\"fallo:\", question)' 'print(\"aciertos\", hit, \"fallos\", wrong, \"nada\", none, \"tasa\", round(hit / (hit + wrong + none), 2))' >> steps/ask.py"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Puntuar la coincidencia sencilla
docker exec jupyter python3 steps/ask.py crudoSe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Arreglar una cosa y puntuar de nuevo
Dos preguntas fueron al párrafo equivocado por palabras pequeñas como «de» y «un». Quita de la pregunta esas palabras vacías antes de contar y puntúa de nuevo.
Se cambia una sola cosa cada vez. Si cambiaras tres a la vez, no sabrías cuál ayudó.
Puntuar de nuevo ignorando las palabras vacías
docker exec jupyter python3 steps/ask.py vaciasSe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Un error frecuente
Advertencia
Mucha gente cree que la app mejoró porque la tasa subió de 0,5 a 0,67. Mira los demás números. Los dos fallos pasaron a acierto o a sin respuesta, lo cual es mejor, pero dos preguntas siguen sin hallar nada. Un conjunto de prueba tan pequeño dice dónde mirar, no lo buena que es la app.
Ejemplo resuelto: las dos ejecuciones juntas
| Ejecución | Aciertos | Fallos | Sin respuesta | Tasa |
|---|---|---|---|---|
| Palabras enteras, nada ignorado | 3 | 2 | 1 | 0.5 |
| Palabras vacías ignoradas | 4 | 0 | 2 | 0.67 |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Para ir más lejos: añade tus preguntas
Añade cuatro preguntas al archivo de prueba, cada una con un dato. Incluye una que las cinco respuestas de la tienda no puedan contestar.
Puntúa de nuevo las dos ejecuciones. ¿Qué resultado sería el correcto para la pregunta sin respuesta en el texto de la tienda? ¿Lo da la app?