Классификация радиолокационных целей с использованием машинного обучения и глубокого обучения
В этом примере демонстрируются подходы к классификации данных радара с использованием методов машинного и глубокого обучения. Для решения задачи применяются следующие подходы:
- Машинное обучение: метод опорных векторов (SVM).
- Глубое Обучение: SqueezeNet, LSTM
Классификация объектов — важная задача для радиолокационных систем. В данном примере рассматривается задача определения, какой именно объект отражает сигнал радара — цилиндр или конус. Хотя в примере используются синтетические данные, этот подход можно применять и для реальных радиолокационных измерений.
Обучение SVM
Первых делом необходимо импортировать используемые пакеты
В файле install_packages.jl
находятся пакеты, которые необходимы для скрипта. Они добавляются в рабочее окружение. В файле import_packages
все установленные пакеты импортируются для скрипта. В ячейках ниже запускаем их выполнение
include("$(@__DIR__)/Install_packages.jl")
include("$(@__DIR__)/import_packages.jl")
init()
Загрузна данных
Данные, которые будут использоваться для обучения моделей, были взяты из демо примера по моделированию ЭПР радарных объектов.
Пропишем путь, в которым лежат данные
data_path = "$(@__DIR__)/gen_data.csv"
В CSV файле лежат данные по конусу и цилиндру. Первые 100 элементов - цилиндр, вторые 100 элементов - конус. Разобьем наши данные на тренировочный и тестовый датасет
# Допустим, ваш файл называется "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);
Код ниже выполняет извлечение признаков из временных рядов с использованием преобразования непрерывной вейвлет-функцией Морле. Сначала ко всем данным применяется вейвлет-преобразование с последующим расчетом абсолютных значений коэффициентов, чтобы оставить только амплитуды. Затем происходит усреднение этих амплитуд по временной оси, что позволяет снизить размерность признаков. После этого результат "сжимается" для удаления лишних измерений.
Все это делается для тренировочного и тестового наборов данных
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);
Инициализируем список, содержащий имена классов
class_names = ["Cylinder","Cone"]
Перед обучением классификатора данные необходимо преобразовать в формат, совместимый с моделью: тренировочные признаки преобразуются в табличный вид, а метки переводятся в категориальный тип.
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
Инициализация модели опорных векторов и ее обучение
Далее настраиваем модель опорных векторов, инициализируя параметры и кросс-валидацию
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);
Выполняем обучение модели с использованием кросс-валидации и рассчитываем точность на обучении
@info "Load model config, cross-valid"
cv_results = evaluate!(mach; resampling=cv, measures=[accuracy], verbosity=0) # Выполняем кросс-валидацию модели
println("Точность модели: $(cv_results.measurement[1] * 100)" , "%")
Оценка обученной модели
Оценим обученную модель на тестовых данных
@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), "%")
Далее построим матрицу ошибок для оценки качества классификации модели - функция plot_confusion_matrix
выполняет эту задачу
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)
Как можно увидеть из построенной матрицы ошибок, модель хорошо классифицирует конус, однако цилиндр частенько путает с конусом.
Обучение SqueezeNet
Далее обучим сеть глубокого обучения - SqueezeNet. SqueezeNet — компактная свёрточная нейронная сеть, предложенная в 2016 году, достигающая производительности AlexNet при значительно меньшем размере. Использует Fire-модули, которые включают слои squeeze (1x1 свёртки для уменьшения числа каналов) и expand (1x1 и 3x3 свёртки для восстановления размерности), что снижает количество параметров без потери качества. Подходит для встроенных устройств благодаря компактности.
Необходимые параметры
Инициализируем параметры, участвующие в обучении модели и подготовки данных
batch_size = 2 # Размер батча для обучения
num_classes = 2 # Количество классов в задаче классификации
lr = 1e-4 # Скорость обучения (learning rate)
Epochs = 15 # Количество эпох для обучения
Classes = 1:num_classes; # Список индексов классов, например, 1 и 2
Создание наборов данных
Прежде всего необходимо подготовить данные для обучения сети. Нужно выполнить и построить непрерывное вейвлет-преобразование для сигналов, чтобы получить его временно-частотные характеристики. Вейвлеты «сжимаются», чтобы локализовать кратковременные всплески с высокой временной точностью, и «растягиваются», чтобы уловить плавные изменения структуры сигнала.
Вспомогательная функция save_wavelet_images
получает непрерывное вейвлет-преобразование (CWT)
для каждого радиолокационного сигнала, преобразует результат в формат, совместимый с моделями компьютерного зрения, и сохраняет спектрограммы в виде изображений.
Инициализируем несколько вспомогательных функций
# Функция для нормализации значений
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;
Объект c
представляет собой настраиваемый вейвлет-преобразователь на основе Морле-вейвлета
c = wavelet(Morlet(π), averagingType=NoAve(), β=1);
Получим базу изображений, для обучения нейронной сети
save_wavelet_images(TrainFeatures_T, c, "$(@__DIR__)/New_imgs/train")
save_wavelet_images(TestFeatures_T, c, "$(@__DIR__)/New_imgs/test", false)
Посмотрим на экземпляр полученного изображения
i = Images.load("$(@__DIR__)/New_imgs/train/cylinder/sample_2.png")
Инициализируем функцию для аугментации данных: она отвечает за приведение изображений к размеру 224x224 и преобразование данных в тензоры
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;
Функция Create_dataset
выполняет создание обучающих наборов данных, обрабатывая директории, в которых находятся изображения.
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;
В следующей ячейке кода выполним функцию по созданию обучающаюх и тестовых наборов
path_to_data = "$(@__DIR__)/New_imgs"
img_train, img_test, label_train, label_test = Create_dataset(path_to_data);
Создаем DataLoader, которые подают изображения на вход модели пачками - батчами. Переводим их на GPU
Важное учтонение: модель обучается на GPU, поскольку это ускоряет процесс обучения многократно. Если вам необходимо использование GPU - свяжитесь с менеджерами, вам выделят доступ к GPU. В рабочей директории будут лежать веса уже предобученной сети, переведенной на CPU. После обучения основной сети вы можете посмотреть на сеть в формате CPU, подгрузив в модель соответсвующие веса
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"
Подготовка к обучению
Инициализируем нашу модель, переведя ее на GPU
Net = SqueezeNet(; pretrain=false,
nclasses = num_classes) |>gpu;
Инициализируем оптимизатор, функцию потерь
optimizer_Snet = Flux.Adam(lr, (0.9, 0.99));
lossSnet(x, y) = Flux.Losses.logitcrossentropy(Net(x), y);
Обучение SqueezeNet
Опишем функцию, которая отвечает за обучение модели на эпоху. Функция train_one_epoch
выполняет обучение модели на одной эпохе, проходя по всем батчам данных из загрузчика Loader
. В последствии эта функция будет использоваться для обучения модели LSTM
. У этой функции есть параметр type_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;
Запускаем процесс обучения SqueezeNet
@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
Сохраняем нашу обученную модель
mkdir("$(@__DIR__)/models")
cpu(Net)
@save "$(@__DIR__)/models/SNET.bson" Net
Оценка обученной модели SqueezeNet
Оценим обученную модель. Функция evaluate_model_accuracy
отвечает за вычисление точности модели
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, "%")
Как видно выше, точность модели - 100%. Это дает понять, что модель идеально разделяет два класса между собой. Посмотрим на конкретном примере то, что предсказывает модель
Построим матрицу ошибок, используя функцию 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)
Предсказание модели
Загружаем картинку из тестового набора данных
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);
Подгружаем веса модели
model_data = BSON.load("$(@__DIR__)/models/SNET.bson")
snet_cpu = model_data[:Net] |> cpu;
Делаем предсказание
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")
Сообственно, модель справилась со своей задачей.
LSTM
В заключительном разделе этого примера описывается рабочий процесс LSTM. Сначала определяются уровни LSTM:
Инициализация параметров
Инициализируем параметры, участвующие в обучении модели и подготовки данных
MaxEpochs = 50;
BatchSize = 100;
learningrate = 0.01;
n_features = 1;
num_classes = 2;
Сбор данных
Признаки, которые подаюся на вход были определены в начале скрипта. Лэйблы несколько переопределим
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]);
Далее данные приводятся к виду, который требует на входе сеть LSTM
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)
Приведем обучающие и тестовые данные в типу DataLoader
и переведем их на GPU
Важное учтонение: модель обучается на GPU, поскольку это ускоряет процесс обучения многократно. Если вам необходимо использование GPU - свяжитесь с менеджерами, вам выделят доступ к GPU. В рабочей директории будут лежать веса уже предобученной сети, переведенной на CPU. После обучения основной сети вы можете посмотреть на сеть в формате CPU, подгрузив в модель соответсвующие веса
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_lstm = Chain(
LSTM(n_features, 100),
x -> x[:, end, :],
Dense(100, num_classes),
Flux.softmax) |> gpu;
Инициализируем оптимизатор, функцию потерь
optLSTM = Flux.Adam(learningrate, (0.9, 0.99));
lossLSTM(x, y) = Flux.Losses.crossentropy(model_lstm(x), y);
Обучение
Далее идет цикл обучения модели
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
Сохранение модели
cpu(model_lstm)
@save "$(@__DIR__)/models/lstm.bson" model_lstm
Оценка обученной модели
Оценим нашу модель, вычислив точность на тестовом наборе данных
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, "%")
Теперь построим матрицу ошибок для визуальной оценки модели
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)
Протестируем модель на конкретном наблюдении
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")
Как видно из результатов выше, модель правильно классифицировала предоставленный ей экземпляр
Заключение
В этом примере представлен рабочий процесс для выполнения классификации радиолокационных целей с использованием методов машинного обучения и глубокого обучения. Хотя в этом примере для обучения и тестирования использовались синтезированные данные, его можно легко расширить, чтобы учесть реальные результаты работы радара.