Опыт применения глубокого обучения для идентификации видов цифровой модуляции по сырым I/Q отсчетам (Keras)
ВВЕДЕНИЕ
До недавнего времени одним из популярных классических алгоритмов распознавания модуляций является метод, основанный на статистической обработке кумулянтов и гипотезе максимального правдоподобия. Использование такого метода достаточно трудоемко и требует достаточно глубоких экспертных знаний в предметной области.
В далеком 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.
Аргументированная и обоснованная критика приветствуется)))
Используемые источники: