Классификация сигналов сердцебиения на электрокардиограмме (ЭКГ)

Имея набор изображений полос ЭКГ, обученный человеческий глаз может найти местоположение V-биения на каждом изображении. На графике ЭКГ регистрируется V-биение во время преждевременного сокращения желудочков в сердцебиении.

Может ли машина его распознать?

В этой статье объясняется, что я сделал, чтобы обучить модель машинного обучения определять форму V-биения. Но перед этим изображения необходимо предварительно обработать, чтобы определить местонахождение сигналов сердцебиения и извлечь данные. Имеется 540 изображений поездов (для обучения и проверки) и 180 тестовых изображений.

Изучите набор данных

Сначала я проверяю каждое изображение, чтобы увидеть, как выглядит полоса ЭКГ, используя OpenCV. Каждое изображение занимает около 20 секунд слева направо и имеет размер 7522 x 750 пикселей (визуализируйте это как координаты x и y) с третьим измерением для обозначения цвета RGB.

import cv2
image = cv2.imread('a3_45.png')
image.shape

Пожалуйста, обратитесь к кодам Python со встроенными комментариями на моем GitHub.

Ниже приведена простая функция построения графика для визуализации сигнала ЭКГ:

def show_graph(x_list, y_list, width, height):
    plt.figure(figsize = [width, height]) 
    plt.scatter(x_list, y_list, marker='.', s=5)
    plt.show()
    return

С помощью этой функции черные пиксели в файле изображения могут быть извлечены в список x-координат и соответствующий список y-координат:

x_list, y_list = [], []
for x in np.arange(0, 7522, 1):
    for y in np.arange(0, 750, 1):
        if np.all(image[y][x] == (0, 0, 0)):
            x_list.append(x)
            y_list.append(750-y)
show_graph(x_list, y_list, 18, 3)

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

print(image[0][0])   # coordinate format is [y][x]
print(image[749][7521])

Вместо стандартной последовательности красный-зеленый-синий (RGB) OpenCV использует массив десятичных кодов Синий-зеленый-красный, поэтому
[255, 0, 0] синий,
[0, 255, 0] зеленый,
[0, 0, 255] красный,
[255, 255, 255] белый,
[0, 0, 0 ] черный.

V-биения отмечены темно-зеленым цветом [0, 128, 0]. Я использовал функцию Пипетка в Microsoft Powerpoint, чтобы найти десятичный код цвета на изображении.

Набор данных TRAIN: обработка изображений, извлечение признаков

Ниже функция «locate_pos» используется для определения положения сигналов биений («V» темно-зеленым, «N» синим). Он сканирует от y_level = 42 до 100, чтобы извлечь первый пиксель указанного цвета, который является координатой x сигнала биений.

def locate_pos(image, color):
    position_list = []
    y_level = 42
    while len(position_list) == 0 and y_level < 100:
        x=100
        while x<7422:
            if np.all(image[y_level][x] == color):
                position_list.append(x)
                x += 25
            x += 1
        y_level+=2
    return position_list

После определения координаты x сигнала биений задается окно размером 160 на 750 пикселей для извлечения сигнала биений.

Для каждой координаты x функция «extract_feat» извлекает 1 координату y. Эти 160 y-координат становятся основными характеристиками модели машинного обучения.

def extract_feat(image, begin, end):
    x_list, y_list = [], [0]   # boundary padding add '0'
    for x in np.arange(begin, end, 1):
        x_list.append(x-begin)
        for y in np.arange(0, 750, 1):
            if np.all(image[y][x] == (0, 0, 0)):
                y_list.append(750-y)
                break
            if y==749:
                y_list.append(y_list[x-begin])
    y_list.pop(0)   # remove boundary padding '0'
    show_graph(x_list, y_list, 2, 2)
    return y_list

Feature Engineering: для создания новых полезных функций

Чтобы улучшить модель, были добавлены 16 дополнительных функций:
1. Разделить сигнал биений на 4 квадранта
2. Найти минимальное, максимальное, среднее, медианное значение для каждого квадранта.

Последовательность сигналов также может быть зафиксирована путем объединения 2 квадрантов, таким образом формируя дополнительные 12 функций:
1. Разделить сигнал биений на 4 квадранта
2. Объединить 2 последовательных квадранта, чтобы получилось 3 сегмента
3. Найдите минимум, максимум, среднее, медианное значение для каждого сегмента.

from statistics import mean, median
def find_stats(y_list, begin, end):
    temp = []
    for i in np.arange(begin, end, 1):
        temp.append(y_list[i])
    return [min(temp), mean(temp), median(temp), max(temp)]

После определения вышеупомянутой функции «find_stats» функция «extract_feat» может быть добавлена ​​для включения дополнительных функций из сводной статистики:

def extract_feat(image, begin, end):
    x_list, y_list = [], [0]   # boundary padding add '0'
    for x in np.arange(begin, end, 1):
        x_list.append(x-begin)
        for y in np.arange(0, 750, 1):
            if np.all(image[y][x] == (0, 0, 0)):
                y_list.append(750-y)
                break
            if y==749:
                y_list.append(y_list[x-begin])
    y_list.pop(0)   # remove boundary padding '0'
    
    y_list.extend(find_stats(y_list, 0, 40))   # quardrant 1
    y_list.extend(find_stats(y_list, 40, 80))   # quardrant 2
    y_list.extend(find_stats(y_list, 80, 120))   # quardrant 3
    y_list.extend(find_stats(y_list, 120, 160))   # quardrant 4
    y_list.extend(find_stats(y_list, 0, 80))   # segment 1
    y_list.extend(find_stats(y_list, 40, 120))   # segment 2
    y_list.extend(find_stats(y_list, 80, 160))   # segment 3
    
    return y_list

Моему верному ноутбуку потребовалось около получаса, чтобы извлечь элементы из 540 изображений поездов. Есть 1113 V-битов и 2098 других долей, помеченных как «N», таким образом, общие данные поезда содержат 3211 строк.

train_X, train_y = [], []
imagePaths = sorted(list(paths.list_images('train')))
for imagePath in imagePaths:
    image = cv2.imread(imagePath)
    position_list = locate_pos(image, (0, 128, 0)) # V
    count = len(position_list)
    for i in range(count):
        train_X.append(extract_feat(image, 
                position_list[i]-70, position_list[i]+90))
        train_y.append(1)
    position_list = locate_pos(image, (255, 0, 0)) # N
    for i in range(count):
        train_X.append(extract_feat(image, 
                position_list[i]-70, position_list[i]+90))
        train_y.append(0)

Модель: модель поезда / подгонки, гиперпараметр настройки

Я выбрал модель логистической регрессии, которая отлично подходит для бинарной классификации.

Пожалуйста, обратитесь к кодам Python со встроенными комментариями на моем GitHub.

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(train_X, train_y, test_size=.2, random_state=SEED)
logit = LogisticRegression(C = 0.001)
logit.fit(X_train, y_train)
y_pred = (logit.predict_proba(X_test)[:,1] >= 0.026)
print(f1_score(y_test, y_pred))
print(confusion_matrix(y_test, y_pred))

С разделением поезд / тест 80/20 модель была настроена с C = 0,001 и оптимальным порогом 0,026, используя F1-оценку в качестве метрики измерения. Достигнутая точность составляет 100% как для обучения, так и для проверки. Матрица путаницы выглядит действительно хорошо, поскольку все прогнозы верны.

С такой хорошей моделью было бы очень обидно ее потерять.

import pickle
pickle.dump(logit, open('logistic_model.pickle', 'wb'))

Набор данных TEST: обработка изображений, прогнозирование с использованием модели

Такая же обработка изображений применяется к тестовым изображениям. На этот раз синий "?" символы должны быть расположены для извлечения сигнала биений. Необходимо выделить и спрогнозировать около 5075 сигналов биений, и это заняло около 40 минут.

filename_list = []
location_list = []imagePaths = sorted(list(paths.list_images('test')))
for imagePath in imagePaths:
    image = cv2.imread(imagePath)
    filename_list.append(imagePath)
    position_list = locate_pos(image, (255, 0, 0))
    count = len(position_list)
    
    test_X = []
    if count > 0:
        for i in range(count):
            test_X.append(extract_feat(image, 
		position_list[i]-75, position_list[i]+85))
        y_pred = logit.predict(test_X)
        
    timing = []
    for i in range(len(y_pred)):
        if y_pred[i] == 1:
            timing.append((position_list[i]-36)*(20/7450))
    location_list.append(timing_list)

Большую часть времени я потратил на эту задачу, чтобы подготовить и обработать изображения, извлечь данные и преобразовать их в функции. Наконец, результаты теста сохраняются в файл csv:

df = pd.DataFrame()
df['filename'] = filename_list
df['location(sec)'] = location_list
df.to_csv('test_results.csv', index=False)

Заключение

Сигнал V-биений можно легко идентифицировать, если различные характеристики его формы можно преобразовать в полезное числовое / векторное представление.

Модели машинного обучения (ML) становятся все более распространенными в отрасли медицинских технологий, помогая нам выполнять трудоемкие задачи точно и более эффективно.

Коды Python для вышеупомянутого анализа доступны на моем GitHub, не стесняйтесь ссылаться на них.

Https://github.com/JNYH/ecg_vbeat

Спасибо за чтение.