ВВЕДЕНИЕ
До недавнего времени одним из популярных классических алгоритмов распознавания модуляций является метод, основанный на статистической обработке кумулянтов и гипотезе максимального правдоподобия. Использование такого метода достаточно трудоемко и требует достаточно глубоких экспертных знаний в предметной области.
В далеком 2016 году пионер применения методов машинного обучения в обработке радиосигналов Timothy J. O’Shea предложил нейронную сеть (НС) с использованием двумерных сверточных слоев [1], которые хорошо себя показали при обработке изображений. В своей статье [2] O’Shea утверждает, что такая НС может конкурировать с классическими методами автоматического распознавания модуляции (АРМ), особенно при низком отношении сигнал/шум (SNR). После получения некоторого опыта в работе с нейронками захотелось проверить заявленную возможность классификации таких сигналов.
Применение машинного обучения с использованием НС позволяет производить классификацию видов модуляции радиосигналов без глубоких знаний в области DSP. Для создания классификатора необходимо сформировать массив данных (Dataset) обучения, в качестве которых используем сырые синфазные и квадратурные I/Q отсчеты радиосигнала. Далее следует подобрать архитектуру сети и провести обучение. Результатом обучения будет файл полученных весов и файл описания архитектуры сети. При обучении сети обычно используют машину с большой вычислительной мощностью, а полученный результат обучения можно применять на простой машине.
Попробуем проверить подход глубокого обучения на соответствие минимальной жизнеспособности классификации для девяти цифровых видов модуляции: QPSK, OQPSK, P\4QPSK, QAM-16, QAM-32, QAM-64, QAM-128, QAM256 и QAM512.
Статья состоит из следующих основных частей:
1. Создание массива данных Dataset
2. Архитектура нейронной сети
3. Обучение, тестирование, эксперименты
4. Выводы
1. Создание Dataset
Авторский Dataset RADIOML 2016.10A [3] состоял из 8 цифровых и 3 аналоговых нормированных сигналов с шагом уровня SNR в 1 dB. Наш массив данных будет состоять только из 9 цифровых сигналов, без предварительного нормирования, по 20 файлов каждого вида модуляции, с различной символьной скоростью, частотой дискретизации, разным уровнем шума и размером примерно по 1 Мбайт.
Для этого использовались следующие источники:
— векторный генератор,
— реальные записи сигналов с разным уровнем SNR,
— специальная программа синтеза I/Q сигналов.
Приёмная часть состояла из конвектора в ПЧ 140 МГц и модуля ADC+FPGA, выходной формат данных 2 по 16 бит на один I/Q отсчет. Для создания обучающего массива и устранения инвариантности сдвига применялась аугментация в виде нарезки семплов размером 2 х 128 с шагом в один отсчет и сдвигом начала в файле на 8192 байт. Увеличение размера семпла более 2 х 128 не дало значимого прироста в точности.
В качестве проверочных данных будет использоваться массив от начала до 8192 байта, и в обучении он не будет использоваться. Суммарный размер Dataset получился X =(5400000,2,128) семплов или по 600000 на каждый вид модуляции. Массив меток Y =(5400000,9) сформирован в формате ohe (One Hot Encoder) при помощи утилиты to_categoricall от Keras.
# загружаем необходимые библиотеки import pandas as pd from tensorflow.keras import utils #Используем для to_categoricall import numpy as np #Библиотека работы с массивами import os #Для работы с файлами from sklearn.model_selection import train_test_split # добавляем библиотеку для разбивки # АУГМЕНТАЦИЯ, делаем нарезку шагом 1 отсчет, семплы по 128 отсчетов Mode_Type9 = ["QPSK","OQPSK","P4QPSK","QAM16","QAM32","QAM64","QAM128","QAM256","QAM512"] X_ds = [] # массив X Y_ds = [] # метки Y for i in range(len(Mode_Type9)): #проходим по всем типам модуляции md = Mode_Type9[i] #берём текущий тип модуляции for filename in os.listdir("D:/ASD/"+ md): #проходим по файлам папки name = "D:/ASD/"+md+"/"+filename # получаем полный путь к файлу dtt = np.fromfile(name, dtype = np.int16, count = 98304,offset = 8192).astype(np.float16) # считываем count for n in range(0, 120000, 4): # берем с шагом по 4 байт – это 1 отсчетI\Q dd1 = np.frombuffer(dtt, dtype = np.float16, count = 256, offset = n) # считываем по 1 семплу X_ds.append(np.reshape(dd1, (-1,2)).T) # расщепляем на I и Q, транспанируем-Т и кидаем в массив X_ds Y_ds.append(to_categorical(i, len(Mode_Type9))) # кидаем в массив меток Y_ds X_ds = np.array(X_ds) # переводим в нампи массив Y_ds = np.array(Y_ds) print(X_ds.shape) # проверяем формат массива Х= (5400000, 2, 128) print(Y_ds.shape) # проверяем формат меток Y= (5400000, 9)
Далее при помощи утилиты train_test_split от Scikit-learn разбиваем полученный массив на train и test выборки в пропорции 70 на 30, добавляем размерность и сохраняем в NumPy массив. Такая разбивка не дает точно сбалансированные массивы по отдельным классам и незначительная разбалансировка не повлияла на точность.
# test_size=0.3 – для теста будет выделено 30% от тренировочных данных # shuffle=True - перемешать данные # x_train - данные для обучения # x_test - данные для проверки # y_train - правильные ответы – метки для обучения, формат OHE # y_test - правильные ответы – метки для проверки, формат OHE x_train, x_test, y_train, y_test = train_test_split(X_ds, Y_ds, test_size=0.3, shuffle=True) print (x_train.shape) # (3780000, 2, 128) проверка формата print (x_test.shape) # (1620000, 2, 128) print (y_train.shape) # (3780000, 9) print (y_test.shape) # (1620000, 9) # добавляем размерность x_train = x_train.reshape(x_train.shape[0], 2, 128,1) x_test = x_test.reshape(x_test.shape[0], 2, 128,1) # Сохраняем X_train и X_test np.save("D:/DSET9/Signal_test", x_test) np.save("D:/DSET9/Signal_train", x_train) # Сохраняем Y_train и Y_test np.save("D:/DSET9/test_label", y_test) np.save("D:/DSET9/train_label", y_train)
2. Архитектура нейронной сети
Наша НС является сетью прямого распространения, относится к типу обучения с учителем (supervised learning) и похожа на VGG-16 с добавлением на входе слоя нормализации BatchNormalization для устранения ковариационного сдвига. Далее применены три сдвоенных двумерных сверточных слоя Conv2D со следующим порядком фильтров 32, 64, 128 с размером окна (1,3) и (2,3) для первого слоя и (2,3) для всех остальных. Дальнейшее увеличение размера ядра свертки и количества сверточных слоев не дает существенного улучшения качества распознавания. Функция активации – линейный выпрямитель relu. Затем проводились эксперименты с плотным слоем Dense, было обнаружено, что увеличение количества нейронов сверх 256 не улучшает производительность. Для устранения эффекта переобучения добавлялись параметры регуляризации Dropout до оптимальных значений в 20% для первого и второго и 30% третьего слоя Conv2D. После плотного слоя Dense так же добавлен слой Dropout в 50 %.
Используем стандартный подход для классификации, в качестве функции ошибки используем «categorical_crossentropy» (категориальную кросс-энтропию), оптимизатор Adam с шагом 0.001, метрику «accuracy».
# загружаем необходимые библиотеки from tensorflow.keras.models import Sequential #Сеть прямого распространения #Базовые слои для свёрточных сетей from tensorflow.keras.layers import Dense, Conv2D, MaxPooling2D, Flatten, Dropout, BatchNormalization from tensorflow.keras.optimizers import Adam # оптимизатор from tensorflow.keras.preprocessing import image #Для отрисовки изображений from sklearn.preprocessing import LabelEncoder, StandardScaler # Функции для нормализации данных from sklearn import preprocessing # Пакет предварительной обработки данных import numpy as np #Библиотека работы с массивами import matplotlib.pyplot as plt #Для отрисовки графиков import math # математика import os #Для работы с файлами import pandas as pd %matplotlib inline #============= Загружаем DataSet train\test выборки============ # Train data Xtrain = np.load('D:/DataSet9/Signal_train.npy') Ytrain = np.load('D:/DataSet9/train_label.npy') # Test data Xtest = np.load('D:/DataSet9/Signal_test.npy') Ytest = np.load('D:/DataSet9/test_label.npy') # =================СЕТЬ============ #задаём batch_size batch_size = 512 #Создаем последовательную модель model = Sequential() model.add(BatchNormalization(input_shape=(2, 128, 1))) #Первый сверточный слой model.add(Conv2D(32, (1, 3), padding='same', activation='relu')) model.add(Conv2D(32, (2, 3), padding='same', activation='relu')) model.add(MaxPooling2D(pool_size=(1, 2))) model.add(Dropout(0.2)) #Второй сверточный слой model.add(Conv2D(64, (2, 3), padding='same', activation='relu')) model.add(Conv2D(64, (2, 3), padding='same', activation='relu')) model.add(MaxPooling2D(pool_size=(2, 2))) #Слой регуляризации Dropout model.add(Dropout(0.2)) #Третий сверточный слой model.add(Conv2D(128, (2, 3), padding='same', activation='relu')) model.add(Conv2D(128, (2, 3), padding='same', activation='relu')) model.add(MaxPooling2D(pool_size=(1, 2))) #Слой регуляризации Dropout model.add(Dropout(0.3)) model.add(Flatten()) #Полносвязный слой для классификации model.add(Dense(256, activation='relu')) model.add(Dropout(0.5)) #Выходной полносвязный слой model.add(Dense(9, activation='softmax')) # #Компилируем сеть model.compile(loss="categorical_crossentropy", optimizer=Adam(lr=1e-3), metrics=["accuracy"])
3. Обучение, тестирование, эксперименты
Методом fit запускаем обучение, на вход сети поступают сегменты Xtrain и Ytrain размером batch_size и последовательно прогоняются через все слои обучения. Наша обучающая выборка состоит из 7383 батчей (batch) по 512 семплов, что соответствует одной эпохе. Одна эпоха — это один раз полностью пройденная моделью обучающая выборка, в нашем случае время обработки 1 эпохи заняло около 23 минут. Всего модель обучалась на 10 эпохах.
#Обучаем сеть history = model.fit(Xtrain, Ytrain, batch_size=batch_size, epochs=10, validation_data=(Xtest, Ytest), verbose=1) Epoch 1/10 7383/7383 [==============================] - 1327s 180ms/step - loss: 0.2671 - accuracy: 0.8936 - val_loss: 0.0899 - val_accuracy: 0.9636 Epoch 2/10 7383/7383 [==============================] - 1339s 181ms/step - loss: 0.1544 - accuracy: 0.9391 - val_loss: 0.0520 - val_accuracy: 0.9779 Epoch 3/10 7383/7383 [==============================] - 1402s 190ms/step - loss: 0.1156 - accuracy: 0.9548 - val_loss: 0.0369 - val_accuracy: 0.9843 Epoch 4/10 7383/7383 [==============================] - 1392s 189ms/step - loss: 0.0947 - accuracy: 0.9640 - val_loss: 0.0273 - val_accuracy: 0.9883 Epoch 5/10 7383/7383 [==============================] - 1355s 184ms/step - loss: 0.0808 - accuracy: 0.9693 - val_loss: 0.0213 - val_accuracy: 0.9905 Epoch 6/10 7383/7383 [==============================] - 1346s 182ms/step - loss: 0.0716 - accuracy: 0.9732 - val_loss: 0.0214 - val_accuracy: 0.9902 Epoch 7/10 7383/7383 [==============================] - 1410s 191ms/step - loss: 0.0672 - accuracy: 0.9749 - val_loss: 0.0154 - val_accuracy: 0.9938 Epoch 8/10 7383/7383 [==============================] - 1389s 188ms/step - loss: 0.0639 - accuracy: 0.9766 - val_loss: 0.0195 - val_accuracy: 0.9908 Epoch 9/10 7383/7383 [==============================] - 1340s 182ms/step - loss: 0.0567 - accuracy: 0.9795 - val_loss: 0.0390 - val_accuracy: 0.9860 Epoch 10/10 7383/7383 [==============================] - 1375s 186ms/step - loss: 0.0538 - accuracy: 0.9811 - val_loss: 0.0116 - val_accuracy: 0.9959 # После обучения получили точность на валидационной (тестовой) выборке в 99.59 %
Делаем проверку
Создаем проверочный массив PROV и метку Prov_Metka аналогично обучающему массиву, берем не использованные при обучении данные из файлов до 8192 байта, получаем массивы (252000, 2, 128, 1) и (252000, 9) соответственно. Методом evaluate вычисляем долю верно распознанных семплов нашей обученной модели.
# Загружаем проверку PROV = np.load('D:/DataSet9/PROVERKA/Signal_prov.npy') Prov_Metka = np.load("D:/DataSet9/PROVERKA/Label_prov.npy") print(PROV.shape) print(Prov_Metka.shape) # Вычисляем результаты сети на PROVERKA наборе scores = model.evaluate(PROV, Prov_Metka, verbose=1) print(scores) print("Доля верных ответов на данных PROV, в процентах: ", round(scores[1] * 100, 4), "%", sep="") (252000, 2, 128, 1) (252000, 9) 7875/7875 [==============================] - 41s 5ms/step - loss: 0.0519 - accuracy: 0.9838 [0.051874879747629166, 0.9837777614593506] # Доля верных ответов на данных PROV, в процентах: 98.38%, ошибка: 0.0519
Сделаем матрицу распределения ошибок классификации. Из массива PROV будем «откусывать» по 100 семплов из каждого файла и прогонять через predict (20 файлов по 100 = 2000 семплов) для каждого вида модуляции.
|
qpsk |
oqpsk |
p/4qpsk |
qam16 |
qam32 |
qam64 |
qam128 |
qam256 |
qam512 |
qpsk |
2000 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
oqpsk |
0 |
2000 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
p/4qpsk |
0 |
0 |
2000 |
0 |
0 |
0 |
0 |
0 |
0 |
qam16 |
0 |
0 |
0 |
1954 |
0 |
44 |
0 |
2 |
0 |
qam32 |
0 |
0 |
0 |
0 |
1965 |
10 |
25 |
0 |
0 |
qam64 |
0 |
0 |
0 |
7 |
0 |
1993 |
0 |
0 |
0 |
qam128 |
0 |
0 |
0 |
0 |
14 |
0 |
1903 |
0 |
83 |
qam256 |
0 |
0 |
0 |
0 |
7 |
15 |
0 |
1959 |
19 |
qam512 |
0 |
0 |
0 |
0 |
0 |
0 |
107 |
0 |
1893 |
При анализе таблицы видно, что ошибки классификации появляются, если один класс является подмножеством другого. Например, модуляция QAM128 имеет вид созвездия «крест» и входит в подмножество модуляции QAM512 и если оба сигнала имеют одинаковую частоту дискретизации и символьную скорость, то сеть иногда их путает.
Оценка качества классификации с использованием утилиты classification_report от Scikit-learn
|
precision |
recall |
f1-score |
support |
qpsk |
1.00 |
1.00 |
1.00 |
2000 |
oqpsk |
1.00 |
1.00 |
1.00 |
2000 |
p/4qpsk |
1.00 |
1.00 |
1.00 |
2000 |
qam16 |
1.00 |
0.98 |
0.99 |
2000 |
qam32 |
0.99 |
0.98 |
0.99 |
2000 |
qam64 |
0.97 |
1.00 |
0.98 |
2000 |
qam128 |
0.93 |
0.95 |
0.94 |
2000 |
qam256 |
1.00 |
0.98 |
0.99 |
2000 |
qam512 |
0.95 |
0.94 |
0.95 |
2000 |
Прогоним через predict по 200 синтезированных I/Q семплов QAM32 с символьной скоростью отличной от сигналов в обучающем наборе.
mode |
qpsk |
oqpsk |
p/4qpsk |
qam16 |
qam32 |
qam64 |
qam128 |
qam256 |
qan512 |
qam32_12 |
0 |
0 |
0 |
0 |
200 |
0 |
0 |
0 |
0 |
qam32_18 |
0 |
0 |
0 |
0 |
187 |
0 |
13 |
0 |
0 |
qam32_32 |
0 |
0 |
0 |
0 |
200 |
0 |
0 |
0 |
0 |
qam32_40 |
0 |
0 |
0 |
0 |
1 |
177 |
0 |
22 |
0 |
qam32_46 |
0 |
0 |
0 |
1 |
0 |
199 |
0 |
0 |
0 |
qam32_50 |
0 |
0 |
0 |
0 |
200 |
0 |
0 |
0 |
0 |
Тестовые сигналы, у которых в обучающем наборе были синтезированные сигналы с близкой символьной скоростью, показали хорошую точность. В обучающей выборке в диапазоне символьных скоростей 40-46 МБод для QAM32 были реальные эфирные данные с канальными шумами и эффектами многолучевого распространения сигнала, а для сигнала QAM64 только синтезированные сигналы. Поэтому можно предположить, что сеть ошибочно распознала QAM64 по совокупности признаков в обучающем наборе.
4. Вывод
Рассмотренная сеть CNN соответствует минимальному требованию классификации, но для получения качественного классификатора требуется очень большой массив (Dataset) обучения в котором должны отражаться все эффекты многолучевого распространения сигнала реальных каналов связи, что сделать весьма затруднительно. Для эффективной классификации необходима архитектура сети, которая выделяет только «семантическую соль» и не сильно требовательна к обучающему массиву. Поэтому в последнее время лидируют различные гибридные сети, например сеть с многоуровневым вниманием MCBL (Multilevel attention CNN Bi-LSTM) [4] от китайской команды 63-rd Research Institute, National University of Defense Technology, Nanjing.
Аргументированная и обоснованная критика приветствуется)))
Используемые источники:
ссылка на оригинал статьи https://habr.com/ru/company/stc_spb/blog/712362/
Добавить комментарий