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:
- Machine learning: Support Vector Machine (SVM).
- 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 are using
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
include("$(@__DIR__)/Install_packages.jl")
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
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.
# Допустим, ваш файл называется "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 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.
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);
Initialize the list containing the class names
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.
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
Initialization of the support vector model and its training
Next, we configure the support vector model by initializing 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 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)" , "%")
Evaluation of the trained model
Let's evaluate the trained model based 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, we will construct an error matrix to evaluate the classification quality of the model - function plot_confusion_matrix performs 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 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
batch_size = 2 # Размер батча для обучения
num_classes = 2 # Количество классов в задаче классификации
lr = 1e-4 # Скорость обучения (learning rate)
Epochs = 15 # Количество эпох для обучения
Classes = 1:num_classes; # Список индексов классов, например, 1 и 2
Creating datasets
First of all, it is necessary to prepare 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
# Функция для нормализации значений
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;
An object c It is a configurable wavelet converter based on the Morlaix wavelet
c = wavelet(Morlet(π), averagingType=NoAve(), β=1);
We will get a database of images for training a 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 resulting image
i = Images.load("$(@__DIR__)/New_imgs/train/cylinder/sample_2.png")
Initialize the function for data augmentation: it is responsible for reducing images to the size of 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;
Function Create_dataset creates 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 cell of the code, we will perform the function of creating training and test sets.
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.
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"
Preparation for training
Initialize our model by transferring it to the GPU
Net = SqueezeNet(; pretrain=false,
nclasses = num_classes) |>gpu;
Initialize the optimizer, 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 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
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;
Launching the SqueezeNet learning 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
Saving our trained model
mkdir("$(@__DIR__)/models")
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
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 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
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
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);
Loading the weights of the model
model_data = BSON.load("$(@__DIR__)/models/SNET.bson")
snet_cpu = model_data[:Net] |> cpu;
Making a prediction
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")
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
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
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.
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 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 it 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.
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.
model_lstm = Chain(
LSTM(n_features, 100),
x -> x[:, end, :],
Dense(100, num_classes),
Flux.softmax) |> gpu;
Initialize the optimizer, 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 of 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
Evaluation of the trained model
Let's evaluate our model by calculating 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 build 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 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.
