def __init__(self, classes: Sequence[str], input_len: Optional[int] = None, config: Optional[ED] = None) -> NoReturn: """ finished, checked, Parameters: ----------- classes: list, list of the classes for classification input_len: int, optional, sequence length (last dim.) of the input, defaults to `TrainCfg.input_len`, will not be used in the inference mode config: dict, optional, other hyper-parameters, including kernel sizes, etc. ref. the corresponding config file """ super().__init__() self.classes = list(classes) self.n_classes = len(classes) self.n_leads = 12 self.input_len = input_len or TrainCfg.input_len self.config = deepcopy(ECG_CRNN_CONFIG) self.config.update(deepcopy(config) or {}) if self.__DEBUG__: print( f"classes (totally {self.n_classes}) for prediction:{self.classes}" ) print( f"configuration of {self.__name__} is as follows\n{dict_to_str(self.config)}" ) cnn_choice = self.config.cnn.name.lower() if "vgg16" in cnn_choice: self.cnn = VGG16(self.n_leads, **(self.config.cnn[cnn_choice])) rnn_input_size = self.config.cnn.vgg16.num_filters[-1] elif "resnet" in cnn_choice: self.cnn = ResNet(self.n_leads, **(self.config.cnn[cnn_choice])) rnn_input_size = \ 2**len(self.config.cnn.resnet.num_blocks) * self.config.cnn.resnet.init_num_filters else: raise NotImplementedError # self.cnn_output_len = cnn_output_shape[2] if self.__DEBUG__: cnn_output_shape = self.cnn.compute_output_shape(self.input_len, batch_size=None) print( f"cnn output shape (batch_size, features, seq_len) = {cnn_output_shape}" ) if self.config.rnn.name.lower() == 'none': self.rnn = None _, clf_input_size, _ = self.cnn.compute_output_shape( self.input_len, batch_size=None) self.max_pool = nn.AdaptiveMaxPool1d((1, ), return_indices=False) elif self.config.rnn.name.lower() == 'lstm': hidden_sizes = self.config.rnn.lstm.hidden_sizes + [self.n_classes] if self.__DEBUG__: print( f"lstm hidden sizes {self.config.rnn.lstm.hidden_sizes} ---> {hidden_sizes}" ) self.rnn = StackedLSTM( input_size=rnn_input_size, hidden_sizes=hidden_sizes, bias=self.config.rnn.lstm.bias, dropouts=self.config.rnn.lstm.dropouts, bidirectional=self.config.rnn.lstm.bidirectional, return_sequences=self.config.rnn.lstm.retseq, # nonlinearity=self.config.rnn.lstm.nonlinearity, ) if self.config.rnn.lstm.retseq: self.max_pool = nn.AdaptiveMaxPool1d((1, ), return_indices=False) else: self.max_pool = None clf_input_size = self.rnn.compute_output_shape(None, None)[-1] elif self.config.rnn.name.lower() == 'attention': hidden_sizes = self.config.rnn.attention.hidden_sizes attn_in_channels = hidden_sizes[-1] if self.config.rnn.attention.bidirectional: attn_in_channels *= 2 self.rnn = nn.Sequential( StackedLSTM( input_size=rnn_input_size, hidden_sizes=hidden_sizes, bias=self.config.rnn.attention.bias, dropouts=self.config.rnn.attention.dropouts, bidirectional=self.config.rnn.attention.bidirectional, return_sequences=True, # nonlinearity=self.config.rnn.attention.nonlinearity, ), SelfAttention( in_features=attn_in_channels, head_num=self.config.rnn.attention.head_num, dropout=self.config.rnn.attention.dropout, bias=self.config.rnn.attention.bias, )) self.max_pool = nn.AdaptiveMaxPool1d((1, ), return_indices=False) clf_input_size = self.rnn[-1].compute_output_shape(None, None)[-1] else: raise NotImplementedError if self.__DEBUG__: print(f"clf_input_size = {clf_input_size}") # input of `self.clf` has shape: batch_size, channels self.clf = nn.Linear(clf_input_size, self.n_classes) self.sigmoid = nn.Sigmoid() # for making inference
def __init__(self, classes: Sequence[str], config: dict) -> NoReturn: """ finished, checked, Parameters: ----------- classes: list, list of the classes for sequence labeling config: dict, optional, other hyper-parameters, including kernel sizes, etc. ref. the corresponding config file """ super().__init__() self.classes = list(classes) self.n_classes = len(classes) self.n_leads = 12 self.config = ED(deepcopy(config)) if self.__DEBUG__: print( f"classes (totally {self.n_classes}) for prediction:{self.classes}" ) print( f"configuration of {self.__name__} is as follows\n{dict_to_str(self.config)}" ) __debug_seq_len = 4000 # currently, the CNN part only uses `MultiScopicCNN` # can be 'multi_scopic' or 'multi_scopic_leadwise' cnn_choice = self.config.cnn.name.lower() self.cnn = MultiScopicCNN(self.n_leads, **(self.config.cnn[cnn_choice])) rnn_input_size = self.cnn.compute_output_shape(__debug_seq_len, batch_size=None)[1] if self.__DEBUG__: cnn_output_shape = self.cnn.compute_output_shape(__debug_seq_len, batch_size=None) print( f"cnn output shape (batch_size, features, seq_len) = {cnn_output_shape}, given input seq_len = {__debug_seq_len}" ) __debug_seq_len = cnn_output_shape[-1] if self.config.rnn.name.lower() == 'none': self.rnn = None attn_input_size = rnn_input_size elif self.config.rnn.name.lower() == 'lstm': self.rnn = StackedLSTM( input_size=rnn_input_size, hidden_sizes=self.config.rnn.lstm.hidden_sizes, bias=self.config.rnn.lstm.bias, dropout=self.config.rnn.lstm.dropout, bidirectional=self.config.rnn.lstm.bidirectional, return_sequences=True, ) attn_input_size = self.rnn.compute_output_shape(None, None)[-1] else: raise NotImplementedError if self.__DEBUG__: if self.rnn: rnn_output_shape = self.rnn.compute_output_shape( __debug_seq_len, batch_size=None) print( f"rnn output shape (seq_len, batch_size, features) = {rnn_output_shape}, given input seq_len = {__debug_seq_len}" ) self.pool = nn.AdaptiveAvgPool1d((1, )) self.attn = nn.Sequential() attn_out_channels = self.config.attn.out_channels + [attn_input_size] self.attn.add_module( "attn", SeqLin( in_channels=attn_input_size, out_channels=attn_out_channels, activation=self.config.attn.activation, bias=self.config.attn.bias, kernel_initializer=self.config.attn.kernel_initializer, dropouts=self.config.attn.dropouts, )) self.attn.add_module("softmax", nn.Softmax(-1)) if self.__DEBUG__: print(f"") clf_input_size = self.config.attn.out_channels[-1] clf_out_channels = self.config.clf.out_channels + [self.n_classes] self.clf = SeqLin( in_channels=clf_input_size, out_channels=clf_out_channels, activation=self.config.clf.activation, bias=self.config.clf.bias, kernel_initializer=self.config.clf.kernel_initializer, dropouts=self.config.clf.dropouts, skip_last_activation=True, ) # sigmoid for inference self.softmax = nn.Softmax(-1)
class ECG_CRNN(nn.Module): """ C(R)NN models modified from the following refs. References: ----------- [1] Yao, Qihang, et al. "Time-Incremental Convolutional Neural Network for Arrhythmia Detection in Varied-Length Electrocardiogram." 2018 IEEE 16th Intl Conf on Dependable, Autonomic and Secure Computing, 16th Intl Conf on Pervasive Intelligence and Computing, 4th Intl Conf on Big Data Intelligence and Computing and Cyber Science and Technology Congress (DASC/PiCom/DataCom/CyberSciTech). IEEE, 2018. [2] Yao, Qihang, et al. "Multi-class Arrhythmia detection from 12-lead varied-length ECG using Attention-based Time-Incremental Convolutional Neural Network." Information Fusion 53 (2020): 174-182. [3] Hannun, Awni Y., et al. "Cardiologist-level arrhythmia detection and classification in ambulatory electrocardiograms using a deep neural network." Nature medicine 25.1 (2019): 65. [4] https://stanfordmlgroup.github.io/projects/ecg2/ [5] https://github.com/awni/ecg [6] CPSC2018 entry 0236 """ __DEBUG__ = True __name__ = 'ECG_CRNN' def __init__(self, classes: Sequence[str], input_len: Optional[int] = None, config: Optional[ED] = None) -> NoReturn: """ finished, checked, Parameters: ----------- classes: list, list of the classes for classification input_len: int, optional, sequence length (last dim.) of the input, defaults to `TrainCfg.input_len`, will not be used in the inference mode config: dict, optional, other hyper-parameters, including kernel sizes, etc. ref. the corresponding config file """ super().__init__() self.classes = list(classes) self.n_classes = len(classes) self.n_leads = 12 self.input_len = input_len or TrainCfg.input_len self.config = deepcopy(ECG_CRNN_CONFIG) self.config.update(deepcopy(config) or {}) if self.__DEBUG__: print( f"classes (totally {self.n_classes}) for prediction:{self.classes}" ) print( f"configuration of {self.__name__} is as follows\n{dict_to_str(self.config)}" ) cnn_choice = self.config.cnn.name.lower() if "vgg16" in cnn_choice: self.cnn = VGG16(self.n_leads, **(self.config.cnn[cnn_choice])) rnn_input_size = self.config.cnn.vgg16.num_filters[-1] elif "resnet" in cnn_choice: self.cnn = ResNet(self.n_leads, **(self.config.cnn[cnn_choice])) rnn_input_size = \ 2**len(self.config.cnn.resnet.num_blocks) * self.config.cnn.resnet.init_num_filters else: raise NotImplementedError # self.cnn_output_len = cnn_output_shape[2] if self.__DEBUG__: cnn_output_shape = self.cnn.compute_output_shape(self.input_len, batch_size=None) print( f"cnn output shape (batch_size, features, seq_len) = {cnn_output_shape}" ) if self.config.rnn.name.lower() == 'none': self.rnn = None _, clf_input_size, _ = self.cnn.compute_output_shape( self.input_len, batch_size=None) self.max_pool = nn.AdaptiveMaxPool1d((1, ), return_indices=False) elif self.config.rnn.name.lower() == 'lstm': hidden_sizes = self.config.rnn.lstm.hidden_sizes + [self.n_classes] if self.__DEBUG__: print( f"lstm hidden sizes {self.config.rnn.lstm.hidden_sizes} ---> {hidden_sizes}" ) self.rnn = StackedLSTM( input_size=rnn_input_size, hidden_sizes=hidden_sizes, bias=self.config.rnn.lstm.bias, dropouts=self.config.rnn.lstm.dropouts, bidirectional=self.config.rnn.lstm.bidirectional, return_sequences=self.config.rnn.lstm.retseq, # nonlinearity=self.config.rnn.lstm.nonlinearity, ) if self.config.rnn.lstm.retseq: self.max_pool = nn.AdaptiveMaxPool1d((1, ), return_indices=False) else: self.max_pool = None clf_input_size = self.rnn.compute_output_shape(None, None)[-1] elif self.config.rnn.name.lower() == 'attention': hidden_sizes = self.config.rnn.attention.hidden_sizes attn_in_channels = hidden_sizes[-1] if self.config.rnn.attention.bidirectional: attn_in_channels *= 2 self.rnn = nn.Sequential( StackedLSTM( input_size=rnn_input_size, hidden_sizes=hidden_sizes, bias=self.config.rnn.attention.bias, dropouts=self.config.rnn.attention.dropouts, bidirectional=self.config.rnn.attention.bidirectional, return_sequences=True, # nonlinearity=self.config.rnn.attention.nonlinearity, ), SelfAttention( in_features=attn_in_channels, head_num=self.config.rnn.attention.head_num, dropout=self.config.rnn.attention.dropout, bias=self.config.rnn.attention.bias, )) self.max_pool = nn.AdaptiveMaxPool1d((1, ), return_indices=False) clf_input_size = self.rnn[-1].compute_output_shape(None, None)[-1] else: raise NotImplementedError if self.__DEBUG__: print(f"clf_input_size = {clf_input_size}") # input of `self.clf` has shape: batch_size, channels self.clf = nn.Linear(clf_input_size, self.n_classes) self.sigmoid = nn.Sigmoid() # for making inference def forward(self, input: Tensor) -> Tensor: """ finished, partly checked (rnn part might have bugs), input: of shape (batch_size, channels, seq_len) output: of shape (batch_size, n_classes) """ x = self.cnn(input) # batch_size, channels, seq_len # print(f"cnn out shape = {x.shape}") if self.rnn: # (batch_size, channels, seq_len) -> (seq_len, batch_size, input_size) x = x.permute(2, 0, 1) x = self.rnn(x) if self.max_pool: # (seq_len, batch_size, channels) -> (batch_size, channels, seq_len) x = x.permute(1, 2, 0) x = self.max_pool(x) # (batch_size, channels, 1) # x = torch.flatten(x, start_dim=1) # (batch_size, channels) x = x.squeeze(dim=-1) else: # x of shape (batch_size, channels) pass # print(f"rnn out shape = {x.shape}") else: # (batch_size, channels, seq_len) --> (batch_size, channels) x = self.max_pool(x) # print(f"max_pool out shape = {x.shape}") # x = torch.flatten(x, start_dim=1) x = x.squeeze(dim=-1) # print(f"clf in shape = {x.shape}") pred = self.clf(x) # batch_size, n_classes return pred @torch.no_grad() def inference( self, input: Tensor, class_names: bool = False, bin_pred_thr: float = 0.5 ) -> Tuple[Union[np.ndarray, pd.DataFrame], np.ndarray]: """ finished, checked, auxiliary function to `forward`, Parameters: ----------- input: Tensor, input tensor, of shape (batch_size, channels, seq_len) class_names: bool, default False, if True, the returned scalar predictions will be a `DataFrame`, with class names for each scalar prediction bin_pred_thr: float, default 0.5, the threshold for making binary predictions from scalar predictions Returns: -------- pred: ndarray or DataFrame, scalar predictions, (and binary predictions if `class_names` is True) bin_pred: ndarray, the array (with values 0, 1 for each class) of binary prediction """ if "NSR" in self.classes: nsr_cid = self.classes.index("NSR") elif "426783006" in self.classes: nsr_cid = self.classes.index("426783006") else: nsr_cid = None pred = self.forward(input) pred = self.sigmoid(pred) bin_pred = (pred >= bin_pred_thr).int() pred = pred.cpu().detach().numpy() bin_pred = bin_pred.cpu().detach().numpy() for row_idx, row in enumerate(bin_pred): row_max_prob = pred[row_idx, ...].max() if row_max_prob < ModelCfg.bin_pred_nsr_thr and nsr_cid is not None: bin_pred[row_idx, nsr_cid] = 1 elif row.sum() == 0: bin_pred[row_idx,...] = \ (((pred[row_idx,...]+ModelCfg.bin_pred_look_again_tol) >= row_max_prob) & (pred[row_idx,...] >= ModelCfg.bin_pred_nsr_thr)).astype(int) if class_names: pred = pd.DataFrame(pred) pred.columns = self.classes # pred['bin_pred'] = pred.apply( # lambda row: np.array(self.classes)[np.where(row.values>=bin_pred_thr)[0]], # axis=1 # ) pred['bin_pred'] = '' for row_idx in range(len(pred)): pred.at[row_idx, 'bin_pred'] = \ np.array(self.classes)[np.where(bin_pred==1)[0]].tolist() return pred, bin_pred @property def module_size(self): """ """ return compute_module_size(self)
class ECG_SEQ_LAB_NET(nn.Module): """ NOT finished, SOTA model from CPSC2019 challenge (entry 0416) pipeline: multi-scopic cnn --> (bidi-lstm -->) "attention" --> seq linear """ __DEBUG__ = True __name__ = "ECG_SEQ_LAB_NET" def __init__(self, classes: Sequence[str], config: dict) -> NoReturn: """ finished, checked, Parameters: ----------- classes: list, list of the classes for sequence labeling config: dict, optional, other hyper-parameters, including kernel sizes, etc. ref. the corresponding config file """ super().__init__() self.classes = list(classes) self.n_classes = len(classes) self.n_leads = 12 self.config = ED(deepcopy(config)) if self.__DEBUG__: print( f"classes (totally {self.n_classes}) for prediction:{self.classes}" ) print( f"configuration of {self.__name__} is as follows\n{dict_to_str(self.config)}" ) __debug_seq_len = 4000 # currently, the CNN part only uses `MultiScopicCNN` # can be 'multi_scopic' or 'multi_scopic_leadwise' cnn_choice = self.config.cnn.name.lower() self.cnn = MultiScopicCNN(self.n_leads, **(self.config.cnn[cnn_choice])) rnn_input_size = self.cnn.compute_output_shape(__debug_seq_len, batch_size=None)[1] if self.__DEBUG__: cnn_output_shape = self.cnn.compute_output_shape(__debug_seq_len, batch_size=None) print( f"cnn output shape (batch_size, features, seq_len) = {cnn_output_shape}, given input seq_len = {__debug_seq_len}" ) __debug_seq_len = cnn_output_shape[-1] if self.config.rnn.name.lower() == 'none': self.rnn = None attn_input_size = rnn_input_size elif self.config.rnn.name.lower() == 'lstm': self.rnn = StackedLSTM( input_size=rnn_input_size, hidden_sizes=self.config.rnn.lstm.hidden_sizes, bias=self.config.rnn.lstm.bias, dropout=self.config.rnn.lstm.dropout, bidirectional=self.config.rnn.lstm.bidirectional, return_sequences=True, ) attn_input_size = self.rnn.compute_output_shape(None, None)[-1] else: raise NotImplementedError if self.__DEBUG__: if self.rnn: rnn_output_shape = self.rnn.compute_output_shape( __debug_seq_len, batch_size=None) print( f"rnn output shape (seq_len, batch_size, features) = {rnn_output_shape}, given input seq_len = {__debug_seq_len}" ) self.pool = nn.AdaptiveAvgPool1d((1, )) self.attn = nn.Sequential() attn_out_channels = self.config.attn.out_channels + [attn_input_size] self.attn.add_module( "attn", SeqLin( in_channels=attn_input_size, out_channels=attn_out_channels, activation=self.config.attn.activation, bias=self.config.attn.bias, kernel_initializer=self.config.attn.kernel_initializer, dropouts=self.config.attn.dropouts, )) self.attn.add_module("softmax", nn.Softmax(-1)) if self.__DEBUG__: print(f"") clf_input_size = self.config.attn.out_channels[-1] clf_out_channels = self.config.clf.out_channels + [self.n_classes] self.clf = SeqLin( in_channels=clf_input_size, out_channels=clf_out_channels, activation=self.config.clf.activation, bias=self.config.clf.bias, kernel_initializer=self.config.clf.kernel_initializer, dropouts=self.config.clf.dropouts, skip_last_activation=True, ) # sigmoid for inference self.softmax = nn.Softmax(-1) def forward(self, input: Tensor) -> Tensor: """ finished, NOT checked, input: of shape (batch_size, channels, seq_len) """ # cnn cnn_output = self.cnn(input) # (batch_size, channels, seq_len) # rnn or none if self.rnn: rnn_output = cnn_output.permute( 2, 0, 1) # (seq_len, batch_size, channels) rnn_output = self.rnn( rnn_output) # (seq_len, batch_size, channels) rnn_output = rnn_output.permute( 1, 2, 0) # (batch_size, channels, seq_len) else: rnn_output = cnn_output x = self.pool(rnn_output) # (batch_size, channels, 1) x = x.squeeze(-1) # (batch_size, channels) # attention x = self.attn(x) # (batch_size, channels) x = x.unsqueeze(-1) # (batch_size, channels, 1) x = rnn_output * x # (batch_size, channels, seq_len) x = x.permute(0, 2, 1) # (batch_size, seq_len, channels) ouput = self.clf(x) return output def compute_output_shape(self, seq_len: int, batch_size: Optional[int] = None ) -> Sequence[Union[int, type(None)]]: """ NOT finished, Parameters: ----------- seq_len: int, length of the 1d sequence batch_size: int, optional, the batch size, can be None Returns: -------- output_shape: sequence, the output shape of this block, given `seq_len` and `batch_size` """ _seq_len = seq_len output_shape = self.cnn.compute_output_shape(_seq_len, batch_size) _, _, _seq_len = output_shape if self.rnn: output_shape = self.rnn.compute_output_shape(_seq_len, batch_size) @property def module_size(self): """ """ module_parameters = filter(lambda p: p.requires_grad, self.parameters()) n_params = sum([np.prod(p.size()) for p in module_parameters]) return n_params