Skip to content

Commit

Permalink
keras toy example plus almost working model fit
Browse files Browse the repository at this point in the history
  • Loading branch information
kyle-woodward committed Mar 14, 2024
1 parent 3f2a1db commit 74943ec
Show file tree
Hide file tree
Showing 3 changed files with 538 additions and 55 deletions.
305 changes: 305 additions & 0 deletions fao_models/ate_tensorflow_train_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
# example from crop mapping workflow to look at
#%%writefile {PACKAGE_PATH}/task.py

# Imports
import tensorflow as tf
from tensorflow.keras import layers, models, optimizers, losses
from functools import partial
from tensorflow.keras.metrics import categorical_accuracy
from tensorflow.keras import backend as backend
from tensorflow import keras

# Paths and Dataset Parameters
PROJECT = 'trainingcambodia'
OUTPUT_DIR = 'gs://cambodiatraining/ate/cashew_v5/model/cashew_trained-modelv2'
LOGS_DIR = 'gs://cambodiatraining/model/logs'
TRAINING_PATTERN = 'gs://cambodiatraining/ate/cashew_v6/training/*'
VALIDATION_PATTERN = 'gs://cambodiatraining/ate/cashew_v6/testing/*'

# Model and Training Configuration
BANDS = ['R', 'G', 'B', 'N']
RESPONSE = ["class"]
FEATURES = BANDS + RESPONSE


TRAIN_SIZE = 7000
EVAL_SIZE = 2000

BATCH_SIZE = 32
EPOCHS = 12

BUFFER_SIZE = 512
optimizer = tf.keras.optimizers.Adam()

# Specify the size and shape of patches expected by the model.
KERNEL_SIZE = 128
KERNEL_SHAPE = [KERNEL_SIZE, KERNEL_SIZE]
COLUMNS = [
tf.io.FixedLenFeature(shape=KERNEL_SHAPE, dtype=tf.float32) for k in FEATURES
]
FEATURES_DICT = dict(zip(FEATURES, COLUMNS))


def recall_m(y_true, y_pred):
true_positives = backend.sum(backend.round(backend.clip(y_true * y_pred, 0, 1)))
possible_positives = backend.sum(backend.round(backend.clip(y_true, 0, 1)))
recall = true_positives / (possible_positives + backend.epsilon())
return recall

def precision_m(y_true, y_pred):
true_positives = backend.sum(backend.round(backend.clip(y_true * y_pred, 0, 1)))
predicted_positives = backend.sum(backend.round(backend.clip(y_pred, 0, 1)))
precision = true_positives / (predicted_positives + backend.epsilon())
return precision

def f1_m(y_true, y_pred):
precision = precision_m(y_true, y_pred)
recall = recall_m(y_true, y_pred)
return 2 * ((precision * recall) / (precision + recall + backend.epsilon()))

def dice_coef(y_true, y_pred, smooth=1):
y_true_f = backend.flatten(y_true)
y_pred_f = backend.flatten(y_pred)
intersection = backend.sum(y_true_f * y_pred_f)
return (2. * intersection + smooth) / (backend.sum(y_true_f) + backend.sum(y_pred_f) + smooth)

def dice_loss(y_true, y_pred, smooth=1):
intersection = backend.sum(backend.abs(y_true * y_pred), axis=-1)
true_sum = backend.sum(backend.square(y_true), -1)
pred_sum = backend.sum(backend.square(y_pred), -1)
return 1 - ((2. * intersection + smooth) / (true_sum + pred_sum + smooth))

evaluation_metrics = [categorical_accuracy, f1_m, precision_m, recall_m]

@tf.function
def random_transform(data, label):
x = tf.random.uniform(())

if x < 0.10:
# Apply flip left-right to both data and label
data = tf.image.flip_left_right(data)
label = tf.image.flip_left_right(label)
elif tf.math.logical_and(x >= 0.10, x < 0.20):
# Apply flip up-down to both data and label
data = tf.image.flip_up_down(data)
label = tf.image.flip_up_down(label)
elif tf.math.logical_and(x >= 0.20, x < 0.30):
# Apply flip left-right and up-down to both data and label
data = tf.image.flip_left_right(tf.image.flip_up_down(data))
label = tf.image.flip_left_right(tf.image.flip_up_down(label))
elif tf.math.logical_and(x >= 0.30, x < 0.40):
# Rotate both data and label 90 degrees
data = tf.image.rot90(data, k=1)
label = tf.image.rot90(label, k=1)
elif tf.math.logical_and(x >= 0.40, x < 0.50):
# Rotate both data and label 180 degrees
data = tf.image.rot90(data, k=2)
label = tf.image.rot90(label, k=2)
elif tf.math.logical_and(x >= 0.50, x < 0.60):
# Rotate both data and label 270 degrees
data = tf.image.rot90(data, k=3)
label = tf.image.rot90(label, k=3)
else:
pass

return data, label


@tf.function
def flip_inputs_up_down(inputs):
return tf.image.flip_up_down(inputs)

@tf.function
def flip_inputs_left_right(inputs):
return tf.image.flip_left_right(inputs)

@tf.function
def transpose_inputs(inputs):
flip_up_down = tf.image.flip_up_down(inputs)
transpose = tf.image.flip_left_right(flip_up_down)
return transpose

@tf.function
def rotate_inputs_90(inputs):
return tf.image.rot90(inputs, k=1)

@tf.function
def rotate_inputs_180(inputs):
return tf.image.rot90(inputs, k=2)

@tf.function
def rotate_inputs_270(inputs):
return tf.image.rot90(inputs, k=3)




# Tensorflow setup for GPU
gpus = tf.config.list_physical_devices('GPU')
if gpus:
try:
for gpu in gpus:
tf.config.experimental.set_memory_growth(gpu, True)
except RuntimeError as e:
print(e)
def parse_tfrecord(example_proto):
"""The parsing function.
Read a serialized example into the structure defined by FEATURES_DICT.
Args:
example_proto: a serialized Example.
Returns:
A dictionary of tensors, keyed by feature name.
"""
return tf.io.parse_single_example(example_proto, FEATURES_DICT)


def to_tuple(inputs):
"""Function to convert a dictionary of tensors to a tuple of (inputs, outputs).
Turn the tensors returned by parse_tfrecord into a stack in HWC shape.
Args:
inputs: A dictionary of tensors, keyed by feature name.
Returns:
A tuple of (inputs, outputs).
"""
inputsList = [inputs.get(key) for key in FEATURES]
stacked = tf.stack(inputsList, axis=0)
# Convert from CHW to HWC
stacked = tf.transpose(stacked, [1, 2, 0])

# Extract the label tensor
label = stacked[:, :, len(BANDS):]

# Create the complementary label tensor (opposite value)
opposite_label = 1 - label

# Concatenate the label and its opposite value
labels_combined = tf.concat([label, opposite_label], axis=-1)

return stacked[:, :, :len(BANDS)], labels_combined
#return stacked[:, :, :len(BANDS)], label

def get_dataset(pattern):
"""Function to read, parse, and format to tuple a set of input tfrecord files.
Get all the files matching the pattern, parse and convert to tuple.
Args:
pattern: A file pattern to match in a Cloud Storage bucket.
Returns:
A tf.data.Dataset
"""
glob = tf.io.gfile.glob(pattern)
dataset = tf.data.TFRecordDataset(glob, compression_type='GZIP')
dataset = dataset.map(parse_tfrecord, num_parallel_calls=5)
dataset = dataset.map(to_tuple, num_parallel_calls=5)

# Apply random transformations to each pair of data and label in the dataset
transformed_dataset = dataset.map(lambda data, label: random_transform(data, label),
num_parallel_calls=tf.data.experimental.AUTOTUNE)

# Concatenate the original dataset with the transformed dataset to double the size
dataset = dataset.concatenate(transformed_dataset)
return dataset


def get_training_dataset(glob):
"""Get the preprocessed training dataset
Returns:
A tf.data.Dataset of training data.
"""
dataset = get_dataset(glob)
dataset = dataset.shuffle(BUFFER_SIZE).batch(BATCH_SIZE) #.repeat()
return dataset

training = get_training_dataset(TRAINING_PATTERN)
eval = get_training_dataset(VALIDATION_PATTERN)


def conv_block(input_tensor, num_filters):
encoder = layers.Conv2D(num_filters, (3, 3), padding='same')(input_tensor)
encoder = layers.BatchNormalization()(encoder)
encoder = layers.Activation('relu')(encoder)
encoder = layers.Conv2D(num_filters, (3, 3), padding='same')(encoder)
encoder = layers.BatchNormalization()(encoder)
encoder = layers.Activation('relu')(encoder)
return encoder

def encoder_block(input_tensor, num_filters):
encoder = conv_block(input_tensor, num_filters)
encoder_pool = layers.MaxPooling2D((2, 2), strides=(2, 2))(encoder)
return encoder_pool, encoder

def decoder_block(input_tensor, concat_tensor, num_filters):
decoder = layers.Conv2DTranspose(num_filters, (2, 2), strides=(2, 2), padding='same')(input_tensor)
decoder = layers.concatenate([concat_tensor, decoder], axis=-1)
decoder = layers.BatchNormalization()(decoder)
decoder = layers.Activation('relu')(decoder)
decoder = layers.Conv2D(num_filters, (3, 3), padding='same')(decoder)
decoder = layers.BatchNormalization()(decoder)
decoder = layers.Activation('relu')(decoder)
decoder = layers.Conv2D(num_filters, (3, 3), padding='same')(decoder)
decoder = layers.BatchNormalization()(decoder)
decoder = layers.Activation('relu')(decoder)
return decoder

def get_model():
inputs = layers.Input(shape=[None, None, 4])
encoder0_pool, encoder0 = encoder_block(inputs, 16)
encoder1_pool, encoder1 = encoder_block(encoder0_pool, 32)
encoder2_pool, encoder2 = encoder_block(encoder1_pool, 64)
encoder3_pool, encoder3 = encoder_block(encoder2_pool, 128)
encoder4_pool, encoder4 = encoder_block(encoder3_pool, 256)
center = conv_block(encoder4_pool, 512)

decoder4 = decoder_block(center, encoder4, 256)
decoder3 = decoder_block(decoder4, encoder3, 128)
decoder2 = decoder_block(decoder3, encoder2, 64)
decoder1 = decoder_block(decoder2, encoder1, 32)
decoder0 = decoder_block(decoder1, encoder0, 16)
outputs = layers.Conv2D(2, (1, 1), activation='sigmoid')(decoder0)

model = models.Model(inputs=[inputs], outputs=[outputs])

model.compile(
optimizer=optimizer,
loss=dice_loss,
metrics=evaluation_metrics
)

return model

# Now, include all the custom objects (metrics and loss functions) in a dictionary
custom_objects_dict = {
'f1_m': f1_m,
'precision_m': precision_m,
'recall_m': recall_m,
'dice_coef': dice_coef,
'dice_loss': dice_loss # Including the custom loss function
}


if __name__ == '__main__':

LOGS_DIR = 'logs' # This will be a local path within the Colab environment.
callbacks = [tf.keras.callbacks.TensorBoard(log_dir=LOGS_DIR, histogram_freq=1)]

# Prepare datasets
training = get_training_dataset(TRAINING_PATTERN)
eval = get_training_dataset(VALIDATION_PATTERN)

# Initialize and compile model
model = get_model()
MODEL_DIR = "gs://cambodiatraining/ate/model/version3"
model = keras.models.load_model(MODEL_DIR, custom_objects=custom_objects_dict)
print(model.summary())

# Model training

model.fit(
training,
validation_data=eval,
epochs=EPOCHS, # You might want to use the EPOCHS variable here
callbacks=[tf.keras.callbacks.TensorBoard(LOGS_DIR)]
)

# Save the trained model
#model.save(OUTPUT_DIR)

67 changes: 67 additions & 0 deletions fao_models/keras_layers_toy_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#%%
import tensorflow as tf
import tensorflow.keras.layers as layers
# The inputs is a batch of 10 32x32 RGBN images with 4 channels
input_shape = (10, 32, 32, 4)
x = tf.random.normal(input_shape)
# %%
# example from Ates crop mapping CNN
# model architecture will consist of encoder blocks which consists of a conv_block then maxPooling2D
def conv_block(input_tensor, num_filters):
encoder = layers.Conv2D(num_filters, (3, 3), padding='same')(input_tensor)
encoder = layers.BatchNormalization()(encoder)
encoder = layers.Activation('relu')(encoder)
encoder = layers.Conv2D(num_filters, (3, 3), padding='same')(encoder)
encoder = layers.BatchNormalization()(encoder)
encoder = layers.Activation('relu')(encoder)
return encoder

def encoder_block(input_tensor, num_filters):
encoder = conv_block(input_tensor, num_filters)
encoder_pool = layers.MaxPooling2D((2, 2), strides=(2, 2))(encoder)
return encoder_pool, encoder
#%%
# demonstrating what happens in the encoders...
print('original shape',x.shape) # start with a batch of 10 32x32x4 arrays
# inputs = layers.Input(shape=[None, None, 4])
encoder0_pool, encoder0 = encoder_block(x, 16)
print('encoder0_pool.shape',encoder0_pool.shape)
print(encoder0_pool[0][0])
print('encoder0.shape',encoder0.shape)
print(encoder0[0][0])
# %%
# pass that result thru another encoder block
encoder1_pool, encoder1 = encoder_block(encoder0_pool, 32)
print('encoder1_pool.shape',encoder1_pool.shape)
print(encoder1_pool[0][0])
print('encoder1.shape',encoder1.shape)
print(encoder1[0][0])
# %%
# do this a few more times till we get to the center
encoder2_pool, encoder2 = encoder_block(encoder1_pool, 64)
encoder3_pool, encoder3 = encoder_block(encoder2_pool, 128)
encoder4_pool, encoder4 = encoder_block(encoder3_pool, 256)
center = conv_block(encoder4_pool, 512)

# eventually get center which is 10 1D arays of 512 values
print('center.shape',center.shape) # (10, 1, 1, 512)
print(center[0][0])
# %%
# and then pass to output
# this is the output in Ates model arch, this is 1D arrays with 2 channels,
outputs_num_filter2 = layers.Conv2D(2, (1, 1), activation='sigmoid')(center)
print('outputs_num_filter2.shape',outputs_num_filter2.shape) # (10, 1, 1, 2)
print(outputs_num_filter2[0][0])

# wouldn't we want just one value? (10, 1, 1, 1)?
outputs_num_filter1 = layers.Conv2D(1, (1, 1), activation='sigmoid')(center)
print('outputs_num_filter1.shape',outputs_num_filter1.shape) # (10, 1, 1, 1)
print(outputs_num_filter1[0][0])

# %%
# and then how do you get a 1 or 0, not floating point output?
# but I'm assuming that you don't want to have this as the output in the compiled model because you can't validate assess loss,
# rather would the model output the floating point value and then you'd use a threshold to convert to 1 or 0 when serving inferences?
output_class = tf.cast(tf.greater(outputs_num_filter1, 0.5), dtype=tf.int32)
print('output_class',output_class[0][0])
# %%
Loading

0 comments on commit 74943ec

Please sign in to comment.