Skip to content

Commit 129f58f

Browse files
Merge pull request #109 from OSIPI/DL_wrapper
Dl wrapper + DL testing
2 parents 7c03210 + e9d46b6 commit 129f58f

17 files changed

Lines changed: 1403 additions & 43 deletions

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ tests/IVIMmodels/unit_tests/*.log
2323
junit/*
2424
ivim_simulation.bval
2525
ivim_simulation.bvec
26+
*.pt
2627

2728
# Unit test / coverage reports
2829
.tox/
@@ -32,4 +33,5 @@ nosetests.xml
3233
coverage.xml
3334
*.pyc
3435
phantoms/MR_XCAT_qMRI/*.json
35-
phantoms/MR_XCAT_qMRI/*.txt
36+
phantoms/MR_XCAT_qMRI/*.txt
37+
tests/IVIMmodels/unit_tests/models

conftest.py

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ def pytest_addoption(parser):
3737
type=str,
3838
help="Default data file name",
3939
)
40+
parser.addoption(
41+
"--dataFileDL",
42+
default="tests/IVIMmodels/unit_tests/generic_DL.json",
43+
type=str,
44+
help="Default data file name",
45+
)
4046
parser.addoption(
4147
"--saveFileName",
4248
default="",
@@ -179,6 +185,10 @@ def pytest_generate_tests(metafunc):
179185
if "bound_input" in metafunc.fixturenames:
180186
args = bound_input(metafunc.config.getoption("dataFile"),metafunc.config.getoption("algorithmFile"))
181187
metafunc.parametrize("bound_input", args)
188+
if "deep_learning_algorithms" in metafunc.fixturenames:
189+
args = deep_learning_algorithms(metafunc.config.getoption("dataFileDL"),metafunc.config.getoption("algorithmFile"))
190+
metafunc.parametrize("deep_learning_algorithms", args)
191+
182192

183193

184194
def data_list(filename):
@@ -210,17 +220,18 @@ def data_ivim_fit_saved(datafile, algorithmFile):
210220
first = True
211221
for name, data in all_data.items():
212222
algorithm_dict = algorithm_information.get(algorithm, {})
213-
xfail = {"xfail": name in algorithm_dict.get("xfail_names", {}),
214-
"strict": algorithm_dict.get("xfail_names", {}).get(name, True)}
215-
kwargs = algorithm_dict.get("options", {})
216-
tolerances = algorithm_dict.get("tolerances", {})
217-
skiptime=False
218-
if first:
219-
if algorithm_dict.get("fail_first_time", False):
220-
skiptime = True
221-
first = False
222-
requires_matlab = algorithm_dict.get("requires_matlab", False)
223-
yield name, bvals, data, algorithm, xfail, kwargs, tolerances, skiptime, requires_matlab
223+
if not algorithm_dict.get('deep_learning',False):
224+
xfail = {"xfail": name in algorithm_dict.get("xfail_names", {}),
225+
"strict": algorithm_dict.get("xfail_names", {}).get(name, True)}
226+
kwargs = algorithm_dict.get("options", {})
227+
tolerances = algorithm_dict.get("tolerances", {})
228+
skiptime=False
229+
if first:
230+
if algorithm_dict.get("fail_first_time", False):
231+
skiptime = True
232+
first = False
233+
requires_matlab = algorithm_dict.get("requires_matlab", False)
234+
yield name, bvals, data, algorithm, xfail, kwargs, tolerances, skiptime, requires_matlab
224235

225236
def algorithmlist(algorithmFile):
226237
# Find the algorithms from algorithms.json
@@ -233,7 +244,7 @@ def algorithmlist(algorithmFile):
233244
for algorithm in algorithms:
234245
algorithm_dict = algorithm_information.get(algorithm, {})
235246
requires_matlab = algorithm_dict.get("requires_matlab", False)
236-
yield algorithm, requires_matlab
247+
yield algorithm, requires_matlab, algorithm_dict.get('deep_learning', False)
237248

238249
def bound_input(datafile,algorithmFile):
239250
# Find the algorithms from algorithms.json
@@ -251,9 +262,31 @@ def bound_input(datafile,algorithmFile):
251262
for name, data in all_data.items():
252263
for algorithm in algorithms:
253264
algorithm_dict = algorithm_information.get(algorithm, {})
254-
xfail = {"xfail": name in algorithm_dict.get("xfail_names", {}),
255-
"strict": algorithm_dict.get("xfail_names", {}).get(name, True)}
265+
if not algorithm_dict.get('deep_learning',False):
266+
xfail = {"xfail": name in algorithm_dict.get("xfail_names", {}),
267+
"strict": algorithm_dict.get("xfail_names", {}).get(name, True)}
268+
kwargs = algorithm_dict.get("options", {})
269+
tolerances = algorithm_dict.get("tolerances", {})
270+
requires_matlab = algorithm_dict.get("requires_matlab", False)
271+
yield name, bvals, data, algorithm, xfail, kwargs, tolerances, requires_matlab
272+
273+
def deep_learning_algorithms(datafile,algorithmFile):
274+
# Find the algorithms from algorithms.json
275+
current_folder = pathlib.Path.cwd()
276+
algorithm_path = current_folder / algorithmFile
277+
with algorithm_path.open() as f:
278+
algorithm_information = json.load(f)
279+
# Load generic test data generated from the included phantom: phantoms/MR_XCAT_qMRI
280+
generic = current_folder / datafile
281+
with generic.open() as f:
282+
all_data = json.load(f)
283+
algorithms = algorithm_information["algorithms"]
284+
bvals = all_data.pop('config')
285+
bvals = bvals['bvalues']
286+
for algorithm in algorithms:
287+
algorithm_dict = algorithm_information.get(algorithm, {})
288+
if algorithm_dict.get('deep_learning',False):
256289
kwargs = algorithm_dict.get("options", {})
257-
tolerances = algorithm_dict.get("tolerances", {})
258290
requires_matlab = algorithm_dict.get("requires_matlab", False)
259-
yield name, bvals, data, algorithm, xfail, kwargs, tolerances, requires_matlab
291+
tolerances = algorithm_dict.get("tolerances", {"atol":{"f": 2e-1, "D": 8e-4, "Dp": 8e-2},"rtol":{"f": 0.2, "D": 0.3, "Dp": 0.4}})
292+
yield algorithm, all_data, bvals, kwargs, requires_matlab, tolerances

src/standardized/IVIM_NEToptim.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
from src.wrappers.OsipiBase import OsipiBase
2+
import numpy as np
3+
import IVIMNET.deep as deep
4+
import torch
5+
import warnings
6+
from utilities.data_simulation.GenerateData import GenerateData
7+
8+
class IVIM_NEToptim(OsipiBase):
9+
"""
10+
Bi-exponential fitting algorithm by Oliver Gurney-Champion, Amsterdam UMC
11+
"""
12+
13+
# I'm thinking that we define default attributes for each submission like this
14+
# And in __init__, we can call the OsipiBase control functions to check whether
15+
# the user inputs fulfil the requirements
16+
17+
# Some basic stuff that identifies the algorithm
18+
id_author = "Oliver Gurney Champion, Amsterdam UMC"
19+
id_algorithm_type = "Deep learnt bi-exponential fit"
20+
id_return_parameters = "f, D*, D, S0"
21+
id_units = "seconds per milli metre squared or milliseconds per micro metre squared"
22+
23+
# Algorithm requirements
24+
required_bvalues = 4
25+
required_thresholds = [0,
26+
0] # Interval from "at least" to "at most", in case submissions allow a custom number of thresholds
27+
required_bounds = False
28+
required_bounds_optional = True # Bounds may not be required but are optional
29+
required_initial_guess = False
30+
required_initial_guess_optional = False
31+
accepted_dimensions = 1 # Not sure how to define this for the number of accepted dimensions. Perhaps like the thresholds, at least and at most?
32+
33+
34+
# Supported inputs in the standardized class
35+
supported_bounds = True
36+
supported_initial_guess = False
37+
supported_thresholds = False
38+
39+
def __init__(self, SNR=None, bvalues=None, thresholds=None, bounds=None, initial_guess=None, fitS0=True, traindata=None):
40+
"""
41+
Everything this algorithm requires should be implemented here.
42+
Number of segmentation thresholds, bounds, etc.
43+
44+
Our OsipiBase object could contain functions that compare the inputs with
45+
the requirements.
46+
"""
47+
if bvalues == None:
48+
raise ValueError("for deep learning models, bvalues need defining at initiaition")
49+
#super(OGC_AmsterdamUMC_biexp, self).__init__(bvalues, bounds, initial_guess, fitS0)
50+
super(IVIM_NEToptim, self).__init__(bvalues=bvalues, bounds=bounds, initial_guess=initial_guess)
51+
self.fitS0=fitS0
52+
self.bvalues=np.array(bvalues)
53+
self.initialize(bounds, initial_guess, fitS0, traindata, SNR)
54+
55+
def initialize(self, bounds, initial_guess, fitS0, traindata, SNR):
56+
self.fitS0=fitS0
57+
self.deep_learning = True
58+
self.supervised = False
59+
if traindata is None:
60+
warnings.warn('no training data provided (traindata = None). Training data will be simulated')
61+
if SNR is None:
62+
warnings.warn('No SNR indicated. Data simulated with SNR = (5-1000)')
63+
SNR = (5, 1000)
64+
self.training_data(self.bvalues,n=1000000,SNR=SNR)
65+
self.arg=Arg()
66+
if bounds is not None:
67+
self.arg.net_pars.cons_min = bounds[0] # Dt, Fp, Ds, S0
68+
self.arg.net_pars.cons_max = bounds[1] # Dt, Fp, Ds, S0
69+
if traindata is None:
70+
self.net = deep.learn_IVIM(self.train_data['data'], self.bvalues, self.arg)
71+
else:
72+
self.net = deep.learn_IVIM(traindata, self.bvalues, self.arg)
73+
self.algorithm =lambda data: deep.predict_IVIM(data, self.bvalues, self.net, self.arg)
74+
75+
76+
def ivim_fit(self, signals, bvalues, **kwargs):
77+
"""Perform the IVIM fit
78+
79+
Args:
80+
signals (array-like)
81+
bvalues (array-like): b-values for the signals. If None, self.bvalues will be used. Default is None.
82+
83+
Returns:
84+
_type_: _description_
85+
"""
86+
if not np.array_equal(bvalues, self.bvalues):
87+
raise ValueError("bvalue list at fitting must be identical as the one at initiation, otherwise it will not run")
88+
89+
paramsNN = deep.predict_IVIM(signals, self.bvalues, self.net, self.arg)
90+
91+
results = {}
92+
results["D"] = paramsNN[0]
93+
results["f"] = paramsNN[1]
94+
results["Dp"] = paramsNN[2]
95+
96+
return results
97+
98+
99+
def ivim_fit_full_volume(self, signals, bvalues, retrain_on_input_data=False, **kwargs):
100+
"""Perform the IVIM fit
101+
102+
Args:
103+
signals (array-like)
104+
bvalues (array-like): b-values for the signals. If None, self.bvalues will be used. Default is None.
105+
106+
Returns:
107+
_type_: _description_
108+
"""
109+
if not np.array_equal(bvalues, self.bvalues):
110+
raise ValueError("bvalue list at fitting must be identical as the one at initiation, otherwise it will not run")
111+
112+
signals = self.reshape_to_voxelwise(signals)
113+
if retrain_on_input_data:
114+
self.net = deep.learn_IVIM(signals, self.bvalues, self.arg, net=self.net)
115+
paramsNN = deep.predict_IVIM(signals, self.bvalues, self.net, self.arg)
116+
117+
results = {}
118+
results["D"] = paramsNN[0]
119+
results["f"] = paramsNN[1]
120+
results["Dp"] = paramsNN[2]
121+
122+
return results
123+
124+
def reshape_to_voxelwise(self, data):
125+
"""
126+
reshapes multi-D input (spatial dims, bvvalue) data to 2D voxel-wise array
127+
Args:
128+
data (array): mulit-D array (data x b-values)
129+
Returns:
130+
out (array): 2D array (voxel x b-value)
131+
"""
132+
B = data.shape[-1]
133+
voxels = int(np.prod(data.shape[:-1])) # e.g., X*Y*Z
134+
return data.reshape(voxels, B)
135+
136+
137+
def training_data(self, bvalues, data=None, SNR=(5,1000), n=1000000,Drange=(0.0005,0.0034),frange=(0,1),Dprange=(0.005,0.1),rician_noise=False):
138+
rng = np.random.RandomState(42)
139+
if data is None:
140+
gen = GenerateData(rng=rng)
141+
data, D, f, Dp = gen.simulate_training_data(bvalues, SNR=SNR, n=n,Drange=Drange,frange=frange,Dprange=Dprange,rician_noise=rician_noise)
142+
if self.supervised:
143+
self.train_data = {'data':data,'D':D,'f':f,'Dp':Dp}
144+
else:
145+
self.train_data = {'data': data}
146+
147+
class NetArgs:
148+
def __init__(self):
149+
self.optim = 'adam' # these are the optimisers implementd. Choices are: 'sgd'; 'sgdr'; 'adagrad' adam
150+
self.lr = 0.00003 # this is the learning rate.
151+
self.patience = 10 # this is the number of epochs without improvement that the network waits untill determining it found its optimum
152+
self.batch_size = 128 # number of datasets taken along per iteration
153+
self.maxit = 500 # max iterations per epoch
154+
self.split = 0.9 # split of test and validation data
155+
self.load_nn = False # load the neural network instead of retraining
156+
self.loss_fun = 'rms' # what is the loss used for the model. rms is root mean square (linear regression-like); L1 is L1 normalisation (less focus on outliers)
157+
self.skip_net = False # skip the network training and evaluation
158+
self.scheduler = False # as discussed in the article, LR is important. This approach allows to reduce the LR itteratively when there is no improvement throughout an 5 consecutive epochs
159+
# use GPU if available
160+
self.use_cuda = torch.cuda.is_available()
161+
self.device = torch.device("cuda:0" if self.use_cuda else "cpu")
162+
self.select_best = False
163+
# the optimized network settings
164+
165+
class NetPars:
166+
def __init__(self):
167+
self.dropout = 0.1 # 0.0/0.1 chose how much dropout one likes. 0=no dropout; internet says roughly 20% (0.20) is good, although it also states that smaller networks might desire smaller amount of dropout
168+
self.batch_norm = True # False/True turns on batch normalistion
169+
self.parallel = 'parallel' # defines whether the network exstimates each parameter seperately (each parameter has its own network) or whether 1 shared network is used instead
170+
self.con = 'sigmoid' # defines the constraint function; 'sigmoid' gives a sigmoid function giving the max/min; 'abs' gives the absolute of the output, 'none' does not constrain the output
171+
self.tri_exp = False
172+
#### only if sigmoid constraint is used!
173+
self.cons_min = [0, 0, 0.005, 0] # Dt, Fp, Ds, S0
174+
self.cons_max = [0.005, 0.8, 0.2, 2.0] # Dt, Fp, Ds, S0
175+
####
176+
self.fitS0 = True # indicates whether to fit S0 (True) or fix it to 1 (for normalised signals); I prefer fitting S0 as it takes along the potential error is S0.
177+
self.depth = 2 # number of layers
178+
self.width = 0 # new option that determines network width. Putting to 0 makes it as wide as the number of b-values
179+
boundsrange = 0.3 * (np.array(self.cons_max)-np.array(self.cons_min)) # ensure that we are on the most lineair bit of the sigmoid function
180+
self.cons_min = np.array(self.cons_min) - boundsrange
181+
self.cons_max = np.array(self.cons_max) + boundsrange
182+
class Arg:
183+
def __init__(self):
184+
self.train_pars = NetArgs()
185+
self.net_pars = NetPars()

0 commit comments

Comments
 (0)