Skip to content

Adds batching and CTCLoss functionality to RNNProcessor - #16

Open
Sam-NT wants to merge 2 commits into
devfrom
nt/rnn-batch
Open

Adds batching and CTCLoss functionality to RNNProcessor#16
Sam-NT wants to merge 2 commits into
devfrom
nt/rnn-batch

Conversation

@Sam-NT

@Sam-NT Sam-NT commented Jul 7, 2026

Copy link
Copy Markdown

This PR adds support for RNNProcessor.partial_fit to fit on a batch (multiple samples of sequences of features) instead of one sample at a time. It further adds support for using torch.nn.CTCLoss as a customized loss function (similar to torch.nn.CrossEntropyLoss), due to its atypical function arguments.

I roughly designed the data structure containing sequence and target lengths to match the existing method for passing targets and data in one AxisArray, but it's not the cleanest. Happy to work through a better implementation in this PR if desired.

Also worth noting that torch.nn.CTCLoss is not implemented in MPS in the latest release, but is in active development. The latest nightly worked for me.

@kylmcgr kylmcgr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also add some test coverage for new functionality you added? Thanks!

Comment on lines +38 to +43
batch_train: bool = False
"""
When True, train on the full batch in a single forward/backward pass
using packed sequences to handle varying sequence lengths.
When False, train each sample individually (current behavior).
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be good to specify that when this is true, the partial fit message must contain data_len and trigger_len.

target_lens: torch.Tensor | None = None,
) -> None:
y_pred, self._state.hx = self._state.model(X, hx=self._state.hx)
y_pred, self._state.hx = self._state.model(X, hx=self._state.hx, input_lens=input_lens)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will break existing custom RNN models that can be used with the RNN processor if they don't accept input_len argument in their forward pass. If we want to enforce that the models have the input_lens parameter, we can do that (I think its would just break a couple of my models, so I could update those), otherwise it might be good only pass the kwarg when it's actually set.

if input_lens is None or target_lens is None:
raise ValueError("CTCLoss requires input_lens and target_lens in batch training mode.")
log_probs = torch.nn.functional.log_softmax(y_pred[key], dim=-1).permute(1, 0, 2)
targets = y_targ[key].flatten().long()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Targets: Tensor of size (N,S) or (sum(target_lengths)), where N=batch size and S=max target length, if shape is (N,S). It represents the target sequences. Each element in the target sequence is a class index. And the target index cannot be blank (default=0). In the (N,S) form, targets are padded to the length of the longest sequence, and stacked. In the (sum(target_lengths)) form, the targets are assumed to be un-padded and concatenated within 1 dimension.

From the torch.nn.CTCLoss documentation, it looks like it expects since (N,S) or a sum of target_lengths. Correct me if I'm wrong, but this would lead me to believe that flattening the targets would not be the correct way to pass it into the loss function. Were you able to train a model like this that decoded well? If so, maybe I'm missing something about how you are passing the targets in.

self.reset_hidden(batch_size)
self._train_step(X, y_targ, loss_fns, input_lens=input_lens, target_lens=target_lens)
else:
ez.logger.warning(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If loss is CTC, then fall-back will error since it needs data_len. Consider if we should give an informative ezmsg logger error here rather than wait for it to hit the train step and error there.

Comment on lines +241 to +242
for i in range(batch_size):
self._train_step(X[i].unsqueeze(0), {k: v[i].unsqueeze(0) for k, v in y_targ.items()}, loss_fns)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fall-back is for batch_train=True but no 'data_len'. Should this not still be a single train step for the batch rather than loop over the batch? Maybe this whole section can turn into something like this?

if self.settings.batch_train:
    input_lens = message.attrs.get("data_len")
    target_lens = message.attrs.get("trigger_len")

    if input_lens is None and any(isinstance(fn, torch.nn.CTCLoss) for fn in loss_fns.values()):
        raise ValueError(
            "CTCLoss requires 'data_len'/'trigger_len' in message.attrs."
        )
    if input_lens is None:
        ez.logger.warning(
            "batch_train=True but 'data_len' not in attrs; training on the full "
            "batch without length masking (padded timesteps contribute to the loss)."
        )

    if input_lens is not None:
        input_lens = torch.as_tensor(input_lens, dtype=torch.int64, device="cpu")
    if target_lens is not None:
        target_lens = torch.as_tensor(target_lens, dtype=torch.int64, device="cpu")

    self.reset_hidden(batch_size)
    self._train_step(X, y_targ, loss_fns, input_lens=input_lens, target_lens=target_lens)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants