Классификация рассеиваний времени вейвлета данных о фонокардиограмме

Этот пример показывает, как классифицировать человеческую фонокардиограмму (PCG) записи с помощью времени вейвлета, рассеявшись и классификатора машины вектора поддержки (SVM). Фонокардиограммы являются акустическими записями звуков, произведенных систолическими и диастолическими фазами основы. Выслушивание основы продолжает играть важную диагностическую роль в оценке сердечного здоровья. К сожалению, много областей мировых достаточных чисел отсутствия медперсонала, обученного в сердечном выслушивании. Соответственно, необходимо разработать надежные автоматизированные способы интерпретировать данные о фонокардиограмме.

Этот пример использует вейвлет, рассеивающийся в качестве экстрактора функции для классификации PCG. В рассеивании вейвлета данные распространены через серию вейвлета, преобразовывает, нелинейность и усреднение, чтобы произвести представления низкого отклонения данных. Эти представления низкого отклонения затем используются в качестве входных параметров к классификатору. Этим примером является бинарная проблема классификации, где каждая запись PCG является или "нормальной" или "аварийной".

Описание данных

Этот пример использует фонокардиограмму (PCG) данные, полученные от людей с нормальной и аварийной сердечной функцией. Набор данных состоит из 3 829 записей, 2575 от людей с нормальной сердечной функцией и 1 254 записями от людей с аварийной сердечной функцией. Каждая запись является 10 000 выборок долго и выбирается на уровне 2 кГц. Это представляет пять секунд данных о фонокардиограмме. Набор данных создается из данных об обучении и валидации, используемых в Вычислении PhysioNet в проблеме Кардиологии 2016 [1] [2].

Загрузите данные

Первый шаг должен загрузить данные из репозитория GitHub. Чтобы загрузить данные, нажмите Clone or download и выберите Download ZIP. Сохраните файл physionet_phonocardiogram-master в папке, где у вас есть разрешение записи. Инструкции для этого примера принимают, что вы загрузили файл на свою временную директорию, (tempdir в MATLAB™). Измените последующие инструкции для того, чтобы разархивировать и загрузить данные, если вы принимаете решение загрузить данные в папке, отличающейся от tempdir. Если вы знакомы с Git, можно загрузить последнюю версию инструментов (Git) и получить данные из системной командной строки с помощью git clone https://github.com/mathworks/physionet_phonocardiogram.

Файл physionet_phonocardiogram-master.zip содержит

  • PCG_Data.zip

  • README.md

и PCG_Data.zip содержит

  • heartSoundData.mat

  • extrafiles.mat

  • Modified_physionet_data.txt

  • License.txt.

heartSoundData.mat содержит данные и метки класса, используемые в этом примере. .txt файл, Modified_physionet_data.txt, требуется копированием PhysioNet политики и обеспечивает исходные приписывания для данных, а также описания того, как каждый сигнал в heartSoundData.mat соответствует файлу в исходных данных PhysioNet. extrafiles.mat также содержит приписывания исходного файла и объяснен в файле Modified_physionet_data.txt. Единственным файлом, требуемым запускать пример, является heartSoundData.mat.

Загрузка данных

Если вы следовали инструкциям по загрузке в предыдущем разделе, введите следующие команды, чтобы разархивировать эти два архивных файла.

unzip(fullfile(tempdir,'physionet_phonocardiogram-master.zip'),tempdir)
unzip(fullfile(tempdir,'physionet_phonocardiogram-master','PCG_Data.zip'),...
    fullfile(tempdir,'PCG_Data'))

После того, как вы разархивировали файл PCG_Data.zip, загружаете данные в MATLAB.

load(fullfile(tempdir,'PCG_Data','heartSoundData.mat'))

heartSoundData является массивом структур с двумя полями: Data и Classes. Data 10000 3829 матрица, где каждый столбец является записью PCG. Classes является 3829 1 категориальным массивом диагностических меток, один для каждого столбца Data. Поскольку это - бинарная проблема классификации, классы являются "нормальными" и "аварийными". Как ранее утверждено, существует 2 575 нормальных записей и 1 254 аварийных записи. Эквивалентно, 67,25% примеров в данных от людей с нормальной сердечной функцией, в то время как 32,75% от людей с аварийной сердечной функцией. Можно проверить это путем ввода:

summary(heartSoundData.Classes)
     normal        2575 
     abnormal      1254 
countcats(heartSoundData.Classes)./sum(countcats(heartSoundData.Classes))
ans = 2×1

    0.6725
    0.3275

Среда рассеивания вейвлета

Используйте waveletScattering, чтобы создать среду рассеивания времени вейвлета. Установите инвариантную шкалу совпадать с длиной сигнала. Среда рассеивания значения по умолчанию имеет два вейвлета, преобразовывает (наборы фильтров). Первый набор фильтров вейвлета имеет восемь вейвлетов на октаву. Второй набор фильтров имеет один вейвлет на октаву.

N = 1e4;
sn = waveletScattering('SignalLength',N,'InvarianceScale',N);

Создайте наборы обучающих данных и наборы тестов

Функция помощника, partition_heartsounds, делит 3 829 наблюдений так, чтобы 70% (2680) были в наборе обучающих данных с 1 802 нормальными и 878 аварийными. Остающиеся 1 149 записей (773 нормальных и 376 аварийных) протянуты в наборе тестов для прогноза. Генератор случайных чисел отобран в функции помощника, таким образом, результаты повторяемы. Код для partition_heartsounds и всех других функций помощника, используемых в этом примере, дан в разделе Supporting Functions в конце примера.

[trainData, testData, trainLabels, testLabels] = ...
    partition_heartsounds(70,heartSoundData.Data,heartSoundData.Classes);

Можно проверять количества каждого класса в наборах обучающих данных и наборах тестов.

summary(trainLabels)
     normal        1802 
     abnormal       878 
summary(testLabels)
     normal        773 
     abnormal      376 

Обратите внимание на то, что наборы обучающих данных и наборы тестов были разделены так, чтобы пропорция "нормальных" и "аварийных" записей в наборах обучающих данных и наборах тестов совпала с их пропорциями в полных данных. Можно подтвердить это со следующим.

countcats(trainLabels)./sum(countcats(trainLabels))
ans = 2×1

    0.6724
    0.3276

countcats(testLabels)./sum(countcats(testLabels))
ans = 2×1

    0.6728
    0.3272

Рассеивание функций

Получите рассеивающееся преобразование всех 2 680 записей в наборе обучающих данных. Для многомерных временных рядов рассеивающееся преобразование принимает, что каждый столбец является отдельным сигналом. Используйте опцию 'log', чтобы получить натуральный логарифм рассеивающихся коэффициентов.

scat_features_train = featureMatrix(sn,trainData,'Transform','log');

Для данных рассеивающихся параметров scat_features_train является 340 5 2 680 матрицами. Существует 340 рассеивающихся путей и пять рассеивающихся окон для каждого из 2 680 сигналов. В порядке передать это классификатору SVM, измените тензор в 13400 340 матрица, где каждая строка представляет одно окно рассеивания через 340 рассеивающихся путей. Общее количество строк равно продукту 5 и 2680 (количество записей в данных тренировки).

Nseq = size(scat_features_train,2);
scat_features_train = permute(scat_features_train,[2 3 1]);
scat_features_train = reshape(scat_features_train,...
    size(scat_features_train,1)*size(scat_features_train,2),[]);

Повторите процесс для тестовых данных.

scat_features_test = featureMatrix(sn,testData,'Transform','log');
scat_features_test = permute(scat_features_test,[2 3 1]);
scat_features_test = reshape(scat_features_test,...
    size(scat_features_test,1)*size(scat_features_test,2),[]);

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

[sequence_labels_train,sequence_labels_test] = ...
    createSequenceLabels_heartsounds(Nseq,trainLabels,testLabels);

Соответствуйте SVM к данным тренировки. В этом примере мы используем ядро кубического полинома. После подбора кривой SVM к данным тренировки мы выполняем 5-кратную перекрестную проверку, чтобы оценить ошибку обобщения на данных тренировки.

rng default;
classificationSVM = fitcsvm(...
    scat_features_train, ...
    sequence_labels_train , ...
    'KernelFunction', 'polynomial', ...
    'PolynomialOrder', 3, ...
    'KernelScale', 'auto', ...
    'BoxConstraint', 1, ...
    'Standardize', true, ...
    'ClassNames', categorical({'normal','abnormal'}));
kfoldmodel = crossval(classificationSVM, 'KFold', 5);

Вычислите потерю как процент и отобразите матрицу беспорядка.

predLabels = kfoldPredict(kfoldmodel);
loss = kfoldLoss(kfoldmodel)*100;
fprintf('Loss is %2.2f percent\n',loss);
Loss is 0.79 percent
accuracy = 100-loss;
fprintf('Accuracy is %2.2f percent\n',accuracy);
Accuracy is 99.21 percent
confmatCV = confusionchart(sequence_labels_train,predLabels);

Обратите внимание на то, что рассеивающаяся среда приводит к точности на 99,21 процентов, когда каждый раз окно классифицируется отдельно. Однако производительность на самом деле лучше, чем это значение, потому что у нас есть пять рассеивающихся окон на запись, и точность на 99,21 процентов основана на классификации всех окон отдельно. В этом случае используйте решение большинством голосов, чтобы получить присвоение единого класса на запись. Голосование класса соответствует режиму голосов за эти пять окон. Если никакой уникальный режим не найден, helperMajorityVote классифицирует тот набор рассеивающихся окон как 'NoUniqueMode', чтобы указать на ошибку классификации. Это приводит к дополнительному столбцу в матрице беспорядка.

classes = categorical({'abnormal','normal'});
ClassVotes = helperMajorityVote(predLabels,trainLabels,classes);
CVaccuracy = sum(eq(ClassVotes,trainLabels))./numel(trainLabels)*100;
fprintf('The true cross-validation accuracy is %2.2f percent.\n',CVaccuracy);
The true cross-validation accuracy is 99.93 percent.

Отобразите матрицу беспорядка для классификаций решений большинством голосов.

cmCV = confusionchart(ClassVotes,trainLabels);

Точность перекрестной проверки на данных тренировки - на самом деле 99,93 процентов. Существует две нормальных записи, которые неправильно классифицируются как аварийные.

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

predTestLabels = predict(classificationSVM,scat_features_test);

Определите точность прогнозов на наборе тестов с помощью решения большинством голосов.

ClassVotes = helperMajorityVote(predTestLabels,testLabels,classes);
testaccuracy = sum(eq(ClassVotes,testLabels))./numel(testLabels)*100;
fprintf('The test accuracy is %2.2f percent.\n',testaccuracy);
The test accuracy is 92.25 percent.
cmTest = confusionchart(ClassVotes,testLabels);

Из 1 149 тестовых записей 92,25% правильно классифицируются как "Нормальные" или "Аварийные". Из 773 нормальных записей PCG в наборе тестов, 728 правильно классифицируются. Из 376 аварийных записей в наборе тестов, 332 правильно классифицируются.

Точность, вспомните, и счет F1

В задаче классификации точность для класса является количеством правильных положительных результатов, разделенных на количество положительных результатов. Другими словами, всех записей, что классификатор присваивает данную метку, какая пропорция на самом деле принадлежит классу. Отзыв задан как количество правильных меток, разделенных на количество меток для данного класса. А именно, всех записей, принадлежащих классу, какая пропорция сделала нашу марку классификатора как тот класс. В оценке точности ваш классификатор вы идеально хотите преуспеть и на точности и на отзыве. Например, предположите, что у нас был классификатор, который маркировал каждый PCG, записывающий как аварийный. Затем наш отзыв для аварийного класса составил бы 100%. Все записи, принадлежащие аварийному классу, были бы маркированы аварийными. Однако точность была бы низкой. Поскольку наш классификатор маркировал все записи как аварийные, будет 2 575 ложных положительных сторон в этом случае для точности 1254/3829, или 32,75%. Счет F1 является средним гармоническим точности и отзыва и обеспечивает одну метрику, которая обобщает производительность классификатора и с точки зрения отзыва и с точки зрения точности. Функция помощника, helperF1heartSounds, вычисляет точность, вспомните, и музыка F1 к результатам классификации на наборе тестов, и возвращает те результаты в таблице.

PRTable = helperF1heartSounds(cmTest.NormalizedValues);
disp(PRTable)
                Precision    Recall    F1_Score
                _________    ______    ________

    Abnormal     88.298      88.064     88.181 
    Normal       94.179      94.301     94.239 

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

cmTest = confusionchart(ClassVotes,testLabels);

Отзыв для аварийного класса является количеством аварийных записей, идентифицированных как аварийным, который является записью в первой строке и первом столбце матрицы беспорядка, разделенной на сумму записей в первой строке. Точность для аварийного класса является пропорцией истинных аварийных записей в общем количестве, идентифицированном как аварийной классификатором. Это соответствует записи в первой строке и первом столбце матрицы беспорядка, разделенной на сумму записей в первом столбце. Счет F1 является средним гармоническим двух.

RecallAbnormal = cmTest.NormalizedValues(1,1)/sum(cmTest.NormalizedValues(1,:));
PrecisionAbnormal = cmTest.NormalizedValues(1,1)/sum(cmTest.NormalizedValues(:,1));
F1Abnormal = harmmean([RecallAbnormal PrecisionAbnormal]);
fprintf('RecallAbnormal = %2.3f\nPrecisionAbnormal = %2.3f\nF1Abnormal = %2.3f\n',...
    100*RecallAbnormal,100*PrecisionAbnormal,100*F1Abnormal);
RecallAbnormal = 88.064
PrecisionAbnormal = 88.298
F1Abnormal = 88.181

Повторите вышеупомянутое для нормального класса.

RecallNormal = cmTest.NormalizedValues(2,2)/sum(cmTest.NormalizedValues(2,:));
PrecisionNormal = cmTest.NormalizedValues(2,2)/sum(cmTest.NormalizedValues(:,2));
F1Normal = harmmean([RecallNormal PrecisionNormal]);
fprintf('RecallNormal = %2.3f\nPrecisionNormal = %2.3f\nF1Normal = %2.3f\n',...
    100*RecallNormal,100*PrecisionNormal,100*F1Normal);
RecallNormal = 94.301
PrecisionNormal = 94.179
F1Normal = 94.239

Сводные данные

Этот пример использовал время вейвлета, рассеиваясь, чтобы надежно идентифицировать человеческие записи фонокардиограммы как нормальные или аварийные в бинарной проблеме классификации. Вейвлет, рассеивающийся, потребовал только спецификации одного параметра, длины инвариантного к масштабу, в порядке произвести представления низкого отклонения данных PCG, которые позволили классификатору машины вектора поддержки точно смоделировать различие между этими двумя группами. Классификатор машины вектора поддержки с рассеиванием вейвлета смог достигнуть наилучшего решения и в точности и в отзыве для обеих групп несмотря на значительно несбалансированные количества нормальных и аварийных записей PCG и в наборе обучающих данных и наборе тестов.

Ссылки

  1. Голдбергер, A. L. Л. А. Н. Амарал, L. Стекло, Дж. М. Гаусдорф, P. Ch. Иванов, Р. Г. Марк, Дж. Э. Митус, Г. Б. Муди, C.-K. Пенг и Х. Э. Стэнли. "PhysioBank, PhysioToolkit и PhysioNet: Компоненты Нового Ресурса Исследования для Комплексных Физиологических Сигналов". Циркуляция. Издание 101, № 23, 13 июня 2000, стр e215-e220. http://circ.ahajournals.org/content/101/23/e215.full

  2. Лю и др. "Находящаяся в открытом доступе база данных для оценки сердечных звуковых алгоритмов". Физиологическое Измерение. Издание 37, № 12, 21 ноября 2016, стр 2181-2213. https://www.ncbi.nlm.nih.gov/pubmed/27869105

Поддерживание функций

partition_heartsounds создает наборы обучающих данных и наборы тестов, состоящие из заданных пропорций данных. Функция также сохраняет пропорцию аварийных и нормальных записей PCG в каждом наборе.

function [trainData, testData, trainLabels, testLabels] = partition_heartsounds(percent_train_split,Data,Labels)
% This function is only in support of the Wavelet Time Scattering 
% Classification of Phonocardiogram Data example. It may change or be
% removed in a future release.

% Labels in heart sound data are not sequential.
percent_train_split = percent_train_split/100;
% Each column is an observation
NormalData = Data(:,Labels == 'normal');
AbnormalData = Data(:,Labels == 'abnormal');
LabelsNormal = Labels(Labels == 'normal');
LabelsAbnormal = Labels(Labels == 'abnormal');
Nnormal = size(NormalData,2);
Nabnormal = size(AbnormalData,2);
num_train_normal = round(percent_train_split*Nnormal);
num_train_abnormal = round(percent_train_split*Nabnormal);
rng default;
Pnormal = randperm(Nnormal,num_train_normal);
Pabnormal = randperm(Nabnormal,num_train_abnormal);
notPnormal = setdiff(1:Nnormal,Pnormal);
notPabnormal = setdiff(1:Nabnormal,Pabnormal);
trainNormalData = NormalData(:,Pnormal);
trainNormalLabels = LabelsNormal(Pnormal);
trainAbnormalData = AbnormalData(:,Pabnormal);
trainAbnormalLabels = LabelsAbnormal(Pabnormal);
testNormalData = NormalData(:,notPnormal);
testNormalLabels = LabelsNormal(notPnormal);
testAbnormalData = AbnormalData(:,notPabnormal);
testAbnormalLabels = LabelsAbnormal(notPabnormal);
trainData = [trainNormalData trainAbnormalData];
trainData = (trainData-mean(trainData))./std(trainData,1);
trainLabels = [trainNormalLabels;  trainAbnormalLabels];
testData = [testNormalData  testAbnormalData];
testData = (testData-mean(testData))./std(testData,1);
testLabels = [testNormalLabels; testAbnormalLabels];
   
end

createSequenceLabels_heartsounds создает метки класса для последовательностей рассеивания времени вейвлета.

function [sequence_labels_train,sequence_labels_test] = createSequenceLabels_heartsounds(Nseq,trainLabels,testLabels)
% This function is only in support of the Wavelet Time Scattering 
% Classification of Phonocardiogram Data example. It may change or be
% removed in a future release.
Ntrain = numel(trainLabels);
trainLabels = repmat(trainLabels',Nseq,1);
sequence_labels_train = reshape(trainLabels,Nseq*Ntrain,1);
Ntest = numel(testLabels);
testLabels = repmat(testLabels',Nseq,1);
sequence_labels_test = reshape(testLabels,Ntest*Nseq,1);
end

helperMajorityVote реализует решение большинством голосов для классификации на основе режима. Если никакой уникальный режим не присутствует, голосование NoUniqueMode возвращено, чтобы гарантировать, что ошибка классификации зарегистрирована.

function [ClassVotes,ClassCounts] = helperMajorityVote(predLabels,origLabels,classes)
% This function is in support of ECGWaveletTimeScatteringExample. It may
% change or be removed in a future release.

% Make categorical arrays if the labels are not already categorical
predLabels = categorical(predLabels);
origLabels = categorical(origLabels);
% Expects both predLabels and origLabels to be categorical vectors
Npred = numel(predLabels);
Norig = numel(origLabels);
Nwin = Npred/Norig;
predLabels = reshape(predLabels,Nwin,Norig);
ClassCounts = countcats(predLabels);
[mxcount,idx] = max(ClassCounts);
ClassVotes = classes(idx);
% Check for any ties in the maximum values and ensure they are marked as
% error if the mode occurs more than once
modecnt = modecount(ClassCounts,mxcount);
ClassVotes(modecnt>1) = categorical({'NoUniqueMode'});
ClassVotes = ClassVotes(:);

%-------------------------------------------------------------------------
function modecnt = modecount(ClassCounts,mxcount)
modecnt = Inf(size(ClassCounts,2),1);
for nc = 1:size(ClassCounts,2)
    modecnt(nc) = histc(ClassCounts(:,nc),mxcount(nc));
end

end

% EOF
end

helperF1heartSounds вычисляет точность, вспоминает, и музыка F1 к результатам классификатора.

function PRTable = helperF1heartSounds(confmat)
% This function is only in support of the Wavelet Time Scattering 
% Classification of Phonocardiogram Data example. It may change or be
% removed in a future release.

precisionAB = confmat(1,1)/sum(confmat(:,1))*100;
precisionNR = confmat(2,2)/sum(confmat(:,2))*100 ;
recallAB = confmat(1,1)/sum(confmat(1,:))*100;
recallNR = confmat(2,2)/sum(confmat(2,:))*100;
F1AB = 2*(precisionAB*recallAB)/(precisionAB+recallAB);
F1NR = 2*(precisionNR*recallNR)/(precisionNR+recallNR);
% Construct a MATLAB Table to display the results.
PRTable = array2table([precisionAB recallAB F1AB;...
    precisionNR recallNR F1NR],...
    'VariableNames',{'Precision','Recall','F1_Score'},'RowNames',...
    {'Abnormal','Normal'});

end

Смотрите также

Похожие темы