Install ClinicalBERT requirements
masterInstall the required dependency for BERT models using pip:
pip install pytorch-pretrained-bertrepository·master·Indexed 19 days ago
https://github.com/kexinhuang12345/clinicalbertClinicalBERT provides pretraining and fine-tuning weights and scripts for generating contextual representations of clinical notes. It is specifically optimized for hospital readmission prediction tasks, including early notes (2-day and 3-day windows) and discharge summaries. The repository includes tools for training custom readmission models, visualizing BERT attention scores, and loading Gensim-based Word2Vec or FastText clinical models.
Install the required dependency for BERT models using pip:
pip install pytorch-pretrained-bertClinicalBERT expects clinical notes to be organized in a specific directory structure. Data files must be in .csv format and contain the columns TEXT, ID, and Label (representing Note chunks, Admission ID, and the Label of readmission, respectively).
-data
-discharge
-train.csv
-val.csv
-test.csv
-3days
-train.csv
-val.csv
-test.csv
-2days
-test.csvWhen using pretrained weights, ensure your model directory follows this structure. The scripts expect specific subdirectories for different tasks (discharge readmission, early readmission, or pretraining weights).
-model
-discharge_readmission
-bert_config.json
-pytorch_model.bin
-early_readmission
-bert_config.json
-pytorch_model.bin
-pretraining
-bert_config.json
-pytorch_model.bin
-vocab.txtTo perform K-fold cross-validation, the code uses sklearn.model_selection.KFold on the non-readmitted and readmitted ID pools.
Note on indexing: The code treats the first fold as a special case (generated manually) and uses i=0 to represent Fold 2, i=1 for Fold 3, etc., when iterating through the KFold results.
For each fold i:
train_id_folds[i] contains the training IDs.val_id_folds[i] contains the validation IDs.test_id_folds[i] contains the test IDs.These folds can be used to generate complete datasets for Discharge Summaries or Early Notes tasks by saving them to ./good_datasets/fold{i+2}/....
from sklearn.model_selection import KFold
# Setup KFold
kf = KFold(n_splits = 4, shuffle = True, random_state = 1)
# Loop through folds to populate val_id_folds and test_id_folds
# (Logic involves sampling and dropping duplicates to ensure clean splits)Once the ID splits are created, use them to filter the readmission_task.csv file (which contains the actual text/notes) to create task-specific datasets for Discharge Summaries.
To prevent class imbalance in the training set, a sample of non-readmitted notes (dis_train) is concatenated with the actual discharge training notes (discharge_train) to create discharge_train_snippets.csv.
want = pd.read_csv('readmission_task.csv')
discharge_train = want[want.ID.isin(train_id_label.id)]
# Create balanced training snippets
dis_train = want[want.ID.isin(df.sample(n=437, random_state=1))]
discharge_train_snippets = pd.concat([dis_train, discharge_train])
discharge_train_snippets.to_csv('./good_datasets/discharge/train_snippets.csv')Early notes tasks use specific note files (less_3_days_notes.csv or less_2_days_notes.csv).
For the 3-day task:
STAY_DAYS >= 3.For the 2-day task:
STAY_DAYS >= 2.Training sets are augmented with extra non-readmitted snippets to maintain balance.
# 3-day early notes test set filtering
actionable_ID_3days = df_adm1[df_adm1['STAY_DAYS'] >= 3].HADM_ID
test_actionable_id_3days = pd.Series(test_id_folds[i])[pd.Series(test_id_folds[i]).isin(actionable_ID_3days)]
early_test = want_early[want_early.ID.isin(test_actionable_id_3days)]
early_test.to_csv('./good_datasets/3days/test_snippets.csv')To generate pretraining data for Clinical BERT, use the create_pretraining_data.py script. It is recommended to perform pretraining in two stages: first on short sequences (128), then continuing on longer sequences (512) using the first model as the checkpoint.
Stage 1: 128 Max Sequence Length
python create_pretraining_data.py \
--input_file=PRETRAIN_DATA_PATH/clinical_sentences_pretrain.txt \
--output_file=PRETRAIN_DATA_PATH/tf_examples_128.tfrecord \
--vocab_file=INITIAL_MODEL_PATH/vocab.txt \
--do_lower_case=True \
--max_seq_length=128 \
--max_predictions_per_seq=20 \
--masked_lm_prob=0.15 \
--random_seed=12345 \
--dupe_factor=3Stage 2: 512 Max Sequence Length
python create_pretraining_data.py \
--input_file=PRETRAIN_DATA_PATH/clinical_sentences_pretrain.txt \
--output_file=PRETRAIN_DATA_PATH/tf_examples_512.tfrecord \
--vocab_file=INITIAL_MODEL_PATH/vocab.txt \
--do_lower_case=True \
--max_seq_length=512 \
--max_predictions_per_seq=76 \
--masked_lm_prob=0.15 \
--random_seed=12345 \
--dupe_factor=3# Example for 128 max seq
python create_pretraining_data.py \
--input_file=PRETRAIN_DATA_PATH/clinical_sentences_pretrain.txt \
--output_file=PRETRAIN_DATA_PATH/tf_examples_128.tfrecord \
--vocab_file=INITIAL_MODEL_PATH/vocab.txt \
--do_lower_case=True \
--max_seq_length=128 \
--max_predictions_per_seq=20 \
--masked_lm_prob=0.15 \
--random_seed=12345 \
--dupe_factor=3The dataset is split into training, validation, and test sets based on HADM_ID. To ensure balanced classes, a subset of non-readmitted IDs (not_readmit_ID_use) is sampled to match the count of readmitted IDs (readmit_ID).
Split logic:
Results are saved as CSV files containing id and label columns.
# Example of creating the training label dataframe
train_id_label = pd.DataFrame(data = list(zip(id_train, [1]*len(id_train_t)+[0]*len(id_train_f))), columns = ['id','label'])
# Save to disk
train_id_label.to_csv('./good_datasets/good_train_id_label.csv')Clinical text requires specific cleaning to remove de-identification markers and standardize medical abbreviations.
Cleaning Steps:
[ ].1.2..dr. $\rightarrow$ doctor, m.d. $\rightarrow$ md.admission date: and discharge date:.--, __, or ==.Sentence Segmentation:
Use spacy with a sentencizer to split notes into sentences. For very short segments (length < 20), it is recommended to append them to the previous sentence to handle abbreviations that are segmented as single lines.
import re
import string
import spacy
from spacy.lang.en import English
def preprocess1(x):
y=re.sub('\\[(.*?)\\]','',x) # remove de-identified brackets
y=re.sub('[0-9]+\.','',y) # remove 1.2. markers
y=re.sub('dr\.', 'doctor', y)
y=re.sub('m\.d\.', 'md', y)
y=re.sub('admission date:', '', y)
y=re.sub('discharge date:', '', y)
y=re.sub('--|__|==', '', y)
y = y.translate(str.maketrans('', '', string.digits))
y = " ".join(y.split())
return y
def preprocessing(df_notes):
df_notes['TEXT']=df_notes['TEXT'].fillna(' ')
df_notes['TEXT']=df_notes['TEXT'].str.replace('\n',' ')
df_notes['TEXT']=df_notes['TEXT'].str.replace('\r',' ')
df_notes['TEXT']=df_notes['TEXT'].apply(str.strip)
df_notes['TEXT']=df_notes['TEXT'].str.lower()
df_notes['TEXT']=df_notes['TEXT'].apply(lambda x: preprocess1(x))
return df_notes
# Sentence segmentation logic
nlp = English()
nlp.add_pipe(nlp.create_pipe('sentencizer'))
def toSentence(x):
doc = nlp(x)
text=[]
try:
for sent in doc.sents:
st=str(sent).strip()
if len(st)<20:
if len(text)!=0:
text[-1]=' '.join((text[-1],st))
else:
text=[st]
else:
text.append((st))
except:
print(doc)
return textPretraining for Clinical BERT is performed using run_pretraining.py. It is recommended to use a two-stage approach:
max_seq_length of 128.max_seq_length of 512, initializing from the checkpoint produced in Stage 1.Stage 1 Command:
python run_pretraining.py \
--input_file=PRETRAIN_DATA_PATH/tf_examples_128.tfrecord \
--output_dir=PRETRAINED_MODEL_PATH/pretraining_output \
--do_train=True \
--do_eval=True \
--bert_config_file=INITIAL_DATA_PATH/bert_config.json \
--init_checkpoint=INITIAL_DATA_PATH/bert_model.ckpt \
--train_batch_size=64 \
--max_seq_length=128 \
--max_predictions_per_seq=20 \
--num_train_steps=100000 \
--num_warmup_steps=10 \
--learning_rate=2e-5Stage 2 Command:
python run_pretraining.py \
--input_file=PRETRAIN_DATA_PATH/tf_examples_512.tfrecord \
--output_dir=PRETRAINED_MODEL_PATH/pretraining_output \
--do_train=True \
--do_eval=True \
--bert_config_file=INITIAL_DATA_PATH/bert_config.json \
--init_checkpoint=PRETRAINED_MODEL_PATH/pretraining_output_128/model.ckpt-100000 \
--train_batch_size=16 \
--max_seq_length=512 \
--max_predictions_per_seq=76 \
--num_train_steps=100000 \
--learning_rate=2e-5# Stage 1
python run_pretraining.py \
--input_file=PRETRAIN_DATA_PATH/tf_examples_128.tfrecord \
--output_dir=PRETRAINED_MODEL_PATH/pretraining_output \
--do_train=True \
--do_eval=True \
--bert_config_file=INITIAL_DATA_PATH/bert_config.json \
--init_checkpoint=INITIAL_DATA_PATH/bert_model.ckpt \
--train_batch_size=64 \
--max_seq_length=128 \
--max_predictions_per_seq=20 \
--num_train_steps=100000 \
--num_warmup_steps=10 \
--learning_rate=2e-5You can visualize the attention mechanism of a BertForSequenceClassification model by extracting the query and key tensors using PyTorch forward hooks. This allows you to see how much weight each token (query) places on other tokens (keys) in a given text sequence.
To implement this, you need to:
transpose_for_scores helper to reshape the attention heads.register_forward_hook on the query and key modules of a specific encoder layer.matplotlib and seaborn to plot the resulting matrix as a heatmap.import torch
import math
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from pytorch_pretrained_bert import BertTokenizer
# Assuming model, bert_config, and tokenizer are already initialized
def transpose_for_scores(config, x):
new_x_shape = x.size()[:-1] + (config.num_attention_heads, int(config.hidden_size / config.num_attention_heads))
x = x.view(*new_x_shape)
return x.permute(0, 2, 1, 3)
def get_attention_scores(model, i, text):
tokenized = tokenizer.tokenize(text)
indexed_tokens = tokenizer.convert_tokens_to_ids(tokenized)
segment_ids = [0] * len(indexed_tokens)
t_tensor = torch.tensor([indexed_tokens])
s_ids = torch.tensor([segment_ids])
outputs_query = []
outputs_key = []
def hook_query(module, input, output):
outputs_query.append(output)
def hook_key(module, input, output):
outputs_key.append(output)
# Register hooks on the specific layer
model.bert.encoder.layer[i].attention.self.query.register_forward_hook(hook_query)
model.bert.encoder.layer[i].attention.self.key.register_forward_hook(hook_key)
model(t_tensor, s_ids)
query_layer = transpose_for_scores(bert_config, outputs_query[0])
key_layer = transpose_for_scores(bert_config, outputs_key[0])
attention_scores = torch.matmul(query_layer, key_layer.transpose(-1, -2))
attention_scores = attention_scores / math.sqrt(int(bert_config.hidden_size / bert_config.num_attention_heads))
attention_probs = torch.nn.Softmax(dim=-1)(attention_scores)
return attention_probs, tokenized
# Usage
text = 'he has experienced acute on chronic diastolic heart failure'
attention_probs, tokens = get_attention_scores(model, 0, text)
# Extracting a specific head's map (e.g., head 1)
map1 = np.asarray(attention_probs[0][1].detach().numpy())
# Plotting
f, ax = plt.subplots(figsize=(10,10))
ax.imshow(map1, interpolation='nearest', cmap='gray')
ax.set_yticks(range(len(tokens)))
ax.set_yticklabels(tokens)
ax.set_xticks(range(len(tokens)))
ax.set_xticklabels(tokens, rotation=60)
plt.show()To prepare the dataset for readmission prediction, you must process the ADMISSIONS.csv file to calculate the time between discharge and the next admission.
Key steps include:
ADMITTIME, DISCHTIME, and DEATHTIME to datetime objects.SUBJECT_ID and ADMITTIME.NEXT_ADMITTIME and NEXT_ADMISSION_TYPE using a groupby-shift operation.NaT.OUTPUT_LABEL as 1 if the patient is readmitted within 30 days (DAYS_NEXT_ADMIT < 30), and 0 otherwise.DEATHTIME is not null).# Convert times
df_adm.ADMITTIME = pd.to_datetime(df_adm.ADMITTIME, format = '%Y-%m-%d %H:%M:%S', errors = 'coerce')
df_adm.DISCHTIME = pd.to_datetime(df_adm.DISCHTIME, format = '%Y-%m-%d %H:%M:%S', errors = 'coerce')
df_adm.DEATHTIME = pd.to_datetime(df_adm.DEATHTIME, format = '%Y-%m-%d %H:%M:%S', errors = 'coerce')
# Calculate next admission info
df_adm = df_adm.sort_values(['SUBJECT_ID','ADMITTIME'])
df_adm['NEXT_ADMITTIME'] = df_adm.groupby('SUBJECT_ID').ADMITTIME.shift(-1)
df_adm['NEXT_ADMISSION_TYPE'] = df_adm.groupby('SUBJECT_ID').ADMISSION_TYPE.shift(-1)
# Calculate days until next admit
df_adm['DAYS_NEXT_ADMIT']= (df_adm.NEXT_ADMITTIME - df_adm.DISCHTIME).dt.total_seconds()/(24*60*60)
# Define label (1 if readmitted < 30 days)
df_adm1['OUTPUT_LABEL'] = (df_adm1.DAYS_NEXT_ADMIT < 30).astype('int')
# Filter
df_adm1 = df_adm1[df_adm1['ADMISSION_TYPE']!='NEWBORN']
df_adm1 = df_adm1[df_adm1.DEATHTIME.isnull()]