Processamento de linguagem natural de texto biomédico (BioNLP) usando scispaCy
Como identificar doenças, medicamentos e dosagens a partir de transcrições de registros médicos
A mineração de texto biomédico e processamento de linguagem natural (BioNLP) é um domínio de pesquisa interessante que lida com o processamento de dados de periódicos, registros médicos e outros documentos biomédicos. Considerando a disponibilidade de literatura biomédica, tem havido um interesse crescente em extrair informações, relacionamentos e percepções de dados textuais. No entanto, a organização desestruturada e a complexidade do domínio dos documentos biomédicos tornam essas tarefas difíceis. Felizmente, alguns pacotes Python de NLP legais podem nos ajudar com isso!
scispaCy é um pacote Python contendo modelos spaCy para processar texto biomédico, científico ou clínico. Os recursos mais alucinantes do spaCy são os modelos de rede neural para marcação, análise, reconhecimento de entidade nomeada (NER), classificação de texto e muito mais. Adicione modelos scispaCy em cima dele e podemos fazer tudo isso no domínio biomédico!
Aqui, veremos como usar os modelos de NER do scispaCy para identificar nomes de medicamentos e doenças mencionados em um conjunto de dados de transcrição médica. Além disso, vamos combinar NER e correspondência baseada em regras para extrair os nomes e dosagens dos medicamentos relatados em cada transcrição.
Lista de Conteúdos
Requisitos
- Python 3
- pandas
- spacy>=3.0
- scispacy
Você pode instalar facilmente utilizando pip install
.
Também precisamos baixar e instalar o modelo NER do scispaCy. Para instalar o modelo en_ner_bc5cdr_md
use o seguinte comando:
pip install https://s3-us-west-2.amazonaws.com/ai2-s2-scispacy/releases/v0.4.0/en_ner_bc5cdr_md-0.4.0.tar.gz
Para versões atualizadas ou outros modelos, consulte a página do scispaCy.
Conjunto de dados
Dados médicos não estruturados, como transcrições médicas, são muito difíceis de encontrar. Aqui, estamos usando um conjunto de dados de transcrição médica extraído do site MTSamples por Tara Boyle e disponibilizado no Kaggle.
import pandas as pd
med_transcript = pd.read_csv("mtsamples.csv", index_col=0)
med_transcript.info()
med_transcript.head()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 4999 entries, 0 to 4998
Data columns (total 5 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 description 4999 non-null object
1 medical_specialty 4999 non-null object
2 sample_name 4999 non-null object
3 transcription 4966 non-null object
4 keywords 3931 non-null object
dtypes: object(5)
memory usage: 234.3+ KB
description | medical_specialty | sample_name | transcription | keywords | |
---|---|---|---|---|---|
0 | A 23-year-old white female presents with comp... | Allergy / Immunology | Allergic Rhinitis | SUBJECTIVE:, This 23-year-old white female pr... | allergy / immunology, allergic rhinitis, aller... |
1 | Consult for laparoscopic gastric bypass. | Bariatrics | Laparoscopic Gastric Bypass Consult - 2 | PAST MEDICAL HISTORY:, He has difficulty climb... | bariatrics, laparoscopic gastric bypass, weigh... |
2 | Consult for laparoscopic gastric bypass. | Bariatrics | Laparoscopic Gastric Bypass Consult - 1 | HISTORY OF PRESENT ILLNESS: , I have seen ABC ... | bariatrics, laparoscopic gastric bypass, heart... |
3 | 2-D M-Mode. Doppler. | Cardiovascular / Pulmonary | 2-D Echocardiogram - 1 | 2-D M-MODE: , ,1. Left atrial enlargement wit... | cardiovascular / pulmonary, 2-d m-mode, dopple... |
4 | 2-D Echocardiogram | Cardiovascular / Pulmonary | 2-D Echocardiogram - 2 | 1. The left ventricular cavity size and wall ... | cardiovascular / pulmonary, 2-d, doppler, echo... |
O conjunto de dados tem quase 5.000 registros, mas vamos trabalhar com uma pequena subamostra aleatória para que não demore muito para processar. Também precisamos descartar todas as linhas cujas transcrições estão faltando.
med_transcript.dropna(subset=['transcription'], inplace=True)
med_transcript_small = med_transcript.sample(n=100, replace=False, random_state=42)
med_transcript_small.info()
med_transcript_small.head()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 100 entries, 3162 to 3581
Data columns (total 5 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 description 100 non-null object
1 medical_specialty 100 non-null object
2 sample_name 100 non-null object
3 transcription 100 non-null object
4 keywords 78 non-null object
dtypes: object(5)
memory usage: 4.7+ KB
description | medical_specialty | sample_name | transcription | keywords | |
---|---|---|---|---|---|
3162 | Markedly elevated PT INR despite stopping Cou... | Hematology - Oncology | Hematology Consult - 1 | HISTORY OF PRESENT ILLNESS:, The patient is w... | NaN |
1981 | Intercostal block from fourth to tenth interc... | Pain Management | Intercostal block - 1 | PREPROCEDURE DIAGNOSIS:, Chest pain secondary... | pain management, xylocaine, marcaine, intercos... |
1361 | The patient is a 65-year-old female who under... | SOAP / Chart / Progress Notes | Lobectomy - Followup | HISTORY OF PRESENT ILLNESS: , The patient is a... | soap / chart / progress notes, non-small cell ... |
3008 | Construction of right upper arm hemodialysis ... | Nephrology | Hemodialysis Fistula Construction | PREOPERATIVE DIAGNOSIS: , End-stage renal dise... | nephrology, end-stage renal disease, av dialys... |
4943 | Bronchoscopy with brush biopsies. Persistent... | Cardiovascular / Pulmonary | Bronchoscopy - 8 | PREOPERATIVE DIAGNOSIS: , Persistent pneumonia... | cardiovascular / pulmonary, persistent pneumon... |
Vamos pegar uma transcrição para ver como podemos trabalhar com NER:
sample_transcription = med_transcript_small['transcription'].iloc[0]
print(sample_transcription[:1000]) # prints just the first 1000 characters
HISTORY OF PRESENT ILLNESS:, The patient is well known to me for a history of iron-deficiency anemia due to chronic blood loss from colitis. We corrected her hematocrit last year with intravenous (IV) iron. Ultimately, she had a total proctocolectomy done on 03/14/2007 to treat her colitis. Her course has been very complicated since then with needing multiple surgeries for removal of hematoma. This is partly because she was on anticoagulation for a right arm deep venous thrombosis (DVT) she had early this year, complicated by septic phlebitis.,Chart was reviewed, and I will not reiterate her complex history.,I am asked to see the patient again because of concerns for coagulopathy.,She had surgery again last month to evacuate a pelvic hematoma, and was found to have vancomycin resistant enterococcus, for which she is on multiple antibiotics and followed by infectious disease now.,She is on total parenteral nutrition (TPN) as well.,LABORATORY DATA:, Labs today showed a white blood
Então, podemos ver muitas entidades nesta transcrição. Existem nomes de medicamentos, doenças e exames, por exemplo. O texto foi extraído de uma página da web e podemos identificar as diferentes seções do prontuário como “HISTORY OF PRESENT ILLNESS” e “LABORATORY DATA”, mas isso varia de registro para registro.
Reconhecimento de entidade nomeada
O reconhecimento de entidade nomeada (NER) é uma subtarefa do processamento de linguagem natural usada para identificar e classificar entidades nomeadas mencionadas em texto não estruturado em categorias predefinidas. scispaCy
tem diferentes modelos para identificar diferentes tipos de entidade e você pode verificá-los aqui.
Vamos usar o modelo NER treinado no corpus BC5CDR (en_ner_bc5cdr_md
). Este corpus consiste em 1.500 artigos PubMed com 4.409 produtos químicos anotados, 5.818 doenças e 3.116 interações químico-doenças. Não se esqueça de baixar e instalar o modelo.
import scispacy
import spacy
nlp = spacy.load("en_ner_bc5cdr_md")
spacy.load
retornará um objeto Language
contendo todos os componentes e dados necessários para processar o texto. Este objeto é normalmente chamado de nlp
na documentação e nos tutoriais. Chamar o objeto nlp
em uma string de texto retornará um objeto Doc
processado com o texto dividido em palavras e anotado.
Vamos obter todas as entidades identificadas e imprimir seu texto, posição inicial, posição final e tipo:
doc = nlp(sample_transcription)
print("TEXT", "START", "END", "ENTITY TYPE")
for ent in doc.ents:
print(ent.text, ent.start_char, ent.end_char, ent.label_)
TEXT START END ENTITY TYPE
iron-deficiency anemia 79 101 DISEASE
chronic blood loss 109 127 DISEASE
colitis 133 140 DISEASE
iron 203 207 CHEMICAL
...
vancomycin 781 791 CHEMICAL
infectious disease 873 891 DISEASE
improved.,PT 1348 1360 CHEMICAL
vitamin K 1503 1512 CHEMICAL
uric acid 1830 1839 CHEMICAL
bilirubin 1853 1862 CHEMICAL
Creatinine 1911 1921 CHEMICAL
...
Compazine 2474 2483 CHEMICAL
Zofran 2487 2493 CHEMICAL
epistaxis 2629 2638 DISEASE
bleeding 3057 3065 DISEASE
edema.,CARDIAC 3109 3123 CHEMICAL
adenopathy 3156 3166 DISEASE
...
Podemos ver que o modelo corretamente identificou e rotulou doenças como anemia por deficiência de ferro, perda crônica de sangue e muito mais. Muitos medicamentos também foram identificados, como vancomicina, Compazine, Zofran. O modelo também pode identificar moléculas comuns testadas em laboratório, como creatinina, ferro, bilirrubina, ácido úrico.
Mas nem tudo é perfeito. Veja como alguns tokens são estranhamente classificados como produtos químicos, possivelmente devido à pontuação:
- improved.,PT 1348 1360 CHEMICAL
- edema.,CARDIAC 3109 3123 CHEMICAL
Os caracteres de pontuação geralmente são removidos nas etapas de pré-processamento de NLP, mas não podemos removê-los todos aqui, caso contrário, podemos perder nomes de substâncias químicas e bagunçar quantidades como dosagem de medicamentos. No entanto, podemos resolver esse problema removendo as marcas “.,” que parecem separar algumas seções da transcrição. É importante conhecer seus dados e o domínio de seus dados para uma melhor compreensão de seus resultados.
import re
med_transcript_small['transcription'] = med_transcript_small['transcription'].apply(lambda x: re.sub('(\.,)', ". ", x))
Também podemos verificar as entidades usando o visualizador displacy
:
from spacy import displacy
displacy.render(doc[:100], style='ent', jupyter=True) # here I am printing just the first 100 tokens
Correspondência baseada em regras
A correspondência baseada em regras é semelhante às expressões regulares, mas os mecanismos e componentes de correspondência baseada em regras do spaCy fornecem acesso aos tokens dentro do documento e seus relacionamentos. Podemos combinar isso com os modelos de NER para identificar algum padrão que inclui nossas entidades.
Vamos extrair do texto os nomes dos medicamentos e suas dosagens relatadas. Isso poderia ser útil para identificar possíveis erros de medicação, verificando se as dosagens estão de acordo com padrões e diretrizes recomendados.
from spacy.matcher import Matcher
pattern = [{'ENT_TYPE':'CHEMICAL'}, {'LIKE_NUM': True}, {'IS_ASCII': True}]
matcher = Matcher(nlp.vocab)
matcher.add("DRUG_DOSE", [pattern])
O código acima cria um padrão para identificar uma sequência de três tokens:
- Um token cujo tipo de entidade é CHEMICAL (nome do medicamento)
- Um token que se assemelha a um número (dosagem)
- Um token que consiste em caracteres ASCII (unidades, como mg ou mL)
Em seguida, inicializamos o Matcher
com um vocabulário. O matcher deve sempre compartilhar o mesmo vocabulário com os documentos em que irá trabalhar, então usamos o vocabulário do objeto nlp
. Em seguida, adicionamos esse padrão ao matcher e fornecemos a ele uma ID.
Agora podemos percorrer todas as transcrições e extrair o texto que corresponde a este padrão:
for transcription in med_transcript_small['transcription']:
doc = nlp(transcription)
matches = matcher(doc)
for match_id, start, end in matches:
string_id = nlp.vocab.strings[match_id] # get string representation
span = doc[start:end] # the matched span
print(string_id, start, end, span.text)
DRUG_DOSE 137 140 Xylocaine 20 mL
DRUG_DOSE 141 144 Marcaine 0.25%
DRUG_DOSE 208 211 Aspirin 81 mg
DRUG_DOSE 216 219 Spiriva 10 mcg
DRUG_DOSE 399 402 nifedipine 10 mg
DRUG_DOSE 226 229 aspirin one tablet
DRUG_DOSE 245 248 Warfarin 2.5 mg
DRUG_DOSE 67 70 Topamax 100 mg
...
DRUG_DOSE 193 196 Metamucil one pack
DRUG_DOSE 207 210 Nexium 40 mg
DRUG_DOSE 1133 1136 Naprosyn one p.o
DRUG_DOSE 290 293 Lidocaine 1%
DRUG_DOSE 37 40 Altrua 60,
...
DRUG_DOSE 74 77 Lidocaine 1.5%
DRUG_DOSE 209 212 Dilantin 300 mg
DRUG_DOSE 217 220 Haloperidol 1 mg
DRUG_DOSE 225 228 Dexamethasone 4 mg
DRUG_DOSE 234 237 Docusate 100 mg
DRUG_DOSE 250 253 Ibuprofen 600 mg
DRUG_DOSE 258 261 Zantac 150 mg
...
DRUG_DOSE 204 207 epinephrine 7 ml
DRUG_DOSE 214 217 Percocet 5,
DRUG_DOSE 55 58 . 4.
DRUG_DOSE 146 149 . 4.
DRUG_DOSE 2409 2412 Naprosyn 375 mg
DRUG_DOSE 141 144 Wellbutrin 300 mg
DRUG_DOSE 146 149 Xanax 0.25 mg
DRUG_DOSE 158 161 omeprazole 20 mg
...
Legal, conseguimos!
Extraímos com sucesso medicamentos e dosagens, incluindo diferentes tipos de unidades como mg, mL, %.
Conclusões
Aprendemos como usar alguns recursos de scispaCy e spaCy, como NER e correspondência de base de regra. Usamos um modelo de NER, mas existem muitos outros e você deve dar uma olhada neles. Por exemplo, o modelo en_ner_bionlp13cg_md
pode identificar partes anatômicas, tecidos, tipos de células e muito mais. Imagine o que mais você poderia fazer com isso!
Também não nos concentramos muito nas etapas de pré-processamento, mas elas são fundamentais para obter melhores resultados. Não se esqueça de explorar seus dados e adaptar as etapas de pré-processamento às tarefas de NLP que deseja realizar.
Referências
Neumann, M., King, D., Beltagy, I., & Ammar, W. (2019). Scispacy: Fast and robust models for biomedical natural language processing. arXiv preprint arXiv:1902.07669.
Honnibal, M., Montani, I., Van Landeghem, S., Boyd, A. (2020). spaCy: Industrial-strength Natural Language Processing in Python.