Add support for computing CTC / RNNT alignments
> ```python
> change_decoding_strategy
> ```

+ Can we call the change_decoding_strategy automatically in the transcribe method if necessary so that user does not need to do that?

+ Why do we have setting preserve_alignments for for RNNT but not CTC?

+ Why do we need to use asr_model.decoding.decode_tokens_to_str  for RNNT but asr_model._wer.decode_tokens_to_str for CTC models? is there a way to unify how these stuff work for both models?

+ Do we have preserve_alignments in our configs now? Do we need to add it to all configs? 1) Calling it automatically is wrong - user might not want to change config or might not want to silently update config. That's why there needs to be explicit call. 

2) CTC is cheap to decode, just argmax. RNNT is Autoregressive and expensive to compute alignments and preserve them (each alignment gets pushed onto CPU). So it needs a special flag to prevent degradation of performance in general case when used doesn't care about alignments.

3) This is unified already per model type. CTC doesn't need an additional decoding class. RNNT does. The two models are too different to share more attributes. 

4) we don't need to add preserve_alignments to config, it's optional. Eventually we will have a tutorial for RNNT models which will demonstrate this.  1- One solution can be that we store the preserve_alignments  somewhere in the transcribe(), change it true, then restore it.
3-How about adding an extra method decode_tokens_to_str to the model and it calls the main corresponding decode_tokens_to_str  based on the model. In this case, we will have a unfied API for all models. It is nice to cover differences from user's perspective as much as we can.
# Changelog
- Adds support for CTC/RNNT models to compute alignments for their respective decoding schemes. Valid schemes listed below.
- Adds support for CTC models to return a `Hypothesis` object that contains additional information about the decoded / transcribed tensor.
- Adds test for hypothesis in CTC

## Valid alignment computations
- CTC
  - CTC + Char encoding
  - CTC + Subword encoding
- RNNT
  - RNNT with Greedy (sample) and Greedy (batched, both variants)
  - RNNT with Basic Beam (beam size >= 1)

## Invalid alignment computations
- RNNT
  - RNNT with TSD / ALSD

# Example (CTC / CTC-BPE)

```python
import torch
...

trainer = Trainer(...)
asr_model = EncDecCTCModel.from_pretrained(pretrained_name, map_location=torch.device('cpu'), strict=True)
asr_model.set_trainer(trainer)

device = torch.device('cuda:0') if torch.cuda.is_available() else torch.device('cpu')
asr_model = asr_model.to(device)

# Compute alignments
audio_paths = [...]  # path of audio files
hypotheses = asr_model.transcribe(audio_paths , batch_size=32, return_hypotheses=True)

SAMPLE_IDX = 0
h = hypotheses[SAMPLE_IDX]  # type: Hypothesis
alignments = h.alignments  # log probs of first sample

token_set = []
for p in alignments:
    tokens = asr_model._wer.decode_tokens_to_str([p])
    token_set.append(tokens)

print("Predicted text :", h.text)
print()
print("Alignments :")
for t, p in enumerate(token_set):
    print(f"t = {t:4d} : {p}")
```

# Example (RNNT / RNNT-BPE)

```python
import torch
from omegaconf import OmegaConf, open_dict
...

trainer = Trainer(...)
asr_model = EncDecRNNTBPEModel.restore_from(<path to RNNT model>, map_location=torch.device('cpu'), strict=True)
asr_model.set_trainer(trainer)

device = torch.device('cuda:0') if torch.cuda.is_available() else torch.device('cpu')
asr_model = asr_model.to(device)

# Change decoding strategy to preserve alignments
decoding_cfg = asr_model.cfg.decoding

# Add `preserve_alignments` flag to RNNT models.
with open_dict(decoding_cfg):
    decoding_cfg.preserve_alignments = True

asr_model.change_decoding_strategy(decoding_cfg)  # cfg.model.decoding is the `decoding` portion of an RNNT yaml config.

# Compute alignments
audio_paths = [...]  # path of audio files
hypotheses = asr_model.transcribe(audio_paths , batch_size=32, return_hypotheses=True)

# Above hypotheses is a list of 2 items
# [0] - best Hypothesis. This is always available, for both greedy and beam search
# [1] - NBestHypothesis (if beam search size > 1). None for greedy and beam search size of 1.

SAMPLE_IDX = 0
hypotheses = hypotheses[0]  # select best hypothesis
h = hypotheses[SAMPLE_IDX]  # type: Hypothesis
alignments = h.alignments  # log probs of first sample

token_set = []
for p in alignments:
    tokens = [asr_model.decoding.decode_tokens_to_str([p_i]) for p_i in p]
    token_set.append(tokens)

print("Predicted text :", h.text)
print()
print("Alignments  :")
for t, p in enumerate(token_set):
    print(f"t = {t:4d} : {p}")
```