In this study, we use real-time air quality data from the Ministry of Environment to conduct research on the $SSSD^{S4}$ model. Before proceeding, since model training requires a dataset with time series characteristics and no missing values, we first use the autoFRK model. By leveraging the spatial correlations between monitoring stations, we impute the missing data. After imputation, the data is divided into training and testing sets, which are then used for $SSSD^{S4}$ model training and imputation.
# import modulesimportnumpyasnpimportmatplotlib.pyplotasplt# functionsdeftest_plot(full_data,missing_data,title,save_path):"""
Plot the first 3 dimensions of the first sample, comparing full data and missing data.
"""fig,axes=plt.subplots(3,1,figsize=(12,8),sharex=True)forjinrange(3):axes[j].plot(full_data[0,j],color='gray',label='full data',alpha=0.6)axes[j].plot(missing_data[0,j],color='red',label=title)axes[j].set_ylabel(f'Dim {j}')axes[j].legend()plt.suptitle(title)plt.xlabel('Time')plt.tight_layout()plt.savefig(f"{save_path}.png",dpi=300)plt.show()
Load and split the dataset into training and testing sets. Here, the last 500 data entries are selected as the test set, and the data from 2,500 to 500 entries are used as the training set.
Now, adjust the test set using the missing data methods rm, rbm, bm, and tf, and plot the first three monitoring stations. For each missing method, select 200 data entries as missing from the $500 \times 0.4 = 200$ data points.
In each plot, the gray color represents the test set data with no missing values, and the red color represents the data from various monitoring stations to be imputed.
There are 5 measurement items, 26 monitoring stations, and $464 \times 24 = 11136$ time points in total.
Through previous processing, we obtained 2000 data points for training and 500 data points for testing, with different missing data conditions. The shape of the input dataset is (measurement items, time, stations), and after imputation, the output shape is (measurement items, stations, time).
The configuration file for training.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Training configurationbatch_size:50# Batch sizeoutput_directory:"./results/checkpoint"# Output directory for checkpoints and logsckpt_iter:"max"# Checkpoint mode (max or min)iters_per_ckpt:100# Checkpoint frequency (number of epochs)iters_per_logging:100# Log frequency (number of iterations)n_iters:10000# Maximum number of iterationslearning_rate:0.002# Learning rate# Additional training settingsonly_generate_missing:true# Generate missing values onlyuse_model:2# Model to use for trainingmasking:"forecast"# Masking strategy for missing valuesmissing_k:5# Number of missing values# Data pathsdata:train_path:"./datasets/real_time_by_autoFRK/pollutants_train.npy"# Path to training data
wavenet:# WaveNet model parametersinput_channels:26# Number of input channelsoutput_channels:26# Number of output channelsresidual_layers:32# Number of residual layersresidual_channels:128# Number of channels in residual blocksskip_channels:128# Number of channels in skip connections# Diffusion step embedding dimensionsdiffusion_step_embed_dim_input:64# Input dimensiondiffusion_step_embed_dim_hidden:512# Middle dimensiondiffusion_step_embed_dim_output:512# Output dimension# Structured State Spaces sequence model (S4) configurationss4_max_sequence_length:2000# Maximum sequence lengths4_state_dim:64# State dimensions4_dropout:0.0# Dropout rates4_bidirectional:true# Whether to use bidirectional layerss4_use_layer_norm:true# Whether to use layer normalizationdiffusion:# Diffusion model parametersT:200# Number of diffusion stepsbeta_0:0.0001# Initial beta valuebeta_T:0.02# Final beta value
The configuration file for imputation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Inference configurationbatch_size:50# Batch size for inferenceoutput_directory:"./results/checkpoint/rm"# Output directory for inference resultsckpt_path:"./results/checkpoint"# Path to checkpoint for inferencetrials:1# Replications# Additional training settingsonly_generate_missing:true# Generate missing values onlyuse_model:2# Model to use for trainingmasking:"forecast"# Masking strategy for missing valuesmissing_k:200# Number of missing values# Data pathsdata:test_path:"./datasets/real_time_by_autoFRK/pollutants_test_rm.npy"# Path to test data
defget_mask_forecast(sample:torch.Tensor,k:int)->torch.Tensor:"""
Get mask of same segments (black-out missing) across channels based on k.
Args:
sample (torch.Tensor): Tensor of shape [# of samples, # of channels].
k (int): Number of missing values.
Returns:
torch.Tensor: Mask of sample's shape where 0's indicate missing values to be imputed, and 1's indicate preserved values.
"""#mask = torch.ones_like(sample) # Initialize mask with all ones# Calculate the indices of missing values#s_nan = torch.arange(mask.shape[0] - k, mask.shape[0])# Apply mask for each channel#for channel in range(mask.shape[1]):# mask[s_nan, channel] = 0mask=(~torch.isnan(sample)).float()# replace only missing valuesreturnmask
The training time is approximately 2 hours, and the imputation time is about 2 minutes.
It is important to note that the original code only accepts datasets with complete, non-missing values, which means that imputation of missing values will fail.
(sssd) u6025091@iq0l0ttest1140426-9bhg4:~/SSSD_CP$ ./scripts/diffusion/inference_job.sh -m configs/model.yaml -i configs/inference.yaml
Intializing conda
Activating Conda Env: sssd
[Execution - Inference]/home/u6025091/SSSD_CP/scripts/diffusion/infer.py --model_config configs/model.yaml --inference_config configs/inference.yaml
2025-04-26 14:28:16,698 - sssd.utils.logger - INFO - Using 1 GPUs!
2025-04-26 14:28:16,939 - sssd.utils.logger - INFO - Current time: 2025-04-26 14:28:16
2025-04-26 14:28:28,592 - sssd.utils.logger - INFO - The 1th inference trial
2025-04-26 14:28:28,592 - sssd.utils.logger - INFO - Output directory: ./results/checkpoint/bm/T200_beta00.0001_betaT0.02/max
2025-04-26 14:28:29,827 - sssd.utils.logger - INFO - Successfully loaded model at iteration 10000Traceback (most recent call last):
File "/home/u6025091/SSSD_CP/scripts/diffusion/infer.py", line 117, in <module>
run_job(model_config, inference_config, device, args.ckpt_iter) File "/home/u6025091/SSSD_CP/scripts/diffusion/infer.py", line 96, in run_job
).generate() File "/home/u6025091/.local/lib/python3.10/site-packages/sssd/inference/generator.py", line 151, in generate
mse= mean_squared_error( File "/home/u6025091/local/envs/sssd/lib/python3.10/site-packages/sklearn/metrics/_regression.py", line 438, in mean_squared_error
y_type, y_true, y_pred, multioutput= _check_reg_targets( File "/home/u6025091/local/envs/sssd/lib/python3.10/site-packages/sklearn/metrics/_regression.py", line 96, in _check_reg_targets
y_pred= check_array(y_pred, ensure_2d=False, dtype=dtype) File "/home/u6025091/local/envs/sssd/lib/python3.10/site-packages/sklearn/utils/validation.py", line 800, in check_array
_assert_all_finite(array, allow_nan=force_all_finite=="allow-nan") File "/home/u6025091/local/envs/sssd/lib/python3.10/site-packages/sklearn/utils/validation.py", line 114, in _assert_all_finite
raise ValueError(ValueError: Input contains NaN, infinity or a value too large for dtype('float32').
The following code was modified to allow the model to select the mask based on the actual missing values. This segment of code is located in /SSSD_CP-main/sssd/core/utils.py.
defget_mask_forecast(sample:torch.Tensor,k:int)->torch.Tensor:"""
Get mask of same segments (black-out missing) across channels based on k.
Args:
sample (torch.Tensor): Tensor of shape [# of samples, # of channels].
k (int): Number of missing values.
Returns:
torch.Tensor: Mask of sample's shape where 0's indicate missing values to be imputed, and 1's indicate preserved values.
"""#mask = torch.ones_like(sample) # Initialize mask with all ones# Calculate the indices of missing values#s_nan = torch.arange(mask.shape[0] - k, mask.shape[0])# Apply mask for each channel#for channel in range(mask.shape[1]):# mask[s_nan, channel] = 0mask=(~torch.isnan(sample)).float()returnmask
The following modification is also necessary to fill missing values with 0, allowing the code to perform imputation (since NA cannot be used in numerical operations and will cause errors). This segment of code is located in /SSSD_CP-main/sssd/inference/generator.py.
The new line of code added is:
1
batch=torch.nan_to_num(batch,nan=0.0)# Replace NaN with 0.0
It is important to note that this change will make the original mse and mspe calculations invalid. These metrics should be recalculated after imputation is completed. The modification here only affects mse and mspe, and it has been confirmed that it does not affect other calculations. Furthermore, mse and mspe are only used for the output after inference.
defgenerate(self)->list:"""Generate samples using the given neural network model."""all_mses=[]all_mapes=[]forindex,(batch,)inenumerate(self.dataloader):batch=batch.to(self.device)mask=self._update_mask(batch)iftorch.isnan(mask).any():# debugprint(f"[Batch {index}] NaN in mask!")batch=torch.nan_to_num(batch,nan=0.0)# Replace NaN with 0.0iftorch.isnan(batch).any():# debugprint(f"[NaN DETECTED] Batch {index} has NaNs!")batch=batch.permute(0,2,1)generated_series=(sampling(net=self.net,size=batch.shape,diffusion_hyperparams=self.diffusion_hyperparams,cond=batch,mask=mask,only_generate_missing=self.only_generate_missing,device=self.device,).detach().cpu().numpy())ifnp.isnan(generated_series).any():# debugprint("[NaN DETECTED] in generated_series")print("Locations:",np.argwhere(np.isnan(generated_series)))batch=batch.detach().cpu().numpy()mask=mask.detach().cpu().numpy()mse=mean_squared_error(batch[~mask.astype(bool)],generated_series[~mask.astype(bool)])mape=mean_absolute_percentage_error(batch[~mask.astype(bool)],generated_series[~mask.astype(bool)])all_mses.append(mse)all_mapes.append(mape)results={"imputation":generated_series,"original":batch,"mask":mask,}self._save_data(results,index)returnall_mses,all_mapes
Note
Note: The above modifications are only used during model inference. When training the model, the original code should still be used.
After the adjustments, you can use the following command for imputation.
(sssd) u6025091@iq0l0ttest1140426-9bhg4:~/SSSD_CP$ ./scripts/diffusion/inference_job.sh -m configs/model.yaml -i configs/inference.yaml
Intializing conda
Activating Conda Env: sssd
[Execution - Inference]/home/u6025091/SSSD_CP/scripts/diffusion/infer.py --model_config configs/model.yaml --inference_config configs/inference.yaml
2025-04-26 15:05:30,600 - sssd.utils.logger - INFO - Using 1 GPUs!
2025-04-26 15:05:30,749 - sssd.utils.logger - INFO - Current time: 2025-04-26 15:05:30
2025-04-26 15:05:41,170 - sssd.utils.logger - INFO - The 1th inference trial
2025-04-26 15:05:41,171 - sssd.utils.logger - INFO - Output directory: ./results/checkpoint/rm/T200_beta00.0001_betaT0.02/max
2025-04-26 15:05:41,544 - sssd.utils.logger - INFO - Successfully loaded model at iteration 100002025-04-26 15:06:10,954 - sssd.utils.logger - INFO - Average MSE: 13.989262580871582
2025-04-26 15:06:10,955 - sssd.utils.logger - INFO - Average MAPE: 6969775217442816.0
2025-04-26 15:06:10,955 - sssd.utils.logger - INFO - Current time: 2025-04-26 15:06:10
Inference Job completed
(sssd) u6025091@iq0l0ttest1140426-9bhg4:~/SSSD_CP$ ./scripts/diffusion/inference_job.sh -m configs/model.yaml -i configs/inference.yaml
Intializing conda
Activating Conda Env: sssd
[Execution - Inference]/home/u6025091/SSSD_CP/scripts/diffusion/infer.py --model_config configs/model.yaml --inference_config configs/inference.yaml
2025-04-26 15:06:22,825 - sssd.utils.logger - INFO - Using 1 GPUs!
2025-04-26 15:06:23,006 - sssd.utils.logger - INFO - Current time: 2025-04-26 15:06:23
2025-04-26 15:06:33,472 - sssd.utils.logger - INFO - The 1th inference trial
2025-04-26 15:06:33,474 - sssd.utils.logger - INFO - Output directory: ./results/checkpoint/rbm/T200_beta00.0001_betaT0.02/max
2025-04-26 15:06:34,008 - sssd.utils.logger - INFO - Successfully loaded model at iteration 100002025-04-26 15:07:03,308 - sssd.utils.logger - INFO - Average MSE: 13.990039825439453
2025-04-26 15:07:03,308 - sssd.utils.logger - INFO - Average MAPE: 8490500730388480.0
2025-04-26 15:07:03,308 - sssd.utils.logger - INFO - Current time: 2025-04-26 15:07:03
Inference Job completed
(sssd) u6025091@iq0l0ttest1140426-9bhg4:~/SSSD_CP$ ./scripts/diffusion/inference_job.sh -m configs/model.yaml -i configs/inference.yaml
Intializing conda
Activating Conda Env: sssd
[Execution - Inference]/home/u6025091/SSSD_CP/scripts/diffusion/infer.py --model_config configs/model.yaml --inference_config configs/inference.yaml
2025-04-26 15:07:19,904 - sssd.utils.logger - INFO - Using 1 GPUs!
2025-04-26 15:07:20,081 - sssd.utils.logger - INFO - Current time: 2025-04-26 15:07:20
2025-04-26 15:07:30,561 - sssd.utils.logger - INFO - The 1th inference trial
2025-04-26 15:07:30,562 - sssd.utils.logger - INFO - Output directory: ./results/checkpoint/bm/T200_beta00.0001_betaT0.02/max
2025-04-26 15:07:30,889 - sssd.utils.logger - INFO - Successfully loaded model at iteration 100002025-04-26 15:08:00,273 - sssd.utils.logger - INFO - Average MSE: 14.108148574829102
2025-04-26 15:08:00,273 - sssd.utils.logger - INFO - Average MAPE: 1.3516574539382784e+16
2025-04-26 15:08:00,273 - sssd.utils.logger - INFO - Current time: 2025-04-26 15:08:00
Inference Job completed
(sssd) u6025091@iq0l0ttest1140426-9bhg4:~/SSSD_CP$ ./scripts/diffusion/inference_job.sh -m configs/model.yaml -i configs/inference.yaml
Intializing conda
Activating Conda Env: sssd
[Execution - Inference]/home/u6025091/SSSD_CP/scripts/diffusion/infer.py --model_config configs/model.yaml --inference_config configs/inference.yaml
2025-04-26 15:08:14,197 - sssd.utils.logger - INFO - Using 1 GPUs!
2025-04-26 15:08:14,366 - sssd.utils.logger - INFO - Current time: 2025-04-26 15:08:14
2025-04-26 15:08:24,075 - sssd.utils.logger - INFO - The 1th inference trial
2025-04-26 15:08:24,076 - sssd.utils.logger - INFO - Output directory: ./results/checkpoint/tf/T200_beta00.0001_betaT0.02/max
2025-04-26 15:08:24,428 - sssd.utils.logger - INFO - Successfully loaded model at iteration 100002025-04-26 15:08:53,786 - sssd.utils.logger - INFO - Average MSE: 14.107381820678711
2025-04-26 15:08:53,786 - sssd.utils.logger - INFO - Average MAPE: 1.3518593174011904e+16
2025-04-26 15:08:53,786 - sssd.utils.logger - INFO - Current time: 2025-04-26 15:08:53
Inference Job completed
The screen of a successful execution.
Imputation Result Analysis
The following code is used for analysis and plotting. In the images below, the gray color represents the test set data with no missing values, the red color represents the data sent for imputation, and the orange color represents the imputed results.
# test imputationimportosimportnumpyasnpimportmatplotlib.pyplotaspltfromtqdmimporttqdmtest_data=np.load(r'real_time\pollutants_test.npy').transpose(0,2,1)## functionsdefimputation_plot(full_data,missing_data,imputation_data,title,save_path):"""
Plot the first 3 dimensions of the first sample, comparing full data and missing data.
"""show_dims=2fig,axes=plt.subplots(show_dims,1,figsize=(12,8),sharex=True)#imputation_data = np.where(np.isnan(missing_data), imputation_data, np.nan)forjinrange(show_dims):axes[j].plot(full_data[0,j],color='gray',label='full data',alpha=0.6)axes[j].plot(imputation_data[0,j],color='orange',label='imputation data',alpha=0.6)axes[j].plot(missing_data[0,j],color='red',label=title)axes[j].set_ylabel(f'Dim {j}')axes[j].legend()plt.suptitle(title)plt.xlabel('Time')plt.tight_layout()plt.savefig(f"{save_path}.png",dpi=300)#plt.show()defimputation_plot_each_dim(full_data,missing_data,imputation_data,title,save_dir):"""
Plot each dimension of the first sample separately, comparing full data, missing data, and imputation data.
Save each plot as a separate file.
"""num_dims=full_data.shape[1]os.makedirs(save_dir,exist_ok=True)forjintqdm(range(num_dims)):fig,ax=plt.subplots(figsize=(12,4))ax.plot(full_data[0,j],color='gray',label='full data',alpha=0.6)ax.plot(imputation_data[0,j],color='orange',label='imputation data',alpha=0.6)ax.plot(missing_data[0,j],color='red',label=title)ax.set_ylabel(f'Dim {j}')ax.set_xlabel('Time')ax.set_title(f'{title} - Dimension {j}')ax.legend(loc='center left',bbox_to_anchor=(1,0.5))fig.tight_layout(rect=[0,0,0.85,1])save_path=os.path.join(save_dir,f"{title}_dim{j}.png")plt.savefig(save_path,dpi=300)plt.close(fig)
rm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
## rmfilename=r'real_time\imputation\imputation0_rm.npy'rm_data=np.load(filename)print(f'Shape: {rm_data.shape}')print(f'NAs: {np.isnan(rm_data).sum()}')# print(rm_data[0, 0])print(f'MSPE for all: {((test_data-rm_data)**2).mean()}')test_data_rm=np.load(r'real_time\pollutants_test_rm.npy').transpose(0,2,1)print(f'MSPE only for missing: {((test_data[np.isnan(test_data_rm)]-rm_data[np.isnan(test_data_rm)])**2).mean()}')imputation_plot(test_data,test_data_rm,rm_data,'imputation rm test data',r'real_time\imputation\imputation0_rm')imputation_plot_each_dim(test_data,test_data_rm,rm_data,'imputation rm test data',r'real_time\imputation\imputation0_rm\each_dim')# test_data[0, 0][1:10]# test_data_rm[0, 0][1:10]# rm_data[0, 0][1:10]
Execution result reference
1
2
3
4
5
Shape: (5, 26, 500)NAs: 0MSPE for all: 5.630354000694568
MSPE only for missing: 7.2555356208301784
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 26/26 [00:13<00:00, 1.95it/s]
imputation rm 。
The imputation results for all monitoring stations are as follows.
gallery_made_with_nanogallery2-1-rm
rbm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
## rbmfilename=r'real_time\imputation\imputation0_rbm.npy'rbm_data=np.load(filename)print(f'Shape: {rbm_data.shape}')print(f'NAs: {np.isnan(rbm_data).sum()}')# print(rbm_data[0, 0])print(f'MSPE for all: {((test_data-rbm_data)**2).mean()}')test_data_rbm=np.load(r'real_time\pollutants_test_rbm.npy').transpose(0,2,1)print(f'MSPE only for missing: {((test_data[np.isnan(test_data_rbm)]-rbm_data[np.isnan(test_data_rbm)])**2).mean()}')imputation_plot(test_data,test_data_rbm,rbm_data,'imputation rbm test data',r'real_time\imputation\imputation0_rbm')imputation_plot_each_dim(test_data,test_data_rbm,rbm_data,'imputation rbm test data',r'real_time\imputation\imputation0_rbm\each_dim')# test_data[0, 0][1:10]# test_data_rbm[0, 0][1:10]# rbm_data[0, 0][1:10]
Execution result reference
1
2
3
4
5
Shape: (5, 26, 500)NAs: 0MSPE for all: 5.67021524531403
MSPE only for missing: 8.879126539941137
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 26/26 [00:13<00:00, 1.97it/s]
imputation rbm 。
The imputation results for all monitoring stations are as follows.
gallery_made_with_nanogallery2-2-rbm
bm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
## bmfilename=r'real_time\imputation\imputation0_bm.npy'bm_data=np.load(filename)print(f'Shape: {bm_data.shape}')print(f'NAs: {np.isnan(bm_data).sum()}')# print(bm_data[0, 0])print(f'MSPE for all: {((test_data-bm_data)**2).mean()}')test_data_bm=np.load(r'real_time\pollutants_test_bm.npy').transpose(0,2,1)print(f'MSPE only for missing: {((test_data[np.isnan(test_data_bm)]-bm_data[np.isnan(test_data_bm)])**2).mean()}')imputation_plot(test_data,test_data_bm,bm_data,'imputation bm test data',r'real_time\imputation\imputation0_bm')imputation_plot_each_dim(test_data,test_data_bm,bm_data,'imputation bm test data',r'real_time\imputation\imputation0_bm\each_dim')# test_data[0, 0][1:10]# test_data_bm[0, 0][1:10]# bm_data[0, 0][1:10]
Execution result reference
1
2
3
4
5
Shape: (5, 26, 500)NAs: 0MSPE for all: 5.733927828240412
MSPE only for missing: 14.33481884698311
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 26/26 [00:13<00:00, 1.95it/s]
imputation bm 。
The imputation results for all monitoring stations are as follows.
gallery_made_with_nanogallery2-3-bm
tf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
## tffilename=r'real_time\imputation\imputation0_tf.npy'tf_data=np.load(filename)print(f'Shape: {tf_data.shape}')print(f'NAs: {np.isnan(tf_data).sum()}')# print(tf_data[0, 0])print(f'MSPE for all: {((test_data-tf_data)**2).mean()}')test_data_tf=np.load(r'real_time\pollutants_test_tf.npy').transpose(0,2,1)print(f'MSPE only for missing: {((test_data[np.isnan(test_data_tf)]-tf_data[np.isnan(test_data_tf)])**2).mean()}')imputation_plot(test_data,test_data_tf,tf_data,'imputation tf test data',r'real_time\imputation\imputation0_tf')imputation_plot_each_dim(test_data,test_data_tf,tf_data,'imputation tf test data',r'real_time\imputation\imputation0_tf\each_dim')# test_data[0, 0][1:10]# test_data_tf[0, 0][1:10]# tf_data[0, 0][1:10]
Execution result reference
1
2
3
4
5
Shape: (5, 26, 500)NAs: 0MSPE for all: 5.728256563966483
MSPE only for missing: 14.320640763169513
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 26/26 [00:13<00:00, 1.94it/s]
imputation tf 。
The imputation results for all monitoring stations are as follows.
gallery_made_with_nanogallery2-4-tf
Conclusion
Although the imputation was successfully performed, it seems to have “succeeded” in failing?! While each imputed result appears to resemble white noise, from rbm, we can see that some missing values were indeed properly imputed, and the predictions are close to the actual values (covering the gray true values). However, both tf and bm show that the predictions for non-missing values are also accurate, but for rm and rbm, the non-missing values were poorly predicted, which is quite unreasonable (since only_generate_missing: true was set).
The reason for this behavior has not yet been identified and requires further investigation.