Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Open In Colab Open In Kaggle

The rain fell alike upon the just and upon the unjust, and for nothing was there a why and a wherefore. - William Somerset Maugham


Rain is oft described as having a gloomy beauty to it, and it continues to inspire authors and scientists alike. Can we use machine learning algorithms to predict rainfall runoff with comparable accuracies to other established methods?

Welcome to the practical application notebook for this week, where we’ll be using a Long-Short Term Memory (LSTM) network to try our hand at Rainfall-Runoff modeling! Rainfall-runoff models predict streamflow (runoff) from precipitation and other meteorological data, and are a core tool in hydrology for flood forecasting, water resource management, and understanding catchment behavior.

Q1) Go ahead and load the csv file with the data for our project today. The filepath is stored in the data_file variable that was defined in the cell above this one!

We now have our data in a nice format for us to use. You’ll see that our data has an index column, a T column, a P1 column, a P2 column, and a runoff column. The index column is the date of our measurements, the T column is the mean daily temperature at a station, the P1 and P2 columns are the daily precipitations at two stations, and the runoff column is the daily streamflow at one station.

Let’s go ahead and figure out how many years of data we have by looking at the time index.

Q2) Store the list of available years of data in a variable, and print out the length of the list.

Now we need to split the dataset into a training, validation, and test set to use with our algorithm. Since we’re dealing with time series data, let’s use the last 10 of the years available as the test dataset, the 10 years previous to that as the validation dataset, and the remainder of the data will be used to train our algorithm.

Q3) Split the dataset into a training, validation, and test set.

The data we’re using today has already been cleaned up - we know it doesn’t have any NaN values.

Still, it’s good practice to check! Do so in the cell below.

You should now have your dataset split up as three pandas dataframes. If you print out the first five rows of each dataset, you should get the following:

We now have our data for training, validating, and testing our algorithm! However, the values are still the original ones from our measurements. We saw before that we generally want to transform these into standardized values, otherwise we might end up with a problem of scales.

With that joke out of our system, let’s go ahead and prepare a Scikit-Learn pipeline to scale our data.

Q4) Scale the input data using a StandardScaler (i.e., using the mean and standard deviation).

If you did everything right, the first two rows of your dataset should return:

Training:
[[-0.67248997 -0.24609328 -0.32674546 3.91 ]
[-0.67248997 -0.07489897 0.08869363 4.01 ]]

Validation:
[[-1.28097285 -0.47435235 -0.47511657 3.22 ]
[-1.06462338 -0.10343136 -0.03000326 3.12 ]]

Test:
[[-1.02405785 0.38161917 0.6228296 3.6 ]
[-1.1051889 -0.44581996 -0.47511657 3.4 ]]

We’re almost ready to start training our algorithm! However, we currently have a list of temperature, precipitation, and runoff readings for each day in our datasets. However, we’re interested in looking at an nn-sized window of temperature and precipitation readings to predict a point in the runoff series!

As an example with n=5 n= 5:   \; to predict the runoff on a given Friday, we need the temperature and precipitation readings for Monday, Tuesday, Wednesday, Thursday, and the Friday itself.

Q5) Turn the training, validation, and test data into a set of series to feed into an LSTM

Let’s try printing out the shape of a transformed dataset...

You should find that the dataset has the wrong shape for our purposes! The sliding_window_view function will have returned the data in the shape:
(number of samples, number_of_features, sequence_length)

However, the convention we’ve followed so far in our course (which is quite common in the field) is features last:
(number of samples, sequence_length, number_of_features).

We can fix this with numpy.moveaxis()!

We now need to prepare the target data using the runoff column!
Here, we just need to select the column, skipping over the first (window_size−1window\_size - 1) elements.

Let’s check the shape of our arrays to make sure that things make sense!

During development, a window size of 365 days was used. With this window size, the shape of our input/output datasets are:
Train Shape: X:(6941, 365, 3), y:(6941,)
Validation Shape: X:(3289, 365, 3), y:(3289,)
Test Shape: X:(3288, 365, 3), y:(3288,)

We finally have a dataset that we can easily use to train an LSTM! From this point on, we’ll be relying on PyTorch to get our model ready.

Let’s start by importing the parts that we’ll need. We won’t hide any of the code here so you can see everything that’s being done without extra clicking. \

😀

First, let’s start by moving away from Pandas Dataframes and NumPy N-Dimensional Arrays and into the realm of PyTorch Tensors.

Q6) Convert the prepared datasets into PyTorch Datasets

Let’s check that your tensors and datasets have been converted properly. Run the code below and compare it to our results (remember, we used 365 days as the window_size and a batch size of 128 - if your hyperparameters are different your numbers will be different)

During development, the output of the cell above was:
X tensor size (train): torch.Size([6941, 365, 3])
X tensor size (val): torch.Size([6941, 365, 3])
X tensor size (test): torch.Size([6941, 365, 3]) \

Train dataset size =: 6941
Validation dataset size =: 3289
Test dataset size =: 3288 \

Now that we have our datasets as PyTorch tensors, we can go ahead and load them into a DataLoader\color{Green}{\textit{DataLoader}}.

In PyTorch, DataLoaders are an abstraction that let’s you do many powerful things to a dataset, such as: getting samples, shuffling, and other operations that are outside of the scope of this lab / course.

Before we make the DataLoaders, we need to define some hyperparameters for our training. Specifically, we want to define the batch size (i.e., how many sample you train on at a time). We also need to define a setting: the number of workers we’ll use.

We recommend you use a batch size of 128, and two workers. We won’t go into details on how large a batch size to use, especially as there is ongoing research on the topic (e.g., in these papers). However, too large a batch size tends to make it harder for ML algorithms to generalize. As for the number of workers, this is generally dictaded by how many cpu/gpu cores you have available.

Q7) Define the hyperparameters and settings for training

Q8) Define the training, validation, and test Dataloaders

We’re now fully ready on the data side - let’s go on to setting up the neural network.

To define a neural network in PyTorch, we extend the nn.Module class. You can read more about the class in the documentation online.

Whenever we want to design a model with an LSTM layer, we’ll need to define how many LSTM units we want to use (this will be the first hyperparameter for our simple LSTM model).

We’ll also be adding a dropout layer to our simple LSTM architecture. That is, there will be a fixed chance that the output of each LSTM unit will be zeroed during training - this will make our model more robust. Also, the probability of zeroing an output will be another hyperparameter.

Finally, the last state of the LSTM layer are going to be combined linearly into a prediction for the runoff at the end of the time series. The default notebook will use a simple linear combination (i.e., we won’t be using an activation function on the combination the way we often did before).

Q9) Define the LSTM model architecture

Now that we have the model defined, we’ll also be using a metric of performance that is somewhat more uncommon in machine learning applications than in hydrology - the Nash-Sutcliff-Efficiency (NSE) Coefficient. You can read more about it on Wikipedia.

The next thing we need to think about is the hyperparameters for our model and training. More specifically, we need to choose:

  • a number of LSTM units

  • the dropout rate

  • define our loss function

  • choose our optimizer and its parameters.

  • define the number of epochs we will train for

Thankfully, it’s not our first rodeo! \

🤠

Regarding the number of units, we chose 16 units during development of the notebook. Feel free to change this, e.g. to values between 1 and 128.

The dropout rate is the probability that an LSTM unit will be zeroed. We’ll set it to 0.125 (i.e., 1/8), which means we expect to drop the output from ~two of the LSTM cells at random. You can read more here.

Since this is a regression problem, we’ll rely on MSE as the loss function.

We’ve also know that Adam is a reliable optimizer, so we’ll go ahead and use that. Adam needs us to define a learning rate, and 1∗10−31*10^{-3} is a common default value. We’ll try it out to see if it’s appropriate.

Note that you’re free to play around with these hyperparameters - your performance will just be different from the ones we will show if you do so. I’m sure you can find a better solution 😀

Q10) Define the loss function, instantiate the model, define the optimizer, and set the number of epochs to iterate through

We’re almost ready to train our model. Before we move on to the training routine, let’s take a minute to define how we will evaluate the performance of the model - both for validation during training and for testing after training!

Q11) Define the model evaluation function

With PyTorch, we need to write out the training and evaluation routines ourselves. We’ll do this by using nested for loops - the outer loop will run for the number of epochs, while the inner loops will iterate over the batches to train and validate the model.

The outer loop will do the following:

  • Set the training loss to zero

  • Train the model

  • Get the validation loss and metrics using our evaluation function

  • Store the training and validation metrics

Q12) Write the training loop

If you did everything the exact same way we did during development of the notebook (i.e., you chose the same hyperparameters as us) you should∗^* get a set of training curves that look just like the ones below:

∗GPU calculations are often non-deterministic for performance reasons, so you should get something remarkably similar, though not quite the same_{^{*}\text{GPU calculations are often non-deterministic for performance reasons, so you should get something remarkably similar, though not quite the same}}

Finally, assuming your decisions and ours were the same, evaluating your model with the code above should∗^* give you the figure below:

∗Once again, GPU operations are generally non-deterministic - you should get something remarkably similar_{^{*}\text{Once again, GPU operations are generally non-deterministic - you should get something remarkably similar}}