Artículo
· 1 abr, 2025 Lectura de 6 min

d[IA]gnosis: buscando similitudes en nuestra base de datos vectorial y usando LLM para extraer diagnósticos

¡Acabo de caer en la cuenta de que nunca terminé esta serie de artículos!

GIF de Shame On You Meme | Tenor

En el artículo de hoy vamos a echar un vistazo a la producción que se encarga de extraer los diagnósticos en CIE-10 más similares a nuestro texto, de tal forma que podamos seleccionar la opción más adecuada desde nuestro frontend.

Buscando similitudes de diagnósticos:

Desde la pantalla que nos muestra las solicitudes de diagnósticos recibidas en HL7 en nuestra aplicación podremos buscar cuales son los diagnósticos CIE-10 más próximos al texto introducido por el profesional.

Para acelerar el proceso de búsquedas hemos almacenado en nuestra base de datos el texto vectorizado del diagnóstico recibido en el momento de capturar el mensaje de HL7. Para ello hemos implementado un BPL simple que extrae el código del diagnóstico del mensaje y lo envía a un método para generar el vector:

Y aquí está el código que vectoriza el diagnóstico recibido:

ClassMethod GetEncoding(sentence As %String) As %String [ Language = python ]
{
        import sentence_transformers
        # create the model and form the embeddings
        model = sentence_transformers.SentenceTransformer('/iris-shared/model/')
        embeddings = model.encode(sentence, normalize_embeddings=True).tolist() # Convert search phrase into a vector
        # convert the embeddings to a string
        return str(embeddings)
}

De esta manera tenemos nuestros diagnósticos vectorizados, evitando tener que vectorizarlo nuevamente cada vez que tengamos que hacer una búsqueda sobre el mismo. Como podéis ver, estamos usando la librería sentence_transformer para generar el vector con el modelo descargado.

Con todos nuestros diagnósticos recibidos vectorizados y almacenados en nuestra base de datos sólo necesitaremos ejecutar una consulta de tipo SELECT para extraer aquellos diagnósticos CIE-10 más próximos a los diagnósticos recibidos.

Veamos el código del método publicado en nuestro servicio web que retornará los 25 diagnósticos más similares:

ClassMethod GetCodeOptions(idRequest As %String) As %Status
{
	set ret = $$$OK
    try {
        set sql = 
            "SELECT TOP 25 * FROM (SELECT C.CodeId, C.Description, VECTOR_DOT_PRODUCT(C.VectorDescription, R.VectorDescription) AS Similarity FROM ENCODER_Object.Codes C, ENCODER_Object.CodeRequests R WHERE R.ID = ?) WHERE Similarity > 0.5 ORDER BY Similarity DESC"

		set statement = ##class(%SQL.Statement).%New()
		$$$ThrowOnError(statement.%Prepare(sql))
        set rs = statement.%Execute(idRequest)

        set array = []
        while rs.%Next() {
            do array.%Push({
                    "CodeId": (rs.%Get("CodeId")),
                    "Description": (rs.%Get("Description")),
                    "Similarity": (rs.%Get("Similarity"))
                })
        }
        set %response.Status = ..#HTTP200OK
        write array.%ToJSON()

    } catch ex {
        set %response.Status = ..#HTTP400BADREQUEST
        return ex.DisplayString()
    }
    quit ret
}

Si veis la consulta, estamos limitando la búsqueda a 25 resultados y únicamente de aquellos diagnósticos que superen un nivel de similitud de 0.5. Para el cálculo de similitud hemos elegido el método VECTOR_DOT_PRODUCT aunque podríamos haber usado VECTOR_COSINE. Particularmente no he encontrado una diferencia sustancial entre ellos para este caso.

Aquí tenemos el resultado de la búsqueda:

Utilizando LLM para identificar diagnósticos

Hasta ahora sólo hemos hecho una búsqueda sencilla sobre diagnósticos perfectamente identificados, pero...¿Podríamos identificar diagnósticos directamente sobre texto libre?

¡Vamos a intentarlo!

Para esta funcionalidad vamos a hacer uso de Ollama que nos proporcione una API a la que enviar nuestras preguntas a un modelo LLM que elijamos. Si echáis un vistazo a nuestro docker-compose.yml veréis la declaración del contenedor de Ollama:

  ## llm locally installed
  ollama:
    build:
      context: .
      dockerfile: ollama/Dockerfile
    container_name: ollama
    volumes:
    - ./ollama/shared:/ollama-shared
    ports:
      - "11434:11434"

Para la puerta hemos definido en el despliegue del contenedor que se descargue el LLM llama3.2 ¿El motivo? Pues que me ha parecido el que mejor rendimiento me ofreció en las pruebas.

Este es el contenido del fichero entrypoint.sh que se ejecuta en el despliegue del contenedor:

#!/bin/bash

echo "Starting Ollama server..."
ollama serve &
SERVE_PID=$!

echo "Waiting for Ollama server to be active..."
while ! ollama list | grep -q 'NAME'; do
  sleep 1
done

ollama pull llama3.2

wait $SERVE_PID

Para hacer uso de las capacidades de Ollama he modificado la pantalla que nos analiza los textos libres para indicarle que debe usar el LLM para extraer los diagnósticos del texto introducido.

También hemos modificado el business process añadiendo un método que construirá el prompt necesario para que llama3.2 extraiga directamente los diagnósticos:

Method AnalyzeText(text As %String, analysisId As %String, language As %String) As %String [ Language = python ]
{
    import sentence_transformers
    import iris
    import requests

    try:
        url = "http://ollama:11434/api/generate"
        data = {
            "model": "llama3.2",
            "prompt": "Extrae únicamente los diagnósticos del siguiente texto separándolos por , y sin añadir interpretaciones: "+text,
            "stream": False
        }
        response = requests.post(url, json=data)
        analyzedText = response.json()
        
        model = sentence_transformers.SentenceTransformer('/iris-shared/model/')
        phrases = analyzedText['response'].split(",")
        sqlsentence = ""
        # iris.cls("Ens.Util.Log").LogInfo("ENCODER.BP.AnalyzeTextProcess", "AnalyzeText", "Starting process")
        for phraseToAnalyze in phrases :
            if phraseToAnalyze != "":
                embedding = model.encode(phraseToAnalyze, normalize_embeddings=True).tolist()
                sqlsentence = "INSERT INTO ENCODER_Object.TextMatches (CodeId, Description, Similarity, AnalysisId, RawText) SELECT TOP 50 * FROM (SELECT CodeId, Description, VECTOR_DOT_PRODUCT(VectorDescription, TO_VECTOR('"+str(embedding)+"', DECIMAL)) AS Similarity, '"+analysisId+"', '"+phraseToAnalyze+"' FROM ENCODER_Object.Codes) ORDER BY Similarity DESC"                    
                stmt = iris.sql.prepare("INSERT INTO ENCODER_Object.TextMatches (CodeId, Description, Similarity, AnalysisId, RawText) SELECT TOP 50 * FROM (SELECT CodeId, Description, VECTOR_DOT_PRODUCT(VectorDescription, TO_VECTOR(?, DECIMAL)) AS Similarity, ?, ? FROM ENCODER_Object.Codes) WHERE Similarity > 0.65 ORDER BY Similarity DESC")                    
                rs = stmt.execute(str(embedding), analysisId, phraseToAnalyze)        
    except Exception as err:
        iris.cls("Ens.Util.Log").LogInfo("ENCODER.BP.AnalyzeTextProcess", "AnalyzeText", repr(err))
        return repr(err)

    return "Success"
}

El prompt es muy sencillito, posiblemente se podrá mejorar y afinar lo necesario, lo dejo a vuestra voluntad. Este método lo que va a hacer es recuperar la respuesta del LLM separada por comas y almacenar los diagnósticos hallados vectorizados en nuestra base de datos. Aquí tenéis un ejemplo del resultado obtenido:

En la parte inferior derecha podéis ver todos los hallazgos del LLM sobre el texto de la parte inferior izquierda.

Pues ya tendríamos una aplicación que hace uso de modelos LLM para ayudarnos a la codificación de diagnósticos. Como habéis visto, no es para nada complicado de implementar y puede ser una buena base para construir soluciones más complejas y completas.

¡Muchas gracias por vuestro tiempo!

Comentarios (0)1
Inicie sesión o regístrese para continuar