En este escenario, el LLM identifica tanto el Código ICD 1 como el Código ICD 2 como relevantes para la nota médica. Luego, el algoritmo examina los nodos secundarios de cada código. Cada código principal tiene dos nodos secundarios que representan códigos ICD más específicos. A partir del Código ICD 1, el LLM utiliza las descripciones del Código ICD 1.1 y el Código ICD 1.2 junto con la nota médica para determinar los códigos relevantes. El LLM concluye que el Código ICD 1.1 es relevante, mientras que el Código ICD 1.2 no lo es. Dado que el Código ICD 1.1 no tiene más nodos secundarios, el algoritmo verifica si es un código asignable y lo asigna al documento. A continuación, el algoritmo evalúa los nodos secundarios del Código ICD 2. Al invocar nuevamente el LLM, determina que solo el Código ICD 2.1 es relevante. Este es un ejemplo simplificado; en realidad, el árbol ICD es extenso y más profundo, lo que significa que el algoritmo continuará atravesando los hijos de cada nodo relevante hasta llegar al final del árbol o agotar los recorridos válidos.
Reflejos
- Este método no requiere ningún ajuste del LLM; aprovecha la capacidad del LLM para comprender contextualmente la nota médica e identificar dinámicamente los códigos ICD relevantes según las descripciones proporcionadas.
- Además, este artículo muestra que los LLM pueden adaptarse eficazmente a un gran espacio de salida cuando se les proporciona información relevante de forma rápida, superando a PLM-ICD. [6] en códigos raros en términos de métricas macropromedio.
- Esta técnica también supera la línea de base de pedirle directamente al LLM que prediga los códigos ICD de una nota médica en función de su conocimiento paramétrico. Esto resalta el potencial de integrar los LLM con herramientas o conocimiento externo para resolver tareas de codificación clínica.
Desventajas
- El algoritmo invoca el LLM en todos los niveles del árbol. Esto conduce a una gran cantidad de invocaciones de LLM a medida que se recorre el árbol, agravado por la inmensidad del árbol ICD. Esto genera una alta latencia y costos en el procesamiento de un solo documento.
- Como también señalan los autores en el artículo, para predecir correctamente un código relevante, el LLM debe identificar correctamente sus nodos principales en todos los niveles. Incluso si se comete un error en un nivel, el LLM no podrá alcanzar el código final relevante.
- Los autores no pudieron evaluar su método utilizando conjuntos de datos como MIMIC-III debido a limitaciones que prohíben la transferencia de datos a servicios externos como los puntos finales GPT de OpenAI. En cambio, evaluaron el método utilizando el conjunto de pruebas del conjunto de datos CodiEsp. [7,8], que incluye 250 notas médicas. El pequeño tamaño de este conjunto de datos sugiere que aún no se ha establecido la eficacia del método en conjuntos de datos clínicos más grandes.
Todo el código y los recursos relacionados con este artículo están disponibles en este enlace con un espejo del repositorio disponible en mi original repositorio relacionado con blogs. Deseo enfatizar que mi reimplementación no es exactamente idéntica al documento y difiere en formas sutiles de lo que he documentado en el repositorio original. Intenté replicar las indicaciones utilizadas para invocar GPT-3.5 y Llama-70B según los detalles del artículo original. Para traducir los conjuntos de datos del español al inglés, creé mi propio mensaje para hacerlo, ya que no se podía acceder a los detalles en el documento.
Implementemos la técnica para comprender mejor cómo funciona. Como se mencionó, el artículo utiliza el conjunto de pruebas CodiEsp para su evaluación. Este conjunto de datos consta de notas médicas españolas junto con sus códigos ICD. Aunque el conjunto de datos incluye una versión traducida al inglés, los autores señalan que tradujeron las notas médicas en español al inglés utilizando GPT-3.5, que, según afirman, proporcionó una mejora modesta en el rendimiento con respecto al uso de la versión pretraducida. Replicamos esta funcionalidad y traducimos las notas al inglés.
def construct_translation_prompt(medical_note):
"""
Construct a prompt template for translating spanish medical notes to english.Args:
medical_note (str): The medical case note.
Returns:
str: A structured template ready to be used as input for a language model.
"""
translation_prompt = """You are an expert Spanish-to-English translator. You are provided with a clinical note written in Spanish.
You must translate the note into English. You must ensure that you properly translate the medical and technical terms from Spanish to English without any mistakes.
Spanish Medical Note:
{medical_note}"""
return translation_prompt.format(medical_note = medical_note)
Ahora que tenemos listo el corpus de evaluación, implementemos la lógica central para el algoritmo de búsqueda de árbol. Definimos la funcionalidad en obtener_códigos_icd, que acepta la nota médica a procesar, el nombre del modelo y el ajuste de temperatura. El nombre del modelo debe ser “gpt-3.5-turbo-0613” para GPT-3.5 o “meta-llama/Llama-2–70b-chat-hf” para Chat Llama-2 70B. Esta especificación determina el LLM que invocará el algoritmo de búsqueda de árbol durante su procesamiento.
Es posible evaluar GPT-4 utilizando la misma base de código proporcionando el nombre del modelo apropiado, pero decidimos omitirlo porque lleva bastante tiempo.
def get_icd_codes(medical_note, model_name="gpt-3.5-turbo-0613", temperature=0.0):
"""
Identifies relevant ICD-10 codes for a given medical note by querying a language model.This function implements the tree-search algorithm for ICD coding described in https://openreview.net/forum?id=mqnR8rGWkn.
Args:
medical_note (str): The medical note for which ICD-10 codes are to be identified.
model_name (str): The identifier for the language model used in the API (default is 'gpt-3.5-turbo-0613').
Returns:
list of str: A list of confirmed ICD-10 codes that are relevant to the medical note.
"""
assigned_codes = []
candidate_codes = [x.name for x in CHAPTER_LIST]
parent_codes = []
prompt_count = 0
while prompt_count < 50:
code_descriptions = {}
for x in candidate_codes:
description, code = get_name_and_description(x, model_name)
code_descriptions[description] = code
prompt = build_zero_shot_prompt(medical_note, list(code_descriptions.keys()), model_name=model_name)
lm_response = get_response(prompt, model_name, temperature=temperature, max_tokens=500)
predicted_codes = parse_outputs(lm_response, code_descriptions, model_name=model_name)
for code in predicted_codes:
if cm.is_leaf(code["code"]):
assigned_codes.append(code["code"])
else:
parent_codes.append(code)
if len(parent_codes) > 0:
parent_code = parent_codes.pop(0)
candidate_codes = cm.get_children(parent_code["code"])
else:
break
prompt_count += 1
return assigned_codes
Al igual que en el artículo, utilizamos el simple_icd_10_cm biblioteca, que proporciona acceso al árbol ICD-10. Esto nos permite recorrer el árbol, acceder a las descripciones de cada código e identificar códigos válidos. Primero, obtenemos los nodos en el primer nivel del árbol.
import simple_icd_10_cm as cmdef get_name_and_description(code, model_name):
"""
Retrieve the name and description of an ICD-10 code.
Args:
code (str): The ICD-10 code.
Returns:
tuple: A tuple containing the formatted description and the name of the code.
"""
full_data = cm.get_full_data(code).split("\n")
return format_code_descriptions(full_data[3], model_name), full_data[1]
Dentro del bucle obtenemos las descripciones correspondientes a cada uno de los nodos. Ahora, necesitamos construir el mensaje para el LLM basado en la nota médica y las descripciones del código. Creamos las indicaciones para GPT-3.5 y Llama-2 según los detalles proporcionados en el documento.
prompt_template_dict = {"gpt-3.5-turbo-0613" : """[Case note]:
{note}
[Example]:
<example prompt>
Gastro-esophageal reflux disease
Enteropotosis<response>
Gastro-esophageal reflux disease: Yes, Patient was prescribed omeprazole.
Enteropotosis: No.
[Task]:
Consider each of the following ICD-10 code descriptions and evaluate if there are any related mentions in the case note.
Follow the format in the example precisely.
{code_descriptions}""",
"meta-llama/Llama-2-70b-chat-hf": """[Case note]:
{note}
[Example]:
<code descriptions>
* Gastro-esophageal reflux disease
* Enteroptosis
* Acute Nasopharyngitis [Common Cold]
</code descriptions>
<response>
* Gastro-esophageal reflux disease: Yes, Patient was prescribed omeprazole.
* Enteroptosis: No.
* Acute Nasopharyngitis [Common Cold]: No.
</response>
[Task]:
Follow the format in the example response exactly, including the entire description before your (Yes|No) judgement, followed by a newline.
Consider each of the following ICD-10 code descriptions and evaluate if there are any related mentions in the Case note.
{code_descriptions}"""
}
Ahora construimos el mensaje basado en la nota médica y las descripciones del código. Una ventaja para nosotros, en términos de indicaciones y codificación, es que podemos utilizar los mismos abierto biblioteca para interactuar con GPT-3.5 y Llama 2, siempre que Llama-2 se implemente utilizando infraestructura profundaque también apoya la abierto formato para enviar solicitudes al LLM.
def construct_prompt_template(case_note, code_descriptions, model_name):
"""
Construct a prompt template for evaluating ICD-10 code descriptions against a given case note.Args:
case_note (str): The medical case note.
code_descriptions (str): The ICD-10 code descriptions formatted as a single string.
Returns:
str: A structured template ready to be used as input for a language model.
"""
template = prompt_template_dict[model_name]
return template.format(note=case_note, code_descriptions=code_descriptions)
def build_zero_shot_prompt(input_note, descriptions, model_name, system_prompt=""):
"""
Build a zero-shot classification prompt with system and user roles for a language model.
Args:
input_note (str): The input note or query.
descriptions (list of str): List of ICD-10 code descriptions.
system_prompt (str): Optional initial system prompt or instruction.
Returns:
list of dict: A structured list of dictionaries defining the role and content of each message.
"""
if model_name == "meta-llama/Llama-2-70b-chat-hf":
code_descriptions = "\n".join(["* " + x for x in descriptions])
else:
code_descriptions = "\n".join(descriptions)
input_prompt = construct_prompt_template(input_note, code_descriptions, model_name)
return [{"role": "system", "content": system_prompt}, {"role": "user", "content": input_prompt}]
Habiendo construido las indicaciones, ahora invocamos el LLM para obtener la respuesta:
def get_response(messages, model_name, temperature=0.0, max_tokens=500):
"""
Obtain responses from a specified model via the chat-completions API.Args:
messages (list of dict): List of messages structured for API input.
model_name (str): Identifier for the model to query.
temperature (float): Controls randomness of response, where 0 is deterministic.
max_tokens (int): Limit on the number of tokens in the response.
Returns:
str: The content of the response message from the model.
"""
response = client.chat.completions.create(
model=model_name,
messages=messages,
temperature=temperature,
max_tokens=max_tokens
)
return response.choices[0].message.content
¡Genial, hemos obtenido el resultado! A partir de la respuesta, ahora analizamos cada descripción de código para identificar los nodos que el LLM ha considerado relevantes para un mayor recorrido, así como aquellos nodos que el LLM ha rechazado. Dividimos la respuesta de salida en nuevas líneas y dividimos cada respuesta para identificar la predicción del LLM para cada descripción de código.
def remove_noisy_prefix(text):
# Removing numbers or letters followed by a dot and optional space at the beginning of the string
cleaned_text = text.replace("* ", "").strip()
cleaned_text = re.sub(r"^\s*\w+\.\s*", "", cleaned_text)
return cleaned_text.strip()def parse_outputs(output, code_description_map, model_name):
"""
Parse model outputs to confirm ICD-10 codes based on a given description map.
Args:
output (str): The model output containing confirmations.
code_description_map (dict): Mapping of descriptions to ICD-10 codes.
Returns:
list of dict: A list of confirmed codes and their descriptions.
"""
confirmed_codes = []
split_outputs = [x for x in output.split("\n") if x]
for item in split_outputs:
try:
code_description, confirmation = item.split(":", 1)
if model_name == "meta-llama/Llama-2-70b-chat-hf":
code_description = remove_noisy_prefix(code_description)
if confirmation.lower().strip().startswith("yes"):
try:
code = code_description_map[code_description]
confirmed_codes.append({"code": code, "description": code_description})
except Exception as e:
print(str(e) + " Here")
continue
except:
continue
return confirmed_codes
Veamos ahora el resto del ciclo. Hasta ahora, hemos construido el mensaje, obtenido la respuesta del LLM y analizado el resultado para identificar los códigos que el LLM considera relevantes.
while prompt_count < 50:
code_descriptions = {}
for x in candidate_codes:
description, code = get_name_and_description(x, model_name)
code_descriptions[description] = codeprompt = build_zero_shot_prompt(medical_note, list(code_descriptions.keys()), model_name=model_name)
lm_response = get_response(prompt, model_name, temperature=temperature, max_tokens=500)
predicted_codes = parse_outputs(lm_response, code_descriptions, model_name=model_name)
for code in predicted_codes:
if cm.is_leaf(code["code"]):
assigned_codes.append(code["code"])
else:
parent_codes.append(code)
if len(parent_codes) > 0:
parent_code = parent_codes.pop(0)
candidate_codes = cm.get_children(parent_code["code"])
else:
break
prompt_count += 1
Ahora iteramos a través de los códigos predichos y verificamos si cada código es un “hoja” código, que esencialmente garantiza que el código sea un código ICD válido y asignable. Si el código previsto es válido, lo consideramos como una predicción del LLM para esa nota médica. De lo contrario, lo agregamos a nuestros códigos principales y obtenemos los nodos secundarios para recorrer más el árbol ICD. Salimos del bucle si no hay más códigos principales que recorrer.
En teoría, la cantidad de invocaciones de LLM por nota médica puede ser arbitrariamente alta, lo que genera una mayor latencia si el algoritmo atraviesa muchos nodos. Los autores imponen un máximo de 50 indicaciones/invocaciones de LLM por nota médica para finalizar el procesamiento, un límite que también adoptamos en nuestra implementación.
Resultados
Ahora podemos evaluar los resultados del algoritmo de búsqueda de árbol utilizando GPT-3.5 y Llama-2 como LLM. Evaluamos el rendimiento del algoritmo en términos de precisión micropromedio y macropromedio, recuperación y puntuación F1.
Si bien los resultados de la implementación se encuentran aproximadamente en el estadio de las puntuaciones reportadas en el documento, existen algunas diferencias dignas de mención.
- En esta implementación, las métricas micropromedio de GPT-3.5 superan ligeramente las cifras informadas, mientras que las métricas macropromedio están un poco por debajo de los valores informados.
- De manera similar, las métricas micropromedio de Llama-70B coinciden o superan ligeramente las cifras reportadas, pero las métricas macropromedio son más bajas que los valores reportados.
Como se mencionó anteriormente, esta implementación difiere del documento en algunos aspectos menores, los cuales impactan el rendimiento final. por favor refiérase a repositorio vinculado para una discusión más detallada de cómo esta implementación difiere del documento original.
Comprender e implementar este método fue muy revelador para mí en muchos sentidos. Me permitió desarrollar una comprensión más matizada de las fortalezas y debilidades de los modelos de lenguaje grande (LLM) en el caso de codificación clínica. Específicamente, se hizo evidente que cuando los LLM tienen acceso dinámico a información pertinente sobre los códigos, pueden comprender de manera efectiva el contexto clínico e identificar con precisión los códigos relevantes.
Sería interesante explorar si la utilización de LLM como agentes para la codificación clínica podría mejorar aún más el rendimiento. Dada la abundancia de fuentes de conocimiento externas para textos biomédicos y clínicos en forma de artículos o gráficos de conocimiento, los agentes LLM podrían usarse en flujos de trabajo que analizan documentos médicos con una granularidad más fina. También podrían invocar herramientas que les permitan consultar conocimiento externo sobre la marcha si es necesario, para llegar al código final.
Reconocimiento
¡Muchísimas gracias a Joseph, el autor principal de este artículo, por aclarar mis dudas sobre la evaluación de este método!
[1] https://www.who.int/standards/classifications/classification-of-diseases
[2] Johnson, AE, Pollard, TJ, Shen, L., Lehman, LWH, Feng, M., Ghassemi, M.,… y Mark, RG (2016). MIMIC-III, una base de datos de cuidados críticos de libre acceso Sci. Datos, 3(1), 1.
[3] Agrawal, M., Hegselmann, S., Lang, H., Kim, Y. y Sontag, D. (2022). Los modelos de lenguaje grandes son extractores de información clínica de pocas posibilidades. preimpresión de arXiv arXiv:2205.12689.
[4] Zhou, H., Li, M., Xiao, Y., Yang, H. y Zhang, R. (2023). Marco de instrucción adaptativa de ejemplo (LEAP) de instrucción LLM para la extracción de relaciones clínicas. medRxiv: el servidor de preimpresión para ciencias de la salud2023.12.15.23300059. https://doi.org/10.1101/2023.12.15.23300059
[5] Boyle, JS, Kascenas, A., Lok, P., Liakata, M. y O’Neil, AQ (octubre de 2023). Codificación clínica automatizada utilizando modelos de lenguaje grandes disponibles en el mercado. En Taller de Modelos Generativos Profundos para la Salud NeurIPS 2023.
[6] Huang, CW, Tsai, SC y Chen, YN (2022). PLM-ICD: codificación ICD automática con modelos de lenguaje previamente entrenados. preimpresión de arXiv arXiv:2207.05289.
[7] Miranda-Escalada, A., González-Agirre, A., Armengol-Estapé, J. y Krallinger, M. (2020). Descripción general de la codificación clínica automática: anotaciones, pautas y soluciones para casos clínicos que no están en inglés en CodiEsp Track de CLEF eHealth 2020. CLEF (Notas de trabajo), 2020.
[8] Miranda-Escalada, A., González-Agirre, A. y Krallinger, M. (2020). Corpus CodiEsp: casos clínicos españoles gold standard codificados en CIE10 (CIE10) — eHealth CLEF2020 (1.4) [Data set]. Zenodo. https://doi.org/10.5281/zenodo.3837305 (CC POR 4.0)