Classification of radar targets using machine learning and deep learning
This example demonstrates approaches to classifying radar data using machine learning and deep learning techniques. The following approaches are used to solve the problem:
- Machine learning: support vector method (SVM).
- Deep Learning: SqueezeNet, LSTM
Object classification is an important task for radar systems. This example addresses the problem of determining whether an object reflects a radar signal as a cylinder or a cone. Although the example uses synthetic data, the approach can be applied to real radar measurements.
SVM training
The first step is to import the packages you are using
The file install_packages.jl
contains the packages that are needed for the script. They are added to the working environment. In the file import_packages
all installed packages are imported for the script. In the cells below we start their execution
include("$(@__DIR__)/Install_packages.jl")
include("$(@__DIR__)/import_packages.jl")
init()
Data loading
The data that will be used to train the models was taken from a demo example on EPR modelling of radar objects.
Let's specify the path where the data are located
data_path = "$(@__DIR__)/gen_data.csv"
The CSV file contains data on a cone and a cylinder. The first 100 elements are cylinder, the second 100 elements are cone. Let's split our data into training and test dataset
# Допустим, ваш файл называется "data.csv"
df = CSV.read(data_path, DataFrame)
data = Matrix(df)
cyl_train = data[:, 1:75] # первые 75 элементов
cyl_test = data[:, 76:100] # оставшиеся 25 элементов
# Для конусов:
cone_train = data[:, 101:175] # 75 элементов
cone_test = data[:, 176:200] # 25 элементов
# Объединяем в тренировочную и тестовую выборки:
train_data = hcat(cyl_train, cone_train)
test_data = hcat(cyl_test, cone_test);
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 performs feature extraction from time series using Morlet continuous wavelet transform. First, a wavelet transform is applied to all the data followed by calculation of the absolute values of the coefficients to leave only the amplitudes. These amplitudes are then averaged over the time axis to reduce the dimensionality of the features. The result is then "compressed" to remove unnecessary dimensions.
All this is done for the training and test datasets
wavelet_cfg = wavelet(Morlet(π), averagingType=Dirac(), β=1.5)
train_features_cwt = dropdims(mean(abs.(cwt(TrainFeatures_T, wavelet_cfg)), dims=2), dims=2);
wavelet_cfg = wavelet(Morlet(π), averagingType=Dirac(), β=1.5)
test_features_cwt = dropdims(mean(abs.(cwt(TestFeatures_T, wavelet_cfg)), dims=2), dims=2);
Initialise the list containing class names
class_names = ["Cylinder","Cone"]
Before training the classifier, the data must be converted into a format compatible with the model: training features are converted into tabular form and labels are converted into a categorical type.
X_train = MLJ.table(train_features_cwt) # Преобразуем X_train в датафрейм
X_test = MLJ.table(test_features_cwt) # Преобразуем X_test в датафрейм
y_train = coerce(vec(TrainLabels), Multiclass) # Преобразуем y_train в вектор и задаем тип Multiclass
y_test = coerce(vec(TestLabels), Multiclass); # Преобразуем y_test в вектор и задаем тип Multiclass
Initialisation of the support vector model and its training
Next, we set up the support vector model by initialising the parameters and cross validation
svm = (@MLJ.load SVC pkg=LIBSVM verbosity=true)() # Загружаем и создаем модель SVM с использованием LIBSVM через MLJ
# Задаем параметры для модели SVM
svm.kernel = LIBSVM.Kernel.Polynomial # Тип ядра: полиномиальное (Polynomial)
svm.degree = 2 # Степень полинома для полиномиального ядра
svm.gamma = 0.1 # Параметр γ, контролирующий влияние каждой обучающей точки
svm.cost = 1.0 # Параметр регуляризации
# Создаем "машину" (machine) для связывания модели с данными
mach = machine(svm, X_train, y_train)
# Настраиваем кросс-валидацию с 5 фолдами
cv = CV(nfolds=5);
We train the model using cross-validation and calculate the accuracy on training
@info "Load model config, cross-valid"
cv_results = evaluate!(mach; resampling=cv, measures=[accuracy], verbosity=0) # Выполняем кросс-валидацию модели
println("Точность модели: $(cv_results.measurement[1] * 100)" , "%")
Evaluate the trained model
Let's evaluate the trained model on the test data
@info "Predict test"
y_pred_SVM = MLJ.predict(mach, X_test) # Выполняеем предсказания модели на тестовых данных
accuracy_score_SVM = accuracy(y_pred_SVM, y_test) # Вычисляем точность модели на тестовом наборе
println("Точность модели на тесте: ", Int(accuracy_score_SVM * 100), "%")
Next, let's build an error matrix to evaluate the quality of model classification - the function plot_confusion_matrix
fulfils this task
function plot_confusion_matrix(C)
# Создаем heatmap с большими шрифтами и контрастными цветами
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, # Более контрастная цветовая схема
colorbar_title = "Count",
size = (600, 400)
)
# Добавим значения в ячейки для улучшенной наглядности
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
# Явно отображаем график
display(current())
end;
# Пример использования функции
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 a deep learning network - SqueezeNet. SqueezeNet is a compact convolutional neural network proposed in 2016 that achieves the performance of AlexNet at a much smaller size. It uses Fire modules that include squeeze (1x1 convolutional layers to reduce the number of channels) and expand (1x1 and 3x3 convolutional layers to restore dimensionality) layers, which reduces the number of parameters without losing quality. Suitable for embedded devices due to its compactness.
Required parameters
Initialise the parameters involved in model training and data preparation
batch_size = 2 # Размер батча для обучения
num_classes = 2 # Количество классов в задаче классификации
lr = 1e-4 # Скорость обучения (learning rate)
Epochs = 15 # Количество эпох для обучения
Classes = 1:num_classes; # Список индексов классов, например, 1 и 2
Creating datasets
First of all we need to prepare the data for training the network. We need to perform and construct a continuous wavelet transform for the signals to get its time-frequency characteristics. Wavelets are "compressed" to localise short-term bursts with high temporal accuracy, and "stretched" to capture smooth changes in the signal structure.
The auxiliary function save_wavelet_images
obtains a continuous wavelet transform (CWT)
for each radar signal, converts the result into a format compatible with computer vision models, and saves the spectrograms as images.
Initialise several auxiliary functions
# Функция для нормализации значений
function rescale(img)
min_val = minimum(img)
max_val = maximum(img)
return (img .- min_val) ./ (max_val - min_val)
end
# Применение colormap jet и преобразование в 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
# Преобразование непрерывного вейвлет-преобразования в изображение
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;
function save_wavelet_images(features_matrix, wavelet_filter, save_path, is_train=true)
# Определение количества примеров для каждого класса
class1_count = is_train ? 75 : 25 # Количество примеров для класса cone
class2_count = size(features_matrix, 1) - class1_count # Количество примеров для класса cylinder
println(class1_count)
# Создание папок для каждого класса
cone_path = joinpath(save_path, "cone") # cone — класс 1
cylinder_path = joinpath(save_path, "cylinder") # cylinder — класс 2
mkpath(cone_path)
mkpath(cylinder_path)
# Обработка строк матрицы для класса cylinder
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
# Обработка строк матрицы для класса cone
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;
The object c
is a customisable wavelet transformer based on the Morlet wavelet
c = wavelet(Morlet(π), averagingType=NoAve(), β=1);
Let's get the image database for training the neural network
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 obtained image
i = Images.load("$(@__DIR__)/New_imgs/train/cylinder/sample_2.png")
Initialise the function for data augmentation: it is responsible for converting images to size 224x224 and converting data into tensors
function Augment_func(img)
resized_img = imresize(img, 224, 224) #Изменение размеров изображения до 224х224
tensor_image = channelview(resized_img); #Представление данных в виде тензора
permutted_tensor = permutedims(tensor_image, (2, 3, 1)); #Изменение порядка размерности до формата (H, W, C)
permutted_tensor = Float32.(permutted_tensor) #Преобразование в тип Float32
return permutted_tensor
end;
The function Create_dataset
performs the creation of training datasets by processing the directories where the images are located.
function Create_dataset(path)
img_train = []
img_test = []
label_train = []
label_test = []
train_path = joinpath(path, "train");
test_path = joinpath(path, "test");
# Функция для обработки изображений в заданной директории
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
# Обработка папки train
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
# Обработка папки test
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 code cell let's execute the function to create training and test sets
path_to_data = "$(@__DIR__)/New_imgs"
img_train, img_test, label_train, label_test = Create_dataset(path_to_data);
Create a DataLoader that feeds images to the model input in batches. Transfer them to GPU
Important note: the model is trained on GPU, as it speeds up the learning process many times. If you need to use the GPU, contact the managers and they will give you access to the GPU. In the working directory will lie the weights of the already pre-trained network, transferred to CPU. After training the main network, you can view the network in CPU format by loading the corresponding weights into the model
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"
Preparing for training
Initialise our model by transferring it to the GPU
Net = SqueezeNet(; pretrain=false,
nclasses = num_classes) |>gpu;
Initialise the optimiser, the loss function
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 per epoch. The function train_one_epoch
performs model training on a single epoch by traversing all data batches from the loader Loader
. This function will later be used to train the model LSTM
. This function has a parameter type_model
, which determines whether we are training a convolutional or recurrent model
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)) # Рассчитываем градиенты
elseif type_model == "Recurrent"
TSamples += size(y, 2)
gs = gradient(() -> loss_function(x, y), Flux.params(model)) # Рассчитываем градиенты
end
Flux.update!(Optimizer, Flux.params(model), gs) # Обновляем оптимизатор
y_pred = model(x) # Делаем предсказание модели
# Далее вычисляем точность и ошибку нашей моделе на эпохе
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;
Starting the SqueezeNet training process
@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
Save our trained model
mkdir("$(@__DIR__)/models")
cpu(Net)
@save "$(@__DIR__)/models/SNET.bson" Net
Evaluating the SqueezeNet trained model
Let's evaluate the trained model. The function evaluate_model_accuracy
is responsible for calculating the accuracy of the model
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
# Накопление потерь
total_loss += type_model == "Conv" ? loss_function(x, onehotbatch(y, classes)) : loss_function(x, y)
# Предсказания и вычисление точности
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
# Вычисление точности
accuracy = 100.0 * correct_predictions / total_samples
return accuracy, all_preds, True_labels
end;
accuracy_score_Snet, all_predsSnet, true_predS = evaluate_model_accuracy(test_loader_Snet, Net, Classes, lossSnet, "Conv");
println("Accuracy trained model:", accuracy_score_Snet, "%")
As can be seen 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 concrete example of what the model predicts
Let's build the error matrix using the function plot_confusion_matrix
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
Loading an image from the test dataset
path = "$(@__DIR__)/New_imgs/test/cone/sample_14.png";
img = Images.load(path) # Загружаем изображение
img_aug = Augment_func(img);
img_res = reshape(img_aug, size(img_aug, 1), size(img_aug, 2), size(img_aug, 3), 1);
Load model weights
model_data = BSON.load("$(@__DIR__)/models/SNET.bson")
snet_cpu = model_data[:Net] |> cpu;
Making predictions
y_pred = (snet_cpu(img_res))
pred = onecold(y_pred, Classes)
# pred = cpu(preds) # Переносим предсказания на CPU
predicted_class_name = class_names[pred] # Получаем название предсказанного класса
println("Предсказанный класс: $predicted_class_name")
So, the model has fulfilled its task.
LSTM
The final section of this example describes the LSTM workflow. First, the LSTM levels are defined:
Parameter Initialisation
Initialise the parameters involved in model training and data preparation
MaxEpochs = 50;
BatchSize = 100;
learningrate = 0.01;
n_features = 1;
num_classes = 2;
Data acquisition
The attributes that are fed to the input were defined at the beginning of the script. Labels will be slightly redefined
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]);
The data is then cast to the form that the LSTM network requires as input
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)
Let's bring the training and test data to the type DataLoader
and translate them to GPU
Important note: the model is trained on GPU as it speeds up the learning process many times over. If you need to use GPU - contact the managers, you will be allocated access to GPU. In the working directory will lie the weights of the already pre-trained network, transferred to CPU. After training the main network, you can view the network in CPU format by loading the corresponding weights into the model
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);
Model initialisation
Initialise the model we are going to train. In this example, our model is a chain of layers connected to each other
model_lstm = Chain(
LSTM(n_features, 100),
x -> x[:, end, :],
Dense(100, num_classes),
Flux.softmax) |> gpu;
Initialise the optimiser, the loss function
optLSTM = Flux.Adam(learningrate, (0.9, 0.99));
lossLSTM(x, y) = Flux.Losses.crossentropy(model_lstm(x), y);
Training
Next comes the training cycle for the model
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")
# Вычисление точности
accuracy = 100.0 * correct_predictions / total_samples
println("Epoch $epoch, Loss: $(total_loss), Accuracy: $(accuracy)%")
end
Saving the model
cpu(model_lstm)
@save "$(@__DIR__)/models/lstm.bson" model_lstm
Evaluating the trained model
Let's evaluate our model by computing the accuracy on a test dataset
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, "%")
Now let's construct an error matrix for visual evaluation of the model
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
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);
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)
# Вывод результата
println("Random Sample Index: $random_index")
println("True Label: $random_label")
println("Predicted Probabilities: $predicted_probs")
println("Predicted Class: $predicted_class")
As can be seen from the results above, the model correctly classified the instance given to it
Conclusion
This example presented a workflow for performing radar target classification using machine learning and deep learning techniques. Although this example used synthesised data for training and testing, it can be easily extended to incorporate real radar results.