TensorFlow LSTM timeseries class weight or sample weight

Viewed 231

I have been following this tutorial on using an LSTM model for timeseries classification: https://www.tensorflow.org/tutorials/structured_data/time_series

My dataset consists of multiple timeseries, which I partition/pad into equal window sizes and then make sure that each input "window" is treated separately.

I would like to weight the different windows, either by sample or by class, but so far have hit issues in both cases.

Here is my adapted code, attempting sample-based weighting:

class WindowGenerator():
    def __init__(self, input_width, label_width, shift,
               train_df, val_df, test_df, window_width,
               input_columns=None, label_columns=None, weight_column=['weight']):
        if window_width is not None:
            if (input_width + shift != window_width) :
                raise ValueError('Data partitioned into windows of length %d'%window_width)
        
        # Store the raw data.
        self.train_df = train_df
        self.val_df = val_df
        self.test_df = test_df

        # Work out the label column indices.
        self.column_indices = {name: i for i, name in
                               enumerate(train_df.columns)}

        self.input_columns = input_columns
        if input_columns is not None:
            self.input_columns_indices = {name: i for i, name in
                                        enumerate(input_columns)}
            
        self.weight_column = weight_column
        if weight_column is not None:
            self.weight_column_index = {name: i for i, name in
                                        enumerate(weight_column)}
        
        self.label_columns = label_columns
        if label_columns is not None:
            self.label_columns_indices = {name: i for i, name in
                                        enumerate(label_columns)}
            
            

        # Work out the window parameters.
        self.input_width = input_width
        self.label_width = label_width
        self.shift = shift

        self.total_window_size = input_width + shift

        self.input_slice = slice(0, input_width)
        self.input_indices = np.arange(self.total_window_size)[self.input_slice]
        
        self.label_start = self.total_window_size - self.label_width
        self.labels_slice = slice(self.label_start, None)
        self.label_indices = np.arange(self.total_window_size)[self.labels_slice]
        
        

    def __repr__(self):
        return '\n'.join([
            f'Total window size: {self.total_window_size}',
            f'Input indices: {self.input_indices}',
            f'Label indices: {self.label_indices}',
            f'Input column name(s): {self.input_columns}',
            f'Label column name(s): {self.label_columns}'])
    
    def split_window(self, features):
        inputs = features[:, self.input_slice, :]
        if self.input_columns is not None:
            inputs = tf.stack(
                [inputs[:, :, self.column_indices[name]] for name in self.input_columns],
                axis=-1)
            
        weights = features[:, self.input_slice, :]
        if self.weight_column is not None:
            weights = tf.stack(
                [weights[:, :, self.column_indices[name]] for name in self.weight_column],
                axis=-1)

            
        labels = features[:, self.labels_slice, :]
        if self.label_columns is not None:
            labels = tf.stack(
                [labels[:, :, self.column_indices[name]] for name in self.label_columns],
                axis=-1)

        # Slicing doesn't preserve static shape information, so set the shapes
        # manually. This way the `tf.data.Datasets` are easier to inspect.
        inputs.set_shape([None, self.input_width, None])
        weights.set_shape([None, self.input_width, None])
        labels.set_shape([None, self.label_width, None])

        return inputs, labels, weights

    def plot(self, model=None, plot_col='pressure', sample_freq=30, max_subplots=3):
        inputs, labels = self.example
        plt.figure(figsize=(12, 8))
        plot_col_index = self.column_indices[plot_col]
        max_n = min(max_subplots, len(inputs))
        for n in range(max_n):
            plt.subplot(max_n, 1, n+1)
            plt.ylabel(f'{plot_col} [normed]')
            plt.plot(self.input_indices/sample_freq, inputs[n, :, plot_col_index],
                     label='Inputs', marker='.', zorder=-10)

            if self.label_columns:
                label_col_index = self.label_columns_indices.get(plot_col, None)
            else:
                label_col_index = plot_col_index

            if label_col_index is None:
                continue

            plt.scatter(self.label_indices/sample_freq, labels[n, :, label_col_index],
                        edgecolors='k', label='Labels', c='#2ca02c', s=64)
            if model is not None:
                predictions = model(inputs)
                try:
                    plt.scatter(self.label_indices/sample_freq, predictions[n, :, label_col_index],
                          marker='X', edgecolors='k', label='Predictions',
                          c='#ff7f0e', s=64)
                except:
                    plt.scatter(self.label_indices/sample_freq, predictions[n, -1, label_col_index],
                          marker='X', edgecolors='k', label='Predictions',
                          c='#ff7f0e', s=64)

            if n == 0:
                plt.legend()

        plt.xlabel('time offset [s]')


    def make_dataset(self, data):
        data = np.array(data, dtype=np.float32)
        ds = tf.keras.preprocessing.timeseries_dataset_from_array(
          data=data,
          targets=None,
          sequence_length=self.total_window_size,
          sequence_stride=self.total_window_size,
          shuffle=False,
          batch_size=32,)

        ds = ds.map(self.split_window)

        return ds

    @property
    def train(self):
        return self.make_dataset(self.train_df)

    @property
    def val(self):
        return self.make_dataset(self.val_df)

    @property
    def test(self):
        return self.make_dataset(self.test_df)

    @property
    def example(self):
        """Get and cache an example batch of `inputs, labels` for plotting."""
        result = getattr(self, '_example', None)
        if result is None:
            # No example batch was found, so get one from the `.train` dataset
            result = next(iter(self.train))
            # And cache it for next time
            self._example = result
        return result

def compile_and_fit(model, window, patience=2, max_epochs = 20):
    early_stopping = tf.keras.callbacks.EarlyStopping(monitor='val_loss',
                                                    patience=patience,
                                                    mode='min')

    model.compile(loss="categorical_crossentropy", optimizer= "adam", 
                  metrics=['categorical_accuracy', 'categorical_crossentropy'])

    history = model.fit(window.train, epochs=max_epochs,
                      validation_data=window.val,
                      callbacks=[early_stopping]
                       )
    return history

Here is my example call:

   df_columns = input_columns+label_columns
        wide_window =  WindowGenerator(input_width=(window_width-1), 
                                       label_width=(window_width-1),shift=1,
                                        train_df=train_df[df_columns+['weight']], 
                                       val_df=val_df[df_columns], 
                                        test_df=test_df[df_columns], 
                                        input_columns=input_columns, 
                                       label_columns=label_columns, 
                                       weight_column = ['weight'],
                                        window_width=window_width)


        print(wide_window)

        for example_inputs, example_labels, example_weights in wide_window.train.take(1):
            print(f'Inputs shape (batch, time, features): {example_inputs.shape}')
            print(f'Labels shape (batch, time, features): {example_labels.shape}')
            print(f'Weights shape (batch, time, features): {example_weights.shape}')
            
        lstm = tf.keras.models.Sequential([
            # Shape [batch, time, features] => [batch, time, lstm_units]
            tf.keras.layers.LSTM(32, return_sequences=True),
            # Shape => [batch, time, features]
            tf.keras.layers.Dense(units=len(label_columns), activation='softmax')
        ])

And the output/error:

Total window size: 120
Input indices: [  0   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17
  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35
  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53
  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71
  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89
  90  91  92  93  94  95  96  97  98  99 100 101 102 103 104 105 106 107
 108 109 110 111 112 113 114 115 116 117 118]
Label indices: [  1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18
  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36
  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53  54
  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72
  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90
  91  92  93  94  95  96  97  98  99 100 101 102 103 104 105 106 107 108
 109 110 111 112 113 114 115 116 117 118 119]
Input column name(s): ['f0', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6']
Label column name(s): ['c0', 'c1', 'c2', 'c3']
Inputs shape (batch, time, features): (32, 119, 7)
Labels shape (batch, time, features): (32, 119, 4)
Weights shape (batch, time, features): (32, 119, 1)

Wide window
Input shape: (32, 119, 7)
Labels shape: (32, 119, 4)
Weights shape: (32, 119, 1)
Output shape: (32, 119, 4)

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/var/folders/81/gpwcygkx6v39bl3d3p38n0n00000gn/T/ipykernel_8173/3400155790.py in <module>
     48                 continue
     49 
---> 50             history = compile_and_fit(lstm, wide_window, max_epochs = num_epochs)
     51 
     52             stacked_predictions = lstm.predict(wide_window.train)

/var/folders/81/gpwcygkx6v39bl3d3p38n0n00000gn/T/ipykernel_8173/2330381046.py in compile_and_fit(model, window, patience, max_epochs)
      8 
      9     history = model.fit(window.train, epochs=max_epochs,
---> 10                       validation_data=window.val,
     11                       callbacks=[early_stopping]
     12                        )

/var/folders/81/gpwcygkx6v39bl3d3p38n0n00000gn/T/ipykernel_8173/2013461020.py in val(self)
    143     @property
    144     def val(self):
--> 145         return self.make_dataset(self.val_df)
    146 
    147     @property

/var/folders/81/gpwcygkx6v39bl3d3p38n0n00000gn/T/ipykernel_8173/2013461020.py in make_dataset(self, data)
    133           batch_size=32,)
    134 
--> 135         ds = ds.map(self.split_window)
    136 
    137         return ds

~/.pyenv/versions/3.8.12/lib/python3.8/site-packages/tensorflow/python/data/ops/dataset_ops.py in map(self, map_func, num_parallel_calls, deterministic, name)
   2002         warnings.warn("The `deterministic` argument has no effect unless the "
   2003                       "`num_parallel_calls` argument is specified.")
-> 2004       return MapDataset(self, map_func, preserve_cardinality=True, name=name)
   2005     else:
   2006       return ParallelMapDataset(

~/.pyenv/versions/3.8.12/lib/python3.8/site-packages/tensorflow/python/data/ops/dataset_ops.py in __init__(self, input_dataset, map_func, use_inter_op_parallelism, preserve_cardinality, use_legacy_function, name)
   5453     self._use_inter_op_parallelism = use_inter_op_parallelism
   5454     self._preserve_cardinality = preserve_cardinality
-> 5455     self._map_func = StructuredFunctionWrapper(
   5456         map_func,
   5457         self._transformation_name(),

~/.pyenv/versions/3.8.12/lib/python3.8/site-packages/tensorflow/python/data/ops/dataset_ops.py in __init__(self, func, transformation_name, dataset, input_classes, input_shapes, input_types, input_structure, add_to_graph, use_legacy_function, defun_kwargs)
   4531         fn_factory = trace_tf_function(defun_kwargs)
   4532 
-> 4533     self._function = fn_factory()
   4534     # There is no graph to add in eager mode.
   4535     add_to_graph &= not context.executing_eagerly()

~/.pyenv/versions/3.8.12/lib/python3.8/site-packages/tensorflow/python/eager/function.py in get_concrete_function(self, *args, **kwargs)
   3242          or `tf.Tensor` or `tf.TensorSpec`.
   3243     """
-> 3244     graph_function = self._get_concrete_function_garbage_collected(
   3245         *args, **kwargs)
   3246     graph_function._garbage_collector.release()  # pylint: disable=protected-access

~/.pyenv/versions/3.8.12/lib/python3.8/site-packages/tensorflow/python/eager/function.py in _get_concrete_function_garbage_collected(self, *args, **kwargs)
   3208       args, kwargs = None, None
   3209     with self._lock:
-> 3210       graph_function, _ = self._maybe_define_function(args, kwargs)
   3211       seen_names = set()
   3212       captured = object_identity.ObjectIdentitySet(

~/.pyenv/versions/3.8.12/lib/python3.8/site-packages/tensorflow/python/eager/function.py in _maybe_define_function(self, args, kwargs)
   3555 
   3556           self._function_cache.missed.add(call_context_key)
-> 3557           graph_function = self._create_graph_function(args, kwargs)
   3558           self._function_cache.primary[cache_key] = graph_function
   3559 

~/.pyenv/versions/3.8.12/lib/python3.8/site-packages/tensorflow/python/eager/function.py in _create_graph_function(self, args, kwargs, override_flat_arg_shapes)
   3390     arg_names = base_arg_names + missing_arg_names
   3391     graph_function = ConcreteFunction(
-> 3392         func_graph_module.func_graph_from_py_func(
   3393             self._name,
   3394             self._python_function,

~/.pyenv/versions/3.8.12/lib/python3.8/site-packages/tensorflow/python/framework/func_graph.py in func_graph_from_py_func(name, python_func, args, kwargs, signature, func_graph, autograph, autograph_options, add_control_dependencies, arg_names, op_return_value, collections, capture_by_value, override_flat_arg_shapes, acd_record_initial_resource_uses)
   1141         _, original_func = tf_decorator.unwrap(python_func)
   1142 
-> 1143       func_outputs = python_func(*func_args, **func_kwargs)
   1144 
   1145       # invariant: `func_outputs` contains only Tensors, CompositeTensors,

~/.pyenv/versions/3.8.12/lib/python3.8/site-packages/tensorflow/python/data/ops/dataset_ops.py in wrapped_fn(*args)
   4508           attributes=defun_kwargs)
   4509       def wrapped_fn(*args):  # pylint: disable=missing-docstring
-> 4510         ret = wrapper_helper(*args)
   4511         ret = structure.to_tensor_list(self._output_structure, ret)
   4512         return [ops.convert_to_tensor(t) for t in ret]

~/.pyenv/versions/3.8.12/lib/python3.8/site-packages/tensorflow/python/data/ops/dataset_ops.py in wrapper_helper(*args)
   4438       if not _should_unpack(nested_args):
   4439         nested_args = (nested_args,)
-> 4440       ret = autograph.tf_convert(self._func, ag_ctx)(*nested_args)
   4441       if _should_pack(ret):
   4442         ret = tuple(ret)

~/.pyenv/versions/3.8.12/lib/python3.8/site-packages/tensorflow/python/autograph/impl/api.py in wrapper(*args, **kwargs)
    697       except Exception as e:  # pylint:disable=broad-except
    698         if hasattr(e, 'ag_error_metadata'):
--> 699           raise e.ag_error_metadata.to_exception(e)
    700         else:
    701           raise

ValueError: in user code:

    File "/var/folders/81/gpwcygkx6v39bl3d3p38n0n00000gn/T/ipykernel_8173/1692845515.py", line 68, in split_window  *
        weights = tf.stack(

    ValueError: slice index 11 of dimension 2 out of bounds. for '{{node strided_slice_9}} = StridedSlice[Index=DT_INT32, T=DT_FLOAT, begin_mask=3, ellipsis_mask=0, end_mask=3, new_axis_mask=0, shrink_axis_mask=4](strided_slice_8, strided_slice_9/stack, strided_slice_9/stack_1, strided_slice_9/stack_2)' with input shapes: [?,?,11], [3], [3], [3] and with computed input tensors: input[1] = <0 0 11>, input[2] = <0 0 12>, input[3] = <1 1 1>.

And when I attempted feeding just a dictionary of weights into the model.fit() call (so only using ds = ds.map((inputs, labels)), etc.) I received the error

ValueError: `class_weight` not supported for 3+ dimensional targets.

Does anyone know how to work around either of these issues?

Thank you!

0 Answers
Related