For a machine learning task I have coded with support of the internet a configurable PoinNet Version in tensorflow keras.
The keras example from PointNet I got here: Point cloud classification with PointNet
I am using tensorflow = "2.11.0"
What do I want?
- Implemented an easy version of PointNet (see code below)
- I have some stl files which I load with
open3d = "^0.18.0"
to convert it to PointClouds - I want to use
tf.data.Dataset
pipe to load the data and pre- and postprocess it - I want to predict a certain curve/values on the end, depending on the input (but this is not the problem)
- For simplicity and clarity I implemented two options: 1. Load all pointcloud data in one numpy array before training AND 2.Load pointcloud data while training via
tf.py_function
My Problem
- When using option 2 (load pointcloud data while training via
tf.py_function
): regardless of the number of points in a point cloud, the data is passed through the neural network, although it does not match theINPUT_SHAPE
from the neural net - When I use option 1 (load all pointclouds before training and then build the
tf.data.Dataset
pipe): everything works as expected. If the pointcloud shape does not match theINPUT_SHAPE
of the neural net, an error occurs. - Why is there the strange behavior? Is it because of the usage of
tf.py_function
?
My code block for the PointNet
import os import numpy as np import tensorflow as tf from tensorflow import keras from tensorflow.keras import layers import open3d as o3d import pandas as pd import random ############################################################################################## # define constants NUM_POINTS = 20 INPUT_SHAPE = [20, 3] BATCHSIZE = 6 NUM_CLASSES = 10 NUM_SAMPLES = 6 WHICH_LOADING = "all" # all, single_py_function PATH_FOLDER = "data/few_examples/stl_examples/" ############################################################################################## # define loading functions def load_all_pointclouds(list_filepaths): list_arrays = [] for path in list_filepaths: mesh = o3d.io.read_triangle_mesh(path) pointcloud = mesh.sample_points_poisson_disk( number_of_points=NUM_POINTS, init_factor=5 ) array_pointcloud = np.asarray(pointcloud.points) list_arrays.append(array_pointcloud) print("array_pointcloud.shape: ", array_pointcloud.shape) arrays_pointcloud = np.array(list_arrays) return arrays_pointcloud def load_single_pointcloud(filepath): filepath = filepath.numpy().decode("utf-8") mesh = o3d.io.read_triangle_mesh(filepath) pointcloud = mesh.sample_points_poisson_disk( number_of_points=NUM_POINTS, init_factor=5 ) array_pointcloud = np.asarray(pointcloud.points) array_pointcloud = np.float32(array_pointcloud) array_pointcloud = tf.ensure_shape(array_pointcloud, INPUT_SHAPE) print("array_pointcloud.shape: ", array_pointcloud.shape) return array_pointcloud ############################################################################################## ### create filepaths list_files = os.listdir(PATH_FOLDER) list_filepaths = [PATH_FOLDER + x for x in list_files] ### create y data df_y = pd.DataFrame(np.random.rand(NUM_SAMPLES, NUM_CLASSES)) ds_y = tf.data.Dataset.from_tensor_slices(df_y) ### logic for loading while train or preload all data if WHICH_LOADING == "single_py_function": df_3d = pd.DataFrame(list_filepaths) ds_3d = tf.data.Dataset.from_tensor_slices(df_3d) ds_3d = ds_3d.map( lambda filepath: tf.py_function( load_single_pointcloud, [filepath[0]], Tout=tf.float32 ) ) if WHICH_LOADING == "all": arr_3d = load_all_pointclouds(list_filepaths) ds_3d = tf.data.Dataset.from_tensor_slices(arr_3d) ### zip datasets and postprocess data ds = tf.data.Dataset.zip((ds_3d, ds_y)) ds = ds.shuffle(buffer_size=1000, seed=42).batch(BATCHSIZE).prefetch(tf.data.AUTOTUNE) ### check ds data for i, element in enumerate(ds.as_numpy_iterator()): print("array_pointcloud.type: ", type(element[0])) print("array_pointcloud.shape: ", element[0].shape) print("y.shape", element[1].shape) ### Build a model def conv_bn(x, filters): x = layers.Conv1D(filters, kernel_size=1, padding="valid")(x) x = layers.BatchNormalization(momentum=0.0)(x) return layers.Activation("relu")(x) def dense_bn(x, filters): x = layers.Dense(filters)(x) x = layers.BatchNormalization(momentum=0.0)(x) return layers.Activation("relu")(x) class OrthogonalRegularizer(keras.regularizers.Regularizer): def __init__(self, num_features, l2reg=0.001): self.num_features = num_features self.l2reg = l2reg self.eye = tf.eye(num_features) def __call__(self, x): x = tf.reshape(x, (-1, self.num_features, self.num_features)) xxt = tf.tensordot(x, x, axes=(2, 2)) xxt = tf.reshape(xxt, (-1, self.num_features, self.num_features)) return tf.reduce_sum(self.l2reg * tf.square(xxt - self.eye)) def tnet(inputs, num_features): # Initalise bias as the indentity matrix bias = keras.initializers.Constant(np.eye(num_features).flatten()) reg = OrthogonalRegularizer(num_features) x = conv_bn(inputs, 32) x = conv_bn(x, 64) x = conv_bn(x, 512) x = layers.GlobalMaxPooling1D()(x) x = dense_bn(x, 256) x = dense_bn(x, 128) x = layers.Dense( num_features * num_features, kernel_initializer="zeros", bias_initializer=bias, activity_regularizer=reg, )(x) feat_T = layers.Reshape((num_features, num_features))(x) return layers.Dot(axes=(2, 1))([inputs, feat_T]) inputs = keras.Input(shape=INPUT_SHAPE) x = tnet(inputs, 3) x = conv_bn(x, 32) x = conv_bn(x, 32) x = tnet(x, 32) x = conv_bn(x, 32) x = conv_bn(x, 64) x = conv_bn(x, 512) x = layers.GlobalMaxPooling1D()(x) x = dense_bn(x, 256) x = layers.Dropout(0.3)(x) x = dense_bn(x, 128) x = layers.Dropout(0.3)(x) outputs = layers.Dense(NUM_CLASSES, activation="softmax")(x) model = keras.Model(inputs=inputs, outputs=outputs, name="pointnet") ### compile and fit data # model.summary() model.compile( loss="mse", optimizer=keras.optimizers.Adam(learning_rate=0.001), ) model.fit(ds, epochs=2)
Print Results on different configurations
Configuration 1 - loading all before train works fine when shapes match
NUM_POINTS = 20 INPUT_SHAPE = [20, 3] BATCHSIZE = 6 WHICH_LOADING = "all_before"
Print Result for Config 1
array_pointcloud.shape in load_all_pointclouds function: (20, 3) array_pointcloud.shape in load_all_pointclouds function: (20, 3) array_pointcloud.shape in load_all_pointclouds function: (20, 3) array_pointcloud.shape in load_all_pointclouds function: (20, 3) array_pointcloud.shape in load_all_pointclouds function: (20, 3) array_pointcloud.shape in load_all_pointclouds function: (20, 3) array_pointcloud.type in dataset loop: <class 'numpy.ndarray'> array_pointcloud.shape in dataset loop: (6, 20, 3) y.shape in dataset loop (6, 10) INPUT_SHAPE neural net: [20, 3] NUM_POINTS predefined: 20 BATCHSIZE predefined: 6 Epoch 1/2 1/1 [==============================] - 9s 9s/step - loss: 0.7237 Epoch 2/2 1/1 [==============================] - 0s 16ms/step - loss: 0.6924
Configuration 2 - loading all before train throws error as expected when shapes do not match
NUM_POINTS = 21 INPUT_SHAPE = [20, 3] BATCHSIZE = 6 WHICH_LOADING = "all_before"
Print Result for Config 2
array_pointcloud.shape in load_all_pointclouds function: (21, 3) array_pointcloud.shape in load_all_pointclouds function: (21, 3) array_pointcloud.shape in load_all_pointclouds function: (21, 3) array_pointcloud.shape in load_all_pointclouds function: (21, 3) array_pointcloud.shape in load_all_pointclouds function: (21, 3) array_pointcloud.shape in load_all_pointclouds function: (21, 3) array_pointcloud.type in dataset loop: <class 'numpy.ndarray'> array_pointcloud.shape in dataset loop: (6, 21, 3) y.shape in dataset loop (6, 10) INPUT_SHAPE neural net: [20, 3] NUM_POINTS predefined: 21 BATCHSIZE predefined: 6
Error message for Config 2
ValueError: Input 0 of layer "pointnet" is incompatible with the layer: expected shape=(None, 20, 3), found shape=(None, 21, 3)
Configuration 3 - loading while train works fine when shapes match
NUM_POINTS = 20 INPUT_SHAPE = [20, 3] BATCHSIZE = 6 WHICH_LOADING = "single_py_function_while_train"
Print Result for Config 3
array_pointcloud.shape in load_single_pointcloud function: (20, 3) array_pointcloud.shape in load_single_pointcloud function: (20, 3) array_pointcloud.shape in load_single_pointcloud function: (20, 3) array_pointcloud.shape in load_single_pointcloud function: (20, 3) array_pointcloud.shape in load_single_pointcloud function: (20, 3) array_pointcloud.shape in load_single_pointcloud function: (20, 3) array_pointcloud.type in dataset loop: <class 'numpy.ndarray'> array_pointcloud.shape in dataset loop: (6, 20, 3) y.shape in dataset loop (6, 10) INPUT_SHAPE neural net: [20, 3] NUM_POINTS predefined: 20 BATCHSIZE predefined: 6 Epoch 1/2 array_pointcloud.shape in load_single_pointcloud function: (20, 3) array_pointcloud.shape in load_single_pointcloud function: (20, 3) array_pointcloud.shape in load_single_pointcloud function: (20, 3) array_pointcloud.shape in load_single_pointcloud function: (20, 3) array_pointcloud.shape in load_single_pointcloud function: (20, 3) array_pointcloud.shape in load_single_pointcloud function: (20, 3) 1/1 [==============================] - 16s 16s/step - loss: 0.5778 Epoch 2/2 array_pointcloud.shape in load_single_pointcloud function: (20, 3) array_pointcloud.shape in load_single_pointcloud function: (20, 3) array_pointcloud.shape in load_single_pointcloud function: (20, 3) array_pointcloud.shape in load_single_pointcloud function: (20, 3) array_pointcloud.shape in load_single_pointcloud function: (20, 3) array_pointcloud.shape in load_single_pointcloud function: (20, 3) 1/1 [==============================] - 0s 193ms/step - loss: 0.5620
Configuration 4 - loading while train does not work as expected if shapes do not match. It should throw an error. But it does not.
NUM_POINTS = 21 INPUT_SHAPE = [20, 3] BATCHSIZE = 6 WHICH_LOADING = "single_py_function_while_train"
Print Result for Config 3
array_pointcloud.shape in load_single_pointcloud function: (21, 3) array_pointcloud.shape in load_single_pointcloud function: (21, 3) array_pointcloud.shape in load_single_pointcloud function: (21, 3) array_pointcloud.shape in load_single_pointcloud function: (21, 3) array_pointcloud.shape in load_single_pointcloud function: (21, 3) array_pointcloud.shape in load_single_pointcloud function: (21, 3) array_pointcloud.type in dataset loop: <class 'numpy.ndarray'> array_pointcloud.shape in dataset loop: (6, 21, 3) y.shape in dataset loop (6, 10) INPUT_SHAPE neural net: [20, 3] NUM_POINTS predefined: 21 BATCHSIZE predefined: 6 Epoch 1/2 array_pointcloud.shape in load_single_pointcloud function: (21, 3) array_pointcloud.shape in load_single_pointcloud function: (21, 3) array_pointcloud.shape in load_single_pointcloud function: (21, 3) array_pointcloud.shape in load_single_pointcloud function: (21, 3) array_pointcloud.shape in load_single_pointcloud function: (21, 3) array_pointcloud.shape in load_single_pointcloud function: (21, 3) 1/1 [==============================] - 13s 13s/step - loss: 0.6657 Epoch 2/2 array_pointcloud.shape in load_single_pointcloud function: (21, 3) array_pointcloud.shape in load_single_pointcloud function: (21, 3) array_pointcloud.shape in load_single_pointcloud function: (21, 3) array_pointcloud.shape in load_single_pointcloud function: (21, 3) array_pointcloud.shape in load_single_pointcloud function: (21, 3) array_pointcloud.shape in load_single_pointcloud function: (21, 3) 1/1 [==============================] - 0s 64ms/step - loss: 0.6431
Conclusion
So, how is it possible that a tf neural net can be successful trained, although the data shape and the input shape does not match when loading and feeding data while train? Is it because of the usage of tf.py_function
?
Thanks for help.