CorNET (PPG heart rate, convolution + LSTM)
helia_edge.models.cornet.build rebuilds the CorNET heart-rate regressor from
Biswas et al., IEEE TBioCAS 2019:
two convolution stages followed by stacked LSTMs. CorNetParams holds the
backend-free config.
from helia_edge.models import CorNetParams, ModelSpec, build
model = build(ModelSpec(params=CorNetParams(unroll=True), input_shape=(1000, 1)))Input: 8 s of wrist PPG at 125 Hz (1000 samples, one channel). The paper band-passes the signal from 0.1 to 18 Hz and z-scores each window. This preprocessing is the caller’s responsibility.
Default architecture (paper Sec. III, Fig. 6, Table III):
- Two convolution stages. Each is Conv1D (32 filters, kernel 40, valid padding, stride 1), then BatchNorm, ReLU, MaxPooling 4 and Dropout 0.1. The window goes from 1000 samples to 961, 240, 201 and finally 50 timesteps.
- LSTM(128), returning its sequence.
- LSTM(128), returning the last step.
- A single linear output,
hr.
Trainable parameters match Table III: 1,312 and 40,992 for the convolutions, and 82,432 and 131,584 for the LSTMs. Table III’s dense row is the two-class identification head (258); the one-neuron HR head has 129.
Some details are not stated in the paper and are reconstructed here:
- Stride and padding are inferred from Table III’s MAC counts.
- Fig. 6 places BatchNorm before ReLU, while the text places it after. The constructor follows Fig. 6.
- The dropout position is not stated.
- The LSTM gate activation is not stated. The paper used Keras 2.0.4, whose
LSTM default was
hard_sigmoid. The constructor defaults tosigmoid, which current Keras uses and fused LSTM kernels require;recurrent_activation="hard_sigmoid"reproduces the older default.
The model processes one window per call. It is not a streaming model, and the LSTM state starts at zero for every window.
LSTM lowering
Section titled “LSTM lowering”CorNetParams.unroll changes only how the LSTMs are built, not their weights:
| Form | How to build it | LiteRT lowering | Notes |
|---|---|---|---|
| Rolled | unroll=False (default) |
WHILE loop |
Float only. With TensorFlow 2.21, INT8 or 16x8 conversion of the loop aborts the converter process. Some engines do not parse WHILE. |
| Unrolled | unroll=True |
Per-timestep FULLY_CONNECTED, LOGISTIC, TANH, MUL and ADD operations |
These can be quantized to INT8 and 16x8. |
Keras 3 does not emit the fused UNIDIRECTIONAL_SEQUENCE_LSTM operator. A
fused export needs a tf-keras rebuild of the same architecture with the
weights copied over; that is export tooling, not part of this constructor.
Weights and terms
Section titled “Weights and terms”The paper publishes no code or weights. These constructors produce untrained models, suited to performance measurement only; no heart-rate accuracy is implied.