Módulo 5: ReAct, Self-Consistency y Patrones Avanzados
3. Self-Consistency: Múltiples Caminos, Una Respuesta
Descripción
Self-Consistency es una técnica propuesta por Wang et al. en 2022 que mejora la precisión de los modelos de lenguaje en tareas de razonamiento mediante un principio estadístico simple: si generas múltiples respuestas independientes para el mismo problema y la mayoría coincide, esa respuesta es probablemente correcta.
La intuición es análoga a cómo funcionan los juries en un juicio, los comités de expertos, o incluso el concepto de "sabiduría de la multitud": una sola opinión puede estar sesgada o equivocada, pero el consenso de muchos caminos independientes tiende a ser más robusto.
La Intuición Estadística
Imagina que un modelo tiene un 70% de probabilidad de responder correctamente una pregunta difícil con CoT puro. ¿Qué pasa si preguntamos N veces?
P(al menos 1 correcta con N=5) = 1 - P(todas incorrectas)
P(todas incorrectas) = (0.30)^5 = 0.00243
P(al menos 1 correcta) ≈ 99.76%
PERO: con majority vote (3 de 5), la distribución de aciertos mejora más:
- Con p=0.7, P(mayoría correcta con N=5) ≈ 83.69%
- Con p=0.7, P(mayoría correcta con N=9) ≈ 90.12%
- Con p=0.7, P(mayoría correcta con N=15) ≈ 95.24%
La clave: la variación en el razonamiento (temperature > 0) hace que el camino incorrecto tome diferentes rutas erróneas, mientras que el camino correcto tiende a converger en la misma respuesta.
Implementación Base
from openai import OpenAI
from collections import Counter
import re
from typing import Optional
client = OpenAI()
def extraer_respuesta_numerica(texto: str) -> Optional[str]:
"""
Extrae la respuesta final numérica de un texto de razonamiento CoT.
Maneja múltiples formatos: 'Answer: 42', '= 42', 'the answer is 42', etc.
"""
texto = texto.strip()
# Patrones comunes de respuesta final
patrones = [
r'(?:respuesta|answer|resultado|result|final)[\s:=]+(-?\d+\.?\d*)',
r'(?:por lo tanto|therefore|entonces|thus|so)[,\s]+(?:la respuesta es\s+)?(-?\d+\.?\d*)',
r'=\s*(-?\d+\.?\d*)\s*$', # Al final: = 42
r'\*\*(-?\d+\.?\d*)\*\*', # Negritas: **42**
]
for patron in patrones:
match = re.search(patron, texto, re.IGNORECASE | re.MULTILINE)
if match:
return match.group(1)
# Fallback: último número del texto
numeros = re.findall(r'-?\d+\.?\d*', texto)
if numeros:
return numeros[-1]
# Fallback final: últimas 30 chars del texto (para respuestas no numéricas)
return texto.strip()[-30:].strip()
def extraer_respuesta_categorica(texto: str, categorias: list[str]) -> str:
"""
Extrae una respuesta categórica buscando las categorías en el texto.
Args:
texto: Texto de respuesta del modelo
categorias: Lista de categorías válidas (ej: ["POSITIVO", "NEGATIVO", "NEUTRAL"])
"""
texto_upper = texto.upper()
for cat in categorias:
if cat.upper() in texto_upper:
return cat
return texto.strip()[-30:] # Fallback
def self_consistency(
problema: str,
n: int = 5,
temperatura: float = 0.7,
max_tokens: int = 500,
extraer_fn = None
) -> dict:
"""
Implementa Self-Consistency: genera N respuestas y vota por mayoría.
Args:
problema: El problema o pregunta a resolver
n: Número de muestras (típicamente 3-10)
temperatura: Temperature para variación (0.5-1.0 recomendado)
max_tokens: Máximo tokens por respuesta
extraer_fn: Función para extraer la respuesta final del texto
Returns:
dict con respuesta ganadora, todas las respuestas, y estadísticas
"""
if extraer_fn is None:
extraer_fn = extraer_respuesta_numerica
prompt_cot = f"""{problema}
Piensa paso a paso y muestra todo tu razonamiento antes de dar la respuesta final.
Al final, escribe explícitamente: "Respuesta: [tu respuesta]"
"""
respuestas_raw = []
respuestas_extraidas = []
for i in range(n):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt_cot}],
temperature=temperatura,
max_tokens=max_tokens
)
raw = response.choices[0].message.content
extraida = extraer_fn(raw)
respuestas_raw.append(raw)
respuestas_extraidas.append(extraida)
# Majority vote
conteo = Counter(respuestas_extraidas)
respuesta_ganadora, votos = conteo.most_common(1)[0]
return {
"respuesta": respuesta_ganadora,
"votos": votos,
"total": n,
"confianza": votos / n,
"distribucion": dict(conteo),
"respuestas_raw": respuestas_raw,
"todas_respuestas": respuestas_extraidas
}
Implementación Avanzada con Normalización
def normalizar_respuesta(texto: str) -> str:
"""
Normaliza respuestas para que el voting sea más robusto.
Maneja casos como "42.0" vs "42", "42,000" vs "42000", etc.
"""
texto = texto.strip()
# Remover puntuación final
texto = texto.rstrip('.,;:')
# Normalizar números: remover comas de miles
texto = texto.replace(',', '')
# Normalizar números decimales: "42.0" → "42"
try:
num = float(texto)
if num == int(num):
return str(int(num))
return str(round(num, 4))
except ValueError:
pass
# Para respuestas categóricas: uppercase y strip
return texto.upper().strip()
def self_consistency_robusto(
problema: str,
n: int = 5,
temperatura: float = 0.7,
normalizar: bool = True
) -> dict:
"""
Self-Consistency con normalización de respuestas para voting más robusto.
"""
resultado = self_consistency(problema, n=n, temperatura=temperatura)
if normalizar:
# Re-normalizar todas las respuestas y re-votar
respuestas_norm = [normalizar_respuesta(r) for r in resultado["todas_respuestas"]]
conteo_norm = Counter(respuestas_norm)
ganadora_norm, votos_norm = conteo_norm.most_common(1)[0]
resultado["respuesta_normalizada"] = ganadora_norm
resultado["distribucion_normalizada"] = dict(conteo_norm)
resultado["confianza_normalizada"] = votos_norm / n
return resultado
# Ejemplo de uso:
if __name__ == "__main__":
problema = "En una tienda, un artículo cuesta $80 después de un descuento del 20%. ¿Cuál era el precio original?"
resultado = self_consistency_robusto(problema, n=5)
print(f"Respuesta: {resultado['respuesta']}")
print(f"Distribución: {resultado['distribucion']}")
print(f"Confianza: {resultado['confianza']:.0%}")
print(f"¿Consenso claro? {resultado['confianza'] >= 0.6}")
Variantes de Self-Consistency
Variante 1: Self-Consistency para Clasificación
def self_consistency_clasificacion(
texto: str,
categorias: list[str],
n: int = 5
) -> dict:
"""
Self-Consistency para tareas de clasificación.
Útil cuando la categorización es ambigua.
"""
cats_str = ", ".join(categorias)
prompt = f"""Clasifica el siguiente texto en una de estas categorías: {cats_str}
Texto: {texto}
Primero analiza el texto, luego clasifica. Responde con exactamente una de las categorías.
"""
def extraer_categoria(respuesta: str) -> str:
return extraer_respuesta_categorica(respuesta, categorias)
resultado = self_consistency(
prompt,
n=n,
temperatura=0.5, # Menos temperatura para clasificación
extraer_fn=extraer_categoria
)
return resultado
# Ejemplo:
reviews = [
"El producto llegó bien pero el servicio al cliente fue horrible.", # Ambiguo: POSITIVO/NEGATIVO
"Absolutamente increíble, lo recomiendo a todos.", # Claro: POSITIVO
"Podría ser mejor, pero tampoco es tan malo." # Ambiguo: NEUTRAL
]
for review in reviews:
resultado = self_consistency_clasificacion(
review,
["POSITIVO", "NEGATIVO", "NEUTRAL"],
n=5
)
print(f"Review: {review[:50]}...")
print(f"Clasificación: {resultado['respuesta']} (confianza: {resultado['confianza']:.0%})")
print(f"Distribución: {resultado['distribucion']}\n")
Variante 2: Self-Consistency con Pesos
def self_consistency_ponderado(
problema: str,
n: int = 5
) -> dict:
"""
En lugar de majority vote simple, asignar pesos según la longitud
y coherencia del razonamiento.
"""
respuestas_con_metadata = []
for _ in range(n):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"{problema}\nPiensa paso a paso. Respuesta: [número]"}],
temperature=0.7,
max_tokens=500,
logprobs=True # Obtener log-probabilidades para pesos
)
msg = response.choices[0].message
raw = msg.content
# Extraer respuesta
respuesta = extraer_respuesta_numerica(raw)
# Calcular peso basado en longitud del razonamiento
# (razonamientos más largos y detallados suelen ser más confiables)
peso = min(len(raw.split()) / 100, 2.0) # Normalizar, máx peso 2x
respuestas_con_metadata.append({
"respuesta": respuesta,
"peso": peso,
"longitud": len(raw.split())
})
# Weighted voting
votos_ponderados = {}
for item in respuestas_con_metadata:
resp = item["respuesta"]
votos_ponderados[resp] = votos_ponderados.get(resp, 0) + item["peso"]
ganadora = max(votos_ponderados, key=votos_ponderados.get)
return {
"respuesta": ganadora,
"votos_ponderados": votos_ponderados,
"detalle": respuestas_con_metadata
}
Variante 3: Self-Consistency Adaptativo (Stop Temprano)
def self_consistency_adaptativo(
problema: str,
n_min: int = 3,
n_max: int = 10,
umbral_confianza: float = 0.8
) -> dict:
"""
Para cuando la respuesta ya es muy clara (alta confianza),
deja de generar más muestras para ahorrar tokens.
"""
respuestas_extraidas = []
for i in range(n_max):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"{problema}\nPiensa paso a paso. Respuesta: [tu respuesta]"}],
temperature=0.7,
max_tokens=400
)
raw = response.choices[0].message.content
respuestas_extraidas.append(extraer_respuesta_numerica(raw))
# Solo evaluar después del mínimo
if i + 1 >= n_min:
conteo = Counter(respuestas_extraidas)
ganadora, votos = conteo.most_common(1)[0]
confianza = votos / (i + 1)
if confianza >= umbral_confianza:
return {
"respuesta": ganadora,
"confianza": confianza,
"n_usadas": i + 1,
"stop_temprano": True,
"distribucion": dict(conteo)
}
# Si no llegamos al umbral, retornar la mejor respuesta
conteo = Counter(respuestas_extraidas)
ganadora, votos = conteo.most_common(1)[0]
return {
"respuesta": ganadora,
"confianza": votos / n_max,
"n_usadas": n_max,
"stop_temprano": False,
"distribucion": dict(conteo)
}
# Benchmark de ahorros:
preguntas_simples = ["¿Cuánto es 8 * 9?", "¿Cuánto es 15 + 27?"]
preguntas_dificiles = ["Si el radio de un círculo aumenta 50%, ¿en qué porcentaje aumenta el área?"]
for pregunta in preguntas_simples + preguntas_dificiles:
r = self_consistency_adaptativo(pregunta)
print(f"Pregunta: {pregunta[:50]}...")
print(f" Respuesta: {r['respuesta']}, Confianza: {r['confianza']:.0%}, "
f"Llamadas: {r['n_usadas']}, Stop temprano: {r['stop_temprano']}")
Cuándo Usar Self-Consistency
Tareas donde brilla
| Tipo de tarea | Mejora típica | Recomendado N |
|---|---|---|
| Matemáticas word problems | +8-15% | 5-7 |
| Razonamiento lógico | +5-12% | 3-5 |
| Clasificación ambigua | +3-8% | 3-5 |
| Comprensión lectora | +3-6% | 3-5 |
| Código/debugging | +5-10% | 5 |
Tareas donde NO ayuda
- Tareas creativas: Escribir un poema — no tiene "respuesta correcta" única
- Tareas de formato: Traducción — la majority vote puede mezclar idiomas
- Cuando los datos son escasos: Si el modelo no sabe la respuesta, N respuestas incorrectas siguen siendo incorrectas
- Conversaciones: En diálogos multi-turn, el contexto importa más que la consistencia
Trade-offs: Costo vs. Accuracy
def analizar_tradeoffs(problema: str, max_n: int = 10) -> list[dict]:
"""
Analiza el trade-off entre N muestras y confianza.
"""
historial = []
todas_respuestas = []
for i in range(1, max_n + 1):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"{problema}\nPiensa paso a paso. Respuesta: [número]"}],
temperature=0.7,
max_tokens=300
)
todas_respuestas.append(extraer_respuesta_numerica(response.choices[0].message.content))
conteo = Counter(todas_respuestas)
ganadora, votos = conteo.most_common(1)[0]
historial.append({
"n": i,
"respuesta": ganadora,
"confianza": votos / i,
"costo_relativo": i # Simplificación: cada llamada cuesta lo mismo
})
return historial
# Visualizar:
# import matplotlib.pyplot as plt # Si tienes matplotlib
# for punto in analizar_tradeoffs("Si un tren viaja a 120 km/h, ¿cuánto tarda en recorrer 300 km?"):
# print(f"N={punto['n']}: Respuesta={punto['respuesta']}, Confianza={punto['confianza']:.0%}")
Tabla de referencia
| N | Costo | Accuracy típica (math) | ¿Vale la pena? |
|---|---|---|---|
| 1 | 1x | Base (70-80%) | Baseline |
| 3 | 3x | +5-8% | Sí, si accuracy importa |
| 5 | 5x | +8-12% | Sí, para problemas críticos |
| 7 | 7x | +10-13% | Solo si budget lo permite |
| 10 | 10x | +12-15% | Rendimientos decrecientes |
| 20 | 20x | +13-16% | Raramente justificado |
El punto de inflexión suele estar en N=5: buen balance entre mejora y costo.
Self-Consistency con Anthropic
import anthropic
client_anthropic = anthropic.Anthropic()
def self_consistency_claude(
problema: str,
n: int = 5,
temperatura: float = 0.7
) -> dict:
"""
Self-Consistency usando Claude (Anthropic).
"""
prompt = f"""{problema}
Analiza el problema paso a paso y al final escribe "Respuesta: [tu respuesta]"."""
respuestas_extraidas = []
respuestas_raw = []
for _ in range(n):
message = client_anthropic.messages.create(
model="claude-3-5-haiku-20241022",
max_tokens=500,
temperature=temperatura,
messages=[{"role": "user", "content": prompt}]
)
raw = message.content[0].text
respuestas_raw.append(raw)
respuestas_extraidas.append(extraer_respuesta_numerica(raw))
conteo = Counter(respuestas_extraidas)
ganadora, votos = conteo.most_common(1)[0]
return {
"respuesta": ganadora,
"confianza": votos / n,
"distribucion": dict(conteo),
"modelo": "claude-3-5-haiku-20241022"
}
Benchmark Real: Self-Consistency vs CoT Single
import json
import time
# Conjunto de prueba con respuestas conocidas
BENCHMARK = [
{
"problema": "Un comerciante compra 50 kg de manzanas a $2/kg y las vende a $3/kg. Si se pudren 10 kg antes de venderlos, ¿cuál es su ganancia o pérdida?",
"respuesta_correcta": "20", # (40 * 3) - (50 * 2) = 120 - 100 = 20 USD
"tipo": "word_problem"
},
{
"problema": "¿Cuál es el 15% de 840?",
"respuesta_correcta": "126",
"tipo": "porcentaje"
},
{
"problema": "Si 5 máquinas hacen 5 piezas en 5 minutos, ¿cuántas máquinas necesitas para hacer 100 piezas en 100 minutos?",
"respuesta_correcta": "5", # Respuesta contraintuitiva
"tipo": "logica"
},
{
"problema": "Un bote tiene capacidad para 10 personas. ¿Cuántos viajes necesita para cruzar a 45 personas?",
"respuesta_correcta": "5", # 45/10 redondeado arriba
"tipo": "division"
}
]
def evaluar_respuesta(pred: str, correcta: str) -> bool:
"""Compara respuestas normalizando."""
try:
return abs(float(normalizar_respuesta(pred)) - float(normalizar_respuesta(correcta))) < 0.01
except ValueError:
return pred.strip() == correcta.strip()
def run_benchmark(n_muestras: int = 5) -> dict:
"""Ejecuta el benchmark comparando CoT simple vs Self-Consistency."""
resultados_cot = []
resultados_sc = []
for item in BENCHMARK:
# CoT single
cot_resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"{item['problema']}\nPiensa paso a paso. Respuesta: [número]"}],
temperature=0
).choices[0].message.content
cot_pred = extraer_respuesta_numerica(cot_resp)
cot_correcto = evaluar_respuesta(cot_pred, item["respuesta_correcta"])
resultados_cot.append(cot_correcto)
# Self-Consistency
sc_result = self_consistency(item["problema"], n=n_muestras)
sc_correcto = evaluar_respuesta(sc_result["respuesta"], item["respuesta_correcta"])
resultados_sc.append(sc_correcto)
print(f"\nProblema: {item['problema'][:60]}...")
print(f" CoT: {cot_pred} → {'✓' if cot_correcto else '✗'}")
print(f" SC: {sc_result['respuesta']} (confianza {sc_result['confianza']:.0%}) → {'✓' if sc_correcto else '✗'}")
accuracy_cot = sum(resultados_cot) / len(resultados_cot)
accuracy_sc = sum(resultados_sc) / len(resultados_sc)
print(f"\n=== RESULTADOS ===")
print(f"CoT simple: {accuracy_cot:.0%}")
print(f"Self-Consistency N={n_muestras}: {accuracy_sc:.0%}")
print(f"Mejora: +{(accuracy_sc - accuracy_cot) * 100:.1f}%")
return {
"accuracy_cot": accuracy_cot,
"accuracy_sc": accuracy_sc,
"mejora": accuracy_sc - accuracy_cot
}
Troubleshooting
Problema 1: Respuestas en formatos distintos impiden el voting correcto
Síntoma: "42" y "42.0" y "42,0" cuentan como respuestas diferentes aunque sean iguales.
Solución:
def normalizar_agresivo(texto: str) -> str:
"""Normalización más agresiva para voting."""
# Remover todo excepto dígitos y punto decimal
solo_numeros = re.sub(r'[^\d.-]', '', texto.split()[-1] if texto.split() else texto)
try:
num = float(solo_numeros)
# Redondear a 2 decimales para evitar diferencias de precisión
num = round(num, 2)
return str(int(num)) if num == int(num) else str(num)
except ValueError:
return texto.upper().strip()
Problema 2: Empate entre respuestas (ej: 2 vs 2 con N=4)
Síntoma: Counter.most_common(1) retorna una de las empatadas arbitrariamente.
Solución:
def resolver_empate(conteo: Counter, respuestas_raw: list[str]) -> str:
"""
Estrategias para resolver empates:
1. Elegir la respuesta más larga (más razonamiento)
2. Hacer una llamada de desempate
3. Retornar la más "conservadora" (menor número en finanzas, por ejemplo)
"""
max_votos = conteo.most_common(1)[0][1]
empatadas = [r for r, v in conteo.items() if v == max_votos]
if len(empatadas) == 1:
return empatadas[0]
# Estrategia: desempate con una llamada adicional
candidates_str = "\n".join([f"- {r}" for r in empatadas])
desempate = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": f"Las siguientes respuestas empatan con el mismo número de votos:\n{candidates_str}\n\n¿Cuál es matemáticamente más probable que sea correcta? Responde solo con la respuesta correcta."
}],
temperature=0
)
return desempate.choices[0].message.content.strip()
Problema 3: Costo alto para N grande
Síntoma: N=10 es demasiado caro para volumen alto de consultas.
Solución: Usar self_consistency_adaptativo (ver arriba) o un threshold dinámico:
def self_consistency_presupuesto(
problema: str,
presupuesto_usd: float = 0.001 # $0.001 por problema
) -> dict:
"""
Self-Consistency con presupuesto fijo.
Estima N según el presupuesto disponible.
"""
# Estimación: ~500 tokens por llamada, gpt-4o-mini ~$0.000195/llamada
costo_por_llamada = 0.000195
n_max = max(1, int(presupuesto_usd / costo_por_llamada))
n_efectivo = min(n_max, 10) # Cap en 10
return self_consistency(problema, n=n_efectivo)
Ejercicios
Ejercicio 1: Implementar extracción de respuesta robusta
Mejora la función extraer_respuesta_numerica para que maneje todos estos formatos:
"The answer is 42"
"42"
"42.0"
"= 42"
"Por lo tanto, la respuesta es **42**"
"Ganancia: $42 USD"
"Conclusión: 42 elementos"
"La respuesta es cuarenta y dos (42)"
Ver solución
import re
def extraer_respuesta_v2(texto: str) -> str:
"""Extracción robusta de respuesta numérica."""
texto = texto.strip()
# Patrón 1: "Answer: 42" o "Respuesta: 42"
m = re.search(r'(?:answer|respuesta|resultado|ganancia|pérdida|total|final)[:\s]+\$?(-?\d+\.?\d*)', texto, re.IGNORECASE)
if m:
return m.group(1)
# Patrón 2: "**42**" (negritas markdown)
m = re.search(r'\*\*\$?(-?\d+\.?\d*)\*\*', texto)
if m:
return m.group(1)
# Patrón 3: "= 42" al final de línea
m = re.search(r'=\s*\$?(-?\d+\.?\d*)\s*(?:USD|EUR|€|\$)?$', texto, re.MULTILINE)
if m:
return m.group(1)
# Patrón 4: número entre paréntesis con contexto "cuarenta y dos (42)"
m = re.search(r'\((\d+)\)', texto)
if m:
return m.group(1)
# Patrón 5: "42 elementos", "42 USD", etc.
m = re.search(r'\b(-?\d+\.?\d*)\s*(?:elementos?|USD|EUR|€|\$|kg|km|años?|días?|meses?)?\s*$', texto)
if m:
return m.group(1)
# Fallback: último número del texto
numeros = re.findall(r'-?\d+\.?\d*', texto)
return numeros[-1] if numeros else texto[-20:]
# Test:
casos = [
"The answer is 42",
"42",
"42.0",
"= 42",
"Por lo tanto, la respuesta es **42**",
"Ganancia: $42 USD",
"Conclusión: 42 elementos",
"La respuesta es cuarenta y dos (42)"
]
for caso in casos:
extraido = extraer_respuesta_v2(caso)
print(f"'{caso[:40]}' → '{extraido}'")
Ejercicio 2: Self-Consistency para múltiple choice
Implementa Self-Consistency para preguntas de opción múltiple (A, B, C, D). El voting debe buscar la letra, no el número.
Ver solución
def self_consistency_multiple_choice(
pregunta: str,
opciones: dict, # {"A": "opción a", "B": "opción b", ...}
n: int = 5
) -> dict:
"""Self-Consistency para opción múltiple."""
opciones_str = "\n".join([f"{k}) {v}" for k, v in opciones.items()])
prompt = f"""{pregunta}
Opciones:
{opciones_str}
Analiza cada opción y razona por qué es o no correcta. Al final escribe SOLO la letra de la respuesta correcta."""
def extraer_letra(texto: str) -> str:
# Buscar letra al final o la más reciente
letras = re.findall(r'\b([A-D])\b', texto.upper())
return letras[-1] if letras else "?"
resultado = self_consistency(
prompt,
n=n,
temperatura=0.5, # Menos variación para MC
extraer_fn=extraer_letra
)
respuesta_letra = resultado["respuesta"]
respuesta_texto = opciones.get(respuesta_letra, "No encontrada")
return {
**resultado,
"respuesta_completa": f"{respuesta_letra}) {respuesta_texto}"
}
# Test:
pregunta = "¿Cuál es la capital de Brasil?"
opciones = {"A": "São Paulo", "B": "Río de Janeiro", "C": "Brasília", "D": "Buenos Aires"}
r = self_consistency_multiple_choice(pregunta, opciones, n=5)
print(f"Respuesta: {r['respuesta_completa']} (confianza: {r['confianza']:.0%})")
Ejercicio 3: Análisis de confianza
Ejecuta Self-Consistency con N=10 sobre 5 preguntas de distinta dificultad. Grafica la relación entre confianza (mayoría/total) y exactitud de la respuesta. ¿Hay correlación?
Ver solución
preguntas_con_respuesta = [
("¿Cuánto es 7 * 8?", "56"), # Fácil, confianza alta
("¿Cuánto es 23 * 17?", "391"), # Media
("Un tren va a 120km/h, ¿cuánto tarda en 300km?", "2.5"), # Word problem
("Si el radio aumenta 50%, ¿en qué % aumenta el área?", "125"), # Conceptual
("¿Cuántas rotaciones hace una rueda de 2m de circunferencia en 1km?", "500"), # Cálculo
]
analisis = []
for pregunta, correcta in preguntas_con_respuesta:
r = self_consistency(pregunta, n=10)
es_correcta = evaluar_respuesta(r["respuesta"], correcta)
analisis.append({
"pregunta": pregunta[:40],
"respuesta": r["respuesta"],
"correcta": correcta,
"confianza": r["confianza"],
"acertado": es_correcta
})
print(f"Pregunta: {pregunta[:40]}...")
print(f" Pred: {r['respuesta']}, Correcta: {correcta}")
print(f" Confianza: {r['confianza']:.0%}, Acertado: {'✓' if es_correcta else '✗'}")
# Calcular correlación (simplificada):
# Alta confianza (>0.6) + Acertado: True positivo
# Alta confianza + No acertado: Falso positivo
# Baja confianza + Acertado: Falso negativo
# Baja confianza + No acertado: Verdadero negativo
Ejercicio 4: Comparar temperaturas
Experimenta con diferentes valores de temperatura (0.3, 0.5, 0.7, 1.0) para N=5. ¿Qué temperatura produce el mejor balance entre diversidad de razonamiento y convergencia a la respuesta correcta?
Ver solución
problema = "Un comerciante vende un artículo con 25% de ganancia. Si el precio de costo es $80, ¿cuál es el precio de venta?"
temperaturas = [0.3, 0.5, 0.7, 1.0]
for temp in temperaturas:
resultados = []
for _ in range(5): # 5 runs para promediar
r = self_consistency(problema, n=5, temperatura=temp)
resultados.append(r["confianza"])
avg_confianza = sum(resultados) / len(resultados)
# Respuesta correcta: 80 * 1.25 = 100
print(f"Temperature={temp}: confianza promedio={avg_confianza:.0%}")
# Resultado esperado:
# Temperature=0.3: confianza alta (~90%), poca diversidad
# Temperature=0.7: balance (~80%), buena diversidad
# Temperature=1.0: confianza baja (~65%), mucha diversidad
Resumen
- Self-Consistency: Generar N respuestas independientes con temperatura > 0, votar por mayoría
- Mejora empírica: +5-15% en precisión para math/logic/reasoning vs CoT single
- Temperature: 0.5-0.8 es el rango ideal para balance diversidad/convergencia
- N óptimo: 5 para la mayoría de casos; stop temprano si confianza > 80%
- Normalización: Crítica para que el voting funcione (42 vs 42.0 vs "42")
- Cuándo usar: Math word problems, razonamiento lógico, clasificación ambigua
- Cuándo NO usar: Tareas creativas, conversaciones, cuando el modelo definitivamente no sabe la respuesta
Recursos adicionales
- Self-Consistency Improves Chain of Thought Reasoning (Wang et al., 2022) - Paper original
- Large Language Models are Zero-Shot Reasoners (Kojima et al., 2022) - Relacionado con CoT
- OpenAI API - Temperature parameter
- Python Counter documentation
- Prompt Engineering Guide - Self-Consistency
- Benchmark datasets para math: GSM8K