Upload model
Browse files- config.json +14 -55
- configuration_cxrmate_ed.py +39 -0
- configuration_uniformer.py +51 -0
- create_section_files.py +7 -5
- dataset.py +70 -241
- generation_config.json +1 -1
- model.safetensors +2 -2
- modelling_cxrmate_ed.py +412 -337
- modelling_uniformer.py +4 -4
- prepare_dataset.py +558 -0
- utils.py +20 -0
config.json
CHANGED
@@ -21,11 +21,6 @@
|
|
21 |
"diversity_penalty": 0.0,
|
22 |
"do_sample": false,
|
23 |
"early_stopping": false,
|
24 |
-
"ed_module_columns": [
|
25 |
-
"triage_chiefcomplaint",
|
26 |
-
"triage_pain",
|
27 |
-
"vitalsign_pain"
|
28 |
-
],
|
29 |
"encoder_no_repeat_ngram_size": 0,
|
30 |
"eos_token_id": 2,
|
31 |
"exponential_decay_length_penalty": null,
|
@@ -34,16 +29,12 @@
|
|
34 |
"forced_eos_token_id": null,
|
35 |
"hidden_act": "silu",
|
36 |
"hidden_size": 768,
|
|
|
37 |
"id2label": {
|
38 |
"0": "LABEL_0",
|
39 |
"1": "LABEL_1"
|
40 |
},
|
41 |
"include_time_delta": true,
|
42 |
-
"index_value_encoder_config": {
|
43 |
-
"edstays": 40,
|
44 |
-
"triage": 7,
|
45 |
-
"vitalsign": 1177
|
46 |
-
},
|
47 |
"index_value_encoder_intermediate_size": 2048,
|
48 |
"initializer_range": 0.02,
|
49 |
"intermediate_size": 3072,
|
@@ -56,10 +47,6 @@
|
|
56 |
"length_penalty": 1.0,
|
57 |
"max_length": 20,
|
58 |
"max_position_embeddings": 2048,
|
59 |
-
"mimic_cxr_columns": [
|
60 |
-
"indication",
|
61 |
-
"history"
|
62 |
-
],
|
63 |
"min_length": 0,
|
64 |
"model_type": "llama",
|
65 |
"no_repeat_ngram_size": 0,
|
@@ -69,7 +56,6 @@
|
|
69 |
"num_hidden_layers": 6,
|
70 |
"num_key_value_heads": 12,
|
71 |
"num_return_sequences": 1,
|
72 |
-
"num_token_types": 19,
|
73 |
"output_attentions": false,
|
74 |
"output_hidden_states": false,
|
75 |
"output_scores": false,
|
@@ -77,6 +63,10 @@
|
|
77 |
"prefix": null,
|
78 |
"pretraining_tp": 1,
|
79 |
"problem_type": null,
|
|
|
|
|
|
|
|
|
80 |
"pruned_heads": {},
|
81 |
"remove_invalid_values": false,
|
82 |
"repetition_penalty": 1.0,
|
@@ -85,39 +75,19 @@
|
|
85 |
"rms_norm_eps": 1e-06,
|
86 |
"rope_scaling": null,
|
87 |
"rope_theta": 10000.0,
|
88 |
-
"section_ids": [
|
89 |
-
12,
|
90 |
-
13
|
91 |
-
],
|
92 |
"sep_token_id": null,
|
93 |
"suppress_tokens": null,
|
|
|
|
|
|
|
|
|
|
|
94 |
"task_specific_params": null,
|
95 |
"temperature": 1.0,
|
96 |
"tf_legacy_loss": false,
|
97 |
"tie_encoder_decoder": false,
|
98 |
"tie_word_embeddings": false,
|
99 |
"time_delta_monotonic_inversion": true,
|
100 |
-
"token_type_to_token_type_id": {
|
101 |
-
"comparison": 15,
|
102 |
-
"edstays": 1,
|
103 |
-
"findings": 12,
|
104 |
-
"history": 11,
|
105 |
-
"image": 14,
|
106 |
-
"impression": 13,
|
107 |
-
"indication": 10,
|
108 |
-
"medrecon": 0,
|
109 |
-
"medrecon_name": 6,
|
110 |
-
"mimic_cxr_2_0_0_metadata": 5,
|
111 |
-
"previous_findings": 16,
|
112 |
-
"previous_image": 18,
|
113 |
-
"previous_impression": 17,
|
114 |
-
"pyxis": 4,
|
115 |
-
"triage": 2,
|
116 |
-
"triage_chiefcomplaint": 7,
|
117 |
-
"triage_pain": 8,
|
118 |
-
"vitalsign": 3,
|
119 |
-
"vitalsign_pain": 9
|
120 |
-
},
|
121 |
"tokenizer_class": null,
|
122 |
"top_k": 50,
|
123 |
"top_p": 1.0,
|
@@ -126,14 +96,12 @@
|
|
126 |
"typical_p": 1.0,
|
127 |
"use_bfloat16": false,
|
128 |
"use_cache": true,
|
129 |
-
"vocab_size": 30000
|
130 |
-
"zero_time_delta_value": 1.0
|
131 |
},
|
132 |
"encoder": {
|
133 |
"_name_or_path": "",
|
134 |
"add_cross_attention": false,
|
135 |
"architectures": null,
|
136 |
-
"attention_probs_dropout_prob": 0.0,
|
137 |
"attn_drop_rate": 0.0,
|
138 |
"bad_words_ids": null,
|
139 |
"begin_suppress_tokens": null,
|
@@ -160,24 +128,18 @@
|
|
160 |
512
|
161 |
],
|
162 |
"encoder_no_repeat_ngram_size": 0,
|
163 |
-
"encoder_stride": 16,
|
164 |
"eos_token_id": null,
|
165 |
"exponential_decay_length_penalty": null,
|
166 |
"finetuning_task": null,
|
167 |
"forced_bos_token_id": null,
|
168 |
"forced_eos_token_id": null,
|
169 |
"head_dim": 64,
|
170 |
-
"hidden_act": "gelu",
|
171 |
-
"hidden_dropout_prob": 0.0,
|
172 |
-
"hidden_size": 768,
|
173 |
"id2label": {
|
174 |
"0": "LABEL_0",
|
175 |
"1": "LABEL_1"
|
176 |
},
|
177 |
"image_size": 384,
|
178 |
"in_chans": 3,
|
179 |
-
"initializer_range": 0.02,
|
180 |
-
"intermediate_size": 3072,
|
181 |
"is_decoder": false,
|
182 |
"is_encoder_decoder": false,
|
183 |
"label2id": {
|
@@ -189,14 +151,11 @@
|
|
189 |
"max_length": 20,
|
190 |
"min_length": 0,
|
191 |
"mlp_ratio": 4,
|
192 |
-
"model_type": "
|
193 |
"no_repeat_ngram_size": 0,
|
194 |
-
"num_attention_heads": 12,
|
195 |
"num_beam_groups": 1,
|
196 |
"num_beams": 1,
|
197 |
-
"num_channels": 3,
|
198 |
"num_classes": 1000,
|
199 |
-
"num_hidden_layers": 12,
|
200 |
"num_return_sequences": 1,
|
201 |
"output_attentions": false,
|
202 |
"output_hidden_states": false,
|
@@ -234,8 +193,8 @@
|
|
234 |
"typical_p": 1.0,
|
235 |
"use_bfloat16": false
|
236 |
},
|
237 |
-
"model_type": "
|
238 |
"tie_word_embeddings": false,
|
239 |
"torch_dtype": "float32",
|
240 |
-
"transformers_version": "4.
|
241 |
}
|
|
|
21 |
"diversity_penalty": 0.0,
|
22 |
"do_sample": false,
|
23 |
"early_stopping": false,
|
|
|
|
|
|
|
|
|
|
|
24 |
"encoder_no_repeat_ngram_size": 0,
|
25 |
"eos_token_id": 2,
|
26 |
"exponential_decay_length_penalty": null,
|
|
|
29 |
"forced_eos_token_id": null,
|
30 |
"hidden_act": "silu",
|
31 |
"hidden_size": 768,
|
32 |
+
"history": 0,
|
33 |
"id2label": {
|
34 |
"0": "LABEL_0",
|
35 |
"1": "LABEL_1"
|
36 |
},
|
37 |
"include_time_delta": true,
|
|
|
|
|
|
|
|
|
|
|
38 |
"index_value_encoder_intermediate_size": 2048,
|
39 |
"initializer_range": 0.02,
|
40 |
"intermediate_size": 3072,
|
|
|
47 |
"length_penalty": 1.0,
|
48 |
"max_length": 20,
|
49 |
"max_position_embeddings": 2048,
|
|
|
|
|
|
|
|
|
50 |
"min_length": 0,
|
51 |
"model_type": "llama",
|
52 |
"no_repeat_ngram_size": 0,
|
|
|
56 |
"num_hidden_layers": 6,
|
57 |
"num_key_value_heads": 12,
|
58 |
"num_return_sequences": 1,
|
|
|
59 |
"output_attentions": false,
|
60 |
"output_hidden_states": false,
|
61 |
"output_scores": false,
|
|
|
63 |
"prefix": null,
|
64 |
"pretraining_tp": 1,
|
65 |
"problem_type": null,
|
66 |
+
"prompt_report_sections_filter": [
|
67 |
+
"indication",
|
68 |
+
"history"
|
69 |
+
],
|
70 |
"pruned_heads": {},
|
71 |
"remove_invalid_values": false,
|
72 |
"repetition_penalty": 1.0,
|
|
|
75 |
"rms_norm_eps": 1e-06,
|
76 |
"rope_scaling": null,
|
77 |
"rope_theta": 10000.0,
|
|
|
|
|
|
|
|
|
78 |
"sep_token_id": null,
|
79 |
"suppress_tokens": null,
|
80 |
+
"tables_filter": [
|
81 |
+
"mimic_cxr_sectioned",
|
82 |
+
"triage",
|
83 |
+
"medrecon"
|
84 |
+
],
|
85 |
"task_specific_params": null,
|
86 |
"temperature": 1.0,
|
87 |
"tf_legacy_loss": false,
|
88 |
"tie_encoder_decoder": false,
|
89 |
"tie_word_embeddings": false,
|
90 |
"time_delta_monotonic_inversion": true,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
91 |
"tokenizer_class": null,
|
92 |
"top_k": 50,
|
93 |
"top_p": 1.0,
|
|
|
96 |
"typical_p": 1.0,
|
97 |
"use_bfloat16": false,
|
98 |
"use_cache": true,
|
99 |
+
"vocab_size": 30000
|
|
|
100 |
},
|
101 |
"encoder": {
|
102 |
"_name_or_path": "",
|
103 |
"add_cross_attention": false,
|
104 |
"architectures": null,
|
|
|
105 |
"attn_drop_rate": 0.0,
|
106 |
"bad_words_ids": null,
|
107 |
"begin_suppress_tokens": null,
|
|
|
128 |
512
|
129 |
],
|
130 |
"encoder_no_repeat_ngram_size": 0,
|
|
|
131 |
"eos_token_id": null,
|
132 |
"exponential_decay_length_penalty": null,
|
133 |
"finetuning_task": null,
|
134 |
"forced_bos_token_id": null,
|
135 |
"forced_eos_token_id": null,
|
136 |
"head_dim": 64,
|
|
|
|
|
|
|
137 |
"id2label": {
|
138 |
"0": "LABEL_0",
|
139 |
"1": "LABEL_1"
|
140 |
},
|
141 |
"image_size": 384,
|
142 |
"in_chans": 3,
|
|
|
|
|
143 |
"is_decoder": false,
|
144 |
"is_encoder_decoder": false,
|
145 |
"label2id": {
|
|
|
151 |
"max_length": 20,
|
152 |
"min_length": 0,
|
153 |
"mlp_ratio": 4,
|
154 |
+
"model_type": "uniformer",
|
155 |
"no_repeat_ngram_size": 0,
|
|
|
156 |
"num_beam_groups": 1,
|
157 |
"num_beams": 1,
|
|
|
158 |
"num_classes": 1000,
|
|
|
159 |
"num_return_sequences": 1,
|
160 |
"output_attentions": false,
|
161 |
"output_hidden_states": false,
|
|
|
193 |
"typical_p": 1.0,
|
194 |
"use_bfloat16": false
|
195 |
},
|
196 |
+
"model_type": "encoder-decoder",
|
197 |
"tie_word_embeddings": false,
|
198 |
"torch_dtype": "float32",
|
199 |
+
"transformers_version": "4.39.3"
|
200 |
}
|
configuration_cxrmate_ed.py
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from transformers.configuration_utils import PretrainedConfig
|
2 |
+
from transformers.utils import logging
|
3 |
+
|
4 |
+
logger = logging.get_logger(__name__)
|
5 |
+
|
6 |
+
|
7 |
+
class EncoderDecoderConfig(PretrainedConfig):
|
8 |
+
|
9 |
+
model_type = "encoder-decoder"
|
10 |
+
is_composition = True
|
11 |
+
|
12 |
+
def __init__(self, **kwargs):
|
13 |
+
super().__init__(**kwargs)
|
14 |
+
if "encoder" not in kwargs or "decoder" not in kwargs:
|
15 |
+
raise ValueError(
|
16 |
+
f"A configuraton of type {self.model_type} cannot be instantiated because "
|
17 |
+
f"both `encoder` and `decoder` sub-configurations were not passed, only {kwargs}"
|
18 |
+
)
|
19 |
+
|
20 |
+
self.encoder = kwargs.pop("encoder")
|
21 |
+
self.decoder = kwargs.pop("decoder")
|
22 |
+
self.is_encoder_decoder = True
|
23 |
+
|
24 |
+
@classmethod
|
25 |
+
def from_encoder_decoder_configs(
|
26 |
+
cls, encoder_config: PretrainedConfig, decoder_config: PretrainedConfig, **kwargs
|
27 |
+
) -> PretrainedConfig:
|
28 |
+
r"""
|
29 |
+
Instantiate a [`EncoderDecoderConfig`] (or a derived class) from a pre-trained encoder model configuration and
|
30 |
+
decoder model configuration.
|
31 |
+
|
32 |
+
Returns:
|
33 |
+
[`EncoderDecoderConfig`]: An instance of a configuration object
|
34 |
+
"""
|
35 |
+
logger.info("Set `config.is_decoder=True` and `config.add_cross_attention=True` for decoder_config")
|
36 |
+
decoder_config.is_decoder = True
|
37 |
+
decoder_config.add_cross_attention = True
|
38 |
+
|
39 |
+
return cls(encoder=encoder_config, decoder=decoder_config, **kwargs)
|
configuration_uniformer.py
ADDED
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from transformers import PretrainedConfig
|
2 |
+
from transformers.utils import logging
|
3 |
+
|
4 |
+
logger = logging.get_logger(__name__)
|
5 |
+
|
6 |
+
|
7 |
+
class UniFormerWithProjectionHeadConfig(PretrainedConfig):
|
8 |
+
|
9 |
+
model_type = 'uniformer'
|
10 |
+
|
11 |
+
def __init__(
|
12 |
+
self,
|
13 |
+
projection_size=None,
|
14 |
+
embed_dim=[64, 128, 320, 512],
|
15 |
+
image_size=384,
|
16 |
+
in_chans=3,
|
17 |
+
depth=[5, 8, 20, 7],
|
18 |
+
patch_size=[4, 2, 2, 2],
|
19 |
+
head_dim=64,
|
20 |
+
mlp_ratio=4,
|
21 |
+
qkv_bias=True,
|
22 |
+
num_classes=1000,
|
23 |
+
qk_scale=None,
|
24 |
+
representation_size=None,
|
25 |
+
drop_rate=0.0,
|
26 |
+
drop_path_rate=0.3,
|
27 |
+
attn_drop_rate=0.0,
|
28 |
+
conv_stem=False,
|
29 |
+
layer_norm_eps=1e-6,
|
30 |
+
**kwargs,
|
31 |
+
):
|
32 |
+
super().__init__(
|
33 |
+
layer_norm_eps=layer_norm_eps,
|
34 |
+
image_size=image_size,
|
35 |
+
qkv_bias=qkv_bias,
|
36 |
+
**kwargs,
|
37 |
+
)
|
38 |
+
self.projection_size = projection_size
|
39 |
+
self.embed_dim = embed_dim
|
40 |
+
self.in_chans = in_chans
|
41 |
+
self.depth = depth
|
42 |
+
self.patch_size = patch_size
|
43 |
+
self.head_dim = head_dim
|
44 |
+
self.mlp_ratio = mlp_ratio
|
45 |
+
self.num_classes = num_classes
|
46 |
+
self.qk_scale = qk_scale
|
47 |
+
self.representation_size = representation_size
|
48 |
+
self.drop_rate = drop_rate
|
49 |
+
self.drop_path_rate = drop_path_rate
|
50 |
+
self.attn_drop_rate = attn_drop_rate
|
51 |
+
self.conv_stem = conv_stem
|
create_section_files.py
CHANGED
@@ -4,8 +4,10 @@ from pathlib import Path
|
|
4 |
|
5 |
from tqdm import tqdm
|
6 |
|
7 |
-
|
8 |
-
from .section_parser import custom_mimic_cxr_rules, section_text
|
|
|
|
|
9 |
|
10 |
|
11 |
def list_rindex(l, s):
|
@@ -98,7 +100,7 @@ def create_section_files(reports_path, output_path, no_split):
|
|
98 |
# exist the radiologist has usually written the report
|
99 |
# in the comparison section
|
100 |
idx = -1
|
101 |
-
for sn in ('impression', 'findings', 'indication', 'history', 'last_paragraph', 'comparison'):
|
102 |
if sn in section_names:
|
103 |
idx = list_rindex(section_names, sn)
|
104 |
break
|
@@ -112,7 +114,7 @@ def create_section_files(reports_path, output_path, no_split):
|
|
112 |
patient_studies.append([s_stem, sections[idx].strip()])
|
113 |
|
114 |
study_sectioned = [s_stem]
|
115 |
-
for sn in ('impression', 'findings', 'indication', 'history', 'last_paragraph', 'comparison'):
|
116 |
if sn in section_names:
|
117 |
idx = list_rindex(section_names, sn)
|
118 |
study_sectioned.append(sections[idx].strip())
|
@@ -125,7 +127,7 @@ def create_section_files(reports_path, output_path, no_split):
|
|
125 |
with open(output_path / 'mimic_cxr_sectioned.csv', 'w') as fp:
|
126 |
csvwriter = csv.writer(fp)
|
127 |
# write header
|
128 |
-
csvwriter.writerow(['study', 'impression', 'findings', 'indication', 'history', 'last_paragraph', 'comparison'])
|
129 |
for row in study_sections:
|
130 |
csvwriter.writerow(row)
|
131 |
|
|
|
4 |
|
5 |
from tqdm import tqdm
|
6 |
|
7 |
+
try:
|
8 |
+
from .section_parser import custom_mimic_cxr_rules, section_text
|
9 |
+
except ImportError:
|
10 |
+
from section_parser import custom_mimic_cxr_rules, section_text
|
11 |
|
12 |
|
13 |
def list_rindex(l, s):
|
|
|
100 |
# exist the radiologist has usually written the report
|
101 |
# in the comparison section
|
102 |
idx = -1
|
103 |
+
for sn in ('impression', 'findings', 'indication', 'history', 'technique', 'last_paragraph', 'comparison'):
|
104 |
if sn in section_names:
|
105 |
idx = list_rindex(section_names, sn)
|
106 |
break
|
|
|
114 |
patient_studies.append([s_stem, sections[idx].strip()])
|
115 |
|
116 |
study_sectioned = [s_stem]
|
117 |
+
for sn in ('impression', 'findings', 'indication', 'history', 'technique', 'last_paragraph', 'comparison'):
|
118 |
if sn in section_names:
|
119 |
idx = list_rindex(section_names, sn)
|
120 |
study_sectioned.append(sections[idx].strip())
|
|
|
127 |
with open(output_path / 'mimic_cxr_sectioned.csv', 'w') as fp:
|
128 |
csvwriter = csv.writer(fp)
|
129 |
# write header
|
130 |
+
csvwriter.writerow(['study', 'impression', 'findings', 'indication', 'history', 'technique', 'last_paragraph', 'comparison'])
|
131 |
for row in study_sections:
|
132 |
csvwriter.writerow(row)
|
133 |
|
dataset.py
CHANGED
@@ -1,253 +1,82 @@
|
|
1 |
-
import
|
|
|
2 |
|
3 |
-
import lmdb
|
4 |
-
import pandas as pd
|
5 |
import torch
|
6 |
-
from torch.utils.data import Dataset
|
7 |
-
from torchvision.io import decode_image, read_image
|
8 |
|
9 |
-
|
10 |
-
VIEW_ORDER = ['LPO', 'RAO', 'LAO', 'SWIMMERS', 'XTABLE LATERAL', 'LL', 'LATERAL', 'AP AXIAL', 'AP RLD', 'AP LLD', 'AP', 'PA RLD', 'PA LLD', 'PA']
|
11 |
|
12 |
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
Study ID & ED stay ID subset. Examples are indexed by the study identifier.
|
21 |
-
Information from the ED module is added by finding the study_id that is within
|
22 |
-
the timespan of the stay_id for the subject_id. The history and indication
|
23 |
-
sections are also included.
|
24 |
-
"""
|
25 |
-
def __init__(
|
26 |
-
self,
|
27 |
-
split,
|
28 |
-
records,
|
29 |
-
mimic_cxr_jpg_lmdb_path=None,
|
30 |
-
mimic_cxr_dir=None,
|
31 |
-
max_images_per_study=None,
|
32 |
-
transforms=None,
|
33 |
-
images=True,
|
34 |
-
columns='study_id, dicom_id, subject_id, findings, impression',
|
35 |
-
and_condition='',
|
36 |
-
study_id_inclusion_list=None,
|
37 |
-
return_images=True,
|
38 |
-
ed_module=True,
|
39 |
-
extension='jpg',
|
40 |
-
):
|
41 |
-
"""
|
42 |
-
Argument/s:
|
43 |
-
split - 'train', 'validate', or 'test'.
|
44 |
-
records - MIMIC-CXR & MIMIC-IV-ED records class instance.
|
45 |
-
mimic_cxr_jpg_lmdb_path - JPG database for MIMIC-CXR-JPG.
|
46 |
-
mimic_cxr_dir - Path to the MIMIC-CXR directory containing the patient study subdirectories with the JPG or DCM images.
|
47 |
-
max_images_per_study - the maximum number of images per study.
|
48 |
-
transforms - torchvision transformations.
|
49 |
-
colour_space - PIL target colour space.
|
50 |
-
images - flag to return processed images.
|
51 |
-
columns - which columns to query on.
|
52 |
-
and_condition - AND condition to add to the SQL query.
|
53 |
-
study_id_inclusion_list - studies not in this list are excluded.
|
54 |
-
return_images - return CXR images for the study as tensors.
|
55 |
-
ed_module - use the ED module.
|
56 |
-
extension - 'jpg' or 'dcm'.
|
57 |
-
"""
|
58 |
-
super(StudyIDEDStayIDSubset, self).__init__()
|
59 |
-
self.split = split
|
60 |
-
self.mimic_cxr_jpg_lmdb_path = mimic_cxr_jpg_lmdb_path
|
61 |
-
self.mimic_cxr_dir = mimic_cxr_dir
|
62 |
-
self.records = records
|
63 |
-
self.max_images_per_study = max_images_per_study
|
64 |
-
self.transforms = transforms
|
65 |
-
self.images = images
|
66 |
-
self.columns = columns
|
67 |
-
self.and_condition = and_condition
|
68 |
-
self.return_images = return_images
|
69 |
-
self.ed_module = ed_module
|
70 |
-
self.extension = extension
|
71 |
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
assert (mimic_cxr_jpg_lmdb_path is None) != (mimic_cxr_dir is None), 'Either "mimic_cxr_jpg_lmdb_path" or "mimic_cxr_dir" can be set.'
|
77 |
-
|
78 |
-
if self.mimic_cxr_dir is not None and self.mimic_cxr_jpg_lmdb_path is None:
|
79 |
-
if self.extension == 'jpg':
|
80 |
-
if 'physionet.org/files/mimic-cxr-jpg/2.0.0/files' not in self.mimic_cxr_dir:
|
81 |
-
self.mimic_cxr_dir = os.path.join(self.mimic_cxr_dir, 'physionet.org/files/mimic-cxr-jpg/2.0.0/files')
|
82 |
-
elif self.extension == 'dcm':
|
83 |
-
if 'physionet.org/files/mimic-cxr/2.0.0/files' not in self.mimic_cxr_dir:
|
84 |
-
self.mimic_cxr_dir = os.path.join(self.mimic_cxr_dir, 'physionet.org/files/mimic-cxr/2.0.0/files')
|
85 |
-
|
86 |
-
query = f"""
|
87 |
-
SELECT {columns}
|
88 |
-
FROM mimic_cxr
|
89 |
-
WHERE split = '{split}'
|
90 |
-
{and_condition}
|
91 |
-
ORDER BY study_id
|
92 |
-
"""
|
93 |
-
|
94 |
-
# For multi-image, the study identifiers make up the training examples:
|
95 |
-
df = self.records.connect.sql(query).df()
|
96 |
-
|
97 |
-
# Drop studies that don't have a findings or impression section:
|
98 |
-
df = df.dropna(subset=['findings', 'impression'], how='any')
|
99 |
-
|
100 |
-
# This study has two rows in edstays (removed as it causes issues):
|
101 |
-
if self.ed_module:
|
102 |
-
df = df[df['study_id'] != 59128861]
|
103 |
-
|
104 |
-
# Exclude studies not in list:
|
105 |
-
if study_id_inclusion_list is not None:
|
106 |
-
df = df[df['study_id'].isin(study_id_inclusion_list)]
|
107 |
-
|
108 |
-
# Example study identifiers for the subset:
|
109 |
-
self.examples = df['study_id'].unique().tolist()
|
110 |
-
|
111 |
-
# Record statistics:
|
112 |
-
self.num_study_ids = len(self.examples)
|
113 |
-
self.num_dicom_ids = len(df['dicom_id'].unique().tolist())
|
114 |
-
self.num_subject_ids = len(df['subject_id'].unique().tolist())
|
115 |
-
|
116 |
-
# Prepare the LMDB .jpg database:
|
117 |
-
if self.mimic_cxr_jpg_lmdb_path is not None:
|
118 |
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
|
|
|
|
|
|
|
|
|
|
124 |
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
study = self.records.connect.sql(
|
137 |
-
f"""
|
138 |
-
SELECT dicom_id, study_id, subject_id, study_datetime, ViewPosition
|
139 |
-
FROM mimic_cxr
|
140 |
-
WHERE (study_id = {study_id});
|
141 |
"""
|
142 |
-
|
143 |
-
subject_id = study.iloc[0, study.columns.get_loc('subject_id')]
|
144 |
-
study_id = study.iloc[0, study.columns.get_loc('study_id')]
|
145 |
-
study_datetime = study['study_datetime'].max()
|
146 |
-
|
147 |
-
example_dict = {
|
148 |
-
'study_ids': study_id,
|
149 |
-
'subject_id': subject_id,
|
150 |
-
'index': index,
|
151 |
-
}
|
152 |
-
|
153 |
-
example_dict.update(self.records.return_mimic_cxr_features(study_id))
|
154 |
-
|
155 |
-
if self.ed_module:
|
156 |
-
edstays = self.records.connect.sql(
|
157 |
-
f"""
|
158 |
-
SELECT stay_id, intime, outtime
|
159 |
-
FROM edstays
|
160 |
-
WHERE (subject_id = {subject_id})
|
161 |
-
AND intime < '{study_datetime}'
|
162 |
-
AND outtime > '{study_datetime}';
|
163 |
-
"""
|
164 |
-
).df()
|
165 |
-
|
166 |
-
assert len(edstays) <= 1
|
167 |
-
stay_id = edstays.iloc[0, edstays.columns.get_loc('stay_id')] if not edstays.empty else None
|
168 |
-
self.records.clear_start_end_times()
|
169 |
-
example_dict.update(self.records.return_ed_module_features(stay_id, study_datetime))
|
170 |
-
|
171 |
-
example_dict['stay_ids'] = stay_id
|
172 |
-
|
173 |
-
if self.return_images:
|
174 |
-
example_dict['images'], example_dict['image_time_deltas'] = self.get_images(study, study_datetime)
|
175 |
-
|
176 |
-
return example_dict
|
177 |
-
|
178 |
-
def get_images(self, example, reference_time):
|
179 |
-
"""
|
180 |
-
Get the image/s for a given example.
|
181 |
-
|
182 |
-
Argument/s:
|
183 |
-
example - dataframe for the example.
|
184 |
-
reference_time - reference_time for time delta.
|
185 |
-
|
186 |
-
Returns:
|
187 |
-
The image/s for the example
|
188 |
-
"""
|
189 |
-
|
190 |
-
# Sample if over max_images_per_study. Only allowed during training:
|
191 |
-
if len(example) > self.max_images_per_study:
|
192 |
-
assert self.split == 'train'
|
193 |
-
example = example.sample(n=self.max_images_per_study, axis=0)
|
194 |
-
|
195 |
-
# Order by ViewPostion:
|
196 |
-
example['ViewPosition'] = example['ViewPosition'].astype(pd.CategoricalDtype(categories=VIEW_ORDER, ordered=True))
|
197 |
-
|
198 |
-
# Sort the DataFrame based on the categorical column
|
199 |
-
example = example.sort_values(by=['study_datetime', 'ViewPosition'])
|
200 |
-
|
201 |
-
# Load and pre-process each CXR:
|
202 |
-
images, time_deltas = [], []
|
203 |
-
for _, row in example.iterrows():
|
204 |
-
images.append(
|
205 |
-
self.load_and_preprocess_image(
|
206 |
-
row['subject_id'],
|
207 |
-
row['study_id'],
|
208 |
-
row['dicom_id'],
|
209 |
-
),
|
210 |
-
)
|
211 |
-
time_deltas.append(self.records.compute_time_delta(row['study_datetime'], reference_time, to_tensor=False))
|
212 |
-
|
213 |
-
if self.transforms is not None:
|
214 |
-
images = torch.stack(images, 0)
|
215 |
-
return images, time_deltas
|
216 |
-
|
217 |
-
def load_and_preprocess_image(self, subject_id, study_id, dicom_id):
|
218 |
-
"""
|
219 |
-
Load and preprocess an image using torchvision.transforms.v2:
|
220 |
-
https://pytorch.org/vision/stable/auto_examples/transforms/plot_transforms_getting_started.html#sphx-glr-auto-examples-transforms-plot-transforms-getting-started-py
|
221 |
-
|
222 |
-
Argument/s:
|
223 |
-
subject_id - subject identifier.
|
224 |
-
study_id - study identifier.
|
225 |
-
dicom_id - DICOM identifier.
|
226 |
-
|
227 |
-
Returns:
|
228 |
-
image - Tensor of the CXR.
|
229 |
-
"""
|
230 |
-
|
231 |
-
if self.extension == 'jpg':
|
232 |
|
233 |
-
if
|
234 |
-
|
235 |
-
# Convert to bytes:
|
236 |
-
key = bytes(dicom_id, 'utf-8')
|
237 |
-
|
238 |
-
# Retrieve image:
|
239 |
-
image = bytearray(self.txn.get(key))
|
240 |
-
image = torch.frombuffer(image, dtype=torch.uint8)
|
241 |
-
image = decode_image(image)
|
242 |
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
252 |
|
253 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import itertools
|
2 |
+
from typing import List
|
3 |
|
|
|
|
|
4 |
import torch
|
|
|
|
|
5 |
|
6 |
+
from .utils import compute_time_delta
|
|
|
7 |
|
8 |
|
9 |
+
class PriorsDataset:
|
10 |
+
def __init__(self, dataset, history, time_delta_map):
|
11 |
+
self.dataset = dataset
|
12 |
+
self.history = history
|
13 |
+
self.study_id_to_index = dict(zip(dataset['study_id'], range(len(dataset))))
|
14 |
+
self.time_delta_map = time_delta_map
|
15 |
+
self.inf_time_delta_value = time_delta_map(float('inf'))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
|
17 |
+
def __getitem__(self, idx):
|
18 |
+
batch = self.dataset[idx]
|
19 |
+
|
20 |
+
if self.history:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
|
22 |
+
# Prior studies:
|
23 |
+
prior_study_indices = [
|
24 |
+
None if i is None else [self.study_id_to_index[j] for j in i[:self.history]] for i in batch['prior_study_ids']
|
25 |
+
]
|
26 |
+
prior_studies = [None if i is None else [self.dataset[j] for j in i] for i in prior_study_indices]
|
27 |
+
|
28 |
+
# Prior time deltas:
|
29 |
+
time_deltas = [
|
30 |
+
None if i is None else [compute_time_delta(k['latest_study_datetime'], j, self.time_delta_map, to_tensor=False) for k in i] for i, j in zip(prior_studies, batch['latest_study_datetime'])
|
31 |
+
]
|
32 |
|
33 |
+
# Prior findings and impressions:
|
34 |
+
batch['prior_findings'] = [
|
35 |
+
None if i is None else [j['findings'] for j in i] for i in prior_studies
|
36 |
+
]
|
37 |
+
batch['prior_impression'] = [
|
38 |
+
None if i is None else [j['findings'] for j in i] for i in prior_studies
|
39 |
+
]
|
40 |
+
batch['prior_findings_time_delta'] = time_deltas.copy()
|
41 |
+
batch['prior_impression_time_delta'] = time_deltas.copy()
|
42 |
+
|
43 |
+
# Prior images:
|
|
|
|
|
|
|
|
|
|
|
44 |
"""
|
45 |
+
Note:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
46 |
|
47 |
+
Random selection of max_train_images_per_study from the study if the number of images for a study exceeds max_train_images_per_study is performed in train_set_transform and test_set_transform.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
48 |
|
49 |
+
Sorting the images based on the view is done in test_set_transform.
|
50 |
+
|
51 |
+
No need to do it here.
|
52 |
+
"""
|
53 |
+
prior_images = [
|
54 |
+
torch.cat(
|
55 |
+
[
|
56 |
+
torch.empty(0, *batch['images'].shape[-3:])
|
57 |
+
] if i is None else [j['images'] for j in i]
|
58 |
+
) for i in prior_studies
|
59 |
+
]
|
60 |
+
prior_images = torch.nn.utils.rnn.pad_sequence(prior_images, batch_first=True, padding_value=0.0)
|
61 |
+
batch['images'] = torch.cat([batch['images'], prior_images], dim=1)
|
62 |
+
prior_image_time_deltas = [
|
63 |
+
None if i is None else list(itertools.chain.from_iterable([y] * x['images'].shape[0] for x, y in zip(i, j)))
|
64 |
+
for i, j in zip(prior_studies, time_deltas)
|
65 |
+
]
|
66 |
+
max_len = max((len(item) for item in prior_image_time_deltas if item is not None), default=0)
|
67 |
+
prior_image_time_deltas = [i + [self.inf_time_delta_value] * (max_len - len(i)) if i else [self.inf_time_delta_value] * max_len for i in prior_image_time_deltas]
|
68 |
+
batch['image_time_deltas'] = [i + j for i, j in zip(batch['image_time_deltas'], prior_image_time_deltas)]
|
69 |
+
|
70 |
+
return batch
|
71 |
|
72 |
+
def __len__(self):
|
73 |
+
return len(self.dataset)
|
74 |
+
|
75 |
+
def __getattr__(self, name):
|
76 |
+
return getattr(self.dataset, name)
|
77 |
+
|
78 |
+
def __getitems__(self, keys: List):
|
79 |
+
batch = self.__getitem__(keys)
|
80 |
+
n_examples = len(batch[next(iter(batch))])
|
81 |
+
return [{col: array[i] for col, array in batch.items()} for i in range(n_examples)]
|
82 |
+
|
generation_config.json
CHANGED
@@ -3,5 +3,5 @@
|
|
3 |
"bos_token_id": 1,
|
4 |
"eos_token_id": 2,
|
5 |
"pad_token_id": 4,
|
6 |
-
"transformers_version": "4.
|
7 |
}
|
|
|
3 |
"bos_token_id": 1,
|
4 |
"eos_token_id": 2,
|
5 |
"pad_token_id": 4,
|
6 |
+
"transformers_version": "4.39.3"
|
7 |
}
|
model.safetensors
CHANGED
@@ -1,3 +1,3 @@
|
|
1 |
version https://git-lfs.github.com/spec/v1
|
2 |
-
oid sha256:
|
3 |
-
size
|
|
|
1 |
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:ffbf3e699a139ad98f20f8e057cd085586aea444b4b015471d697b43b440c14e
|
3 |
+
size 789958760
|
modelling_cxrmate_ed.py
CHANGED
@@ -1,33 +1,32 @@
|
|
|
|
1 |
import math
|
2 |
import os
|
3 |
-
|
4 |
-
from pathlib import Path
|
5 |
from typing import Optional, Tuple, Union
|
6 |
|
7 |
-
import
|
8 |
-
import pandas as pd
|
9 |
import torch
|
10 |
import transformers
|
11 |
from torch.nn import CrossEntropyLoss
|
12 |
-
from
|
|
|
13 |
from transformers import PreTrainedTokenizerFast, VisionEncoderDecoderModel
|
14 |
from transformers.configuration_utils import PretrainedConfig
|
15 |
from transformers.modeling_outputs import Seq2SeqLMOutput
|
16 |
from transformers.modeling_utils import PreTrainedModel
|
17 |
-
from transformers.models.vision_encoder_decoder.configuration_vision_encoder_decoder import (
|
18 |
-
VisionEncoderDecoderConfig,
|
19 |
-
)
|
20 |
from transformers.utils import logging
|
21 |
|
22 |
-
from .
|
23 |
-
from .dataset import
|
24 |
-
from .lmdb_jpg import prepare_mimic_cxr_jpg_lmdb
|
25 |
from .modelling_uniformer import MultiUniFormerWithProjectionHead
|
26 |
-
from .
|
27 |
-
from .
|
28 |
|
29 |
logger = logging.get_logger(__name__)
|
30 |
|
|
|
|
|
|
|
31 |
|
32 |
def create_lookup_table(df, columns, start_idx):
|
33 |
df = df.groupby(columns).head(1)[columns].sort_values(by=columns)
|
@@ -49,12 +48,12 @@ class FNNEncoder(torch.nn.Module):
|
|
49 |
|
50 |
class MIMICIVEDCXRMultimodalModel(VisionEncoderDecoderModel):
|
51 |
|
52 |
-
config_class =
|
53 |
base_model_prefix = "vision_encoder_decoder"
|
54 |
main_input_name = "input_ids"
|
55 |
supports_gradient_checkpointing = True
|
56 |
|
57 |
-
def __init__(
|
58 |
self,
|
59 |
config: Optional[PretrainedConfig] = None,
|
60 |
encoder: Optional[PreTrainedModel] = None,
|
@@ -70,7 +69,7 @@ class MIMICIVEDCXRMultimodalModel(VisionEncoderDecoderModel):
|
|
70 |
if config is None and (encoder is None or decoder is None):
|
71 |
raise ValueError("Either a configuration or an encoder and a decoder has to be provided.")
|
72 |
if config is None:
|
73 |
-
config =
|
74 |
else:
|
75 |
if not isinstance(config, self.config_class):
|
76 |
raise ValueError(f"Config: {config} has to be of type {self.config_class}")
|
@@ -111,29 +110,50 @@ class MIMICIVEDCXRMultimodalModel(VisionEncoderDecoderModel):
|
|
111 |
assert not config.decoder.is_encoder_decoder
|
112 |
assert 'pad_token_id' in self.decoder.config.__dict__
|
113 |
assert 'time_delta_monotonic_inversion' in self.decoder.config.__dict__
|
114 |
-
assert 'zero_time_delta_value' in self.decoder.config.__dict__
|
115 |
assert 'add_time_deltas' in self.decoder.config.__dict__
|
|
|
|
|
|
|
116 |
|
117 |
assert isinstance(self.decoder.config.time_delta_monotonic_inversion, bool)
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
130 |
if self.decoder.config.add_time_deltas:
|
131 |
self.time_delta_encoder = FNNEncoder(
|
132 |
num_features=1,
|
133 |
intermediate_size=self.decoder.config.index_value_encoder_intermediate_size,
|
134 |
decoder_hidden_size=self.decoder.config.hidden_size,
|
135 |
)
|
136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
137 |
|
138 |
@classmethod
|
139 |
def from_encoder_decoder_pretrained(
|
@@ -281,7 +301,7 @@ class MIMICIVEDCXRMultimodalModel(VisionEncoderDecoderModel):
|
|
281 |
decoder = transformers.AutoModelForCausalLM.from_pretrained(decoder_pretrained_model_name_or_path, **kwargs_decoder)
|
282 |
|
283 |
# instantiate config with corresponding kwargs
|
284 |
-
config =
|
285 |
|
286 |
# make sure input & output embeddings is not tied
|
287 |
config.tie_word_embeddings = False
|
@@ -292,13 +312,13 @@ class MIMICIVEDCXRMultimodalModel(VisionEncoderDecoderModel):
|
|
292 |
|
293 |
def forward(
|
294 |
self,
|
|
|
|
|
|
|
295 |
decoder_input_ids: Optional[torch.LongTensor] = None,
|
296 |
-
decoder_attention_mask: Optional[torch.FloatTensor] = None,
|
297 |
-
decoder_token_type_ids: Optional[torch.LongTensor] = None,
|
298 |
encoder_outputs: Optional[Tuple[torch.FloatTensor]] = None,
|
299 |
past_key_values: Optional[Tuple[Tuple[torch.FloatTensor]]] = None,
|
300 |
decoder_inputs_embeds: Optional[torch.FloatTensor] = None,
|
301 |
-
decoder_position_ids: Optional[torch.LongTensor] = None,
|
302 |
labels: Optional[torch.LongTensor] = None,
|
303 |
use_cache: Optional[bool] = None,
|
304 |
output_attentions: Optional[bool] = None,
|
@@ -313,10 +333,7 @@ class MIMICIVEDCXRMultimodalModel(VisionEncoderDecoderModel):
|
|
313 |
argument[len("decoder_") :]: value for argument, value in kwargs.items() if argument.startswith("decoder_")
|
314 |
}
|
315 |
|
316 |
-
assert decoder_position_ids is not None
|
317 |
-
assert decoder_attention_mask is not None
|
318 |
assert decoder_attention_mask.dtype == torch.long, f'The dtype for {decoder_attention_mask} was {decoder_attention_mask.dtype}. It should be torch.long'
|
319 |
-
assert decoder_token_type_ids is not None
|
320 |
|
321 |
if decoder_inputs_embeds is None:
|
322 |
decoder_inputs_embeds = self.decoder.get_input_embeddings()(decoder_input_ids)
|
@@ -362,7 +379,6 @@ class MIMICIVEDCXRMultimodalModel(VisionEncoderDecoderModel):
|
|
362 |
special_token_ids,
|
363 |
prompt_attention_mask,
|
364 |
prompt_position_ids,
|
365 |
-
token_type_id_sections=None,
|
366 |
past_key_values=None,
|
367 |
use_cache=None,
|
368 |
**kwargs,
|
@@ -387,7 +403,10 @@ class MIMICIVEDCXRMultimodalModel(VisionEncoderDecoderModel):
|
|
387 |
# `inputs_embeds` are only to be used in the 1st generation step:
|
388 |
inputs_embeds = torch.cat([kwargs['decoder_inputs_embeds'], self.decoder.get_input_embeddings()(input_ids)], dim=1)
|
389 |
|
390 |
-
decoder_token_type_ids = self.token_ids_to_token_type_ids(
|
|
|
|
|
|
|
391 |
decoder_token_type_ids = torch.cat(
|
392 |
[
|
393 |
kwargs['decoder_token_type_ids'],
|
@@ -411,7 +430,11 @@ class MIMICIVEDCXRMultimodalModel(VisionEncoderDecoderModel):
|
|
411 |
decoder_position_ids.masked_fill_(report_attention_mask == 0, 1)
|
412 |
|
413 |
# Always place token_ids_to_token_type_ids_past_key_values before input_ids = input_ids[:, remove_prefix_length:]:
|
414 |
-
decoder_token_type_ids = self.token_ids_to_token_type_ids_past_key_values(
|
|
|
|
|
|
|
|
|
415 |
decoder_position_ids = decoder_position_ids[:, -1:]
|
416 |
|
417 |
past_length = past_key_values[0][0].shape[2]
|
@@ -437,7 +460,7 @@ class MIMICIVEDCXRMultimodalModel(VisionEncoderDecoderModel):
|
|
437 |
)
|
438 |
return input_dict
|
439 |
|
440 |
-
def token_ids_to_token_type_ids(self, token_ids, special_token_ids, token_type_id_sections
|
441 |
"""
|
442 |
Extract token type identifiers from the token identifiers.
|
443 |
|
@@ -480,7 +503,7 @@ class MIMICIVEDCXRMultimodalModel(VisionEncoderDecoderModel):
|
|
480 |
|
481 |
return token_type_ids
|
482 |
|
483 |
-
def token_ids_to_token_type_ids_past_key_values(self, token_ids, special_token_ids, token_type_id_sections
|
484 |
"""
|
485 |
Extract token type identifiers from the token identifiers if past != None. Make sure to input all the
|
486 |
token_ids (e.g., do not input input_ids = input_ids[:, remove_prefix_length:] from prepare_inputs_for_generation).
|
@@ -649,7 +672,7 @@ class MIMICIVEDCXRMultimodalModel(VisionEncoderDecoderModel):
|
|
649 |
|
650 |
return tuple(sections.values())
|
651 |
|
652 |
-
def
|
653 |
"""
|
654 |
Tokenize the text columns from MIMIC-IV ED and MIMIC-CXR (excluding the findings and impression sections).
|
655 |
Time deltas for the input_ids are also prepared here.
|
@@ -662,7 +685,7 @@ class MIMICIVEDCXRMultimodalModel(VisionEncoderDecoderModel):
|
|
662 |
cxr - dictionary containing the input_ids, token_type_ids, and attention_mask for MIMIC-CXR columns.
|
663 |
"""
|
664 |
|
665 |
-
batch_size = len(kwargs['
|
666 |
|
667 |
tokenized = {
|
668 |
'input_ids': {i: [] for i in range(batch_size)},
|
@@ -671,34 +694,37 @@ class MIMICIVEDCXRMultimodalModel(VisionEncoderDecoderModel):
|
|
671 |
'attention_mask': torch.empty(batch_size, 0, 1, device=self.device),
|
672 |
}
|
673 |
|
674 |
-
for
|
|
|
|
|
675 |
if i in kwargs:
|
676 |
if f'{i}_time_delta' not in kwargs:
|
677 |
-
kwargs[f'{i}_time_delta'] = [[self.
|
678 |
for x, (y, z) in enumerate(zip(kwargs[i], kwargs[f'{i}_time_delta'])):
|
679 |
if y is not None:
|
680 |
assert isinstance(y, list)
|
681 |
assert isinstance(z, list)
|
682 |
for text, time_delta in zip(y, z):
|
683 |
-
|
684 |
-
|
685 |
-
|
686 |
-
tokenized['token_type_ids'][x].append(
|
687 |
-
torch.full(
|
688 |
-
(1, tokenized['input_ids'][x][-1].shape[-1]),
|
689 |
-
self.decoder.config.token_type_to_token_type_id[i],
|
690 |
-
dtype=torch.long,
|
691 |
-
device=self.device,
|
692 |
)
|
693 |
-
|
694 |
-
|
695 |
-
|
696 |
-
|
697 |
-
|
698 |
-
|
699 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
700 |
)
|
701 |
-
)
|
702 |
|
703 |
tokenized['input_ids'] = [torch.cat(j, dim=1).T if j else torch.empty(0, 1, dtype=torch.long, device=self.device) for j in tokenized['input_ids'].values()]
|
704 |
tokenized['token_type_ids'] = [torch.cat(j, dim=1).T if j else torch.empty(0, 1, dtype=torch.long, device=self.device) for j in tokenized['token_type_ids'].values()]
|
@@ -725,7 +751,6 @@ class MIMICIVEDCXRMultimodalModel(VisionEncoderDecoderModel):
|
|
725 |
tokenizer: PreTrainedTokenizerFast,
|
726 |
tokenized_report=None,
|
727 |
sep_token_id=None,
|
728 |
-
section_ids=None,
|
729 |
**batch,
|
730 |
):
|
731 |
"""
|
@@ -736,8 +761,7 @@ class MIMICIVEDCXRMultimodalModel(VisionEncoderDecoderModel):
|
|
736 |
tokenizer - Hugging Face tokenizer.
|
737 |
tokenized_report - if training/teacher forcing, input the tokenized_report dict to include it in the prepared inputs.
|
738 |
separator_token_id - separator token identifier.
|
739 |
-
|
740 |
-
|
741 |
Returns:
|
742 |
inputs_embeds - input embeddings.
|
743 |
attention_mask - attention mask.
|
@@ -755,23 +779,24 @@ class MIMICIVEDCXRMultimodalModel(VisionEncoderDecoderModel):
|
|
755 |
bos_token_ids = None
|
756 |
|
757 |
# Index and value columns:
|
758 |
-
batch_size =
|
759 |
-
for k in self.
|
760 |
-
if
|
761 |
-
|
762 |
-
|
763 |
-
|
764 |
-
|
765 |
-
|
766 |
-
|
767 |
-
|
768 |
-
|
769 |
-
|
770 |
-
|
771 |
-
|
|
|
772 |
|
773 |
# Tokenize text columns for prompt:
|
774 |
-
tokenized = self.
|
775 |
input_ids.append(tokenized['input_ids'])
|
776 |
token_type_ids.append(tokenized['token_type_ids'])
|
777 |
attention_mask.append(tokenized['attention_mask'])
|
@@ -780,14 +805,17 @@ class MIMICIVEDCXRMultimodalModel(VisionEncoderDecoderModel):
|
|
780 |
# Image encoder:
|
781 |
encoder_outputs = self.encoder(images)
|
782 |
inputs_embeds.append(encoder_outputs[0])
|
|
|
783 |
inputs_per_image = encoder_outputs[0].shape[-2] // images.shape[1]
|
784 |
-
|
785 |
-
time_delta_image_features = torch.tensor(padded_image_time_deltas, device=self.device).repeat_interleave(inputs_per_image, dim=1)
|
786 |
token_type_ids.append(
|
787 |
torch.where(
|
788 |
-
|
789 |
-
|
790 |
-
|
|
|
|
|
|
|
791 |
),
|
792 |
)
|
793 |
attention_mask.append(encoder_outputs[1])
|
@@ -819,7 +847,7 @@ class MIMICIVEDCXRMultimodalModel(VisionEncoderDecoderModel):
|
|
819 |
report_token_type_ids = self.token_ids_to_token_type_ids(
|
820 |
token_ids=tokenized_report['decoder_input_ids'],
|
821 |
special_token_ids=[sep_token_id],
|
822 |
-
token_type_id_sections=
|
823 |
)
|
824 |
token_type_ids.append(report_token_type_ids)
|
825 |
|
@@ -906,8 +934,11 @@ class MIMICIVEDCXRMultimodalModel(VisionEncoderDecoderModel):
|
|
906 |
return mixed_causality_4d_attention_mask
|
907 |
|
908 |
def position_ids_from_time_deltas_and_attention_mask(self, time_deltas, attention_mask):
|
909 |
-
|
910 |
-
|
|
|
|
|
|
|
911 |
num_rows, num_cols, _ = time_deltas.shape
|
912 |
|
913 |
row_indices = torch.arange(num_rows, device=time_deltas.device).view(-1, 1).repeat(1, num_cols).view(-1)
|
@@ -917,272 +948,316 @@ class MIMICIVEDCXRMultimodalModel(VisionEncoderDecoderModel):
|
|
917 |
|
918 |
return position_ids
|
919 |
|
920 |
-
|
921 |
-
def prepare_data(physionet_dir, database_dir):
|
922 |
|
923 |
-
|
924 |
-
|
925 |
-
|
926 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
927 |
|
928 |
-
|
929 |
|
930 |
-
|
931 |
-
|
932 |
-
print(f'{mimic_cxr_sectioned_path} does not exist, creating...')
|
933 |
|
934 |
-
#
|
935 |
-
|
936 |
-
|
937 |
-
|
938 |
-
|
939 |
-
|
940 |
-
os.path.join(physionet_dir, 'mimic-cxr/2.0.0/files/p19/p19999987/s55368167.txt'),
|
941 |
-
os.path.join(physionet_dir, 'mimic-cxr/2.0.0/files/p19/p19999987/s58621812.txt'),
|
942 |
-
os.path.join(physionet_dir, 'mimic-cxr/2.0.0/files/p19/p19999987/s58971208.txt'),
|
943 |
-
]
|
944 |
-
assert all([os.path.isfile(i) for i in report_paths]), f"""The reports do not exist with the following regex: {os.path.join(physionet_dir, 'mimic-cxr/2.0.0/files/p1*/p1*/s*.txt')}.
|
945 |
-
"Please download them using wget -r -N -c -np --reject dcm --user <username> --ask-password https://physionet.org/files/mimic-cxr/2.0.0/"""
|
946 |
-
|
947 |
-
print('Extracting sections from reports...')
|
948 |
-
create_section_files(
|
949 |
-
reports_path=os.path.join(physionet_dir, 'mimic-cxr', '2.0.0', 'files'),
|
950 |
-
output_path=sectioned_dir,
|
951 |
-
no_split=True,
|
952 |
-
)
|
953 |
|
954 |
-
|
|
|
|
|
955 |
|
956 |
-
|
957 |
-
|
958 |
-
|
959 |
-
|
960 |
-
|
961 |
-
csv_paths.append(glob(os.path.join(physionet_dir, 'mimic-iv-ed', '*', 'ed', 'pyxis.csv.gz'))[0])
|
962 |
-
csv_paths.append(glob(os.path.join(physionet_dir, 'mimic-iv-ed', '*', 'ed', 'triage.csv.gz'))[0])
|
963 |
-
csv_paths.append(glob(os.path.join(physionet_dir, 'mimic-iv-ed', '*', 'ed', 'vitalsign.csv.gz'))[0])
|
964 |
|
965 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
966 |
|
967 |
-
|
968 |
-
assert i in base_names, f"""Table {i} is missing from MIMIC-IV-ED.
|
969 |
-
Please download the tables from https://physionet.org/content/mimic-iv-ed. Do not decompress them."""
|
970 |
-
|
971 |
-
csv_paths.append(glob(os.path.join(physionet_dir, 'mimic-cxr-jpg', '*', 'mimic-cxr-2.0.0-metadata.csv.gz'))[0])
|
972 |
-
csv_paths.append(glob(os.path.join(physionet_dir, 'mimic-cxr-jpg', '*', 'mimic-cxr-2.0.0-chexpert.csv.gz'))[0])
|
973 |
-
csv_paths.append(glob(os.path.join(physionet_dir, 'mimic-cxr-jpg', '*', 'mimic-cxr-2.0.0-split.csv.gz'))[0])
|
974 |
-
|
975 |
-
base_names = [os.path.basename(i) for i in csv_paths[-3:]]
|
976 |
-
|
977 |
-
for i in ['mimic-cxr-2.0.0-metadata.csv.gz', 'mimic-cxr-2.0.0-chexpert.csv.gz', 'mimic-cxr-2.0.0-split.csv.gz']:
|
978 |
-
assert i in base_names, f"""CSV file {i} is missing from MIMIC-IV-ED.
|
979 |
-
Please download the tables from https://physionet.org/content/mimic-cxr-jpg. Do not decompress them."""
|
980 |
-
|
981 |
-
for i in csv_paths:
|
982 |
-
name = Path(i).stem.replace('.csv', '').replace('.gz', '').replace('-', '_').replace('.', '_')
|
983 |
-
print(f'Copying {name} into database...')
|
984 |
-
connect.sql(f"CREATE OR REPLACE TABLE {name} AS FROM '{i}';")
|
985 |
-
|
986 |
-
# MIMIC-CXR report sections:
|
987 |
-
print(f'Copying mimic_cxr_sectioned into database...')
|
988 |
-
connect.sql(f"CREATE OR REPLACE TABLE mimic_cxr_sectioned AS FROM '{mimic_cxr_sectioned_path}';")
|
989 |
-
columns = list(connect.sql('FROM mimic_cxr_sectioned LIMIT 1').df().columns)
|
990 |
-
if 'column0' in columns: # If the column headers are not read correctly:
|
991 |
-
connect.sql("ALTER TABLE mimic_cxr_sectioned RENAME COLUMN column0 TO study;")
|
992 |
-
connect.sql("ALTER TABLE mimic_cxr_sectioned RENAME COLUMN column1 TO impression;")
|
993 |
-
connect.sql("ALTER TABLE mimic_cxr_sectioned RENAME COLUMN column2 TO findings;")
|
994 |
-
connect.sql("ALTER TABLE mimic_cxr_sectioned RENAME COLUMN column3 TO indication;")
|
995 |
-
connect.sql("ALTER TABLE mimic_cxr_sectioned RENAME COLUMN column4 TO history;")
|
996 |
-
connect.sql("ALTER TABLE mimic_cxr_sectioned RENAME COLUMN column5 TO last_paragraph;")
|
997 |
-
connect.sql("ALTER TABLE mimic_cxr_sectioned RENAME COLUMN column6 TO comparison;")
|
998 |
-
connect.sql("DELETE FROM mimic_cxr_sectioned WHERE study='study';")
|
999 |
-
|
1000 |
-
splits = connect.sql("FROM mimic_cxr_2_0_0_split").df()
|
1001 |
-
reports = connect.sql("FROM mimic_cxr_sectioned").df()
|
1002 |
-
metadata = connect.sql("FROM mimic_cxr_2_0_0_metadata").df()
|
1003 |
-
chexpert = connect.sql("FROM mimic_cxr_2_0_0_chexpert").df()
|
1004 |
-
|
1005 |
-
# Create datetime column:
|
1006 |
-
metadata['StudyTime'] = metadata['StudyTime'].astype(int)
|
1007 |
-
metadata['study_datetime'] = pd.to_datetime(
|
1008 |
-
metadata.apply(lambda x: f'{x["StudyDate"]} {x["StudyTime"]:06}', axis=1),
|
1009 |
-
format='%Y%m%d %H%M%S',
|
1010 |
-
)
|
1011 |
-
reports.rename(columns={'study': 'study_id'}, inplace=True)
|
1012 |
-
reports.study_id = reports.study_id.str[1:].astype('int32')
|
1013 |
-
df = pd.merge(splits, reports, on='study_id')
|
1014 |
-
df = pd.merge(df, metadata, on=['dicom_id', 'study_id', 'subject_id'])
|
1015 |
-
df = pd.merge(df, chexpert, on=['study_id', 'subject_id'])
|
1016 |
-
|
1017 |
-
connect.sql(f"CREATE OR REPLACE TABLE mimic_cxr AS SELECT * FROM df")
|
1018 |
|
1019 |
-
|
1020 |
-
for k, v in (ed_module_tables | mimic_cxr_tables).items():
|
1021 |
-
if v.load and v.index_columns:
|
1022 |
-
start_idx = 0
|
1023 |
-
for i in v.index_columns_source:
|
1024 |
-
lut_name = f'{k}_{i}_lut'
|
1025 |
-
table = k
|
1026 |
-
lut, end_idx = create_lookup_table(connect.sql(f"SELECT {i} FROM {table}").df(), [i], start_idx)
|
1027 |
-
start_idx = end_idx + 1
|
1028 |
-
lut = lut.rename(columns={'index': f'{i}_index'})
|
1029 |
-
|
1030 |
-
print(f'Creating {lut_name}...')
|
1031 |
-
|
1032 |
-
connect.sql(f"CREATE OR REPLACE TABLE {lut_name} AS SELECT * FROM lut")
|
1033 |
-
|
1034 |
-
if f'{i}_index' in connect.sql(f"FROM {k} LIMIT 0").df().columns:
|
1035 |
-
connect.sql(
|
1036 |
-
f"""
|
1037 |
-
ALTER TABLE {k}
|
1038 |
-
DROP COLUMN {i}_index;
|
1039 |
-
"""
|
1040 |
-
)
|
1041 |
-
|
1042 |
-
connect.sql(
|
1043 |
-
f"""
|
1044 |
-
CREATE OR REPLACE TABLE {k} AS
|
1045 |
-
SELECT {k}.*, {lut_name}.{i}_index
|
1046 |
-
FROM {k} LEFT JOIN {lut_name}
|
1047 |
-
ON {k}.{i} = {lut_name}.{i}
|
1048 |
-
"""
|
1049 |
-
)
|
1050 |
-
|
1051 |
-
connect.sql(
|
1052 |
-
f"""
|
1053 |
-
CREATE TABLE IF NOT EXISTS lut_info (table_name VARCHAR PRIMARY KEY, start_index INT, end_index INT);
|
1054 |
-
INSERT OR REPLACE INTO lut_info VALUES ('{k}', {0}, {end_idx});
|
1055 |
-
"""
|
1056 |
-
)
|
1057 |
-
|
1058 |
-
table_studies = {
|
1059 |
-
'edstays': [],
|
1060 |
-
'triage': [],
|
1061 |
-
'medrecon': [],
|
1062 |
-
'vitalsign': [],
|
1063 |
-
'pyxis': [],
|
1064 |
-
}
|
1065 |
-
stay_id_tables = ['triage']
|
1066 |
-
stay_id_charttime_tables = ['medrecon', 'vitalsign', 'pyxis']
|
1067 |
-
|
1068 |
-
df = connect.sql(f"FROM mimic_cxr").df()
|
1069 |
|
1070 |
-
#
|
1071 |
-
|
1072 |
-
|
1073 |
-
|
1074 |
-
|
1075 |
-
|
1076 |
-
|
1077 |
-
|
1078 |
-
SELECT stay_id, intime, outtime
|
1079 |
-
FROM edstays
|
1080 |
-
WHERE (subject_id = {row['subject_id']})
|
1081 |
-
AND intime < '{row['study_datetime']}'
|
1082 |
-
AND outtime > '{row['study_datetime']}';
|
1083 |
-
"""
|
1084 |
-
).df()
|
1085 |
-
|
1086 |
-
if len(edstays) > 0:
|
1087 |
-
|
1088 |
-
for i in edstays['stay_id'].to_list():
|
1089 |
-
table_studies['edstays'].append({'study_id': row['study_id'], 'stay_id': i})
|
1090 |
-
for j in stay_id_tables:
|
1091 |
-
table = connect.sql(
|
1092 |
-
f"""
|
1093 |
-
SELECT stay_id
|
1094 |
-
FROM {j}
|
1095 |
-
WHERE (stay_id = {i});
|
1096 |
-
"""
|
1097 |
-
).df()
|
1098 |
-
|
1099 |
-
for k in table['stay_id'].to_list():
|
1100 |
-
table_studies[j].append({'study_id': row['study_id'], 'stay_id': k})
|
1101 |
-
|
1102 |
-
for j in stay_id_charttime_tables:
|
1103 |
-
table = connect.sql(
|
1104 |
-
f"""
|
1105 |
-
SELECT stay_id
|
1106 |
-
FROM {j}
|
1107 |
-
WHERE (stay_id = {i})
|
1108 |
-
AND charttime < '{row['study_datetime']}';
|
1109 |
-
"""
|
1110 |
-
).df()
|
1111 |
-
|
1112 |
-
for k in table['stay_id'].to_list():
|
1113 |
-
table_studies[j].append({'study_id': row['study_id'], 'stay_id': k})
|
1114 |
-
|
1115 |
-
for k, v in table_studies.items():
|
1116 |
-
df = pd.DataFrame(v)
|
1117 |
-
df = df.drop_duplicates(subset=['study_id', 'stay_id'])
|
1118 |
-
connect.sql(f"CREATE TABLE {k}_study_ids AS SELECT * FROM df")
|
1119 |
|
1120 |
-
|
1121 |
-
|
1122 |
-
|
1123 |
-
|
1124 |
-
|
1125 |
-
|
1126 |
-
|
1127 |
-
|
1128 |
-
mimic_iv_duckdb_path=mimic_iv_duckdb_path,
|
1129 |
-
mimic_cxr_jpg_dir=mimic_cxr_jpg_dir[0],
|
1130 |
-
mimic_cxr_jpg_lmdb_path=mimic_cxr_jpg_lmdb_path,
|
1131 |
-
map_size_tb=0.65
|
1132 |
-
)
|
1133 |
|
1134 |
-
|
1135 |
-
|
1136 |
-
|
1137 |
-
mimic_iv_duckdb_path = os.path.join(database_dir, 'mimic_iv_duckdb.db')
|
1138 |
-
mimic_cxr_jpg_lmdb_path = os.path.join(database_dir, 'mimic_cxr_jpg_lmdb.db') if mimic_cxr_jpg_dir is None else None
|
1139 |
-
|
1140 |
-
if records is None:
|
1141 |
|
1142 |
-
#
|
1143 |
-
|
|
|
|
|
|
|
|
|
1144 |
|
1145 |
-
|
1146 |
-
|
1147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1148 |
|
1149 |
-
|
1150 |
-
|
1151 |
-
|
1152 |
-
|
1153 |
-
|
1154 |
-
|
1155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1156 |
)
|
1157 |
-
|
1158 |
-
|
1159 |
-
|
1160 |
-
|
1161 |
-
|
1162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1163 |
|
1164 |
@staticmethod
|
1165 |
def collate_fn(batch):
|
1166 |
keys = set().union(*(d.keys() for d in batch))
|
1167 |
batch = {j: [i.setdefault(j, None) for i in batch] for j in keys}
|
1168 |
-
batch
|
1169 |
-
|
1170 |
-
|
1171 |
-
|
1172 |
-
|
1173 |
-
|
1174 |
-
batch[k] = [i if i is not None else torch.empty(0, total_indices) for i in batch[k]]
|
1175 |
-
batch[k] = torch.nn.utils.rnn.pad_sequence(batch[k], batch_first=True, padding_value=-1) # Pad value of -1 is not ideal. Need to use something else.
|
1176 |
-
token_type_id_name = k.replace('_feats', '_token_type_ids')
|
1177 |
-
batch[token_type_id_name] = [i if i is not None else torch.empty(0, dtype=torch.long) for i in batch[token_type_id_name]]
|
1178 |
-
batch[token_type_id_name] = torch.nn.utils.rnn.pad_sequence(
|
1179 |
-
batch[token_type_id_name], batch_first=True, padding_value=0,
|
1180 |
-
)
|
1181 |
-
mask_name = k.replace('_feats', '_mask')
|
1182 |
-
batch[mask_name] = (batch[k] != -1).any(dim=-1).int()
|
1183 |
-
|
1184 |
-
if 'time_delta' in k and 'index_value' in k:
|
1185 |
-
batch[k] = [i if i is not None else torch.empty(0, 1) for i in batch[k]]
|
1186 |
-
batch[k] = torch.nn.utils.rnn.pad_sequence(batch[k], batch_first=True, padding_value=0)
|
1187 |
-
|
1188 |
-
return batch
|
|
|
1 |
+
import json
|
2 |
import math
|
3 |
import os
|
4 |
+
import random
|
|
|
5 |
from typing import Optional, Tuple, Union
|
6 |
|
7 |
+
import datasets
|
|
|
8 |
import torch
|
9 |
import transformers
|
10 |
from torch.nn import CrossEntropyLoss
|
11 |
+
from torch.utils.data import Subset
|
12 |
+
from torchvision.io import decode_image
|
13 |
from transformers import PreTrainedTokenizerFast, VisionEncoderDecoderModel
|
14 |
from transformers.configuration_utils import PretrainedConfig
|
15 |
from transformers.modeling_outputs import Seq2SeqLMOutput
|
16 |
from transformers.modeling_utils import PreTrainedModel
|
|
|
|
|
|
|
17 |
from transformers.utils import logging
|
18 |
|
19 |
+
from .configuration_cxrmate_ed import EncoderDecoderConfig
|
20 |
+
from .dataset import PriorsDataset
|
|
|
21 |
from .modelling_uniformer import MultiUniFormerWithProjectionHead
|
22 |
+
from .prepare_dataset import prepare_dataset
|
23 |
+
from .utils import compute_time_delta
|
24 |
|
25 |
logger = logging.get_logger(__name__)
|
26 |
|
27 |
+
# Ordered by oblique, lateral, AP, and then PA views so that PA views are closest in position to the generated tokens (and oblique is furtherest).
|
28 |
+
VIEW_ORDER = [None, 'LPO', 'RAO', 'LAO', 'SWIMMERS', 'XTABLE LATERAL', 'LL', 'LATERAL', 'AP AXIAL', 'AP RLD', 'AP LLD', 'AP', 'PA RLD', 'PA LLD', 'PA']
|
29 |
+
|
30 |
|
31 |
def create_lookup_table(df, columns, start_idx):
|
32 |
df = df.groupby(columns).head(1)[columns].sort_values(by=columns)
|
|
|
48 |
|
49 |
class MIMICIVEDCXRMultimodalModel(VisionEncoderDecoderModel):
|
50 |
|
51 |
+
config_class = EncoderDecoderConfig
|
52 |
base_model_prefix = "vision_encoder_decoder"
|
53 |
main_input_name = "input_ids"
|
54 |
supports_gradient_checkpointing = True
|
55 |
|
56 |
+
def __init__(
|
57 |
self,
|
58 |
config: Optional[PretrainedConfig] = None,
|
59 |
encoder: Optional[PreTrainedModel] = None,
|
|
|
69 |
if config is None and (encoder is None or decoder is None):
|
70 |
raise ValueError("Either a configuration or an encoder and a decoder has to be provided.")
|
71 |
if config is None:
|
72 |
+
config = EncoderDecoderConfig.from_encoder_decoder_configs(encoder.config, decoder.config)
|
73 |
else:
|
74 |
if not isinstance(config, self.config_class):
|
75 |
raise ValueError(f"Config: {config} has to be of type {self.config_class}")
|
|
|
110 |
assert not config.decoder.is_encoder_decoder
|
111 |
assert 'pad_token_id' in self.decoder.config.__dict__
|
112 |
assert 'time_delta_monotonic_inversion' in self.decoder.config.__dict__
|
|
|
113 |
assert 'add_time_deltas' in self.decoder.config.__dict__
|
114 |
+
assert 'history' in self.decoder.config.__dict__
|
115 |
+
assert 'tables_filter' in self.decoder.config.__dict__
|
116 |
+
assert 'prompt_report_sections_filter' in self.decoder.config.__dict__
|
117 |
|
118 |
assert isinstance(self.decoder.config.time_delta_monotonic_inversion, bool)
|
119 |
+
|
120 |
+
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'tables.json'), 'r') as f:
|
121 |
+
self.tables = json.load(f)
|
122 |
+
|
123 |
+
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'lookup_tables.json'), 'r') as f:
|
124 |
+
self.luts = json.load(f)
|
125 |
+
|
126 |
+
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'token_type_ids.json'), 'r') as f:
|
127 |
+
self.token_type_to_token_type_id = json.load(f)
|
128 |
+
|
129 |
+
self.tables = {k: self.tables[k] for k in self.decoder.config.tables_filter}
|
130 |
+
self.tables['mimic_cxr_sectioned']['text_columns'] = self.decoder.config.prompt_report_sections_filter
|
131 |
+
|
132 |
+
for k in self.tables.keys():
|
133 |
+
if self.luts[k]['total'] > 0:
|
134 |
+
setattr(
|
135 |
+
self,
|
136 |
+
f'{k}_index_value_encoder',
|
137 |
+
FNNEncoder(
|
138 |
+
num_features=self.luts[k]['total'],
|
139 |
+
intermediate_size=self.decoder.config.index_value_encoder_intermediate_size,
|
140 |
+
decoder_hidden_size=self.decoder.config.hidden_size,
|
141 |
+
),
|
142 |
+
)
|
143 |
+
|
144 |
if self.decoder.config.add_time_deltas:
|
145 |
self.time_delta_encoder = FNNEncoder(
|
146 |
num_features=1,
|
147 |
intermediate_size=self.decoder.config.index_value_encoder_intermediate_size,
|
148 |
decoder_hidden_size=self.decoder.config.hidden_size,
|
149 |
)
|
150 |
+
|
151 |
+
self.token_type_embeddings = torch.nn.Embedding(max(self.token_type_to_token_type_id.values()) + 1, self.decoder.config.hidden_size)
|
152 |
+
|
153 |
+
self.time_delta_map = lambda x: 1 / math.sqrt(x + 1)
|
154 |
+
self.zero_time_delta_value = self.time_delta_map(0)
|
155 |
+
|
156 |
+
self.inf_time_delta_value = self.time_delta_map(float('inf'))
|
157 |
|
158 |
@classmethod
|
159 |
def from_encoder_decoder_pretrained(
|
|
|
301 |
decoder = transformers.AutoModelForCausalLM.from_pretrained(decoder_pretrained_model_name_or_path, **kwargs_decoder)
|
302 |
|
303 |
# instantiate config with corresponding kwargs
|
304 |
+
config = EncoderDecoderConfig.from_encoder_decoder_configs(encoder.config, decoder.config, **kwargs)
|
305 |
|
306 |
# make sure input & output embeddings is not tied
|
307 |
config.tie_word_embeddings = False
|
|
|
312 |
|
313 |
def forward(
|
314 |
self,
|
315 |
+
decoder_position_ids: torch.LongTensor,
|
316 |
+
decoder_attention_mask: torch.FloatTensor,
|
317 |
+
decoder_token_type_ids: torch.LongTensor,
|
318 |
decoder_input_ids: Optional[torch.LongTensor] = None,
|
|
|
|
|
319 |
encoder_outputs: Optional[Tuple[torch.FloatTensor]] = None,
|
320 |
past_key_values: Optional[Tuple[Tuple[torch.FloatTensor]]] = None,
|
321 |
decoder_inputs_embeds: Optional[torch.FloatTensor] = None,
|
|
|
322 |
labels: Optional[torch.LongTensor] = None,
|
323 |
use_cache: Optional[bool] = None,
|
324 |
output_attentions: Optional[bool] = None,
|
|
|
333 |
argument[len("decoder_") :]: value for argument, value in kwargs.items() if argument.startswith("decoder_")
|
334 |
}
|
335 |
|
|
|
|
|
336 |
assert decoder_attention_mask.dtype == torch.long, f'The dtype for {decoder_attention_mask} was {decoder_attention_mask.dtype}. It should be torch.long'
|
|
|
337 |
|
338 |
if decoder_inputs_embeds is None:
|
339 |
decoder_inputs_embeds = self.decoder.get_input_embeddings()(decoder_input_ids)
|
|
|
379 |
special_token_ids,
|
380 |
prompt_attention_mask,
|
381 |
prompt_position_ids,
|
|
|
382 |
past_key_values=None,
|
383 |
use_cache=None,
|
384 |
**kwargs,
|
|
|
403 |
# `inputs_embeds` are only to be used in the 1st generation step:
|
404 |
inputs_embeds = torch.cat([kwargs['decoder_inputs_embeds'], self.decoder.get_input_embeddings()(input_ids)], dim=1)
|
405 |
|
406 |
+
decoder_token_type_ids = self.token_ids_to_token_type_ids(
|
407 |
+
input_ids, special_token_ids,
|
408 |
+
[self.token_type_to_token_type_id['findings'], self.token_type_to_token_type_id['impression']],
|
409 |
+
)
|
410 |
decoder_token_type_ids = torch.cat(
|
411 |
[
|
412 |
kwargs['decoder_token_type_ids'],
|
|
|
430 |
decoder_position_ids.masked_fill_(report_attention_mask == 0, 1)
|
431 |
|
432 |
# Always place token_ids_to_token_type_ids_past_key_values before input_ids = input_ids[:, remove_prefix_length:]:
|
433 |
+
decoder_token_type_ids = self.token_ids_to_token_type_ids_past_key_values(
|
434 |
+
input_ids,
|
435 |
+
special_token_ids,
|
436 |
+
[self.token_type_to_token_type_id['findings'], self.token_type_to_token_type_id['impression']],
|
437 |
+
)
|
438 |
decoder_position_ids = decoder_position_ids[:, -1:]
|
439 |
|
440 |
past_length = past_key_values[0][0].shape[2]
|
|
|
460 |
)
|
461 |
return input_dict
|
462 |
|
463 |
+
def token_ids_to_token_type_ids(self, token_ids, special_token_ids, token_type_id_sections):
|
464 |
"""
|
465 |
Extract token type identifiers from the token identifiers.
|
466 |
|
|
|
503 |
|
504 |
return token_type_ids
|
505 |
|
506 |
+
def token_ids_to_token_type_ids_past_key_values(self, token_ids, special_token_ids, token_type_id_sections):
|
507 |
"""
|
508 |
Extract token type identifiers from the token identifiers if past != None. Make sure to input all the
|
509 |
token_ids (e.g., do not input input_ids = input_ids[:, remove_prefix_length:] from prepare_inputs_for_generation).
|
|
|
672 |
|
673 |
return tuple(sections.values())
|
674 |
|
675 |
+
def tokenize_text_prompt(self, tokenizer: PreTrainedTokenizerFast, **kwargs):
|
676 |
"""
|
677 |
Tokenize the text columns from MIMIC-IV ED and MIMIC-CXR (excluding the findings and impression sections).
|
678 |
Time deltas for the input_ids are also prepared here.
|
|
|
685 |
cxr - dictionary containing the input_ids, token_type_ids, and attention_mask for MIMIC-CXR columns.
|
686 |
"""
|
687 |
|
688 |
+
batch_size = len(kwargs['study_id'])
|
689 |
|
690 |
tokenized = {
|
691 |
'input_ids': {i: [] for i in range(batch_size)},
|
|
|
694 |
'attention_mask': torch.empty(batch_size, 0, 1, device=self.device),
|
695 |
}
|
696 |
|
697 |
+
prompt_text_columns = [f'{k}_{j}' if k != 'mimic_cxr_sectioned' else j for k, v in self.tables.items() if 'text_columns' in v for j in (v['text_columns'] if isinstance(v['text_columns'], list) else [v['text_columns']])] + ['prior_findings', 'prior_impression']
|
698 |
+
|
699 |
+
for i in prompt_text_columns:
|
700 |
if i in kwargs:
|
701 |
if f'{i}_time_delta' not in kwargs:
|
702 |
+
kwargs[f'{i}_time_delta'] = [[self.zero_time_delta_value for _ in j] if j is not None else None for j in kwargs[i]]
|
703 |
for x, (y, z) in enumerate(zip(kwargs[i], kwargs[f'{i}_time_delta'])):
|
704 |
if y is not None:
|
705 |
assert isinstance(y, list)
|
706 |
assert isinstance(z, list)
|
707 |
for text, time_delta in zip(y, z):
|
708 |
+
if text is not None:
|
709 |
+
tokenized['input_ids'][x].append(
|
710 |
+
tokenizer(text, add_special_tokens=False, return_tensors='pt')['input_ids'].to(device=self.device)
|
|
|
|
|
|
|
|
|
|
|
|
|
711 |
)
|
712 |
+
tokenized['token_type_ids'][x].append(
|
713 |
+
torch.full(
|
714 |
+
(1, tokenized['input_ids'][x][-1].shape[-1]),
|
715 |
+
self.token_type_to_token_type_id[i],
|
716 |
+
dtype=torch.long,
|
717 |
+
device=self.device,
|
718 |
+
)
|
719 |
+
)
|
720 |
+
tokenized['time_delta'][x].append(
|
721 |
+
torch.full(
|
722 |
+
(1, tokenized['input_ids'][x][-1].shape[-1]),
|
723 |
+
time_delta,
|
724 |
+
dtype=torch.float32,
|
725 |
+
device=self.device,
|
726 |
+
)
|
727 |
)
|
|
|
728 |
|
729 |
tokenized['input_ids'] = [torch.cat(j, dim=1).T if j else torch.empty(0, 1, dtype=torch.long, device=self.device) for j in tokenized['input_ids'].values()]
|
730 |
tokenized['token_type_ids'] = [torch.cat(j, dim=1).T if j else torch.empty(0, 1, dtype=torch.long, device=self.device) for j in tokenized['token_type_ids'].values()]
|
|
|
751 |
tokenizer: PreTrainedTokenizerFast,
|
752 |
tokenized_report=None,
|
753 |
sep_token_id=None,
|
|
|
754 |
**batch,
|
755 |
):
|
756 |
"""
|
|
|
761 |
tokenizer - Hugging Face tokenizer.
|
762 |
tokenized_report - if training/teacher forcing, input the tokenized_report dict to include it in the prepared inputs.
|
763 |
separator_token_id - separator token identifier.
|
764 |
+
|
|
|
765 |
Returns:
|
766 |
inputs_embeds - input embeddings.
|
767 |
attention_mask - attention mask.
|
|
|
779 |
bos_token_ids = None
|
780 |
|
781 |
# Index and value columns:
|
782 |
+
batch_size = images.shape[0]
|
783 |
+
for k, v in self.tables.items():
|
784 |
+
if 'index_columns' in v or 'value_columns' in v:
|
785 |
+
if f'{k}_index_value_feats' not in batch:
|
786 |
+
batch[f'{k}_index_value_feats'] = torch.empty(batch_size, 0, self.luts[k]['total'], device=self.device)
|
787 |
+
inputs_embeds.append(
|
788 |
+
getattr(self, f'{k}_index_value_encoder')(batch[f'{k}_index_value_feats'])
|
789 |
+
)
|
790 |
+
token_type_ids.append(batch[f'{k}_index_value_token_type_ids'] if f'{k}_index_value_token_type_ids' in batch else torch.empty(batch_size, 0, dtype=torch.long, device=self.device))
|
791 |
+
attention_mask.append(batch[f'{k}_index_value_mask'] if f'{k}_index_value_mask' in batch else torch.empty(batch_size, 0, dtype=torch.long, device=self.device))
|
792 |
+
if f'{k}_index_value_time_delta' in batch:
|
793 |
+
time_delta.append(batch[f'{k}_index_value_time_delta'])
|
794 |
+
else:
|
795 |
+
time_delta_index_value = torch.zeros(*batch[f'{k}_index_value_mask'].shape, 1, device=self.device) if f'{k}_index_value_mask' in batch else torch.empty(batch_size, 0, 1, device=self.device)
|
796 |
+
time_delta.append(time_delta_index_value)
|
797 |
|
798 |
# Tokenize text columns for prompt:
|
799 |
+
tokenized = self.tokenize_text_prompt(tokenizer, **batch)
|
800 |
input_ids.append(tokenized['input_ids'])
|
801 |
token_type_ids.append(tokenized['token_type_ids'])
|
802 |
attention_mask.append(tokenized['attention_mask'])
|
|
|
805 |
# Image encoder:
|
806 |
encoder_outputs = self.encoder(images)
|
807 |
inputs_embeds.append(encoder_outputs[0])
|
808 |
+
|
809 |
inputs_per_image = encoder_outputs[0].shape[-2] // images.shape[1]
|
810 |
+
time_delta_image_features = torch.tensor(batch['image_time_deltas'], device=self.device).repeat_interleave(inputs_per_image, dim=1)
|
|
|
811 |
token_type_ids.append(
|
812 |
torch.where(
|
813 |
+
torch.logical_or(
|
814 |
+
time_delta_image_features == self.zero_time_delta_value,
|
815 |
+
time_delta_image_features == self.inf_time_delta_value,
|
816 |
+
),
|
817 |
+
self.token_type_to_token_type_id['image'],
|
818 |
+
self.token_type_to_token_type_id['prior_image'],
|
819 |
),
|
820 |
)
|
821 |
attention_mask.append(encoder_outputs[1])
|
|
|
847 |
report_token_type_ids = self.token_ids_to_token_type_ids(
|
848 |
token_ids=tokenized_report['decoder_input_ids'],
|
849 |
special_token_ids=[sep_token_id],
|
850 |
+
token_type_id_sections=[self.token_type_to_token_type_id['findings'], self.token_type_to_token_type_id['impression']],
|
851 |
)
|
852 |
token_type_ids.append(report_token_type_ids)
|
853 |
|
|
|
934 |
return mixed_causality_4d_attention_mask
|
935 |
|
936 |
def position_ids_from_time_deltas_and_attention_mask(self, time_deltas, attention_mask):
|
937 |
+
mask_value = torch.finfo(time_deltas.dtype).max if self.decoder.config.time_delta_monotonic_inversion else torch.finfo(time_deltas.dtype).min
|
938 |
+
|
939 |
+
masked_time_deltas = torch.where(attention_mask == 1, time_deltas[:, :, 0], mask_value)
|
940 |
+
_, col_indices = torch.sort(masked_time_deltas, descending=not self.decoder.config.time_delta_monotonic_inversion)
|
941 |
+
|
942 |
num_rows, num_cols, _ = time_deltas.shape
|
943 |
|
944 |
row_indices = torch.arange(num_rows, device=time_deltas.device).view(-1, 1).repeat(1, num_cols).view(-1)
|
|
|
948 |
|
949 |
return position_ids
|
950 |
|
951 |
+
def get_dataset(self, dataset_path, train_transforms, test_transforms, max_train_images_per_study, study_id_split='mimic_iv_ed_mimic_cxr_jpg', test_set_only=False):
|
|
|
952 |
|
953 |
+
def train_set_transform(batch):
|
954 |
+
|
955 |
+
# Randomly select max_train_images_per_study if the number of images for a study exceeds max_train_images_per_study.
|
956 |
+
keys = ['images', 'dicom_id']
|
957 |
+
keys = keys + self.tables['mimic_cxr_2_0_0_metadata']['index_columns'] if 'mimic_cxr_2_0_0_metadata' in self.tables else keys
|
958 |
+
for i in range(len(batch['images'])):
|
959 |
+
if len(batch['images'][i]) > max_train_images_per_study:
|
960 |
+
paired = list(zip(*(batch[key][i] for key in keys)))
|
961 |
+
sampled_pairs = random.sample(paired, max_train_images_per_study)
|
962 |
+
unzipped_samples = zip(*sampled_pairs)
|
963 |
+
for key, values in zip(keys, unzipped_samples):
|
964 |
+
batch[key][i] = list(values)
|
965 |
+
|
966 |
+
batch['images'] = [[decode_image(torch.frombuffer(bytearray(j), dtype=torch.uint8)) for j in i] for i in batch['images']]
|
967 |
+
|
968 |
+
# Sort based on ViewPosition:
|
969 |
+
batch['images'] = [list(zip(*sorted(zip(i, v), key=lambda x: VIEW_ORDER.index(x[1]))))[0] for i, v in zip(batch['images'], batch['ViewPosition'])]
|
970 |
+
batch['images'] = [torch.stack([train_transforms(j) for j in i]) for i in batch['images']]
|
971 |
+
max_size = max(i.shape[0] for i in batch['images'])
|
972 |
+
|
973 |
+
batch['image_time_deltas'] = [[self.zero_time_delta_value if j < i.shape[0] else self.inf_time_delta_value for j in range(max_size)] for i in batch['images']]
|
974 |
+
batch['images'] = torch.nn.utils.rnn.pad_sequence(batch['images'], batch_first=True, padding_value=0.0)
|
975 |
+
|
976 |
+
for k, v in self.tables.items():
|
977 |
+
if 'index_columns' in v or 'value_columns' in v:
|
978 |
+
batch[f'{k}_index_value_feats'], batch[f'{k}_index_value_token_type_ids'], batch[f'{k}_index_value_time_delta'], batch[f'{k}_index_value_mask'] = self.prepare_index_value_feats(k, batch)
|
979 |
+
|
980 |
+
for k, v in self.tables.items():
|
981 |
+
if 'text_columns' in v:
|
982 |
+
for i in v['text_columns']:
|
983 |
+
key = f'{k}_{i}' if not k == 'mimic_cxr_sectioned' else i
|
984 |
+
batch[key], batch[f'{key}_time_delta'] = self.prepare_text_prompt(k, i, batch)
|
985 |
|
986 |
+
return batch
|
987 |
|
988 |
+
def test_set_transform(batch):
|
989 |
+
batch['images'] = [[decode_image(torch.frombuffer(bytearray(j), dtype=torch.uint8)) for j in i] for i in batch['images']]
|
|
|
990 |
|
991 |
+
# Sort based on ViewPosition:
|
992 |
+
batch['images'] = [list(zip(*sorted(zip(i, v), key=lambda x: VIEW_ORDER.index(x[1]))))[0] for i, v in zip(batch['images'], batch['ViewPosition'])]
|
993 |
+
batch['images'] = [torch.stack([test_transforms(j) for j in i]) for i in batch['images']]
|
994 |
+
max_size = max(i.shape[0] for i in batch['images'])
|
995 |
+
batch['image_time_deltas'] = [[self.zero_time_delta_value if j < i.shape[0] else self.inf_time_delta_value for j in range(max_size)] for i in batch['images']]
|
996 |
+
batch['images'] = torch.nn.utils.rnn.pad_sequence(batch['images'], batch_first=True, padding_value=0.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
997 |
|
998 |
+
for k, v in self.tables.items():
|
999 |
+
if 'index_columns' in v or 'value_columns' in v:
|
1000 |
+
batch[f'{k}_index_value_feats'], batch[f'{k}_index_value_token_type_ids'], batch[f'{k}_index_value_time_delta'], batch[f'{k}_index_value_mask'] = self.prepare_index_value_feats(k, batch)
|
1001 |
|
1002 |
+
for k, v in self.tables.items():
|
1003 |
+
if 'text_columns' in v:
|
1004 |
+
for i in v['text_columns']:
|
1005 |
+
key = f'{k}_{i}' if not k == 'mimic_cxr_sectioned' else i
|
1006 |
+
batch[key], batch[f'{key}_time_delta'] = self.prepare_text_prompt(k, i, batch)
|
|
|
|
|
|
|
1007 |
|
1008 |
+
return batch
|
1009 |
+
|
1010 |
+
dataset = datasets.load_from_disk(dataset_path)
|
1011 |
+
|
1012 |
+
# Train set:
|
1013 |
+
if not test_set_only:
|
1014 |
+
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), f'{study_id_split}_train_study_ids.json'), 'r') as f:
|
1015 |
+
study_ids = json.load(f)
|
1016 |
+
train_set = dataset['train']
|
1017 |
+
train_set_study_ids = train_set['study_id']
|
1018 |
+
index_map = {study_id: idx for idx, study_id in enumerate(train_set_study_ids)}
|
1019 |
+
indices = [index_map[study_id] for study_id in study_ids if study_id in index_map]
|
1020 |
+
indices.sort()
|
1021 |
+
train_set = PriorsDataset(train_set, self.decoder.config.history, self.time_delta_map)
|
1022 |
+
train_set.set_transform(train_set_transform)
|
1023 |
+
train_set = Subset(train_set, indices)
|
1024 |
+
else:
|
1025 |
+
train_set = None
|
1026 |
+
|
1027 |
+
# Validation set:
|
1028 |
+
if not test_set_only:
|
1029 |
+
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), f'{study_id_split}_validate_study_ids.json'), 'r') as f:
|
1030 |
+
study_ids = json.load(f)
|
1031 |
+
val_set = dataset['validate']
|
1032 |
+
val_set_study_ids = val_set['study_id']
|
1033 |
+
index_map = {study_id: idx for idx, study_id in enumerate(val_set_study_ids)}
|
1034 |
+
indices = [index_map[study_id] for study_id in study_ids if study_id in index_map]
|
1035 |
+
indices.sort()
|
1036 |
+
val_set = PriorsDataset(val_set, self.decoder.config.history, self.time_delta_map)
|
1037 |
+
val_set.set_transform(test_set_transform)
|
1038 |
+
val_set = Subset(val_set, indices)
|
1039 |
+
else:
|
1040 |
+
val_set = None
|
1041 |
+
|
1042 |
+
# Test set:
|
1043 |
+
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), f'{study_id_split}_test_study_ids.json'), 'r') as f:
|
1044 |
+
study_ids = json.load(f)
|
1045 |
+
test_set = dataset['test']
|
1046 |
+
test_set_study_ids = test_set['study_id']
|
1047 |
+
index_map = {study_id: idx for idx, study_id in enumerate(test_set_study_ids)}
|
1048 |
+
indices = [index_map[study_id] for study_id in study_ids if study_id in index_map]
|
1049 |
+
indices.sort()
|
1050 |
+
test_set = PriorsDataset(test_set, self.decoder.config.history, self.time_delta_map)
|
1051 |
+
test_set.set_transform(test_set_transform)
|
1052 |
+
test_set = Subset(test_set, indices)
|
1053 |
+
|
1054 |
+
return train_set, val_set, test_set
|
1055 |
|
1056 |
+
def get_stage_1_dataset(self, dataset_path, train_transforms, test_transforms, max_train_images_per_study):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1057 |
|
1058 |
+
def train_set_transform(batch):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1059 |
|
1060 |
+
# Randomly select max_train_images_per_study if the number of images for a study exceeds max_train_images_per_study.
|
1061 |
+
for i in range(len(batch['images'])):
|
1062 |
+
if len(batch['images'][i]) > max_train_images_per_study:
|
1063 |
+
paired = list(zip(batch['images'][i], batch['ViewPosition'][i]))
|
1064 |
+
sampled_pairs = random.sample(paired, max_train_images_per_study)
|
1065 |
+
batch['images'][i], batch['ViewPosition'][i] = zip(*sampled_pairs)
|
1066 |
+
|
1067 |
+
batch['images'] = [[decode_image(torch.frombuffer(bytearray(j), dtype=torch.uint8)) for j in i] for i in batch['images']]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1068 |
|
1069 |
+
# Sort based on ViewPosition:
|
1070 |
+
batch['images'] = [list(zip(*sorted(zip(i, v), key=lambda x: VIEW_ORDER.index(x[1]))))[0] for i, v in zip(batch['images'], batch['ViewPosition'])]
|
1071 |
+
batch['images'] = [torch.stack([train_transforms(j) for j in i]) for i in batch['images']]
|
1072 |
+
max_size = max(i.shape[0] for i in batch['images'])
|
1073 |
+
batch['image_time_deltas'] = [[self.zero_time_delta_value if j < i.shape[0] else self.inf_time_delta_value for j in range(max_size)] for i in batch['images']]
|
1074 |
+
batch['images'] = torch.nn.utils.rnn.pad_sequence(batch['images'], batch_first=True, padding_value=0.0)
|
1075 |
+
|
1076 |
+
return batch
|
|
|
|
|
|
|
|
|
|
|
1077 |
|
1078 |
+
def test_set_transform(batch):
|
1079 |
+
batch['images'] = [[decode_image(torch.frombuffer(bytearray(j), dtype=torch.uint8)) for j in i] for i in batch['images']]
|
|
|
|
|
|
|
|
|
|
|
1080 |
|
1081 |
+
# Sort based on ViewPosition:
|
1082 |
+
batch['images'] = [list(zip(*sorted(zip(i, v), key=lambda x: VIEW_ORDER.index(x[1]))))[0] for i, v in zip(batch['images'], batch['ViewPosition'])]
|
1083 |
+
batch['images'] = [torch.stack([test_transforms(j) for j in i]) for i in batch['images']]
|
1084 |
+
max_size = max(i.shape[0] for i in batch['images'])
|
1085 |
+
batch['image_time_deltas'] = [[self.zero_time_delta_value if j < i.shape[0] else self.inf_time_delta_value for j in range(max_size)] for i in batch['images']]
|
1086 |
+
batch['images'] = torch.nn.utils.rnn.pad_sequence(batch['images'], batch_first=True, padding_value=0.0)
|
1087 |
|
1088 |
+
return batch
|
1089 |
+
|
1090 |
+
dataset = datasets.load_from_disk(dataset_path)
|
1091 |
+
|
1092 |
+
# Train set:
|
1093 |
+
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), f'mimic_cxr_jpg_train_study_ids.json'), 'r') as f:
|
1094 |
+
study_ids = json.load(f)
|
1095 |
+
train_set = dataset['train']
|
1096 |
+
train_set_study_ids = train_set['study_id']
|
1097 |
+
index_map = {study_id: idx for idx, study_id in enumerate(train_set_study_ids)}
|
1098 |
+
indices = [index_map[study_id] for study_id in study_ids if study_id in index_map]
|
1099 |
+
indices.sort()
|
1100 |
+
train_set = PriorsDataset(train_set, self.decoder.config.history, self.time_delta_map)
|
1101 |
+
train_set.set_transform(train_set_transform)
|
1102 |
+
train_set = Subset(train_set, indices)
|
1103 |
+
|
1104 |
+
# Validation set:
|
1105 |
+
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), f'mimic_cxr_jpg_validate_study_ids.json'), 'r') as f:
|
1106 |
+
study_ids = json.load(f)
|
1107 |
+
val_set = dataset['validate']
|
1108 |
+
val_set_study_ids = val_set['study_id']
|
1109 |
+
index_map = {study_id: idx for idx, study_id in enumerate(val_set_study_ids)}
|
1110 |
+
indices = [index_map[study_id] for study_id in study_ids if study_id in index_map]
|
1111 |
+
indices.sort()
|
1112 |
+
val_set = PriorsDataset(val_set, self.decoder.config.history, self.time_delta_map)
|
1113 |
+
val_set.set_transform(test_set_transform)
|
1114 |
+
val_set = Subset(val_set, indices)
|
1115 |
+
|
1116 |
+
# Test set:
|
1117 |
+
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), f'mimic_cxr_jpg_test_study_ids.json'), 'r') as f:
|
1118 |
+
study_ids = json.load(f)
|
1119 |
+
test_set = dataset['test']
|
1120 |
+
test_set_study_ids = test_set['study_id']
|
1121 |
+
index_map = {study_id: idx for idx, study_id in enumerate(test_set_study_ids)}
|
1122 |
+
indices = [index_map[study_id] for study_id in study_ids if study_id in index_map]
|
1123 |
+
indices.sort()
|
1124 |
+
test_set = PriorsDataset(test_set, self.decoder.config.history, self.time_delta_map)
|
1125 |
+
test_set.set_transform(test_set_transform)
|
1126 |
+
test_set = Subset(test_set, indices)
|
1127 |
+
|
1128 |
+
return train_set, val_set, test_set
|
1129 |
+
|
1130 |
+
def prepare_index_value_feats(self, table, batch):
|
1131 |
+
|
1132 |
+
index_value_columns = (self.tables[table].get('index_columns', []) + self.tables[table].get('value_columns', []))
|
1133 |
+
index_value_columns = [f'{table}_{i}' for i in index_value_columns] if table != 'mimic_cxr_2_0_0_metadata' else index_value_columns
|
1134 |
+
|
1135 |
+
# Map to indices with lookup table:
|
1136 |
+
if 'index_columns' in self.tables[table]:
|
1137 |
+
for i in self.tables[table]['index_columns']:
|
1138 |
+
k = f'{table}_{i}' if not table == 'mimic_cxr_2_0_0_metadata' else i
|
1139 |
+
batch[k] = [
|
1140 |
+
[self.luts[table][i][str(k)] if k is not None else None for k in j] if j is not None else None for j in batch[k]
|
1141 |
+
]
|
1142 |
+
|
1143 |
+
batch_index_value_feats_list = []
|
1144 |
+
batch_token_type_ids_list = []
|
1145 |
+
batch_time_deltas_list = []
|
1146 |
|
1147 |
+
for batch_idx in range(len(batch['study_id'])):
|
1148 |
+
|
1149 |
+
if any([batch[k][batch_idx] for k in index_value_columns]):
|
1150 |
+
|
1151 |
+
num_rows = [len(batch[i][batch_idx]) for i in index_value_columns]
|
1152 |
+
assert all(x == num_rows[0] for x in num_rows)
|
1153 |
+
num_rows = num_rows[0]
|
1154 |
+
|
1155 |
+
# The y-index and the datetime for each group:
|
1156 |
+
if isinstance(batch[self.tables[table]['groupby']][batch_idx], list):
|
1157 |
+
y_indices = [d.setdefault(x, len(d)) for d in [{}] for x in batch[self.tables[table]['groupby']][batch_idx]]
|
1158 |
+
datetime = [j for i, j in enumerate(batch[self.tables[table]['time_column']][batch_idx]) if j not in batch[self.tables[table]['time_column']][batch_idx][:i]]
|
1159 |
+
assert len(set(y_indices)) == len(datetime)
|
1160 |
+
else:
|
1161 |
+
y_indices = [0] * num_rows
|
1162 |
+
datetime = batch[self.tables[table]['time_column']][batch_idx] if 'time_column' in self.tables[table] else [batch['latest_study_datetime'][batch_idx]]
|
1163 |
+
|
1164 |
+
time_deltas = torch.tensor([compute_time_delta(i, batch['latest_study_datetime'][batch_idx], self.time_delta_map, to_tensor=False) for i in datetime])[:, None]
|
1165 |
+
|
1166 |
+
tensor = torch.zeros(max(y_indices) + 1, self.luts[table]['total'])
|
1167 |
+
|
1168 |
+
# Index columns to feats:
|
1169 |
+
if 'index_columns' in self.tables[table]:
|
1170 |
+
|
1171 |
+
for i in self.tables[table]['index_columns']:
|
1172 |
+
k = f'{table}_{i}' if not table == 'mimic_cxr_2_0_0_metadata' else i
|
1173 |
+
y_indices_column = [y_idx for y_idx, x_idx in zip(y_indices, batch[k][batch_idx]) if x_idx is not None]
|
1174 |
+
x_indices_column = [x_idx for x_idx in batch[k][batch_idx] if x_idx is not None]
|
1175 |
+
|
1176 |
+
tensor[y_indices_column, x_indices_column] = 1.0
|
1177 |
+
|
1178 |
+
if 'value_columns' in self.tables[table]:
|
1179 |
+
for i in self.tables[table]['value_columns']:
|
1180 |
+
|
1181 |
+
k = f'{table}_{i}' if not table == 'mimic_cxr_2_0_0_metadata' else i
|
1182 |
+
y_indices_column = [y_idx for y_idx, value in zip(y_indices, batch[k][batch_idx]) if value is not None]
|
1183 |
+
x_indices_column = [self.luts[table][i] for value in batch[k][batch_idx] if value is not None]
|
1184 |
+
values = [value for value in batch[k][batch_idx] if value is not None]
|
1185 |
+
|
1186 |
+
tensor[y_indices_column, x_indices_column] = torch.tensor(values, dtype=tensor.dtype)
|
1187 |
+
assert not torch.isnan(tensor).any()
|
1188 |
+
else:
|
1189 |
+
tensor = torch.empty(0, self.luts[table]['total'])
|
1190 |
+
time_deltas = torch.empty(0, 1)
|
1191 |
+
|
1192 |
+
batch_index_value_feats_list.append(tensor)
|
1193 |
+
batch_token_type_ids_list.append(torch.full(
|
1194 |
+
[tensor.shape[0]],
|
1195 |
+
self.token_type_to_token_type_id[table],
|
1196 |
+
dtype=torch.long,
|
1197 |
+
)
|
1198 |
)
|
1199 |
+
batch_time_deltas_list.append(time_deltas)
|
1200 |
+
|
1201 |
+
assert tensor.shape[0] == batch_token_type_ids_list[-1].shape[0]
|
1202 |
+
assert tensor.shape[0] == time_deltas.shape[0]
|
1203 |
+
|
1204 |
+
batch_index_value_feats = torch.nn.utils.rnn.pad_sequence(batch_index_value_feats_list, batch_first=True, padding_value=-1) # Pad value of -1 is not ideal. Need to use something else.
|
1205 |
+
batch_token_type_ids = torch.nn.utils.rnn.pad_sequence(batch_token_type_ids_list, batch_first=True, padding_value=0)
|
1206 |
+
batch_time_deltas = torch.nn.utils.rnn.pad_sequence(batch_time_deltas_list, batch_first=True, padding_value=0)
|
1207 |
+
|
1208 |
+
batch_mask = (batch_index_value_feats != -1).any(dim=-1).int()
|
1209 |
+
|
1210 |
+
return batch_index_value_feats, batch_token_type_ids, batch_time_deltas, batch_mask
|
1211 |
+
|
1212 |
+
def prepare_text_prompt(self, table, column, batch):
|
1213 |
+
|
1214 |
+
key = f'{table}_{column}' if not table == 'mimic_cxr_sectioned' else column
|
1215 |
+
|
1216 |
+
batch_text_list = []
|
1217 |
+
batch_time_deltas_list = []
|
1218 |
+
|
1219 |
+
for batch_idx in range(len(batch['study_id'])):
|
1220 |
+
if batch[key][batch_idx]:
|
1221 |
+
|
1222 |
+
num_rows = len(batch[key][batch_idx])
|
1223 |
+
|
1224 |
+
# The y-index and the datetime for each group:
|
1225 |
+
if isinstance(batch[self.tables[table]['groupby']][batch_idx], list):
|
1226 |
+
y_indices = [d.setdefault(x, len(d)) for d in [{}] for x in batch[self.tables[table]['groupby']][batch_idx]]
|
1227 |
+
datetime = [j for i, j in enumerate(batch[self.tables[table]['time_column']][batch_idx]) if j not in batch[self.tables[table]['time_column']][batch_idx][:i]]
|
1228 |
+
assert len(set(y_indices)) == len(datetime)
|
1229 |
+
else:
|
1230 |
+
y_indices = [0] * num_rows
|
1231 |
+
datetime = batch[self.tables[table]['time_column']][batch_idx] if 'time_column' in self.tables[table] else [batch['latest_study_datetime'][batch_idx]]
|
1232 |
+
|
1233 |
+
# Remove None values:
|
1234 |
+
text_rows = batch[key][batch_idx] if isinstance(batch[key][batch_idx], list) else [batch[key][batch_idx]]
|
1235 |
+
y_indices = [i for i, j in zip(y_indices, text_rows) if j is not None]
|
1236 |
+
text_rows = [i for i in text_rows if i is not None]
|
1237 |
+
datetime = [datetime[i] for i in set(y_indices)]
|
1238 |
+
if text_rows:
|
1239 |
+
|
1240 |
+
# Those in the same group (or those with the same y-index) get joined as the same string:
|
1241 |
+
batch_text_list.append([', '.join([text_rows[j] for j in range(len(y_indices)) if y_indices[j] == k]) + '.' for k in set(y_indices)])
|
1242 |
+
batch_time_deltas_list.append([compute_time_delta(i, batch['latest_study_datetime'][batch_idx], self.time_delta_map, to_tensor=False) for i in datetime])
|
1243 |
+
|
1244 |
+
assert len(batch_time_deltas_list[-1]) == len(batch_text_list[-1])
|
1245 |
+
else:
|
1246 |
+
batch_text_list.append([])
|
1247 |
+
batch_time_deltas_list.append([])
|
1248 |
+
else:
|
1249 |
+
batch_text_list.append([])
|
1250 |
+
batch_time_deltas_list.append([])
|
1251 |
+
|
1252 |
+
return batch_text_list, batch_time_deltas_list
|
1253 |
|
1254 |
@staticmethod
|
1255 |
def collate_fn(batch):
|
1256 |
keys = set().union(*(d.keys() for d in batch))
|
1257 |
batch = {j: [i.setdefault(j, None) for i in batch] for j in keys}
|
1258 |
+
batch = {k: torch.stack(v) if isinstance(v[0], torch.Tensor) else v for k, v in batch.items()}
|
1259 |
+
return batch
|
1260 |
+
|
1261 |
+
@staticmethod
|
1262 |
+
def prepare_dataset(physionet_dir: str, database_dir: str):
|
1263 |
+
prepare_dataset(physionet_dir=physionet_dir, database_dir=database_dir)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
modelling_uniformer.py
CHANGED
@@ -1,16 +1,17 @@
|
|
1 |
from collections import OrderedDict
|
2 |
from functools import partial
|
3 |
-
from typing import Optional, Tuple, Union
|
4 |
from math import isqrt
|
|
|
5 |
|
6 |
import torch
|
7 |
import torch.nn as nn
|
8 |
from timm.models.layers import DropPath, to_2tuple, trunc_normal_
|
9 |
-
from transformers import ViTConfig
|
10 |
from transformers.modeling_outputs import ModelOutput
|
11 |
from transformers.modeling_utils import PreTrainedModel
|
12 |
from transformers.utils import logging
|
13 |
|
|
|
|
|
14 |
logger = logging.get_logger(__name__)
|
15 |
|
16 |
|
@@ -293,8 +294,7 @@ class UniFormerPreTrainedModel(PreTrainedModel):
|
|
293 |
models.
|
294 |
"""
|
295 |
|
296 |
-
config_class =
|
297 |
-
base_model_prefix = "vit"
|
298 |
main_input_name = "pixel_values"
|
299 |
|
300 |
def _init_weights(self, m):
|
|
|
1 |
from collections import OrderedDict
|
2 |
from functools import partial
|
|
|
3 |
from math import isqrt
|
4 |
+
from typing import Optional, Tuple, Union
|
5 |
|
6 |
import torch
|
7 |
import torch.nn as nn
|
8 |
from timm.models.layers import DropPath, to_2tuple, trunc_normal_
|
|
|
9 |
from transformers.modeling_outputs import ModelOutput
|
10 |
from transformers.modeling_utils import PreTrainedModel
|
11 |
from transformers.utils import logging
|
12 |
|
13 |
+
from .configuration_uniformer import UniFormerWithProjectionHeadConfig
|
14 |
+
|
15 |
logger = logging.get_logger(__name__)
|
16 |
|
17 |
|
|
|
294 |
models.
|
295 |
"""
|
296 |
|
297 |
+
config_class = UniFormerWithProjectionHeadConfig
|
|
|
298 |
main_input_name = "pixel_values"
|
299 |
|
300 |
def _init_weights(self, m):
|
prepare_dataset.py
ADDED
@@ -0,0 +1,558 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
import multiprocessing
|
3 |
+
import os
|
4 |
+
import re
|
5 |
+
import shutil
|
6 |
+
from glob import glob
|
7 |
+
from pathlib import Path
|
8 |
+
|
9 |
+
import datasets
|
10 |
+
import duckdb
|
11 |
+
import numpy as np
|
12 |
+
import pandas as pd
|
13 |
+
|
14 |
+
try:
|
15 |
+
from .create_section_files import create_section_files
|
16 |
+
except ImportError:
|
17 |
+
from create_section_files import create_section_files
|
18 |
+
|
19 |
+
|
20 |
+
def mimic_cxr_image_path(dir, subject_id, study_id, dicom_id, ext='dcm'):
|
21 |
+
return os.path.join(dir, 'p' + str(subject_id)[:2], 'p' + str(subject_id),
|
22 |
+
's' + str(study_id), str(dicom_id) + '.' + ext)
|
23 |
+
|
24 |
+
|
25 |
+
def format(text):
|
26 |
+
# Remove newline, tab, repeated whitespaces, and leading and trailing whitespaces:
|
27 |
+
def remove(text):
|
28 |
+
text = re.sub(r'\n|\t', ' ', text)
|
29 |
+
text = re.sub(r'\s+', ' ', text)
|
30 |
+
return text.strip()
|
31 |
+
|
32 |
+
if isinstance(text, np.ndarray) or isinstance(text, list):
|
33 |
+
return [remove(t) if not pd.isna(t) else t for t in text]
|
34 |
+
else:
|
35 |
+
if pd.isna(text):
|
36 |
+
return text
|
37 |
+
return remove(text)
|
38 |
+
|
39 |
+
|
40 |
+
def create_lookup_table(df, columns, start_idx):
|
41 |
+
df = df.groupby(columns).head(1)[columns].sort_values(by=columns)
|
42 |
+
indices = range(start_idx, start_idx + len(df))
|
43 |
+
df['index'] = indices
|
44 |
+
return df, indices[-1]
|
45 |
+
|
46 |
+
|
47 |
+
def lookup_tables(con, tables):
|
48 |
+
luts_dict = {}
|
49 |
+
for k, v in tables.items():
|
50 |
+
luts_dict[k] = {}
|
51 |
+
start_idx = 0
|
52 |
+
if 'index_columns' in v:
|
53 |
+
for i in v['index_columns']:
|
54 |
+
lut, end_idx = create_lookup_table(con.sql(f"SELECT {i} FROM {k}").df(), [i], start_idx)
|
55 |
+
start_idx = end_idx + 1
|
56 |
+
luts_dict[k][i] = {str(row[i]): int(row['index']) for _, row in lut.iterrows()}
|
57 |
+
if 'value_columns' in v:
|
58 |
+
for i in v['value_columns']:
|
59 |
+
luts_dict[k][i] = start_idx
|
60 |
+
start_idx += 1
|
61 |
+
|
62 |
+
luts_dict[k]['total'] = start_idx
|
63 |
+
|
64 |
+
with open( os.path.join(os.path.dirname(os.path.abspath(__file__)), 'lookup_tables.json'), 'w') as file:
|
65 |
+
json.dump(luts_dict, file)
|
66 |
+
|
67 |
+
|
68 |
+
def prepare_dataset(physionet_dir, database_dir, num_workers=None):
|
69 |
+
|
70 |
+
num_workers = num_workers if num_workers is not None else multiprocessing.cpu_count()
|
71 |
+
|
72 |
+
Path(database_dir).mkdir(parents=True, exist_ok=True)
|
73 |
+
|
74 |
+
sectioned_dir = os.path.join(database_dir, 'mimic_cxr_sectioned')
|
75 |
+
mimic_cxr_sectioned_path = os.path.join(sectioned_dir, 'mimic_cxr_sectioned.csv')
|
76 |
+
if not os.path.exists(mimic_cxr_sectioned_path):
|
77 |
+
print(f'{mimic_cxr_sectioned_path} does not exist, creating...')
|
78 |
+
|
79 |
+
# Check if reports exist. Reports for the first and last patients are checked only for speed, this comprimises comprehensiveness for speed:
|
80 |
+
report_paths = [
|
81 |
+
os.path.join(physionet_dir, 'mimic-cxr/2.0.0/files/p10/p10000032/s50414267.txt'),
|
82 |
+
os.path.join(physionet_dir, 'mimic-cxr/2.0.0/files/p10/p10000032/s53189527.txt'),
|
83 |
+
os.path.join(physionet_dir, 'mimic-cxr/2.0.0/files/p10/p10000032/s53911762.txt'),
|
84 |
+
os.path.join(physionet_dir, 'mimic-cxr/2.0.0/files/p10/p10000032/s56699142.txt'),
|
85 |
+
os.path.join(physionet_dir, 'mimic-cxr/2.0.0/files/p19/p19999987/s55368167.txt'),
|
86 |
+
os.path.join(physionet_dir, 'mimic-cxr/2.0.0/files/p19/p19999987/s58621812.txt'),
|
87 |
+
os.path.join(physionet_dir, 'mimic-cxr/2.0.0/files/p19/p19999987/s58971208.txt'),
|
88 |
+
]
|
89 |
+
assert all([os.path.isfile(i) for i in report_paths]), f"""The reports do not exist with the following regex: {os.path.join(physionet_dir, 'mimic-cxr/2.0.0/files/p1*/p1*/s*.txt')}.
|
90 |
+
"Please download them using wget -r -N -c -np --reject dcm --user <username> --ask-password https://physionet.org/files/mimic-cxr/2.0.0/"""
|
91 |
+
|
92 |
+
print('Extracting sections from reports...')
|
93 |
+
create_section_files(
|
94 |
+
reports_path=os.path.join(physionet_dir, 'mimic-cxr', '2.0.0', 'files'),
|
95 |
+
output_path=sectioned_dir,
|
96 |
+
no_split=True,
|
97 |
+
)
|
98 |
+
|
99 |
+
csv_paths = []
|
100 |
+
csv_paths.append(glob(os.path.join(physionet_dir, 'mimic-iv-ed', '*', 'ed', 'edstays.csv.gz'))[0])
|
101 |
+
csv_paths.append(glob(os.path.join(physionet_dir, 'mimic-iv-ed', '*', 'ed', 'medrecon.csv.gz'))[0])
|
102 |
+
csv_paths.append(glob(os.path.join(physionet_dir, 'mimic-iv-ed', '*', 'ed', 'pyxis.csv.gz'))[0])
|
103 |
+
csv_paths.append(glob(os.path.join(physionet_dir, 'mimic-iv-ed', '*', 'ed', 'triage.csv.gz'))[0])
|
104 |
+
csv_paths.append(glob(os.path.join(physionet_dir, 'mimic-iv-ed', '*', 'ed', 'vitalsign.csv.gz'))[0])
|
105 |
+
|
106 |
+
base_names = [os.path.basename(i) for i in csv_paths]
|
107 |
+
|
108 |
+
for i in ['edstays.csv.gz', 'medrecon.csv.gz', 'pyxis.csv.gz', 'triage.csv.gz', 'vitalsign.csv.gz']:
|
109 |
+
assert i in base_names, f"""Table {i} is missing from MIMIC-IV-ED.
|
110 |
+
Please download the tables from https://physionet.org/content/mimic-iv-ed. Do not decompress them."""
|
111 |
+
|
112 |
+
csv_paths.append(glob(os.path.join(physionet_dir, 'mimic-cxr-jpg', '*', 'mimic-cxr-2.0.0-metadata.csv.gz'))[0])
|
113 |
+
csv_paths.append(glob(os.path.join(physionet_dir, 'mimic-cxr-jpg', '*', 'mimic-cxr-2.0.0-chexpert.csv.gz'))[0])
|
114 |
+
csv_paths.append(glob(os.path.join(physionet_dir, 'mimic-cxr-jpg', '*', 'mimic-cxr-2.0.0-split.csv.gz'))[0])
|
115 |
+
|
116 |
+
base_names = [os.path.basename(i) for i in csv_paths[-3:]]
|
117 |
+
|
118 |
+
for i in ['mimic-cxr-2.0.0-metadata.csv.gz', 'mimic-cxr-2.0.0-chexpert.csv.gz', 'mimic-cxr-2.0.0-split.csv.gz']:
|
119 |
+
assert i in base_names, f"""CSV file {i} is missing from MIMIC-CXR-JPG.
|
120 |
+
Please download the tables from https://physionet.org/content/mimic-cxr-jpg. Do not decompress them."""
|
121 |
+
|
122 |
+
con = duckdb.connect(':memory:')
|
123 |
+
for i in csv_paths:
|
124 |
+
name = Path(i).stem.replace('.csv', '').replace('.gz', '').replace('-', '_').replace('.', '_')
|
125 |
+
print(f'Copying {name} into database...')
|
126 |
+
con.sql(f"CREATE OR REPLACE TABLE {name} AS FROM '{i}';")
|
127 |
+
|
128 |
+
# DuckDB has trouble reading the sectioned .csv file, read with pandas instead:
|
129 |
+
sections = pd.read_csv(mimic_cxr_sectioned_path)
|
130 |
+
|
131 |
+
# Remove the first character from the study column and rename it to study_id:
|
132 |
+
con.sql(
|
133 |
+
"""
|
134 |
+
CREATE OR REPLACE TABLE mimic_cxr_sectioned AS
|
135 |
+
SELECT *, CAST(SUBSTR(study, 2) AS INT32) AS study_id
|
136 |
+
FROM sections;
|
137 |
+
"""
|
138 |
+
)
|
139 |
+
|
140 |
+
# Combine StudyDate and StudyTime into a single column and create the studies table:
|
141 |
+
con.sql(
|
142 |
+
"""
|
143 |
+
CREATE OR REPLACE TABLE studies AS
|
144 |
+
SELECT *,
|
145 |
+
strptime(
|
146 |
+
CAST(StudyDate AS VARCHAR) || ' ' || lpad(split_part(CAST(StudyTime AS VARCHAR), '.', 1), 6, '0'),
|
147 |
+
'%Y%m%d %H%M%S'
|
148 |
+
) AS study_datetime
|
149 |
+
FROM mimic_cxr_2_0_0_metadata;
|
150 |
+
"""
|
151 |
+
)
|
152 |
+
|
153 |
+
# Load the table configuration:
|
154 |
+
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'tables.json'), 'r') as file:
|
155 |
+
tables = json.load(file)
|
156 |
+
|
157 |
+
# Create lookup tables:
|
158 |
+
lookup_tables(con, tables)
|
159 |
+
|
160 |
+
# Collapse to one row per study, aggregate each studies columns as a list:
|
161 |
+
con.sql(
|
162 |
+
"""
|
163 |
+
CREATE OR REPLACE TABLE studies AS
|
164 |
+
SELECT
|
165 |
+
LIST(dicom_id) AS dicom_id,
|
166 |
+
FIRST(subject_id) AS subject_id,
|
167 |
+
study_id,
|
168 |
+
LIST(PerformedProcedureStepDescription) AS PerformedProcedureStepDescription,
|
169 |
+
LIST(ViewPosition) AS ViewPosition,
|
170 |
+
LIST(Rows) AS Rows,
|
171 |
+
LIST(Columns) AS Columns,
|
172 |
+
LIST(StudyDate) AS StudyDate,
|
173 |
+
LIST(StudyTime) AS StudyTime,
|
174 |
+
LIST(ProcedureCodeSequence_CodeMeaning) AS ProcedureCodeSequence_CodeMeaning,
|
175 |
+
LIST(ViewCodeSequence_CodeMeaning) AS ViewCodeSequence_CodeMeaning,
|
176 |
+
LIST(PatientOrientationCodeSequence_CodeMeaning) AS PatientOrientationCodeSequence_CodeMeaning,
|
177 |
+
LIST(study_datetime) AS study_datetime,
|
178 |
+
MAX(study_datetime) AS latest_study_datetime,
|
179 |
+
FROM studies
|
180 |
+
GROUP BY study_id;
|
181 |
+
"""
|
182 |
+
)
|
183 |
+
|
184 |
+
# Join and filter the studies that overlap with ED stays:
|
185 |
+
con.sql(
|
186 |
+
"""
|
187 |
+
CREATE OR REPLACE TABLE studies AS
|
188 |
+
SELECT
|
189 |
+
s.*,
|
190 |
+
e.hadm_id,
|
191 |
+
e.stay_id,
|
192 |
+
e.intime,
|
193 |
+
e.outtime,
|
194 |
+
FROM studies s
|
195 |
+
LEFT JOIN edstays e
|
196 |
+
ON s.subject_id = e.subject_id
|
197 |
+
AND e.intime < s.latest_study_datetime
|
198 |
+
AND e.outtime > s.latest_study_datetime
|
199 |
+
AND s.study_id != 59128861;
|
200 |
+
"""
|
201 |
+
) # Don't join study 59128861 as it overlaps with two ED stays
|
202 |
+
|
203 |
+
|
204 |
+
# Aggregate and add the edstays table:
|
205 |
+
con.sql(
|
206 |
+
"""
|
207 |
+
CREATE OR REPLACE TABLE edstays_aggregated AS
|
208 |
+
SELECT
|
209 |
+
FIRST(subject_id) AS subject_id,
|
210 |
+
stay_id,
|
211 |
+
LIST(intime) AS intime,
|
212 |
+
LIST(outtime) AS outtime,
|
213 |
+
LIST(gender) AS gender,
|
214 |
+
LIST(race) AS race,
|
215 |
+
LIST(arrival_transport) AS arrival_transport,
|
216 |
+
LIST(disposition) AS disposition,
|
217 |
+
FROM edstays
|
218 |
+
GROUP BY stay_id;
|
219 |
+
"""
|
220 |
+
)
|
221 |
+
con.sql(
|
222 |
+
"""
|
223 |
+
CREATE OR REPLACE TABLE studies AS
|
224 |
+
SELECT
|
225 |
+
s.*,
|
226 |
+
e.intime AS edstays_intime,
|
227 |
+
e.outtime AS edstays_outtime,
|
228 |
+
e.gender AS edstays_gender,
|
229 |
+
e.race AS edstays_race,
|
230 |
+
e.arrival_transport AS edstays_arrival_transport,
|
231 |
+
e.disposition AS edstays_disposition,
|
232 |
+
FROM studies s
|
233 |
+
LEFT JOIN edstays_aggregated e
|
234 |
+
ON s.stay_id = e.stay_id;
|
235 |
+
"""
|
236 |
+
)
|
237 |
+
|
238 |
+
# Aggregate and add the triage table:
|
239 |
+
con.sql(
|
240 |
+
"""
|
241 |
+
CREATE OR REPLACE TABLE triage_aggregated AS
|
242 |
+
SELECT
|
243 |
+
FIRST(subject_id) AS subject_id,
|
244 |
+
stay_id,
|
245 |
+
LIST(temperature) as temperature,
|
246 |
+
LIST(heartrate) AS heartrate,
|
247 |
+
LIST(resprate) AS resprate,
|
248 |
+
LIST(o2sat) AS o2sat,
|
249 |
+
LIST(sbp) AS sbp,
|
250 |
+
LIST(dbp) AS dbp,
|
251 |
+
LIST(pain) AS pain,
|
252 |
+
LIST(acuity) AS acuity,
|
253 |
+
LIST(chiefcomplaint) AS chiefcomplaint,
|
254 |
+
FROM triage
|
255 |
+
GROUP BY stay_id;
|
256 |
+
"""
|
257 |
+
)
|
258 |
+
con.sql(
|
259 |
+
"""
|
260 |
+
CREATE OR REPLACE TABLE studies AS
|
261 |
+
SELECT
|
262 |
+
s.*,
|
263 |
+
t.temperature AS triage_temperature,
|
264 |
+
t.heartrate AS triage_heartrate,
|
265 |
+
t.resprate AS triage_resprate,
|
266 |
+
t.o2sat AS triage_o2sat,
|
267 |
+
t.sbp AS triage_sbp,
|
268 |
+
t.dbp AS triage_dbp,
|
269 |
+
t.pain AS triage_pain,
|
270 |
+
t.acuity AS triage_acuity,
|
271 |
+
t.chiefcomplaint AS triage_chiefcomplaint,
|
272 |
+
FROM studies s
|
273 |
+
LEFT JOIN triage_aggregated t
|
274 |
+
ON s.stay_id = t.stay_id;
|
275 |
+
"""
|
276 |
+
)
|
277 |
+
|
278 |
+
# Aggregate and then add the vitalsign table (ensuring no rows with a charttime after the latest study_datetime):
|
279 |
+
con.sql(
|
280 |
+
"""
|
281 |
+
CREATE OR REPLACE TABLE vitalsign_causal AS
|
282 |
+
SELECT v.*, s.latest_study_datetime, s.study_id,
|
283 |
+
FROM vitalsign v
|
284 |
+
JOIN studies s ON v.stay_id = s.stay_id
|
285 |
+
WHERE v.charttime < s.latest_study_datetime;
|
286 |
+
"""
|
287 |
+
) # This duplicates the rows for stay_ids that cover multiple study_ids. Hence, the following joins must be on study_id, not stay_id.
|
288 |
+
con.sql(
|
289 |
+
"""
|
290 |
+
CREATE OR REPLACE TABLE vitalsign_aggregated AS
|
291 |
+
SELECT
|
292 |
+
study_id,
|
293 |
+
FIRST(subject_id) AS subject_id,
|
294 |
+
FIRST(stay_id) as stay_id,
|
295 |
+
LIST(charttime) AS charttime,
|
296 |
+
LIST(temperature) as temperature,
|
297 |
+
LIST(heartrate) AS heartrate,
|
298 |
+
LIST(resprate) AS resprate,
|
299 |
+
LIST(o2sat) AS o2sat,
|
300 |
+
LIST(sbp) AS sbp,
|
301 |
+
LIST(dbp) AS dbp,
|
302 |
+
LIST(rhythm) AS rhythm,
|
303 |
+
LIST(pain) AS pain,
|
304 |
+
FROM vitalsign_causal
|
305 |
+
GROUP BY study_id;
|
306 |
+
"""
|
307 |
+
)
|
308 |
+
con.sql(
|
309 |
+
"""
|
310 |
+
CREATE OR REPLACE TABLE studies AS
|
311 |
+
SELECT
|
312 |
+
s.*,
|
313 |
+
v.charttime AS vitalsign_charttime,
|
314 |
+
v.temperature AS vitalsign_temperature,
|
315 |
+
v.heartrate AS vitalsign_heartrate,
|
316 |
+
v.resprate AS vitalsign_resprate,
|
317 |
+
v.o2sat AS vitalsign_o2sat,
|
318 |
+
v.sbp AS vitalsign_sbp,
|
319 |
+
v.dbp AS vitalsign_dbp,
|
320 |
+
v.rhythm AS vitalsign_rhythm,
|
321 |
+
v.pain AS vitalsign_pain,
|
322 |
+
FROM studies s
|
323 |
+
LEFT JOIN vitalsign_aggregated v
|
324 |
+
ON s.study_id = v.study_id;
|
325 |
+
"""
|
326 |
+
)
|
327 |
+
|
328 |
+
# Aggregate and then add the medrecon table:
|
329 |
+
con.sql(
|
330 |
+
"""
|
331 |
+
CREATE OR REPLACE TABLE medrecon_aggregated AS
|
332 |
+
SELECT
|
333 |
+
FIRST(subject_id) AS subject_id,
|
334 |
+
stay_id,
|
335 |
+
LIST(charttime) AS charttime,
|
336 |
+
LIST(name) as name,
|
337 |
+
LIST(gsn) AS gsn,
|
338 |
+
LIST(ndc) AS ndc,
|
339 |
+
LIST(etc_rn) AS etc_rn,
|
340 |
+
LIST(etccode) AS etccode,
|
341 |
+
LIST(etcdescription) AS etcdescription,
|
342 |
+
FROM medrecon
|
343 |
+
GROUP BY stay_id;
|
344 |
+
"""
|
345 |
+
)
|
346 |
+
con.sql(
|
347 |
+
"""
|
348 |
+
CREATE OR REPLACE TABLE studies AS
|
349 |
+
SELECT
|
350 |
+
s.*,
|
351 |
+
m.charttime AS medrecon_charttime,
|
352 |
+
m.name AS medrecon_name,
|
353 |
+
m.gsn AS medrecon_gsn,
|
354 |
+
m.ndc AS medrecon_ndc,
|
355 |
+
m.etc_rn AS medrecon_etc_rn,
|
356 |
+
m.etccode AS medrecon_etccode,
|
357 |
+
m.etcdescription AS medrecon_etcdescription,
|
358 |
+
FROM studies s
|
359 |
+
LEFT JOIN medrecon_aggregated m
|
360 |
+
ON s.stay_id = m.stay_id;
|
361 |
+
"""
|
362 |
+
)
|
363 |
+
|
364 |
+
# Aggregate and then add the pyxis table (ensuring no rows with a charttime after the latest study_datetime):
|
365 |
+
con.sql(
|
366 |
+
"""
|
367 |
+
CREATE OR REPLACE TABLE pyxis_causal AS
|
368 |
+
SELECT p.*, s.latest_study_datetime, s.study_id,
|
369 |
+
FROM pyxis p
|
370 |
+
JOIN studies s ON p.stay_id = s.stay_id
|
371 |
+
WHERE p.charttime < s.latest_study_datetime;
|
372 |
+
"""
|
373 |
+
) # This duplicates the rows for stay_ids that cover multiple study_ids. Hence, the following joins must be on study_id, not stay_id.
|
374 |
+
con.sql(
|
375 |
+
"""
|
376 |
+
CREATE OR REPLACE TABLE pyxis_aggregated AS
|
377 |
+
SELECT
|
378 |
+
study_id,
|
379 |
+
FIRST(subject_id) AS subject_id,
|
380 |
+
FIRST(stay_id) as stay_id,
|
381 |
+
LIST(charttime) AS charttime,
|
382 |
+
LIST(med_rn) as med_rn,
|
383 |
+
LIST(name) as name,
|
384 |
+
LIST(gsn_rn) AS gsn_rn,
|
385 |
+
LIST(gsn) AS gsn,
|
386 |
+
FROM pyxis_causal
|
387 |
+
GROUP BY study_id;
|
388 |
+
"""
|
389 |
+
)
|
390 |
+
con.sql(
|
391 |
+
"""
|
392 |
+
CREATE OR REPLACE TABLE studies AS
|
393 |
+
SELECT
|
394 |
+
s.*,
|
395 |
+
p.charttime AS pyxis_charttime,
|
396 |
+
p.med_rn AS pyxis_med_rn,
|
397 |
+
p.name AS pyxis_name,
|
398 |
+
p.gsn_rn AS pyxis_gsn_rn,
|
399 |
+
p.gsn AS pyxis_gsn,
|
400 |
+
FROM studies s
|
401 |
+
LEFT JOIN pyxis_aggregated p
|
402 |
+
ON s.study_id = p.study_id;
|
403 |
+
"""
|
404 |
+
)
|
405 |
+
|
406 |
+
# Add the reports:
|
407 |
+
con.sql(
|
408 |
+
"""
|
409 |
+
CREATE OR REPLACE TABLE studies AS
|
410 |
+
SELECT s.*, r.findings, r.impression, r.indication, r.history, r.comparison, r.last_paragraph, r.technique,
|
411 |
+
FROM studies s
|
412 |
+
LEFT JOIN mimic_cxr_sectioned r
|
413 |
+
ON s.study_id = r.study_id
|
414 |
+
"""
|
415 |
+
)
|
416 |
+
|
417 |
+
# Aggregate and then add the splits:
|
418 |
+
con.sql(
|
419 |
+
"""
|
420 |
+
CREATE OR REPLACE TABLE split_aggregated AS
|
421 |
+
SELECT
|
422 |
+
study_id,
|
423 |
+
FIRST(split) AS split,
|
424 |
+
FROM mimic_cxr_2_0_0_split
|
425 |
+
GROUP BY study_id;
|
426 |
+
"""
|
427 |
+
)
|
428 |
+
con.sql(
|
429 |
+
"""
|
430 |
+
CREATE OR REPLACE TABLE studies AS
|
431 |
+
SELECT s.*, x.split,
|
432 |
+
FROM studies s
|
433 |
+
JOIN split_aggregated x
|
434 |
+
ON s.study_id = x.study_id;
|
435 |
+
"""
|
436 |
+
)
|
437 |
+
|
438 |
+
# Prior studies column:
|
439 |
+
con.sql(
|
440 |
+
"""
|
441 |
+
CREATE OR REPLACE TABLE prior_studies AS
|
442 |
+
WITH sorted AS (
|
443 |
+
SELECT *,
|
444 |
+
ROW_NUMBER() OVER (PARTITION BY subject_id ORDER BY latest_study_datetime) AS rn
|
445 |
+
FROM studies
|
446 |
+
),
|
447 |
+
aggregated AS (
|
448 |
+
SELECT subject_id,
|
449 |
+
study_id,
|
450 |
+
latest_study_datetime,
|
451 |
+
ARRAY_AGG(study_id) OVER (PARTITION BY subject_id ORDER BY rn ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) AS prior_study_ids,
|
452 |
+
ARRAY_AGG(latest_study_datetime) OVER (PARTITION BY subject_id ORDER BY rn ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) AS prior_study_datetimes
|
453 |
+
FROM sorted
|
454 |
+
)
|
455 |
+
SELECT *
|
456 |
+
FROM aggregated;
|
457 |
+
"""
|
458 |
+
)
|
459 |
+
con.sql(
|
460 |
+
"""
|
461 |
+
CREATE OR REPLACE TABLE studies AS
|
462 |
+
SELECT s.*, p.prior_study_ids, p.prior_study_datetimes,
|
463 |
+
FROM studies s
|
464 |
+
LEFT JOIN prior_studies p
|
465 |
+
ON s.study_id = p.study_id
|
466 |
+
ORDER BY s.subject_id, s.study_datetime DESC;
|
467 |
+
"""
|
468 |
+
)
|
469 |
+
|
470 |
+
# Text columns:
|
471 |
+
text_columns = [f'{k}_{j}' if k != 'mimic_cxr_sectioned' else j for k, v in tables.items() if 'text_columns' in v for j in (v['text_columns'] if isinstance(v['text_columns'], list) else [v['text_columns']])] + ['findings', 'impression']
|
472 |
+
|
473 |
+
pattern = os.path.join(physionet_dir, 'mimic-cxr-jpg', '*', 'files')
|
474 |
+
mimic_cxr_jpg_dir = glob(pattern)
|
475 |
+
assert len(mimic_cxr_jpg_dir), f'Multiple directories matched the pattern {pattern}: {mimic_cxr_jpg_dir}. Only one is required.'
|
476 |
+
mimic_cxr_jpg_dir = mimic_cxr_jpg_dir[0]
|
477 |
+
|
478 |
+
def load_image(row):
|
479 |
+
images = []
|
480 |
+
for dicom_ids, study_id, subject_id in zip(row['dicom_id'], row['study_id'], row['subject_id']):
|
481 |
+
study_images = []
|
482 |
+
for dicom_id in dicom_ids:
|
483 |
+
image_path = mimic_cxr_image_path(mimic_cxr_jpg_dir, subject_id, study_id, dicom_id, 'jpg')
|
484 |
+
with open(image_path, 'rb') as f:
|
485 |
+
image = f.read()
|
486 |
+
study_images.append(image)
|
487 |
+
images.append(study_images)
|
488 |
+
row['images'] = images
|
489 |
+
return row
|
490 |
+
|
491 |
+
dataset_dict = {}
|
492 |
+
for split in ['test', 'validate', 'train']:
|
493 |
+
df = con.sql(f"FROM studies WHERE split = '{split}'").df()
|
494 |
+
|
495 |
+
# Format text columns:
|
496 |
+
for i in text_columns:
|
497 |
+
df[i] = df[i].apply(format)
|
498 |
+
|
499 |
+
# Save indices for each split:
|
500 |
+
df[df['findings'].notna() & df['impression'].notna()]['study_id'].to_json(
|
501 |
+
os.path.join(os.path.dirname(os.path.abspath(__file__)), f'mimic_cxr_jpg_{split}_study_ids.json'),
|
502 |
+
orient='records',
|
503 |
+
lines=False,
|
504 |
+
)
|
505 |
+
df_stay_id = df[df['findings'].notna() & df['impression'].notna() & df['stay_id'].notna()][['study_id', 'stay_id']]
|
506 |
+
df_stay_id['stay_id'] = df_stay_id['stay_id'].astype(int)
|
507 |
+
df_stay_id['study_id'].to_json(
|
508 |
+
os.path.join(os.path.dirname(os.path.abspath(__file__)), f'mimic_iv_ed_mimic_cxr_jpg_{split}_study_ids.json'),
|
509 |
+
orient='records',
|
510 |
+
lines=False,
|
511 |
+
)
|
512 |
+
|
513 |
+
if split == 'test':
|
514 |
+
pyxis_columns = [col for col in df.columns if col.startswith('pyxis_')]
|
515 |
+
df_pyxis = df[df['findings'].notna() & df['impression'].notna() & df['stay_id'].notna()]
|
516 |
+
df_pyxis = df_pyxis[~df_pyxis[pyxis_columns].isna().all(axis=1)]
|
517 |
+
df_pyxis['study_id'].to_json(
|
518 |
+
os.path.join(os.path.dirname(os.path.abspath(__file__)), f'mimic_iv_ed_mimic_cxr_jpg_pyxis_{split}_study_ids.json'),
|
519 |
+
orient='records',
|
520 |
+
lines=False,
|
521 |
+
)
|
522 |
+
|
523 |
+
vitalsign_columns = [col for col in df.columns if col.startswith('vitalsign_')]
|
524 |
+
df_vitalsign = df[df['findings'].notna() & df['impression'].notna() & df['stay_id'].notna()]
|
525 |
+
df_vitalsign = df_vitalsign[~df_vitalsign[vitalsign_columns].isna().all(axis=1)]
|
526 |
+
df_vitalsign['study_id'].to_json(
|
527 |
+
os.path.join(os.path.dirname(os.path.abspath(__file__)), f'mimic_iv_ed_mimic_cxr_jpg_vitalsign_{split}_study_ids.json'),
|
528 |
+
orient='records',
|
529 |
+
lines=False,
|
530 |
+
)
|
531 |
+
|
532 |
+
# dataset_dict[split] = datasets.Dataset.from_pandas(df)
|
533 |
+
# cache_dir = os.path.join(database_dir, '.cache')
|
534 |
+
# Path(cache_dir).mkdir(parents=True, exist_ok=True)
|
535 |
+
# dataset_dict[split] = dataset_dict[split].map(
|
536 |
+
# load_image,
|
537 |
+
# num_proc=num_workers,
|
538 |
+
# writer_batch_size=8,
|
539 |
+
# batched=True,
|
540 |
+
# batch_size=8,
|
541 |
+
# keep_in_memory=False,
|
542 |
+
# cache_file_name=os.path.join(cache_dir, f'.{split}'),
|
543 |
+
# load_from_cache_file=False,
|
544 |
+
# )
|
545 |
+
# dataset_dict[split].cleanup_cache_files()
|
546 |
+
# shutil.rmtree(cache_dir)
|
547 |
+
|
548 |
+
# dataset = datasets.DatasetDict(dataset_dict)
|
549 |
+
# dataset.save_to_disk(os.path.join(database_dir, 'mimic_iv_ed_mimic_cxr_jpg_dataset'))
|
550 |
+
|
551 |
+
# con.close()
|
552 |
+
|
553 |
+
|
554 |
+
if __name__ == "__main__":
|
555 |
+
physionet_dir = '/datasets/work/hb-mlaifsp-mm/work/archive/physionet.org/files' # Where MIMIC-CXR, MIMIC-CXR-JPG, and MIMIC-IV-ED are stored.
|
556 |
+
database_dir = '/scratch3/nic261/database/cxrmate_ed' # Where the resultant database will be stored.
|
557 |
+
|
558 |
+
prepare_dataset(physionet_dir=physionet_dir, database_dir=database_dir)
|
utils.py
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import torch
|
2 |
+
|
3 |
+
|
4 |
+
def compute_time_delta(event_time, reference_time, time_delta_map, denominator = 3600, to_tensor=True):
|
5 |
+
"""
|
6 |
+
How to we transform time delta inputs? It appears that minutes are used as the input to
|
7 |
+
a weight matrix in "Self-Supervised Transformer for Sparse and Irregularly Sampled Multivariate
|
8 |
+
Clinical Time-Series". This is almost confirmed by the CVE class defined here:
|
9 |
+
https://github.com/sindhura97/STraTS/blob/main/strats_notebook.ipynb, where the input has
|
10 |
+
a size of one.
|
11 |
+
"""
|
12 |
+
time_delta = reference_time - event_time
|
13 |
+
time_delta = time_delta.total_seconds() / (denominator)
|
14 |
+
assert isinstance(time_delta, float), f'time_delta should be float, not {type(time_delta)}.'
|
15 |
+
if time_delta < 0:
|
16 |
+
raise ValueError(f'time_delta should be greater than or equal to zero, not {time_delta}.')
|
17 |
+
time_delta = time_delta_map(time_delta)
|
18 |
+
if to_tensor:
|
19 |
+
time_delta = torch.tensor(time_delta)
|
20 |
+
return time_delta
|