diff --git a/superpixel_classification/SuperpixelClassification/SuperpixelClassification.xml b/superpixel_classification/SuperpixelClassification/SuperpixelClassification.xml index 38c7b77..bbb276a 100644 --- a/superpixel_classification/SuperpixelClassification/SuperpixelClassification.xml +++ b/superpixel_classification/SuperpixelClassification/SuperpixelClassification.xml @@ -100,6 +100,13 @@ true + + useCuda + usecuda + Whether or not to use GPU/cuda (true) or cpu (false). + + true + batchSize batchsize diff --git a/superpixel_classification/SuperpixelClassification/SuperpixelClassificationBase.py b/superpixel_classification/SuperpixelClassification/SuperpixelClassificationBase.py index adc1148..6502496 100644 --- a/superpixel_classification/SuperpixelClassification/SuperpixelClassificationBase.py +++ b/superpixel_classification/SuperpixelClassification/SuperpixelClassificationBase.py @@ -505,7 +505,7 @@ def trainModelAddItem(self, gc, record, item, annotrec, elem, feature, def trainModel(self, gc, folderId, annotationName, features, modelFolderId, batchSize, epochs, trainingSplit, randomInput, labelList, - excludeLabelList, prog): + excludeLabelList, use_cuda, prog): itemsAndAnnot = self.getItemsAndAnnotations(gc, folderId, annotationName) with tempfile.TemporaryDirectory(dir=os.getcwd()) as tempdir: trainingPath = os.path.join(tempdir, 'training.h5') @@ -544,7 +544,7 @@ def trainModel(self, gc, folderId, annotationName, features, modelFolderId, prog.progress(0) history, modelPath = self.trainModelDetails( record, annotationName, batchSize, epochs, itemsAndAnnot, prog, tempdir, - trainingSplit) + trainingSplit, use_cuda) modTrainingPath = os.path.join(tempdir, '%s ModTraining Epoch %d.h5' % ( annotationName, self.getCurrentEpoch(itemsAndAnnot))) @@ -568,7 +568,7 @@ def trainModel(self, gc, folderId, annotationName, features, modelFolderId, def predictLabelsForItem(self, gc, annotationName, annotationFolderId, tempdir, model, item, annotrec, elem, feature, curEpoch, userId, labels, groups, - makeHeatmaps, radius, magnification, certainty, batchSize, prog): + makeHeatmaps, radius, magnification, certainty, batchSize, use_cuda, prog): import al_bench.factory print('Predicting %s' % (item['name'])) @@ -771,7 +771,7 @@ def makeHeatmapsForItem(self, gc, annotationName, userId, tempdir, radius, item, def predictLabels(self, gc, folderId, annotationName, features, modelFolderId, annotationFolderId, saliencyMaps, radius, magnification, - certainty, batchSize, prog): + certainty, batchSize, use_cuda, prog): itemsAndAnnot = self.getItemsAndAnnotations(gc, folderId, annotationName) curEpoch = self.getCurrentEpoch(itemsAndAnnot) folder = gc.getFolder(folderId) @@ -833,7 +833,7 @@ def predictLabels(self, gc, folderId, annotationName, features, modelFolderId, self.predictLabelsForItem( gc, annotationName, annotationFolderId, tempdir, model, item, annotrec, elem, features.get(item['_id']), curEpoch, userId, labels, groups, saliencyMaps, - radius, magnification, certainty, batchSize, prog) + radius, magnification, certainty, batchSize, use_cuda, prog) prog.progress(1) def main(self, args): @@ -864,5 +864,5 @@ def main(self, args): self.predictLabels( gc, args.images, args.annotationName, features, args.modeldir, args.annotationDir, - args.heatmaps, args.radius, args.magnification, args.certainty, args.batchSize, + args.heatmaps, args.radius, args.magnification, args.certainty, args.batchSize, args.useCuda, prog) diff --git a/superpixel_classification/SuperpixelClassification/SuperpixelClassificationTensorflow.py b/superpixel_classification/SuperpixelClassification/SuperpixelClassificationTensorflow.py index 0af02d8..27ab67e 100644 --- a/superpixel_classification/SuperpixelClassification/SuperpixelClassificationTensorflow.py +++ b/superpixel_classification/SuperpixelClassification/SuperpixelClassificationTensorflow.py @@ -35,33 +35,56 @@ class SuperpixelClassificationTensorflow(SuperpixelClassificationBase): def __init__(self): self.training_optimal_batchsize: Optional[int] = None self.prediction_optimal_batchsize: Optional[int] = None + self.use_cuda = False def trainModelDetails(self, record, annotationName, batchSize, epochs, itemsAndAnnot, prog, - tempdir, trainingSplit): - # print(f'Tensorflow trainModelDetails(batchSize={batchSize}, ...)') - # make model - num_classes = len(record['labels']) - model = tf.keras.Sequential([ - tf.keras.layers.Rescaling(1.0 / 255), - tf.keras.layers.Conv2D(16, 3, padding='same', activation='relu'), - tf.keras.layers.MaxPooling2D(), - tf.keras.layers.Conv2D(32, 3, padding='same', activation='relu'), - tf.keras.layers.MaxPooling2D(), - tf.keras.layers.Conv2D(64, 3, padding='same', activation='relu'), - tf.keras.layers.MaxPooling2D(), - tf.keras.layers.Flatten(), - # tf.keras.layers.Dropout(0.2), - tf.keras.layers.Dense(128, activation='relu'), - tf.keras.layers.Dense(num_classes)]) - prog.progress(0.2) - model.compile(optimizer='adam', - loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True), - metrics=['accuracy']) + tempdir, trainingSplit, use_cuda): + self.use_cuda = use_cuda + + # Enable GPU memory growth globally to avoid precondition errors + gpus = tf.config.list_physical_devices('GPU') + if gpus and self.use_cuda: + try: + for gpu in gpus: + tf.config.experimental.set_memory_growth(gpu, True) + except RuntimeError as e: + print(f"Could not set memory growth: {e}") + if not self.use_cuda: + tf.config.set_visible_devices([], 'GPU') + device = "gpu" if use_cuda else "cpu" + print(f"Using device: {device}") + + # Dataset preparation (outside strategy scope) + ds_h5 = record['ds'] + labelds_h5 = record['labelds'] + # Fully load to memory and break h5py reference + ds_numpy = np.array(ds_h5[:]) + labelds_numpy = np.array(labelds_h5[:]) + + strategy = tf.distribute.MirroredStrategy() + with strategy.scope(): + num_classes = len(record['labels']) + model = tf.keras.Sequential([ + tf.keras.layers.Rescaling(1.0 / 255), + tf.keras.layers.Conv2D(16, 3, padding='same', activation='relu'), + tf.keras.layers.MaxPooling2D(), + tf.keras.layers.Conv2D(32, 3, padding='same', activation='relu'), + tf.keras.layers.MaxPooling2D(), + tf.keras.layers.Conv2D(64, 3, padding='same', activation='relu'), + tf.keras.layers.MaxPooling2D(), + tf.keras.layers.Flatten(), + tf.keras.layers.Dense(128, activation='relu'), + tf.keras.layers.Dense(num_classes)]) + prog.progress(0.2) + model.compile(optimizer='adam', + loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True), + metrics=['accuracy']) + prog.progress(0.7) - # generate split - full_ds = tf.data.Dataset.from_tensor_slices((record['ds'], record['labelds'])) - full_ds = full_ds.shuffle(1000) # add seed=123 ? - count = len(full_ds) + # generate split using numpy arrays + full_ds = tf.data.Dataset.from_tensor_slices((ds_numpy, labelds_numpy)) + full_ds = full_ds.shuffle(1000) + count = len(ds_numpy) train_size = int(count * trainingSplit) if batchSize < 1: batchSize = self.findOptimalBatchSize(model, full_ds, training=True) @@ -85,24 +108,53 @@ def trainModelDetails(self, record, annotationName, batchSize, epochs, itemsAndA self.saveModel(model, modelPath) return history, modelPath + def _get_device(self, use_cuda): + if tf.config.list_physical_devices('GPU') and use_cuda: + return '/GPU:0' + return '/CPU:0' + def predictLabelsForItemDetails( - self, batchSize, ds: h5py._hl.dataset.Dataset, item, model, prog, + self, batchSize, ds: h5py._hl.dataset.Dataset, indices, item, model, use_cuda, prog, ): - # print(f'Tensorflow predictLabelsForItemDetails(batchSize={batchSize}, ...)') if batchSize < 1: batchSize = self.findOptimalBatchSize( model, tf.data.Dataset.from_tensor_slices(ds), training=False, ) print(f'Optimal batch size for prediction = {batchSize}') - predictions = model.predict( - ds, - batch_size=batchSize, - callbacks=[_LogTensorflowProgress( - prog, (ds.shape[0] + batchSize - 1) // batchSize, 0.05, 0.35, item)]) - prog.item_progress(item, 0.4) - # softmax to scale to 0 to 1 - catWeights = tf.nn.softmax(predictions) - return catWeights, predictions + + device = self._get_device(use_cuda) + with tf.device(device): + # Create a dataset that pairs the data with their indices + dataset = tf.data.Dataset.from_tensor_slices((ds, indices)) + dataset = dataset.batch(batchSize) + + # Initialize arrays to store results + all_predictions = [] + all_cat_weights = [] + all_indices = [] + + # Iterate through batches manually to keep track of indices + for data, batch_indices in dataset: + batch_predictions = model.predict( + data, + batch_size=batchSize, + verbose=0) # Set verbose=0 to avoid multiple progress bars + + # Apply softmax to scale to 0 to 1 + batch_cat_weights = tf.nn.softmax(batch_predictions) + + all_predictions.append(batch_predictions) + all_cat_weights.append(batch_cat_weights) + all_indices.append(batch_indices) + + prog.item_progress(item, 0.4) + + # Concatenate all results + predictions = tf.concat(all_predictions, axis=0) + catWeights = tf.concat(all_cat_weights, axis=0) + final_indices = tf.concat(all_indices, axis=0) + + return catWeights.numpy(), predictions.numpy(), final_indices.numpy().astype(np.int64) def findOptimalBatchSize(self, model, ds, training) -> int: if training and self.training_optimal_batchsize is not None: diff --git a/superpixel_classification/SuperpixelClassification/SuperpixelClassificationTorch.py b/superpixel_classification/SuperpixelClassification/SuperpixelClassificationTorch.py index e06d247..85acfb3 100644 --- a/superpixel_classification/SuperpixelClassification/SuperpixelClassificationTorch.py +++ b/superpixel_classification/SuperpixelClassification/SuperpixelClassificationTorch.py @@ -66,12 +66,10 @@ class _BayesianPatchTorchModel(bbald.consistent_mc_dropout.BayesianModule): # A Bayesian model that takes patches (2-dimensional shape) rather than vectors # (1-dimensional shape) as input. It is useful when feature != 'vector' and # SuperpixelClassificationBase.certainty == 'batchbald'. - def __init__(self, num_classes: int) -> None: + def __init__(self, num_classes: int, device: torch.device) -> None: # Set `self.device` as early as possible so that other code does not lock out # what we want. - self.device: str = torch.device( - ('cuda' if torch.cuda.is_available() and torch.cuda.device_count() > 0 else 'cpu'), - ) + self.device : torch.device = device # print(f'Initial model.device = {self.device}') super(_BayesianPatchTorchModel, self).__init__() @@ -311,7 +309,10 @@ def trainModelDetails( prog: ProgressHelper, tempdir: str, trainingSplit: float, + cuda : bool, ): + device = torch.device("cuda" if cuda else "cpu") + print(f"Using device: {device}") # make model num_classes: int = len(record['labels']) model: torch.nn.Module @@ -507,7 +508,7 @@ def fitModel( return history def predictLabelsForItemDetails( - self, batchSize: int, ds_h5, item, model: torch.nn.Module, prog: ProgressHelper, + self, batchSize: int, ds_h5, item, model: torch.nn.Module, use_cuda : bool, prog: ProgressHelper, ): # print(f'Torch predictLabelsForItemDetails(batchSize={batchSize}, ...)') num_superpixels: int = ds_h5.shape[0] @@ -528,6 +529,9 @@ def predictLabelsForItemDetails( ) if self.certainty == 'batchbald' else dict(num_superpixels=num_superpixels, num_classes=num_classes) + # also set on model.device, ideally + #device = torch.device("cuda" if use_cuda else "cpu") + ) for cb in callbacks: cb.on_predict_begin(logs=logs)