Skip to main content

A Deep Dive Into Time Masking Using PyTorch

This article delves into time masking techniques in deep learning using PyTorch, exploring strategies, their effects on models, and monitoring with W&B.
Created on October 16|Last edited on October 23
​​Time masking (or time-frequency masking), a pivotal technique in the realm of deep learning, has revolutionized the way we handle sequential data, particularly in the domains of audio processing and natural language processing. The ability to intentionally obfuscate portions of data to enhance model robustness and generalization is of paramount importance, and PyTorch provides an excellent platform to implement and experiment with such techniques. Coupled with advanced experiment tracking tools like Weights & Biases (W&B), researchers and practitioners can effectively monitor and refine their models.
This article delves deep into the intricacies of time masking with PyTorch, covering various strategies, their impact on model performance, and their myriad applications in contemporary deep learning challenges.

Table of Contents



Understanding Time Masking Techniques

What is Time Masking?

Time masking is a data augmentation technique primarily used in processing sequential data, such as audio or time series. It involves selectively obscuring or "masking" portions of the time-axis data, ensuring that a model doesn't over-rely on specific time segments and learns more generalized features.
Source

An Example Of Masking

Take BERT as an example. BERT is designed to pre-train deep bidirectional representations from the unlabeled text by jointly conditioning on both the left and right context in all layers. This means it predicts each word in a sentence based on the words before and after it. To achieve this, during its pre-training phase, BERT uses a technique called "masked language modeling."
In traditional language modeling, a model might predict the next word in a sequence (like GPT models). BERT, however, uses the MLM objective where it randomly masks (hides) some percentage of the input tokens and then tries to predict those masked tokens based on their context. This is done to train a deep bidirectional model.
How It Works:
Input Preparation:
  • Take a sentence: "The cat sat on the mat."
  • Randomly mask a word, for instance: "The cat sat on the [MASK]."
BERT's Task:
  • Given the masked sentence, BERT tries to predict the original word in place of [MASK], which in this case is "mat".
Training:
  • This masking is done for a certain percentage of words in each sentence in the training dataset. BERT learns to understand the context from both the left and right sides of a masked word, and over time, it gets good at predicting the masked words.
In essence, "masking" in BERT is a technique to "hide" some words in a sentence and then ask the model to predict them, leveraging the surrounding context. This helps BERT learn a deeply bidirectional representation of text.

Overview of Common Time Masking Methods

Random Masking

As the name implies, in the case of Random Masking, random segments of the time sequence are masked (set to zero or replaced by a certain value). Such a technique is commonly used in audio data augmentation, particularly in training deep learning models for tasks like Automatic Speech Recognition (ASR). This method introduces variability and randomness, which can prevent overfitting, but on the other hand, it can sometimes be too aggressive and remove critical parts of the sequence

Window-based Masking

In the case of Window-based Masking, a window (contiguous segment) of a fixed size is chosen, and all the values within this window are masked. Such a time-masking technique can be used in tasks where local temporal structures are crucial and where it might be beneficial for the model to ignore certain consistent chunks of data. This method offers high consistency in the amount of data being masked, however, it provides less randomness when compared to pure random masking, which might not introduce as much variability.

Frequency-Aware Masking

Though it might not be considered a direct time-masking method, Frequency-aware masking is specialized to audio data. In the context of audio, this method involves masking certain frequency channels in a spectrogram.
It is commonly used for Augmentation for audio classification or ASR tasks. Often combined with time masking.
Source
Frequency-aware masking encourages models to be less reliant on specific frequency bands which is of absolute necessity when training on real-life audio sounds, however, it can lead to loss of crucial frequency information in some cases.

Comparison of Time Masking Techniques and Their Impact on Model Performance

Each time masking method has a different impact on the performance of the model we are training.
For example, in the case of Random Masking, the main impact of such a time masking technique is to typically improve model generalization by preventing overfitting to specific segments of data. However, excessive random masking can sometimes hinder the model from capturing important temporal patterns. Thus, this method is best used for datasets with a lot of variability where no single segment is crucial for understanding the overall sequence.
Another example is Window-based Masking, which assists the model in focusing on larger temporal contexts and can improve performance by forcing the model to make predictions without always relying on localized features. This method is generally best used for tasks where the data has strong local temporal structures and the model should learn to generalize beyond these.
Last but not least is Frequency-aware Masking, which is more commonly used for audio data. This time masking method promotes frequency robustness. The model learns to not rely overly on specific frequency bands, which can be especially beneficial if the testing data has different frequency characteristics than the training data. Such a method is best used for audio classification or ASR tasks where the input might come from varied.

Time Masking in PyTorch

PyTorch, at its core, is a tensor library with GPU acceleration, making it highly conducive for deep learning operations. However, when it comes to audio-specific operations like time masking, you often have to either create custom functions or use specialized libraries built on top of PyTorch.
PyTorch offers a rich set of modules and functionalities tailored for deep learning and tensor operations. Among the most pivotal is the torch.nn, which provides an extensive collection of pre-defined layers, loss functions, and optimization techniques crucial for constructing neural network architectures. Another indispensable component is torch.utils.data.Dataset, which streamlines the data loading and preprocessing pipeline, making it seamless to work with large and diverse datasets.

Applying Time Masking Using Pytorch

The main reason that we chose to use PyTorch for this article, is because PyTorch provides a high degree of customization and flexibility when it comes to operations like time masking due to its dynamic computation graph and powerful tensor operations.

Weights & Biases for Time Masking

Weights & Biases (W&B) is an advanced platform tailored for machine learning practitioners, serving as an essential tool for experiment tracking and model management. It efficiently logs intricate details of machine learning experiments, including the hyperparameters utilized, architectural nuances of models, and critical metrics such as accuracy or loss.
Why this is relevant here is that these logs play a pivotal role in comparing distinct model variations, subsequently assisting researchers and developers in identifying optimal configurations. The platform also boasts a comprehensive visualization, offering graphical representations of metrics over epochs and facilitating model performance analyses. Integration capabilities are another strength of W&B, as it seamlessly aligns with popular machine learning frameworks, including TensorFlow and PyTorch.

Practical Application of Integrating W&B With Pytorch for Time Masking Experiments

Below we have provided a simple Time masking example using Pytorch and Weights & Biases. The goal is to provide the reader with an even better explanation of time masking using PyTorch and the integration of such an experiment with Weights & Biases’ tracking tool.

Step 1: Import Necessary Libraries

import torch
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as data
import wandb
import numpy as np
import torchvision.transforms as transforms
import matplotlib.pyplot as plt


Step 2: Initialize Weights & Biases

wandb.init(project="time_masking_experiments", name="random_masking")

Step 3: Neural Network Layer Definition

class SimpleCNN(nn.Module):
def __init__(self, num_classes=5):
super(SimpleCNN, self).__init__()
# Convert the 2D spectrogram data to a 3D tensor: [batch_size, 1, freq_bins, time_frames]
self.unsqueeze = lambda x: x.unsqueeze(1)
self.conv1 = nn.Conv2d(1, 16, 3, padding=1)
self.conv2 = nn.Conv2d(16, 32, 3, padding=1)
self.relu = nn.ReLU()
self.maxpool = nn.MaxPool2d(2)
self.flatten = nn.Flatten()
# Calculate the output shape of the conv layers to adjust the FC layers
self.sample_shape_after_convs = self._get_shape_after_convs(torch.zeros((1, 65, 77)).float())

self.fc1 = nn.Linear(self.sample_shape_after_convs, 128)
self.fc2 = nn.Linear(128, num_classes)

def _get_shape_after_convs(self, x):
x = self.unsqueeze(x)
x = self.conv1(x)
x = self.maxpool(x)
x = self.conv2(x)
x = self.maxpool(x)
return np.prod(x.shape[1:]) # Multiply all dimensions except batch_size

def forward(self, x):
x = self.unsqueeze(x) # Add channel dimension
x = self.relu(self.conv1(x))
x = self.maxpool(x)
x = self.relu(self.conv2(x))
x = self.maxpool(x)
x = self.flatten(x)
x = self.relu(self.fc1(x))
x = self.fc2(x)
return x

Step 4: Defining Our Time Masking Function

Random Time Masking

The random_masking function applies a random mask along the time dimension of a provided spectrogram. It first clones the original spectrogram to ensure no in-place modifications. Then, it randomly determines the width and starting point of the time mask. The function then sets the values in this range to zero, effectively "masking" them out. Finally, it returns the masked spectrogram.
def random_masking(spec, T=40):
# Clone the spectrogram to avoid in-place modifications
masked_spectrogram = spec.clone()

# Get the length of the time dimension
time_length = masked_spectrogram.shape[2]

# Determine the width of the mask
mask_width = torch.randint(0, T + 1, (1,)).item()

# Determine the starting point of the mask
mask_start = torch.randint(0, time_length - mask_width + 1, (1,)).item()

# Apply the mask
masked_spectrogram[:, :, mask_start:mask_start + mask_width] = 0

return masked_spectrogram

Frequency Time Masking

The frequency_time_masking function applies random masking on both the frequency and time dimensions of a given spectrogram. It first randomly selects a segment of the frequency bins and sets their values to zero, representing the frequency mask. Then, it randomly chooses a segment of the time frames and sets them to zero, representing the time mask. The function then returns the spectrogram with these applied masks.
def frequency_time_masking(spectrogram, max_freq_mask_width=15, max_time_mask_width=40):
# Clone the spectrogram to avoid in-place modifications
masked_spectrogram = spectrogram.clone()

# Apply frequency masking
num_frequency_bins = masked_spectrogram.shape[1]
freq_mask_width = torch.randint(0, max_freq_mask_width + 1, (1,)).item()
freq_mask_start = torch.randint(0, num_frequency_bins - freq_mask_width + 1, (1,)).item()
masked_spectrogram[:, freq_mask_start:freq_mask_start + freq_mask_width, :] = 0

# Apply time masking
num_time_frames = masked_spectrogram.shape[2]
time_mask_width = torch.randint(0, max_time_mask_width + 1, (1,)).item()
time_mask_start = torch.randint(0, num_time_frames - time_mask_width + 1, (1,)).item()
masked_spectrogram[:, :, time_mask_start:time_mask_start + time_mask_width] = 0

return masked_spectrogram

Step 5: Create Our Own Dataset

For the sake of clarity and to facilitate a more streamlined approach, we've crafted a custom dataset from the ground up. This custom dataset encompasses a collection of 1,000 spectrograms, each randomly generated.
Defining a random Sin wave generational function
def generate_sine_wave(freq, time, sample_rate=5000):
"""Generate a sine wave of a given frequency."""
t = np.linspace(0, time, int(time * sample_rate), endpoint=False)
return np.sin(2 * np.pi * freq * t)

def generate_spectrogram(signal, sample_rate=5000, window_size=128, step_size=64):
"""Generate a spectrogram from a signal."""
window = np.hanning(window_size)
return plt.specgram(signal, NFFT=window_size, Fs=sample_rate, noverlap=window_size - step_size, window=window, mode='magnitude')[0]

class StructuredSpectrogramDataset(data.Dataset):
def __init__(self, num_samples=100): # Note the change here
self.num_samples = num_samples
self.data = []
self.labels = []

for i in range(num_samples):
freq = np.random.choice(np.arange(10, 2000, 10)) # Frequency up to 2000Hz

# Remap the frequencies to 5 classes
label = freq // 200 # This will automatically map to 0-4 for freq from 10-1000, and then 5-9 for freq from 1000-2000
label %= 5 # This will make sure label is between 0-4

sine_wave = generate_sine_wave(freq, 1) # 1-second sine wave
spectrogram = generate_spectrogram(sine_wave)
self.data.append(spectrogram)
self.labels.append(label)

def __len__(self):
return self.num_samples

def __getitem__(self, idx):
return torch.tensor(self.data[idx], dtype=torch.float32).float(), self.labels[idx]

Step 6: Split Our Dataset Into Training and Evaluation

combined_dataset = StructuredSpectrogramDataset()

# Randomly shuffle and split
train_size = int(0.8 * len(combined_dataset))
eval_size = len(combined_dataset) - train_size

train_dataset, eval_dataset = torch.utils.data.random_split(combined_dataset, [train_size, eval_size])

# Create the dataloaders
train_loader = data.DataLoader(train_dataset, batch_size=16, shuffle=True)
eval_loader = data.DataLoader(eval_dataset, batch_size=16, shuffle=False)

def log_first_n_samples_to_wandb(n, data_loader):
spectrogram_images = []
for i, (data, target) in enumerate(data_loader):
if i >= n:
break
# Convert tensor to numpy for visualization
image = np.squeeze(data[0].numpy())
spectrogram_images.append(wandb.Image(image, caption=f"Label: {target[0]}"))

wandb.log({"First 5 Spectrograms": spectrogram_images})

log_first_n_samples_to_wandb(5, train_loader)
To better understand the dataset, here are the spectrograms of the first 5 data points as logged into W&B.

Given that each graph showcases pure frequencies, there isn't significant variation depicted. As a result, they are presented as straight horizontal lines.

Step 7: Create Our Training Function

In this section, we've formulated a dedicated training function, seamlessly integrating the desired time masking technique within it.
def train_one_epoch(model, data_loader, optimizer, criterion):
model.train()
total_loss = 0.0

for batch_idx, (data, target) in enumerate(data_loader):
# print("Shape of data:", data.shape) # Add this line to print shape
# Apply random time masking
masked_data = random_masking(data)
masked_data = masked_data.float()
target = target.long() # CrossEntropyLoss expects targets to be long type



# Log masked spectrograms to W&B (logging first sample in each batch)
if batch_idx % 10 == 0: # Log every 10 batches
original_img = np.squeeze(data[0].numpy())
masked_img = np.squeeze(masked_data[0].numpy())
wandb.log({
"Original Spectrogram": [wandb.Image(original_img, caption="Original")],
"Masked Spectrogram": [wandb.Image(masked_img, caption="Masked")]
})

optimizer.zero_grad()
outputs = model(masked_data)
loss = criterion(outputs, target)
loss.backward()
optimizer.step()
total_loss += loss.item()

avg_loss = total_loss / len(data_loader)
return avg_loss

Step 8: Create Our Evaluation Function

def evaluate(model, eval_loader, criterion):
model.eval()
total_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for batch_idx, (data, target) in enumerate(eval_loader):
outputs = model(data)
loss = criterion(outputs, target)
total_loss += loss.item()

_, predicted = outputs.max(1)
total += target.size(0)
correct += predicted.eq(target).sum().item()

avg_loss = total_loss / len(eval_loader)
accuracy = 100. * correct / total
return avg_loss, accuracy

Step 9: Define the Model, Optimizer, and Training Loss

model = SimpleCNN(num_classes=5)
model = model.float()
optimizer = optim.Adam(model.parameters(), lr=0.001)
criterion = nn.CrossEntropyLoss()

Step 10: Train the Model for 10 Epochs and W&B Logging

num_epochs = 10
for epoch in range(num_epochs):
train_loss = train_one_epoch(model, train_loader, optimizer, criterion)
eval_loss, eval_accuracy = evaluate(model, eval_loader, criterion)
print(f"Epoch [{epoch+1}/{num_epochs}], Train Loss: {train_loss:.4f}, Eval Loss: {eval_loss:.4f}, Eval Accuracy: {eval_accuracy:.2f}%")
# Log both Training and Evaluation Metrics
wandb.log({
"Train Loss": train_loss,
"Eval Loss": eval_loss,
"Eval Accuracy": eval_accuracy
})

Step 11: Finish Logging

wandb.finish()

Logging and Visualizing Time Masking Experiments is PyTorch Using W&B

We've implemented and examined three distinct time masking techniques with PyTorch, logging each using the W&B platform. For each method, not only have we captured the progression of the training loss graphically, but we've also provided a comparative visualization of both the original and masked spectrums to underscore the impact of each technique. Thus ensuring a deeper understanding of the interplay between time masking and model performance.

Random Time Masking With PyTorch As Logged Into W&B

We have also logged the training loss, evaluation loss, and evaluation accuracy graphs for each of the three masking techniques into W&B. This comprehensive logging provides us with a clear understanding of each model's performance throughout the training process.

In addition, we have logged the spectrograms before and after performing each of our three masking techniques.

As illustrated above, the Random Masking technique has effectively obscured a segment of the displayed line.

Frequency Time Masking With PyTorch As Logged Into W&B

Similarly, we have saved the Frequency Time Masking-based model graphs.

Along with the original and masked spectrogram


Both Random and Frequency Time Masking As Logged Into W&B

As a supplementary measure, we've archived the model graphs resulting from the application of Random Masking, followed by Frequency-Time Masking. This combined approach could yield superior outcomes in certain scenarios.

Evaluation Explanation

What we are observing here in the spectrogram visualizations after applying the masking techniques is consistent with the expected behavior of time and frequency masking:
  • Time Masking (Random Masking): This technique masks a consecutive segment of time steps, leading to a "vertical" blackout in the spectrogram because it affects all frequency channels for a specific time duration. That's why you're seeing a single vertical line.
  • Frequency Masking: It masks a consecutive range of frequency channels, causing a "horizontal" blackout in the spectrogram since it affects all time steps for specific frequencies. This results in a thicker horizontal line.
  • Combining Both: When you apply both time and frequency masking one after the other, you get regions in the spectrogram that are blacked out both vertically (from time masking) and horizontally (from frequency masking). This can sometimes resemble the letter "T", where the vertical line is from time masking and the horizontal line (or crossbar) is from frequency masking.
The "T" shape is a visual representation of the combined effect of both masking techniques. When the model sees such "T"-shaped masked regions during training, it is compelled to learn features from the surrounding unmasked areas, which aids in improving its robustness and generalization.
Moreover, we have also logged the training loss, evaluation loss, and evaluation accuracy graphs for each of the three masking techniques into Weights & Biases. This comprehensive logging provides us with a clear understanding of each model's performance throughout the training process. Among the three, the Random Masking approach provided us with the best training loss and eval accuracy indicating its effectiveness compared to the other techniques.

Conclusion

In this article, we've explored time masking strategies from random masking to frequency-aware masking, offering diverse approaches to augmenting data and enhancing model resilience. The powerful combination of PyTorch's flexibility and the meticulous tracking of W&B empowers researchers to push the boundaries of what's possible in sequential data processing.
As the field continues to evolve, it's imperative to understand, adapt, and innovate with these techniques, ensuring models are not only accurate, but robust and generalizable across a myriad of real-world challenges.
Iterate on AI agents and models faster. Try Weights & Biases today.