Распознавание азбуки Морзе с помощью нейронной сети

в 19:33, , рубрики: CW, ham radio, keras, MLP, python, азбука Морзе, Алгоритмы, искусственный интеллект, Научно-популярное, нейросеть, Программирование, радиолюбители

В процессе изучения нейронных сетей возникла мысль, как бы применить их для чего-то практически интересного, и не столь заезженного и тривиального, как готовые датасеты от MNIST. Например, почему бы не распознавать азбуку Морзе.

Распознавание азбуки Морзе с помощью нейронной сети - 1

Сказано, сделано. Для тех кому интересно, как с нуля создать работающий декодер CW, подробности под катом.

Повторить описанные ниже эксперименты может любой, для этого даже не нужно иметь радиоприемник. Все исходные файлы были записаны через websdr, где легко можно услышать радиолюбителей, например на частотах 7 и 14МГц. Там же есть кнопка Record, с помощью которой любой сигнал можно записать в формате wav.

Выделение сигнала из записи

Чтобы нейросеть могла распознать символы азбуки Морзе, сначала их надо выделить из исходной записи.
Загрузим данные из wav-файла и выведем его на экран.

from scipy.io import wavfile
import matplotlib.pyplot as plt

file_name = "websdr_recording_2019-08-17T16_26_52Z_14026.0kHz.wav"
fs, data = wavfile.read(file_name)

plt.plot(data)
plt.show()

Если все было сделано правильно, мы увидим что-то типа такого:

Распознавание азбуки Морзе с помощью нейронной сети - 2

Исторически, сигнал азбуки Морзе это простейший вид модуляции, которую можно придумать — тон либо есть, либо его нет. Поэтому в записи может быть одновременно несколько сигналов, и они не мешают друг другу.

Распознавание азбуки Морзе с помощью нейронной сети - 3

При записи сигналов CW я устанавливал частоту на 1КГц ниже и режим верхней боковой полосы (Upper Side Band), так что интересующий нас сигнал всегда находится в записи на частоте 1КГц. Выделим его с помощью полосового фильтра (фильтр Баттерворта).

from scipy.signal import butter, lfilter, hilbert

def butter_bandpass(lowcut, highcut, fs, order=5):
    nyq = 0.5 * fs
    low = lowcut / nyq
    high = highcut / nyq
    b, a = butter(order, [low, high], btype='band')
    return b, a

def butter_bandpass_filter(data, lowcut, highcut, fs, order=5):
    b, a = butter_bandpass(lowcut, highcut, fs, order)
    y = lfilter(b, a, data)
    return y

cw_freq = 1000
cw_width_hz = 50
data_filtered = butter_bandpass_filter(data, cw_freq - cw_width_hz, cw_freq + cw_width_hz, fs, order=5)

К получившемуся сигналу применяем преобразование Гильберта чтобы получить огибающую.

def hilbert_envelope(data):
    analytical_signal = hilbert(data)
    amplitude_envelope = np.abs(analytical_signal)
    return amplitude_envelope

y_env = hilbert_envelope(data_filtered)

В результате получаем вполне узнаваемый сигнал кода Морзе:

Распознавание азбуки Морзе с помощью нейронной сети - 4

Следующей задачей является выделение отдельных символов. Сложность тут в том, что сигналы могут быть разного уровня — как видно на картинке, из-за особенностей распространения в атмосфере, уровень сигнала «плавает», он может затухнуть и усилиться вновь. Так что просто обрезать данные по некоторому уровню было бы недостаточно. Используем moving average («скользящее среднее») и фильтр низких частот чтобы получить сильно сглаженное текущее среднее значение сигнала.

def moving_average(a, n=3):
    ret = np.cumsum(a, dtype=float)
    ret[n:] = ret[n:] - ret[:-n]
    return ret[n - 1:] / n

def butter_lowpass_filter(data, cutOff, fs, order=5):
    b, a = butter_lowpass(cutOff, fs, order=order)
    y = lfilter(b, a, data)
    return y

ma_size = 5000
y_env2 = y_env  # butter_lowpass_filter(y_env, 20, fs)
y_ma = moving_average(y_env2, n=ma_size)  # butter_lowpass_filter(y_env, 1, fs)
y_ma2 = butter_lowpass_filter(y_ma, 2, fs)
# Enlarge array from right to the original size
y_ma3 = np.pad(y_ma2, (0, ma_size-1), 'mean')

Как можно видеть из картинки, результат вполне адекватный изменению сигнала:

Распознавание азбуки Морзе с помощью нейронной сети - 5

И наконец, последнее: получим битовый массив, показывающий наличие или отсутствие сигнала — считаем сигнал «единицей», если его уровень выше среднего.

y_normalized = y_ma3 < y_env2
y_normalized2 = y_normalized.astype("int16")

Мы перешли от шумного и неодинакового по уровню входного сигнала к заметно более удобному для обработки сигналу цифровому.

Распознавание азбуки Морзе с помощью нейронной сети - 6

Выделение символов

Следующая задача — выделить отдельные символы, для этого нужно знать скорость передачи. Существуют определенные правила соотношения длительности точек, тире и пауз в азбуке Морзе (подробнее тут), для упрощения я просто задаю длительность минимальной паузы в миллисекундах. Вообще, скорость может варьироваться даже в пределах одной записи (в радиопередаче участвуют минимум два абонента, настройки передатчиков которых могут отличаться). Скорость также может сильно отличаться для разных записей — опытный радист может передавать в 2-3 раза быстрее, чем начинающий.

Дальше все просто, код не претендует на красоту и изящество, но вполне работает. Выделяем подъем и спад сигналов, и в зависимости от длины, разделяем слова и символы.

Выделение символов

min_len = 0.05
symbols = []
pos_start, pos_end, sym_start = -1, -1, -1
data_mask = np.zeros_like(y_env2)  # For debugging
pause_min = int(min_len*fs)
sym_min, sym_max = 0, 10*min_len
margin = int(min_len*fs)
for p in range(len(y_normalized2) - 1):
    if y_normalized2[p] < 0.5 and y_normalized2[p+1] > 0.5:
        # Signal rize
        pause_len = p - pos_end
        if pause_len > pause_min:
            # Save previous symbol if exist
            if sym_start != -1 and pos_end != -1:
                sym_len = (pos_end - pos_start)/fs
                if sym_len > sym_min and sym_len < sym_max:
                    # print("Code found: %d - %d, %fs" % (sym_start, pos_end, (pos_end - pos_start) / fs))
                    data_out = y_env2[sym_start - margin:pos_end + margin]
                    symbols.append(data_out)
                    data_mask[sym_start:pos_end] = 1
                    # Add empty symbol at the word end
                    if pause_len > 3*pause_min:
                        symbols.append(np.array([]))
                        data_mask[pos_end:p] = 0.4
            # New symbol started
            sym_start = p
        pos_start = p

    if y_normalized2[p] > 0.5 and y_normalized2[p+1] < 0.5:
        # Signal fall
        pos_end = p

Это временное решение, т.к. в идеале скорость нужно определять динамически.

На картинке зеленой линией показана огибающая выделенных символов и слов.

Распознавание азбуки Морзе с помощью нейронной сети - 7

В результате работы программы мы получаем список, каждый элемент которого представляет собой отдельный символ, выглядит это примерно так.

Распознавание азбуки Морзе с помощью нейронной сети - 8

Эти данные уже вполне достаточны, чтобы обрабатывать и распознавать их нейросетью.

Текст получается достаточно длинным, так что продолжение (оно же окончание) во второй части.

Автор: DmitrySpb79

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js