V tomto článku bych rád navázal na předchozí zabývající se klasifikací EKG signálů s využitím konvolučních neuronových sítí. Na problematiku bych se chtěl ale podívat poněkud z jiného úhlu.
Dejme tomu, že již jsem spokojen s výsledky nějakého modelu a chtěl bych jej začít aktivně používat. Myslím to tak, že bych chtěl vytvořit nějakou webovou službu, do které se pošlou data EKG křivek, a výsledkem by bylo zařazení vzorku do klasifikačních tříd.
A o tom bude tento článek.
Je zřejmé, že budu vycházet ze stejné datové sady, jako tomu bylo v předchozích článcích. Jedná se o datovou sadu PTB-XL ECG dataset. Vzhledem k tomu, že jsem již dříve provedl přípravu dat pro modelování a tu jsem si uložil, můžu nyní načíst již předzpracovaná vstupní data jak pro EKG křivky, a metadata, tak také pro zařazení vzorků do klasifikačních tříd.
To budu s výhodou používat při přípravě klienta a testování webové služby.
Druhou věcí, kterou dopředu potřebuji mít k dispozici, je trénovaný model.
Z předchozích článků jsem si pro tento účel vybral kombinovaný model pro EKG křivky a metadata. V předchozích článcích byl tento model označovaný jako X_Y_model.
Pokud se podíváte blíže na postup, jak byl model trénován, tak tam uvidíte příkaz pro uložení hotového modelu:
model04.save('/kaggle/working/model/model04.keras')
Podstatná je přípona .keras, která znamená, že se bude jednat o ZIP soubor obsahující jak konfiguraci modelu (vstupy, výstupy, definované vrstvy a jejich propojení), tak také váhy všech parametrů vypočtených v průběhu trénování.
Tento soubor jsem si pro jednoduchost stáhl a bude přiložen k implementaci serverové služby.
Pokusil jsem se vytvořit co nejjednodušší řešení, takže se v podstatě jedná o jediný skript.
Pro jeho spuštění potřebuji doinstalovat ještě tyto knihovny (používám virtuální prostředí):
TensorFlow – pro načtení a spuštění modelu
Numpy – pro práci s poli všeho druhu
Flask – pro vytvoření webového servery
Takto vypadá řešení pro webovou službu (skript main.py):
import numpy as np
import keras
from flask import Flask, request, jsonify
KERAS_FILE = "./data/model04.keras"
COLUMNS = ['NORM', 'MI', 'STTC', 'CD', 'HYP']
app = Flask(__name__)
@app.route('/model-summary', methods=['GET', 'POST'])
def model_summary():
output_lines = []
model.summary(print_fn=lambda x: output_lines.append(x))
return "\n".join(output_lines)
@app.route('/predict', methods=['POST'])
def predict():
data = request.get_json()
x = np.expand_dims(np.asarray(data['X']), 0)
y = np.expand_dims(np.asarray(data['Y']), 0)
prediction = model.predict([x, y], verbose=0)
return jsonify({k: v for k, v in zip(COLUMNS, np.ndarray.tolist(prediction[0]))})
if __name__ == '__main__':
model = keras.models.load_model(KERAS_FILE)
app.run(port=8000)
Keras model je stažen do adresáře ./data.
Dále mám definované dvě služby, a sice model-summary a predict.
To je jen pomocná služba, kterou si můžu vypsat konfiguraci modelu.
Volá se bez parametrů, a výsledek může vypadat například takto:
[raska@fedora ~]$ curl http://localhost:8000/model-summary
Model: "X_Y_model"
__________________________________________________________________________________________________
Layer (type) Output Shape Param # Connected to
==================================================================================================
Y_inputs (InputLayer) [(None, 1000, 12)] 0 []
Y_norm (Normalization) (None, 1000, 12) 25 ['Y_inputs[0][0]']
Y_conv_1 (Conv1D) (None, 1000, 64) 5440 ['Y_norm[0][0]']
Y_norm_1 (BatchNormalizati (None, 1000, 64) 256 ['Y_conv_1[0][0]']
on)
Y_relu_1 (ReLU) (None, 1000, 64) 0 ['Y_norm_1[0][0]']
Y_pool_1 (MaxPooling1D) (None, 500, 64) 0 ['Y_relu_1[0][0]']
Y_conv_2 (Conv1D) (None, 500, 128) 24704 ['Y_pool_1[0][0]']
Y_norm_2 (BatchNormalizati (None, 500, 128) 512 ['Y_conv_2[0][0]']
on)
Y_relu_2 (ReLU) (None, 500, 128) 0 ['Y_norm_2[0][0]']
X_inputs (InputLayer) [(None, 7)] 0 []
Y_pool_2 (MaxPooling1D) (None, 250, 128) 0 ['Y_relu_2[0][0]']
X_norm (Normalization) (None, 7) 15 ['X_inputs[0][0]']
Y_conv_3 (Conv1D) (None, 250, 256) 98560 ['Y_pool_2[0][0]']
X_dense_1 (Dense) (None, 32) 256 ['X_norm[0][0]']
Y_norm_3 (BatchNormalizati (None, 250, 256) 1024 ['Y_conv_3[0][0]']
on)
X_drop_1 (Dropout) (None, 32) 0 ['X_dense_1[0][0]']
Y_relu_3 (ReLU) (None, 250, 256) 0 ['Y_norm_3[0][0]']
X_dense_2 (Dense) (None, 32) 1056 ['X_drop_1[0][0]']
Y_aver (GlobalAveragePooli (None, 256) 0 ['Y_relu_3[0][0]']
ng1D)
X_drop_2 (Dropout) (None, 32) 0 ['X_dense_2[0][0]']
Y_drop (Dropout) (None, 256) 0 ['Y_aver[0][0]']
Z_concat (Concatenate) (None, 288) 0 ['X_drop_2[0][0]',
'Y_drop[0][0]']
Z_dense_1 (Dense) (None, 128) 36992 ['Z_concat[0][0]']
Z_dense_2 (Dense) (None, 128) 16512 ['Z_dense_1[0][0]']
Z_drop_1 (Dropout) (None, 128) 0 ['Z_dense_2[0][0]']
Z_outputs (Dense) (None, 5) 645 ['Z_drop_1[0][0]']
==================================================================================================
Total params: 185997 (726.56 KB)
Trainable params: 185061 (722.89 KB)
Non-trainable params: 936 (3.66 KB)
__________________________________________________________________________________________________
To je ta služba, o kterou mně vlastně jde.
Na jejím vstupu by měla být data EKG křivek a metadata k jednomu vzorku a to ve formátu JSON.
Služba tedy příjme obsah požadavku a převede jej z JSON do interní Python reprezentace ve formě slovníku s klíči X pro metadata a Y pro křivky. Dalším krokem je převedení metadat a křivek na Numpy pole a rozšíření polí o jednu dimenzi (ta je potřeba, protože se předpokládá možnost predikce pro více vzorků najednou).
Tato pole pak slouží jako vstupní data pro vyvolání metody predict na modelu.
Výsledek predikce, což je jednorozměrné pole pravděpodobností zařazení do klasifikační skupiny, ještě převedu na slovník s názvy skupin. A to je výsledek služby, opět převeden do JSON.
Pro ověření webové služby predikce jsem si vytvořil jednoduchého klienta.
import sys import json import numpy as np import pandas as pd import requests
Dále si načtu vzorky dat z předpřipravené datové sady:
PREPROCESSED_DATA_FILE = './data/data.npz'
thismodule = sys.modules[__name__]
with np.load(PREPROCESSED_DATA_FILE) as data:
for k in ('X_test', 'Y_test', 'Z_test'):
setattr(thismodule, k, data[k].astype(float))
COLUMNS = ['NORM', 'MI', 'STTC', 'CD', 'HYP']
Z = pd.DataFrame(Z_test, columns=COLUMNS, dtype=int)
A nakonec si udělám jednu funkci pro ověření vzorku dat:
def check_sample(sample_id):
print(f"SAMPLE: id={sample_id}")
print(f" target values: {Z_test[sample_id]}")
data = json.dumps({
'X': X_test[sample_id].tolist(),
'Y': Y_test[sample_id].tolist()
})
headers = {'Content-type': 'application/json'}
print(f" prediction:")
prediction = requests.post("http://localhost:8000/predict", data=data, headers=headers).json()
for diag in COLUMNS:
print(f" {diag:4} = {prediction[diag]:.03f}")
Do funkce se zadá číslo vzorku, který chci testovat.
Zobrazím si skutečné třídy, které tomuto vzorku byly přiřazeny ve zdrojové datové sadě pro ověření.
Dále si připravím tělo dotazu, a to jako dictionary s klíči X a Y a přiřazenými daty daného vzorku (převedeno z Array na List[float] případně List[List[float]]).
Nyní již stačí zavolat dříve zmiňovanou službu predict a naformátovat odpovědi.
Dříve, než začnu něco zkoušet, tak je dobré si vybrat nějaké vzorky. Takto jsem si vybral ty, které mají více jak jednu klasifikační třídu:
In [4]: z = pd.DataFrame(Z_test, columns=COLUMNS, dtype=int) z[z.sum(axis=1) >=2] Out[4]:
NORM |
MI |
STTC |
CD |
HYP |
|
24 |
0 |
1 |
1 |
1 |
1 |
33 |
1 |
0 |
0 |
1 |
0 |
39 |
0 |
1 |
1 |
0 |
1 |
40 |
0 |
1 |
1 |
0 |
1 |
41 |
0 |
0 |
1 |
0 |
1 |
... |
... |
... |
... |
... |
... |
2191 |
0 |
0 |
1 |
0 |
1 |
2194 |
0 |
0 |
1 |
0 |
1 |
2195 |
0 |
0 |
1 |
1 |
1 |
2196 |
0 |
1 |
0 |
1 |
0 |
2197 |
0 |
1 |
0 |
1 |
0 |
511 rows × 5 columns
A nyní nějaké pokusy:
In [5]:
check_sample(33)
SAMPLE: id=33
target values: [1. 0. 0. 1. 0.]
prediction:
NORM = 0.940
MI = 0.003
STTC = 0.012
CD = 0.046
HYP = 0.020
Výsledkem predikce jsou pravděpodobnosti zařazení vzorku do dané skupiny. Z cílových hodnot je zřejmé, že by vzorek měl být zařazen do třídy NORM a CD. Predikce ale našla pouze jednu třídu, a sice NORM.
In [6]:
check_sample(24)
SAMPLE: id=24
target values: [0. 1. 1. 1. 1.]
prediction:
NORM = 0.000
MI = 0.970
STTC = 0.585
CD = 0.975
HYP = 0.331
Zde se jedná o vzorek, který by měl být zařazen do všech tříd s výjimkou NORM. Pokud budu za limitní pravděpodobnost považovat hodnotu 0.5, pak mně bude opět chybět jedna skupina, v tomto případě HYP.
A ještě jeden vzorek:
In [7]:
check_sample(2197)
SAMPLE: id=2197
target values: [0. 1. 0. 1. 0.]
prediction:
NORM = 0.000
MI = 0.999
STTC = 0.038
CD = 0.886
HYP = 0.077
Zde již zařazení do skupin MI a CD vypadá skutečně přesvědčivě.
A to je dnes vše.
pracuje na pozici IT architekta. Poslední roky se zaměřuje na integrační a komunikační projekty ve zdravotnictví. Mezi jeho koníčky patří také paragliding a jízda na horském kole.
Přečteno 33 524×
Přečteno 29 927×
Přečteno 27 156×
Přečteno 25 070×
Přečteno 20 657×