Engee documentation
Notebook

Classification of radar targets using machine learning and deep learning

This example demonstrates approaches to classifying radar data using machine and deep learning methods. The following approaches are used to solve the problem:

  1. Machine learning: Support Vector Machine (SVM).
  2. Deep Learning: SqueezeNet, LSTM

Object classification is an important task for radar systems. In this example, we consider the problem of determining which object reflects the radar signal — a cylinder or a cone. Although the example uses synthetic data, this approach can also be applied to real radar measurements.

SVM Training

First of all, you need to import the packages you use

In the file install_packages.jl there are packages that are needed for the script. They are added to the work environment. In the file import_packages all installed packages are imported for the script. In the cells below, we run their execution

In [ ]:
include("$(@__DIR__)/Install_packages.jl")
In [ ]:
include("$(@__DIR__)/import_packages.jl")
init()

Loading data

The data that will be used to train the models was taken from a demo example of radar object EPR modeling.

Let's write down the path in which the data lies

In [ ]:
data_path = "$(@__DIR__)/gen_data.csv"

The CSV file contains data on the cone and cylinder. The first 100 elements are a cylinder, the second 100 elements are a cone. Let's split our data into a training and test dataset.

In [ ]:
# Let's say your file is called "data.csv"
df = CSV.read(data_path, DataFrame)
data = Matrix(df)
cyl_train = data[:, 1:75]    # the first 75 elements
cyl_test  = data[:, 76:100]  # the remaining 25 elements

# For cones:
cone_train = data[:, 101:175]  # 75 elements
cone_test  = data[:, 176:200]  # 25 elements
# Combining the training and test samples:
train_data = hcat(cyl_train, cone_train)
test_data  = hcat(cyl_test, cone_test);
In [ ]:
TrainFeatures_T = permutedims(train_data, (2,1))
TestFeatures_T = permutedims(test_data, (2, 1))

TrainLabels = reshape(vcat(fill(1, 75), fill(2, 75)), :, 1)
TestLabels = reshape(vcat(fill(1, 25), fill(2, 25)), :, 1);

The code below extracts features from time series using the Morlaix continuous wavelet function transformation. First, a wavelet transform is applied to all data, followed by the calculation of the absolute values of the coefficients in order to leave only the amplitudes. Then these amplitudes are averaged along the time axis, which reduces the dimension of the features. After that, the result is "compressed" to remove unnecessary dimensions.

All this is done for training and test datasets.

In [ ]:
wavelet_cfg = wavelet(Morlet(π), averagingType=Dirac(), β=1.5)
train_features_cwt = dropdims(mean(abs.(cwt(TrainFeatures_T, wavelet_cfg)), dims=2), dims=2);
In [ ]:
wavelet_cfg = wavelet(Morlet(π), averagingType=Dirac(), β=1.5)
test_features_cwt = dropdims(mean(abs.(cwt(TestFeatures_T, wavelet_cfg)), dims=2), dims=2);

Initialize the list containing the class names

In [ ]:
class_names = ["Cylinder","Cone"]

Before training the classifier, the data must be converted to a format compatible with the model: the training features are converted to a tabular format, and the labels are converted to a categorical type.

In [ ]:
X_train = MLJ.table(train_features_cwt)  # Converting X_train to a dataframe
X_test = MLJ.table(test_features_cwt)  # Converting X_test to a dataframe
y_train  = coerce(vec(TrainLabels), Multiclass)  # Converting y_train to a vector and setting the Multiclass type
y_test = coerce(vec(TestLabels), Multiclass);  # Converting y_test to a vector and setting the Multiclass type

Initialization of the support vector model and its training

Next, we configure the support vector model by initializing the parameters and cross-validation.

In [ ]:
svm = (@MLJ.load SVC pkg=LIBSVM verbosity=true)()  # Download and create an SVM model using LIBSVM via MLJ

# Setting the parameters for the SVM model
svm.kernel = LIBSVM.Kernel.Polynomial         # Core type: Polynomial
svm.degree = 2                                # The degree of a polynomial for a polynomial kernel
svm.gamma = 0.1                               # The parameter γ that controls the influence of each training point
svm.cost = 1.0                                # Regularization parameter

# Creating a "machine" to associate the model with the data
mach = machine(svm, X_train, y_train)

# Setting up cross-validation with 5 folds
cv = CV(nfolds=5);

We train the model using cross-validation and calculate accuracy on training

In [ ]:
@info "Load model config, cross-valid"

cv_results = evaluate!(mach; resampling=cv, measures=[accuracy], verbosity=0) # Performing cross-validation of the model
println("Model accuracy: $(cv_results.measurement[1] * 100)" , "%")

Evaluation of the trained model

Let's evaluate the trained model based on the test data

In [ ]:
@info "Predict test"

y_pred_SVM = MLJ.predict(mach, X_test)   # Performing model predictions on test data

accuracy_score_SVM = accuracy(y_pred_SVM, y_test)  # Calculating the accuracy of the model on the test set

println("The accuracy of the model on the test: ", Int(accuracy_score_SVM * 100), "%")

Next, we will construct an error matrix to evaluate the classification quality of the model - function plot_confusion_matrix performs this task

In [ ]:
function plot_confusion_matrix(C)
    # Creating a heatmap with large fonts and contrasting colors
    heatmap(
        C,
        title = "Confusion Matrix",
        xlabel = "Predicted",
        ylabel = "True",
        xticks = (1:length(class_names), class_names),
        yticks = (1:length(class_names), class_names),
        c = :viridis,  # More contrasting color scheme
        colorbar_title = "Count",
        size = (600, 400)
    )

    # Let's add values to the cells for improved visibility.
    for i in 1:size(C, 1)
        for j in 1:size(C, 2)
            annotate!(j, i, text(C[i, j], :white, 12, :bold)) 
        end
    end

    # We display the graph explicitly
    display(current())
end;
In [ ]:
# An example of using the function
conf_matrix = CM.confmat(y_pred_SVM, y_test, levels=[2, 1])
conf_matrix = CM.matrix(conf_matrix)
plot_confusion_matrix(conf_matrix)

As can be seen from the constructed error matrix, the model classifies the cone well, but the cylinder is often confused with the cone.

SqueezeNet Training

Next, we will train the deep learning network - SqueezeNet. SqueezeNet is a compact convolutional neural network proposed in 2016 that achieves AlexNet performance with a significantly smaller size. It uses Fire modules that include squeeze layers (1x1 convolutions to reduce the number of channels) and expand (1x1 and 3x3 convolutions to restore dimensionality), which reduces the number of parameters without loss of quality. It is suitable for embedded devices due to its compactness.

Required parameters

Initialize the parameters involved in model training and data preparation

In [ ]:
batch_size = 2              # The size of the training batch
num_classes = 2             # The number of classes in the classification problem
lr = 1e-4                   # Learning rate
Epochs = 15                 # Number of epochs for training
Classes = 1:num_classes;     # A list of class indexes, for example, 1 and 2

Creating datasets

First of all, it is necessary to prepare the data for network training. It is necessary to perform and construct a continuous wavelet transform for the signals in order to obtain its time-frequency characteristics. The wavelets are "compressed" to localize short-term bursts with high temporal accuracy, and "stretched" to capture smooth changes in the signal structure.

Auxiliary function save_wavelet_images obtains a continuous wavelet transform (CWT) For each radar signal, it converts the result into a format compatible with computer vision models and saves the spectrograms as images.

Initialize several auxiliary functions

In [ ]:
# A function for normalizing values
function rescale(img)
    min_val = minimum(img)
    max_val = maximum(img)
    return (img .- min_val) ./ (max_val - min_val)
end

# Applying colormap jet and converting to RGB
function apply_colormap(data, cmap)
    h, w = size(data)
    rgb_image = [RGB(get(cmap, val)) for val in Iterators.flatten(eachrow(data))]
    return reshape(rgb_image, w, h)
end
# Converting a continuous wavelet transform to an image
function apply_image(wt)
    rescaled_data = rescale(abs.(wt))
    colored_image = apply_colormap(rescaled_data, ColorSchemes.inferno)
    resized_image = imresize(colored_image, (224, 224))
    flipped_image = reverse(resized_image, dims=1)
    return flipped_image
end;
In [ ]:
function save_wavelet_images(features_matrix, wavelet_filter, save_path, is_train=true)
    # Determining the number of examples for each class
    
    class1_count = is_train ? 75 : 25  # Number of examples for the cone class
    class2_count = size(features_matrix, 1) - class1_count  # Number of examples for the cylinder class
    println(class1_count)
    # Creating folders for each class
    cone_path = joinpath(save_path, "cone")  # cone — class 1
    cylinder_path = joinpath(save_path, "cylinder")  # cylinder — class 2
    mkpath(cone_path)
    mkpath(cylinder_path)

    # Processing matrix rows for the cylinder class
    for i in 1:class1_count
        res = cwt(features_matrix[i, :], wavelet_filter)
        image_wt = apply_image(res)
        img_filename = joinpath(cylinder_path, "sample_$(i).png")
        save(img_filename, image_wt)

    end

    # Processing matrix rows for the cone class
    for i in class1_count+1:class1_count+class2_count
        res = cwt(features_matrix[i, :], wavelet_filter)
        image_wt = apply_image(res)
        img_filename = joinpath(cone_path, "sample_$(i - class1_count).png")
        save(img_filename, image_wt)
    end
end;

An object c It is a configurable wavelet converter based on the Morlaix wavelet

In [ ]:
c = wavelet(Morlet(π), averagingType=NoAve(), β=1);

We will get a database of images for training a neural network

In [ ]:
save_wavelet_images(TrainFeatures_T, c, "$(@__DIR__)/New_imgs/train")
save_wavelet_images(TestFeatures_T, c, "$(@__DIR__)/New_imgs/test", false)

Let's look at an instance of the resulting image

In [ ]:
i = Images.load("$(@__DIR__)/New_imgs/train/cylinder/sample_2.png")
Out[0]:
No description has been provided for this image

Initialize the function for data augmentation: it is responsible for reducing images to the size of 224x224 and converting data into tensors

In [ ]:
function Augment_func(img)
    resized_img = imresize(img, 224, 224)               # Resizing the image to 224x224
    tensor_image = channelview(resized_img);            # Representation of data as a tensor
    permutted_tensor = permutedims(tensor_image, (2, 3, 1));        # Changing the order of the dimension to the format (H, W, C)
    permutted_tensor = Float32.(permutted_tensor)                   # Conversion to Float32 type
    return permutted_tensor
end;

Function Create_dataset creates training datasets by processing the directories where the images are located.

In [ ]:
function Create_dataset(path)
    img_train = []
    img_test = []
    label_train = []
    label_test = []

    train_path = joinpath(path, "train");
    test_path = joinpath(path, "test");

    # A function for processing images in a given directory
    function process_directory(directory, img_array, label_array, label_idx)
        for file in readdir(directory)
            if endswith(file, ".jpg") || endswith(file, ".png")
                file_path = joinpath(directory, file);
                img = Images.load(file_path);
                img = Augment_func(img);
                push!(img_array, img)
                push!(label_array, label_idx)
            end
        end
    end


    # Processing the train folder
    for (idx, label) in enumerate(readdir(train_path))
        println("Processing label in train: ", label)
        label_dir = joinpath(train_path, label)
        process_directory(label_dir, img_train, label_train, idx);
    end

    # Processing the test folder
    for (idx, label) in enumerate(readdir(test_path))
        println("Processing label in test: ", label)
        label_dir = joinpath(test_path, label)
        process_directory(label_dir, img_test, label_test, idx);
    end

    return img_train, img_test, label_train, label_test;
end;

In the next cell of the code, we will perform the function of creating training and test sets.

In [ ]:
path_to_data = "$(@__DIR__)/New_imgs"

img_train, img_test, label_train, label_test = Create_dataset(path_to_data);

We create a DataLoader that feeds images to the model input in batches. We transfer them to the GPU

Important note: The model is trained on the GPU, as this speeds up the learning process many times. If you need to use a GPU, contact our managers, and you will be given access to the GPU. The working directory will contain the weights of the already pre-trained network transferred to the CPU. After training the main network, you can look at the network in the CPU format by loading the appropriate weights into the model.

In [ ]:
train_loader_Snet = DataLoader((data=img_train, label=label_train), batchsize=batch_size, shuffle=true, collate=true)
test_loader_Snet = DataLoader((data=img_test, label=label_test), batchsize=batch_size, shuffle=true, collate=true)
train_loader_Snet = gpu.(train_loader_Snet)
test_loader_Snet = gpu.(test_loader_Snet)
@info "loading succes"
[ Info: loading succes

Preparation for training

Initialize our model by transferring it to the GPU

In [ ]:
Net = SqueezeNet(;  pretrain=false,
           nclasses = num_classes) |>gpu;

Initialize the optimizer, the loss function

In [ ]:
optimizer_Snet = Flux.Adam(lr, (0.9, 0.99));   
lossSnet(x, y) = Flux.Losses.logitcrossentropy(Net(x), y);

SqueezeNet Training

Let's describe the function that is responsible for training the model for an epoch. Function train_one_epoch performs model training on a single epoch, going through all the data batches from the loader Loader. Later, this function will be used to train the model. LSTM. This function has a parameter type_model which will determine which specific model we are training - convolutional or recurrent

In [ ]:
function train_one_epoch(model, Loader, Tloss, correct_sample, TSamples, loss_function, Optimizer, type_model)
    for (i, (x, y)) in enumerate(Loader) 
         
        if type_model == "Conv"
            TSamples += length(y) 
            gs = gradient(() -> loss_function(x, onehotbatch(y, Classes)), Flux.params(model))          # Calculating gradients
        elseif type_model == "Recurrent"
            TSamples += size(y, 2) 
            gs = gradient(() -> loss_function(x, y), Flux.params(model))                                # Calculating gradients
        end
        Flux.update!(Optimizer, Flux.params(model), gs)                                                 # Updating the optimizer
        y_pred = model(x)                                                                               # Making a prediction of the model
        # Next, we calculate the accuracy and error of our model based on the epoch
        if type_model == "Conv" 
            preds = onecold(y_pred, Classes)
            correct_sample += sum(preds .== y)
            Tloss += loss_function(x, onehotbatch(y, Classes))   
        elseif type_model == "Recurrent"
            Tloss += loss_function(x, y)
            predicted_classes = onecold(y_pred)  
            true_classes = onecold(y)         
            correct_sample += sum(predicted_classes .== true_classes)  
        end
    end
    return Tloss, TSamples, correct_sample
end;

Launching the SqueezeNet learning process

In [ ]:
@info "Starting training loop"

for epoch in 1:10 
    total_loss = 0.0 
    train_running_correct = 0  
    total_samples = 0   
    @info "Epoch $epoch"

    total_loss, total_samples, train_running_correct = train_one_epoch(Net, train_loader_Snet, total_loss, 
                                                train_running_correct, total_samples, lossSnet, optimizer_Snet, "Conv")



    epoch_loss = total_loss / total_samples
    epoch_acc = 100.0 * (train_running_correct / total_samples)
    println("loss: $epoch_loss, accuracy: $epoch_acc")  
end

Saving our trained model

In [ ]:
mkdir("$(@__DIR__)/models")
Out[0]:
"/user/nn/radar_classification_using_ML_DL/models"
In [ ]:
cpu(Net)
@save "$(@__DIR__)/models/SNET.bson" Net

Evaluation of the trained SqueezeNet model

Let's evaluate the trained model. Function evaluate_model_accuracy responsible for calculating the accuracy of the model

In [ ]:
function evaluate_model_accuracy(loader, model, classes, loss_function, type_model)
    total_loss, correct_predictions, total_samples = 0.0, 0, 0
    all_preds = []  
    True_labels = []
    for (x, y) in loader
        # Accumulation of losses
        total_loss += type_model == "Conv" ? loss_function(x, onehotbatch(y, classes)) : loss_function(x, y)

        # Predictions and accuracy calculations
        y_pred = model(x)
        
        preds = type_model == "Conv" ? onecold(y_pred, classes) : onecold(y_pred)
        true_classes = type_model == "Conv" ? y : onecold(y)
        append!(all_preds, preds)
        append!(True_labels, true_classes)
        correct_predictions += sum(preds .== true_classes)
        total_samples += type_model == "Conv" ? length(y) : size(y, 2)
    end
    # Calculating accuracy
    accuracy = 100.0 * correct_predictions / total_samples
    return accuracy, all_preds, True_labels
end;
In [ ]:
accuracy_score_Snet, all_predsSnet, true_predS = evaluate_model_accuracy(test_loader_Snet, Net, Classes, lossSnet, "Conv");
println("Accuracy trained model:", accuracy_score_Snet, "%")
Accuracy trained model:100.0%

As you can see above, the accuracy of the model is 100%. This makes it clear that the model perfectly separates the two classes from each other. Let's look at a specific example of what the model predicts.

Let's construct the error matrix using the function plot_confusion_matrix

In [ ]:
preds_for_CM = map(x -> x[1], all_predsSnet);
conf_matrix = CM.confmat(preds_for_CM, true_predS, levels=[1, 2])
conf_matrix = CM.matrix(conf_matrix)
plot_confusion_matrix(conf_matrix)

Model prediction

Uploading an image from the test dataset

In [ ]:
path = "$(@__DIR__)/New_imgs/test/cone/sample_14.png";
img = Images.load(path)  # Uploading the image
img_aug = Augment_func(img);
img_res = reshape(img_aug, size(img_aug, 1), size(img_aug, 2), size(img_aug, 3), 1);

Loading the weights of the model

In [ ]:
model_data = BSON.load("$(@__DIR__)/models/SNET.bson")
snet_cpu = model_data[:Net] |> cpu;

Making a prediction

In [ ]:
y_pred = (snet_cpu(img_res))
pred = onecold(y_pred, Classes)
# pred = cpu(preds) # Transferring predictions to the CPU
predicted_class_name = class_names[pred]  # We get the name of the predicted class
println("Predicted class: $predictedclass_name")
Предсказанный класс: ["Cone"]

Accordingly, the model coped with its task.

LSTM

The final section of this example describes the LSTM workflow. First, the LSTM levels are determined:

Initialization of parameters

Initialize the parameters involved in model training and data preparation

In [ ]:
MaxEpochs = 50;
BatchSize = 100;
learningrate = 0.01;
n_features = 1;
num_classes = 2;

Data collection

The signs that are submitted to the input were defined at the beginning of the script. Labels are somewhat redefined

In [ ]:
Trainlabels = vcat(fill(1, 75), fill(2, 75));
Testlabels = vcat(fill(1, 25), fill(2, 25));

Trainlabels = CategoricalArray(Trainlabels; levels=[1, 2]);
Testlabels = CategoricalArray(Testlabels; levels=[1, 2]);

Next, the data is reduced to the form that the LSTM network requires at the input.

In [ ]:
train_features = reshape(train_data, 1, size(train_data, 1), size(train_data, 2))
test_features = reshape(test_data, 1, size(test_data, 1), size(test_data, 2))

# TrainFeatures = permutedims(TrainFeatures, (2, 1))
TrainLabels = onehotbatch(Trainlabels, 1:num_classes)
TestLabels = onehotbatch(Testlabels, 1:num_classes)
Out[0]:
2×50 OneHotMatrix(::Vector{UInt32}) with eltype Bool:
 1  1  1  1  1  1  1  1  1  1  1  1  1  …  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅
 ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅     1  1  1  1  1  1  1  1  1  1  1  1

Let's put the training and test data into the type DataLoader and transfer them to the GPU.

Important note: The model is trained on the GPU, as this speeds up the learning process many times. If you need to use a GPU, contact our managers, and you will be given access to the GPU. The working directory will contain the weights of the already pre-trained network transferred to the CPU. After training the main network, you can look at the network in the CPU format by loading the appropriate weights into the model.

In [ ]:
train_loader_lstm = DataLoader((data=train_features, label=TrainLabels), batchsize=BatchSize, shuffle=true);
train_loader_lstm = gpu.(train_loader_lstm);
test_loader_lstm = DataLoader((data=test_features, label=TestLabels), batchsize=BatchSize, shuffle=true);
test_loader_lstm = gpu.(test_loader_lstm);

Initializing the model

Initialize the model that we will train. In this example, our model is a chain of layers connected to each other.

In [ ]:
model_lstm = Chain(
  LSTM(n_features, 100),
  x -> x[:, end, :], 
  Dense(100, num_classes),
  Flux.softmax) |> gpu;

Initialize the optimizer, the loss function

In [ ]:
optLSTM = Flux.Adam(learningrate, (0.9, 0.99));   
lossLSTM(x, y) = Flux.Losses.crossentropy(model_lstm(x), y);

Training

Next comes the training cycle of the model

In [ ]:
for epoch in 1:MaxEpochs
    total_loss = 0.0
    correct_predictions = 0
    total_samples = 0

    total_loss, total_samples, correct_predictions = train_one_epoch(model_lstm, train_loader_lstm, total_loss, 
                                                correct_predictions, total_samples, lossLSTM, optLSTM, "Recurrent")

    # Calculating accuracy
    accuracy = 100.0 * correct_predictions / total_samples

    println("Epoch $epoch, Loss: $(total_loss), Accuracy: $(accuracy)%")
end

Saving the model

In [ ]:
cpu(model_lstm)
@save "$(@__DIR__)/models/lstm.bson" model_lstm

Evaluation of the trained model

Let's evaluate our model by calculating the accuracy on a test dataset.

In [ ]:
accuracy_score_LSTM, all_predsLSTMm, true_predS_LSTM = evaluate_model_accuracy(test_loader_lstm, model_lstm, classes, lossLSTM, "Recurrent");
println("Accuracy trained model:", accuracy_score_LSTM, "%")
Accuracy trained model:84.0%

Now let's build an error matrix for visual evaluation of the model.

In [ ]:
preds_for_CM_LSTM = map(x -> x[1], all_predsLSTMm);
conf_matrix = CM.confmat(preds_for_CM_LSTM, true_predS_LSTM, levels=[1, 2])
conf_matrix = CM.matrix(conf_matrix)
plot_confusion_matrix(conf_matrix)

Let's test the model on a specific observation

In [ ]:
random_index = rand(1:size(test_features, 3))  
random_sample = test_features[:, :, random_index] 
random_label = onecold(TestLabels[:, random_index]) 
random_sample = cpu(random_sample);
In [ ]:
model_data = BSON.load("$(@__DIR__)/models/lstm.bson")
cpu_lstm = model_data[:model_lstm] |>cpu

predicted_probs = cpu_lstm(random_sample)
predicted_class = onecold(predicted_probs) 
# Output of the result
println("Random Sample Index: $random_index")
println("True Label: $random_label")
println("Predicted Probabilities: $predicted_probs")
println("Predicted Class: $predicted_class")
Random Sample Index: 12
True Label: 1
Predicted Probabilities: Float32[0.99999905; 9.580198f-7;;]
Predicted Class: [1]

As can be seen from the results above, the model correctly classified the instance provided to it.

Conclusion

This example shows a workflow for performing radar target classification using machine learning and deep learning techniques. Although this example used synthesized data for training and testing, it can be easily expanded to take into account the actual results of the radar.