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 25 732×
Přečteno 25 727×
Přečteno 25 398×
Přečteno 23 620×
Přečteno 19 356×