Обнаружение аномалии в промышленном машинном оборудовании Используя данные о вибрации с тремя осями

В этом примере показано, как обнаружить аномалии в данных о вибрации с помощью машинного обучения и глубокого обучения. Данные о вибрации использования в качестве примера собраны от промышленной машины. Во-первых, вы извлекаете функции из необработанных измерений, соответствующих нормальному функционированию с помощью Приложения Diagnostic Feature Designer. Используйте выбранные функции, чтобы обучить каждую модель на рассмотрении. Затем используйте каждую обученную модель, чтобы идентифицировать, действует ли машина при нормальном состоянии.

Набор данных

Набор данных содержит измерения вибрации с 3 осями от промышленной машины. Данные собраны оба сразу до и после запланированного обслуживания. Данные, собранные после запланированного обслуживания, приняты, чтобы представлять нормальные условия работы машины. Прежде чем данные об обслуживании могут представлять или нормальные или аномальные условия. Данные для каждой оси хранятся в отдельном столбце, и каждый файл содержит 7 000 измерений. Загрузите данные MathWorks supportfiles сайт и постройте выборку нормального и аномального набора данных.

url = 'https://ssd.mathworks.com/supportfiles/predmaint/anomalyDetection3axisVibration/v1/vibrationData.zip';
websave('vibrationData.zip',url);
unzip('vibrationData.zip');
load("MachineData.mat")
trainData
trainData=40×4 table
          ch1                 ch2                 ch3           label 
    ________________    ________________    ________________    ______

    {70000×1 double}    {70000×1 double}    {70000×1 double}    Before
    {70000×1 double}    {70000×1 double}    {70000×1 double}    Before
    {70000×1 double}    {70000×1 double}    {70000×1 double}    Before
    {70000×1 double}    {70000×1 double}    {70000×1 double}    Before
    {70000×1 double}    {70000×1 double}    {70000×1 double}    Before
    {70000×1 double}    {70000×1 double}    {70000×1 double}    Before
    {70000×1 double}    {70000×1 double}    {70000×1 double}    Before
    {70000×1 double}    {70000×1 double}    {70000×1 double}    Before
    {70000×1 double}    {70000×1 double}    {70000×1 double}    Before
    {70000×1 double}    {70000×1 double}    {70000×1 double}    Before
    {70000×1 double}    {70000×1 double}    {70000×1 double}    Before
    {70000×1 double}    {70000×1 double}    {70000×1 double}    Before
    {70000×1 double}    {70000×1 double}    {70000×1 double}    Before
    {70000×1 double}    {70000×1 double}    {70000×1 double}    Before
    {70000×1 double}    {70000×1 double}    {70000×1 double}    Before
    {70000×1 double}    {70000×1 double}    {70000×1 double}    Before
      ⋮

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

ensMember = 4;
helperPlotVibrationData(trainData, ensMember)

Извлечение функций с приложением Diagnostic Feature Designer

Используя необработанные данные для учебного машинного обучения модели не очень эффективно. Приложение Diagnostic Feature Designer позволяет вам в интерактивном режиме исследовать и предварительно обработать свои данные, извлечь время и функции частотного диапазона, затем отранжировать признаки, чтобы определить, который будет самым эффективным. Можно затем экспортировать функцию, чтобы извлечь выбранные функции из набора данных программно. Откройте приложение DFD:

diagnosticFeatureDesigner

Нажмите кнопку New Session, выберите trainData как источник и затем установите label как условная переменная. label переменная идентифицирует условие машины для соответствующих данных.

Можно использовать Diagnostic Feature Designer, чтобы выполнить итерации на функциях и оценить их. Представление гистограммы для всех генерированных признаков создается, чтобы визуализировать распределения, разделенные метками для различных функций, извлеченных из ch1. Обратите внимание на то, что гистограммы, показанные ниже, от намного большего набора данных, таким образом, разделение легче визуализировать. Набор данных, используемый до сих пор, является подмножеством исходных данных, таким образом, ваши результаты могут отличаться.

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

  • ch1: Фактор Гребня, Эксцесс, RMS, Станд.

  • ch2: Среднее значение, RMS, Скошенность, Станд.

  • ch3: Фактор Гребня, SINAD, ОСШ, THD

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

trainFeatures = generateFeatures(trainData);
head(trainFeatures)
ans=8×13 table
    label     ch1_stats/Col1_CrestFactor    ch1_stats/Col1_Kurtosis    ch1_stats/Col1_RMS    ch1_stats/Col1_Std    ch2_stats/Col1_Mean    ch2_stats/Col1_RMS    ch2_stats/Col1_Skewness    ch2_stats/Col1_Std    ch3_stats/Col1_CrestFactor    ch3_stats/Col1_SINAD    ch3_stats/Col1_SNR    ch3_stats/Col1_THD
    ______    __________________________    _______________________    __________________    __________________    ___________________    __________________    _______________________    __________________    __________________________    ____________________    __________________    __________________

    Before              2.2811                      1.8087                   2.3074                2.3071               -0.032332              0.64962                   4.523                  0.64882                    11.973                    -15.945                -15.886                -2.732      
    Before              2.3276                      1.8379                   2.2613                 2.261                -0.03331              0.59458                   5.548                  0.59365                    10.284                    -15.984                -15.927               -2.7507      
    Before              2.3276                      1.8626                   2.2613                2.2612               -0.012052              0.48248                  4.3638                  0.48233                    8.9125                    -15.858                -15.798               -2.7104      
    Before              2.8781                      2.1986                   1.8288                1.8285               -0.005049              0.34984                  2.3324                  0.34981                    11.795                    -16.191                 -16.14               -3.0683      
    Before              2.8911                        2.06                   1.8205                1.8203              -0.0018988              0.27366                  1.7661                  0.27365                    11.395                    -15.947                -15.893               -3.1126      
    Before              2.8979                      2.1204                   1.8163                1.8162              -0.0044174               0.3674                  2.8969                  0.36737                    11.685                    -15.963                -15.908               -2.9761      
    Before              2.9494                        1.92                   1.7846                1.7844              -0.0067284              0.36262                  4.1308                  0.36256                    12.396                    -15.999                -15.942               -2.8281      
    Before              2.5106                      1.6774                   1.7513                1.7511              -0.0089548              0.32348                  3.7691                  0.32335                    8.8808                     -15.79                -15.732               -2.9532      

Обучите модели обнаружению аномалии

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

load("FeatureEntire.mat")
head(featureAll)
ans=8×13 table
    label     ch1_stats/Col1_CrestFactor    ch1_stats/Col1_Kurtosis    ch1_stats/Col1_RMS    ch1_stats/Col1_Std    ch2_stats/Col1_Mean    ch2_stats/Col1_RMS    ch2_stats/Col1_Skewness    ch2_stats/Col1_Std    ch3_stats/Col1_CrestFactor    ch3_stats/Col1_SINAD    ch3_stats/Col1_SNR    ch3_stats/Col1_THD
    ______    __________________________    _______________________    __________________    __________________    ___________________    __________________    _______________________    __________________    __________________________    ____________________    __________________    __________________

    Before              2.3683                       1.927                   2.2225                2.2225               -0.015149              0.62512                  4.2931                  0.62495                    5.6569                    -5.4476                -4.9977               -4.4608      
    Before               2.402                      1.9206                   2.1807                2.1803               -0.018269              0.56773                  3.9985                  0.56744                    8.7481                    -12.532                -12.419               -3.2353      
    Before              2.4157                      1.9523                   2.1789                2.1788              -0.0063652              0.45646                  2.8886                  0.45642                    8.3111                    -12.977                -12.869               -2.9591      
    Before              2.4595                      1.8205                     2.14                2.1401               0.0017307              0.41418                  2.0635                  0.41418                    7.2318                    -13.566                -13.468               -2.7944      
    Before              2.2502                      1.8609                   2.3391                 2.339              -0.0081829               0.3694                  3.3498                  0.36931                    6.8134                     -13.33                -13.225               -2.7182      
    Before              2.4211                      2.2479                   2.1286                2.1285                0.011139              0.36638                  1.8602                  0.36621                    7.4712                    -13.324                -13.226               -3.0313      
    Before              3.3111                      4.0304                   1.5896                1.5896              -0.0080759              0.47218                  2.1132                  0.47211                    8.2412                     -13.85                -13.758               -2.7822      
    Before              2.2655                      2.0656                   2.3233                2.3233              -0.0049447              0.37829                  2.4936                  0.37827                    7.6947                    -13.781                -13.683               -2.5601      

Используйте cvpartition к данным о разделе в набор обучающих данных и независимый набор тестов. helperExtractLabeledData функция помощника используется, чтобы найти все функции, соответствующие метке 'AfterfeatureTrain переменная.

rng(0) % set for reproducibility
idx = cvpartition(featureAll.label, 'holdout', 0.1);
featureTrain = featureAll(idx.training, :);
featureTest = featureAll(idx.test, :);

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

trueAnomaliesTest = featureTest.label;
featureNormal = featureTrain(featureTrain.label=='After', :);

Обнаружьте аномалии с SVM одного класса

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

mdlSVM = fitcsvm(featureNormal, 'label', 'Standardize', true, 'OutlierFraction', 0);

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

featureTestNoLabels = featureTest(:, 2:end);
[~,scoreSVM] = predict(mdlSVM,featureTestNoLabels);
isanomalySVM = scoreSVM<0;
predSVM = categorical(isanomalySVM, [1, 0], ["Anomaly", "Normal"]);
trueAnomaliesTest = renamecats(trueAnomaliesTest,["After","Before"], ["Normal","Anomaly"]);
figure;
confusionchart(trueAnomaliesTest, predSVM, Title="Anomaly Detection with One-class SVM", Normalization="row-normalized");

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

Обнаружьте аномалии с лесом изоляции

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

[mdlIF,~,scoreTrainIF] = iforest(featureNormal{:,2:13},'ContaminationFraction',0.09);

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

[isanomalyIF,scoreTestIF] = isanomaly(mdlIF,featureTestNoLabels.Variables);
predIF = categorical(isanomalyIF, [1, 0], ["Anomaly", "Normal"]);
figure;
confusionchart(trueAnomaliesTest,predIF,Title="Anomaly Detection with Isolation Forest",Normalization="row-normalized");

На этих данных лес изоляции не делает, а также SVM одного класса, но это не ваше типичное обнаружение аномалии с таким количеством "аномалий" в этом почти сбалансированном наборе данных. Причина этого состоит в том, что обучающие данные содержали нормальные данные только, в то время как тестовые данные содержат приблизительно 30%-е аномальные данные. Это указывает, что лесная модель изоляции лучше подходит, когда распределение аномалий и нормальных данных ближе.

Обнаружьте аномалии с сетью автоэнкодера LSTM

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

featuresAfter = helperExtractLabeledData(featureTrain, ...
   "After");

Затем создайте biLSTM сеть автоэнкодера и установите опции обучения.

featureDimension = 1;

% Define biLSTM network layers
layers = [ sequenceInputLayer(featureDimension, 'Name', 'in')
   bilstmLayer(16, 'Name', 'bilstm1')
   reluLayer('Name', 'relu1')
   bilstmLayer(32, 'Name', 'bilstm2')
   reluLayer('Name', 'relu2')
   bilstmLayer(16, 'Name', 'bilstm3')
   reluLayer('Name', 'relu3')
   fullyConnectedLayer(featureDimension, 'Name', 'fc')
   regressionLayer('Name', 'out') ];

% Set Training Options
options = trainingOptions('adam', ...
   'Plots', 'training-progress', ...
   'MiniBatchSize', 500,...
   'MaxEpochs',200);

MaxEpochs параметр опций обучения устанавливается на 200. Для более высокой точности валидации можно установить этот параметр на большее число. Однако эта причина силы сеть, чтобы сверхсоответствовать. Теперь обучите модель.

net = trainNetwork(featuresAfter, featuresAfter, layers, options);
Training on single CPU.
|========================================================================================|
|  Epoch  |  Iteration  |  Time Elapsed  |  Mini-batch  |  Mini-batch  |  Base Learning  |
|         |             |   (hh:mm:ss)   |     RMSE     |     Loss     |      Rate       |
|========================================================================================|
|       1 |           1 |       00:00:04 |         5.81 |         16.9 |          0.0010 |
|       3 |          50 |       00:00:16 |         5.43 |         14.8 |          0.0010 |
|       5 |         100 |       00:00:28 |         3.99 |          7.9 |          0.0010 |
|       8 |         150 |       00:00:39 |         4.27 |          9.1 |          0.0010 |
|      10 |         200 |       00:00:51 |         3.48 |          6.1 |          0.0010 |
|      13 |         250 |       00:01:03 |         3.97 |          7.9 |          0.0010 |
|      15 |         300 |       00:01:15 |         3.17 |          5.0 |          0.0010 |
|      18 |         350 |       00:01:27 |         3.72 |          6.9 |          0.0010 |
|      20 |         400 |       00:01:39 |         2.89 |          4.2 |          0.0010 |
|      23 |         450 |       00:01:51 |         3.49 |          6.1 |          0.0010 |
|      25 |         500 |       00:02:03 |         2.67 |          3.6 |          0.0010 |
|      28 |         550 |       00:02:15 |         3.31 |          5.5 |          0.0010 |
|      30 |         600 |       00:02:27 |         2.49 |          3.1 |          0.0010 |
|      33 |         650 |       00:02:39 |         3.14 |          4.9 |          0.0010 |
|      35 |         700 |       00:02:51 |         2.29 |          2.6 |          0.0010 |
|      38 |         750 |       00:03:03 |         2.96 |          4.4 |          0.0010 |
|      40 |         800 |       00:03:16 |         2.12 |          2.2 |          0.0010 |
|      43 |         850 |       00:03:28 |         2.81 |          4.0 |          0.0010 |
|      45 |         900 |       00:03:40 |         1.98 |          2.0 |          0.0010 |
|      48 |         950 |       00:03:51 |         2.71 |          3.7 |          0.0010 |
|      50 |        1000 |       00:04:04 |         1.89 |          1.8 |          0.0010 |
|      53 |        1050 |       00:04:16 |         2.62 |          3.4 |          0.0010 |
|      55 |        1100 |       00:04:28 |         1.81 |          1.6 |          0.0010 |
|      58 |        1150 |       00:04:41 |         2.54 |          3.2 |          0.0010 |
|      60 |        1200 |       00:04:53 |         1.74 |          1.5 |          0.0010 |
|      63 |        1250 |       00:05:06 |         2.47 |          3.0 |          0.0010 |
|      65 |        1300 |       00:05:18 |         1.66 |          1.4 |          0.0010 |
|      68 |        1350 |       00:05:31 |         2.38 |          2.8 |          0.0010 |
|      70 |        1400 |       00:05:43 |         1.53 |          1.2 |          0.0010 |
|      73 |        1450 |       00:05:55 |         2.28 |          2.6 |          0.0010 |
|      75 |        1500 |       00:06:08 |         1.44 |          1.0 |          0.0010 |
|      78 |        1550 |       00:06:20 |         2.20 |          2.4 |          0.0010 |
|      80 |        1600 |       00:06:33 |         1.36 |          0.9 |          0.0010 |
|      83 |        1650 |       00:06:46 |         2.14 |          2.3 |          0.0010 |
|      85 |        1700 |       00:07:00 |         1.29 |          0.8 |          0.0010 |
|      88 |        1750 |       00:07:14 |         2.07 |          2.1 |          0.0010 |
|      90 |        1800 |       00:07:28 |         1.22 |          0.8 |          0.0010 |
|      93 |        1850 |       00:07:42 |         2.01 |          2.0 |          0.0010 |
|      95 |        1900 |       00:07:56 |         1.16 |          0.7 |          0.0010 |
|      98 |        1950 |       00:08:10 |         1.95 |          1.9 |          0.0010 |
|     100 |        2000 |       00:08:24 |         1.10 |          0.6 |          0.0010 |
|     103 |        2050 |       00:08:38 |         1.90 |          1.8 |          0.0010 |
|     105 |        2100 |       00:08:52 |         1.05 |          0.5 |          0.0010 |
|     108 |        2150 |       00:09:06 |         1.86 |          1.7 |          0.0010 |
|     110 |        2200 |       00:09:20 |         1.00 |          0.5 |          0.0010 |
|     113 |        2250 |       00:09:34 |         1.82 |          1.6 |          0.0010 |
|     115 |        2300 |       00:09:48 |         0.96 |          0.5 |          0.0010 |
|     118 |        2350 |       00:10:02 |         1.78 |          1.6 |          0.0010 |
|     120 |        2400 |       00:10:16 |         0.91 |          0.4 |          0.0010 |
|     123 |        2450 |       00:10:30 |         1.74 |          1.5 |          0.0010 |
|     125 |        2500 |       00:10:44 |         0.88 |          0.4 |          0.0010 |
|     128 |        2550 |       00:10:58 |         1.71 |          1.5 |          0.0010 |
|     130 |        2600 |       00:11:12 |         0.84 |          0.4 |          0.0010 |
|     133 |        2650 |       00:11:26 |         1.68 |          1.4 |          0.0010 |
|     135 |        2700 |       00:11:40 |         0.81 |          0.3 |          0.0010 |
|     138 |        2750 |       00:11:54 |         1.65 |          1.4 |          0.0010 |
|     140 |        2800 |       00:12:08 |         0.78 |          0.3 |          0.0010 |
|     143 |        2850 |       00:12:22 |         1.62 |          1.3 |          0.0010 |
|     145 |        2900 |       00:12:36 |         0.76 |          0.3 |          0.0010 |
|     148 |        2950 |       00:12:49 |         1.59 |          1.3 |          0.0010 |
|     150 |        3000 |       00:13:02 |         0.73 |          0.3 |          0.0010 |
|     153 |        3050 |       00:13:15 |         1.57 |          1.2 |          0.0010 |
|     155 |        3100 |       00:13:29 |         0.71 |          0.3 |          0.0010 |
|     158 |        3150 |       00:13:42 |         1.55 |          1.2 |          0.0010 |
|     160 |        3200 |       00:13:55 |         0.68 |          0.2 |          0.0010 |
|     163 |        3250 |       00:14:09 |         1.52 |          1.2 |          0.0010 |
|     165 |        3300 |       00:14:22 |         0.66 |          0.2 |          0.0010 |
|     168 |        3350 |       00:14:36 |         1.50 |          1.1 |          0.0010 |
|     170 |        3400 |       00:14:50 |         0.64 |          0.2 |          0.0010 |
|     173 |        3450 |       00:15:03 |         1.48 |          1.1 |          0.0010 |
|     175 |        3500 |       00:15:16 |         0.62 |          0.2 |          0.0010 |
|     178 |        3550 |       00:15:30 |         1.46 |          1.1 |          0.0010 |
|     180 |        3600 |       00:15:43 |         0.60 |          0.2 |          0.0010 |
|     183 |        3650 |       00:15:56 |         1.44 |          1.0 |          0.0010 |
|     185 |        3700 |       00:16:10 |         0.59 |          0.2 |          0.0010 |
|     188 |        3750 |       00:16:23 |         1.42 |          1.0 |          0.0010 |
|     190 |        3800 |       00:16:37 |         0.57 |          0.2 |          0.0010 |
|     193 |        3850 |       00:16:50 |         1.40 |          1.0 |          0.0010 |
|     195 |        3900 |       00:17:04 |         0.55 |          0.2 |          0.0010 |
|     198 |        3950 |       00:17:17 |         1.38 |          1.0 |          0.0010 |
|     200 |        4000 |       00:17:31 |         0.54 |          0.1 |          0.0010 |
|========================================================================================|
Training finished: Reached final iteration.

Визуализируйте поведение модели и ошибку на данных о валидации

Извлеките и визуализируйте выборку каждый из Аномального и Нормального состояния. Обратите внимание на то, что графики ниже сравнивают ошибку в значениях каждой из 12 функций (обозначенный на оси X). В этой выборке функции 10, 11 и 12 не восстанавливают хорошо для аномального входа и таким образом имеют высокое ошибочное значение. Модель использует это ошибочное значение, чтобы идентифицировать аномалию.

testNormal = {featureTest(1200, 2:end).Variables};
testAnomaly = {featureTest(200, 2:end).Variables};

% Predict decoded signal for both
decodedNormal = predict(net,testNormal);
decodedAnomaly = predict(net,testAnomaly);

% Visualize
helperVisualizeModelBehavior(testNormal, testAnomaly, decodedNormal, decodedAnomaly)

Извлеките функции нормальных и аномальных данных. Рисунок показывает, что ошибка реконструкции для аномальных данных значительно выше, чем нормальные данные. Это целесообразно, поскольку автоэнкодер был обучен на нормальных данных, таким образом, это лучше восстановит подобные сигналы.

% Extract data Before maintenance
XTestBefore = helperExtractLabeledData(featureTest, "Before");

% Predict output before maintenance and calculate error
yHatBefore = predict(net, XTestBefore);
errorBefore = helperCalculateError(XTestBefore, yHatBefore);

% Extract data after maintenance
XTestAfter = helperExtractLabeledData(featureTest, "After");

% Predict output after maintenance and calculate error
yHatAfter = predict(net, XTestAfter);
errorAfter = helperCalculateError(XTestAfter, yHatAfter);

helperVisualizeError(errorBefore, errorAfter);

Идентифицируйте аномалии

Вычислите ошибку на полные данные о валидации.

XTestAll = helperExtractLabeledData(featureTest, "All");

yHatAll = predict(net, XTestAll);
errorAll = helperCalculateError(XTestAll, yHatAll);

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

thresh = 0.5;
anomalies = errorAll > thresh*mean(errorAll);

helperVisualizeAnomalies(anomalies, errorAll, featureTest);

Вспомогательные Функции

function E = helperCalculateError(X, Y)
% HELPERCALCULATEERROR function calculates the rms error value between the
% inputs X, Y
E = zeros(length(X),1);
for i = 1:length(X)
   E(i,:) = sqrt(sum((Y{i} - X{i}).^2));
end

end

function helperVisualizeError(errorBefore, errorAfter)
% HELPERVISUALIZEERROR creates a plot to visualize the errors on detecting
% before and after conditions
figure("Color", "W")
tiledlayout("flow")

nexttile
plot(1:length(errorBefore), errorBefore, 'LineWidth',1.5), grid on
title(["Before Maintenance", ...
   sprintf("Mean Error: %.2f\n", mean(errorBefore))])
xlabel("Observations")
ylabel("Reconstruction Error")
ylim([0 15])

nexttile
plot(1:length(errorAfter), errorAfter, 'LineWidth',1.5), grid on,
title(["After Maintenance", ...
   sprintf("Mean Error: %.2f\n", mean(errorAfter))])
xlabel("Observations")
ylabel("Reconstruction Error")
ylim([0 15])

end

function helperVisualizeAnomalies(anomalies, errorAll, featureTest)
% HELPERVISUALIZEANOMALIES creates a plot of the detected anomalies
anomalyIdx = find(anomalies);
anomalyErr = errorAll(anomalies);

predAE = categorical(anomalies, [1, 0], ["Anomaly", "Normal"]);
trueAE = renamecats(featureTest.label,["Before","After"],["Anomaly","Normal"]);

acc = numel(find(trueAE == predAE))/numel(predAE)*100;
figure;
t = tiledlayout("flow");
title(t, "Test Accuracy: " + round(mean(acc),2) + "%");
nexttile
hold on
plot(errorAll)
plot(anomalyIdx, anomalyErr, 'x')
hold off
ylabel("Reconstruction Error")
xlabel("Observation")
legend("Error", "Candidate Anomaly")

nexttile
confusionchart(trueAE,predAE)

end

function helperVisualizeModelBehavior(normalData, abnormalData, decodedNorm, decodedAbNorm)
%HELPERVISUALIZEMODELBEHAVIOR Visualize model behavior on sample validation data

figure("Color", "W")
tiledlayout("flow")

nexttile()
hold on
colororder('default')
yyaxis left
plot(normalData{:})
plot(decodedNorm{:},":","LineWidth",1.5)
hold off
title("Normal Input")
grid on
ylabel("Feature Value")
yyaxis right
stem(abs(normalData{:} - decodedNorm{:}))
ylim([0 2])
ylabel("Error")
legend(["Input", "Decoded","Error"],"Location","southwest")

nexttile()
hold on
yyaxis left
plot(abnormalData{:})
plot(decodedAbNorm{:},":","LineWidth",1.5)
hold off
title("Abnormal Input")
grid on
ylabel("Feature Value")
yyaxis right
stem(abs(abnormalData{:} - decodedAbNorm{:}))
ylim([0 2])
ylabel("Error")
legend(["Input", "Decoded","Error"],"Location","southwest")

end

function X = helperExtractLabeledData(featureTable, label)
%HELPEREXTRACTLABELEDDATA Extract data from before or after operating
%conditions and re-format to support input to autoencoder network

% Select data with label After
if label == "All"
   Xtemp = featureTable(:, 2:end).Variables;
else
   tF = featureTable.label == label;
   Xtemp = featureTable(tF, 2:end).Variables;
end

% Arrange data into cells
X = cell(length(Xtemp),1);
for i = 1:length(Xtemp)
   X{i,:} = Xtemp(i,:);
end

end