netloader 3.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- netloader/__init__.py +61 -0
- netloader/architectures/__init__.py +48 -0
- netloader/architectures/base.py +1350 -0
- netloader/architectures/encoder_decoder.py +630 -0
- netloader/architectures/flows.py +501 -0
- netloader/architectures/utils.py +179 -0
- netloader/data.py +1092 -0
- netloader/layers/__init__.py +88 -0
- netloader/layers/base.py +367 -0
- netloader/layers/blocks.py +145 -0
- netloader/layers/convolutional.py +1008 -0
- netloader/layers/flows.py +123 -0
- netloader/layers/linear.py +445 -0
- netloader/layers/misc.py +879 -0
- netloader/layers/multi_layer.py +470 -0
- netloader/layers/pooling.py +298 -0
- netloader/layers/recurrent.py +163 -0
- netloader/layers/utils.py +168 -0
- netloader/loss_funcs.py +201 -0
- netloader/models/__init__.py +22 -0
- netloader/models/convnext.py +582 -0
- netloader/models/misc.py +209 -0
- netloader/network.py +895 -0
- netloader/networks/__init__.py +48 -0
- netloader/schedulers.py +409 -0
- netloader/transforms.py +847 -0
- netloader/utils/__init__.py +46 -0
- netloader/utils/configs.py +241 -0
- netloader/utils/transforms.py +22 -0
- netloader/utils/types.py +130 -0
- netloader/utils/utils.py +677 -0
- netloader-3.11.0.dist-info/METADATA +59 -0
- netloader-3.11.0.dist-info/RECORD +36 -0
- netloader-3.11.0.dist-info/WHEEL +5 -0
- netloader-3.11.0.dist-info/licenses/LICENSE.txt +21 -0
- netloader-3.11.0.dist-info/top_level.txt +1 -0
netloader/utils/utils.py
ADDED
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Misc functions used elsewhere
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import inspect
|
|
8
|
+
import logging as log
|
|
9
|
+
from copy import deepcopy
|
|
10
|
+
from types import ModuleType
|
|
11
|
+
from contextlib import contextmanager
|
|
12
|
+
from typing import Any, Literal, Generator, cast, overload
|
|
13
|
+
|
|
14
|
+
import torch
|
|
15
|
+
import numpy as np
|
|
16
|
+
from torch import Tensor
|
|
17
|
+
from numpy import ndarray
|
|
18
|
+
from packaging.version import Version
|
|
19
|
+
|
|
20
|
+
from netloader.utils.types import ArrayT
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Shapes(list[list[int] | list[list[int]]]):
|
|
24
|
+
"""
|
|
25
|
+
Shapes list that allows for indexing of multiple shapes.
|
|
26
|
+
"""
|
|
27
|
+
@overload # type: ignore[override]
|
|
28
|
+
def __getitem__(self, idx: int | tuple[int, int]) -> list[int]: ...
|
|
29
|
+
|
|
30
|
+
@overload
|
|
31
|
+
def __getitem__(self, idx: slice) -> Shapes: ...
|
|
32
|
+
|
|
33
|
+
def __getitem__(self, idx: int | tuple[int, int] | slice) -> list[int] | Shapes:
|
|
34
|
+
"""
|
|
35
|
+
Get shape from the shapes list.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
idx : int | tuple[int, int] | slice
|
|
40
|
+
Index of the shape to get, if tuple then first index is the layer index and second is
|
|
41
|
+
the shape index if the layer has multiple shapes, if slice, returns a Shapes object with
|
|
42
|
+
the sliced shapes
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
list[int] | Shapes
|
|
47
|
+
Shape at the provided index
|
|
48
|
+
"""
|
|
49
|
+
return self.get(idx, list_=False)
|
|
50
|
+
|
|
51
|
+
@overload # type: ignore[override]
|
|
52
|
+
def __setitem__(self, idx: slice, value: list[list[int] | list[list[int]]]) -> None: ...
|
|
53
|
+
|
|
54
|
+
@overload
|
|
55
|
+
def __setitem__(self, idx: tuple[int, int], value: list[int]) -> None: ...
|
|
56
|
+
|
|
57
|
+
@overload
|
|
58
|
+
def __setitem__(self, idx: int, value: list[int] | list[list[int]]) -> None: ...
|
|
59
|
+
|
|
60
|
+
def __setitem__(
|
|
61
|
+
self,
|
|
62
|
+
idx: int | tuple[int, int] | slice,
|
|
63
|
+
value: list[int] | list[list[int]] | list[list[int] | list[list[int]]]) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Sets shape in the shapes list.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
idx : int | tuple[int, int] | slice
|
|
70
|
+
Index of the shape to set, if tuple then first index is the layer index and second is
|
|
71
|
+
the shape index if the layer has multiple shapes, if slice, sets multiple shapes
|
|
72
|
+
value : list[int] | list[list[int]]
|
|
73
|
+
Shape to set at the provided index
|
|
74
|
+
"""
|
|
75
|
+
if isinstance(idx, tuple):
|
|
76
|
+
if not isinstance(self[idx[0]][0], list):
|
|
77
|
+
raise ValueError(f'Index {idx} does not contain multiple shapes, shape at index '
|
|
78
|
+
f'is {self[idx[0]]}')
|
|
79
|
+
if not isinstance(value[0], int):
|
|
80
|
+
raise ValueError(f'Index {idx} requires a single shape, got {value}')
|
|
81
|
+
|
|
82
|
+
cast(list[list[int]], self[idx[0]])[idx[1]] = cast(list[int], value)
|
|
83
|
+
elif isinstance(idx, int):
|
|
84
|
+
if isinstance(value[0], list) and isinstance(value[0][0], list):
|
|
85
|
+
raise ValueError(f'If index is an int, value must be a single shape or a list of '
|
|
86
|
+
f'shapes, got {value}')
|
|
87
|
+
super().__setitem__(idx, cast(list[int] | list[list[int]], value))
|
|
88
|
+
else:
|
|
89
|
+
if isinstance(value[0], int):
|
|
90
|
+
raise ValueError(f'If index is a slice, value must be a list of shapes, got '
|
|
91
|
+
f'{value}')
|
|
92
|
+
super().__setitem__(idx, cast(list[list[int] | list[list[int]]], value))
|
|
93
|
+
|
|
94
|
+
def __repr__(self) -> str:
|
|
95
|
+
return f'Shapes({super().__repr__()})'
|
|
96
|
+
|
|
97
|
+
def check(self, idx: int) -> bool:
|
|
98
|
+
"""
|
|
99
|
+
Checks if the shape at the provided index is a single shape.
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
idx : int
|
|
104
|
+
Index of the shape to check
|
|
105
|
+
|
|
106
|
+
Returns
|
|
107
|
+
-------
|
|
108
|
+
bool
|
|
109
|
+
True if the shape at the provided index is a single shape
|
|
110
|
+
"""
|
|
111
|
+
if isinstance(self.get(idx, list_=True)[0], int):
|
|
112
|
+
return True
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
@overload
|
|
116
|
+
def get(self, idx: slice, list_: bool) -> Shapes: ...
|
|
117
|
+
|
|
118
|
+
@overload
|
|
119
|
+
def get(self, idx: tuple[int, int], list_: bool) -> list[int]: ...
|
|
120
|
+
|
|
121
|
+
@overload
|
|
122
|
+
def get(self, idx: int, list_: Literal[False]) -> list[int]: ...
|
|
123
|
+
|
|
124
|
+
@overload
|
|
125
|
+
def get(self, idx: int, list_: Literal[True]) -> list[int] | list[list[int]]: ...
|
|
126
|
+
|
|
127
|
+
@overload
|
|
128
|
+
def get(self, idx: int, list_: bool) -> list[int] | list[list[int]]: ...
|
|
129
|
+
|
|
130
|
+
def get(
|
|
131
|
+
self,
|
|
132
|
+
idx: int | tuple[int, int] | slice,
|
|
133
|
+
list_: bool = True) -> list[int] | list[list[int]] | Shapes:
|
|
134
|
+
"""
|
|
135
|
+
Get shape from the shapes list.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
idx : int | tuple[int, int] | slice
|
|
140
|
+
Index of the shape to get, if tuple then first index is the layer index and second is
|
|
141
|
+
the shape index if the layer has multiple shapes, if slice, returns a Shapes object with
|
|
142
|
+
the sliced shapes
|
|
143
|
+
list_ : bool, Optional
|
|
144
|
+
If true, returns a list of shapes if the index contains multiple shapes, else returns a
|
|
145
|
+
single shape, default = True
|
|
146
|
+
|
|
147
|
+
Returns
|
|
148
|
+
-------
|
|
149
|
+
list[int] | list[list[int]] | Shapes
|
|
150
|
+
Shape at the provided index
|
|
151
|
+
"""
|
|
152
|
+
item: list[int] | list[list[int]] | list[list[int] | list[list[int]]] = super().__getitem__(
|
|
153
|
+
idx if isinstance(idx, (int, slice)) else idx[0],
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if isinstance(idx, slice):
|
|
157
|
+
return Shapes(cast(list[list[int] | list[list[int]]], item))
|
|
158
|
+
if isinstance(idx, tuple) and isinstance(item[0], list):
|
|
159
|
+
return cast(list[int], item[idx[1]])
|
|
160
|
+
if isinstance(idx, tuple):
|
|
161
|
+
raise ValueError(f'Index {idx} does not contain multiple shapes, shape at index is '
|
|
162
|
+
f'{item}')
|
|
163
|
+
if isinstance(item[0], list) and not list_:
|
|
164
|
+
raise ValueError(f'Index {idx} does not contain a single shape, shape at index is '
|
|
165
|
+
f'{item}, if you want to return all shapes at that index, use '
|
|
166
|
+
f'Shapes.get(idx, list_=True) instead')
|
|
167
|
+
return cast(list[int], item)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def ascii_plot(
|
|
171
|
+
data: list[float],
|
|
172
|
+
*,
|
|
173
|
+
clear: bool = False,
|
|
174
|
+
x_num: int = 20,
|
|
175
|
+
height: int = 6,
|
|
176
|
+
text: str = '',
|
|
177
|
+
label: str = '',
|
|
178
|
+
data2: list[float] | None = None) -> None:
|
|
179
|
+
"""
|
|
180
|
+
Prints a simple ASCII graph of data per index.
|
|
181
|
+
|
|
182
|
+
Parameters
|
|
183
|
+
----------
|
|
184
|
+
data : list[float]
|
|
185
|
+
Data to plot
|
|
186
|
+
clear : bool, Optional
|
|
187
|
+
If true, clears the previous graph, WARNING: this is very buggy and may not work properly,
|
|
188
|
+
default = False
|
|
189
|
+
x_num : int, Optional
|
|
190
|
+
Maximum number of epochs to display in the graph, default = 20
|
|
191
|
+
height : int, Optional
|
|
192
|
+
Height of the graph in lines, default = 6
|
|
193
|
+
text : str, Optional
|
|
194
|
+
Optional text to display at the end of the graph
|
|
195
|
+
label : str, Optional
|
|
196
|
+
Label for the x-axis
|
|
197
|
+
data2 : list[float] | None, Optional
|
|
198
|
+
Secondary data to plot
|
|
199
|
+
"""
|
|
200
|
+
if len(data) < 2:
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
i: int
|
|
204
|
+
row: int
|
|
205
|
+
row2: int
|
|
206
|
+
spacing: int = int(np.ceil(np.log10(len(data)))) + 1
|
|
207
|
+
datum: float
|
|
208
|
+
datum2: float | None
|
|
209
|
+
line: str
|
|
210
|
+
symbol: str
|
|
211
|
+
symbol2: str
|
|
212
|
+
x_label: str = f'{label}:' if label else ''
|
|
213
|
+
grid: list[str]
|
|
214
|
+
symbols: list[str] = ['_', '-', '‾'] # low, mid, high
|
|
215
|
+
symbols2: list[str] = ['.', '·', '˙']
|
|
216
|
+
rows: ndarray
|
|
217
|
+
red_data2: ndarray
|
|
218
|
+
red_data: ndarray = np.array(data)[-x_num:]
|
|
219
|
+
|
|
220
|
+
if data2:
|
|
221
|
+
red_data2 = np.array(data2)[-x_num:]
|
|
222
|
+
range_ = (min(red_data.min(), red_data2.min()), max(red_data.max(), red_data2.max()))
|
|
223
|
+
red_data2 = (red_data2 - range_[0]) / (range_[1] - range_[0])
|
|
224
|
+
else:
|
|
225
|
+
red_data2 = np.array([None] * len(red_data))
|
|
226
|
+
range_ = (red_data.min(), red_data.max())
|
|
227
|
+
|
|
228
|
+
rows = np.linspace(0, 1, height + 1)
|
|
229
|
+
red_data = (red_data - range_[0]) / (range_[1] - range_[0])
|
|
230
|
+
grid = [''] * height
|
|
231
|
+
rows[-1] += 1
|
|
232
|
+
|
|
233
|
+
if clear:
|
|
234
|
+
sys.stdout.write(f'\033[{height + 2}F')
|
|
235
|
+
|
|
236
|
+
for i, (datum, datum2) in enumerate(zip(red_data, red_data2)):
|
|
237
|
+
row = int(np.digitize(datum, rows)) - 1
|
|
238
|
+
symbol = symbols[min(
|
|
239
|
+
int((datum - rows[row]) // (1 / len(symbols) / height)),
|
|
240
|
+
len(symbols) - 1,
|
|
241
|
+
)]
|
|
242
|
+
|
|
243
|
+
if datum2 is not None:
|
|
244
|
+
row2 = int(np.digitize(datum2, rows)) - 1
|
|
245
|
+
symbol2 = symbols2[min(
|
|
246
|
+
int((datum2 - rows[row2]) // (1 / len(symbols2) / height)),
|
|
247
|
+
len(symbols2) - 1,
|
|
248
|
+
)]
|
|
249
|
+
|
|
250
|
+
if row == row2:
|
|
251
|
+
symbol = '*'
|
|
252
|
+
else:
|
|
253
|
+
grid[row2] = f'{grid[row2]:<{i * spacing + len(x_label) + 1}}{symbol2 * spacing}'
|
|
254
|
+
|
|
255
|
+
grid[row] = f'{grid[row]:<{i * spacing + len(x_label) + 1}}{symbol * spacing}'
|
|
256
|
+
|
|
257
|
+
for line in grid[::-1]:
|
|
258
|
+
print(line)
|
|
259
|
+
|
|
260
|
+
print(
|
|
261
|
+
x_label,
|
|
262
|
+
''.join(f'{i + 1:^{spacing}}' for i in range(max(0, len(data) - x_num), len(data))),
|
|
263
|
+
f'\n{text}',
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def check_params(name: str, supported_params: list[str] | ndarray, in_params: ndarray) -> None:
|
|
268
|
+
"""
|
|
269
|
+
Checks if provided parameters are supported by the function.
|
|
270
|
+
|
|
271
|
+
Parameters
|
|
272
|
+
----------
|
|
273
|
+
name : str
|
|
274
|
+
Name of the function
|
|
275
|
+
supported_params : list[str] | ndarray
|
|
276
|
+
Parameters supported by the function
|
|
277
|
+
in_params : ndarray
|
|
278
|
+
Input parameters of shape (N), where N is the number of parameters
|
|
279
|
+
"""
|
|
280
|
+
bad_params: ndarray = in_params[~np.isin(in_params, supported_params)]
|
|
281
|
+
|
|
282
|
+
if len(bad_params):
|
|
283
|
+
log.getLogger(__name__).warning(f'Unknown parameters for {name}: {bad_params}')
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def compare_versions(cur_ver: str, min_ver: str) -> bool:
|
|
287
|
+
"""
|
|
288
|
+
Compares two version strings.
|
|
289
|
+
|
|
290
|
+
Parameters
|
|
291
|
+
----------
|
|
292
|
+
cur_ver : str
|
|
293
|
+
Current version
|
|
294
|
+
min_ver : str
|
|
295
|
+
Minimum version
|
|
296
|
+
|
|
297
|
+
Returns
|
|
298
|
+
-------
|
|
299
|
+
bool
|
|
300
|
+
True if current version is greater than or equal to minimum version
|
|
301
|
+
"""
|
|
302
|
+
if not bool(cur_ver):
|
|
303
|
+
return True
|
|
304
|
+
return not '<' in cur_ver and Version(cur_ver) >= Version(min_ver)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def deep_merge(base: dict, new: dict) -> dict:
|
|
308
|
+
"""
|
|
309
|
+
Performs a deep merge of two dictionaries, equivalent to recursive base | new
|
|
310
|
+
|
|
311
|
+
Parameters
|
|
312
|
+
----------
|
|
313
|
+
base : dict
|
|
314
|
+
Base dictionary
|
|
315
|
+
new : dict
|
|
316
|
+
Dictionary to deep merge into base
|
|
317
|
+
|
|
318
|
+
Returns
|
|
319
|
+
-------
|
|
320
|
+
dict
|
|
321
|
+
Deep merged dictionary
|
|
322
|
+
"""
|
|
323
|
+
merged: dict = base.copy()
|
|
324
|
+
key: Any
|
|
325
|
+
value: Any
|
|
326
|
+
|
|
327
|
+
for key, value in new.items():
|
|
328
|
+
if key in merged and isinstance(merged[key], dict) and isinstance(value, dict):
|
|
329
|
+
merged[key] = deep_merge(merged[key], value)
|
|
330
|
+
else:
|
|
331
|
+
merged[key] = value
|
|
332
|
+
return merged
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def dict_list_append(
|
|
336
|
+
dict1: dict[str, Any],
|
|
337
|
+
dict2: dict[str, Any],
|
|
338
|
+
concat: bool = False,
|
|
339
|
+
balance: bool = False) -> dict[str, Any]:
|
|
340
|
+
"""
|
|
341
|
+
Merges two dictionaries.
|
|
342
|
+
If a key exists in both dictionaries and not concat, the data type must match.
|
|
343
|
+
If data type is list or ndarray, the values are extended.
|
|
344
|
+
If data type is any other type and not concat, the value from the second dictionary overrides
|
|
345
|
+
the first, else if concat, the values are converted to a list and appended.
|
|
346
|
+
If balance is True, all lists and ndarrays in each dictionary must have the same length, if a
|
|
347
|
+
key is missing from one dictionary, it is added with None values to match the length of the
|
|
348
|
+
other lists/ndarrays in that dictionary.
|
|
349
|
+
|
|
350
|
+
Parameters
|
|
351
|
+
----------
|
|
352
|
+
dict1 : dict[str, Any]
|
|
353
|
+
Primary dict to merge secondary dict into
|
|
354
|
+
dict2 : dict[str, Any]
|
|
355
|
+
Secondary dict to merge into primary dict
|
|
356
|
+
concat : bool, Optional
|
|
357
|
+
If true, concatenates non list/ndarray types instead of overriding them, default = False
|
|
358
|
+
balance : bool, Optional
|
|
359
|
+
If true, balances the lengths of lists/ndarrays in each dictionary so that each list/ndarray
|
|
360
|
+
in the returning dictionary has the same length, default = False
|
|
361
|
+
|
|
362
|
+
Returns
|
|
363
|
+
-------
|
|
364
|
+
dict[str, Any]
|
|
365
|
+
First dict with second dict merged into it
|
|
366
|
+
"""
|
|
367
|
+
dict1_len: int = 0
|
|
368
|
+
dict2_len: int = 0
|
|
369
|
+
key: str
|
|
370
|
+
dict2 = deepcopy(dict2)
|
|
371
|
+
|
|
372
|
+
if not concat:
|
|
373
|
+
for key in np.intersect1d(list(dict1.keys()), list(dict2.keys())).tolist():
|
|
374
|
+
if not isinstance(dict1[key], type(dict2[key])):
|
|
375
|
+
raise TypeError(f'Type mismatch for key "{key}": {type(dict1[key])} != '
|
|
376
|
+
f'{type(dict2[key])}')
|
|
377
|
+
|
|
378
|
+
if balance:
|
|
379
|
+
# If primary dict is not empty, find the length of a list/ndarray in the dictionary
|
|
380
|
+
if len(dict1) > 0:
|
|
381
|
+
for value in dict1.values():
|
|
382
|
+
if isinstance(value, (list, ndarray)):
|
|
383
|
+
dict1_len = len(value)
|
|
384
|
+
break
|
|
385
|
+
|
|
386
|
+
if any(len(value) != dict1_len for value in dict1.values()
|
|
387
|
+
if isinstance(value, (list, ndarray))):
|
|
388
|
+
raise ValueError('All lists/ndarrays in dict1 must have the same length')
|
|
389
|
+
|
|
390
|
+
# If secondary dict is not empty, find the length of a list/ndarray in the dictionary
|
|
391
|
+
if len(dict2) > 0:
|
|
392
|
+
for value in dict2.values():
|
|
393
|
+
if isinstance(value, (list, ndarray)):
|
|
394
|
+
dict2_len = len(value)
|
|
395
|
+
break
|
|
396
|
+
|
|
397
|
+
if any(len(value) != dict2_len for value in dict2.values()
|
|
398
|
+
if isinstance(value, (list, ndarray))):
|
|
399
|
+
raise ValueError('All lists/ndarrays in dict2 must have the same length')
|
|
400
|
+
|
|
401
|
+
# Merge two dictionaries
|
|
402
|
+
for key in np.unique(list(dict1.keys()) + list(dict2.keys())).tolist():
|
|
403
|
+
# If the secondary dict has a key not in the primary, pad with Nones
|
|
404
|
+
if key in dict1 or not balance:
|
|
405
|
+
pass
|
|
406
|
+
elif isinstance(dict2[key], ndarray):
|
|
407
|
+
dict1[key] = np.full(
|
|
408
|
+
[dict1_len] + (list(dict2[key][0].shape) if dict2[key].ndim > 1 else []),
|
|
409
|
+
None,
|
|
410
|
+
)
|
|
411
|
+
elif isinstance(dict2[key], list) or concat:
|
|
412
|
+
dict1[key] = [None] * dict1_len
|
|
413
|
+
|
|
414
|
+
if concat and key in dict1 and not isinstance(dict1[key], (list, ndarray)):
|
|
415
|
+
dict1[key] = [dict1[key]]
|
|
416
|
+
|
|
417
|
+
if concat and key in dict2 and not isinstance(dict2[key], (list, ndarray)):
|
|
418
|
+
dict2[key] = [dict2[key]]
|
|
419
|
+
|
|
420
|
+
# If the primary dict has a key not in the secondary dict, pad with Nones, else merge dicts
|
|
421
|
+
if key not in dict2 and not balance:
|
|
422
|
+
continue
|
|
423
|
+
if key not in dict1:
|
|
424
|
+
dict1[key] = dict2[key]
|
|
425
|
+
elif balance and key not in dict2 and isinstance(dict1[key], ndarray):
|
|
426
|
+
dict1[key] = np.concat(
|
|
427
|
+
(dict1[key], [np.full( # type: ignore[arg-type]
|
|
428
|
+
dict1[key][0].shape,
|
|
429
|
+
None,
|
|
430
|
+
) if dict1[key].ndim > 1 else None] * dict2_len),
|
|
431
|
+
axis=0,
|
|
432
|
+
)
|
|
433
|
+
elif balance and key not in dict2 and isinstance(dict1[key], list):
|
|
434
|
+
dict1[key].extend([None] * dict2_len)
|
|
435
|
+
elif isinstance(dict1[key], ndarray):
|
|
436
|
+
dict1[key] = np.concatenate((dict1[key], dict2[key]), axis=0)
|
|
437
|
+
elif isinstance(dict1[key], list):
|
|
438
|
+
dict1[key].extend(dict2[key])
|
|
439
|
+
elif key in dict2:
|
|
440
|
+
dict1[key] = dict2[key]
|
|
441
|
+
return dict1
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def dict_list_convert(data: dict[str, list[Any] | ndarray]) -> list[dict[str, Any]]:
|
|
445
|
+
"""
|
|
446
|
+
Converts a dictionary of lists to a list of dictionaries.
|
|
447
|
+
|
|
448
|
+
Parameters
|
|
449
|
+
----------
|
|
450
|
+
data : dict[str, list[Any] | ndarray]
|
|
451
|
+
Dictionary of lists to convert
|
|
452
|
+
|
|
453
|
+
Returns
|
|
454
|
+
-------
|
|
455
|
+
list[dict[str, Any]]
|
|
456
|
+
List of dictionaries
|
|
457
|
+
"""
|
|
458
|
+
return [{key: data[key][i:i + 1] for key in data} for i in range(len(list(data.values())[0]))]
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def get_device() -> tuple[dict[str, Any], torch.device]:
|
|
462
|
+
"""
|
|
463
|
+
Gets the device for PyTorch to use.
|
|
464
|
+
|
|
465
|
+
Returns
|
|
466
|
+
-------
|
|
467
|
+
tuple[dict[str, Any], torch.device]
|
|
468
|
+
Arguments for the PyTorch DataLoader to use when loading data into memory and PyTorch device
|
|
469
|
+
"""
|
|
470
|
+
device: torch.device = torch.device(
|
|
471
|
+
'cuda' if torch.cuda.is_available() else
|
|
472
|
+
'mps' if torch.backends.mps.is_available() else
|
|
473
|
+
'cpu',
|
|
474
|
+
)
|
|
475
|
+
kwargs: dict[str, Any] = {
|
|
476
|
+
'num_workers': 4,
|
|
477
|
+
'pin_memory': True,
|
|
478
|
+
'persistent_workers': True,
|
|
479
|
+
} if device == torch.device('cuda') else {}
|
|
480
|
+
return kwargs, device
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def label_change(
|
|
484
|
+
data: ArrayT,
|
|
485
|
+
in_label: ArrayT,
|
|
486
|
+
*,
|
|
487
|
+
one_hot: bool = False,
|
|
488
|
+
out_label: ArrayT | None = None) -> ArrayT:
|
|
489
|
+
"""
|
|
490
|
+
Converts an array or tensor of class values to an array or tensor of class indices.
|
|
491
|
+
|
|
492
|
+
Parameters
|
|
493
|
+
----------
|
|
494
|
+
data : ArrayT
|
|
495
|
+
Classes of shape (N) where N is the number of samples
|
|
496
|
+
in_label : ArrayT
|
|
497
|
+
Unique class values of size (C) where C is the number of classes
|
|
498
|
+
one_hot : bool, Optional
|
|
499
|
+
If the returned tensor should be 1D array of class indices or 2D one hot tensor if out_label
|
|
500
|
+
is None or is an int, default = False
|
|
501
|
+
out_label : ArrayT | None, Optional
|
|
502
|
+
Unique class values of size (C) to transform data into, if None, then values will be indexes
|
|
503
|
+
|
|
504
|
+
Returns
|
|
505
|
+
-------
|
|
506
|
+
ArrayT
|
|
507
|
+
ndarray or Tensor of class indices of shape (N) and type float, or if one_hot is True, one
|
|
508
|
+
hot tensor of shape (N,C) and type float
|
|
509
|
+
"""
|
|
510
|
+
data_one_hot: ArrayT
|
|
511
|
+
out_data: ArrayT
|
|
512
|
+
module: ModuleType
|
|
513
|
+
|
|
514
|
+
if isinstance(data, Tensor):
|
|
515
|
+
module = torch
|
|
516
|
+
elif isinstance(data, ndarray):
|
|
517
|
+
module = np
|
|
518
|
+
else:
|
|
519
|
+
raise TypeError(f'Data type {type(data)} not supported')
|
|
520
|
+
|
|
521
|
+
if out_label is None:
|
|
522
|
+
out_label = module.arange(len(in_label))
|
|
523
|
+
|
|
524
|
+
if isinstance(out_label, Tensor):
|
|
525
|
+
out_label = cast(ArrayT, out_label.to(data.device))
|
|
526
|
+
|
|
527
|
+
out_data = cast(ArrayT, out_label[module.searchsorted(in_label, data)])
|
|
528
|
+
|
|
529
|
+
if one_hot:
|
|
530
|
+
data_one_hot = module.zeros((len(data), len(in_label)))
|
|
531
|
+
data_one_hot[module.arange(len(data)), out_data] = 1
|
|
532
|
+
out_data = data_one_hot
|
|
533
|
+
|
|
534
|
+
return cast(ArrayT, out_data.to(data.device) if isinstance(out_data, Tensor) else out_data)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def list_dict_convert(
|
|
538
|
+
data: list[dict[str, Any]],
|
|
539
|
+
balance: bool = False,
|
|
540
|
+
concat: bool = False) -> dict[str, list[Any]]:
|
|
541
|
+
"""
|
|
542
|
+
Converts a list of dictionaries to a dictionary of lists
|
|
543
|
+
|
|
544
|
+
Parameters
|
|
545
|
+
----------
|
|
546
|
+
data : list[dict[str, float | ndarray]]
|
|
547
|
+
List of dictionaries to convert
|
|
548
|
+
balance : bool, Optional
|
|
549
|
+
If true, balances the lengths of lists/ndarrays in each dictionary so that each list/ndarray
|
|
550
|
+
in the returning dictionary has the same length, default = False
|
|
551
|
+
concat : bool, Optional
|
|
552
|
+
If true, concatenates non list/ndarray types instead of overriding them, default = False
|
|
553
|
+
|
|
554
|
+
Returns
|
|
555
|
+
-------
|
|
556
|
+
dict[str, list[float | None] | list[ndarray]]
|
|
557
|
+
Dictionary of lists
|
|
558
|
+
"""
|
|
559
|
+
value: dict[str, float | ndarray]
|
|
560
|
+
new_data: dict[str, list[float] | list[ndarray]] = {}
|
|
561
|
+
|
|
562
|
+
for value in data:
|
|
563
|
+
dict_list_append(new_data, value, concat=concat, balance=balance)
|
|
564
|
+
return new_data
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def progress_bar(i: int, total: int, *, text: str = '', **kwargs: Any) -> None:
|
|
568
|
+
"""
|
|
569
|
+
Terminal progress bar.
|
|
570
|
+
|
|
571
|
+
Parameters
|
|
572
|
+
----------
|
|
573
|
+
i : int
|
|
574
|
+
Current progress
|
|
575
|
+
total : int
|
|
576
|
+
Completion number
|
|
577
|
+
text : str, Optional
|
|
578
|
+
Optional text to place at the end of the progress bar
|
|
579
|
+
**kwargs
|
|
580
|
+
Optional keyword arguments to pass to print
|
|
581
|
+
"""
|
|
582
|
+
filled: int
|
|
583
|
+
length: int = 50
|
|
584
|
+
percent: float
|
|
585
|
+
bar_fill: str
|
|
586
|
+
|
|
587
|
+
i += 1
|
|
588
|
+
filled = int(i * length / total)
|
|
589
|
+
percent = i * 100 / total
|
|
590
|
+
bar_fill = '█' * filled + '-' * (length - filled)
|
|
591
|
+
print(f'\rProgress: |{bar_fill}| {int(percent)}%\t{text}\t', end='', **kwargs)
|
|
592
|
+
|
|
593
|
+
if i == total:
|
|
594
|
+
print()
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def safe_globals(safe_package: str, modules: ModuleType | list[ModuleType]) -> None:
|
|
598
|
+
"""
|
|
599
|
+
Adds all classes in the module from the specified package to the list of safe PyTorch classes
|
|
600
|
+
when loading saved architectures.
|
|
601
|
+
|
|
602
|
+
Parameters
|
|
603
|
+
----------
|
|
604
|
+
safe_package : str
|
|
605
|
+
Package that classes must belong to to be added to the safe globals
|
|
606
|
+
modules : ModuleType | list[ModuleType]
|
|
607
|
+
Module(s) to add all classes from to the list of safe PyTorch classes when loading saved
|
|
608
|
+
architectures
|
|
609
|
+
"""
|
|
610
|
+
if isinstance(modules, ModuleType):
|
|
611
|
+
modules = [modules]
|
|
612
|
+
|
|
613
|
+
for module in modules:
|
|
614
|
+
torch.serialization.add_safe_globals([
|
|
615
|
+
member[1] for member in inspect.getmembers(sys.modules[module.__name__])
|
|
616
|
+
if isinstance(member[1], type) and
|
|
617
|
+
member[1].__module__.split('.', maxsplit=1)[0] == safe_package
|
|
618
|
+
])
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def save_name(num: int | str, states_dir: str, name: str) -> str:
|
|
622
|
+
"""
|
|
623
|
+
Standardises the architecture save file naming.
|
|
624
|
+
|
|
625
|
+
Parameters
|
|
626
|
+
----------
|
|
627
|
+
num : int | str
|
|
628
|
+
File number or name
|
|
629
|
+
states_dir : str
|
|
630
|
+
Directory of network saves
|
|
631
|
+
name : str
|
|
632
|
+
Name of the network
|
|
633
|
+
|
|
634
|
+
Returns
|
|
635
|
+
-------
|
|
636
|
+
str
|
|
637
|
+
Path to the network save file
|
|
638
|
+
"""
|
|
639
|
+
return os.path.join(states_dir, f'{name}_{num}.pth')
|
|
640
|
+
|
|
641
|
+
@contextmanager
|
|
642
|
+
def suppress_logger_warnings(logger_name: str) -> Generator[None, None, None]:
|
|
643
|
+
"""
|
|
644
|
+
Suppresses warnings from a specified logger.
|
|
645
|
+
|
|
646
|
+
Parameters
|
|
647
|
+
----------
|
|
648
|
+
logger_name : str
|
|
649
|
+
Name of the logger to suppress warnings from
|
|
650
|
+
"""
|
|
651
|
+
level: int
|
|
652
|
+
logger: log.Logger = log.getLogger(logger_name)
|
|
653
|
+
level = logger.level
|
|
654
|
+
logger.setLevel(log.ERROR)
|
|
655
|
+
|
|
656
|
+
try:
|
|
657
|
+
yield
|
|
658
|
+
finally:
|
|
659
|
+
logger.setLevel(level)
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
__all__ = [
|
|
663
|
+
'Shapes',
|
|
664
|
+
'ascii_plot',
|
|
665
|
+
'check_params',
|
|
666
|
+
'compare_versions',
|
|
667
|
+
'deep_merge',
|
|
668
|
+
'dict_list_append',
|
|
669
|
+
'dict_list_convert',
|
|
670
|
+
'get_device',
|
|
671
|
+
'label_change',
|
|
672
|
+
'list_dict_convert',
|
|
673
|
+
'progress_bar',
|
|
674
|
+
'safe_globals',
|
|
675
|
+
'save_name',
|
|
676
|
+
'suppress_logger_warnings',
|
|
677
|
+
]
|