Time series forecasting/anomaly detection on Nengo

I’ve started new research with SNNs and this time I’m doing anomaly detection on time series.

I spent some time in this forum looking for threads that may help me, but until now I didn’t find anything that answers my questions. I have found various posts that refer to using the LMU, but as I understand it is not a SNN on its own (correct me if I’m wrong). Anyways I want to start with a less complex more vanilla approach. Is it possible? Has anyone done it before?

My biggest struggle is in how to input the data. I’m currently using NAB’s (Numenta Anomaly Benchmark) ‘Tweet_Volume_AMZN’. In TensorFlow I’m trying to forecast the time series. It has only one feature and in TF I use a window of 50-time-steps and a LSTM network. So, my input layer in TF has shape (50, 1). Trying to Nengo_DL.Convert it didn’t work. When trying to fit after the conversion I get an error about the rank of X. X has shape (instances, window_size, n_features) which is (15781, 50, 1). I don’t see the problem here. A snippet of my code is below.

input_layer = tf.keras.layers.Input(shape=(50, 1))
lstm_layer_1 = tf.keras.layers.LSTM(units=50, return_sequences=True, activation=tf.nn.relu)(input_layer)
lstm_layer_2 = tf.keras.layers.LSTM(units=50, return_sequences=False, activation=tf.nn.relu)(lstm_layer_1)
dense_layer_1 = tf.keras.layers.Dense(units=1)(lstm_layer_2)

model = tf.keras.models.Model(inputs=input_layer, outputs=dense_layer_1)

converter = nengo_dl.Converter(
    model, 
    swap_activations={tf.nn.relu: nengo.RectifiedLinear()},
    )

with nengo_dl.Simulator(converter.net, seed=42, minibatch_size=100) as sim:
    sim.compile(
        'adam',
        loss='mse'
    )
    sim.fit(X, y, epochs=10)

My biggest issue currently is on how to input the time varying data. I thank any help in advance.

Hi @Kinteshi,

Can you provide the actual error message you are getting? It will be easier to debug your issue with the description of the error message. Additionally, can you provide the code on how you formatted the input data (i.e., X)? If you look at the various NengoDL examples, you’ll see that the data is padded with an extra temporal dimension, e.g.,:

# flatten images and add time dimension
train_images = train_images.reshape((train_images.shape[0], 1, -1))
train_labels = train_labels.reshape((train_labels.shape[0], 1, -1))

You’ll need to do the same with your data.

As for your other questions:

The LMU examples on the various Nengo documentation pages are not fully spiking. But, it shouldn’t be too much effort to convert those networks to a spiking version.

Are you asking about using “vanilla” approaches to do anomaly detection? If so, I’m pretty sure that has been done before, and you can probably find some examples online. If they have been implemented in TF, it shouldn’t be too difficult converting them to NengoDL. I’d like to point out that we do have an implementation that does spiking neural networks within Keras (TF) itself, without the need to run NengoDL. If you are familiar with TF, that approach may be quicker / easier to learn. You can find it here.

ValidationError                           Traceback (most recent call last)
c:\Users\jefma\OneDrive\Documentos\GitHub\SNN-AD\scripts\realTweets\RSNN.ipynb Cell 18' in <cell line: 2>()
      2 with nengo_dl.Simulator(converter.net, seed=42, minibatch_size=100) as sim:
      3     sim.compile(
      4         'adam',
      5         loss='mse'
      6     )
----> 7     sim.fit(X, y, epochs=10)

File ~\miniconda3\envs\snn\lib\site-packages\nengo\utils\magic.py:179, in BoundFunctionWrapper.__call__(self, *args, **kwargs)
    177         return self.wrapper(wrapped, instance, args, kwargs)
    178     else:
--> 179         return self.wrapper(self.__wrapped__, self.instance, args, kwargs)
    180 else:
    181     instance = getattr(self.__wrapped__, "__self__", None)

File ~\miniconda3\envs\snn\lib\site-packages\nengo_dl\simulator.py:67, in require_open(wrapped, instance, args, kwargs)
     62 if instance.closed:
     63     raise SimulatorClosed(
     64         f"Cannot call {wrapped.__name__} after simulator is closed"
     65     )
---> 67 return wrapped(*args, **kwargs)

File ~\miniconda3\envs\snn\lib\site-packages\nengo_dl\simulator.py:867, in Simulator.fit(self, x, y, n_steps, stateful, **kwargs)
    864     else:
    865         kwargs["validation_data"] = (x_val, y_val, validation_data[2])
--> 867 return self._call_keras(
    868     "fit", x=x, y=y, n_steps=n_steps, stateful=stateful, **kwargs
    869 )

File ~\miniconda3\envs\snn\lib\site-packages\nengo\utils\magic.py:179, in BoundFunctionWrapper.__call__(self, *args, **kwargs)
    177         return self.wrapper(wrapped, instance, args, kwargs)
    178     else:
--> 179         return self.wrapper(self.__wrapped__, self.instance, args, kwargs)
    180 else:
    181     instance = getattr(self.__wrapped__, "__self__", None)

File ~\miniconda3\envs\snn\lib\site-packages\nengo_dl\simulator.py:50, in with_self(wrapped, instance, args, kwargs)
     48 try:
     49     with tf.device(instance.tensor_graph.device):
---> 50         output = wrapped(*args, **kwargs)
     51 finally:
     52     tf.keras.backend.set_floatx(keras_dtype)

File ~\miniconda3\envs\snn\lib\site-packages\nengo_dl\simulator.py:981, in Simulator._call_keras(self, func_type, x, y, n_steps, stateful, **kwargs)
    978     y = self._standardize_data(y, self.model.probes)
    979     # we set n_steps=None because targets do not necessarily need to have
    980     # the same number of timesteps as input (depending on the loss function)
--> 981     self._check_data(y, n_steps=None, batch_size=input_batch, nodes=False)
    983 if kwargs.get("validation_split", 0) != 0 and input_batch is not None:
    984     # validation_split is only a kwarg in `fit`, but we do it here because
    985     # we need to know `input_batch`.
    986     # split math set up to match
    987     # `keras.engine.training_utils.split_training_and_validation_data`.
    988     split = int(input_batch * (1 - kwargs["validation_split"]))

File ~\miniconda3\envs\snn\lib\site-packages\nengo_dl\simulator.py:1958, in Simulator._check_data(self, data, batch_size, n_steps, nodes)
   1956 # generic shape checks
   1957 if len(x.shape) != 3:
-> 1958     raise ValidationError(
   1959         f"should have rank 3 (batch_size, n_steps, dimensions), found rank "
   1960         f"{len(x.shape)}",
   1961         f"{name} data",
   1962     )
   1963 if x.shape[0] < self.minibatch_size:
   1964     raise ValidationError(
   1965         f"Batch size of data ({x.shape[0]}) less than Simulator "
   1966         f"`minibatch_size` ({self.minibatch_size})",
   1967         f"{name} data",
   1968     )

ValidationError: probe data: should have rank 3 (batch_size, n_steps, dimensions), found rank 2

This is the full error message. Got all of it just to be sure.

Yep:

def create_windows(df, window_size, step_size):
    X = []
    y = []
    for i in range(0, len(df) - window_size, step_size):
        X.append(df.iloc[i:i + window_size])
        y.append(df.iloc[i + window_size])
    return np.array(X), np.array(y)


X, y = create_windows(df, window_size=50, step_size=1)

In this piece of my code, ‘df’ is a DataFrame with one column ‘value’ and timestamps for indexes. The shape is (total_samples, 1). After I run this, I have X.shape = (total_samples-window_size, window_size, 1) and y.shape = (total_samples-window_size, 1).

I want to convert them to NengoDL. If I use just a dense network, I won’t have much of a problem if I just swap the last two axes of X, but in this code, I’m running a recurrent network and getting that error message up there.

So, the error message you get pretty much describes the issue you are facing:

ValidationError: probe data: should have rank 3 (batch_size, n_steps, dimensions), found rank 2

The NengoDL compiler is informing you that the data you have provided is not in the format that is expected. It expects the data to have a shape batch_size, n_steps, dimensions. Each of these parameters are described in the NengoDL documentation for the sim.fit function.

batch_size is the number of trials you have in one batch, and you have this correct in your data (i.e., total_samples - window_size).

n_steps is the number of simulation timesteps you want to run for each trial in the batch. This number is up to you to decide. It is typically set to 1 for converted TF models since the data is propagated through the model in one timestep. However, if you have things like delays or synapses in your model, then you’ll probably want to increase this number to ensure that a sufficient amount of timesteps have elapsed to allow information to propagate throughout the entire model.

dimensions is the dimensionality of your data. In the case of your data, this would be the shape of the input being presented to your model, and that’s window_size. So, the shape of your data should be (total_samples - window_size, 1, window_size).

You can apply the same analysis to your labels. When you do so, you should come to the conclusion that your labels should have a shape (total_samples - window_size, 1, 1). Since you have 1 unit in your output layer, the dimensionality of your output should be 1.

The error message you are seeing is due to the mismatch in the expected vs actual shape of the labels. NengoDL expected a matrix of rank 3, whereas you only provided a matrix of rank 2 (it’s missing the n_steps entry). If you were to correct this, and run the code again, you’ll encounter another error:

ValueError: Dimension 2 in both shapes must be equal, but are ...

This error would be due to the misplacement of the dimensions entry. NengoDL expects it as the 3rd entry of the data shape, but you have it as the second.

Fixing both issues with the data shape should enable you to run your model without errors.