DL: Fix num_class parsing from model architecture

JIRA: MADLIB-1472
get_num_classes function did not work in certain models that end
with activation layers. The regresion was caused by the changes
from the multi-io commit.
diff --git a/src/ports/postgres/modules/deep_learning/madlib_keras.sql_in b/src/ports/postgres/modules/deep_learning/madlib_keras.sql_in
index 429c0f0..05edc0e 100644
--- a/src/ports/postgres/modules/deep_learning/madlib_keras.sql_in
+++ b/src/ports/postgres/modules/deep_learning/madlib_keras.sql_in
@@ -84,7 +84,7 @@
 kernel version (1.14). Using a newer or older version may or may not work as intended.
 
 MADlib's deep learning methods are designed to use the TensorFlow package and its built in Keras
-functions.  To ensure consistency, please use tensorflow.keras objects (models, layers, etc.) 
+functions.  To ensure consistency, please use tensorflow.keras objects (models, layers, etc.)
 instead of importing Keras and using its objects.
 
 @note CUDA GPU memory cannot be released until the process holding it is terminated.
@@ -165,15 +165,15 @@
     @note
     - Custom loss functions and custom metrics can be used as defined in
     <a href="group__grp__custom__function.html">Define Custom Functions.</a>
-    List the custom function name and provide the name of the table where the 
+    List the custom function name and provide the name of the table where the
     serialized Python objects reside using the parameter 'object_table' below.
     - The following loss function is
     not supported: <em>sparse_categorical_crossentropy</em>.
     The following metrics are not
     supported: <em>sparse_categorical_accuracy, sparse_top_k_categorical_accuracy</em>.
-    - The Keras accuracy parameter <em>top_k_categorical_accuracy</em> returns top 5 accuracy by 
+    - The Keras accuracy parameter <em>top_k_categorical_accuracy</em> returns top 5 accuracy by
     default.  If you want a different top k value, use the helper function
-    <a href="group__grp__custom__function.html#top_k_function">Top k Accuracy Function</a> 
+    <a href="group__grp__custom__function.html#top_k_function">Top k Accuracy Function</a>
     to create a custom
     Python function to compute the top k accuracy that you want.
 
@@ -609,10 +609,10 @@
   <DD>TEXT. Column with independent variables in the test table.
   If a 'normalizing_const' is specified when preprocessing the
   training dataset, this same normalization will be applied to
-  the independent variables used in predict. In the case that there 
-  are multiple independent variables, 
+  the independent variables used in predict. In the case that there
+  are multiple independent variables,
   representing a multi-input neural network,
-  put the columns as a comma 
+  put the columns as a comma
   separated list, e.g., 'indep_var1, indep_var2, indep_var3' in the same
   way as was done in the preprocessor step for the training data.
   </DD>
@@ -695,7 +695,8 @@
     pred_type,
     use_gpus,
     class_values,
-    normalizing_const
+    normalizing_const,
+    dependent_count
     )
 </pre>
 
@@ -805,6 +806,11 @@
   array by. For example, you would use 255 for this value if the image data is
   in the form 0-255.
   </DD>
+
+  <DT>dependent_count (optional)</DT>
+  <DD>INTEGER, default: 1.
+  The number of dependent variables in the model.
+  </DD>
 </DL>
 
 
diff --git a/src/ports/postgres/modules/deep_learning/model_arch_info.py_in b/src/ports/postgres/modules/deep_learning/model_arch_info.py_in
index 9c28c43..0081e58 100644
--- a/src/ports/postgres/modules/deep_learning/model_arch_info.py_in
+++ b/src/ports/postgres/modules/deep_learning/model_arch_info.py_in
@@ -66,12 +66,15 @@
     arch_layers = _get_layers(model_arch)
     num_classes = []
 
-    layer_count = len(arch_layers) - 1
-    for i in range(multi_dep_count):
-        if 'units' in arch_layers[layer_count-i]['config']:
-            num_classes.append(arch_layers[layer_count-i]['config']['units'])
-
+    i = len(arch_layers) - 1
+    dep_counter = 0
+    while i >= 0 and dep_counter < multi_dep_count:
+        if 'units' in arch_layers[i]['config']:
+            num_classes.append(arch_layers[i]['config']['units'])
+            dep_counter +=1
+        i -= 1
     if num_classes:
+        num_classes.reverse()
         return num_classes
     plpy.error('Unable to get number of classes from model architecture.')
 
diff --git a/src/ports/postgres/modules/deep_learning/test/unit_tests/test_madlib_keras.py_in b/src/ports/postgres/modules/deep_learning/test/unit_tests/test_madlib_keras.py_in
index 0c91eb4..e46efd7 100644
--- a/src/ports/postgres/modules/deep_learning/test/unit_tests/test_madlib_keras.py_in
+++ b/src/ports/postgres/modules/deep_learning/test/unit_tests/test_madlib_keras.py_in
@@ -1410,7 +1410,7 @@
         self.model.add(Dense(1599))
         with self.assertRaises(plpy.PLPYException) as error:
             self.subject.validate_class_values(
-                self.module_name, [range(1599), range(1598)], 'prob', self.model.to_json())
+                self.module_name, [range(1599)], 'prob', self.model.to_json())
         self.assertIn('1600', str(error.exception))
 
     def test_validate_class_values_valid_class_values_prob(self):
@@ -1487,6 +1487,70 @@
             obj = self.subject._validate_gpu_config(self.module_name, 'foo', [1,0,0,1])
         self.assertIn('does not have gpu', str(error.exception).lower())
 
+    def test_validate_class_values_last_layer_not_dense(self):
+        num_classes = 3
+        model = Sequential()
+        model.add(Conv2D(2, kernel_size=(1, 1), activation='relu',
+                         input_shape=(1,1,1,), padding='same'))
+        model.add(Dense(num_classes))
+        model.add(Activation('relu'))
+        model.add(Activation('softmax'))
+
+        self.subject.validate_class_values(
+            self.module_name, [range(num_classes)], 'prob', model.to_json())
+
+    def test_validate_class_values_last_layer_not_dense_multiio(self):
+        num_classes = 3
+        model = Sequential()
+        model.add(Conv2D(2, kernel_size=(1, 1), activation='relu',
+                         input_shape=(1,1,1,), padding='same'))
+        model.add(Dense(num_classes))
+        model.add(Dense(num_classes))
+        model.add(Activation('relu'))
+        model.add(Activation('softmax'))
+
+        self.subject.validate_class_values(
+            self.module_name, [range(num_classes), range(num_classes)], 'prob', model.to_json())
+
+    def test_validate_class_values_mismatch(self):
+        expected_error_regex = ".*do not match.*architecture"
+        num_classes = 3
+
+        # only one dense layer but len(dep_var) = 2
+        model = Sequential()
+        model.add(Conv2D(2, kernel_size=(1, 1), activation='relu',
+                         input_shape=(1,1,1,), padding='same'))
+        model.add(Dense(num_classes))
+        model.add(Activation('relu'))
+        with self.assertRaisesRegexp(plpy.PLPYException, expected_error_regex):
+            self.subject.validate_class_values(
+                self.module_name, [range(num_classes), range(num_classes)], 'prob', model.to_json())
+
+        # two dense layers
+        model = Sequential()
+        model.add(Conv2D(2, kernel_size=(1, 1), activation='relu',
+                         input_shape=(1,1,1,), padding='same'))
+        model.add(Dense(2))
+        model.add(Dense(num_classes))
+        with self.assertRaisesRegexp(plpy.PLPYException, expected_error_regex):
+            self.subject.validate_class_values(
+                self.module_name, [range(num_classes), range(num_classes)], 'prob', model.to_json())
+
+    def test_validate_class_values_no_units(self):
+        expected_error_regex = ".*Unable.*classes.*architecture"
+        num_classes = 3
+        #model arch is missing a dense layer
+        model = Sequential()
+        model.add(Activation('relu'))
+        model.add(Conv2D(2, kernel_size=(1, 1), activation='relu',
+                         input_shape=(1,1,1,), padding='same'))
+        with self.assertRaisesRegexp(plpy.PLPYException, expected_error_regex):
+            self.subject.validate_class_values(
+                self.module_name, [range(num_classes)], 'prob', model.to_json())
+        with self.assertRaisesRegexp(plpy.PLPYException, expected_error_regex):
+            self.subject.validate_class_values(
+                self.module_name, [range(num_classes), range(num_classes)], 'prob', model.to_json())
+
 
 class MadlibSerializerTestCase(unittest.TestCase):
     def setUp(self):