Initial commit: FunASR Speech Recognition Toolkit
Update API Documentation / build-api-docs (push) Has been cancelled
Update API Documentation / build-api-docs (push) Has been cancelled
Add complete FunASR codebase including models, runtime, and documentation.
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
import time
|
||||
import torch
|
||||
import logging
|
||||
from contextlib import contextmanager
|
||||
from typing import Dict, Optional, Tuple
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from funasr.register import tables
|
||||
from funasr.utils import postprocess_utils
|
||||
from funasr.utils.datadir_writer import DatadirWriter
|
||||
from funasr.models.transducer.model import Transducer
|
||||
from funasr.train_utils.device_funcs import force_gatherable
|
||||
from funasr.models.transformer.scorers.ctc import CTCPrefixScorer
|
||||
from funasr.losses.label_smoothing_loss import LabelSmoothingLoss
|
||||
from funasr.models.transformer.scorers.length_bonus import LengthBonus
|
||||
from funasr.models.transformer.utils.nets_utils import get_transducer_task_io
|
||||
from funasr.utils.load_utils import load_audio_text_image_video, extract_fbank
|
||||
from funasr.models.transducer.beam_search_transducer import BeamSearchTransducer
|
||||
|
||||
|
||||
if LooseVersion(torch.__version__) >= LooseVersion("1.6.0"):
|
||||
from torch.cuda.amp import autocast
|
||||
else:
|
||||
# Nothing to do if torch<1.6.0
|
||||
@contextmanager
|
||||
def autocast(enabled=True):
|
||||
"""Autocast.
|
||||
|
||||
Args:
|
||||
enabled: TODO.
|
||||
"""
|
||||
yield
|
||||
|
||||
|
||||
@tables.register("model_classes", "BAT") # TODO: BAT training
|
||||
class BAT(Transducer):
|
||||
"""BAT (Boundary-Aware Transducer): Low-latency RNN-T model with boundary detection.
|
||||
|
||||
Inherits from Transducer. Designed for streaming ASR with reduced latency
|
||||
by predicting token boundaries explicitly.
|
||||
"""
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,659 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
import torch
|
||||
|
||||
from funasr.register import tables
|
||||
from funasr.models.transformer.utils.nets_utils import make_pad_mask
|
||||
|
||||
|
||||
class mae_loss(torch.nn.Module):
|
||||
|
||||
def __init__(self, normalize_length=False):
|
||||
"""Initialize mae_loss.
|
||||
|
||||
Args:
|
||||
normalize_length: TODO.
|
||||
"""
|
||||
super(mae_loss, self).__init__()
|
||||
self.normalize_length = normalize_length
|
||||
self.criterion = torch.nn.L1Loss(reduction="sum")
|
||||
|
||||
def forward(self, token_length, pre_token_length):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
token_length: TODO.
|
||||
pre_token_length: TODO.
|
||||
"""
|
||||
loss_token_normalizer = token_length.size(0)
|
||||
if self.normalize_length:
|
||||
loss_token_normalizer = token_length.sum().type(torch.float32)
|
||||
loss = self.criterion(token_length, pre_token_length)
|
||||
loss = loss / loss_token_normalizer
|
||||
return loss
|
||||
|
||||
|
||||
def cif(hidden, alphas, threshold):
|
||||
"""Cif.
|
||||
|
||||
Args:
|
||||
hidden: TODO.
|
||||
alphas: TODO.
|
||||
threshold: TODO.
|
||||
"""
|
||||
batch_size, len_time, hidden_size = hidden.size()
|
||||
|
||||
# loop varss
|
||||
integrate = torch.zeros([batch_size], device=hidden.device)
|
||||
frame = torch.zeros([batch_size, hidden_size], device=hidden.device)
|
||||
# intermediate vars along time
|
||||
list_fires = []
|
||||
list_frames = []
|
||||
|
||||
for t in range(len_time):
|
||||
alpha = alphas[:, t]
|
||||
distribution_completion = torch.ones([batch_size], device=hidden.device) - integrate
|
||||
|
||||
integrate += alpha
|
||||
list_fires.append(integrate)
|
||||
|
||||
fire_place = integrate >= threshold
|
||||
integrate = torch.where(
|
||||
fire_place, integrate - torch.ones([batch_size], device=hidden.device), integrate
|
||||
)
|
||||
cur = torch.where(fire_place, distribution_completion, alpha)
|
||||
remainds = alpha - cur
|
||||
|
||||
frame += cur[:, None] * hidden[:, t, :]
|
||||
list_frames.append(frame)
|
||||
frame = torch.where(
|
||||
fire_place[:, None].repeat(1, hidden_size), remainds[:, None] * hidden[:, t, :], frame
|
||||
)
|
||||
|
||||
fires = torch.stack(list_fires, 1)
|
||||
frames = torch.stack(list_frames, 1)
|
||||
list_ls = []
|
||||
len_labels = torch.round(alphas.sum(-1)).int()
|
||||
max_label_len = len_labels.max()
|
||||
for b in range(batch_size):
|
||||
fire = fires[b, :]
|
||||
l = torch.index_select(frames[b, :, :], 0, torch.nonzero(fire >= threshold).squeeze(-1))
|
||||
pad_l = torch.zeros([max_label_len - l.size(0), hidden_size], device=hidden.device)
|
||||
list_ls.append(torch.cat([l, pad_l], 0))
|
||||
return torch.stack(list_ls, 0), fires
|
||||
|
||||
|
||||
def cif_wo_hidden(alphas, threshold):
|
||||
"""Cif wo hidden.
|
||||
|
||||
Args:
|
||||
alphas: TODO.
|
||||
threshold: TODO.
|
||||
"""
|
||||
batch_size, len_time = alphas.size()
|
||||
|
||||
# loop varss
|
||||
integrate = torch.zeros([batch_size], device=alphas.device)
|
||||
# intermediate vars along time
|
||||
list_fires = []
|
||||
|
||||
for t in range(len_time):
|
||||
alpha = alphas[:, t]
|
||||
|
||||
integrate += alpha
|
||||
list_fires.append(integrate)
|
||||
|
||||
fire_place = integrate >= threshold
|
||||
integrate = torch.where(
|
||||
fire_place,
|
||||
integrate - torch.ones([batch_size], device=alphas.device) * threshold,
|
||||
integrate,
|
||||
)
|
||||
|
||||
fires = torch.stack(list_fires, 1)
|
||||
return fires
|
||||
|
||||
|
||||
@tables.register("predictor_classes", "CifPredictorV3")
|
||||
class CifPredictorV3(torch.nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
idim,
|
||||
l_order,
|
||||
r_order,
|
||||
threshold=1.0,
|
||||
dropout=0.1,
|
||||
smooth_factor=1.0,
|
||||
noise_threshold=0,
|
||||
tail_threshold=0.0,
|
||||
tf2torch_tensor_name_prefix_torch="predictor",
|
||||
tf2torch_tensor_name_prefix_tf="seq2seq/cif",
|
||||
smooth_factor2=1.0,
|
||||
noise_threshold2=0,
|
||||
upsample_times=5,
|
||||
upsample_type="cnn",
|
||||
use_cif1_cnn=True,
|
||||
tail_mask=True,
|
||||
):
|
||||
"""Initialize CifPredictorV3.
|
||||
|
||||
Args:
|
||||
idim: TODO.
|
||||
l_order: TODO.
|
||||
r_order: TODO.
|
||||
threshold: TODO.
|
||||
dropout: TODO.
|
||||
smooth_factor: TODO.
|
||||
noise_threshold: TODO.
|
||||
tail_threshold: TODO.
|
||||
tf2torch_tensor_name_prefix_torch: TODO.
|
||||
tf2torch_tensor_name_prefix_tf: TODO.
|
||||
smooth_factor2: TODO.
|
||||
noise_threshold2: TODO.
|
||||
upsample_times: TODO.
|
||||
upsample_type: TODO.
|
||||
use_cif1_cnn: TODO.
|
||||
tail_mask: TODO.
|
||||
"""
|
||||
super(CifPredictorV3, self).__init__()
|
||||
|
||||
self.pad = torch.nn.ConstantPad1d((l_order, r_order), 0)
|
||||
self.cif_conv1d = torch.nn.Conv1d(idim, idim, l_order + r_order + 1)
|
||||
self.cif_output = torch.nn.Linear(idim, 1)
|
||||
self.dropout = torch.nn.Dropout(p=dropout)
|
||||
self.threshold = threshold
|
||||
self.smooth_factor = smooth_factor
|
||||
self.noise_threshold = noise_threshold
|
||||
self.tail_threshold = tail_threshold
|
||||
self.tf2torch_tensor_name_prefix_torch = tf2torch_tensor_name_prefix_torch
|
||||
self.tf2torch_tensor_name_prefix_tf = tf2torch_tensor_name_prefix_tf
|
||||
|
||||
self.upsample_times = upsample_times
|
||||
self.upsample_type = upsample_type
|
||||
self.use_cif1_cnn = use_cif1_cnn
|
||||
if self.upsample_type == "cnn":
|
||||
self.upsample_cnn = torch.nn.ConvTranspose1d(
|
||||
idim, idim, self.upsample_times, self.upsample_times
|
||||
)
|
||||
self.cif_output2 = torch.nn.Linear(idim, 1)
|
||||
elif self.upsample_type == "cnn_blstm":
|
||||
self.upsample_cnn = torch.nn.ConvTranspose1d(
|
||||
idim, idim, self.upsample_times, self.upsample_times
|
||||
)
|
||||
self.blstm = torch.nn.LSTM(
|
||||
idim, idim, 1, bias=True, batch_first=True, dropout=0.0, bidirectional=True
|
||||
)
|
||||
self.cif_output2 = torch.nn.Linear(idim * 2, 1)
|
||||
elif self.upsample_type == "cnn_attn":
|
||||
self.upsample_cnn = torch.nn.ConvTranspose1d(
|
||||
idim, idim, self.upsample_times, self.upsample_times
|
||||
)
|
||||
from funasr.models.transformer.encoder import EncoderLayer as TransformerEncoderLayer
|
||||
from funasr.models.transformer.attention import MultiHeadedAttention
|
||||
from funasr.models.transformer.positionwise_feed_forward import PositionwiseFeedForward
|
||||
|
||||
positionwise_layer_args = (
|
||||
idim,
|
||||
idim * 2,
|
||||
0.1,
|
||||
)
|
||||
self.self_attn = TransformerEncoderLayer(
|
||||
idim,
|
||||
MultiHeadedAttention(4, idim, 0.1),
|
||||
PositionwiseFeedForward(*positionwise_layer_args),
|
||||
0.1,
|
||||
True, # normalize_before,
|
||||
False, # concat_after,
|
||||
)
|
||||
self.cif_output2 = torch.nn.Linear(idim, 1)
|
||||
self.smooth_factor2 = smooth_factor2
|
||||
self.noise_threshold2 = noise_threshold2
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden,
|
||||
target_label=None,
|
||||
mask=None,
|
||||
ignore_id=-1,
|
||||
mask_chunk_predictor=None,
|
||||
target_label_length=None,
|
||||
):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
hidden: TODO.
|
||||
target_label: TODO.
|
||||
mask: TODO.
|
||||
ignore_id: TODO.
|
||||
mask_chunk_predictor: TODO.
|
||||
target_label_length: TODO.
|
||||
"""
|
||||
h = hidden
|
||||
context = h.transpose(1, 2)
|
||||
queries = self.pad(context)
|
||||
output = torch.relu(self.cif_conv1d(queries))
|
||||
|
||||
# alphas2 is an extra head for timestamp prediction
|
||||
if not self.use_cif1_cnn:
|
||||
_output = context
|
||||
else:
|
||||
_output = output
|
||||
if self.upsample_type == "cnn":
|
||||
output2 = self.upsample_cnn(_output)
|
||||
output2 = output2.transpose(1, 2)
|
||||
elif self.upsample_type == "cnn_blstm":
|
||||
output2 = self.upsample_cnn(_output)
|
||||
output2 = output2.transpose(1, 2)
|
||||
output2, (_, _) = self.blstm(output2)
|
||||
elif self.upsample_type == "cnn_attn":
|
||||
output2 = self.upsample_cnn(_output)
|
||||
output2 = output2.transpose(1, 2)
|
||||
output2, _ = self.self_attn(output2, mask)
|
||||
|
||||
alphas2 = torch.sigmoid(self.cif_output2(output2))
|
||||
alphas2 = torch.nn.functional.relu(alphas2 * self.smooth_factor2 - self.noise_threshold2)
|
||||
# repeat the mask in T demension to match the upsampled length
|
||||
if mask is not None:
|
||||
mask2 = (
|
||||
mask.repeat(1, self.upsample_times, 1)
|
||||
.transpose(-1, -2)
|
||||
.reshape(alphas2.shape[0], -1)
|
||||
)
|
||||
mask2 = mask2.unsqueeze(-1)
|
||||
alphas2 = alphas2 * mask2
|
||||
alphas2 = alphas2.squeeze(-1)
|
||||
token_num2 = alphas2.sum(-1)
|
||||
|
||||
output = output.transpose(1, 2)
|
||||
|
||||
output = self.cif_output(output)
|
||||
alphas = torch.sigmoid(output)
|
||||
alphas = torch.nn.functional.relu(alphas * self.smooth_factor - self.noise_threshold)
|
||||
if mask is not None:
|
||||
mask = mask.transpose(-1, -2).float()
|
||||
alphas = alphas * mask
|
||||
if mask_chunk_predictor is not None:
|
||||
alphas = alphas * mask_chunk_predictor
|
||||
alphas = alphas.squeeze(-1)
|
||||
mask = mask.squeeze(-1)
|
||||
if target_label_length is not None:
|
||||
target_length = target_label_length
|
||||
elif target_label is not None:
|
||||
target_length = (target_label != ignore_id).float().sum(-1)
|
||||
else:
|
||||
target_length = None
|
||||
token_num = alphas.sum(-1)
|
||||
|
||||
if target_length is not None:
|
||||
alphas *= (target_length / token_num)[:, None].repeat(1, alphas.size(1))
|
||||
elif self.tail_threshold > 0.0:
|
||||
hidden, alphas, token_num = self.tail_process_fn(hidden, alphas, token_num, mask=mask)
|
||||
|
||||
acoustic_embeds, cif_peak = cif(hidden, alphas, self.threshold)
|
||||
if target_length is None and self.tail_threshold > 0.0:
|
||||
token_num_int = torch.max(token_num).type(torch.int32).item()
|
||||
acoustic_embeds = acoustic_embeds[:, :token_num_int, :]
|
||||
return acoustic_embeds, token_num, alphas, cif_peak, token_num2
|
||||
|
||||
def get_upsample_timestamp(self, hidden, mask=None, token_num=None):
|
||||
"""Get upsample timestamp.
|
||||
|
||||
Args:
|
||||
hidden: TODO.
|
||||
mask: TODO.
|
||||
token_num: TODO.
|
||||
"""
|
||||
h = hidden
|
||||
b = hidden.shape[0]
|
||||
context = h.transpose(1, 2)
|
||||
queries = self.pad(context)
|
||||
output = torch.relu(self.cif_conv1d(queries))
|
||||
|
||||
# alphas2 is an extra head for timestamp prediction
|
||||
if not self.use_cif1_cnn:
|
||||
_output = context
|
||||
else:
|
||||
_output = output
|
||||
if self.upsample_type == "cnn":
|
||||
output2 = self.upsample_cnn(_output)
|
||||
output2 = output2.transpose(1, 2)
|
||||
elif self.upsample_type == "cnn_blstm":
|
||||
output2 = self.upsample_cnn(_output)
|
||||
output2 = output2.transpose(1, 2)
|
||||
output2, (_, _) = self.blstm(output2)
|
||||
elif self.upsample_type == "cnn_attn":
|
||||
output2 = self.upsample_cnn(_output)
|
||||
output2 = output2.transpose(1, 2)
|
||||
output2, _ = self.self_attn(output2, mask)
|
||||
alphas2 = torch.sigmoid(self.cif_output2(output2))
|
||||
alphas2 = torch.nn.functional.relu(alphas2 * self.smooth_factor2 - self.noise_threshold2)
|
||||
# repeat the mask in T demension to match the upsampled length
|
||||
if mask is not None:
|
||||
mask2 = (
|
||||
mask.repeat(1, self.upsample_times, 1)
|
||||
.transpose(-1, -2)
|
||||
.reshape(alphas2.shape[0], -1)
|
||||
)
|
||||
mask2 = mask2.unsqueeze(-1)
|
||||
alphas2 = alphas2 * mask2
|
||||
alphas2 = alphas2.squeeze(-1)
|
||||
_token_num = alphas2.sum(-1)
|
||||
if token_num is not None:
|
||||
alphas2 *= (token_num / _token_num)[:, None].repeat(1, alphas2.size(1))
|
||||
# re-downsample
|
||||
ds_alphas = alphas2.reshape(b, -1, self.upsample_times).sum(-1)
|
||||
ds_cif_peak = cif_wo_hidden(ds_alphas, self.threshold - 1e-4)
|
||||
# upsampled alphas and cif_peak
|
||||
us_alphas = alphas2
|
||||
us_cif_peak = cif_wo_hidden(us_alphas, self.threshold - 1e-4)
|
||||
return ds_alphas, ds_cif_peak, us_alphas, us_cif_peak
|
||||
|
||||
def tail_process_fn(self, hidden, alphas, token_num=None, mask=None):
|
||||
"""Tail process fn.
|
||||
|
||||
Args:
|
||||
hidden: TODO.
|
||||
alphas: TODO.
|
||||
token_num: TODO.
|
||||
mask: TODO.
|
||||
"""
|
||||
b, t, d = hidden.size()
|
||||
tail_threshold = self.tail_threshold
|
||||
if mask is not None:
|
||||
zeros_t = torch.zeros((b, 1), dtype=torch.float32, device=alphas.device)
|
||||
ones_t = torch.ones_like(zeros_t)
|
||||
mask_1 = torch.cat([mask, zeros_t], dim=1)
|
||||
mask_2 = torch.cat([ones_t, mask], dim=1)
|
||||
mask = mask_2 - mask_1
|
||||
tail_threshold = mask * tail_threshold
|
||||
alphas = torch.cat([alphas, zeros_t], dim=1)
|
||||
alphas = torch.add(alphas, tail_threshold)
|
||||
else:
|
||||
tail_threshold = torch.tensor([tail_threshold], dtype=alphas.dtype).to(alphas.device)
|
||||
tail_threshold = torch.reshape(tail_threshold, (1, 1))
|
||||
alphas = torch.cat([alphas, tail_threshold], dim=1)
|
||||
zeros = torch.zeros((b, 1, d), dtype=hidden.dtype).to(hidden.device)
|
||||
hidden = torch.cat([hidden, zeros], dim=1)
|
||||
token_num = alphas.sum(dim=-1)
|
||||
token_num_floor = torch.floor(token_num)
|
||||
|
||||
return hidden, alphas, token_num_floor
|
||||
|
||||
def gen_frame_alignments(
|
||||
self, alphas: torch.Tensor = None, encoder_sequence_length: torch.Tensor = None
|
||||
):
|
||||
"""Gen frame alignments.
|
||||
|
||||
Args:
|
||||
alphas: TODO.
|
||||
encoder_sequence_length: TODO.
|
||||
"""
|
||||
batch_size, maximum_length = alphas.size()
|
||||
int_type = torch.int32
|
||||
|
||||
is_training = self.training
|
||||
if is_training:
|
||||
token_num = torch.round(torch.sum(alphas, dim=1)).type(int_type)
|
||||
else:
|
||||
token_num = torch.floor(torch.sum(alphas, dim=1)).type(int_type)
|
||||
|
||||
max_token_num = torch.max(token_num).item()
|
||||
|
||||
alphas_cumsum = torch.cumsum(alphas, dim=1)
|
||||
alphas_cumsum = torch.floor(alphas_cumsum).type(int_type)
|
||||
alphas_cumsum = alphas_cumsum[:, None, :].repeat(1, max_token_num, 1)
|
||||
|
||||
index = torch.ones([batch_size, max_token_num], dtype=int_type)
|
||||
index = torch.cumsum(index, dim=1)
|
||||
index = index[:, :, None].repeat(1, 1, maximum_length).to(alphas_cumsum.device)
|
||||
|
||||
index_div = torch.floor(torch.true_divide(alphas_cumsum, index)).type(int_type)
|
||||
index_div_bool_zeros = index_div.eq(0)
|
||||
index_div_bool_zeros_count = torch.sum(index_div_bool_zeros, dim=-1) + 1
|
||||
index_div_bool_zeros_count = torch.clamp(
|
||||
index_div_bool_zeros_count, 0, encoder_sequence_length.max()
|
||||
)
|
||||
token_num_mask = (~make_pad_mask(token_num, maxlen=max_token_num)).to(token_num.device)
|
||||
index_div_bool_zeros_count *= token_num_mask
|
||||
|
||||
index_div_bool_zeros_count_tile = index_div_bool_zeros_count[:, :, None].repeat(
|
||||
1, 1, maximum_length
|
||||
)
|
||||
ones = torch.ones_like(index_div_bool_zeros_count_tile)
|
||||
zeros = torch.zeros_like(index_div_bool_zeros_count_tile)
|
||||
ones = torch.cumsum(ones, dim=2)
|
||||
cond = index_div_bool_zeros_count_tile == ones
|
||||
index_div_bool_zeros_count_tile = torch.where(cond, zeros, ones)
|
||||
|
||||
index_div_bool_zeros_count_tile_bool = index_div_bool_zeros_count_tile.type(torch.bool)
|
||||
index_div_bool_zeros_count_tile = 1 - index_div_bool_zeros_count_tile_bool.type(int_type)
|
||||
index_div_bool_zeros_count_tile_out = torch.sum(index_div_bool_zeros_count_tile, dim=1)
|
||||
index_div_bool_zeros_count_tile_out = index_div_bool_zeros_count_tile_out.type(int_type)
|
||||
predictor_mask = (
|
||||
(~make_pad_mask(encoder_sequence_length, maxlen=encoder_sequence_length.max()))
|
||||
.type(int_type)
|
||||
.to(encoder_sequence_length.device)
|
||||
)
|
||||
index_div_bool_zeros_count_tile_out = index_div_bool_zeros_count_tile_out * predictor_mask
|
||||
|
||||
predictor_alignments = index_div_bool_zeros_count_tile_out
|
||||
predictor_alignments_length = predictor_alignments.sum(-1).type(
|
||||
encoder_sequence_length.dtype
|
||||
)
|
||||
return predictor_alignments.detach(), predictor_alignments_length.detach()
|
||||
|
||||
|
||||
@tables.register("predictor_classes", "CifPredictorV3Export")
|
||||
class CifPredictorV3Export(torch.nn.Module):
|
||||
def __init__(self, model, **kwargs):
|
||||
"""Initialize CifPredictorV3Export.
|
||||
|
||||
Args:
|
||||
model: Model instance or model name.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.pad = model.pad
|
||||
self.cif_conv1d = model.cif_conv1d
|
||||
self.cif_output = model.cif_output
|
||||
self.threshold = model.threshold
|
||||
self.smooth_factor = model.smooth_factor
|
||||
self.noise_threshold = model.noise_threshold
|
||||
self.tail_threshold = model.tail_threshold
|
||||
|
||||
self.upsample_times = model.upsample_times
|
||||
self.upsample_cnn = model.upsample_cnn
|
||||
self.blstm = model.blstm
|
||||
self.cif_output2 = model.cif_output2
|
||||
self.smooth_factor2 = model.smooth_factor2
|
||||
self.noise_threshold2 = model.noise_threshold2
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden: torch.Tensor,
|
||||
mask: torch.Tensor,
|
||||
):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
hidden: TODO.
|
||||
mask: TODO.
|
||||
"""
|
||||
h = hidden
|
||||
context = h.transpose(1, 2)
|
||||
queries = self.pad(context)
|
||||
output = torch.relu(self.cif_conv1d(queries))
|
||||
output = output.transpose(1, 2)
|
||||
|
||||
output = self.cif_output(output)
|
||||
alphas = torch.sigmoid(output)
|
||||
alphas = torch.nn.functional.relu(alphas * self.smooth_factor - self.noise_threshold)
|
||||
mask = mask.transpose(-1, -2).float()
|
||||
alphas = alphas * mask
|
||||
alphas = alphas.squeeze(-1)
|
||||
token_num = alphas.sum(-1)
|
||||
|
||||
mask = mask.squeeze(-1)
|
||||
hidden, alphas, token_num = self.tail_process_fn(hidden, alphas, mask=mask)
|
||||
acoustic_embeds, cif_peak = cif_export(hidden, alphas, self.threshold)
|
||||
|
||||
return acoustic_embeds, token_num, alphas, cif_peak
|
||||
|
||||
def get_upsample_timestmap(self, hidden, mask=None, token_num=None):
|
||||
"""Get upsample timestmap.
|
||||
|
||||
Args:
|
||||
hidden: TODO.
|
||||
mask: TODO.
|
||||
token_num: TODO.
|
||||
"""
|
||||
h = hidden
|
||||
b = hidden.shape[0]
|
||||
context = h.transpose(1, 2)
|
||||
|
||||
# generate alphas2
|
||||
_output = context
|
||||
output2 = self.upsample_cnn(_output)
|
||||
output2 = output2.transpose(1, 2)
|
||||
output2, (_, _) = self.blstm(output2)
|
||||
alphas2 = torch.sigmoid(self.cif_output2(output2))
|
||||
alphas2 = torch.nn.functional.relu(alphas2 * self.smooth_factor2 - self.noise_threshold2)
|
||||
|
||||
mask = (
|
||||
mask.repeat(1, self.upsample_times, 1).transpose(-1, -2).reshape(alphas2.shape[0], -1)
|
||||
)
|
||||
mask = mask.unsqueeze(-1)
|
||||
alphas2 = alphas2 * mask
|
||||
alphas2 = alphas2.squeeze(-1)
|
||||
_token_num = alphas2.sum(-1)
|
||||
alphas2 *= (token_num / _token_num)[:, None].repeat(1, alphas2.size(1))
|
||||
# upsampled alphas and cif_peak
|
||||
us_alphas = alphas2
|
||||
us_cif_peak = cif_wo_hidden_export(us_alphas, self.threshold - 1e-4)
|
||||
return us_alphas, us_cif_peak
|
||||
|
||||
def tail_process_fn(self, hidden, alphas, token_num=None, mask=None):
|
||||
"""Tail process fn.
|
||||
|
||||
Args:
|
||||
hidden: TODO.
|
||||
alphas: TODO.
|
||||
token_num: TODO.
|
||||
mask: TODO.
|
||||
"""
|
||||
b, t, d = hidden.size()
|
||||
tail_threshold = self.tail_threshold
|
||||
|
||||
zeros_t = torch.zeros((b, 1), dtype=torch.float32, device=alphas.device)
|
||||
ones_t = torch.ones_like(zeros_t)
|
||||
|
||||
mask_1 = torch.cat([mask, zeros_t], dim=1)
|
||||
mask_2 = torch.cat([ones_t, mask], dim=1)
|
||||
mask = mask_2 - mask_1
|
||||
tail_threshold = mask * tail_threshold
|
||||
alphas = torch.cat([alphas, zeros_t], dim=1)
|
||||
alphas = torch.add(alphas, tail_threshold)
|
||||
|
||||
zeros = torch.zeros((b, 1, d), dtype=hidden.dtype).to(hidden.device)
|
||||
hidden = torch.cat([hidden, zeros], dim=1)
|
||||
token_num = alphas.sum(dim=-1)
|
||||
token_num_floor = torch.floor(token_num)
|
||||
|
||||
return hidden, alphas, token_num_floor
|
||||
|
||||
|
||||
@torch.jit.script
|
||||
def cif_export(hidden, alphas, threshold: float):
|
||||
"""Cif export.
|
||||
|
||||
Args:
|
||||
hidden: TODO.
|
||||
alphas: TODO.
|
||||
threshold: TODO.
|
||||
"""
|
||||
batch_size, len_time, hidden_size = hidden.size()
|
||||
threshold = torch.tensor([threshold], dtype=alphas.dtype).to(alphas.device)
|
||||
|
||||
# loop varss
|
||||
integrate = torch.zeros([batch_size], dtype=alphas.dtype, device=hidden.device)
|
||||
frame = torch.zeros([batch_size, hidden_size], dtype=hidden.dtype, device=hidden.device)
|
||||
# intermediate vars along time
|
||||
list_fires = []
|
||||
list_frames = []
|
||||
|
||||
for t in range(len_time):
|
||||
alpha = alphas[:, t]
|
||||
distribution_completion = (
|
||||
torch.ones([batch_size], dtype=alphas.dtype, device=hidden.device) - integrate
|
||||
)
|
||||
|
||||
integrate += alpha
|
||||
list_fires.append(integrate)
|
||||
|
||||
fire_place = integrate >= threshold
|
||||
integrate = torch.where(
|
||||
fire_place,
|
||||
integrate - torch.ones([batch_size], dtype=alphas.dtype, device=hidden.device),
|
||||
integrate,
|
||||
)
|
||||
cur = torch.where(fire_place, distribution_completion, alpha)
|
||||
remainds = alpha - cur
|
||||
|
||||
frame += cur[:, None] * hidden[:, t, :]
|
||||
list_frames.append(frame)
|
||||
frame = torch.where(
|
||||
fire_place[:, None].repeat(1, hidden_size), remainds[:, None] * hidden[:, t, :], frame
|
||||
)
|
||||
|
||||
fires = torch.stack(list_fires, 1)
|
||||
frames = torch.stack(list_frames, 1)
|
||||
|
||||
fire_idxs = fires >= threshold
|
||||
frame_fires = torch.zeros_like(hidden)
|
||||
max_label_len = frames[0, fire_idxs[0]].size(0)
|
||||
for b in range(batch_size):
|
||||
frame_fire = frames[b, fire_idxs[b]]
|
||||
frame_len = frame_fire.size(0)
|
||||
frame_fires[b, :frame_len, :] = frame_fire
|
||||
|
||||
if frame_len >= max_label_len:
|
||||
max_label_len = frame_len
|
||||
frame_fires = frame_fires[:, :max_label_len, :]
|
||||
return frame_fires, fires
|
||||
|
||||
|
||||
@torch.jit.script
|
||||
def cif_wo_hidden_export(alphas, threshold: float):
|
||||
"""Cif wo hidden export.
|
||||
|
||||
Args:
|
||||
alphas: TODO.
|
||||
threshold: TODO.
|
||||
"""
|
||||
batch_size, len_time = alphas.size()
|
||||
|
||||
# loop varss
|
||||
integrate = torch.zeros([batch_size], dtype=alphas.dtype, device=alphas.device)
|
||||
# intermediate vars along time
|
||||
list_fires = []
|
||||
|
||||
for t in range(len_time):
|
||||
alpha = alphas[:, t]
|
||||
|
||||
integrate += alpha
|
||||
list_fires.append(integrate)
|
||||
|
||||
fire_place = integrate >= threshold
|
||||
integrate = torch.where(
|
||||
fire_place,
|
||||
integrate - torch.ones([batch_size], device=alphas.device) * threshold,
|
||||
integrate,
|
||||
)
|
||||
|
||||
fires = torch.stack(list_fires, 1)
|
||||
return fires
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
import torch
|
||||
import types
|
||||
|
||||
from funasr.register import tables
|
||||
|
||||
|
||||
def export_rebuild_model(model, **kwargs):
|
||||
"""Export rebuild model.
|
||||
|
||||
Args:
|
||||
model: Model instance or model name.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
is_onnx = kwargs.get("type", "onnx") == "onnx"
|
||||
encoder_class = tables.encoder_classes.get(kwargs["encoder"] + "Export")
|
||||
model.encoder = encoder_class(model.encoder, onnx=is_onnx)
|
||||
|
||||
predictor_class = tables.predictor_classes.get(kwargs["predictor"] + "Export")
|
||||
model.predictor = predictor_class(model.predictor, onnx=is_onnx)
|
||||
|
||||
decoder_class = tables.decoder_classes.get(kwargs["decoder"] + "Export")
|
||||
model.decoder = decoder_class(model.decoder, onnx=is_onnx)
|
||||
|
||||
from funasr.utils.torch_function import sequence_mask
|
||||
|
||||
model.make_pad_mask = sequence_mask(kwargs["max_seq_len"], flip=False)
|
||||
|
||||
model.forward = types.MethodType(export_forward, model)
|
||||
model.export_dummy_inputs = types.MethodType(export_dummy_inputs, model)
|
||||
model.export_input_names = types.MethodType(export_input_names, model)
|
||||
model.export_output_names = types.MethodType(export_output_names, model)
|
||||
model.export_dynamic_axes = types.MethodType(export_dynamic_axes, model)
|
||||
|
||||
model.export_name = "model"
|
||||
|
||||
return model
|
||||
|
||||
|
||||
def export_forward(
|
||||
self,
|
||||
speech: torch.Tensor,
|
||||
speech_lengths: torch.Tensor,
|
||||
):
|
||||
# a. To device
|
||||
"""Export forward.
|
||||
|
||||
Args:
|
||||
speech: Speech audio tensor, shape (batch, time).
|
||||
speech_lengths: Length of each speech sample.
|
||||
"""
|
||||
batch = {"speech": speech, "speech_lengths": speech_lengths}
|
||||
|
||||
enc, enc_len = self.encoder(**batch)
|
||||
mask = self.make_pad_mask(enc_len)[:, None, :]
|
||||
pre_acoustic_embeds, pre_token_length, alphas, pre_peak_index = self.predictor(enc, mask)
|
||||
pre_token_length = pre_token_length.round().type(torch.int32)
|
||||
|
||||
decoder_out, _ = self.decoder(enc, enc_len, pre_acoustic_embeds, pre_token_length)
|
||||
decoder_out = torch.log_softmax(decoder_out, dim=-1)
|
||||
|
||||
# get predicted timestamps
|
||||
us_alphas, us_cif_peak = self.predictor.get_upsample_timestmap(enc, mask, pre_token_length)
|
||||
|
||||
return decoder_out, pre_token_length, us_alphas, us_cif_peak
|
||||
|
||||
|
||||
def export_dummy_inputs(self):
|
||||
"""Export dummy inputs."""
|
||||
speech = torch.randn(2, 30, 560)
|
||||
speech_lengths = torch.tensor([6, 30], dtype=torch.int32)
|
||||
return (speech, speech_lengths)
|
||||
|
||||
|
||||
def export_input_names(self):
|
||||
"""Export input names."""
|
||||
return ["speech", "speech_lengths"]
|
||||
|
||||
|
||||
def export_output_names(self):
|
||||
"""Export output names."""
|
||||
return ["logits", "token_num", "us_alphas", "us_cif_peak"]
|
||||
|
||||
|
||||
def export_dynamic_axes(self):
|
||||
"""Export dynamic axes."""
|
||||
return {
|
||||
"speech": {0: "batch_size", 1: "feats_length"},
|
||||
"speech_lengths": {
|
||||
0: "batch_size",
|
||||
},
|
||||
"logits": {0: "batch_size", 1: "logits_length"},
|
||||
"us_alphas": {0: "batch_size", 1: "alphas_length"},
|
||||
"us_cif_peak": {0: "batch_size", 1: "alphas_length"},
|
||||
}
|
||||
|
||||
|
||||
def export_name(self):
|
||||
"""Export name."""
|
||||
return "model.onnx"
|
||||
@@ -0,0 +1,441 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
import copy
|
||||
import time
|
||||
import torch
|
||||
import logging
|
||||
from contextlib import contextmanager
|
||||
from distutils.version import LooseVersion
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from funasr.register import tables
|
||||
from funasr.models.ctc.ctc import CTC
|
||||
from funasr.utils import postprocess_utils
|
||||
from funasr.metrics.compute_acc import th_accuracy
|
||||
from funasr.utils.datadir_writer import DatadirWriter
|
||||
from funasr.models.paraformer.model import Paraformer
|
||||
from funasr.models.paraformer.search import Hypothesis
|
||||
from funasr.train_utils.device_funcs import force_gatherable
|
||||
from funasr.models.transformer.utils.add_sos_eos import add_sos_eos
|
||||
from funasr.utils.timestamp_tools import ts_prediction_lfr6_standard
|
||||
from funasr.models.transformer.utils.nets_utils import make_pad_mask, pad_list
|
||||
from funasr.utils.load_utils import load_audio_text_image_video, extract_fbank
|
||||
from funasr.train_utils.device_funcs import to_device
|
||||
|
||||
if LooseVersion(torch.__version__) >= LooseVersion("1.6.0"):
|
||||
from torch.cuda.amp import autocast
|
||||
else:
|
||||
# Nothing to do if torch<1.6.0
|
||||
@contextmanager
|
||||
def autocast(enabled=True):
|
||||
"""Autocast.
|
||||
|
||||
Args:
|
||||
enabled: TODO.
|
||||
"""
|
||||
yield
|
||||
|
||||
|
||||
@tables.register("model_classes", "BiCifParaformer")
|
||||
class BiCifParaformer(Paraformer):
|
||||
"""BiCifParaformer: Paraformer with Bidirectional CIF for Timestamp Prediction.
|
||||
|
||||
Extends Paraformer with a second CIF predictor that provides accurate
|
||||
character-level timestamp prediction alongside ASR. Uses bidirectional
|
||||
information flow for better alignment between audio frames and text tokens.
|
||||
|
||||
Reference:
|
||||
- FunASR: A Fundamental End-to-End Speech Recognition Toolkit (https://arxiv.org/abs/2305.11013)
|
||||
- Achieving timestamp prediction while recognizing with non-autoregressive end-to-end ASR model
|
||||
(https://arxiv.org/abs/2301.12343)
|
||||
|
||||
Output:
|
||||
{"key": str, "text": str, "timestamp": [[start_ms, end_ms], ...]}
|
||||
|
||||
Author: Speech Lab of DAMO Academy, Alibaba Group
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize BiCifParaformer.
|
||||
|
||||
Args:
|
||||
*args: Variable positional arguments.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _calc_pre2_loss(
|
||||
self,
|
||||
encoder_out: torch.Tensor,
|
||||
encoder_out_lens: torch.Tensor,
|
||||
ys_pad: torch.Tensor,
|
||||
ys_pad_lens: torch.Tensor,
|
||||
):
|
||||
"""Internal: calc pre2 loss.
|
||||
|
||||
Args:
|
||||
encoder_out: Encoder output tensor.
|
||||
encoder_out_lens: Encoder output lengths.
|
||||
ys_pad: TODO.
|
||||
ys_pad_lens: Lengths of ys_pad.
|
||||
"""
|
||||
encoder_out_mask = (
|
||||
~make_pad_mask(encoder_out_lens, maxlen=encoder_out.size(1))[:, None, :]
|
||||
).to(encoder_out.device)
|
||||
if self.predictor_bias == 1:
|
||||
_, ys_pad = add_sos_eos(ys_pad, self.sos, self.eos, self.ignore_id)
|
||||
ys_pad_lens = ys_pad_lens + self.predictor_bias
|
||||
_, _, _, _, pre_token_length2 = self.predictor(
|
||||
encoder_out, ys_pad, encoder_out_mask, ignore_id=self.ignore_id
|
||||
)
|
||||
|
||||
# loss_pre = self.criterion_pre(ys_pad_lens.type_as(pre_token_length), pre_token_length)
|
||||
loss_pre2 = self.criterion_pre(ys_pad_lens.type_as(pre_token_length2), pre_token_length2)
|
||||
|
||||
return loss_pre2
|
||||
|
||||
def _calc_att_loss(
|
||||
self,
|
||||
encoder_out: torch.Tensor,
|
||||
encoder_out_lens: torch.Tensor,
|
||||
ys_pad: torch.Tensor,
|
||||
ys_pad_lens: torch.Tensor,
|
||||
):
|
||||
"""Internal: calc att loss.
|
||||
|
||||
Args:
|
||||
encoder_out: Encoder output tensor.
|
||||
encoder_out_lens: Encoder output lengths.
|
||||
ys_pad: TODO.
|
||||
ys_pad_lens: Lengths of ys_pad.
|
||||
"""
|
||||
encoder_out_mask = (
|
||||
~make_pad_mask(encoder_out_lens, maxlen=encoder_out.size(1))[:, None, :]
|
||||
).to(encoder_out.device)
|
||||
if self.predictor_bias == 1:
|
||||
_, ys_pad = add_sos_eos(ys_pad, self.sos, self.eos, self.ignore_id)
|
||||
ys_pad_lens = ys_pad_lens + self.predictor_bias
|
||||
pre_acoustic_embeds, pre_token_length, _, pre_peak_index, _ = self.predictor(
|
||||
encoder_out, ys_pad, encoder_out_mask, ignore_id=self.ignore_id
|
||||
)
|
||||
|
||||
# 0. sampler
|
||||
decoder_out_1st = None
|
||||
if self.sampling_ratio > 0.0:
|
||||
sematic_embeds, decoder_out_1st = self.sampler(
|
||||
encoder_out, encoder_out_lens, ys_pad, ys_pad_lens, pre_acoustic_embeds
|
||||
)
|
||||
else:
|
||||
sematic_embeds = pre_acoustic_embeds
|
||||
|
||||
# 1. Forward decoder
|
||||
decoder_outs = self.decoder(encoder_out, encoder_out_lens, sematic_embeds, ys_pad_lens)
|
||||
decoder_out, _ = decoder_outs[0], decoder_outs[1]
|
||||
|
||||
if decoder_out_1st is None:
|
||||
decoder_out_1st = decoder_out
|
||||
# 2. Compute attention loss
|
||||
loss_att = self.criterion_att(decoder_out, ys_pad)
|
||||
acc_att = th_accuracy(
|
||||
decoder_out_1st.view(-1, self.vocab_size),
|
||||
ys_pad,
|
||||
ignore_label=self.ignore_id,
|
||||
)
|
||||
loss_pre = self.criterion_pre(ys_pad_lens.type_as(pre_token_length), pre_token_length)
|
||||
|
||||
# Compute cer/wer using attention-decoder
|
||||
if self.training or self.error_calculator is None:
|
||||
cer_att, wer_att = None, None
|
||||
else:
|
||||
ys_hat = decoder_out_1st.argmax(dim=-1)
|
||||
cer_att, wer_att = self.error_calculator(ys_hat.cpu(), ys_pad.cpu())
|
||||
|
||||
return loss_att, acc_att, cer_att, wer_att, loss_pre
|
||||
|
||||
def calc_predictor(self, encoder_out, encoder_out_lens):
|
||||
"""Calc predictor.
|
||||
|
||||
Args:
|
||||
encoder_out: Encoder output tensor.
|
||||
encoder_out_lens: Encoder output lengths.
|
||||
"""
|
||||
encoder_out_mask = (
|
||||
~make_pad_mask(encoder_out_lens, maxlen=encoder_out.size(1))[:, None, :]
|
||||
).to(encoder_out.device)
|
||||
pre_acoustic_embeds, pre_token_length, alphas, pre_peak_index, pre_token_length2 = (
|
||||
self.predictor(encoder_out, None, encoder_out_mask, ignore_id=self.ignore_id)
|
||||
)
|
||||
return pre_acoustic_embeds, pre_token_length, alphas, pre_peak_index
|
||||
|
||||
def calc_predictor_timestamp(self, encoder_out, encoder_out_lens, token_num):
|
||||
"""Calc predictor timestamp.
|
||||
|
||||
Args:
|
||||
encoder_out: Encoder output tensor.
|
||||
encoder_out_lens: Encoder output lengths.
|
||||
token_num: TODO.
|
||||
"""
|
||||
encoder_out_mask = (
|
||||
~make_pad_mask(encoder_out_lens, maxlen=encoder_out.size(1))[:, None, :]
|
||||
).to(encoder_out.device)
|
||||
ds_alphas, ds_cif_peak, us_alphas, us_peaks = self.predictor.get_upsample_timestamp(
|
||||
encoder_out, encoder_out_mask, token_num
|
||||
)
|
||||
return ds_alphas, ds_cif_peak, us_alphas, us_peaks
|
||||
|
||||
def forward(
|
||||
self,
|
||||
speech: torch.Tensor,
|
||||
speech_lengths: torch.Tensor,
|
||||
text: torch.Tensor,
|
||||
text_lengths: torch.Tensor,
|
||||
**kwargs,
|
||||
) -> Tuple[torch.Tensor, Dict[str, torch.Tensor], torch.Tensor]:
|
||||
"""Frontend + Encoder + Decoder + Calc loss
|
||||
Args:
|
||||
speech: (Batch, Length, ...)
|
||||
speech_lengths: (Batch, )
|
||||
text: (Batch, Length)
|
||||
text_lengths: (Batch,)
|
||||
"""
|
||||
if len(text_lengths.size()) > 1:
|
||||
text_lengths = text_lengths[:, 0]
|
||||
if len(speech_lengths.size()) > 1:
|
||||
speech_lengths = speech_lengths[:, 0]
|
||||
|
||||
batch_size = speech.shape[0]
|
||||
|
||||
# Encoder
|
||||
encoder_out, encoder_out_lens = self.encode(speech, speech_lengths)
|
||||
|
||||
loss_ctc, cer_ctc = None, None
|
||||
loss_pre = None
|
||||
stats = dict()
|
||||
|
||||
# decoder: CTC branch
|
||||
if self.ctc_weight != 0.0:
|
||||
loss_ctc, cer_ctc = self._calc_ctc_loss(
|
||||
encoder_out, encoder_out_lens, text, text_lengths
|
||||
)
|
||||
|
||||
# Collect CTC branch stats
|
||||
stats["loss_ctc"] = loss_ctc.detach() if loss_ctc is not None else None
|
||||
stats["cer_ctc"] = cer_ctc
|
||||
|
||||
# decoder: Attention decoder branch
|
||||
loss_att, acc_att, cer_att, wer_att, loss_pre = self._calc_att_loss(
|
||||
encoder_out, encoder_out_lens, text, text_lengths
|
||||
)
|
||||
|
||||
loss_pre2 = self._calc_pre2_loss(encoder_out, encoder_out_lens, text, text_lengths)
|
||||
|
||||
# 3. CTC-Att loss definition
|
||||
if self.ctc_weight == 0.0:
|
||||
loss = (
|
||||
loss_att
|
||||
+ loss_pre * self.predictor_weight
|
||||
+ loss_pre2 * self.predictor_weight * 0.5
|
||||
)
|
||||
else:
|
||||
loss = (
|
||||
self.ctc_weight * loss_ctc
|
||||
+ (1 - self.ctc_weight) * loss_att
|
||||
+ loss_pre * self.predictor_weight
|
||||
+ loss_pre2 * self.predictor_weight * 0.5
|
||||
)
|
||||
|
||||
# Collect Attn branch stats
|
||||
stats["loss_att"] = loss_att.detach() if loss_att is not None else None
|
||||
stats["acc"] = acc_att
|
||||
stats["cer"] = cer_att
|
||||
stats["wer"] = wer_att
|
||||
stats["loss_pre"] = loss_pre.detach().cpu() if loss_pre is not None else None
|
||||
stats["loss_pre2"] = loss_pre2.detach().cpu()
|
||||
|
||||
stats["loss"] = torch.clone(loss.detach())
|
||||
|
||||
# force_gatherable: to-device and to-tensor if scalar for DataParallel
|
||||
if self.length_normalized_loss:
|
||||
batch_size = int((text_lengths + self.predictor_bias).sum())
|
||||
|
||||
loss, stats, weight = force_gatherable((loss, stats, batch_size), loss.device)
|
||||
return loss, stats, weight
|
||||
|
||||
def inference(
|
||||
self,
|
||||
data_in,
|
||||
data_lengths=None,
|
||||
key: list = None,
|
||||
tokenizer=None,
|
||||
frontend=None,
|
||||
**kwargs,
|
||||
):
|
||||
|
||||
# init beamsearch
|
||||
"""Run inference on input data.
|
||||
|
||||
Args:
|
||||
data_in: Input data (audio samples, file paths, or text).
|
||||
data_lengths: Lengths of each input sample in the batch.
|
||||
key: Sample identifiers.
|
||||
tokenizer: Tokenizer instance for text encoding/decoding.
|
||||
frontend: Audio frontend for feature extraction.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
is_use_ctc = kwargs.get("decoding_ctc_weight", 0.0) > 0.00001 and self.ctc != None
|
||||
is_use_lm = (
|
||||
kwargs.get("lm_weight", 0.0) > 0.00001 and kwargs.get("lm_file", None) is not None
|
||||
)
|
||||
if self.beam_search is None and (is_use_lm or is_use_ctc):
|
||||
logging.info("enable beam_search")
|
||||
self.init_beam_search(**kwargs)
|
||||
self.nbest = kwargs.get("nbest", 1)
|
||||
|
||||
meta_data = {}
|
||||
# if isinstance(data_in, torch.Tensor): # fbank
|
||||
# speech, speech_lengths = data_in, data_lengths
|
||||
# if len(speech.shape) < 3:
|
||||
# speech = speech[None, :, :]
|
||||
# if speech_lengths is None:
|
||||
# speech_lengths = speech.shape[1]
|
||||
# else:
|
||||
# extract fbank feats
|
||||
time1 = time.perf_counter()
|
||||
audio_sample_list = load_audio_text_image_video(
|
||||
data_in, fs=frontend.fs, audio_fs=kwargs.get("fs", 16000)
|
||||
)
|
||||
time2 = time.perf_counter()
|
||||
meta_data["load_data"] = f"{time2 - time1:0.3f}"
|
||||
speech, speech_lengths = extract_fbank(
|
||||
audio_sample_list, data_type=kwargs.get("data_type", "sound"), frontend=frontend
|
||||
)
|
||||
time3 = time.perf_counter()
|
||||
meta_data["extract_feat"] = f"{time3 - time2:0.3f}"
|
||||
meta_data["batch_data_time"] = (
|
||||
speech_lengths.sum().item() * frontend.frame_shift * frontend.lfr_n / 1000
|
||||
)
|
||||
|
||||
speech = speech.to(device=kwargs["device"])
|
||||
speech_lengths = speech_lengths.to(device=kwargs["device"])
|
||||
|
||||
# Encoder
|
||||
encoder_out, encoder_out_lens = self.encode(speech, speech_lengths)
|
||||
if isinstance(encoder_out, tuple):
|
||||
encoder_out = encoder_out[0]
|
||||
|
||||
# predictor
|
||||
predictor_outs = self.calc_predictor(encoder_out, encoder_out_lens)
|
||||
pre_acoustic_embeds, pre_token_length, alphas, pre_peak_index = (
|
||||
predictor_outs[0],
|
||||
predictor_outs[1],
|
||||
predictor_outs[2],
|
||||
predictor_outs[3],
|
||||
)
|
||||
pre_token_length = pre_token_length.round().long()
|
||||
if torch.max(pre_token_length) < 1:
|
||||
return []
|
||||
decoder_outs = self.cal_decoder_with_predictor(
|
||||
encoder_out, encoder_out_lens, pre_acoustic_embeds, pre_token_length
|
||||
)
|
||||
decoder_out, ys_pad_lens = decoder_outs[0], decoder_outs[1]
|
||||
|
||||
# BiCifParaformer, test no bias cif2
|
||||
_, _, us_alphas, us_peaks = self.calc_predictor_timestamp(
|
||||
encoder_out, encoder_out_lens, pre_token_length
|
||||
)
|
||||
|
||||
results = []
|
||||
b, n, d = decoder_out.size()
|
||||
for i in range(b):
|
||||
x = encoder_out[i, : encoder_out_lens[i], :]
|
||||
am_scores = decoder_out[i, : pre_token_length[i], :]
|
||||
if self.beam_search is not None:
|
||||
nbest_hyps = self.beam_search(
|
||||
x=x,
|
||||
am_scores=am_scores,
|
||||
maxlenratio=kwargs.get("maxlenratio", 0.0),
|
||||
minlenratio=kwargs.get("minlenratio", 0.0),
|
||||
)
|
||||
|
||||
nbest_hyps = nbest_hyps[: self.nbest]
|
||||
else:
|
||||
|
||||
yseq = am_scores.argmax(dim=-1)
|
||||
score = am_scores.max(dim=-1)[0]
|
||||
score = torch.sum(score, dim=-1)
|
||||
# pad with mask tokens to ensure compatibility with sos/eos tokens
|
||||
yseq = torch.tensor([self.sos] + yseq.tolist() + [self.eos], device=yseq.device)
|
||||
nbest_hyps = [Hypothesis(yseq=yseq, score=score)]
|
||||
for nbest_idx, hyp in enumerate(nbest_hyps):
|
||||
ibest_writer = None
|
||||
if kwargs.get("output_dir") is not None:
|
||||
if not hasattr(self, "writer"):
|
||||
self.writer = DatadirWriter(kwargs.get("output_dir"))
|
||||
ibest_writer = self.writer[f"{nbest_idx+1}best_recog"]
|
||||
|
||||
# remove sos/eos and get results
|
||||
last_pos = -1
|
||||
if isinstance(hyp.yseq, list):
|
||||
token_int = hyp.yseq[1:last_pos]
|
||||
else:
|
||||
token_int = hyp.yseq[1:last_pos].tolist()
|
||||
|
||||
# remove blank symbol id, which is assumed to be 0
|
||||
token_int = list(
|
||||
filter(
|
||||
lambda x: x != self.eos and x != self.sos and x != self.blank_id, token_int
|
||||
)
|
||||
)
|
||||
|
||||
if tokenizer is not None:
|
||||
# Change integer-ids to tokens
|
||||
token = tokenizer.ids2tokens(token_int)
|
||||
text = tokenizer.tokens2text(token)
|
||||
|
||||
_, timestamp = ts_prediction_lfr6_standard(
|
||||
us_alphas[i][: encoder_out_lens[i] * 3],
|
||||
us_peaks[i][: encoder_out_lens[i] * 3],
|
||||
copy.copy(token),
|
||||
vad_offset=kwargs.get("begin_time", 0),
|
||||
)
|
||||
|
||||
text_postprocessed, time_stamp_postprocessed, word_lists = (
|
||||
postprocess_utils.sentence_postprocess(token, timestamp)
|
||||
)
|
||||
|
||||
result_i = {
|
||||
"key": key[i],
|
||||
"text": text_postprocessed,
|
||||
"timestamp": time_stamp_postprocessed,
|
||||
}
|
||||
|
||||
if ibest_writer is not None:
|
||||
ibest_writer["token"][key[i]] = " ".join(token)
|
||||
# ibest_writer["text"][key[i]] = text
|
||||
ibest_writer["timestamp"][key[i]] = time_stamp_postprocessed
|
||||
ibest_writer["text"][key[i]] = text_postprocessed
|
||||
else:
|
||||
result_i = {"key": key[i], "token_int": token_int}
|
||||
results.append(result_i)
|
||||
|
||||
return results, meta_data
|
||||
|
||||
def export(self, **kwargs):
|
||||
"""Export.
|
||||
|
||||
Args:
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
from .export_meta import export_rebuild_model
|
||||
|
||||
if "max_seq_len" not in kwargs:
|
||||
kwargs["max_seq_len"] = 512
|
||||
models = export_rebuild_model(model=self, **kwargs)
|
||||
return models
|
||||
@@ -0,0 +1,134 @@
|
||||
# This is an example that demonstrates how to configure a model file.
|
||||
# You can modify the configuration according to your own requirements.
|
||||
|
||||
# to print the register_table:
|
||||
# from funasr.register import tables
|
||||
# tables.print()
|
||||
|
||||
# network architecture
|
||||
#model: funasr.models.paraformer.model:Paraformer
|
||||
model: BiCifParaformer
|
||||
model_conf:
|
||||
ctc_weight: 0.0
|
||||
lsm_weight: 0.1
|
||||
length_normalized_loss: true
|
||||
predictor_weight: 1.0
|
||||
predictor_bias: 1
|
||||
sampling_ratio: 0.75
|
||||
|
||||
# encoder
|
||||
encoder: SANMEncoder
|
||||
encoder_conf:
|
||||
output_size: 512
|
||||
attention_heads: 4
|
||||
linear_units: 2048
|
||||
num_blocks: 50
|
||||
dropout_rate: 0.1
|
||||
positional_dropout_rate: 0.1
|
||||
attention_dropout_rate: 0.1
|
||||
input_layer: pe
|
||||
pos_enc_class: SinusoidalPositionEncoder
|
||||
normalize_before: true
|
||||
kernel_size: 11
|
||||
sanm_shfit: 0
|
||||
selfattention_layer_type: sanm
|
||||
|
||||
# decoder
|
||||
decoder: ParaformerSANMDecoder
|
||||
decoder_conf:
|
||||
attention_heads: 4
|
||||
linear_units: 2048
|
||||
num_blocks: 16
|
||||
dropout_rate: 0.1
|
||||
positional_dropout_rate: 0.1
|
||||
self_attention_dropout_rate: 0.1
|
||||
src_attention_dropout_rate: 0.1
|
||||
att_layer_num: 16
|
||||
kernel_size: 11
|
||||
sanm_shfit: 0
|
||||
|
||||
predictor: CifPredictorV3
|
||||
predictor_conf:
|
||||
idim: 512
|
||||
threshold: 1.0
|
||||
l_order: 1
|
||||
r_order: 1
|
||||
tail_threshold: 0.45
|
||||
smooth_factor2: 0.25
|
||||
noise_threshold2: 0.01
|
||||
upsample_times: 3
|
||||
use_cif1_cnn: false
|
||||
upsample_type: cnn_blstm
|
||||
|
||||
# frontend related
|
||||
frontend: WavFrontend
|
||||
frontend_conf:
|
||||
fs: 16000
|
||||
window: hamming
|
||||
n_mels: 80
|
||||
frame_length: 25
|
||||
frame_shift: 10
|
||||
lfr_m: 7
|
||||
lfr_n: 6
|
||||
|
||||
specaug: SpecAugLFR
|
||||
specaug_conf:
|
||||
apply_time_warp: false
|
||||
time_warp_window: 5
|
||||
time_warp_mode: bicubic
|
||||
apply_freq_mask: true
|
||||
freq_mask_width_range:
|
||||
- 0
|
||||
- 30
|
||||
lfr_rate: 6
|
||||
num_freq_mask: 1
|
||||
apply_time_mask: true
|
||||
time_mask_width_range:
|
||||
- 0
|
||||
- 12
|
||||
num_time_mask: 1
|
||||
|
||||
train_conf:
|
||||
accum_grad: 1
|
||||
grad_clip: 5
|
||||
max_epoch: 150
|
||||
val_scheduler_criterion:
|
||||
- valid
|
||||
- acc
|
||||
best_model_criterion:
|
||||
- - valid
|
||||
- acc
|
||||
- max
|
||||
keep_nbest_models: 10
|
||||
log_interval: 50
|
||||
|
||||
optim: adam
|
||||
optim_conf:
|
||||
lr: 0.0005
|
||||
scheduler: warmuplr
|
||||
scheduler_conf:
|
||||
warmup_steps: 30000
|
||||
|
||||
dataset: AudioDataset
|
||||
dataset_conf:
|
||||
index_ds: IndexDSJsonl
|
||||
batch_sampler: BatchSampler
|
||||
batch_type: example # example or length
|
||||
batch_size: 1 # if batch_type is example, batch_size is the numbers of samples; if length, batch_size is source_token_len+target_token_len;
|
||||
max_token_length: 2048 # filter samples if source_token_len+target_token_len > max_token_length,
|
||||
buffer_size: 500
|
||||
shuffle: True
|
||||
num_workers: 0
|
||||
|
||||
tokenizer: CharTokenizer
|
||||
tokenizer_conf:
|
||||
unk_symbol: <unk>
|
||||
split_with_space: true
|
||||
|
||||
|
||||
ctc_conf:
|
||||
dropout_rate: 0.0
|
||||
ctc_type: builtin
|
||||
reduce: true
|
||||
ignore_nan_grad: true
|
||||
normalize: null
|
||||
@@ -0,0 +1,150 @@
|
||||
"""MLP with convolutional gating (cgMLP) definition.
|
||||
|
||||
References:
|
||||
https://openreview.net/forum?id=RA-zVvZLYIy
|
||||
https://arxiv.org/abs/2105.08050
|
||||
|
||||
"""
|
||||
|
||||
import torch
|
||||
|
||||
from funasr.models.transformer.utils.nets_utils import get_activation
|
||||
from funasr.models.transformer.layer_norm import LayerNorm
|
||||
|
||||
|
||||
class ConvolutionalSpatialGatingUnit(torch.nn.Module):
|
||||
"""Convolutional Spatial Gating Unit (CSGU)."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
size: int,
|
||||
kernel_size: int,
|
||||
dropout_rate: float,
|
||||
use_linear_after_conv: bool,
|
||||
gate_activation: str,
|
||||
):
|
||||
"""Initialize ConvolutionalSpatialGatingUnit.
|
||||
|
||||
Args:
|
||||
size: TODO.
|
||||
kernel_size: Size/dimension parameter.
|
||||
dropout_rate: TODO.
|
||||
use_linear_after_conv: TODO.
|
||||
gate_activation: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
n_channels = size // 2 # split input channels
|
||||
self.norm = LayerNorm(n_channels)
|
||||
self.conv = torch.nn.Conv1d(
|
||||
n_channels,
|
||||
n_channels,
|
||||
kernel_size,
|
||||
1,
|
||||
(kernel_size - 1) // 2,
|
||||
groups=n_channels,
|
||||
)
|
||||
if use_linear_after_conv:
|
||||
self.linear = torch.nn.Linear(n_channels, n_channels)
|
||||
else:
|
||||
self.linear = None
|
||||
|
||||
if gate_activation == "identity":
|
||||
self.act = torch.nn.Identity()
|
||||
else:
|
||||
self.act = get_activation(gate_activation)
|
||||
|
||||
self.dropout = torch.nn.Dropout(dropout_rate)
|
||||
|
||||
def espnet_initialization_fn(self):
|
||||
"""Espnet initialization fn."""
|
||||
torch.nn.init.normal_(self.conv.weight, std=1e-6)
|
||||
torch.nn.init.ones_(self.conv.bias)
|
||||
if self.linear is not None:
|
||||
torch.nn.init.normal_(self.linear.weight, std=1e-6)
|
||||
torch.nn.init.ones_(self.linear.bias)
|
||||
|
||||
def forward(self, x, gate_add=None):
|
||||
"""Forward method
|
||||
|
||||
Args:
|
||||
x (torch.Tensor): (N, T, D)
|
||||
gate_add (torch.Tensor): (N, T, D/2)
|
||||
|
||||
Returns:
|
||||
out (torch.Tensor): (N, T, D/2)
|
||||
"""
|
||||
|
||||
x_r, x_g = x.chunk(2, dim=-1)
|
||||
|
||||
x_g = self.norm(x_g) # (N, T, D/2)
|
||||
x_g = self.conv(x_g.transpose(1, 2)).transpose(1, 2) # (N, T, D/2)
|
||||
if self.linear is not None:
|
||||
x_g = self.linear(x_g)
|
||||
|
||||
if gate_add is not None:
|
||||
x_g = x_g + gate_add
|
||||
|
||||
x_g = self.act(x_g)
|
||||
out = x_r * x_g # (N, T, D/2)
|
||||
out = self.dropout(out)
|
||||
return out
|
||||
|
||||
|
||||
class ConvolutionalGatingMLP(torch.nn.Module):
|
||||
"""Convolutional Gating MLP (cgMLP)."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
size: int,
|
||||
linear_units: int,
|
||||
kernel_size: int,
|
||||
dropout_rate: float,
|
||||
use_linear_after_conv: bool,
|
||||
gate_activation: str,
|
||||
):
|
||||
"""Initialize ConvolutionalGatingMLP.
|
||||
|
||||
Args:
|
||||
size: TODO.
|
||||
linear_units: TODO.
|
||||
kernel_size: Size/dimension parameter.
|
||||
dropout_rate: TODO.
|
||||
use_linear_after_conv: TODO.
|
||||
gate_activation: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.channel_proj1 = torch.nn.Sequential(
|
||||
torch.nn.Linear(size, linear_units), torch.nn.GELU()
|
||||
)
|
||||
self.csgu = ConvolutionalSpatialGatingUnit(
|
||||
size=linear_units,
|
||||
kernel_size=kernel_size,
|
||||
dropout_rate=dropout_rate,
|
||||
use_linear_after_conv=use_linear_after_conv,
|
||||
gate_activation=gate_activation,
|
||||
)
|
||||
self.channel_proj2 = torch.nn.Linear(linear_units // 2, size)
|
||||
|
||||
def forward(self, x, mask):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
mask: TODO.
|
||||
"""
|
||||
if isinstance(x, tuple):
|
||||
xs_pad, pos_emb = x
|
||||
else:
|
||||
xs_pad, pos_emb = x, None
|
||||
|
||||
xs_pad = self.channel_proj1(xs_pad) # size -> linear_units
|
||||
xs_pad = self.csgu(xs_pad) # linear_units -> linear_units/2
|
||||
xs_pad = self.channel_proj2(xs_pad) # linear_units/2 -> size
|
||||
|
||||
if pos_emb is not None:
|
||||
out = (xs_pad, pos_emb)
|
||||
else:
|
||||
out = xs_pad
|
||||
return out
|
||||
@@ -0,0 +1,564 @@
|
||||
# Copyright 2022 Yifan Peng (Carnegie Mellon University)
|
||||
# Apache 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
"""Branchformer encoder definition.
|
||||
|
||||
Reference:
|
||||
Yifan Peng, Siddharth Dalmia, Ian Lane, and Shinji Watanabe,
|
||||
“Branchformer: Parallel MLP-Attention Architectures to Capture
|
||||
Local and Global Context for Speech Recognition and Understanding,”
|
||||
in Proceedings of ICML, 2022.
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Optional, Tuple, Union
|
||||
|
||||
import numpy
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from funasr.models.branchformer.cgmlp import ConvolutionalGatingMLP
|
||||
from funasr.models.branchformer.fastformer import FastSelfAttention
|
||||
from funasr.models.transformer.utils.nets_utils import make_pad_mask
|
||||
from funasr.models.transformer.attention import ( # noqa: H301
|
||||
LegacyRelPositionMultiHeadedAttention,
|
||||
MultiHeadedAttention,
|
||||
RelPositionMultiHeadedAttention,
|
||||
)
|
||||
from funasr.models.transformer.embedding import ( # noqa: H301
|
||||
LegacyRelPositionalEncoding,
|
||||
PositionalEncoding,
|
||||
RelPositionalEncoding,
|
||||
ScaledPositionalEncoding,
|
||||
)
|
||||
from funasr.models.transformer.layer_norm import LayerNorm
|
||||
from funasr.models.transformer.utils.repeat import repeat
|
||||
from funasr.models.transformer.utils.subsampling import (
|
||||
Conv2dSubsampling,
|
||||
Conv2dSubsampling2,
|
||||
Conv2dSubsampling6,
|
||||
Conv2dSubsampling8,
|
||||
TooShortUttError,
|
||||
check_short_utt,
|
||||
)
|
||||
|
||||
from funasr.register import tables
|
||||
|
||||
|
||||
class BranchformerEncoderLayer(torch.nn.Module):
|
||||
"""Branchformer encoder layer module.
|
||||
|
||||
Args:
|
||||
size (int): model dimension
|
||||
attn: standard self-attention or efficient attention, optional
|
||||
cgmlp: ConvolutionalGatingMLP, optional
|
||||
dropout_rate (float): dropout probability
|
||||
merge_method (str): concat, learned_ave, fixed_ave
|
||||
cgmlp_weight (float): weight of the cgmlp branch, between 0 and 1,
|
||||
used if merge_method is fixed_ave
|
||||
attn_branch_drop_rate (float): probability of dropping the attn branch,
|
||||
used if merge_method is learned_ave
|
||||
stochastic_depth_rate (float): stochastic depth probability
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
size: int,
|
||||
attn: Optional[torch.nn.Module],
|
||||
cgmlp: Optional[torch.nn.Module],
|
||||
dropout_rate: float,
|
||||
merge_method: str,
|
||||
cgmlp_weight: float = 0.5,
|
||||
attn_branch_drop_rate: float = 0.0,
|
||||
stochastic_depth_rate: float = 0.0,
|
||||
):
|
||||
"""Initialize BranchformerEncoderLayer.
|
||||
|
||||
Args:
|
||||
size: TODO.
|
||||
attn: TODO.
|
||||
cgmlp: TODO.
|
||||
dropout_rate: TODO.
|
||||
merge_method: TODO.
|
||||
cgmlp_weight: TODO.
|
||||
attn_branch_drop_rate: TODO.
|
||||
stochastic_depth_rate: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
assert (attn is not None) or (cgmlp is not None), "At least one branch should be valid"
|
||||
|
||||
self.size = size
|
||||
self.attn = attn
|
||||
self.cgmlp = cgmlp
|
||||
self.merge_method = merge_method
|
||||
self.cgmlp_weight = cgmlp_weight
|
||||
self.attn_branch_drop_rate = attn_branch_drop_rate
|
||||
self.stochastic_depth_rate = stochastic_depth_rate
|
||||
self.use_two_branches = (attn is not None) and (cgmlp is not None)
|
||||
|
||||
if attn is not None:
|
||||
self.norm_mha = LayerNorm(size) # for the MHA module
|
||||
if cgmlp is not None:
|
||||
self.norm_mlp = LayerNorm(size) # for the MLP module
|
||||
self.norm_final = LayerNorm(size) # for the final output of the block
|
||||
|
||||
self.dropout = torch.nn.Dropout(dropout_rate)
|
||||
|
||||
if self.use_two_branches:
|
||||
if merge_method == "concat":
|
||||
self.merge_proj = torch.nn.Linear(size + size, size)
|
||||
|
||||
elif merge_method == "learned_ave":
|
||||
# attention-based pooling for two branches
|
||||
self.pooling_proj1 = torch.nn.Linear(size, 1)
|
||||
self.pooling_proj2 = torch.nn.Linear(size, 1)
|
||||
|
||||
# linear projections for calculating merging weights
|
||||
self.weight_proj1 = torch.nn.Linear(size, 1)
|
||||
self.weight_proj2 = torch.nn.Linear(size, 1)
|
||||
|
||||
# linear projection after weighted average
|
||||
self.merge_proj = torch.nn.Linear(size, size)
|
||||
|
||||
elif merge_method == "fixed_ave":
|
||||
assert 0.0 <= cgmlp_weight <= 1.0, "cgmlp weight should be between 0.0 and 1.0"
|
||||
|
||||
# remove the other branch if only one branch is used
|
||||
if cgmlp_weight == 0.0:
|
||||
self.use_two_branches = False
|
||||
self.cgmlp = None
|
||||
self.norm_mlp = None
|
||||
elif cgmlp_weight == 1.0:
|
||||
self.use_two_branches = False
|
||||
self.attn = None
|
||||
self.norm_mha = None
|
||||
|
||||
# linear projection after weighted average
|
||||
self.merge_proj = torch.nn.Linear(size, size)
|
||||
|
||||
else:
|
||||
raise ValueError(f"unknown merge method: {merge_method}")
|
||||
|
||||
else:
|
||||
self.merge_proj = torch.nn.Identity()
|
||||
|
||||
def forward(self, x_input, mask, cache=None):
|
||||
"""Compute encoded features.
|
||||
|
||||
Args:
|
||||
x_input (Union[Tuple, torch.Tensor]): Input tensor w/ or w/o pos emb.
|
||||
- w/ pos emb: Tuple of tensors [(#batch, time, size), (1, time, size)].
|
||||
- w/o pos emb: Tensor (#batch, time, size).
|
||||
mask (torch.Tensor): Mask tensor for the input (#batch, 1, time).
|
||||
cache (torch.Tensor): Cache tensor of the input (#batch, time - 1, size).
|
||||
|
||||
Returns:
|
||||
torch.Tensor: Output tensor (#batch, time, size).
|
||||
torch.Tensor: Mask tensor (#batch, time).
|
||||
"""
|
||||
|
||||
if cache is not None:
|
||||
raise NotImplementedError("cache is not None, which is not tested")
|
||||
|
||||
if isinstance(x_input, tuple):
|
||||
x, pos_emb = x_input[0], x_input[1]
|
||||
else:
|
||||
x, pos_emb = x_input, None
|
||||
|
||||
skip_layer = False
|
||||
# with stochastic depth, residual connection `x + f(x)` becomes
|
||||
# `x <- x + 1 / (1 - p) * f(x)` at training time.
|
||||
stoch_layer_coeff = 1.0
|
||||
if self.training and self.stochastic_depth_rate > 0:
|
||||
skip_layer = torch.rand(1).item() < self.stochastic_depth_rate
|
||||
stoch_layer_coeff = 1.0 / (1 - self.stochastic_depth_rate)
|
||||
|
||||
if skip_layer:
|
||||
if cache is not None:
|
||||
x = torch.cat([cache, x], dim=1)
|
||||
if pos_emb is not None:
|
||||
return (x, pos_emb), mask
|
||||
return x, mask
|
||||
|
||||
# Two branches
|
||||
x1 = x
|
||||
x2 = x
|
||||
|
||||
# Branch 1: multi-headed attention module
|
||||
if self.attn is not None:
|
||||
x1 = self.norm_mha(x1)
|
||||
|
||||
if isinstance(self.attn, FastSelfAttention):
|
||||
x_att = self.attn(x1, mask)
|
||||
else:
|
||||
if pos_emb is not None:
|
||||
x_att = self.attn(x1, x1, x1, pos_emb, mask)
|
||||
else:
|
||||
x_att = self.attn(x1, x1, x1, mask)
|
||||
|
||||
x1 = self.dropout(x_att)
|
||||
|
||||
# Branch 2: convolutional gating mlp
|
||||
if self.cgmlp is not None:
|
||||
x2 = self.norm_mlp(x2)
|
||||
|
||||
if pos_emb is not None:
|
||||
x2 = (x2, pos_emb)
|
||||
x2 = self.cgmlp(x2, mask)
|
||||
if isinstance(x2, tuple):
|
||||
x2 = x2[0]
|
||||
|
||||
x2 = self.dropout(x2)
|
||||
|
||||
# Merge two branches
|
||||
if self.use_two_branches:
|
||||
if self.merge_method == "concat":
|
||||
x = x + stoch_layer_coeff * self.dropout(
|
||||
self.merge_proj(torch.cat([x1, x2], dim=-1))
|
||||
)
|
||||
elif self.merge_method == "learned_ave":
|
||||
if (
|
||||
self.training
|
||||
and self.attn_branch_drop_rate > 0
|
||||
and torch.rand(1).item() < self.attn_branch_drop_rate
|
||||
):
|
||||
# Drop the attn branch
|
||||
w1, w2 = 0.0, 1.0
|
||||
else:
|
||||
# branch1
|
||||
score1 = (
|
||||
self.pooling_proj1(x1).transpose(1, 2) / self.size**0.5
|
||||
) # (batch, 1, time)
|
||||
if mask is not None:
|
||||
min_value = float(
|
||||
numpy.finfo(torch.tensor(0, dtype=score1.dtype).numpy().dtype).min
|
||||
)
|
||||
score1 = score1.masked_fill(mask.eq(0), min_value)
|
||||
score1 = torch.softmax(score1, dim=-1).masked_fill(mask.eq(0), 0.0)
|
||||
else:
|
||||
score1 = torch.softmax(score1, dim=-1)
|
||||
pooled1 = torch.matmul(score1, x1).squeeze(1) # (batch, size)
|
||||
weight1 = self.weight_proj1(pooled1) # (batch, 1)
|
||||
|
||||
# branch2
|
||||
score2 = (
|
||||
self.pooling_proj2(x2).transpose(1, 2) / self.size**0.5
|
||||
) # (batch, 1, time)
|
||||
if mask is not None:
|
||||
min_value = float(
|
||||
numpy.finfo(torch.tensor(0, dtype=score2.dtype).numpy().dtype).min
|
||||
)
|
||||
score2 = score2.masked_fill(mask.eq(0), min_value)
|
||||
score2 = torch.softmax(score2, dim=-1).masked_fill(mask.eq(0), 0.0)
|
||||
else:
|
||||
score2 = torch.softmax(score2, dim=-1)
|
||||
pooled2 = torch.matmul(score2, x2).squeeze(1) # (batch, size)
|
||||
weight2 = self.weight_proj2(pooled2) # (batch, 1)
|
||||
|
||||
# normalize weights of two branches
|
||||
merge_weights = torch.softmax(
|
||||
torch.cat([weight1, weight2], dim=-1), dim=-1
|
||||
) # (batch, 2)
|
||||
merge_weights = merge_weights.unsqueeze(-1).unsqueeze(-1) # (batch, 2, 1, 1)
|
||||
w1, w2 = merge_weights[:, 0], merge_weights[:, 1] # (batch, 1, 1)
|
||||
|
||||
x = x + stoch_layer_coeff * self.dropout(self.merge_proj(w1 * x1 + w2 * x2))
|
||||
elif self.merge_method == "fixed_ave":
|
||||
x = x + stoch_layer_coeff * self.dropout(
|
||||
self.merge_proj((1.0 - self.cgmlp_weight) * x1 + self.cgmlp_weight * x2)
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(f"unknown merge method: {self.merge_method}")
|
||||
else:
|
||||
if self.attn is None:
|
||||
x = x + stoch_layer_coeff * self.dropout(self.merge_proj(x2))
|
||||
elif self.cgmlp is None:
|
||||
x = x + stoch_layer_coeff * self.dropout(self.merge_proj(x1))
|
||||
else:
|
||||
# This should not happen
|
||||
raise RuntimeError("Both branches are not None, which is unexpected.")
|
||||
|
||||
x = self.norm_final(x)
|
||||
|
||||
if pos_emb is not None:
|
||||
return (x, pos_emb), mask
|
||||
|
||||
return x, mask
|
||||
|
||||
|
||||
@tables.register("encoder_classes", "BranchformerEncoder")
|
||||
class BranchformerEncoder(nn.Module):
|
||||
"""Branchformer encoder module."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_size: int,
|
||||
output_size: int = 256,
|
||||
use_attn: bool = True,
|
||||
attention_heads: int = 4,
|
||||
attention_layer_type: str = "rel_selfattn",
|
||||
pos_enc_layer_type: str = "rel_pos",
|
||||
rel_pos_type: str = "latest",
|
||||
use_cgmlp: bool = True,
|
||||
cgmlp_linear_units: int = 2048,
|
||||
cgmlp_conv_kernel: int = 31,
|
||||
use_linear_after_conv: bool = False,
|
||||
gate_activation: str = "identity",
|
||||
merge_method: str = "concat",
|
||||
cgmlp_weight: Union[float, List[float]] = 0.5,
|
||||
attn_branch_drop_rate: Union[float, List[float]] = 0.0,
|
||||
num_blocks: int = 12,
|
||||
dropout_rate: float = 0.1,
|
||||
positional_dropout_rate: float = 0.1,
|
||||
attention_dropout_rate: float = 0.0,
|
||||
input_layer: Optional[str] = "conv2d",
|
||||
zero_triu: bool = False,
|
||||
padding_idx: int = -1,
|
||||
stochastic_depth_rate: Union[float, List[float]] = 0.0,
|
||||
):
|
||||
"""Initialize BranchformerEncoder.
|
||||
|
||||
Args:
|
||||
input_size: Size/dimension parameter.
|
||||
output_size: Size/dimension parameter.
|
||||
use_attn: TODO.
|
||||
attention_heads: TODO.
|
||||
attention_layer_type: TODO.
|
||||
pos_enc_layer_type: TODO.
|
||||
rel_pos_type: TODO.
|
||||
use_cgmlp: TODO.
|
||||
cgmlp_linear_units: TODO.
|
||||
cgmlp_conv_kernel: TODO.
|
||||
use_linear_after_conv: TODO.
|
||||
gate_activation: TODO.
|
||||
merge_method: TODO.
|
||||
cgmlp_weight: TODO.
|
||||
attn_branch_drop_rate: TODO.
|
||||
num_blocks: TODO.
|
||||
dropout_rate: TODO.
|
||||
positional_dropout_rate: TODO.
|
||||
attention_dropout_rate: TODO.
|
||||
input_layer: TODO.
|
||||
zero_triu: TODO.
|
||||
padding_idx: TODO.
|
||||
stochastic_depth_rate: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
self._output_size = output_size
|
||||
|
||||
if rel_pos_type == "legacy":
|
||||
if pos_enc_layer_type == "rel_pos":
|
||||
pos_enc_layer_type = "legacy_rel_pos"
|
||||
if attention_layer_type == "rel_selfattn":
|
||||
attention_layer_type = "legacy_rel_selfattn"
|
||||
elif rel_pos_type == "latest":
|
||||
assert attention_layer_type != "legacy_rel_selfattn"
|
||||
assert pos_enc_layer_type != "legacy_rel_pos"
|
||||
else:
|
||||
raise ValueError("unknown rel_pos_type: " + rel_pos_type)
|
||||
|
||||
if pos_enc_layer_type == "abs_pos":
|
||||
pos_enc_class = PositionalEncoding
|
||||
elif pos_enc_layer_type == "scaled_abs_pos":
|
||||
pos_enc_class = ScaledPositionalEncoding
|
||||
elif pos_enc_layer_type == "rel_pos":
|
||||
assert attention_layer_type == "rel_selfattn"
|
||||
pos_enc_class = RelPositionalEncoding
|
||||
elif pos_enc_layer_type == "legacy_rel_pos":
|
||||
assert attention_layer_type == "legacy_rel_selfattn"
|
||||
pos_enc_class = LegacyRelPositionalEncoding
|
||||
logging.warning("Using legacy_rel_pos and it will be deprecated in the future.")
|
||||
else:
|
||||
raise ValueError("unknown pos_enc_layer: " + pos_enc_layer_type)
|
||||
|
||||
if input_layer == "linear":
|
||||
self.embed = torch.nn.Sequential(
|
||||
torch.nn.Linear(input_size, output_size),
|
||||
torch.nn.LayerNorm(output_size),
|
||||
torch.nn.Dropout(dropout_rate),
|
||||
pos_enc_class(output_size, positional_dropout_rate),
|
||||
)
|
||||
elif input_layer == "conv2d":
|
||||
self.embed = Conv2dSubsampling(
|
||||
input_size,
|
||||
output_size,
|
||||
dropout_rate,
|
||||
pos_enc_class(output_size, positional_dropout_rate),
|
||||
)
|
||||
elif input_layer == "conv2d2":
|
||||
self.embed = Conv2dSubsampling2(
|
||||
input_size,
|
||||
output_size,
|
||||
dropout_rate,
|
||||
pos_enc_class(output_size, positional_dropout_rate),
|
||||
)
|
||||
elif input_layer == "conv2d6":
|
||||
self.embed = Conv2dSubsampling6(
|
||||
input_size,
|
||||
output_size,
|
||||
dropout_rate,
|
||||
pos_enc_class(output_size, positional_dropout_rate),
|
||||
)
|
||||
elif input_layer == "conv2d8":
|
||||
self.embed = Conv2dSubsampling8(
|
||||
input_size,
|
||||
output_size,
|
||||
dropout_rate,
|
||||
pos_enc_class(output_size, positional_dropout_rate),
|
||||
)
|
||||
elif input_layer == "embed":
|
||||
self.embed = torch.nn.Sequential(
|
||||
torch.nn.Embedding(input_size, output_size, padding_idx=padding_idx),
|
||||
pos_enc_class(output_size, positional_dropout_rate),
|
||||
)
|
||||
elif isinstance(input_layer, torch.nn.Module):
|
||||
self.embed = torch.nn.Sequential(
|
||||
input_layer,
|
||||
pos_enc_class(output_size, positional_dropout_rate),
|
||||
)
|
||||
elif input_layer is None:
|
||||
if input_size == output_size:
|
||||
self.embed = None
|
||||
else:
|
||||
self.embed = torch.nn.Linear(input_size, output_size)
|
||||
else:
|
||||
raise ValueError("unknown input_layer: " + input_layer)
|
||||
|
||||
if attention_layer_type == "selfattn":
|
||||
encoder_selfattn_layer = MultiHeadedAttention
|
||||
encoder_selfattn_layer_args = (
|
||||
attention_heads,
|
||||
output_size,
|
||||
attention_dropout_rate,
|
||||
)
|
||||
elif attention_layer_type == "legacy_rel_selfattn":
|
||||
assert pos_enc_layer_type == "legacy_rel_pos"
|
||||
encoder_selfattn_layer = LegacyRelPositionMultiHeadedAttention
|
||||
encoder_selfattn_layer_args = (
|
||||
attention_heads,
|
||||
output_size,
|
||||
attention_dropout_rate,
|
||||
)
|
||||
logging.warning("Using legacy_rel_selfattn and it will be deprecated in the future.")
|
||||
elif attention_layer_type == "rel_selfattn":
|
||||
assert pos_enc_layer_type == "rel_pos"
|
||||
encoder_selfattn_layer = RelPositionMultiHeadedAttention
|
||||
encoder_selfattn_layer_args = (
|
||||
attention_heads,
|
||||
output_size,
|
||||
attention_dropout_rate,
|
||||
zero_triu,
|
||||
)
|
||||
elif attention_layer_type == "fast_selfattn":
|
||||
assert pos_enc_layer_type in ["abs_pos", "scaled_abs_pos"]
|
||||
encoder_selfattn_layer = FastSelfAttention
|
||||
encoder_selfattn_layer_args = (
|
||||
output_size,
|
||||
attention_heads,
|
||||
attention_dropout_rate,
|
||||
)
|
||||
else:
|
||||
raise ValueError("unknown encoder_attn_layer: " + attention_layer_type)
|
||||
|
||||
cgmlp_layer = ConvolutionalGatingMLP
|
||||
cgmlp_layer_args = (
|
||||
output_size,
|
||||
cgmlp_linear_units,
|
||||
cgmlp_conv_kernel,
|
||||
dropout_rate,
|
||||
use_linear_after_conv,
|
||||
gate_activation,
|
||||
)
|
||||
|
||||
if isinstance(stochastic_depth_rate, float):
|
||||
stochastic_depth_rate = [stochastic_depth_rate] * num_blocks
|
||||
if len(stochastic_depth_rate) != num_blocks:
|
||||
raise ValueError(
|
||||
f"Length of stochastic_depth_rate ({len(stochastic_depth_rate)}) "
|
||||
f"should be equal to num_blocks ({num_blocks})"
|
||||
)
|
||||
|
||||
if isinstance(cgmlp_weight, float):
|
||||
cgmlp_weight = [cgmlp_weight] * num_blocks
|
||||
if len(cgmlp_weight) != num_blocks:
|
||||
raise ValueError(
|
||||
f"Length of cgmlp_weight ({len(cgmlp_weight)}) should be equal to "
|
||||
f"num_blocks ({num_blocks})"
|
||||
)
|
||||
|
||||
if isinstance(attn_branch_drop_rate, float):
|
||||
attn_branch_drop_rate = [attn_branch_drop_rate] * num_blocks
|
||||
if len(attn_branch_drop_rate) != num_blocks:
|
||||
raise ValueError(
|
||||
f"Length of attn_branch_drop_rate ({len(attn_branch_drop_rate)}) "
|
||||
f"should be equal to num_blocks ({num_blocks})"
|
||||
)
|
||||
|
||||
self.encoders = repeat(
|
||||
num_blocks,
|
||||
lambda lnum: BranchformerEncoderLayer(
|
||||
output_size,
|
||||
encoder_selfattn_layer(*encoder_selfattn_layer_args) if use_attn else None,
|
||||
cgmlp_layer(*cgmlp_layer_args) if use_cgmlp else None,
|
||||
dropout_rate,
|
||||
merge_method,
|
||||
cgmlp_weight[lnum],
|
||||
attn_branch_drop_rate[lnum],
|
||||
stochastic_depth_rate[lnum],
|
||||
),
|
||||
)
|
||||
self.after_norm = LayerNorm(output_size)
|
||||
|
||||
def output_size(self) -> int:
|
||||
"""Output size."""
|
||||
return self._output_size
|
||||
|
||||
def forward(
|
||||
self,
|
||||
xs_pad: torch.Tensor,
|
||||
ilens: torch.Tensor,
|
||||
prev_states: torch.Tensor = None,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor, Optional[torch.Tensor]]:
|
||||
"""Calculate forward propagation.
|
||||
|
||||
Args:
|
||||
xs_pad (torch.Tensor): Input tensor (#batch, L, input_size).
|
||||
ilens (torch.Tensor): Input length (#batch).
|
||||
prev_states (torch.Tensor): Not to be used now.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: Output tensor (#batch, L, output_size).
|
||||
torch.Tensor: Output length (#batch).
|
||||
torch.Tensor: Not to be used now.
|
||||
|
||||
"""
|
||||
|
||||
masks = (~make_pad_mask(ilens)[:, None, :]).to(xs_pad.device)
|
||||
|
||||
if (
|
||||
isinstance(self.embed, Conv2dSubsampling)
|
||||
or isinstance(self.embed, Conv2dSubsampling2)
|
||||
or isinstance(self.embed, Conv2dSubsampling6)
|
||||
or isinstance(self.embed, Conv2dSubsampling8)
|
||||
):
|
||||
short_status, limit_size = check_short_utt(self.embed, xs_pad.size(1))
|
||||
if short_status:
|
||||
raise TooShortUttError(
|
||||
f"has {xs_pad.size(1)} frames and is too short for subsampling "
|
||||
+ f"(it needs more than {limit_size} frames), return empty results",
|
||||
xs_pad.size(1),
|
||||
limit_size,
|
||||
)
|
||||
xs_pad, masks = self.embed(xs_pad, masks)
|
||||
elif self.embed is not None:
|
||||
xs_pad = self.embed(xs_pad)
|
||||
|
||||
xs_pad, masks = self.encoders(xs_pad, masks)
|
||||
|
||||
if isinstance(xs_pad, tuple):
|
||||
xs_pad = xs_pad[0]
|
||||
|
||||
xs_pad = self.after_norm(xs_pad)
|
||||
olens = masks.squeeze(1).sum(1)
|
||||
return xs_pad, olens, None
|
||||
@@ -0,0 +1,152 @@
|
||||
"""Fastformer attention definition.
|
||||
|
||||
Reference:
|
||||
Wu et al., "Fastformer: Additive Attention Can Be All You Need"
|
||||
https://arxiv.org/abs/2108.09084
|
||||
https://github.com/wuch15/Fastformer
|
||||
|
||||
"""
|
||||
|
||||
import numpy
|
||||
import torch
|
||||
|
||||
|
||||
class FastSelfAttention(torch.nn.Module):
|
||||
"""Fast self-attention used in Fastformer."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
size,
|
||||
attention_heads,
|
||||
dropout_rate,
|
||||
):
|
||||
"""Initialize FastSelfAttention.
|
||||
|
||||
Args:
|
||||
size: TODO.
|
||||
attention_heads: TODO.
|
||||
dropout_rate: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
if size % attention_heads != 0:
|
||||
raise ValueError(
|
||||
f"Hidden size ({size}) is not an integer multiple "
|
||||
f"of attention heads ({attention_heads})"
|
||||
)
|
||||
self.attention_head_size = size // attention_heads
|
||||
self.num_attention_heads = attention_heads
|
||||
|
||||
self.query = torch.nn.Linear(size, size)
|
||||
self.query_att = torch.nn.Linear(size, attention_heads)
|
||||
self.key = torch.nn.Linear(size, size)
|
||||
self.key_att = torch.nn.Linear(size, attention_heads)
|
||||
self.transform = torch.nn.Linear(size, size)
|
||||
self.dropout = torch.nn.Dropout(dropout_rate)
|
||||
|
||||
def espnet_initialization_fn(self):
|
||||
"""Espnet initialization fn."""
|
||||
self.apply(self.init_weights)
|
||||
|
||||
def init_weights(self, module):
|
||||
"""Init weights.
|
||||
|
||||
Args:
|
||||
module: TODO.
|
||||
"""
|
||||
if isinstance(module, torch.nn.Linear):
|
||||
module.weight.data.normal_(mean=0.0, std=0.02)
|
||||
if isinstance(module, torch.nn.Linear) and module.bias is not None:
|
||||
module.bias.data.zero_()
|
||||
|
||||
def transpose_for_scores(self, x):
|
||||
"""Reshape and transpose to compute scores.
|
||||
|
||||
Args:
|
||||
x: (batch, time, size = n_heads * attn_dim)
|
||||
|
||||
Returns:
|
||||
(batch, n_heads, time, attn_dim)
|
||||
"""
|
||||
|
||||
new_x_shape = x.shape[:-1] + (
|
||||
self.num_attention_heads,
|
||||
self.attention_head_size,
|
||||
)
|
||||
return x.reshape(*new_x_shape).transpose(1, 2)
|
||||
|
||||
def forward(self, xs_pad, mask):
|
||||
"""Forward method.
|
||||
|
||||
Args:
|
||||
xs_pad: (batch, time, size = n_heads * attn_dim)
|
||||
mask: (batch, 1, time), nonpadding is 1, padding is 0
|
||||
|
||||
Returns:
|
||||
torch.Tensor: (batch, time, size)
|
||||
"""
|
||||
|
||||
batch_size, seq_len, _ = xs_pad.shape
|
||||
mixed_query_layer = self.query(xs_pad) # (batch, time, size)
|
||||
mixed_key_layer = self.key(xs_pad) # (batch, time, size)
|
||||
|
||||
if mask is not None:
|
||||
mask = mask.eq(0) # padding is 1, nonpadding is 0
|
||||
|
||||
# (batch, n_heads, time)
|
||||
query_for_score = (
|
||||
self.query_att(mixed_query_layer).transpose(1, 2) / self.attention_head_size**0.5
|
||||
)
|
||||
if mask is not None:
|
||||
min_value = float(
|
||||
numpy.finfo(torch.tensor(0, dtype=query_for_score.dtype).numpy().dtype).min
|
||||
)
|
||||
query_for_score = query_for_score.masked_fill(mask, min_value)
|
||||
query_weight = torch.softmax(query_for_score, dim=-1).masked_fill(mask, 0.0)
|
||||
else:
|
||||
query_weight = torch.softmax(query_for_score, dim=-1)
|
||||
|
||||
query_weight = query_weight.unsqueeze(2) # (batch, n_heads, 1, time)
|
||||
query_layer = self.transpose_for_scores(
|
||||
mixed_query_layer
|
||||
) # (batch, n_heads, time, attn_dim)
|
||||
|
||||
pooled_query = (
|
||||
torch.matmul(query_weight, query_layer)
|
||||
.transpose(1, 2)
|
||||
.reshape(-1, 1, self.num_attention_heads * self.attention_head_size)
|
||||
) # (batch, 1, size = n_heads * attn_dim)
|
||||
pooled_query = self.dropout(pooled_query)
|
||||
pooled_query_repeat = pooled_query.repeat(1, seq_len, 1) # (batch, time, size)
|
||||
|
||||
mixed_query_key_layer = mixed_key_layer * pooled_query_repeat # (batch, time, size)
|
||||
|
||||
# (batch, n_heads, time)
|
||||
query_key_score = (
|
||||
self.key_att(mixed_query_key_layer) / self.attention_head_size**0.5
|
||||
).transpose(1, 2)
|
||||
if mask is not None:
|
||||
min_value = float(
|
||||
numpy.finfo(torch.tensor(0, dtype=query_key_score.dtype).numpy().dtype).min
|
||||
)
|
||||
query_key_score = query_key_score.masked_fill(mask, min_value)
|
||||
query_key_weight = torch.softmax(query_key_score, dim=-1).masked_fill(mask, 0.0)
|
||||
else:
|
||||
query_key_weight = torch.softmax(query_key_score, dim=-1)
|
||||
|
||||
query_key_weight = query_key_weight.unsqueeze(2) # (batch, n_heads, 1, time)
|
||||
key_layer = self.transpose_for_scores(
|
||||
mixed_query_key_layer
|
||||
) # (batch, n_heads, time, attn_dim)
|
||||
pooled_key = torch.matmul(query_key_weight, key_layer) # (batch, n_heads, 1, attn_dim)
|
||||
pooled_key = self.dropout(pooled_key)
|
||||
|
||||
# NOTE: value = query, due to param sharing
|
||||
weighted_value = (pooled_key * query_layer).transpose(
|
||||
1, 2
|
||||
) # (batch, time, n_heads, attn_dim)
|
||||
weighted_value = weighted_value.reshape(
|
||||
weighted_value.shape[:-2] + (self.num_attention_heads * self.attention_head_size,)
|
||||
) # (batch, time, size)
|
||||
weighted_value = self.dropout(self.transform(weighted_value)) + mixed_query_layer
|
||||
|
||||
return weighted_value
|
||||
@@ -0,0 +1,29 @@
|
||||
import logging
|
||||
|
||||
from funasr.models.transformer.model import Transformer
|
||||
from funasr.register import tables
|
||||
|
||||
|
||||
@tables.register("model_classes", "Branchformer")
|
||||
class Branchformer(Transformer):
|
||||
"""Branchformer: Parallel branch encoder architecture.
|
||||
|
||||
Uses parallel branches of self-attention and convolution that are
|
||||
merged via concatenation. Alternative to Conformer with similar accuracy.
|
||||
|
||||
Inherits Transformer pipeline for training and inference.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
|
||||
"""Initialize Branchformer.
|
||||
|
||||
Args:
|
||||
*args: Variable positional arguments.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -0,0 +1,116 @@
|
||||
# This is an example that demonstrates how to configure a model file.
|
||||
# You can modify the configuration according to your own requirements.
|
||||
|
||||
# to print the register_table:
|
||||
# from funasr.register import tables
|
||||
# tables.print()
|
||||
|
||||
# network architecture
|
||||
model: Branchformer
|
||||
model_conf:
|
||||
ctc_weight: 0.3
|
||||
lsm_weight: 0.1 # label smoothing option
|
||||
length_normalized_loss: false
|
||||
|
||||
# encoder
|
||||
encoder: BranchformerEncoder
|
||||
encoder_conf:
|
||||
output_size: 256
|
||||
use_attn: true
|
||||
attention_heads: 4
|
||||
attention_layer_type: rel_selfattn
|
||||
pos_enc_layer_type: rel_pos
|
||||
rel_pos_type: latest
|
||||
use_cgmlp: true
|
||||
cgmlp_linear_units: 2048
|
||||
cgmlp_conv_kernel: 31
|
||||
use_linear_after_conv: false
|
||||
gate_activation: identity
|
||||
merge_method: concat
|
||||
cgmlp_weight: 0.5 # used only if merge_method is "fixed_ave"
|
||||
attn_branch_drop_rate: 0.0 # used only if merge_method is "learned_ave"
|
||||
num_blocks: 24
|
||||
dropout_rate: 0.1
|
||||
positional_dropout_rate: 0.1
|
||||
attention_dropout_rate: 0.1
|
||||
input_layer: conv2d
|
||||
stochastic_depth_rate: 0.0
|
||||
|
||||
# decoder
|
||||
decoder: TransformerDecoder
|
||||
decoder_conf:
|
||||
attention_heads: 4
|
||||
linear_units: 2048
|
||||
num_blocks: 6
|
||||
dropout_rate: 0.1
|
||||
positional_dropout_rate: 0.1
|
||||
self_attention_dropout_rate: 0.
|
||||
src_attention_dropout_rate: 0.
|
||||
|
||||
|
||||
# frontend related
|
||||
frontend: WavFrontend
|
||||
frontend_conf:
|
||||
fs: 16000
|
||||
window: hamming
|
||||
n_mels: 80
|
||||
frame_length: 25
|
||||
frame_shift: 10
|
||||
dither: 0.0
|
||||
lfr_m: 1
|
||||
lfr_n: 1
|
||||
|
||||
specaug: SpecAug
|
||||
specaug_conf:
|
||||
apply_time_warp: true
|
||||
time_warp_window: 5
|
||||
time_warp_mode: bicubic
|
||||
apply_freq_mask: true
|
||||
freq_mask_width_range:
|
||||
- 0
|
||||
- 30
|
||||
num_freq_mask: 2
|
||||
apply_time_mask: true
|
||||
time_mask_width_range:
|
||||
- 0
|
||||
- 40
|
||||
num_time_mask: 2
|
||||
|
||||
train_conf:
|
||||
accum_grad: 1
|
||||
grad_clip: 5
|
||||
max_epoch: 150
|
||||
keep_nbest_models: 10
|
||||
log_interval: 50
|
||||
|
||||
optim: adam
|
||||
optim_conf:
|
||||
lr: 0.001
|
||||
weight_decay: 0.000001
|
||||
scheduler: warmuplr
|
||||
scheduler_conf:
|
||||
warmup_steps: 35000
|
||||
|
||||
dataset: AudioDataset
|
||||
dataset_conf:
|
||||
index_ds: IndexDSJsonl
|
||||
batch_sampler: BatchSampler
|
||||
batch_type: example # example or length
|
||||
batch_size: 1 # if batch_type is example, batch_size is the numbers of samples; if length, batch_size is source_token_len+target_token_len;
|
||||
max_token_length: 2048 # filter samples if source_token_len+target_token_len > max_token_length,
|
||||
buffer_size: 500
|
||||
shuffle: True
|
||||
num_workers: 4
|
||||
|
||||
tokenizer: CharTokenizer
|
||||
tokenizer_conf:
|
||||
unk_symbol: <unk>
|
||||
split_with_space: true
|
||||
|
||||
|
||||
ctc_conf:
|
||||
dropout_rate: 0.0
|
||||
ctc_type: builtin
|
||||
reduce: true
|
||||
ignore_nan_grad: true
|
||||
normalize: null
|
||||
@@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
# Modified from 3D-Speaker (https://github.com/alibaba-damo-academy/3D-Speaker)
|
||||
|
||||
import scipy
|
||||
import torch
|
||||
import sklearn
|
||||
import numpy as np
|
||||
|
||||
from sklearn.cluster._kmeans import k_means
|
||||
from sklearn.cluster import HDBSCAN
|
||||
|
||||
|
||||
class SpectralCluster:
|
||||
r"""A spectral clustering mehtod using unnormalized Laplacian of affinity matrix.
|
||||
This implementation is adapted from https://github.com/speechbrain/speechbrain.
|
||||
"""
|
||||
|
||||
def __init__(self, min_num_spks=1, max_num_spks=15, pval=0.022):
|
||||
"""Initialize SpectralCluster.
|
||||
|
||||
Args:
|
||||
min_num_spks: TODO.
|
||||
max_num_spks: TODO.
|
||||
pval: TODO.
|
||||
"""
|
||||
self.min_num_spks = min_num_spks
|
||||
self.max_num_spks = max_num_spks
|
||||
self.pval = pval
|
||||
|
||||
def __call__(self, X, oracle_num=None):
|
||||
# Similarity matrix computation
|
||||
"""Internal: call .
|
||||
|
||||
Args:
|
||||
X: TODO.
|
||||
oracle_num: TODO.
|
||||
"""
|
||||
sim_mat = self.get_sim_mat(X)
|
||||
|
||||
# Refining similarity matrix with pval
|
||||
prunned_sim_mat = self.p_pruning(sim_mat)
|
||||
|
||||
# Symmetrization
|
||||
sym_prund_sim_mat = 0.5 * (prunned_sim_mat + prunned_sim_mat.T)
|
||||
|
||||
# Laplacian calculation
|
||||
laplacian = self.get_laplacian(sym_prund_sim_mat)
|
||||
|
||||
# Get Spectral Embeddings
|
||||
emb, num_of_spk = self.get_spec_embs(laplacian, oracle_num)
|
||||
|
||||
# Perform clustering
|
||||
labels = self.cluster_embs(emb, num_of_spk)
|
||||
|
||||
return labels
|
||||
|
||||
def get_sim_mat(self, X):
|
||||
# Cosine similarities
|
||||
"""Get sim mat.
|
||||
|
||||
Args:
|
||||
X: TODO.
|
||||
"""
|
||||
M = sklearn.metrics.pairwise.cosine_similarity(X, X)
|
||||
return M
|
||||
|
||||
def p_pruning(self, A):
|
||||
"""P pruning.
|
||||
|
||||
Args:
|
||||
A: TODO.
|
||||
"""
|
||||
if A.shape[0] * self.pval < 6:
|
||||
pval = 6.0 / A.shape[0]
|
||||
else:
|
||||
pval = self.pval
|
||||
|
||||
n_elems = int((1 - pval) * A.shape[0])
|
||||
|
||||
# For each row in a affinity matrix
|
||||
for i in range(A.shape[0]):
|
||||
low_indexes = np.argsort(A[i, :])
|
||||
low_indexes = low_indexes[0:n_elems]
|
||||
|
||||
# Replace smaller similarity values by 0s
|
||||
A[i, low_indexes] = 0
|
||||
return A
|
||||
|
||||
def get_laplacian(self, M):
|
||||
"""Get laplacian.
|
||||
|
||||
Args:
|
||||
M: TODO.
|
||||
"""
|
||||
M[np.diag_indices(M.shape[0])] = 0
|
||||
D = np.sum(np.abs(M), axis=1)
|
||||
D = np.diag(D)
|
||||
L = D - M
|
||||
return L
|
||||
|
||||
def get_spec_embs(self, L, k_oracle=None):
|
||||
"""Get spec embs.
|
||||
|
||||
Args:
|
||||
L: TODO.
|
||||
k_oracle: TODO.
|
||||
"""
|
||||
lambdas, eig_vecs = scipy.linalg.eigh(L)
|
||||
|
||||
if k_oracle is not None:
|
||||
num_of_spk = k_oracle
|
||||
else:
|
||||
lambda_gap_list = self.getEigenGaps(
|
||||
lambdas[self.min_num_spks - 1 : self.max_num_spks + 1]
|
||||
)
|
||||
num_of_spk = np.argmax(lambda_gap_list) + self.min_num_spks
|
||||
|
||||
emb = eig_vecs[:, :num_of_spk]
|
||||
return emb, num_of_spk
|
||||
|
||||
def cluster_embs(self, emb, k):
|
||||
"""Cluster embs.
|
||||
|
||||
Args:
|
||||
emb: TODO.
|
||||
k: TODO.
|
||||
"""
|
||||
_, labels, _ = k_means(emb, k)
|
||||
return labels
|
||||
|
||||
def getEigenGaps(self, eig_vals):
|
||||
"""Geteigengaps.
|
||||
|
||||
Args:
|
||||
eig_vals: TODO.
|
||||
"""
|
||||
eig_vals_gap_list = []
|
||||
for i in range(len(eig_vals) - 1):
|
||||
gap = float(eig_vals[i + 1]) - float(eig_vals[i])
|
||||
eig_vals_gap_list.append(gap)
|
||||
return eig_vals_gap_list
|
||||
|
||||
|
||||
class UmapHdbscan:
|
||||
r"""
|
||||
Reference:
|
||||
- Siqi Zheng, Hongbin Suo. Reformulating Speaker Diarization as Community Detection With
|
||||
Emphasis On Topological Structure. ICASSP2022
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, n_neighbors=20, n_components=60, min_samples=10, min_cluster_size=10, metric="cosine"
|
||||
):
|
||||
"""Initialize UmapHdbscan.
|
||||
|
||||
Args:
|
||||
n_neighbors: TODO.
|
||||
n_components: TODO.
|
||||
min_samples: TODO.
|
||||
min_cluster_size: Size/dimension parameter.
|
||||
metric: TODO.
|
||||
"""
|
||||
self.n_neighbors = n_neighbors
|
||||
self.n_components = n_components
|
||||
self.min_samples = min_samples
|
||||
self.min_cluster_size = min_cluster_size
|
||||
self.metric = metric
|
||||
|
||||
def __call__(self, X):
|
||||
"""Internal: call .
|
||||
|
||||
Args:
|
||||
X: TODO.
|
||||
"""
|
||||
import umap.umap_ as umap
|
||||
|
||||
umap_X = umap.UMAP(
|
||||
n_neighbors=self.n_neighbors,
|
||||
min_dist=0.0,
|
||||
n_components=min(self.n_components, X.shape[0] - 2),
|
||||
metric=self.metric,
|
||||
).fit_transform(X)
|
||||
labels = HDBSCAN(
|
||||
min_samples=self.min_samples,
|
||||
min_cluster_size=self.min_cluster_size,
|
||||
allow_single_cluster=True,
|
||||
).fit_predict(umap_X)
|
||||
return labels
|
||||
|
||||
|
||||
class ClusterBackend(torch.nn.Module):
|
||||
r"""Perfom clustering for input embeddings and output the labels.
|
||||
Args:
|
||||
model_dir: A model dir.
|
||||
model_config: The model config.
|
||||
"""
|
||||
|
||||
def __init__(self, merge_thr=0.78):
|
||||
"""Initialize ClusterBackend.
|
||||
|
||||
Args:
|
||||
merge_thr: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
self.model_config = {"merge_thr": merge_thr}
|
||||
# self.other_config = kwargs
|
||||
|
||||
self.spectral_cluster = SpectralCluster()
|
||||
self.umap_hdbscan_cluster = UmapHdbscan()
|
||||
|
||||
def forward(self, X, **params):
|
||||
# clustering and return the labels
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
X: TODO.
|
||||
**params: Additional keyword arguments.
|
||||
"""
|
||||
k = params["oracle_num"] if "oracle_num" in params else None
|
||||
assert len(X.shape) == 2, "modelscope error: the shape of input should be [N, C]"
|
||||
if X.shape[0] < 20:
|
||||
return np.zeros(X.shape[0], dtype="int")
|
||||
if X.shape[0] < 2048 or k is not None:
|
||||
# unexpected corner case
|
||||
labels = self.spectral_cluster(X, k)
|
||||
else:
|
||||
labels = self.umap_hdbscan_cluster(X)
|
||||
|
||||
if k is None and "merge_thr" in self.model_config:
|
||||
labels = self.merge_by_cos(labels, X, self.model_config["merge_thr"])
|
||||
|
||||
return labels
|
||||
|
||||
def merge_by_cos(self, labels, embs, cos_thr):
|
||||
# merge the similar speakers by cosine similarity
|
||||
"""Merge by cos.
|
||||
|
||||
Args:
|
||||
labels: TODO.
|
||||
embs: TODO.
|
||||
cos_thr: TODO.
|
||||
"""
|
||||
assert cos_thr > 0 and cos_thr <= 1
|
||||
while True:
|
||||
spk_num = labels.max() + 1
|
||||
if spk_num == 1:
|
||||
break
|
||||
spk_center = []
|
||||
for i in range(spk_num):
|
||||
spk_emb = embs[labels == i].mean(0)
|
||||
spk_center.append(spk_emb)
|
||||
assert len(spk_center) > 0
|
||||
spk_center = np.stack(spk_center, axis=0)
|
||||
norm_spk_center = spk_center / np.linalg.norm(spk_center, axis=1, keepdims=True)
|
||||
affinity = np.matmul(norm_spk_center, norm_spk_center.T)
|
||||
affinity = np.triu(affinity, 1)
|
||||
spks = np.unravel_index(np.argmax(affinity), affinity.shape)
|
||||
if affinity[spks] < cos_thr:
|
||||
break
|
||||
for i in range(len(labels)):
|
||||
if labels[i] == spks[1]:
|
||||
labels[i] = spks[0]
|
||||
elif labels[i] > spks[1]:
|
||||
labels[i] -= 1
|
||||
return labels
|
||||
@@ -0,0 +1,450 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
# Modified from 3D-Speaker (https://github.com/alibaba-damo-academy/3D-Speaker)
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import torch.utils.checkpoint as cp
|
||||
|
||||
|
||||
class BasicResBlock(torch.nn.Module):
|
||||
expansion = 1
|
||||
|
||||
def __init__(self, in_planes, planes, stride=1):
|
||||
"""Initialize BasicResBlock.
|
||||
|
||||
Args:
|
||||
in_planes: TODO.
|
||||
planes: TODO.
|
||||
stride: TODO.
|
||||
"""
|
||||
super(BasicResBlock, self).__init__()
|
||||
self.conv1 = torch.nn.Conv2d(
|
||||
in_planes, planes, kernel_size=3, stride=(stride, 1), padding=1, bias=False
|
||||
)
|
||||
self.bn1 = torch.nn.BatchNorm2d(planes)
|
||||
self.conv2 = torch.nn.Conv2d(planes, planes, kernel_size=3, stride=1, padding=1, bias=False)
|
||||
self.bn2 = torch.nn.BatchNorm2d(planes)
|
||||
|
||||
self.shortcut = torch.nn.Sequential()
|
||||
if stride != 1 or in_planes != self.expansion * planes:
|
||||
self.shortcut = torch.nn.Sequential(
|
||||
torch.nn.Conv2d(
|
||||
in_planes,
|
||||
self.expansion * planes,
|
||||
kernel_size=1,
|
||||
stride=(stride, 1),
|
||||
bias=False,
|
||||
),
|
||||
torch.nn.BatchNorm2d(self.expansion * planes),
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
out = F.relu(self.bn1(self.conv1(x)))
|
||||
out = self.bn2(self.conv2(out))
|
||||
out += self.shortcut(x)
|
||||
out = F.relu(out)
|
||||
return out
|
||||
|
||||
|
||||
class FCM(torch.nn.Module):
|
||||
def __init__(self, block=BasicResBlock, num_blocks=[2, 2], m_channels=32, feat_dim=80):
|
||||
"""Initialize FCM.
|
||||
|
||||
Args:
|
||||
block: TODO.
|
||||
num_blocks: TODO.
|
||||
m_channels: TODO.
|
||||
feat_dim: Size/dimension parameter.
|
||||
"""
|
||||
super(FCM, self).__init__()
|
||||
self.in_planes = m_channels
|
||||
self.conv1 = torch.nn.Conv2d(1, m_channels, kernel_size=3, stride=1, padding=1, bias=False)
|
||||
self.bn1 = torch.nn.BatchNorm2d(m_channels)
|
||||
|
||||
self.layer1 = self._make_layer(block, m_channels, num_blocks[0], stride=2)
|
||||
self.layer2 = self._make_layer(block, m_channels, num_blocks[0], stride=2)
|
||||
|
||||
self.conv2 = torch.nn.Conv2d(
|
||||
m_channels, m_channels, kernel_size=3, stride=(2, 1), padding=1, bias=False
|
||||
)
|
||||
self.bn2 = torch.nn.BatchNorm2d(m_channels)
|
||||
self.out_channels = m_channels * (feat_dim // 8)
|
||||
|
||||
def _make_layer(self, block, planes, num_blocks, stride):
|
||||
"""Internal: make layer.
|
||||
|
||||
Args:
|
||||
block: TODO.
|
||||
planes: TODO.
|
||||
num_blocks: TODO.
|
||||
stride: TODO.
|
||||
"""
|
||||
strides = [stride] + [1] * (num_blocks - 1)
|
||||
layers = []
|
||||
for stride in strides:
|
||||
layers.append(block(self.in_planes, planes, stride))
|
||||
self.in_planes = planes * block.expansion
|
||||
return torch.nn.Sequential(*layers)
|
||||
|
||||
def forward(self, x):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
x = x.unsqueeze(1)
|
||||
out = F.relu(self.bn1(self.conv1(x)))
|
||||
out = self.layer1(out)
|
||||
out = self.layer2(out)
|
||||
out = F.relu(self.bn2(self.conv2(out)))
|
||||
|
||||
shape = out.shape
|
||||
out = out.reshape(shape[0], shape[1] * shape[2], shape[3])
|
||||
return out
|
||||
|
||||
|
||||
def get_nonlinear(config_str, channels):
|
||||
"""Get nonlinear.
|
||||
|
||||
Args:
|
||||
config_str: TODO.
|
||||
channels: TODO.
|
||||
"""
|
||||
nonlinear = torch.nn.Sequential()
|
||||
for name in config_str.split("-"):
|
||||
if name == "relu":
|
||||
nonlinear.add_module("relu", torch.nn.ReLU(inplace=True))
|
||||
elif name == "prelu":
|
||||
nonlinear.add_module("prelu", torch.nn.PReLU(channels))
|
||||
elif name == "batchnorm":
|
||||
nonlinear.add_module("batchnorm", torch.nn.BatchNorm1d(channels))
|
||||
elif name == "batchnorm_":
|
||||
nonlinear.add_module("batchnorm", torch.nn.BatchNorm1d(channels, affine=False))
|
||||
else:
|
||||
raise ValueError("Unexpected module ({}).".format(name))
|
||||
return nonlinear
|
||||
|
||||
|
||||
def statistics_pooling(x, dim=-1, keepdim=False, unbiased=True, eps=1e-2):
|
||||
"""Statistics pooling.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
dim: TODO.
|
||||
keepdim: TODO.
|
||||
unbiased: TODO.
|
||||
eps: TODO.
|
||||
"""
|
||||
mean = x.mean(dim=dim)
|
||||
std = x.std(dim=dim, unbiased=unbiased)
|
||||
stats = torch.cat([mean, std], dim=-1)
|
||||
if keepdim:
|
||||
stats = stats.unsqueeze(dim=dim)
|
||||
return stats
|
||||
|
||||
|
||||
class StatsPool(torch.nn.Module):
|
||||
def forward(self, x):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
return statistics_pooling(x)
|
||||
|
||||
|
||||
class TDNNLayer(torch.nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
in_channels,
|
||||
out_channels,
|
||||
kernel_size,
|
||||
stride=1,
|
||||
padding=0,
|
||||
dilation=1,
|
||||
bias=False,
|
||||
config_str="batchnorm-relu",
|
||||
):
|
||||
"""Initialize TDNNLayer.
|
||||
|
||||
Args:
|
||||
in_channels: TODO.
|
||||
out_channels: TODO.
|
||||
kernel_size: Size/dimension parameter.
|
||||
stride: TODO.
|
||||
padding: TODO.
|
||||
dilation: TODO.
|
||||
bias: TODO.
|
||||
config_str: TODO.
|
||||
"""
|
||||
super(TDNNLayer, self).__init__()
|
||||
if padding < 0:
|
||||
assert (
|
||||
kernel_size % 2 == 1
|
||||
), "Expect equal paddings, but got even kernel size ({})".format(kernel_size)
|
||||
padding = (kernel_size - 1) // 2 * dilation
|
||||
self.linear = torch.nn.Conv1d(
|
||||
in_channels,
|
||||
out_channels,
|
||||
kernel_size,
|
||||
stride=stride,
|
||||
padding=padding,
|
||||
dilation=dilation,
|
||||
bias=bias,
|
||||
)
|
||||
self.nonlinear = get_nonlinear(config_str, out_channels)
|
||||
|
||||
def forward(self, x):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
x = self.linear(x)
|
||||
x = self.nonlinear(x)
|
||||
return x
|
||||
|
||||
|
||||
class CAMLayer(torch.nn.Module):
|
||||
def __init__(
|
||||
self, bn_channels, out_channels, kernel_size, stride, padding, dilation, bias, reduction=2
|
||||
):
|
||||
"""Initialize CAMLayer.
|
||||
|
||||
Args:
|
||||
bn_channels: TODO.
|
||||
out_channels: TODO.
|
||||
kernel_size: Size/dimension parameter.
|
||||
stride: TODO.
|
||||
padding: TODO.
|
||||
dilation: TODO.
|
||||
bias: TODO.
|
||||
reduction: TODO.
|
||||
"""
|
||||
super(CAMLayer, self).__init__()
|
||||
self.linear_local = torch.nn.Conv1d(
|
||||
bn_channels,
|
||||
out_channels,
|
||||
kernel_size,
|
||||
stride=stride,
|
||||
padding=padding,
|
||||
dilation=dilation,
|
||||
bias=bias,
|
||||
)
|
||||
self.linear1 = torch.nn.Conv1d(bn_channels, bn_channels // reduction, 1)
|
||||
self.relu = torch.nn.ReLU(inplace=True)
|
||||
self.linear2 = torch.nn.Conv1d(bn_channels // reduction, out_channels, 1)
|
||||
self.sigmoid = torch.nn.Sigmoid()
|
||||
|
||||
def forward(self, x):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
y = self.linear_local(x)
|
||||
context = x.mean(-1, keepdim=True) + self.seg_pooling(x)
|
||||
context = self.relu(self.linear1(context))
|
||||
m = self.sigmoid(self.linear2(context))
|
||||
return y * m
|
||||
|
||||
def seg_pooling(self, x, seg_len=100, stype="avg"):
|
||||
"""Seg pooling.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
seg_len: TODO.
|
||||
stype: TODO.
|
||||
"""
|
||||
if stype == "avg":
|
||||
seg = F.avg_pool1d(x, kernel_size=seg_len, stride=seg_len, ceil_mode=True)
|
||||
elif stype == "max":
|
||||
seg = F.max_pool1d(x, kernel_size=seg_len, stride=seg_len, ceil_mode=True)
|
||||
else:
|
||||
raise ValueError("Wrong segment pooling type.")
|
||||
shape = seg.shape
|
||||
seg = seg.unsqueeze(-1).expand(*shape, seg_len).reshape(*shape[:-1], -1)
|
||||
seg = seg[..., : x.shape[-1]]
|
||||
return seg
|
||||
|
||||
|
||||
class CAMDenseTDNNLayer(torch.nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
in_channels,
|
||||
out_channels,
|
||||
bn_channels,
|
||||
kernel_size,
|
||||
stride=1,
|
||||
dilation=1,
|
||||
bias=False,
|
||||
config_str="batchnorm-relu",
|
||||
memory_efficient=False,
|
||||
):
|
||||
"""Initialize CAMDenseTDNNLayer.
|
||||
|
||||
Args:
|
||||
in_channels: TODO.
|
||||
out_channels: TODO.
|
||||
bn_channels: TODO.
|
||||
kernel_size: Size/dimension parameter.
|
||||
stride: TODO.
|
||||
dilation: TODO.
|
||||
bias: TODO.
|
||||
config_str: TODO.
|
||||
memory_efficient: TODO.
|
||||
"""
|
||||
super(CAMDenseTDNNLayer, self).__init__()
|
||||
assert kernel_size % 2 == 1, "Expect equal paddings, but got even kernel size ({})".format(
|
||||
kernel_size
|
||||
)
|
||||
padding = (kernel_size - 1) // 2 * dilation
|
||||
self.memory_efficient = memory_efficient
|
||||
self.nonlinear1 = get_nonlinear(config_str, in_channels)
|
||||
self.linear1 = torch.nn.Conv1d(in_channels, bn_channels, 1, bias=False)
|
||||
self.nonlinear2 = get_nonlinear(config_str, bn_channels)
|
||||
self.cam_layer = CAMLayer(
|
||||
bn_channels,
|
||||
out_channels,
|
||||
kernel_size,
|
||||
stride=stride,
|
||||
padding=padding,
|
||||
dilation=dilation,
|
||||
bias=bias,
|
||||
)
|
||||
|
||||
def bn_function(self, x):
|
||||
"""Bn function.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
return self.linear1(self.nonlinear1(x))
|
||||
|
||||
def forward(self, x):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
if self.training and self.memory_efficient:
|
||||
x = cp.checkpoint(self.bn_function, x)
|
||||
else:
|
||||
x = self.bn_function(x)
|
||||
x = self.cam_layer(self.nonlinear2(x))
|
||||
return x
|
||||
|
||||
|
||||
class CAMDenseTDNNBlock(torch.nn.ModuleList):
|
||||
def __init__(
|
||||
self,
|
||||
num_layers,
|
||||
in_channels,
|
||||
out_channels,
|
||||
bn_channels,
|
||||
kernel_size,
|
||||
stride=1,
|
||||
dilation=1,
|
||||
bias=False,
|
||||
config_str="batchnorm-relu",
|
||||
memory_efficient=False,
|
||||
):
|
||||
"""Initialize CAMDenseTDNNBlock.
|
||||
|
||||
Args:
|
||||
num_layers: TODO.
|
||||
in_channels: TODO.
|
||||
out_channels: TODO.
|
||||
bn_channels: TODO.
|
||||
kernel_size: Size/dimension parameter.
|
||||
stride: TODO.
|
||||
dilation: TODO.
|
||||
bias: TODO.
|
||||
config_str: TODO.
|
||||
memory_efficient: TODO.
|
||||
"""
|
||||
super(CAMDenseTDNNBlock, self).__init__()
|
||||
for i in range(num_layers):
|
||||
layer = CAMDenseTDNNLayer(
|
||||
in_channels=in_channels + i * out_channels,
|
||||
out_channels=out_channels,
|
||||
bn_channels=bn_channels,
|
||||
kernel_size=kernel_size,
|
||||
stride=stride,
|
||||
dilation=dilation,
|
||||
bias=bias,
|
||||
config_str=config_str,
|
||||
memory_efficient=memory_efficient,
|
||||
)
|
||||
self.add_module("tdnnd%d" % (i + 1), layer)
|
||||
|
||||
def forward(self, x):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
for layer in self:
|
||||
x = torch.cat([x, layer(x)], dim=1)
|
||||
return x
|
||||
|
||||
|
||||
class TransitLayer(torch.nn.Module):
|
||||
def __init__(self, in_channels, out_channels, bias=True, config_str="batchnorm-relu"):
|
||||
"""Initialize TransitLayer.
|
||||
|
||||
Args:
|
||||
in_channels: TODO.
|
||||
out_channels: TODO.
|
||||
bias: TODO.
|
||||
config_str: TODO.
|
||||
"""
|
||||
super(TransitLayer, self).__init__()
|
||||
self.nonlinear = get_nonlinear(config_str, in_channels)
|
||||
self.linear = torch.nn.Conv1d(in_channels, out_channels, 1, bias=bias)
|
||||
|
||||
def forward(self, x):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
x = self.nonlinear(x)
|
||||
x = self.linear(x)
|
||||
return x
|
||||
|
||||
|
||||
class DenseLayer(torch.nn.Module):
|
||||
def __init__(self, in_channels, out_channels, bias=False, config_str="batchnorm-relu"):
|
||||
"""Initialize DenseLayer.
|
||||
|
||||
Args:
|
||||
in_channels: TODO.
|
||||
out_channels: TODO.
|
||||
bias: TODO.
|
||||
config_str: TODO.
|
||||
"""
|
||||
super(DenseLayer, self).__init__()
|
||||
self.linear = torch.nn.Conv1d(in_channels, out_channels, 1, bias=bias)
|
||||
self.nonlinear = get_nonlinear(config_str, out_channels)
|
||||
|
||||
def forward(self, x):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
if len(x.shape) == 2:
|
||||
x = self.linear(x.unsqueeze(dim=-1)).squeeze(dim=-1)
|
||||
else:
|
||||
x = self.linear(x)
|
||||
x = self.nonlinear(x)
|
||||
return x
|
||||
@@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
# Modified from 3D-Speaker (https://github.com/alibaba-damo-academy/3D-Speaker)
|
||||
|
||||
import time
|
||||
import torch
|
||||
import numpy as np
|
||||
from collections import OrderedDict
|
||||
from contextlib import contextmanager
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from funasr.register import tables
|
||||
from funasr.models.campplus.utils import extract_feature
|
||||
from funasr.utils.load_utils import load_audio_text_image_video
|
||||
from funasr.models.campplus.components import (
|
||||
DenseLayer,
|
||||
StatsPool,
|
||||
TDNNLayer,
|
||||
CAMDenseTDNNBlock,
|
||||
TransitLayer,
|
||||
get_nonlinear,
|
||||
FCM,
|
||||
)
|
||||
|
||||
|
||||
if LooseVersion(torch.__version__) >= LooseVersion("1.6.0"):
|
||||
from torch.cuda.amp import autocast
|
||||
else:
|
||||
# Nothing to do if torch<1.6.0
|
||||
@contextmanager
|
||||
def autocast(enabled=True):
|
||||
"""Autocast.
|
||||
|
||||
Args:
|
||||
enabled: TODO.
|
||||
"""
|
||||
yield
|
||||
|
||||
|
||||
@tables.register("model_classes", "CAMPPlus")
|
||||
class CAMPPlus(torch.nn.Module):
|
||||
"""CAM++ Speaker Verification Model.
|
||||
|
||||
Extracts fixed-dimensional speaker embeddings from variable-length audio.
|
||||
Used for speaker verification and speaker diarization pipelines.
|
||||
|
||||
Output: 192-dimensional speaker embedding per utterance.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
feat_dim=80,
|
||||
embedding_size=192,
|
||||
growth_rate=32,
|
||||
bn_size=4,
|
||||
init_channels=128,
|
||||
config_str="batchnorm-relu",
|
||||
memory_efficient=True,
|
||||
output_level="segment",
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize CAMPPlus.
|
||||
|
||||
Args:
|
||||
feat_dim: Size/dimension parameter.
|
||||
embedding_size: Size/dimension parameter.
|
||||
growth_rate: TODO.
|
||||
bn_size: Size/dimension parameter.
|
||||
init_channels: TODO.
|
||||
config_str: TODO.
|
||||
memory_efficient: TODO.
|
||||
output_level: TODO.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.head = FCM(feat_dim=feat_dim)
|
||||
channels = self.head.out_channels
|
||||
self.output_level = output_level
|
||||
|
||||
self.xvector = torch.nn.Sequential(
|
||||
OrderedDict(
|
||||
[
|
||||
(
|
||||
"tdnn",
|
||||
TDNNLayer(
|
||||
channels,
|
||||
init_channels,
|
||||
5,
|
||||
stride=2,
|
||||
dilation=1,
|
||||
padding=-1,
|
||||
config_str=config_str,
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
channels = init_channels
|
||||
for i, (num_layers, kernel_size, dilation) in enumerate(
|
||||
zip((12, 24, 16), (3, 3, 3), (1, 2, 2))
|
||||
):
|
||||
block = CAMDenseTDNNBlock(
|
||||
num_layers=num_layers,
|
||||
in_channels=channels,
|
||||
out_channels=growth_rate,
|
||||
bn_channels=bn_size * growth_rate,
|
||||
kernel_size=kernel_size,
|
||||
dilation=dilation,
|
||||
config_str=config_str,
|
||||
memory_efficient=memory_efficient,
|
||||
)
|
||||
self.xvector.add_module("block%d" % (i + 1), block)
|
||||
channels = channels + num_layers * growth_rate
|
||||
self.xvector.add_module(
|
||||
"transit%d" % (i + 1),
|
||||
TransitLayer(channels, channels // 2, bias=False, config_str=config_str),
|
||||
)
|
||||
channels //= 2
|
||||
|
||||
self.xvector.add_module("out_nonlinear", get_nonlinear(config_str, channels))
|
||||
|
||||
if self.output_level == "segment":
|
||||
self.xvector.add_module("stats", StatsPool())
|
||||
self.xvector.add_module(
|
||||
"dense", DenseLayer(channels * 2, embedding_size, config_str="batchnorm_")
|
||||
)
|
||||
else:
|
||||
assert (
|
||||
self.output_level == "frame"
|
||||
), "`output_level` should be set to 'segment' or 'frame'. "
|
||||
|
||||
for m in self.modules():
|
||||
if isinstance(m, (torch.nn.Conv1d, torch.nn.Linear)):
|
||||
torch.nn.init.kaiming_normal_(m.weight.data)
|
||||
if m.bias is not None:
|
||||
torch.nn.init.zeros_(m.bias)
|
||||
|
||||
def forward(self, x):
|
||||
"""Extract speaker embedding from fbank features.
|
||||
|
||||
Args:
|
||||
x (Tensor): Input fbank features, shape (batch, time, feat_dim).
|
||||
|
||||
Returns:
|
||||
Tensor: Speaker embedding, shape (batch, embedding_size) for segment level,
|
||||
or (batch, time, channels) for frame level.
|
||||
"""
|
||||
x = x.permute(0, 2, 1) # (B,T,F) => (B,F,T)
|
||||
x = self.head(x)
|
||||
x = self.xvector(x)
|
||||
if self.output_level == "frame":
|
||||
x = x.transpose(1, 2)
|
||||
return x
|
||||
|
||||
def inference(
|
||||
self,
|
||||
data_in,
|
||||
data_lengths=None,
|
||||
key: list = None,
|
||||
tokenizer=None,
|
||||
frontend=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Run speaker embedding extraction on audio input.
|
||||
|
||||
Args:
|
||||
data_in: Audio input (file path, numpy array, or list).
|
||||
data_lengths: Not used.
|
||||
key (list): Sample identifiers.
|
||||
tokenizer: Not used.
|
||||
frontend: Not used.
|
||||
**kwargs: Must include 'device' (str) and optional 'fs' (int, default 16000).
|
||||
|
||||
Returns:
|
||||
tuple: (results, meta_data) where results is
|
||||
[{"spk_embedding": Tensor of shape (1, 192)}]
|
||||
"""
|
||||
# extract fbank feats
|
||||
meta_data = {}
|
||||
time1 = time.perf_counter()
|
||||
audio_sample_list = load_audio_text_image_video(
|
||||
data_in, fs=16000, audio_fs=kwargs.get("fs", 16000), data_type="sound"
|
||||
)
|
||||
time2 = time.perf_counter()
|
||||
meta_data["load_data"] = f"{time2 - time1:0.3f}"
|
||||
speech, speech_lengths, speech_times = extract_feature(audio_sample_list)
|
||||
speech = speech.to(device=kwargs["device"])
|
||||
time3 = time.perf_counter()
|
||||
meta_data["extract_feat"] = f"{time3 - time2:0.3f}"
|
||||
meta_data["batch_data_time"] = np.array(speech_times).sum().item() / 16000.0
|
||||
results = [{"spk_embedding": self.forward(speech.to(torch.float32))}]
|
||||
return results, meta_data
|
||||
@@ -0,0 +1,23 @@
|
||||
# This is an example that demonstrates how to configure a model file.
|
||||
# You can modify the configuration according to your own requirements.
|
||||
|
||||
# to print the register_table:
|
||||
# from funasr.register import tables
|
||||
# tables.print()
|
||||
|
||||
# network architecture
|
||||
model: CAMPPlus
|
||||
model_conf:
|
||||
feat_dim: 80
|
||||
embedding_size: 192
|
||||
growth_rate: 32
|
||||
bn_size: 4
|
||||
init_channels: 128
|
||||
config_str: 'batchnorm-relu'
|
||||
memory_efficient: True
|
||||
output_level: 'segment'
|
||||
|
||||
# frontend related
|
||||
frontend: WavFrontend
|
||||
frontend_conf:
|
||||
fs: 16000
|
||||
@@ -0,0 +1,649 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
# Modified from 3D-Speaker (https://github.com/alibaba-damo-academy/3D-Speaker)
|
||||
|
||||
import io
|
||||
import os
|
||||
import torch
|
||||
import requests
|
||||
import tempfile
|
||||
import contextlib
|
||||
import numpy as np
|
||||
import librosa as sf
|
||||
from typing import Union
|
||||
from pathlib import Path
|
||||
from typing import Generator, Union
|
||||
from abc import ABCMeta, abstractmethod
|
||||
import torchaudio.compliance.kaldi as Kaldi
|
||||
|
||||
from funasr.models.transformer.utils.nets_utils import pad_list
|
||||
|
||||
|
||||
def check_audio_list(audio: list):
|
||||
"""Check audio list.
|
||||
|
||||
Args:
|
||||
audio: TODO.
|
||||
"""
|
||||
audio_dur = 0
|
||||
for i in range(len(audio)):
|
||||
seg = audio[i]
|
||||
assert seg[1] >= seg[0], "modelscope error: Wrong time stamps."
|
||||
assert isinstance(seg[2], np.ndarray), "modelscope error: Wrong data type."
|
||||
assert (
|
||||
int(seg[1] * 16000) - int(seg[0] * 16000) == seg[2].shape[0]
|
||||
), "modelscope error: audio data in list is inconsistent with time length."
|
||||
if i > 0:
|
||||
assert seg[0] >= audio[i - 1][1], "modelscope error: Wrong time stamps."
|
||||
audio_dur += seg[1] - seg[0]
|
||||
return audio_dur
|
||||
# assert audio_dur > 5, 'modelscope error: The effective audio duration is too short.'
|
||||
|
||||
|
||||
def sv_preprocess(inputs: Union[np.ndarray, list]):
|
||||
"""Sv preprocess.
|
||||
|
||||
Args:
|
||||
inputs: TODO.
|
||||
"""
|
||||
output = []
|
||||
for i in range(len(inputs)):
|
||||
if isinstance(inputs[i], str):
|
||||
file_bytes = File.read(inputs[i])
|
||||
data, fs = sf.load(io.BytesIO(file_bytes), dtype="float32")
|
||||
if len(data.shape) == 2:
|
||||
data = data[:, 0]
|
||||
data = torch.from_numpy(data).unsqueeze(0)
|
||||
data = data.squeeze(0)
|
||||
elif isinstance(inputs[i], np.ndarray):
|
||||
assert len(inputs[i].shape) == 1, "modelscope error: Input array should be [N, T]"
|
||||
data = inputs[i]
|
||||
if data.dtype in ["int16", "int32", "int64"]:
|
||||
data = (data / (1 << 15)).astype("float32")
|
||||
else:
|
||||
data = data.astype("float32")
|
||||
data = torch.from_numpy(data)
|
||||
else:
|
||||
raise ValueError(
|
||||
"modelscope error: The input type is restricted to audio address and nump array."
|
||||
)
|
||||
output.append(data)
|
||||
return output
|
||||
|
||||
|
||||
def sv_chunk(vad_segments: list, fs=16000) -> list:
|
||||
"""Sv chunk.
|
||||
|
||||
Args:
|
||||
vad_segments: TODO.
|
||||
fs: TODO.
|
||||
"""
|
||||
config = {
|
||||
"seg_dur": 1.5,
|
||||
"seg_shift": 0.75,
|
||||
}
|
||||
|
||||
def seg_chunk(seg_data):
|
||||
"""Seg chunk.
|
||||
|
||||
Args:
|
||||
seg_data: TODO.
|
||||
"""
|
||||
seg_st = seg_data[0]
|
||||
data = seg_data[2]
|
||||
chunk_len = int(config["seg_dur"] * fs)
|
||||
chunk_shift = int(config["seg_shift"] * fs)
|
||||
last_chunk_ed = 0
|
||||
seg_res = []
|
||||
for chunk_st in range(0, data.shape[0], chunk_shift):
|
||||
chunk_ed = min(chunk_st + chunk_len, data.shape[0])
|
||||
if chunk_ed <= last_chunk_ed:
|
||||
break
|
||||
last_chunk_ed = chunk_ed
|
||||
chunk_st = max(0, chunk_ed - chunk_len)
|
||||
chunk_data = data[chunk_st:chunk_ed]
|
||||
if chunk_data.shape[0] < chunk_len:
|
||||
chunk_data = np.pad(chunk_data, (0, chunk_len - chunk_data.shape[0]), "constant")
|
||||
seg_res.append([chunk_st / fs + seg_st, chunk_ed / fs + seg_st, chunk_data])
|
||||
return seg_res
|
||||
|
||||
segs = []
|
||||
for i, s in enumerate(vad_segments):
|
||||
segs.extend(seg_chunk(s))
|
||||
|
||||
return segs
|
||||
|
||||
|
||||
def extract_feature(audio):
|
||||
"""Extract feature.
|
||||
|
||||
Args:
|
||||
audio: TODO.
|
||||
"""
|
||||
features = []
|
||||
feature_times = []
|
||||
feature_lengths = []
|
||||
for au in audio:
|
||||
feature = Kaldi.fbank(au.unsqueeze(0), num_mel_bins=80)
|
||||
feature = feature - feature.mean(dim=0, keepdim=True)
|
||||
features.append(feature)
|
||||
feature_times.append(au.shape[0])
|
||||
feature_lengths.append(feature.shape[0])
|
||||
# padding for batch inference
|
||||
features_padded = pad_list(features, pad_value=0)
|
||||
# features = torch.cat(features)
|
||||
return features_padded, feature_lengths, feature_times
|
||||
|
||||
|
||||
def postprocess(
|
||||
segments: list,
|
||||
vad_segments: list,
|
||||
labels: np.ndarray,
|
||||
embeddings: np.ndarray,
|
||||
return_spk_center: bool = False,
|
||||
) -> Union[list, tuple]:
|
||||
"""Postprocess.
|
||||
|
||||
Args:
|
||||
segments: TODO.
|
||||
vad_segments: TODO.
|
||||
labels: TODO.
|
||||
embeddings: TODO.
|
||||
"""
|
||||
assert len(segments) == len(labels)
|
||||
labels = correct_labels(labels)
|
||||
distribute_res = []
|
||||
for i in range(len(segments)):
|
||||
distribute_res.append([segments[i][0], segments[i][1], labels[i]])
|
||||
# merge the same speakers chronologically
|
||||
distribute_res = merge_seque(distribute_res)
|
||||
|
||||
def is_overlapped(t1, t2):
|
||||
"""Is overlapped.
|
||||
|
||||
Args:
|
||||
t1: TODO.
|
||||
t2: TODO.
|
||||
"""
|
||||
if t1 > t2 + 1e-4:
|
||||
return True
|
||||
return False
|
||||
|
||||
# distribute the overlap region
|
||||
for i in range(1, len(distribute_res)):
|
||||
if is_overlapped(distribute_res[i - 1][1], distribute_res[i][0]):
|
||||
p = (distribute_res[i][0] + distribute_res[i - 1][1]) / 2
|
||||
distribute_res[i][0] = p
|
||||
distribute_res[i - 1][1] = p
|
||||
|
||||
# smooth the result
|
||||
distribute_res = smooth(distribute_res)
|
||||
|
||||
if return_spk_center:
|
||||
# spk_embs[i] is the centroid (mean of clustered chunk embeddings) for
|
||||
# corrected speaker label i, aligned with the `spk` ids in sentence_info.
|
||||
# Computed lazily: only when the caller requests speaker centers.
|
||||
spk_embs = np.stack(
|
||||
[embeddings[labels == i].mean(0) for i in range(labels.max() + 1)]
|
||||
)
|
||||
return distribute_res, spk_embs
|
||||
return distribute_res
|
||||
|
||||
|
||||
def correct_labels(labels):
|
||||
"""Correct labels.
|
||||
|
||||
Args:
|
||||
labels: TODO.
|
||||
"""
|
||||
labels_id = 0
|
||||
id2id = {}
|
||||
new_labels = []
|
||||
for i in labels:
|
||||
if i not in id2id:
|
||||
id2id[i] = labels_id
|
||||
labels_id += 1
|
||||
new_labels.append(id2id[i])
|
||||
return np.array(new_labels)
|
||||
|
||||
|
||||
def merge_seque(distribute_res):
|
||||
"""Merge seque.
|
||||
|
||||
Args:
|
||||
distribute_res: TODO.
|
||||
"""
|
||||
res = [distribute_res[0]]
|
||||
for i in range(1, len(distribute_res)):
|
||||
if distribute_res[i][2] != res[-1][2] or distribute_res[i][0] > res[-1][1]:
|
||||
res.append(distribute_res[i])
|
||||
else:
|
||||
res[-1][1] = distribute_res[i][1]
|
||||
return res
|
||||
|
||||
|
||||
def smooth(res, mindur=0.7):
|
||||
# if only one segment, return directly
|
||||
"""Smooth.
|
||||
|
||||
Args:
|
||||
res: TODO.
|
||||
mindur: TODO.
|
||||
"""
|
||||
if len(res) < 2:
|
||||
return res
|
||||
# short segments are assigned to nearest speakers.
|
||||
for i in range(len(res)):
|
||||
res[i][0] = round(res[i][0], 2)
|
||||
res[i][1] = round(res[i][1], 2)
|
||||
if res[i][1] - res[i][0] < mindur:
|
||||
if i == 0:
|
||||
res[i][2] = res[i + 1][2]
|
||||
elif i == len(res) - 1:
|
||||
res[i][2] = res[i - 1][2]
|
||||
elif res[i][0] - res[i - 1][1] <= res[i + 1][0] - res[i][1]:
|
||||
res[i][2] = res[i - 1][2]
|
||||
else:
|
||||
res[i][2] = res[i + 1][2]
|
||||
# merge the speakers
|
||||
res = merge_seque(res)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def distribute_spk(sentence_list, sd_time_list):
|
||||
"""Distribute spk.
|
||||
|
||||
Args:
|
||||
sentence_list: TODO.
|
||||
sd_time_list: TODO.
|
||||
"""
|
||||
sd_time_list = [(spk_st * 1000, spk_ed * 1000, spk) for spk_st, spk_ed, spk in sd_time_list]
|
||||
for d in sentence_list:
|
||||
sentence_start = d['start']
|
||||
sentence_end = d['end']
|
||||
sentence_spk = 0
|
||||
max_overlap = 0
|
||||
for spk_st, spk_ed, spk in sd_time_list:
|
||||
overlap = max(min(sentence_end, spk_ed) - max(sentence_start, spk_st), 0)
|
||||
if overlap > max_overlap:
|
||||
max_overlap = overlap
|
||||
sentence_spk = spk
|
||||
if overlap > 0 and sentence_spk == spk:
|
||||
max_overlap += overlap
|
||||
d['spk'] = int(sentence_spk)
|
||||
return sentence_list
|
||||
|
||||
|
||||
class Storage(metaclass=ABCMeta):
|
||||
"""Abstract class of storage.
|
||||
|
||||
All backends need to implement two apis: ``read()`` and ``read_text()``.
|
||||
``read()`` reads the file as a byte stream and ``read_text()`` reads
|
||||
the file as texts.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def read(self, filepath: str):
|
||||
"""Read.
|
||||
|
||||
Args:
|
||||
filepath: TODO.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def read_text(self, filepath: str):
|
||||
"""Read text.
|
||||
|
||||
Args:
|
||||
filepath: TODO.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def write(self, obj: bytes, filepath: Union[str, Path]) -> None:
|
||||
"""Write.
|
||||
|
||||
Args:
|
||||
obj: TODO.
|
||||
filepath: TODO.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def write_text(self, obj: str, filepath: Union[str, Path], encoding: str = "utf-8") -> None:
|
||||
"""Write text.
|
||||
|
||||
Args:
|
||||
obj: TODO.
|
||||
filepath: TODO.
|
||||
encoding: TODO.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class LocalStorage(Storage):
|
||||
"""Local hard disk storage"""
|
||||
|
||||
def read(self, filepath: Union[str, Path]) -> bytes:
|
||||
"""Read data from a given ``filepath`` with 'rb' mode.
|
||||
|
||||
Args:
|
||||
filepath (str or Path): Path to read data.
|
||||
|
||||
Returns:
|
||||
bytes: Expected bytes object.
|
||||
"""
|
||||
with open(filepath, "rb") as f:
|
||||
content = f.read()
|
||||
return content
|
||||
|
||||
def read_text(self, filepath: Union[str, Path], encoding: str = "utf-8") -> str:
|
||||
"""Read data from a given ``filepath`` with 'r' mode.
|
||||
|
||||
Args:
|
||||
filepath (str or Path): Path to read data.
|
||||
encoding (str): The encoding format used to open the ``filepath``.
|
||||
Default: 'utf-8'.
|
||||
|
||||
Returns:
|
||||
str: Expected text reading from ``filepath``.
|
||||
"""
|
||||
with open(filepath, "r", encoding=encoding) as f:
|
||||
value_buf = f.read()
|
||||
return value_buf
|
||||
|
||||
def write(self, obj: bytes, filepath: Union[str, Path]) -> None:
|
||||
"""Write data to a given ``filepath`` with 'wb' mode.
|
||||
|
||||
Note:
|
||||
``write`` will create a directory if the directory of ``filepath``
|
||||
does not exist.
|
||||
|
||||
Args:
|
||||
obj (bytes): Data to be written.
|
||||
filepath (str or Path): Path to write data.
|
||||
"""
|
||||
dirname = os.path.dirname(filepath)
|
||||
if dirname and not os.path.exists(dirname):
|
||||
os.makedirs(dirname, exist_ok=True)
|
||||
|
||||
with open(filepath, "wb") as f:
|
||||
f.write(obj)
|
||||
|
||||
def write_text(self, obj: str, filepath: Union[str, Path], encoding: str = "utf-8") -> None:
|
||||
"""Write data to a given ``filepath`` with 'w' mode.
|
||||
|
||||
Note:
|
||||
``write_text`` will create a directory if the directory of
|
||||
``filepath`` does not exist.
|
||||
|
||||
Args:
|
||||
obj (str): Data to be written.
|
||||
filepath (str or Path): Path to write data.
|
||||
encoding (str): The encoding format used to open the ``filepath``.
|
||||
Default: 'utf-8'.
|
||||
"""
|
||||
dirname = os.path.dirname(filepath)
|
||||
if dirname and not os.path.exists(dirname):
|
||||
os.makedirs(dirname, exist_ok=True)
|
||||
|
||||
with open(filepath, "w", encoding=encoding) as f:
|
||||
f.write(obj)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def as_local_path(self, filepath: Union[str, Path]) -> Generator[Union[str, Path], None, None]:
|
||||
"""Only for unified API and do nothing."""
|
||||
yield filepath
|
||||
|
||||
|
||||
class HTTPStorage(Storage):
|
||||
"""HTTP and HTTPS storage."""
|
||||
|
||||
def read(self, url):
|
||||
# TODO @wenmeng.zwm add progress bar if file is too large
|
||||
"""Read.
|
||||
|
||||
Args:
|
||||
url: TODO.
|
||||
"""
|
||||
r = requests.get(url)
|
||||
r.raise_for_status()
|
||||
return r.content
|
||||
|
||||
def read_text(self, url):
|
||||
"""Read text.
|
||||
|
||||
Args:
|
||||
url: TODO.
|
||||
"""
|
||||
r = requests.get(url)
|
||||
r.raise_for_status()
|
||||
return r.text
|
||||
|
||||
@contextlib.contextmanager
|
||||
def as_local_path(self, filepath: str) -> Generator[Union[str, Path], None, None]:
|
||||
"""Download a file from ``filepath``.
|
||||
|
||||
``as_local_path`` is decorated by :meth:`contextlib.contextmanager`. It
|
||||
can be called with ``with`` statement, and when exists from the
|
||||
``with`` statement, the temporary path will be released.
|
||||
|
||||
Args:
|
||||
filepath (str): Download a file from ``filepath``.
|
||||
|
||||
Examples:
|
||||
>>> storage = HTTPStorage()
|
||||
>>> # After existing from the ``with`` clause,
|
||||
>>> # the path will be removed
|
||||
>>> with storage.get_local_path('http://path/to/file') as path:
|
||||
... # do something here
|
||||
"""
|
||||
try:
|
||||
f = tempfile.NamedTemporaryFile(delete=False)
|
||||
f.write(self.read(filepath))
|
||||
f.close()
|
||||
yield f.name
|
||||
finally:
|
||||
os.remove(f.name)
|
||||
|
||||
def write(self, obj: bytes, url: Union[str, Path]) -> None:
|
||||
"""Write.
|
||||
|
||||
Args:
|
||||
obj: TODO.
|
||||
url: TODO.
|
||||
"""
|
||||
raise NotImplementedError("write is not supported by HTTP Storage")
|
||||
|
||||
def write_text(self, obj: str, url: Union[str, Path], encoding: str = "utf-8") -> None:
|
||||
"""Write text.
|
||||
|
||||
Args:
|
||||
obj: TODO.
|
||||
url: TODO.
|
||||
encoding: TODO.
|
||||
"""
|
||||
raise NotImplementedError("write_text is not supported by HTTP Storage")
|
||||
|
||||
|
||||
class OSSStorage(Storage):
|
||||
"""OSS storage."""
|
||||
|
||||
def __init__(self, oss_config_file=None):
|
||||
# read from config file or env var
|
||||
"""Initialize OSSStorage.
|
||||
|
||||
Args:
|
||||
oss_config_file: TODO.
|
||||
"""
|
||||
raise NotImplementedError("OSSStorage.__init__ to be implemented in the future")
|
||||
|
||||
def read(self, filepath):
|
||||
"""Read.
|
||||
|
||||
Args:
|
||||
filepath: TODO.
|
||||
"""
|
||||
raise NotImplementedError("OSSStorage.read to be implemented in the future")
|
||||
|
||||
def read_text(self, filepath, encoding="utf-8"):
|
||||
"""Read text.
|
||||
|
||||
Args:
|
||||
filepath: TODO.
|
||||
encoding: TODO.
|
||||
"""
|
||||
raise NotImplementedError("OSSStorage.read_text to be implemented in the future")
|
||||
|
||||
@contextlib.contextmanager
|
||||
def as_local_path(self, filepath: str) -> Generator[Union[str, Path], None, None]:
|
||||
"""Download a file from ``filepath``.
|
||||
|
||||
``as_local_path`` is decorated by :meth:`contextlib.contextmanager`. It
|
||||
can be called with ``with`` statement, and when exists from the
|
||||
``with`` statement, the temporary path will be released.
|
||||
|
||||
Args:
|
||||
filepath (str): Download a file from ``filepath``.
|
||||
|
||||
Examples:
|
||||
>>> storage = OSSStorage()
|
||||
>>> # After existing from the ``with`` clause,
|
||||
>>> # the path will be removed
|
||||
>>> with storage.get_local_path('http://path/to/file') as path:
|
||||
... # do something here
|
||||
"""
|
||||
try:
|
||||
f = tempfile.NamedTemporaryFile(delete=False)
|
||||
f.write(self.read(filepath))
|
||||
f.close()
|
||||
yield f.name
|
||||
finally:
|
||||
os.remove(f.name)
|
||||
|
||||
def write(self, obj: bytes, filepath: Union[str, Path]) -> None:
|
||||
"""Write.
|
||||
|
||||
Args:
|
||||
obj: TODO.
|
||||
filepath: TODO.
|
||||
"""
|
||||
raise NotImplementedError("OSSStorage.write to be implemented in the future")
|
||||
|
||||
def write_text(self, obj: str, filepath: Union[str, Path], encoding: str = "utf-8") -> None:
|
||||
"""Write text.
|
||||
|
||||
Args:
|
||||
obj: TODO.
|
||||
filepath: TODO.
|
||||
encoding: TODO.
|
||||
"""
|
||||
raise NotImplementedError("OSSStorage.write_text to be implemented in the future")
|
||||
|
||||
|
||||
G_STORAGES = {}
|
||||
|
||||
|
||||
class File(object):
|
||||
_prefix_to_storage: dict = {
|
||||
"oss": OSSStorage,
|
||||
"http": HTTPStorage,
|
||||
"https": HTTPStorage,
|
||||
"local": LocalStorage,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _get_storage(uri):
|
||||
"""Internal: get storage.
|
||||
|
||||
Args:
|
||||
uri: TODO.
|
||||
"""
|
||||
assert isinstance(uri, str), f"uri should be str type, but got {type(uri)}"
|
||||
|
||||
if "://" not in uri:
|
||||
# local path
|
||||
storage_type = "local"
|
||||
else:
|
||||
prefix, _ = uri.split("://")
|
||||
storage_type = prefix
|
||||
|
||||
assert storage_type in File._prefix_to_storage, (
|
||||
f"Unsupported uri {uri}, valid prefixs: " f"{list(File._prefix_to_storage.keys())}"
|
||||
)
|
||||
|
||||
if storage_type not in G_STORAGES:
|
||||
G_STORAGES[storage_type] = File._prefix_to_storage[storage_type]()
|
||||
|
||||
return G_STORAGES[storage_type]
|
||||
|
||||
@staticmethod
|
||||
def read(uri: str) -> bytes:
|
||||
"""Read data from a given ``filepath`` with 'rb' mode.
|
||||
|
||||
Args:
|
||||
filepath (str or Path): Path to read data.
|
||||
|
||||
Returns:
|
||||
bytes: Expected bytes object.
|
||||
"""
|
||||
storage = File._get_storage(uri)
|
||||
return storage.read(uri)
|
||||
|
||||
@staticmethod
|
||||
def read_text(uri: Union[str, Path], encoding: str = "utf-8") -> str:
|
||||
"""Read data from a given ``filepath`` with 'r' mode.
|
||||
|
||||
Args:
|
||||
filepath (str or Path): Path to read data.
|
||||
encoding (str): The encoding format used to open the ``filepath``.
|
||||
Default: 'utf-8'.
|
||||
|
||||
Returns:
|
||||
str: Expected text reading from ``filepath``.
|
||||
"""
|
||||
storage = File._get_storage(uri)
|
||||
return storage.read_text(uri)
|
||||
|
||||
@staticmethod
|
||||
def write(obj: bytes, uri: Union[str, Path]) -> None:
|
||||
"""Write data to a given ``filepath`` with 'wb' mode.
|
||||
|
||||
Note:
|
||||
``write`` will create a directory if the directory of ``filepath``
|
||||
does not exist.
|
||||
|
||||
Args:
|
||||
obj (bytes): Data to be written.
|
||||
filepath (str or Path): Path to write data.
|
||||
"""
|
||||
storage = File._get_storage(uri)
|
||||
return storage.write(obj, uri)
|
||||
|
||||
@staticmethod
|
||||
def write_text(obj: str, uri: str, encoding: str = "utf-8") -> None:
|
||||
"""Write data to a given ``filepath`` with 'w' mode.
|
||||
|
||||
Note:
|
||||
``write_text`` will create a directory if the directory of
|
||||
``filepath`` does not exist.
|
||||
|
||||
Args:
|
||||
obj (str): Data to be written.
|
||||
filepath (str or Path): Path to write data.
|
||||
encoding (str): The encoding format used to open the ``filepath``.
|
||||
Default: 'utf-8'.
|
||||
"""
|
||||
storage = File._get_storage(uri)
|
||||
return storage.write_text(obj, uri)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def as_local_path(uri: str) -> Generator[Union[str, Path], None, None]:
|
||||
"""Only for unified API and do nothing."""
|
||||
storage = File._get_storage(uri)
|
||||
with storage.as_local_path(uri) as local_path:
|
||||
yield local_path
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
import logging
|
||||
|
||||
import torch
|
||||
|
||||
from funasr.models.transformer.model import Transformer
|
||||
from funasr.register import tables
|
||||
|
||||
|
||||
@tables.register("model_classes", "Conformer")
|
||||
class Conformer(Transformer):
|
||||
"""Conformer: CTC-attention hybrid encoder-decoder model.
|
||||
|
||||
Combines convolution and self-attention in the encoder for better
|
||||
local and global context modeling. Inherits full Transformer pipeline
|
||||
(CTC + attention decoder + beam search).
|
||||
|
||||
Output: {"key": str, "text": str}
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
|
||||
"""Initialize Conformer.
|
||||
|
||||
Args:
|
||||
*args: Variable positional arguments.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -0,0 +1,117 @@
|
||||
# This is an example that demonstrates how to configure a model file.
|
||||
# You can modify the configuration according to your own requirements.
|
||||
|
||||
# to print the register_table:
|
||||
# from funasr.register import tables
|
||||
# tables.print()
|
||||
|
||||
# network architecture
|
||||
model: Conformer
|
||||
model_conf:
|
||||
ctc_weight: 0.3
|
||||
lsm_weight: 0.1 # label smoothing option
|
||||
length_normalized_loss: false
|
||||
|
||||
# encoder
|
||||
encoder: ConformerEncoder
|
||||
encoder_conf:
|
||||
output_size: 256
|
||||
attention_heads: 4
|
||||
linear_units: 2048
|
||||
num_blocks: 12
|
||||
dropout_rate: 0.1
|
||||
positional_dropout_rate: 0.1
|
||||
attention_dropout_rate: 0.0
|
||||
input_layer: conv2d
|
||||
normalize_before: true
|
||||
pos_enc_layer_type: rel_pos
|
||||
selfattention_layer_type: rel_selfattn
|
||||
activation_type: swish
|
||||
macaron_style: true
|
||||
use_cnn_module: true
|
||||
cnn_module_kernel: 15
|
||||
|
||||
# decoder
|
||||
decoder: TransformerDecoder
|
||||
decoder_conf:
|
||||
attention_heads: 4
|
||||
linear_units: 2048
|
||||
num_blocks: 6
|
||||
dropout_rate: 0.1
|
||||
positional_dropout_rate: 0.1
|
||||
self_attention_dropout_rate: 0.0
|
||||
src_attention_dropout_rate: 0.0
|
||||
|
||||
|
||||
# frontend related
|
||||
frontend: WavFrontend
|
||||
frontend_conf:
|
||||
fs: 16000
|
||||
window: hamming
|
||||
n_mels: 80
|
||||
frame_length: 25
|
||||
frame_shift: 10
|
||||
dither: 0.0
|
||||
lfr_m: 1
|
||||
lfr_n: 1
|
||||
|
||||
specaug: SpecAug
|
||||
specaug_conf:
|
||||
apply_time_warp: true
|
||||
time_warp_window: 5
|
||||
time_warp_mode: bicubic
|
||||
apply_freq_mask: true
|
||||
freq_mask_width_range:
|
||||
- 0
|
||||
- 30
|
||||
num_freq_mask: 2
|
||||
apply_time_mask: true
|
||||
time_mask_width_range:
|
||||
- 0
|
||||
- 40
|
||||
num_time_mask: 2
|
||||
|
||||
train_conf:
|
||||
accum_grad: 1
|
||||
grad_clip: 5
|
||||
max_epoch: 150
|
||||
val_scheduler_criterion:
|
||||
- valid
|
||||
- acc
|
||||
best_model_criterion:
|
||||
- - valid
|
||||
- acc
|
||||
- max
|
||||
keep_nbest_models: 10
|
||||
log_interval: 50
|
||||
|
||||
optim: adam
|
||||
optim_conf:
|
||||
lr: 0.0005
|
||||
scheduler: warmuplr
|
||||
scheduler_conf:
|
||||
warmup_steps: 30000
|
||||
|
||||
dataset: AudioDataset
|
||||
dataset_conf:
|
||||
index_ds: IndexDSJsonl
|
||||
batch_sampler: BatchSampler
|
||||
batch_type: example # example or length
|
||||
batch_size: 1 # if batch_type is example, batch_size is the numbers of samples; if length, batch_size is source_token_len+target_token_len;
|
||||
max_token_length: 2048 # filter samples if source_token_len+target_token_len > max_token_length,
|
||||
buffer_size: 500
|
||||
shuffle: True
|
||||
num_workers: 0
|
||||
|
||||
tokenizer: CharTokenizer
|
||||
tokenizer_conf:
|
||||
unk_symbol: <unk>
|
||||
split_with_space: true
|
||||
|
||||
|
||||
ctc_conf:
|
||||
dropout_rate: 0.0
|
||||
ctc_type: builtin
|
||||
reduce: true
|
||||
ignore_nan_grad: true
|
||||
normalize: null
|
||||
@@ -0,0 +1,553 @@
|
||||
# Copyright 2019 Shigeki Karita
|
||||
# Apache 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
"""Decoder definition."""
|
||||
from typing import Any
|
||||
from typing import List
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
|
||||
from funasr.models.transformer.attention import MultiHeadedAttention
|
||||
from funasr.models.transformer.utils.dynamic_conv import DynamicConvolution
|
||||
from funasr.models.transformer.utils.dynamic_conv2d import DynamicConvolution2D
|
||||
from funasr.models.transformer.embedding import PositionalEncoding
|
||||
from funasr.models.transformer.layer_norm import LayerNorm
|
||||
from funasr.models.transformer.utils.lightconv import LightweightConvolution
|
||||
from funasr.models.transformer.utils.lightconv2d import LightweightConvolution2D
|
||||
from funasr.models.transformer.utils.mask import subsequent_mask
|
||||
from funasr.models.transformer.utils.nets_utils import make_pad_mask
|
||||
from funasr.models.transformer.positionwise_feed_forward import (
|
||||
PositionwiseFeedForward, # noqa: H301
|
||||
)
|
||||
from funasr.models.transformer.utils.repeat import repeat
|
||||
from funasr.models.transformer.scorers.scorer_interface import BatchScorerInterface
|
||||
from omegaconf import OmegaConf
|
||||
from funasr.register import tables
|
||||
|
||||
|
||||
class LayerNorm(nn.LayerNorm):
|
||||
def forward(self, x):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
return super().forward(x.float()).type(x.dtype)
|
||||
|
||||
|
||||
class DecoderLayer(nn.Module):
|
||||
"""Single decoder layer module.
|
||||
|
||||
Args:
|
||||
size (int): Input dimension.
|
||||
self_attn (torch.nn.Module): Self-attention module instance.
|
||||
`MultiHeadedAttention` instance can be used as the argument.
|
||||
src_attn (torch.nn.Module): Self-attention module instance.
|
||||
`MultiHeadedAttention` instance can be used as the argument.
|
||||
feed_forward (torch.nn.Module): Feed-forward module instance.
|
||||
`PositionwiseFeedForward`, `MultiLayeredConv1d`, or `Conv1dLinear` instance
|
||||
can be used as the argument.
|
||||
dropout_rate (float): Dropout rate.
|
||||
normalize_before (bool): Whether to use layer_norm before the first block.
|
||||
concat_after (bool): Whether to concat attention layer's input and output.
|
||||
if True, additional linear will be applied.
|
||||
i.e. x -> x + linear(concat(x, att(x)))
|
||||
if False, no additional linear will be applied. i.e. x -> x + att(x)
|
||||
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
size,
|
||||
# self_attn,
|
||||
src_attn,
|
||||
feed_forward,
|
||||
dropout_rate,
|
||||
normalize_before=True,
|
||||
concat_after=False,
|
||||
layer_id=None,
|
||||
args={},
|
||||
**kwargs,
|
||||
):
|
||||
"""Construct an DecoderLayer object."""
|
||||
super(DecoderLayer, self).__init__()
|
||||
self.size = size
|
||||
# self.self_attn = self_attn.to(torch.bfloat16)
|
||||
self.src_attn = src_attn
|
||||
self.feed_forward = feed_forward
|
||||
self.norm1 = LayerNorm(size)
|
||||
self.norm2 = LayerNorm(size)
|
||||
self.norm3 = LayerNorm(size)
|
||||
self.dropout = nn.Dropout(dropout_rate)
|
||||
self.normalize_before = normalize_before
|
||||
self.concat_after = concat_after
|
||||
if self.concat_after:
|
||||
self.concat_linear1 = nn.Linear(size + size, size)
|
||||
self.concat_linear2 = nn.Linear(size + size, size)
|
||||
self.layer_id = layer_id
|
||||
|
||||
if args.get("version", "v4") == "v4":
|
||||
from funasr.models.sense_voice.rwkv_v4 import RWKVLayer
|
||||
from funasr.models.sense_voice.rwkv_v4 import RWKV_TimeMix as RWKV_Tmix
|
||||
elif args.get("version", "v5") == "v5":
|
||||
from funasr.models.sense_voice.rwkv_v5 import RWKVLayer
|
||||
from funasr.models.sense_voice.rwkv_v5 import RWKV_Tmix_x052 as RWKV_Tmix
|
||||
else:
|
||||
from funasr.models.sense_voice.rwkv_v6 import RWKVLayer
|
||||
from funasr.models.sense_voice.rwkv_v6 import RWKV_Tmix_x060 as RWKV_Tmix
|
||||
# self.attn = RWKVLayer(args=args, layer_id=layer_id)
|
||||
self.self_attn = RWKV_Tmix(args, layer_id=layer_id)
|
||||
|
||||
self.args = args
|
||||
self.ln0 = None
|
||||
if self.layer_id == 0 and not args.get("ln0", True):
|
||||
self.ln0 = LayerNorm(args.n_embd)
|
||||
if args.get("init_rwkv", True):
|
||||
print("init_rwkv")
|
||||
layer_id = 0
|
||||
scale = ((1 + layer_id) / args.get("n_layer")) ** 0.7
|
||||
nn.init.constant_(self.ln0.weight, scale)
|
||||
|
||||
# init
|
||||
if args.get("init_rwkv", True):
|
||||
print("init_rwkv")
|
||||
scale = ((1 + layer_id) / args.get("n_layer")) ** 0.7
|
||||
nn.init.constant_(self.norm1.weight, scale)
|
||||
# nn.init.constant_(self.self_attn.ln2.weight, scale)
|
||||
|
||||
if args.get("init_rwkv", True):
|
||||
print("init_rwkv")
|
||||
nn.init.orthogonal_(self.self_attn.receptance.weight, gain=1)
|
||||
nn.init.orthogonal_(self.self_attn.key.weight, gain=0.1)
|
||||
nn.init.orthogonal_(self.self_attn.value.weight, gain=1)
|
||||
nn.init.orthogonal_(self.self_attn.gate.weight, gain=0.1)
|
||||
nn.init.zeros_(self.self_attn.output.weight)
|
||||
|
||||
if args.get("datatype", "bf16") == "bf16":
|
||||
self.self_attn.to(torch.bfloat16)
|
||||
# self.norm1.to(torch.bfloat16)
|
||||
|
||||
def forward(self, tgt, tgt_mask, memory, memory_mask, cache=None):
|
||||
"""Compute decoded features.
|
||||
|
||||
Args:
|
||||
tgt (torch.Tensor): Input tensor (#batch, maxlen_out, size).
|
||||
tgt_mask (torch.Tensor): Mask for input tensor (#batch, maxlen_out).
|
||||
memory (torch.Tensor): Encoded memory, float32 (#batch, maxlen_in, size).
|
||||
memory_mask (torch.Tensor): Encoded memory mask (#batch, maxlen_in).
|
||||
cache (List[torch.Tensor]): List of cached tensors.
|
||||
Each tensor shape should be (#batch, maxlen_out - 1, size).
|
||||
|
||||
Returns:
|
||||
torch.Tensor: Output tensor(#batch, maxlen_out, size).
|
||||
torch.Tensor: Mask for output tensor (#batch, maxlen_out).
|
||||
torch.Tensor: Encoded memory (#batch, maxlen_in, size).
|
||||
torch.Tensor: Encoded memory mask (#batch, maxlen_in).
|
||||
|
||||
"""
|
||||
|
||||
if self.layer_id == 0 and self.ln0 is not None:
|
||||
tgt = self.ln0(tgt)
|
||||
|
||||
if self.args.get("datatype", "bf16") == "bf16":
|
||||
tgt = tgt.bfloat16()
|
||||
residual = tgt
|
||||
|
||||
tgt = self.norm1(tgt)
|
||||
|
||||
if cache is None:
|
||||
|
||||
x = residual + self.dropout(self.self_attn(tgt, mask=tgt_mask))
|
||||
else:
|
||||
|
||||
# tgt_q = tgt[:, -1:, :]
|
||||
# residual_q = residual[:, -1:, :]
|
||||
tgt_q_mask = None
|
||||
|
||||
x = residual + self.dropout(self.self_attn(tgt, mask=tgt_q_mask))
|
||||
x = x[:, -1, :]
|
||||
if self.args.get("datatype", "bf16") == "bf16":
|
||||
x = x.to(torch.float32)
|
||||
# x = residual + self.dropout(self.self_attn(tgt_q, tgt, tgt, tgt_q_mask))
|
||||
|
||||
residual = x
|
||||
x = self.norm2(x)
|
||||
x = residual + self.dropout(self.src_attn(x, memory, memory, memory_mask))
|
||||
residual = x
|
||||
x = self.norm3(x)
|
||||
x = residual + self.dropout(self.feed_forward(x))
|
||||
|
||||
if cache is not None:
|
||||
x = torch.cat([cache, x], dim=1)
|
||||
|
||||
return x, tgt_mask, memory, memory_mask
|
||||
|
||||
|
||||
class BaseTransformerDecoder(nn.Module, BatchScorerInterface):
|
||||
"""Base class of Transfomer decoder module.
|
||||
|
||||
Args:
|
||||
vocab_size: output dim
|
||||
encoder_output_size: dimension of attention
|
||||
attention_heads: the number of heads of multi head attention
|
||||
linear_units: the number of units of position-wise feed forward
|
||||
num_blocks: the number of decoder blocks
|
||||
dropout_rate: dropout rate
|
||||
self_attention_dropout_rate: dropout rate for attention
|
||||
input_layer: input layer type
|
||||
use_output_layer: whether to use output layer
|
||||
pos_enc_class: PositionalEncoding or ScaledPositionalEncoding
|
||||
normalize_before: whether to use layer_norm before the first block
|
||||
concat_after: whether to concat attention layer's input and output
|
||||
if True, additional linear will be applied.
|
||||
i.e. x -> x + linear(concat(x, att(x)))
|
||||
if False, no additional linear will be applied.
|
||||
i.e. x -> x + att(x)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
vocab_size: int,
|
||||
encoder_output_size: int,
|
||||
dropout_rate: float = 0.1,
|
||||
positional_dropout_rate: float = 0.1,
|
||||
input_layer: str = "embed",
|
||||
use_output_layer: bool = True,
|
||||
pos_enc_class=PositionalEncoding,
|
||||
normalize_before: bool = True,
|
||||
):
|
||||
"""Initialize BaseTransformerDecoder.
|
||||
|
||||
Args:
|
||||
vocab_size: Size/dimension parameter.
|
||||
encoder_output_size: Size/dimension parameter.
|
||||
dropout_rate: TODO.
|
||||
positional_dropout_rate: TODO.
|
||||
input_layer: TODO.
|
||||
use_output_layer: TODO.
|
||||
pos_enc_class: TODO.
|
||||
normalize_before: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
attention_dim = encoder_output_size
|
||||
|
||||
if input_layer == "embed":
|
||||
self.embed = torch.nn.Sequential(
|
||||
torch.nn.Embedding(vocab_size, attention_dim),
|
||||
pos_enc_class(attention_dim, positional_dropout_rate),
|
||||
)
|
||||
elif input_layer == "linear":
|
||||
self.embed = torch.nn.Sequential(
|
||||
torch.nn.Linear(vocab_size, attention_dim),
|
||||
torch.nn.LayerNorm(attention_dim),
|
||||
torch.nn.Dropout(dropout_rate),
|
||||
torch.nn.ReLU(),
|
||||
pos_enc_class(attention_dim, positional_dropout_rate),
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"only 'embed' or 'linear' is supported: {input_layer}")
|
||||
|
||||
self.normalize_before = normalize_before
|
||||
if self.normalize_before:
|
||||
self.after_norm = LayerNorm(attention_dim)
|
||||
if use_output_layer:
|
||||
self.output_layer = torch.nn.Linear(attention_dim, vocab_size)
|
||||
else:
|
||||
self.output_layer = None
|
||||
|
||||
# Must set by the inheritance
|
||||
self.decoders = None
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hs_pad: torch.Tensor,
|
||||
hlens: torch.Tensor,
|
||||
ys_in_pad: torch.Tensor,
|
||||
ys_in_lens: torch.Tensor,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Forward decoder.
|
||||
|
||||
Args:
|
||||
hs_pad: encoded memory, float32 (batch, maxlen_in, feat)
|
||||
hlens: (batch)
|
||||
ys_in_pad:
|
||||
input token ids, int64 (batch, maxlen_out)
|
||||
if input_layer == "embed"
|
||||
input tensor (batch, maxlen_out, #mels) in the other cases
|
||||
ys_in_lens: (batch)
|
||||
Returns:
|
||||
(tuple): tuple containing:
|
||||
|
||||
x: decoded token score before softmax (batch, maxlen_out, token)
|
||||
if use_output_layer is True,
|
||||
olens: (batch, )
|
||||
"""
|
||||
tgt = ys_in_pad
|
||||
# tgt_mask: (B, 1, L)
|
||||
tgt_mask = (~make_pad_mask(ys_in_lens)[:, None, :]).to(tgt.device)
|
||||
# m: (1, L, L)
|
||||
m = subsequent_mask(tgt_mask.size(-1), device=tgt_mask.device).unsqueeze(0)
|
||||
# tgt_mask: (B, L, L)
|
||||
tgt_mask = tgt_mask & m
|
||||
|
||||
memory = hs_pad
|
||||
memory_mask = (~make_pad_mask(hlens, maxlen=memory.size(1)))[:, None, :].to(memory.device)
|
||||
# Padding for Longformer
|
||||
if memory_mask.shape[-1] != memory.shape[1]:
|
||||
padlen = memory.shape[1] - memory_mask.shape[-1]
|
||||
memory_mask = torch.nn.functional.pad(memory_mask, (0, padlen), "constant", False)
|
||||
|
||||
x = self.embed(tgt)
|
||||
x, tgt_mask, memory, memory_mask = self.decoders(x, tgt_mask, memory, memory_mask)
|
||||
if self.normalize_before:
|
||||
x = self.after_norm(x)
|
||||
if self.output_layer is not None:
|
||||
x = self.output_layer(x)
|
||||
|
||||
olens = tgt_mask.sum(1)
|
||||
return x, olens
|
||||
|
||||
def forward_one_step(
|
||||
self,
|
||||
tgt: torch.Tensor,
|
||||
tgt_mask: torch.Tensor,
|
||||
memory: torch.Tensor,
|
||||
cache: List[torch.Tensor] = None,
|
||||
) -> Tuple[torch.Tensor, List[torch.Tensor]]:
|
||||
"""Forward one step.
|
||||
|
||||
Args:
|
||||
tgt: input token ids, int64 (batch, maxlen_out)
|
||||
tgt_mask: input token mask, (batch, maxlen_out)
|
||||
dtype=torch.uint8 in PyTorch 1.2-
|
||||
dtype=torch.bool in PyTorch 1.2+ (include 1.2)
|
||||
memory: encoded memory, float32 (batch, maxlen_in, feat)
|
||||
cache: cached output list of (batch, max_time_out-1, size)
|
||||
Returns:
|
||||
y, cache: NN output value and cache per `self.decoders`.
|
||||
y.shape` is (batch, maxlen_out, token)
|
||||
"""
|
||||
x = self.embed(tgt)
|
||||
if cache is None:
|
||||
cache = [None] * len(self.decoders)
|
||||
new_cache = []
|
||||
for c, decoder in zip(cache, self.decoders):
|
||||
x, tgt_mask, memory, memory_mask = decoder(x, tgt_mask, memory, None, cache=c)
|
||||
new_cache.append(x)
|
||||
|
||||
if self.normalize_before:
|
||||
y = self.after_norm(x[:, -1])
|
||||
else:
|
||||
y = x[:, -1]
|
||||
if self.output_layer is not None:
|
||||
y = torch.log_softmax(self.output_layer(y), dim=-1)
|
||||
|
||||
return y, new_cache
|
||||
|
||||
def score(self, ys, state, x):
|
||||
"""Score."""
|
||||
ys_mask = subsequent_mask(len(ys), device=x.device).unsqueeze(0)
|
||||
logp, state = self.forward_one_step(ys.unsqueeze(0), ys_mask, x.unsqueeze(0), cache=state)
|
||||
return logp.squeeze(0), state
|
||||
|
||||
def batch_score(
|
||||
self, ys: torch.Tensor, states: List[Any], xs: torch.Tensor
|
||||
) -> Tuple[torch.Tensor, List[Any]]:
|
||||
"""Score new token batch.
|
||||
|
||||
Args:
|
||||
ys (torch.Tensor): torch.int64 prefix tokens (n_batch, ylen).
|
||||
states (List[Any]): Scorer states for prefix tokens.
|
||||
xs (torch.Tensor):
|
||||
The encoder feature that generates ys (n_batch, xlen, n_feat).
|
||||
|
||||
Returns:
|
||||
tuple[torch.Tensor, List[Any]]: Tuple of
|
||||
batchfied scores for next token with shape of `(n_batch, n_vocab)`
|
||||
and next state list for ys.
|
||||
|
||||
"""
|
||||
# merge states
|
||||
n_batch = len(ys)
|
||||
n_layers = len(self.decoders)
|
||||
if states[0] is None:
|
||||
batch_state = None
|
||||
else:
|
||||
# transpose state of [batch, layer] into [layer, batch]
|
||||
batch_state = [
|
||||
torch.stack([states[b][i] for b in range(n_batch)]) for i in range(n_layers)
|
||||
]
|
||||
|
||||
# batch decoding
|
||||
ys_mask = subsequent_mask(ys.size(-1), device=xs.device).unsqueeze(0)
|
||||
logp, states = self.forward_one_step(ys, ys_mask, xs, cache=batch_state)
|
||||
|
||||
# transpose state of [layer, batch] into [batch, layer]
|
||||
state_list = [[states[i][b] for i in range(n_layers)] for b in range(n_batch)]
|
||||
return logp, state_list
|
||||
|
||||
|
||||
@tables.register("decoder_classes", "TransformerRWKVDecoder")
|
||||
class TransformerRWKVDecoder(BaseTransformerDecoder):
|
||||
def __init__(
|
||||
self,
|
||||
vocab_size: int,
|
||||
encoder_output_size: int,
|
||||
attention_heads: int = 4,
|
||||
linear_units: int = 2048,
|
||||
num_blocks: int = 6,
|
||||
dropout_rate: float = 0.1,
|
||||
positional_dropout_rate: float = 0.1,
|
||||
self_attention_dropout_rate: float = 0.0,
|
||||
src_attention_dropout_rate: float = 0.0,
|
||||
input_layer: str = "embed",
|
||||
use_output_layer: bool = True,
|
||||
pos_enc_class=PositionalEncoding,
|
||||
normalize_before: bool = True,
|
||||
concat_after: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize TransformerRWKVDecoder.
|
||||
|
||||
Args:
|
||||
vocab_size: Size/dimension parameter.
|
||||
encoder_output_size: Size/dimension parameter.
|
||||
attention_heads: TODO.
|
||||
linear_units: TODO.
|
||||
num_blocks: TODO.
|
||||
dropout_rate: TODO.
|
||||
positional_dropout_rate: TODO.
|
||||
self_attention_dropout_rate: TODO.
|
||||
src_attention_dropout_rate: TODO.
|
||||
input_layer: TODO.
|
||||
use_output_layer: TODO.
|
||||
pos_enc_class: TODO.
|
||||
normalize_before: TODO.
|
||||
concat_after: TODO.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__(
|
||||
vocab_size=vocab_size,
|
||||
encoder_output_size=encoder_output_size,
|
||||
dropout_rate=dropout_rate,
|
||||
positional_dropout_rate=positional_dropout_rate,
|
||||
input_layer=input_layer,
|
||||
use_output_layer=use_output_layer,
|
||||
pos_enc_class=pos_enc_class,
|
||||
normalize_before=normalize_before,
|
||||
)
|
||||
# from funasr.models.sense_voice.rwkv_v6 import RWKVLayer
|
||||
|
||||
rwkv_cfg = kwargs.get("rwkv_cfg", {})
|
||||
args = OmegaConf.create(rwkv_cfg)
|
||||
|
||||
attention_dim = encoder_output_size
|
||||
self.decoders = repeat(
|
||||
num_blocks,
|
||||
lambda lnum: DecoderLayer(
|
||||
attention_dim,
|
||||
MultiHeadedAttention(attention_heads, attention_dim, src_attention_dropout_rate),
|
||||
PositionwiseFeedForward(attention_dim, linear_units, dropout_rate),
|
||||
dropout_rate,
|
||||
normalize_before,
|
||||
concat_after,
|
||||
lnum,
|
||||
args=args,
|
||||
),
|
||||
)
|
||||
|
||||
# init
|
||||
if args.get("init_rwkv", True):
|
||||
print("init_rwkv")
|
||||
nn.init.uniform_(self.embed[0].weight, a=-1e-4, b=1e-4)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hs_pad: torch.Tensor,
|
||||
hlens: torch.Tensor,
|
||||
ys_in_pad: torch.Tensor,
|
||||
ys_in_lens: torch.Tensor,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Forward decoder.
|
||||
|
||||
Args:
|
||||
hs_pad: encoded memory, float32 (batch, maxlen_in, feat)
|
||||
hlens: (batch)
|
||||
ys_in_pad:
|
||||
input token ids, int64 (batch, maxlen_out)
|
||||
if input_layer == "embed"
|
||||
input tensor (batch, maxlen_out, #mels) in the other cases
|
||||
ys_in_lens: (batch)
|
||||
Returns:
|
||||
(tuple): tuple containing:
|
||||
|
||||
x: decoded token score before softmax (batch, maxlen_out, token)
|
||||
if use_output_layer is True,
|
||||
olens: (batch, )
|
||||
"""
|
||||
tgt = ys_in_pad
|
||||
# tgt_mask: (B, 1, L)
|
||||
tgt_mask = (~make_pad_mask(ys_in_lens)[:, None, :]).to(tgt.device)
|
||||
# m: (1, L, L)
|
||||
m = subsequent_mask(tgt_mask.size(-1), device=tgt_mask.device).unsqueeze(0)
|
||||
# tgt_mask: (B, L, L)
|
||||
tgt_mask = tgt_mask & m
|
||||
|
||||
memory = hs_pad
|
||||
memory_mask = (~make_pad_mask(hlens, maxlen=memory.size(1)))[:, None, :].to(memory.device)
|
||||
# Padding for Longformer
|
||||
if memory_mask.shape[-1] != memory.shape[1]:
|
||||
padlen = memory.shape[1] - memory_mask.shape[-1]
|
||||
memory_mask = torch.nn.functional.pad(memory_mask, (0, padlen), "constant", False)
|
||||
|
||||
x = self.embed(tgt)
|
||||
x, tgt_mask, memory, memory_mask = self.decoders(x, tgt_mask, memory, memory_mask)
|
||||
if self.normalize_before:
|
||||
x = self.after_norm(x)
|
||||
if self.output_layer is not None:
|
||||
x = self.output_layer(x)
|
||||
|
||||
olens = tgt_mask.sum(1)
|
||||
return x, olens
|
||||
|
||||
def forward_one_step(
|
||||
self,
|
||||
tgt: torch.Tensor,
|
||||
tgt_mask: torch.Tensor,
|
||||
memory: torch.Tensor,
|
||||
cache: List[torch.Tensor] = None,
|
||||
) -> Tuple[torch.Tensor, List[torch.Tensor]]:
|
||||
"""Forward one step.
|
||||
|
||||
Args:
|
||||
tgt: input token ids, int64 (batch, maxlen_out)
|
||||
tgt_mask: input token mask, (batch, maxlen_out)
|
||||
dtype=torch.uint8 in PyTorch 1.2-
|
||||
dtype=torch.bool in PyTorch 1.2+ (include 1.2)
|
||||
memory: encoded memory, float32 (batch, maxlen_in, feat)
|
||||
cache: cached output list of (batch, max_time_out-1, size)
|
||||
Returns:
|
||||
y, cache: NN output value and cache per `self.decoders`.
|
||||
y.shape` is (batch, maxlen_out, token)
|
||||
"""
|
||||
x = self.embed(tgt)
|
||||
if cache is None:
|
||||
cache = [None] * len(self.decoders)
|
||||
new_cache = []
|
||||
for c, decoder in zip(cache, self.decoders):
|
||||
x, tgt_mask, memory, memory_mask = decoder(x, tgt_mask, memory, None, cache=c)
|
||||
new_cache.append(x)
|
||||
|
||||
if self.normalize_before:
|
||||
y = self.after_norm(x[:, -1])
|
||||
else:
|
||||
y = x[:, -1]
|
||||
if self.output_layer is not None:
|
||||
y = torch.log_softmax(self.output_layer(y), dim=-1)
|
||||
|
||||
return y, new_cache
|
||||
@@ -0,0 +1,25 @@
|
||||
import logging
|
||||
|
||||
import torch
|
||||
|
||||
from funasr.models.transformer.model import Transformer
|
||||
from funasr.register import tables
|
||||
|
||||
|
||||
@tables.register("model_classes", "Conformer")
|
||||
class Conformer(Transformer):
|
||||
"""CTC-attention hybrid Encoder-Decoder model"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
|
||||
"""Initialize Conformer.
|
||||
|
||||
Args:
|
||||
*args: Variable positional arguments.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -0,0 +1,123 @@
|
||||
# This is an example that demonstrates how to configure a model file.
|
||||
# You can modify the configuration according to your own requirements.
|
||||
|
||||
# to print the register_table:
|
||||
# from funasr.register import tables
|
||||
# tables.print()
|
||||
|
||||
# network architecture
|
||||
model: Conformer
|
||||
model_conf:
|
||||
ctc_weight: 0.3
|
||||
lsm_weight: 0.1 # label smoothing option
|
||||
length_normalized_loss: false
|
||||
|
||||
# encoder
|
||||
encoder: ConformerEncoder
|
||||
encoder_conf:
|
||||
output_size: 256 # dimension of attention
|
||||
attention_heads: 4
|
||||
linear_units: 2048 # the number of units of position-wise feed forward
|
||||
num_blocks: 12 # the number of encoder blocks
|
||||
dropout_rate: 0.1
|
||||
positional_dropout_rate: 0.1
|
||||
attention_dropout_rate: 0.0
|
||||
input_layer: conv2d # encoder architecture type
|
||||
normalize_before: true
|
||||
pos_enc_layer_type: rel_pos
|
||||
selfattention_layer_type: rel_selfattn
|
||||
activation_type: swish
|
||||
macaron_style: true
|
||||
use_cnn_module: true
|
||||
cnn_module_kernel: 15
|
||||
|
||||
# decoder
|
||||
decoder: TransformerRWKVDecoder
|
||||
decoder_conf:
|
||||
attention_heads: 4
|
||||
linear_units: 2048
|
||||
num_blocks: 6
|
||||
dropout_rate: 0.1
|
||||
positional_dropout_rate: 0.1
|
||||
self_attention_dropout_rate: 0.0
|
||||
src_attention_dropout_rate: 0.0
|
||||
input_layer: embed
|
||||
rwkv_cfg:
|
||||
n_embd: 256
|
||||
dropout: 0
|
||||
head_size_a: 64
|
||||
ctx_len: 512
|
||||
dim_att: 256 #${model_conf.rwkv_cfg.n_embd}
|
||||
dim_ffn: null
|
||||
head_size_divisor: 4
|
||||
n_layer: 6
|
||||
pre_ffn: 0
|
||||
ln0: false
|
||||
ln1: false
|
||||
|
||||
|
||||
# frontend related
|
||||
frontend: WavFrontend
|
||||
frontend_conf:
|
||||
fs: 16000
|
||||
window: hamming
|
||||
n_mels: 80
|
||||
frame_length: 25
|
||||
frame_shift: 10
|
||||
lfr_m: 1
|
||||
lfr_n: 1
|
||||
|
||||
specaug: SpecAug
|
||||
specaug_conf:
|
||||
apply_time_warp: true
|
||||
time_warp_window: 5
|
||||
time_warp_mode: bicubic
|
||||
apply_freq_mask: true
|
||||
freq_mask_width_range:
|
||||
- 0
|
||||
- 30
|
||||
num_freq_mask: 2
|
||||
apply_time_mask: true
|
||||
time_mask_width_range:
|
||||
- 0
|
||||
- 40
|
||||
num_time_mask: 2
|
||||
|
||||
train_conf:
|
||||
accum_grad: 1
|
||||
grad_clip: 5
|
||||
max_epoch: 150
|
||||
keep_nbest_models: 10
|
||||
log_interval: 50
|
||||
|
||||
optim: adam
|
||||
optim_conf:
|
||||
lr: 0.0005
|
||||
scheduler: warmuplr
|
||||
scheduler_conf:
|
||||
warmup_steps: 30000
|
||||
|
||||
dataset: AudioDataset
|
||||
dataset_conf:
|
||||
index_ds: IndexDSJsonl
|
||||
batch_sampler: EspnetStyleBatchSampler
|
||||
batch_type: length # example or length
|
||||
batch_size: 25000 # if batch_type is example, batch_size is the numbers of samples; if length, batch_size is source_token_len+target_token_len;
|
||||
max_token_length: 2048 # filter samples if source_token_len+target_token_len > max_token_length,
|
||||
buffer_size: 1024
|
||||
shuffle: True
|
||||
num_workers: 4
|
||||
preprocessor_speech: SpeechPreprocessSpeedPerturb
|
||||
preprocessor_speech_conf:
|
||||
speed_perturb: [0.9, 1.0, 1.1]
|
||||
|
||||
tokenizer: CharTokenizer
|
||||
tokenizer_conf:
|
||||
unk_symbol: <unk>
|
||||
|
||||
ctc_conf:
|
||||
dropout_rate: 0.0
|
||||
ctc_type: builtin
|
||||
reduce: true
|
||||
ignore_nan_grad: true
|
||||
normalize: null
|
||||
@@ -0,0 +1,507 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
import torch
|
||||
import logging
|
||||
import numpy as np
|
||||
from typing import Tuple
|
||||
|
||||
from funasr.register import tables
|
||||
from funasr.models.scama import utils as myutils
|
||||
from funasr.models.transformer.utils.repeat import repeat
|
||||
from funasr.models.transformer.layer_norm import LayerNorm
|
||||
from funasr.models.transformer.embedding import PositionalEncoding
|
||||
from funasr.models.paraformer.decoder import DecoderLayerSANM, ParaformerSANMDecoder
|
||||
from funasr.models.sanm.positionwise_feed_forward import PositionwiseFeedForwardDecoderSANM
|
||||
from funasr.models.sanm.attention import (
|
||||
MultiHeadedAttentionSANMDecoder,
|
||||
MultiHeadedAttentionCrossAtt,
|
||||
)
|
||||
|
||||
|
||||
class ContextualDecoderLayer(torch.nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
size,
|
||||
self_attn,
|
||||
src_attn,
|
||||
feed_forward,
|
||||
dropout_rate,
|
||||
normalize_before=True,
|
||||
concat_after=False,
|
||||
):
|
||||
"""Construct an DecoderLayer object."""
|
||||
super(ContextualDecoderLayer, self).__init__()
|
||||
self.size = size
|
||||
self.self_attn = self_attn
|
||||
self.src_attn = src_attn
|
||||
self.feed_forward = feed_forward
|
||||
self.norm1 = LayerNorm(size)
|
||||
if self_attn is not None:
|
||||
self.norm2 = LayerNorm(size)
|
||||
if src_attn is not None:
|
||||
self.norm3 = LayerNorm(size)
|
||||
self.dropout = torch.nn.Dropout(dropout_rate)
|
||||
self.normalize_before = normalize_before
|
||||
self.concat_after = concat_after
|
||||
if self.concat_after:
|
||||
self.concat_linear1 = torch.nn.Linear(size + size, size)
|
||||
self.concat_linear2 = torch.nn.Linear(size + size, size)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
tgt,
|
||||
tgt_mask,
|
||||
memory,
|
||||
memory_mask,
|
||||
cache=None,
|
||||
):
|
||||
# tgt = self.dropout(tgt)
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
tgt: TODO.
|
||||
tgt_mask: TODO.
|
||||
memory: TODO.
|
||||
memory_mask: TODO.
|
||||
cache: State cache dict for streaming inference.
|
||||
"""
|
||||
if isinstance(tgt, Tuple):
|
||||
tgt, _ = tgt
|
||||
residual = tgt
|
||||
if self.normalize_before:
|
||||
tgt = self.norm1(tgt)
|
||||
tgt = self.feed_forward(tgt)
|
||||
|
||||
x = tgt
|
||||
if self.normalize_before:
|
||||
tgt = self.norm2(tgt)
|
||||
if self.training:
|
||||
cache = None
|
||||
x, cache = self.self_attn(tgt, tgt_mask, cache=cache)
|
||||
x = residual + self.dropout(x)
|
||||
x_self_attn = x
|
||||
|
||||
residual = x
|
||||
if self.normalize_before:
|
||||
x = self.norm3(x)
|
||||
x = self.src_attn(x, memory, memory_mask)
|
||||
x_src_attn = x
|
||||
|
||||
x = residual + self.dropout(x)
|
||||
return x, tgt_mask, x_self_attn, x_src_attn
|
||||
|
||||
|
||||
class ContextualBiasDecoder(torch.nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
size,
|
||||
src_attn,
|
||||
dropout_rate,
|
||||
normalize_before=True,
|
||||
):
|
||||
"""Construct an DecoderLayer object."""
|
||||
super(ContextualBiasDecoder, self).__init__()
|
||||
self.size = size
|
||||
self.src_attn = src_attn
|
||||
if src_attn is not None:
|
||||
self.norm3 = LayerNorm(size)
|
||||
self.dropout = torch.nn.Dropout(dropout_rate)
|
||||
self.normalize_before = normalize_before
|
||||
|
||||
def forward(self, tgt, tgt_mask, memory, memory_mask=None, cache=None):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
tgt: TODO.
|
||||
tgt_mask: TODO.
|
||||
memory: TODO.
|
||||
memory_mask: TODO.
|
||||
cache: State cache dict for streaming inference.
|
||||
"""
|
||||
x = tgt
|
||||
if self.src_attn is not None:
|
||||
if self.normalize_before:
|
||||
x = self.norm3(x)
|
||||
x = self.dropout(self.src_attn(x, memory, memory_mask))
|
||||
return x, tgt_mask, memory, memory_mask, cache
|
||||
|
||||
|
||||
@tables.register("decoder_classes", "ContextualParaformerDecoder")
|
||||
class ContextualParaformerDecoder(ParaformerSANMDecoder):
|
||||
"""
|
||||
Author: Speech Lab of DAMO Academy, Alibaba Group
|
||||
Paraformer: Fast and Accurate Parallel Transformer for Non-autoregressive End-to-End Speech Recognition
|
||||
https://arxiv.org/abs/2006.01713
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
vocab_size: int,
|
||||
encoder_output_size: int,
|
||||
attention_heads: int = 4,
|
||||
linear_units: int = 2048,
|
||||
num_blocks: int = 6,
|
||||
dropout_rate: float = 0.1,
|
||||
positional_dropout_rate: float = 0.1,
|
||||
self_attention_dropout_rate: float = 0.0,
|
||||
src_attention_dropout_rate: float = 0.0,
|
||||
input_layer: str = "embed",
|
||||
use_output_layer: bool = True,
|
||||
pos_enc_class=PositionalEncoding,
|
||||
normalize_before: bool = True,
|
||||
concat_after: bool = False,
|
||||
att_layer_num: int = 6,
|
||||
kernel_size: int = 21,
|
||||
sanm_shfit: int = 0,
|
||||
):
|
||||
"""Initialize ContextualParaformerDecoder.
|
||||
|
||||
Args:
|
||||
vocab_size: Size/dimension parameter.
|
||||
encoder_output_size: Size/dimension parameter.
|
||||
attention_heads: TODO.
|
||||
linear_units: TODO.
|
||||
num_blocks: TODO.
|
||||
dropout_rate: TODO.
|
||||
positional_dropout_rate: TODO.
|
||||
self_attention_dropout_rate: TODO.
|
||||
src_attention_dropout_rate: TODO.
|
||||
input_layer: TODO.
|
||||
use_output_layer: TODO.
|
||||
pos_enc_class: TODO.
|
||||
normalize_before: TODO.
|
||||
concat_after: TODO.
|
||||
att_layer_num: TODO.
|
||||
kernel_size: Size/dimension parameter.
|
||||
sanm_shfit: TODO.
|
||||
"""
|
||||
super().__init__(
|
||||
vocab_size=vocab_size,
|
||||
encoder_output_size=encoder_output_size,
|
||||
dropout_rate=dropout_rate,
|
||||
positional_dropout_rate=positional_dropout_rate,
|
||||
input_layer=input_layer,
|
||||
use_output_layer=use_output_layer,
|
||||
pos_enc_class=pos_enc_class,
|
||||
normalize_before=normalize_before,
|
||||
)
|
||||
|
||||
attention_dim = encoder_output_size
|
||||
if input_layer == "none":
|
||||
self.embed = None
|
||||
if input_layer == "embed":
|
||||
self.embed = torch.nn.Sequential(
|
||||
torch.nn.Embedding(vocab_size, attention_dim),
|
||||
# pos_enc_class(attention_dim, positional_dropout_rate),
|
||||
)
|
||||
elif input_layer == "linear":
|
||||
self.embed = torch.nn.Sequential(
|
||||
torch.nn.Linear(vocab_size, attention_dim),
|
||||
torch.nn.LayerNorm(attention_dim),
|
||||
torch.nn.Dropout(dropout_rate),
|
||||
torch.nn.ReLU(),
|
||||
pos_enc_class(attention_dim, positional_dropout_rate),
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"only 'embed' or 'linear' is supported: {input_layer}")
|
||||
|
||||
self.normalize_before = normalize_before
|
||||
if self.normalize_before:
|
||||
self.after_norm = LayerNorm(attention_dim)
|
||||
if use_output_layer:
|
||||
self.output_layer = torch.nn.Linear(attention_dim, vocab_size)
|
||||
else:
|
||||
self.output_layer = None
|
||||
|
||||
self.att_layer_num = att_layer_num
|
||||
self.num_blocks = num_blocks
|
||||
if sanm_shfit is None:
|
||||
sanm_shfit = (kernel_size - 1) // 2
|
||||
self.decoders = repeat(
|
||||
att_layer_num - 1,
|
||||
lambda lnum: DecoderLayerSANM(
|
||||
attention_dim,
|
||||
MultiHeadedAttentionSANMDecoder(
|
||||
attention_dim, self_attention_dropout_rate, kernel_size, sanm_shfit=sanm_shfit
|
||||
),
|
||||
MultiHeadedAttentionCrossAtt(
|
||||
attention_heads, attention_dim, src_attention_dropout_rate
|
||||
),
|
||||
PositionwiseFeedForwardDecoderSANM(attention_dim, linear_units, dropout_rate),
|
||||
dropout_rate,
|
||||
normalize_before,
|
||||
concat_after,
|
||||
),
|
||||
)
|
||||
self.dropout = torch.nn.Dropout(dropout_rate)
|
||||
self.bias_decoder = ContextualBiasDecoder(
|
||||
size=attention_dim,
|
||||
src_attn=MultiHeadedAttentionCrossAtt(
|
||||
attention_heads, attention_dim, src_attention_dropout_rate
|
||||
),
|
||||
dropout_rate=dropout_rate,
|
||||
normalize_before=True,
|
||||
)
|
||||
self.bias_output = torch.nn.Conv1d(attention_dim * 2, attention_dim, 1, bias=False)
|
||||
self.last_decoder = ContextualDecoderLayer(
|
||||
attention_dim,
|
||||
MultiHeadedAttentionSANMDecoder(
|
||||
attention_dim, self_attention_dropout_rate, kernel_size, sanm_shfit=sanm_shfit
|
||||
),
|
||||
MultiHeadedAttentionCrossAtt(
|
||||
attention_heads, attention_dim, src_attention_dropout_rate
|
||||
),
|
||||
PositionwiseFeedForwardDecoderSANM(attention_dim, linear_units, dropout_rate),
|
||||
dropout_rate,
|
||||
normalize_before,
|
||||
concat_after,
|
||||
)
|
||||
if num_blocks - att_layer_num <= 0:
|
||||
self.decoders2 = None
|
||||
else:
|
||||
self.decoders2 = repeat(
|
||||
num_blocks - att_layer_num,
|
||||
lambda lnum: DecoderLayerSANM(
|
||||
attention_dim,
|
||||
MultiHeadedAttentionSANMDecoder(
|
||||
attention_dim, self_attention_dropout_rate, kernel_size, sanm_shfit=0
|
||||
),
|
||||
None,
|
||||
PositionwiseFeedForwardDecoderSANM(attention_dim, linear_units, dropout_rate),
|
||||
dropout_rate,
|
||||
normalize_before,
|
||||
concat_after,
|
||||
),
|
||||
)
|
||||
|
||||
self.decoders3 = repeat(
|
||||
1,
|
||||
lambda lnum: DecoderLayerSANM(
|
||||
attention_dim,
|
||||
None,
|
||||
None,
|
||||
PositionwiseFeedForwardDecoderSANM(attention_dim, linear_units, dropout_rate),
|
||||
dropout_rate,
|
||||
normalize_before,
|
||||
concat_after,
|
||||
),
|
||||
)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hs_pad: torch.Tensor,
|
||||
hlens: torch.Tensor,
|
||||
ys_in_pad: torch.Tensor,
|
||||
ys_in_lens: torch.Tensor,
|
||||
contextual_info: torch.Tensor,
|
||||
clas_scale: float = 1.0,
|
||||
return_hidden: bool = False,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Forward decoder.
|
||||
|
||||
Args:
|
||||
hs_pad: encoded memory, float32 (batch, maxlen_in, feat)
|
||||
hlens: (batch)
|
||||
ys_in_pad:
|
||||
input token ids, int64 (batch, maxlen_out)
|
||||
if input_layer == "embed"
|
||||
input tensor (batch, maxlen_out, #mels) in the other cases
|
||||
ys_in_lens: (batch)
|
||||
Returns:
|
||||
(tuple): tuple containing:
|
||||
|
||||
x: decoded token score before softmax (batch, maxlen_out, token)
|
||||
if use_output_layer is True,
|
||||
olens: (batch, )
|
||||
"""
|
||||
tgt = ys_in_pad
|
||||
tgt_mask = myutils.sequence_mask(ys_in_lens, device=tgt.device)[:, :, None]
|
||||
|
||||
memory = hs_pad
|
||||
memory_mask = myutils.sequence_mask(hlens, device=memory.device)[:, None, :]
|
||||
|
||||
x = tgt
|
||||
x, tgt_mask, memory, memory_mask, _ = self.decoders(x, tgt_mask, memory, memory_mask)
|
||||
_, _, x_self_attn, x_src_attn = self.last_decoder(x, tgt_mask, memory, memory_mask)
|
||||
|
||||
# contextual paraformer related
|
||||
contextual_length = torch.Tensor([contextual_info.shape[1]]).int().repeat(hs_pad.shape[0])
|
||||
contextual_mask = myutils.sequence_mask(contextual_length, device=memory.device)[:, None, :]
|
||||
cx, tgt_mask, _, _, _ = self.bias_decoder(
|
||||
x_self_attn, tgt_mask, contextual_info, memory_mask=contextual_mask
|
||||
)
|
||||
|
||||
if self.bias_output is not None:
|
||||
x = torch.cat([x_src_attn, cx * clas_scale], dim=2)
|
||||
x = self.bias_output(x.transpose(1, 2)).transpose(1, 2) # 2D -> D
|
||||
x = x_self_attn + self.dropout(x)
|
||||
|
||||
if self.decoders2 is not None:
|
||||
x, tgt_mask, memory, memory_mask, _ = self.decoders2(x, tgt_mask, memory, memory_mask)
|
||||
|
||||
x, tgt_mask, memory, memory_mask, _ = self.decoders3(x, tgt_mask, memory, memory_mask)
|
||||
if self.normalize_before:
|
||||
x = self.after_norm(x)
|
||||
olens = tgt_mask.sum(1)
|
||||
if self.output_layer is not None and return_hidden is False:
|
||||
x = self.output_layer(x)
|
||||
return x, olens
|
||||
|
||||
|
||||
@tables.register("decoder_classes", "ContextualParaformerDecoderExport")
|
||||
class ContextualParaformerDecoderExport(torch.nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
model,
|
||||
max_seq_len=512,
|
||||
model_name="decoder",
|
||||
onnx: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize ContextualParaformerDecoderExport.
|
||||
|
||||
Args:
|
||||
model: Model instance or model name.
|
||||
max_seq_len: TODO.
|
||||
model_name: TODO.
|
||||
onnx: TODO.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__()
|
||||
from funasr.utils.torch_function import sequence_mask
|
||||
|
||||
self.model = model
|
||||
self.make_pad_mask = sequence_mask(max_seq_len, flip=False)
|
||||
|
||||
from funasr.models.sanm.attention import MultiHeadedAttentionSANMDecoderExport
|
||||
from funasr.models.sanm.attention import MultiHeadedAttentionCrossAttExport
|
||||
from funasr.models.paraformer.decoder import DecoderLayerSANMExport
|
||||
from funasr.models.transformer.positionwise_feed_forward import (
|
||||
PositionwiseFeedForwardDecoderSANMExport,
|
||||
)
|
||||
|
||||
for i, d in enumerate(self.model.decoders):
|
||||
if isinstance(d.feed_forward, PositionwiseFeedForwardDecoderSANM):
|
||||
d.feed_forward = PositionwiseFeedForwardDecoderSANMExport(d.feed_forward)
|
||||
if isinstance(d.self_attn, MultiHeadedAttentionSANMDecoder):
|
||||
d.self_attn = MultiHeadedAttentionSANMDecoderExport(d.self_attn)
|
||||
if isinstance(d.src_attn, MultiHeadedAttentionCrossAtt):
|
||||
d.src_attn = MultiHeadedAttentionCrossAttExport(d.src_attn)
|
||||
self.model.decoders[i] = DecoderLayerSANMExport(d)
|
||||
|
||||
if self.model.decoders2 is not None:
|
||||
for i, d in enumerate(self.model.decoders2):
|
||||
if isinstance(d.feed_forward, PositionwiseFeedForwardDecoderSANM):
|
||||
d.feed_forward = PositionwiseFeedForwardDecoderSANMExport(d.feed_forward)
|
||||
if isinstance(d.self_attn, MultiHeadedAttentionSANMDecoder):
|
||||
d.self_attn = MultiHeadedAttentionSANMDecoderExport(d.self_attn)
|
||||
self.model.decoders2[i] = DecoderLayerSANMExport(d)
|
||||
|
||||
for i, d in enumerate(self.model.decoders3):
|
||||
if isinstance(d.feed_forward, PositionwiseFeedForwardDecoderSANM):
|
||||
d.feed_forward = PositionwiseFeedForwardDecoderSANMExport(d.feed_forward)
|
||||
self.model.decoders3[i] = DecoderLayerSANMExport(d)
|
||||
|
||||
self.output_layer = model.output_layer
|
||||
self.after_norm = model.after_norm
|
||||
self.model_name = model_name
|
||||
|
||||
# bias decoder
|
||||
if isinstance(self.model.bias_decoder.src_attn, MultiHeadedAttentionCrossAtt):
|
||||
self.model.bias_decoder.src_attn = MultiHeadedAttentionCrossAttExport(
|
||||
self.model.bias_decoder.src_attn
|
||||
)
|
||||
self.bias_decoder = self.model.bias_decoder
|
||||
|
||||
# last decoder
|
||||
if isinstance(self.model.last_decoder.src_attn, MultiHeadedAttentionCrossAtt):
|
||||
self.model.last_decoder.src_attn = MultiHeadedAttentionCrossAttExport(
|
||||
self.model.last_decoder.src_attn
|
||||
)
|
||||
if isinstance(self.model.last_decoder.self_attn, MultiHeadedAttentionSANMDecoder):
|
||||
self.model.last_decoder.self_attn = MultiHeadedAttentionSANMDecoderExport(
|
||||
self.model.last_decoder.self_attn
|
||||
)
|
||||
if isinstance(self.model.last_decoder.feed_forward, PositionwiseFeedForwardDecoderSANM):
|
||||
self.model.last_decoder.feed_forward = PositionwiseFeedForwardDecoderSANMExport(
|
||||
self.model.last_decoder.feed_forward
|
||||
)
|
||||
self.last_decoder = self.model.last_decoder
|
||||
self.bias_output = self.model.bias_output
|
||||
self.dropout = self.model.dropout
|
||||
|
||||
def prepare_mask(self, mask):
|
||||
"""Prepare mask.
|
||||
|
||||
Args:
|
||||
mask: TODO.
|
||||
"""
|
||||
mask_3d_btd = mask[:, :, None]
|
||||
if len(mask.shape) == 2:
|
||||
mask_4d_bhlt = 1 - mask[:, None, None, :]
|
||||
elif len(mask.shape) == 3:
|
||||
mask_4d_bhlt = 1 - mask[:, None, :]
|
||||
mask_4d_bhlt = mask_4d_bhlt * -10000.0
|
||||
|
||||
return mask_3d_btd, mask_4d_bhlt
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hs_pad: torch.Tensor,
|
||||
hlens: torch.Tensor,
|
||||
ys_in_pad: torch.Tensor,
|
||||
ys_in_lens: torch.Tensor,
|
||||
bias_embed: torch.Tensor,
|
||||
):
|
||||
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
hs_pad: TODO.
|
||||
hlens: TODO.
|
||||
ys_in_pad: TODO.
|
||||
ys_in_lens: Lengths of ys_in.
|
||||
bias_embed: TODO.
|
||||
"""
|
||||
tgt = ys_in_pad
|
||||
tgt_mask = self.make_pad_mask(ys_in_lens)
|
||||
tgt_mask, _ = self.prepare_mask(tgt_mask)
|
||||
# tgt_mask = myutils.sequence_mask(ys_in_lens, device=tgt.device)[:, :, None]
|
||||
|
||||
memory = hs_pad
|
||||
memory_mask = self.make_pad_mask(hlens)
|
||||
_, memory_mask = self.prepare_mask(memory_mask)
|
||||
# memory_mask = myutils.sequence_mask(hlens, device=memory.device)[:, None, :]
|
||||
|
||||
x = tgt
|
||||
x, tgt_mask, memory, memory_mask, _ = self.model.decoders(x, tgt_mask, memory, memory_mask)
|
||||
|
||||
_, _, x_self_attn, x_src_attn = self.last_decoder(x, tgt_mask, memory, memory_mask)
|
||||
|
||||
# contextual paraformer related
|
||||
contextual_length = torch.Tensor([bias_embed.shape[1]]).int().repeat(hs_pad.shape[0])
|
||||
# contextual_mask = myutils.sequence_mask(contextual_length, device=memory.device)[:, None, :]
|
||||
contextual_mask = self.make_pad_mask(contextual_length)
|
||||
contextual_mask, _ = self.prepare_mask(contextual_mask)
|
||||
contextual_mask = contextual_mask.transpose(2, 1).unsqueeze(1)
|
||||
cx, tgt_mask, _, _, _ = self.bias_decoder(
|
||||
x_self_attn, tgt_mask, bias_embed, memory_mask=contextual_mask
|
||||
)
|
||||
|
||||
if self.bias_output is not None:
|
||||
x = torch.cat([x_src_attn, cx], dim=2)
|
||||
x = self.bias_output(x.transpose(1, 2)).transpose(1, 2) # 2D -> D
|
||||
x = x_self_attn + self.dropout(x)
|
||||
|
||||
if self.model.decoders2 is not None:
|
||||
x, tgt_mask, memory, memory_mask, _ = self.model.decoders2(
|
||||
x, tgt_mask, memory, memory_mask
|
||||
)
|
||||
x, tgt_mask, memory, memory_mask, _ = self.model.decoders3(x, tgt_mask, memory, memory_mask)
|
||||
x = self.after_norm(x)
|
||||
x = self.output_layer(x)
|
||||
|
||||
return x, ys_in_lens
|
||||
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
import torch
|
||||
import types
|
||||
|
||||
from funasr.register import tables
|
||||
from funasr.models.seaco_paraformer.export_meta import ContextualEmbedderExport
|
||||
|
||||
|
||||
class ContextualEmbedderExport2(ContextualEmbedderExport):
|
||||
def __init__(self, model, **kwargs):
|
||||
"""Initialize ContextualEmbedderExport2.
|
||||
|
||||
Args:
|
||||
model: Model instance or model name.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__(model)
|
||||
self.embedding = model.bias_embed
|
||||
model.bias_encoder.batch_first = False
|
||||
self.bias_encoder = model.bias_encoder
|
||||
|
||||
def export_dummy_inputs(self):
|
||||
"""Export dummy inputs."""
|
||||
hotword = torch.tensor(
|
||||
[
|
||||
[10, 11, 12, 13, 14, 10, 11, 12, 13, 14],
|
||||
[100, 101, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[10, 11, 12, 13, 14, 10, 11, 12, 13, 14],
|
||||
[100, 101, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
],
|
||||
dtype=torch.int32,
|
||||
)
|
||||
# hotword_length = torch.tensor([10, 2, 1], dtype=torch.int32)
|
||||
return (hotword)
|
||||
|
||||
|
||||
def export_rebuild_model(model, **kwargs):
|
||||
"""Export rebuild model.
|
||||
|
||||
Args:
|
||||
model: Model instance or model name.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
is_onnx = kwargs.get("type", "onnx") == "onnx"
|
||||
|
||||
encoder_class = tables.encoder_classes.get(kwargs["encoder"] + "Export")
|
||||
model.encoder = encoder_class(model.encoder, onnx=is_onnx)
|
||||
|
||||
predictor_class = tables.predictor_classes.get(kwargs["predictor"] + "Export")
|
||||
model.predictor = predictor_class(model.predictor, onnx=is_onnx)
|
||||
|
||||
# little difference with bias encoder with seaco paraformer
|
||||
embedder_class = ContextualEmbedderExport2
|
||||
embedder_model = embedder_class(model, onnx=is_onnx)
|
||||
|
||||
if kwargs["decoder"] == "ParaformerSANMDecoder":
|
||||
kwargs["decoder"] = "ParaformerSANMDecoderOnline"
|
||||
decoder_class = tables.decoder_classes.get(kwargs["decoder"] + "Export")
|
||||
model.decoder = decoder_class(model.decoder, onnx=is_onnx)
|
||||
|
||||
from funasr.utils.torch_function import sequence_mask
|
||||
|
||||
model.make_pad_mask = sequence_mask(kwargs["max_seq_len"], flip=False)
|
||||
model.feats_dim = 560
|
||||
|
||||
import copy
|
||||
|
||||
backbone_model = copy.copy(model)
|
||||
|
||||
# backbone
|
||||
backbone_model.forward = types.MethodType(export_backbone_forward, backbone_model)
|
||||
backbone_model.export_dummy_inputs = types.MethodType(
|
||||
export_backbone_dummy_inputs, backbone_model
|
||||
)
|
||||
backbone_model.export_input_names = types.MethodType(
|
||||
export_backbone_input_names, backbone_model
|
||||
)
|
||||
backbone_model.export_output_names = types.MethodType(
|
||||
export_backbone_output_names, backbone_model
|
||||
)
|
||||
backbone_model.export_dynamic_axes = types.MethodType(
|
||||
export_backbone_dynamic_axes, backbone_model
|
||||
)
|
||||
|
||||
embedder_model.export_name = "model_eb"
|
||||
backbone_model.export_name = "model"
|
||||
|
||||
return backbone_model, embedder_model
|
||||
|
||||
|
||||
def export_backbone_forward(
|
||||
self,
|
||||
speech: torch.Tensor,
|
||||
speech_lengths: torch.Tensor,
|
||||
bias_embed: torch.Tensor,
|
||||
):
|
||||
"""Export backbone forward.
|
||||
|
||||
Args:
|
||||
speech: Speech audio tensor, shape (batch, time).
|
||||
speech_lengths: Length of each speech sample.
|
||||
bias_embed: TODO.
|
||||
"""
|
||||
batch = {"speech": speech, "speech_lengths": speech_lengths}
|
||||
|
||||
enc, enc_len = self.encoder(**batch)
|
||||
mask = self.make_pad_mask(enc_len)[:, None, :]
|
||||
pre_acoustic_embeds, pre_token_length, _, _ = self.predictor(enc, mask)
|
||||
pre_token_length = pre_token_length.floor().type(torch.int32)
|
||||
|
||||
decoder_out, _ = self.decoder(enc, enc_len, pre_acoustic_embeds, pre_token_length, bias_embed)
|
||||
decoder_out = torch.log_softmax(decoder_out, dim=-1)
|
||||
|
||||
return decoder_out, pre_token_length
|
||||
|
||||
|
||||
def export_backbone_dummy_inputs(self):
|
||||
"""Export backbone dummy inputs."""
|
||||
speech = torch.randn(2, 30, self.feats_dim)
|
||||
speech_lengths = torch.tensor([6, 30], dtype=torch.int32)
|
||||
bias_embed = torch.randn(2, 1, 512)
|
||||
return (speech, speech_lengths, bias_embed)
|
||||
|
||||
|
||||
def export_backbone_input_names(self):
|
||||
"""Export backbone input names."""
|
||||
return ["speech", "speech_lengths", "bias_embed"]
|
||||
|
||||
|
||||
def export_backbone_output_names(self):
|
||||
"""Export backbone output names."""
|
||||
return ["logits", "token_num"]
|
||||
|
||||
|
||||
def export_backbone_dynamic_axes(self):
|
||||
"""Export backbone dynamic axes."""
|
||||
return {
|
||||
"speech": {0: "batch_size", 1: "feats_length"},
|
||||
"speech_lengths": {
|
||||
0: "batch_size",
|
||||
},
|
||||
"bias_embed": {0: "batch_size", 1: "num_hotwords"},
|
||||
"logits": {0: "batch_size", 1: "logits_length"},
|
||||
}
|
||||
|
||||
|
||||
def export_backbone_name(self):
|
||||
"""Export backbone name."""
|
||||
return "model.onnx"
|
||||
@@ -0,0 +1,673 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import torch
|
||||
import codecs
|
||||
import logging
|
||||
import tempfile
|
||||
import requests
|
||||
import numpy as np
|
||||
from typing import Dict, Tuple
|
||||
from contextlib import contextmanager
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from funasr.register import tables
|
||||
from funasr.utils import postprocess_utils
|
||||
from funasr.metrics.compute_acc import th_accuracy
|
||||
from funasr.models.paraformer.model import Paraformer
|
||||
from funasr.utils.datadir_writer import DatadirWriter
|
||||
from funasr.models.paraformer.search import Hypothesis
|
||||
from funasr.train_utils.device_funcs import force_gatherable
|
||||
from funasr.models.transformer.utils.add_sos_eos import add_sos_eos
|
||||
from funasr.models.transformer.utils.nets_utils import make_pad_mask, pad_list
|
||||
from funasr.utils.load_utils import load_audio_text_image_video, extract_fbank
|
||||
|
||||
|
||||
if LooseVersion(torch.__version__) >= LooseVersion("1.6.0"):
|
||||
from torch.cuda.amp import autocast
|
||||
else:
|
||||
# Nothing to do if torch<1.6.0
|
||||
@contextmanager
|
||||
def autocast(enabled=True):
|
||||
"""Autocast.
|
||||
|
||||
Args:
|
||||
enabled: TODO.
|
||||
"""
|
||||
yield
|
||||
|
||||
|
||||
@tables.register("model_classes", "ContextualParaformer")
|
||||
class ContextualParaformer(Paraformer):
|
||||
"""ContextualParaformer: Paraformer with hotword/context biasing.
|
||||
|
||||
Extends Paraformer with a context encoder that incorporates user-defined
|
||||
hotwords/keywords to boost recognition of domain-specific terms.
|
||||
|
||||
Usage: Pass hotwords via generate(hotword='term1 term2').
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize ContextualParaformer.
|
||||
|
||||
Args:
|
||||
*args: Variable positional arguments.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.target_buffer_length = kwargs.get("target_buffer_length", -1)
|
||||
inner_dim = kwargs.get("inner_dim", 256)
|
||||
bias_encoder_type = kwargs.get("bias_encoder_type", "lstm")
|
||||
use_decoder_embedding = kwargs.get("use_decoder_embedding", False)
|
||||
crit_attn_weight = kwargs.get("crit_attn_weight", 0.0)
|
||||
crit_attn_smooth = kwargs.get("crit_attn_smooth", 0.0)
|
||||
bias_encoder_dropout_rate = kwargs.get("bias_encoder_dropout_rate", 0.0)
|
||||
|
||||
if bias_encoder_type == "lstm":
|
||||
self.bias_encoder = torch.nn.LSTM(
|
||||
inner_dim, inner_dim, 1, batch_first=True, dropout=bias_encoder_dropout_rate
|
||||
)
|
||||
self.bias_embed = torch.nn.Embedding(self.vocab_size, inner_dim)
|
||||
elif bias_encoder_type == "mean":
|
||||
self.bias_embed = torch.nn.Embedding(self.vocab_size, inner_dim)
|
||||
else:
|
||||
logging.error("Unsupport bias encoder type: {}".format(bias_encoder_type))
|
||||
|
||||
if self.target_buffer_length > 0:
|
||||
self.hotword_buffer = None
|
||||
self.length_record = []
|
||||
self.current_buffer_length = 0
|
||||
self.use_decoder_embedding = use_decoder_embedding
|
||||
self.crit_attn_weight = crit_attn_weight
|
||||
if self.crit_attn_weight > 0:
|
||||
self.attn_loss = torch.nn.L1Loss()
|
||||
self.crit_attn_smooth = crit_attn_smooth
|
||||
|
||||
def forward(
|
||||
self,
|
||||
speech: torch.Tensor,
|
||||
speech_lengths: torch.Tensor,
|
||||
text: torch.Tensor,
|
||||
text_lengths: torch.Tensor,
|
||||
**kwargs,
|
||||
) -> Tuple[torch.Tensor, Dict[str, torch.Tensor], torch.Tensor]:
|
||||
"""Frontend + Encoder + Decoder + Calc loss
|
||||
|
||||
Args:
|
||||
speech: (Batch, Length, ...)
|
||||
speech_lengths: (Batch, )
|
||||
text: (Batch, Length)
|
||||
text_lengths: (Batch,)
|
||||
"""
|
||||
text_lengths = text_lengths.squeeze()
|
||||
speech_lengths = speech_lengths.squeeze()
|
||||
|
||||
batch_size = speech.shape[0]
|
||||
|
||||
hotword_pad = kwargs.get("hotword_pad")
|
||||
hotword_lengths = kwargs.get("hotword_lengths")
|
||||
# dha_pad = kwargs.get("dha_pad")
|
||||
|
||||
# 1. Encoder
|
||||
encoder_out, encoder_out_lens = self.encode(speech, speech_lengths)
|
||||
|
||||
loss_ctc, cer_ctc = None, None
|
||||
|
||||
stats = dict()
|
||||
|
||||
# 1. CTC branch
|
||||
if self.ctc_weight != 0.0:
|
||||
loss_ctc, cer_ctc = self._calc_ctc_loss(
|
||||
encoder_out, encoder_out_lens, text, text_lengths
|
||||
)
|
||||
|
||||
# Collect CTC branch stats
|
||||
stats["loss_ctc"] = loss_ctc.detach() if loss_ctc is not None else None
|
||||
stats["cer_ctc"] = cer_ctc
|
||||
|
||||
# 2b. Attention decoder branch
|
||||
loss_att, acc_att, cer_att, wer_att, loss_pre, loss_ideal = self._calc_att_clas_loss(
|
||||
encoder_out, encoder_out_lens, text, text_lengths, hotword_pad, hotword_lengths
|
||||
)
|
||||
|
||||
# 3. CTC-Att loss definition
|
||||
if self.ctc_weight == 0.0:
|
||||
loss = loss_att + loss_pre * self.predictor_weight
|
||||
else:
|
||||
loss = (
|
||||
self.ctc_weight * loss_ctc
|
||||
+ (1 - self.ctc_weight) * loss_att
|
||||
+ loss_pre * self.predictor_weight
|
||||
)
|
||||
|
||||
if loss_ideal is not None:
|
||||
loss = loss + loss_ideal * self.crit_attn_weight
|
||||
stats["loss_ideal"] = loss_ideal.detach().cpu()
|
||||
|
||||
# Collect Attn branch stats
|
||||
stats["loss_att"] = loss_att.detach() if loss_att is not None else None
|
||||
stats["acc"] = acc_att
|
||||
stats["cer"] = cer_att
|
||||
stats["wer"] = wer_att
|
||||
stats["loss_pre"] = loss_pre.detach().cpu() if loss_pre is not None else None
|
||||
|
||||
stats["loss"] = torch.clone(loss.detach())
|
||||
# force_gatherable: to-device and to-tensor if scalar for DataParallel
|
||||
if self.length_normalized_loss:
|
||||
batch_size = int((text_lengths + self.predictor_bias).sum())
|
||||
|
||||
loss, stats, weight = force_gatherable((loss, stats, batch_size), loss.device)
|
||||
return loss, stats, weight
|
||||
|
||||
def _calc_att_clas_loss(
|
||||
self,
|
||||
encoder_out: torch.Tensor,
|
||||
encoder_out_lens: torch.Tensor,
|
||||
ys_pad: torch.Tensor,
|
||||
ys_pad_lens: torch.Tensor,
|
||||
hotword_pad: torch.Tensor,
|
||||
hotword_lengths: torch.Tensor,
|
||||
):
|
||||
"""Internal: calc att clas loss.
|
||||
|
||||
Args:
|
||||
encoder_out: Encoder output tensor.
|
||||
encoder_out_lens: Encoder output lengths.
|
||||
ys_pad: TODO.
|
||||
ys_pad_lens: Lengths of ys_pad.
|
||||
hotword_pad: TODO.
|
||||
hotword_lengths: Lengths of hotword.
|
||||
"""
|
||||
encoder_out_mask = (
|
||||
~make_pad_mask(encoder_out_lens, maxlen=encoder_out.size(1))[:, None, :]
|
||||
).to(encoder_out.device)
|
||||
|
||||
if self.predictor_bias == 1:
|
||||
_, ys_pad = add_sos_eos(ys_pad, self.sos, self.eos, self.ignore_id)
|
||||
ys_pad_lens = ys_pad_lens + self.predictor_bias
|
||||
|
||||
pre_acoustic_embeds, pre_token_length, _, _ = self.predictor(
|
||||
encoder_out, ys_pad, encoder_out_mask, ignore_id=self.ignore_id
|
||||
)
|
||||
# -1. bias encoder
|
||||
if self.use_decoder_embedding:
|
||||
hw_embed = self.decoder.embed(hotword_pad)
|
||||
else:
|
||||
hw_embed = self.bias_embed(hotword_pad)
|
||||
|
||||
hw_embed, (_, _) = self.bias_encoder(hw_embed)
|
||||
_ind = np.arange(0, hotword_pad.shape[0]).tolist()
|
||||
selected = hw_embed[_ind, [i - 1 for i in hotword_lengths.detach().cpu().tolist()]]
|
||||
contextual_info = selected.squeeze(0).repeat(ys_pad.shape[0], 1, 1).to(ys_pad.device)
|
||||
|
||||
# 0. sampler
|
||||
decoder_out_1st = None
|
||||
if self.sampling_ratio > 0.0:
|
||||
|
||||
sematic_embeds, decoder_out_1st = self.sampler(
|
||||
encoder_out,
|
||||
encoder_out_lens,
|
||||
ys_pad,
|
||||
ys_pad_lens,
|
||||
pre_acoustic_embeds,
|
||||
contextual_info,
|
||||
)
|
||||
else:
|
||||
sematic_embeds = pre_acoustic_embeds
|
||||
|
||||
# 1. Forward decoder
|
||||
decoder_outs = self.decoder(
|
||||
encoder_out,
|
||||
encoder_out_lens,
|
||||
sematic_embeds,
|
||||
ys_pad_lens,
|
||||
contextual_info=contextual_info,
|
||||
)
|
||||
decoder_out, _ = decoder_outs[0], decoder_outs[1]
|
||||
"""
|
||||
if self.crit_attn_weight > 0 and attn.shape[-1] > 1:
|
||||
ideal_attn = ideal_attn + self.crit_attn_smooth / (self.crit_attn_smooth + 1.0)
|
||||
attn_non_blank = attn[:,:,:,:-1]
|
||||
ideal_attn_non_blank = ideal_attn[:,:,:-1]
|
||||
loss_ideal = self.attn_loss(attn_non_blank.max(1)[0], ideal_attn_non_blank.to(attn.device))
|
||||
else:
|
||||
loss_ideal = None
|
||||
"""
|
||||
loss_ideal = None
|
||||
|
||||
if decoder_out_1st is None:
|
||||
decoder_out_1st = decoder_out
|
||||
# 2. Compute attention loss
|
||||
loss_att = self.criterion_att(decoder_out, ys_pad)
|
||||
acc_att = th_accuracy(
|
||||
decoder_out_1st.view(-1, self.vocab_size),
|
||||
ys_pad,
|
||||
ignore_label=self.ignore_id,
|
||||
)
|
||||
loss_pre = self.criterion_pre(ys_pad_lens.type_as(pre_token_length), pre_token_length)
|
||||
|
||||
# Compute cer/wer using attention-decoder
|
||||
if self.training or self.error_calculator is None:
|
||||
cer_att, wer_att = None, None
|
||||
else:
|
||||
ys_hat = decoder_out_1st.argmax(dim=-1)
|
||||
cer_att, wer_att = self.error_calculator(ys_hat.cpu(), ys_pad.cpu())
|
||||
|
||||
return loss_att, acc_att, cer_att, wer_att, loss_pre, loss_ideal
|
||||
|
||||
def sampler(
|
||||
self,
|
||||
encoder_out,
|
||||
encoder_out_lens,
|
||||
ys_pad,
|
||||
ys_pad_lens,
|
||||
pre_acoustic_embeds,
|
||||
contextual_info,
|
||||
):
|
||||
"""Sampler.
|
||||
|
||||
Args:
|
||||
encoder_out: Encoder output tensor.
|
||||
encoder_out_lens: Encoder output lengths.
|
||||
ys_pad: TODO.
|
||||
ys_pad_lens: Lengths of ys_pad.
|
||||
pre_acoustic_embeds: TODO.
|
||||
contextual_info: TODO.
|
||||
"""
|
||||
tgt_mask = (~make_pad_mask(ys_pad_lens, maxlen=ys_pad_lens.max())[:, :, None]).to(
|
||||
ys_pad.device
|
||||
)
|
||||
ys_pad = ys_pad * tgt_mask[:, :, 0]
|
||||
if self.share_embedding:
|
||||
ys_pad_embed = self.decoder.output_layer.weight[ys_pad]
|
||||
else:
|
||||
ys_pad_embed = self.decoder.embed(ys_pad)
|
||||
with torch.no_grad():
|
||||
decoder_outs = self.decoder(
|
||||
encoder_out,
|
||||
encoder_out_lens,
|
||||
pre_acoustic_embeds,
|
||||
ys_pad_lens,
|
||||
contextual_info=contextual_info,
|
||||
)
|
||||
decoder_out, _ = decoder_outs[0], decoder_outs[1]
|
||||
pred_tokens = decoder_out.argmax(-1)
|
||||
nonpad_positions = ys_pad.ne(self.ignore_id)
|
||||
seq_lens = (nonpad_positions).sum(1)
|
||||
same_num = ((pred_tokens == ys_pad) & nonpad_positions).sum(1)
|
||||
input_mask = torch.ones_like(nonpad_positions)
|
||||
bsz, seq_len = ys_pad.size()
|
||||
for li in range(bsz):
|
||||
target_num = (
|
||||
((seq_lens[li] - same_num[li].sum()).float()) * self.sampling_ratio
|
||||
).long()
|
||||
if target_num > 0:
|
||||
input_mask[li].scatter_(
|
||||
dim=0,
|
||||
index=torch.randperm(seq_lens[li])[:target_num].to(
|
||||
pre_acoustic_embeds.device
|
||||
),
|
||||
value=0,
|
||||
)
|
||||
input_mask = input_mask.eq(1)
|
||||
input_mask = input_mask.masked_fill(~nonpad_positions, False)
|
||||
input_mask_expand_dim = input_mask.unsqueeze(2).to(pre_acoustic_embeds.device)
|
||||
|
||||
sematic_embeds = pre_acoustic_embeds.masked_fill(
|
||||
~input_mask_expand_dim, 0
|
||||
) + ys_pad_embed.masked_fill(input_mask_expand_dim, 0)
|
||||
return sematic_embeds * tgt_mask, decoder_out * tgt_mask
|
||||
|
||||
def cal_decoder_with_predictor(
|
||||
self,
|
||||
encoder_out,
|
||||
encoder_out_lens,
|
||||
sematic_embeds,
|
||||
ys_pad_lens,
|
||||
hw_list=None,
|
||||
clas_scale=1.0,
|
||||
):
|
||||
"""Cal decoder with predictor.
|
||||
|
||||
Args:
|
||||
encoder_out: Encoder output tensor.
|
||||
encoder_out_lens: Encoder output lengths.
|
||||
sematic_embeds: TODO.
|
||||
ys_pad_lens: Lengths of ys_pad.
|
||||
hw_list: TODO.
|
||||
clas_scale: TODO.
|
||||
"""
|
||||
if hw_list is None:
|
||||
hw_list = [torch.Tensor([1]).long().to(encoder_out.device)] # empty hotword list
|
||||
hw_list_pad = pad_list(hw_list, 0)
|
||||
if self.use_decoder_embedding:
|
||||
hw_embed = self.decoder.embed(hw_list_pad)
|
||||
else:
|
||||
hw_embed = self.bias_embed(hw_list_pad)
|
||||
hw_embed, (h_n, _) = self.bias_encoder(hw_embed)
|
||||
hw_embed = h_n.repeat(encoder_out.shape[0], 1, 1)
|
||||
else:
|
||||
hw_lengths = [len(i) for i in hw_list]
|
||||
hw_list_pad = pad_list([torch.Tensor(i).long() for i in hw_list], 0).to(
|
||||
encoder_out.device
|
||||
)
|
||||
if self.use_decoder_embedding:
|
||||
hw_embed = self.decoder.embed(hw_list_pad)
|
||||
else:
|
||||
hw_embed = self.bias_embed(hw_list_pad)
|
||||
hw_embed = torch.nn.utils.rnn.pack_padded_sequence(
|
||||
hw_embed, hw_lengths, batch_first=True, enforce_sorted=False
|
||||
)
|
||||
_, (h_n, _) = self.bias_encoder(hw_embed)
|
||||
hw_embed = h_n.repeat(encoder_out.shape[0], 1, 1)
|
||||
|
||||
decoder_outs = self.decoder(
|
||||
encoder_out,
|
||||
encoder_out_lens,
|
||||
sematic_embeds,
|
||||
ys_pad_lens,
|
||||
contextual_info=hw_embed,
|
||||
clas_scale=clas_scale,
|
||||
)
|
||||
|
||||
decoder_out = decoder_outs[0]
|
||||
decoder_out = torch.log_softmax(decoder_out, dim=-1)
|
||||
return decoder_out, ys_pad_lens
|
||||
|
||||
def inference(
|
||||
self,
|
||||
data_in,
|
||||
data_lengths=None,
|
||||
key: list = None,
|
||||
tokenizer=None,
|
||||
frontend=None,
|
||||
**kwargs,
|
||||
):
|
||||
# init beamsearch
|
||||
|
||||
"""Run inference on input data.
|
||||
|
||||
Args:
|
||||
data_in: Input data (audio samples, file paths, or text).
|
||||
data_lengths: Lengths of each input sample in the batch.
|
||||
key: Sample identifiers.
|
||||
tokenizer: Tokenizer instance for text encoding/decoding.
|
||||
frontend: Audio frontend for feature extraction.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
is_use_ctc = kwargs.get("decoding_ctc_weight", 0.0) > 0.00001 and self.ctc != None
|
||||
is_use_lm = (
|
||||
kwargs.get("lm_weight", 0.0) > 0.00001 and kwargs.get("lm_file", None) is not None
|
||||
)
|
||||
if self.beam_search is None and (is_use_lm or is_use_ctc):
|
||||
logging.info("enable beam_search")
|
||||
self.init_beam_search(**kwargs)
|
||||
self.nbest = kwargs.get("nbest", 1)
|
||||
|
||||
meta_data = {}
|
||||
|
||||
# extract fbank feats
|
||||
time1 = time.perf_counter()
|
||||
|
||||
audio_sample_list = load_audio_text_image_video(
|
||||
data_in, fs=frontend.fs, audio_fs=kwargs.get("fs", 16000)
|
||||
)
|
||||
|
||||
time2 = time.perf_counter()
|
||||
meta_data["load_data"] = f"{time2 - time1:0.3f}"
|
||||
|
||||
speech, speech_lengths = extract_fbank(
|
||||
audio_sample_list, data_type=kwargs.get("data_type", "sound"), frontend=frontend
|
||||
)
|
||||
time3 = time.perf_counter()
|
||||
meta_data["extract_feat"] = f"{time3 - time2:0.3f}"
|
||||
meta_data["batch_data_time"] = (
|
||||
speech_lengths.sum().item() * frontend.frame_shift * frontend.lfr_n / 1000
|
||||
)
|
||||
|
||||
speech = speech.to(device=kwargs["device"])
|
||||
speech_lengths = speech_lengths.to(device=kwargs["device"])
|
||||
|
||||
# hotword
|
||||
self.hotword_list = self.generate_hotwords_list(
|
||||
kwargs.get("hotword", None), tokenizer=tokenizer, frontend=frontend
|
||||
)
|
||||
|
||||
# Encoder
|
||||
encoder_out, encoder_out_lens = self.encode(speech, speech_lengths)
|
||||
if isinstance(encoder_out, tuple):
|
||||
encoder_out = encoder_out[0]
|
||||
|
||||
# predictor
|
||||
predictor_outs = self.calc_predictor(encoder_out, encoder_out_lens)
|
||||
pre_acoustic_embeds, pre_token_length, alphas, pre_peak_index = (
|
||||
predictor_outs[0],
|
||||
predictor_outs[1],
|
||||
predictor_outs[2],
|
||||
predictor_outs[3],
|
||||
)
|
||||
pre_token_length = pre_token_length.round().long()
|
||||
if torch.max(pre_token_length) < 1:
|
||||
return []
|
||||
|
||||
decoder_outs = self.cal_decoder_with_predictor(
|
||||
encoder_out,
|
||||
encoder_out_lens,
|
||||
pre_acoustic_embeds,
|
||||
pre_token_length,
|
||||
hw_list=self.hotword_list,
|
||||
clas_scale=kwargs.get("clas_scale", 1.0),
|
||||
)
|
||||
decoder_out, ys_pad_lens = decoder_outs[0], decoder_outs[1]
|
||||
|
||||
results = []
|
||||
b, n, d = decoder_out.size()
|
||||
for i in range(b):
|
||||
x = encoder_out[i, : encoder_out_lens[i], :]
|
||||
am_scores = decoder_out[i, : pre_token_length[i], :]
|
||||
if self.beam_search is not None:
|
||||
nbest_hyps = self.beam_search(
|
||||
x=x,
|
||||
am_scores=am_scores,
|
||||
maxlenratio=kwargs.get("maxlenratio", 0.0),
|
||||
minlenratio=kwargs.get("minlenratio", 0.0),
|
||||
)
|
||||
|
||||
nbest_hyps = nbest_hyps[: self.nbest]
|
||||
else:
|
||||
|
||||
yseq = am_scores.argmax(dim=-1)
|
||||
score = am_scores.max(dim=-1)[0]
|
||||
score = torch.sum(score, dim=-1)
|
||||
# pad with mask tokens to ensure compatibility with sos/eos tokens
|
||||
yseq = torch.tensor([self.sos] + yseq.tolist() + [self.eos], device=yseq.device)
|
||||
nbest_hyps = [Hypothesis(yseq=yseq, score=score)]
|
||||
for nbest_idx, hyp in enumerate(nbest_hyps):
|
||||
ibest_writer = None
|
||||
if kwargs.get("output_dir") is not None:
|
||||
if not hasattr(self, "writer"):
|
||||
self.writer = DatadirWriter(kwargs.get("output_dir"))
|
||||
ibest_writer = self.writer[f"{nbest_idx + 1}best_recog"]
|
||||
|
||||
# remove sos/eos and get results
|
||||
last_pos = -1
|
||||
if isinstance(hyp.yseq, list):
|
||||
token_int = hyp.yseq[1:last_pos]
|
||||
else:
|
||||
token_int = hyp.yseq[1:last_pos].tolist()
|
||||
|
||||
# remove blank symbol id, which is assumed to be 0
|
||||
token_int = list(
|
||||
filter(
|
||||
lambda x: x != self.eos and x != self.sos and x != self.blank_id, token_int
|
||||
)
|
||||
)
|
||||
|
||||
if tokenizer is not None:
|
||||
# Change integer-ids to tokens
|
||||
token = tokenizer.ids2tokens(token_int)
|
||||
text = tokenizer.tokens2text(token)
|
||||
|
||||
text_postprocessed, _ = postprocess_utils.sentence_postprocess(token)
|
||||
result_i = {"key": key[i], "text": text_postprocessed}
|
||||
|
||||
if ibest_writer is not None:
|
||||
ibest_writer["token"][key[i]] = " ".join(token)
|
||||
ibest_writer["text"][key[i]] = text
|
||||
ibest_writer["text_postprocessed"][key[i]] = text_postprocessed
|
||||
else:
|
||||
result_i = {"key": key[i], "token_int": token_int}
|
||||
results.append(result_i)
|
||||
|
||||
return results, meta_data
|
||||
|
||||
def generate_hotwords_list(self, hotword_list_or_file, tokenizer=None, frontend=None):
|
||||
"""Generate hotwords list.
|
||||
|
||||
Args:
|
||||
hotword_list_or_file: TODO.
|
||||
tokenizer: Tokenizer instance for text encoding/decoding.
|
||||
frontend: Audio frontend for feature extraction.
|
||||
"""
|
||||
def load_seg_dict(seg_dict_file):
|
||||
"""Load seg dict.
|
||||
|
||||
Args:
|
||||
seg_dict_file: TODO.
|
||||
"""
|
||||
seg_dict = {}
|
||||
assert isinstance(seg_dict_file, str)
|
||||
with open(seg_dict_file, "r", encoding="utf8") as f:
|
||||
lines = f.readlines()
|
||||
for line in lines:
|
||||
s = line.strip().split()
|
||||
key = s[0]
|
||||
value = s[1:]
|
||||
seg_dict[key] = " ".join(value)
|
||||
return seg_dict
|
||||
|
||||
def seg_tokenize(txt, seg_dict):
|
||||
"""Seg tokenize.
|
||||
|
||||
Args:
|
||||
txt: TODO.
|
||||
seg_dict: TODO.
|
||||
"""
|
||||
pattern = re.compile(r"^[\u4E00-\u9FA50-9]+$")
|
||||
out_txt = ""
|
||||
for word in txt:
|
||||
word = word.lower()
|
||||
if word in seg_dict:
|
||||
out_txt += seg_dict[word] + " "
|
||||
else:
|
||||
if pattern.match(word):
|
||||
for char in word:
|
||||
if char in seg_dict:
|
||||
out_txt += seg_dict[char] + " "
|
||||
else:
|
||||
out_txt += "<unk>" + " "
|
||||
else:
|
||||
out_txt += "<unk>" + " "
|
||||
return out_txt.strip().split()
|
||||
|
||||
seg_dict = None
|
||||
if frontend.cmvn_file is not None:
|
||||
model_dir = os.path.dirname(frontend.cmvn_file)
|
||||
seg_dict_file = os.path.join(model_dir, "seg_dict")
|
||||
if os.path.exists(seg_dict_file):
|
||||
seg_dict = load_seg_dict(seg_dict_file)
|
||||
else:
|
||||
seg_dict = None
|
||||
# for None
|
||||
if hotword_list_or_file is None:
|
||||
hotword_list = None
|
||||
# for local txt inputs
|
||||
elif os.path.exists(hotword_list_or_file) and hotword_list_or_file.endswith(".txt"):
|
||||
logging.info("Attempting to parse hotwords from local txt...")
|
||||
hotword_list = []
|
||||
hotword_str_list = []
|
||||
with codecs.open(hotword_list_or_file, "r") as fin:
|
||||
for line in fin.readlines():
|
||||
hw = line.strip()
|
||||
hw_list = hw.split()
|
||||
if seg_dict is not None:
|
||||
hw_list = seg_tokenize(hw_list, seg_dict)
|
||||
hotword_str_list.append(hw)
|
||||
hotword_list.append(tokenizer.tokens2ids(hw_list))
|
||||
hotword_list.append([self.sos])
|
||||
hotword_str_list.append("<s>")
|
||||
logging.info(
|
||||
"Initialized hotword list from file: {}, hotword list: {}.".format(
|
||||
hotword_list_or_file, hotword_str_list
|
||||
)
|
||||
)
|
||||
# for url, download and generate txt
|
||||
elif hotword_list_or_file.startswith("http"):
|
||||
logging.info("Attempting to parse hotwords from url...")
|
||||
work_dir = tempfile.TemporaryDirectory().name
|
||||
if not os.path.exists(work_dir):
|
||||
os.makedirs(work_dir)
|
||||
text_file_path = os.path.join(work_dir, os.path.basename(hotword_list_or_file))
|
||||
local_file = requests.get(hotword_list_or_file)
|
||||
open(text_file_path, "wb").write(local_file.content)
|
||||
hotword_list_or_file = text_file_path
|
||||
hotword_list = []
|
||||
hotword_str_list = []
|
||||
with codecs.open(hotword_list_or_file, "r") as fin:
|
||||
for line in fin.readlines():
|
||||
hw = line.strip()
|
||||
hw_list = hw.split()
|
||||
if seg_dict is not None:
|
||||
hw_list = seg_tokenize(hw_list, seg_dict)
|
||||
hotword_str_list.append(hw)
|
||||
hotword_list.append(tokenizer.tokens2ids(hw_list))
|
||||
hotword_list.append([self.sos])
|
||||
hotword_str_list.append("<s>")
|
||||
logging.info(
|
||||
"Initialized hotword list from file: {}, hotword list: {}.".format(
|
||||
hotword_list_or_file, hotword_str_list
|
||||
)
|
||||
)
|
||||
# for text str input
|
||||
elif not hotword_list_or_file.endswith(".txt"):
|
||||
logging.info("Attempting to parse hotwords as str...")
|
||||
hotword_list = []
|
||||
hotword_str_list = []
|
||||
for hw in hotword_list_or_file.strip().split():
|
||||
hotword_str_list.append(hw)
|
||||
hw_list = hw.strip().split()
|
||||
if seg_dict is not None:
|
||||
hw_list = seg_tokenize(hw_list, seg_dict)
|
||||
hotword_list.append(tokenizer.tokens2ids(hw_list))
|
||||
hotword_list.append([self.sos])
|
||||
hotword_str_list.append("<s>")
|
||||
logging.info("Hotword list: {}.".format(hotword_str_list))
|
||||
else:
|
||||
hotword_list = None
|
||||
return hotword_list
|
||||
|
||||
def export(
|
||||
self,
|
||||
**kwargs,
|
||||
):
|
||||
"""Export.
|
||||
|
||||
Args:
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
if "max_seq_len" not in kwargs:
|
||||
kwargs["max_seq_len"] = 512
|
||||
from .export_meta import export_rebuild_model
|
||||
|
||||
models = export_rebuild_model(model=self, **kwargs)
|
||||
return models
|
||||
@@ -0,0 +1,129 @@
|
||||
# This is an example that demonstrates how to configure a model file.
|
||||
# You can modify the configuration according to your own requirements.
|
||||
|
||||
# to print the register_table:
|
||||
# from funasr.register import tables
|
||||
# tables.print()
|
||||
|
||||
# network architecture
|
||||
model: ContextualParaformer
|
||||
model_conf:
|
||||
ctc_weight: 0.0
|
||||
lsm_weight: 0.1
|
||||
length_normalized_loss: true
|
||||
predictor_weight: 1.0
|
||||
predictor_bias: 1
|
||||
sampling_ratio: 0.75
|
||||
inner_dim: 512
|
||||
|
||||
# encoder
|
||||
encoder: SANMEncoder
|
||||
encoder_conf:
|
||||
output_size: 512
|
||||
attention_heads: 4
|
||||
linear_units: 2048
|
||||
num_blocks: 50
|
||||
dropout_rate: 0.1
|
||||
positional_dropout_rate: 0.1
|
||||
attention_dropout_rate: 0.1
|
||||
input_layer: pe
|
||||
pos_enc_class: SinusoidalPositionEncoder
|
||||
normalize_before: true
|
||||
kernel_size: 11
|
||||
sanm_shfit: 0
|
||||
selfattention_layer_type: sanm
|
||||
|
||||
|
||||
# decoder
|
||||
decoder: ContextualParaformerDecoder
|
||||
decoder_conf:
|
||||
attention_heads: 4
|
||||
linear_units: 2048
|
||||
num_blocks: 16
|
||||
dropout_rate: 0.1
|
||||
positional_dropout_rate: 0.1
|
||||
self_attention_dropout_rate: 0.1
|
||||
src_attention_dropout_rate: 0.1
|
||||
att_layer_num: 16
|
||||
kernel_size: 11
|
||||
sanm_shfit: 0
|
||||
|
||||
predictor: CifPredictorV2
|
||||
predictor_conf:
|
||||
idim: 512
|
||||
threshold: 1.0
|
||||
l_order: 1
|
||||
r_order: 1
|
||||
tail_threshold: 0.45
|
||||
|
||||
# frontend related
|
||||
frontend: WavFrontend
|
||||
frontend_conf:
|
||||
fs: 16000
|
||||
window: hamming
|
||||
n_mels: 80
|
||||
frame_length: 25
|
||||
frame_shift: 10
|
||||
lfr_m: 7
|
||||
lfr_n: 6
|
||||
|
||||
specaug: SpecAugLFR
|
||||
specaug_conf:
|
||||
apply_time_warp: false
|
||||
time_warp_window: 5
|
||||
time_warp_mode: bicubic
|
||||
apply_freq_mask: true
|
||||
freq_mask_width_range:
|
||||
- 0
|
||||
- 30
|
||||
lfr_rate: 6
|
||||
num_freq_mask: 1
|
||||
apply_time_mask: true
|
||||
time_mask_width_range:
|
||||
- 0
|
||||
- 12
|
||||
num_time_mask: 1
|
||||
|
||||
train_conf:
|
||||
accum_grad: 1
|
||||
grad_clip: 5
|
||||
max_epoch: 150
|
||||
val_scheduler_criterion:
|
||||
- valid
|
||||
- acc
|
||||
best_model_criterion:
|
||||
- - valid
|
||||
- acc
|
||||
- max
|
||||
keep_nbest_models: 10
|
||||
log_interval: 50
|
||||
|
||||
optim: adam
|
||||
optim_conf:
|
||||
lr: 0.0005
|
||||
scheduler: warmuplr
|
||||
scheduler_conf:
|
||||
warmup_steps: 30000
|
||||
|
||||
dataset: AudioDataset
|
||||
dataset_conf:
|
||||
index_ds: IndexDSJsonl
|
||||
batch_sampler: BatchSampler
|
||||
batch_type: example # example or length
|
||||
batch_size: 1 # if batch_type is example, batch_size is the numbers of samples; if length, batch_size is source_token_len+target_token_len;
|
||||
max_token_length: 2048 # filter samples if source_token_len+target_token_len > max_token_length,
|
||||
buffer_size: 500
|
||||
shuffle: True
|
||||
num_workers: 0
|
||||
|
||||
tokenizer: CharTokenizer
|
||||
tokenizer_conf:
|
||||
unk_symbol: <unk>
|
||||
split_with_space: true
|
||||
|
||||
ctc_conf:
|
||||
dropout_rate: 0.0
|
||||
ctc_type: builtin
|
||||
reduce: true
|
||||
ignore_nan_grad: true
|
||||
normalize: null
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
import types
|
||||
import torch
|
||||
from funasr.register import tables
|
||||
|
||||
|
||||
def export_rebuild_model(model, **kwargs):
|
||||
|
||||
"""Export rebuild model.
|
||||
|
||||
Args:
|
||||
model: Model instance or model name.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
is_onnx = kwargs.get("type", "onnx") == "onnx"
|
||||
encoder_class = tables.encoder_classes.get(kwargs["encoder"] + "Export")
|
||||
model.encoder = encoder_class(model.encoder, onnx=is_onnx)
|
||||
|
||||
model.forward = types.MethodType(export_forward, model)
|
||||
model.export_dummy_inputs = types.MethodType(export_dummy_inputs, model)
|
||||
model.export_input_names = types.MethodType(export_input_names, model)
|
||||
model.export_output_names = types.MethodType(export_output_names, model)
|
||||
model.export_dynamic_axes = types.MethodType(export_dynamic_axes, model)
|
||||
model.export_name = types.MethodType(export_name, model)
|
||||
|
||||
return model
|
||||
|
||||
|
||||
def export_forward(self, inputs: torch.Tensor, text_lengths: torch.Tensor):
|
||||
"""Compute loss value from buffer sequences.
|
||||
|
||||
Args:
|
||||
input (torch.Tensor): Input ids. (batch, len)
|
||||
hidden (torch.Tensor): Target ids. (batch, len)
|
||||
|
||||
"""
|
||||
x = self.embed(inputs)
|
||||
h, _ = self.encoder(x, text_lengths)
|
||||
y = self.decoder(h)
|
||||
return y
|
||||
|
||||
|
||||
def export_dummy_inputs(self):
|
||||
"""Export dummy inputs."""
|
||||
length = 120
|
||||
text_indexes = torch.randint(0, self.embed.num_embeddings, (2, length)).type(torch.int32)
|
||||
text_lengths = torch.tensor([length - 20, length], dtype=torch.int32)
|
||||
return (text_indexes, text_lengths)
|
||||
|
||||
|
||||
def export_input_names(self):
|
||||
"""Export input names."""
|
||||
return ["inputs", "text_lengths"]
|
||||
|
||||
|
||||
def export_output_names(self):
|
||||
"""Export output names."""
|
||||
return ["logits"]
|
||||
|
||||
|
||||
def export_dynamic_axes(self):
|
||||
"""Export dynamic axes."""
|
||||
return {
|
||||
"inputs": {0: "batch_size", 1: "feats_length"},
|
||||
"text_lengths": {
|
||||
0: "batch_size",
|
||||
},
|
||||
"logits": {0: "batch_size", 1: "logits_length"},
|
||||
}
|
||||
|
||||
|
||||
def export_name(self):
|
||||
"""Export name."""
|
||||
return "model.onnx"
|
||||
@@ -0,0 +1,481 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
import copy
|
||||
import torch
|
||||
import numpy as np
|
||||
import torch.nn.functional as F
|
||||
from contextlib import contextmanager
|
||||
from distutils.version import LooseVersion
|
||||
from typing import Any, List, Tuple, Optional
|
||||
|
||||
from funasr.register import tables
|
||||
from funasr.train_utils.device_funcs import to_device
|
||||
from funasr.train_utils.device_funcs import force_gatherable
|
||||
from funasr.utils.load_utils import load_audio_text_image_video
|
||||
from funasr.models.transformer.utils.nets_utils import make_pad_mask
|
||||
from funasr.models.ct_transformer.utils import split_to_mini_sentence, split_words
|
||||
|
||||
try:
|
||||
import jieba
|
||||
except:
|
||||
pass
|
||||
if LooseVersion(torch.__version__) >= LooseVersion("1.6.0"):
|
||||
from torch.cuda.amp import autocast
|
||||
else:
|
||||
# Nothing to do if torch<1.6.0
|
||||
@contextmanager
|
||||
def autocast(enabled=True):
|
||||
"""Autocast.
|
||||
|
||||
Args:
|
||||
enabled: TODO.
|
||||
"""
|
||||
yield
|
||||
|
||||
|
||||
@tables.register("model_classes", "CTTransformer")
|
||||
class CTTransformer(torch.nn.Module):
|
||||
"""CT-Transformer: Punctuation Restoration Model.
|
||||
|
||||
Adds punctuation (comma, period, question mark) to unpunctuated text.
|
||||
Supports Chinese and English. Used as punc_model in the ASR pipeline.
|
||||
|
||||
Output: {"key": "...", "text": "punctuated text", "punc_array": Tensor}
|
||||
punc_array encoding: 1=none, 2=comma(,), 3=period(。), 4=question(?)
|
||||
|
||||
Note: Not needed for Fun-ASR-Nano/SenseVoice/Qwen3-ASR (they output punctuation natively).
|
||||
Only required for Paraformer models.
|
||||
|
||||
Author: Speech Lab of DAMO Academy, Alibaba Group
|
||||
CT-Transformer: Controllable time-delay transformer for real-time punctuation prediction and disfluency detection
|
||||
https://arxiv.org/pdf/2003.01309.pdf
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
encoder: str = None,
|
||||
encoder_conf: dict = None,
|
||||
vocab_size: int = -1,
|
||||
punc_list: list = None,
|
||||
punc_weight: list = None,
|
||||
embed_unit: int = 128,
|
||||
att_unit: int = 256,
|
||||
dropout_rate: float = 0.5,
|
||||
ignore_id: int = -1,
|
||||
sos: int = 1,
|
||||
eos: int = 2,
|
||||
sentence_end_id: int = 3,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize CTTransformer.
|
||||
|
||||
Args:
|
||||
encoder: TODO.
|
||||
encoder_conf: Configuration dict for encoder.
|
||||
vocab_size: Size/dimension parameter.
|
||||
punc_list: TODO.
|
||||
punc_weight: TODO.
|
||||
embed_unit: TODO.
|
||||
att_unit: TODO.
|
||||
dropout_rate: TODO.
|
||||
ignore_id: TODO.
|
||||
sos: TODO.
|
||||
eos: TODO.
|
||||
sentence_end_id: TODO.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
punc_size = len(punc_list)
|
||||
if punc_weight is None:
|
||||
punc_weight = [1] * punc_size
|
||||
|
||||
self.embed = torch.nn.Embedding(vocab_size, embed_unit)
|
||||
encoder_class = tables.encoder_classes.get(encoder)
|
||||
encoder = encoder_class(**encoder_conf)
|
||||
|
||||
self.decoder = torch.nn.Linear(att_unit, punc_size)
|
||||
self.encoder = encoder
|
||||
self.punc_list = punc_list
|
||||
self.punc_weight = punc_weight
|
||||
self.ignore_id = ignore_id
|
||||
self.sos = sos
|
||||
self.eos = eos
|
||||
self.sentence_end_id = sentence_end_id
|
||||
self.jieba_usr_dict = None
|
||||
if kwargs.get("jieba_usr_dict", None) is not None:
|
||||
jieba.load_userdict(kwargs["jieba_usr_dict"])
|
||||
self.jieba_usr_dict = jieba
|
||||
|
||||
def punc_forward(self, text: torch.Tensor, text_lengths: torch.Tensor, **kwargs):
|
||||
"""Compute loss value from buffer sequences.
|
||||
|
||||
Args:
|
||||
input (torch.Tensor): Input ids. (batch, len)
|
||||
hidden (torch.Tensor): Target ids. (batch, len)
|
||||
|
||||
"""
|
||||
x = self.embed(text)
|
||||
# mask = self._target_mask(input)
|
||||
h, _, _ = self.encoder(x, text_lengths)
|
||||
y = self.decoder(h)
|
||||
return y, None
|
||||
|
||||
def with_vad(self):
|
||||
"""With vad."""
|
||||
return False
|
||||
|
||||
def score(self, y: torch.Tensor, state: Any, x: torch.Tensor) -> Tuple[torch.Tensor, Any]:
|
||||
"""Score new token.
|
||||
|
||||
Args:
|
||||
y (torch.Tensor): 1D torch.int64 prefix tokens.
|
||||
state: Scorer state for prefix tokens
|
||||
x (torch.Tensor): encoder feature that generates ys.
|
||||
|
||||
Returns:
|
||||
tuple[torch.Tensor, Any]: Tuple of
|
||||
torch.float32 scores for next token (vocab_size)
|
||||
and next state for ys
|
||||
|
||||
"""
|
||||
y = y.unsqueeze(0)
|
||||
h, _, cache = self.encoder.forward_one_step(
|
||||
self.embed(y), self._target_mask(y), cache=state
|
||||
)
|
||||
h = self.decoder(h[:, -1])
|
||||
logp = h.log_softmax(dim=-1).squeeze(0)
|
||||
return logp, cache
|
||||
|
||||
def batch_score(
|
||||
self, ys: torch.Tensor, states: List[Any], xs: torch.Tensor
|
||||
) -> Tuple[torch.Tensor, List[Any]]:
|
||||
"""Score new token batch.
|
||||
|
||||
Args:
|
||||
ys (torch.Tensor): torch.int64 prefix tokens (n_batch, ylen).
|
||||
states (List[Any]): Scorer states for prefix tokens.
|
||||
xs (torch.Tensor):
|
||||
The encoder feature that generates ys (n_batch, xlen, n_feat).
|
||||
|
||||
Returns:
|
||||
tuple[torch.Tensor, List[Any]]: Tuple of
|
||||
batchfied scores for next token with shape of `(n_batch, vocab_size)`
|
||||
and next state list for ys.
|
||||
|
||||
"""
|
||||
# merge states
|
||||
n_batch = len(ys)
|
||||
n_layers = len(self.encoder.encoders)
|
||||
if states[0] is None:
|
||||
batch_state = None
|
||||
else:
|
||||
# transpose state of [batch, layer] into [layer, batch]
|
||||
batch_state = [
|
||||
torch.stack([states[b][i] for b in range(n_batch)]) for i in range(n_layers)
|
||||
]
|
||||
|
||||
# batch decoding
|
||||
h, _, states = self.encoder.forward_one_step(
|
||||
self.embed(ys), self._target_mask(ys), cache=batch_state
|
||||
)
|
||||
h = self.decoder(h[:, -1])
|
||||
logp = h.log_softmax(dim=-1)
|
||||
|
||||
# transpose state of [layer, batch] into [batch, layer]
|
||||
state_list = [[states[i][b] for i in range(n_layers)] for b in range(n_batch)]
|
||||
return logp, state_list
|
||||
|
||||
def nll(
|
||||
self,
|
||||
text: torch.Tensor,
|
||||
punc: torch.Tensor,
|
||||
text_lengths: torch.Tensor,
|
||||
punc_lengths: torch.Tensor,
|
||||
max_length: Optional[int] = None,
|
||||
vad_indexes: Optional[torch.Tensor] = None,
|
||||
vad_indexes_lengths: Optional[torch.Tensor] = None,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Compute negative log likelihood(nll)
|
||||
|
||||
Normally, this function is called in batchify_nll.
|
||||
Args:
|
||||
text: (Batch, Length)
|
||||
punc: (Batch, Length)
|
||||
text_lengths: (Batch,)
|
||||
max_lengths: int
|
||||
"""
|
||||
batch_size = text.size(0)
|
||||
# For data parallel
|
||||
if max_length is None:
|
||||
text = text[:, : text_lengths.max()]
|
||||
punc = punc[:, : text_lengths.max()]
|
||||
else:
|
||||
text = text[:, :max_length]
|
||||
punc = punc[:, :max_length]
|
||||
|
||||
if self.with_vad():
|
||||
# Should be VadRealtimeTransformer
|
||||
assert vad_indexes is not None
|
||||
y, _ = self.punc_forward(text, text_lengths, vad_indexes)
|
||||
else:
|
||||
# Should be TargetDelayTransformer,
|
||||
y, _ = self.punc_forward(text, text_lengths)
|
||||
|
||||
# Calc negative log likelihood
|
||||
# nll: (BxL,)
|
||||
if self.training == False:
|
||||
_, indices = y.view(-1, y.shape[-1]).topk(1, dim=1)
|
||||
from sklearn.metrics import f1_score
|
||||
|
||||
f1_score = f1_score(
|
||||
punc.view(-1).detach().cpu().numpy(),
|
||||
indices.squeeze(-1).detach().cpu().numpy(),
|
||||
average="micro",
|
||||
)
|
||||
nll = torch.Tensor([f1_score]).repeat(text_lengths.sum())
|
||||
return nll, text_lengths
|
||||
else:
|
||||
self.punc_weight = self.punc_weight.to(punc.device)
|
||||
nll = F.cross_entropy(
|
||||
y.view(-1, y.shape[-1]),
|
||||
punc.view(-1),
|
||||
self.punc_weight,
|
||||
reduction="none",
|
||||
ignore_index=self.ignore_id,
|
||||
)
|
||||
# nll: (BxL,) -> (BxL,)
|
||||
if max_length is None:
|
||||
nll.masked_fill_(make_pad_mask(text_lengths).to(nll.device).view(-1), 0.0)
|
||||
else:
|
||||
nll.masked_fill_(
|
||||
make_pad_mask(text_lengths, maxlen=max_length + 1).to(nll.device).view(-1),
|
||||
0.0,
|
||||
)
|
||||
# nll: (BxL,) -> (B, L)
|
||||
nll = nll.view(batch_size, -1)
|
||||
return nll, text_lengths
|
||||
|
||||
def forward(
|
||||
self,
|
||||
text: torch.Tensor,
|
||||
punc: torch.Tensor,
|
||||
text_lengths: torch.Tensor,
|
||||
punc_lengths: torch.Tensor,
|
||||
vad_indexes: Optional[torch.Tensor] = None,
|
||||
vad_indexes_lengths: Optional[torch.Tensor] = None,
|
||||
):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
text: Text tensor or string input.
|
||||
punc: TODO.
|
||||
text_lengths: Length of each text sample.
|
||||
punc_lengths: Lengths of punc.
|
||||
vad_indexes: TODO.
|
||||
vad_indexes_lengths: Lengths of vad_indexes.
|
||||
"""
|
||||
nll, y_lengths = self.nll(text, punc, text_lengths, punc_lengths, vad_indexes=vad_indexes)
|
||||
ntokens = y_lengths.sum()
|
||||
loss = nll.sum() / ntokens
|
||||
stats = dict(loss=loss.detach())
|
||||
|
||||
# force_gatherable: to-device and to-tensor if scalar for DataParallel
|
||||
loss, stats, weight = force_gatherable((loss, stats, ntokens), loss.device)
|
||||
return loss, stats, weight
|
||||
|
||||
def inference(
|
||||
self,
|
||||
data_in,
|
||||
data_lengths=None,
|
||||
key: list = None,
|
||||
tokenizer=None,
|
||||
frontend=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Run inference on input data.
|
||||
|
||||
Args:
|
||||
data_in: Input data (audio samples, file paths, or text).
|
||||
data_lengths: Lengths of each input sample in the batch.
|
||||
key: Sample identifiers.
|
||||
tokenizer: Tokenizer instance for text encoding/decoding.
|
||||
frontend: Audio frontend for feature extraction.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
assert len(data_in) == 1
|
||||
if not data_in[0] or (isinstance(data_in[0], str) and not data_in[0].strip()):
|
||||
meta_data = {"batch_data_time": -1}
|
||||
return [{"key": key[0] if key else "", "text": "", "punc_array": None}], meta_data
|
||||
text = load_audio_text_image_video(data_in, data_type=kwargs.get("kwargs", "text"))[0]
|
||||
vad_indexes = kwargs.get("vad_indexes", None)
|
||||
# text = data_in[0]
|
||||
# text_lengths = data_lengths[0] if data_lengths is not None else None
|
||||
split_size = kwargs.get("split_size", 20)
|
||||
|
||||
tokens = split_words(text, jieba_usr_dict=self.jieba_usr_dict)
|
||||
tokens_int = tokenizer.encode(tokens)
|
||||
|
||||
mini_sentences = split_to_mini_sentence(tokens, split_size)
|
||||
mini_sentences_id = split_to_mini_sentence(tokens_int, split_size)
|
||||
assert len(mini_sentences) == len(mini_sentences_id)
|
||||
cache_sent = []
|
||||
cache_sent_id = torch.from_numpy(np.array([], dtype="int32"))
|
||||
new_mini_sentence = ""
|
||||
new_mini_sentence_punc = []
|
||||
cache_pop_trigger_limit = 200
|
||||
results = []
|
||||
meta_data = {}
|
||||
punc_array = None
|
||||
for mini_sentence_i in range(len(mini_sentences)):
|
||||
mini_sentence = mini_sentences[mini_sentence_i]
|
||||
mini_sentence_id = mini_sentences_id[mini_sentence_i]
|
||||
mini_sentence = cache_sent + mini_sentence
|
||||
mini_sentence_id = np.concatenate((cache_sent_id, mini_sentence_id), axis=0)
|
||||
data = {
|
||||
"text": torch.unsqueeze(torch.from_numpy(mini_sentence_id), 0),
|
||||
"text_lengths": torch.from_numpy(np.array([len(mini_sentence_id)], dtype="int32")),
|
||||
}
|
||||
data = to_device(data, kwargs["device"])
|
||||
# y, _ = self.wrapped_model(**data)
|
||||
y, _ = self.punc_forward(**data)
|
||||
_, indices = y.view(-1, y.shape[-1]).topk(1, dim=1)
|
||||
punctuations = torch.squeeze(indices, dim=1)
|
||||
assert punctuations.size()[0] == len(mini_sentence)
|
||||
|
||||
# Search for the last Period/QuestionMark as cache
|
||||
if mini_sentence_i < len(mini_sentences) - 1:
|
||||
sentenceEnd = -1
|
||||
last_comma_index = -1
|
||||
for i in range(len(punctuations) - 2, 1, -1):
|
||||
if (
|
||||
self.punc_list[punctuations[i]] == "。"
|
||||
or self.punc_list[punctuations[i]] == "?"
|
||||
):
|
||||
sentenceEnd = i
|
||||
break
|
||||
if last_comma_index < 0 and self.punc_list[punctuations[i]] == ",":
|
||||
last_comma_index = i
|
||||
|
||||
if (
|
||||
sentenceEnd < 0
|
||||
and len(mini_sentence) > cache_pop_trigger_limit
|
||||
and last_comma_index >= 0
|
||||
):
|
||||
# The sentence it too long, cut off at a comma.
|
||||
sentenceEnd = last_comma_index
|
||||
punctuations[sentenceEnd] = self.sentence_end_id
|
||||
cache_sent = mini_sentence[sentenceEnd + 1 :]
|
||||
cache_sent_id = mini_sentence_id[sentenceEnd + 1 :]
|
||||
mini_sentence = mini_sentence[0 : sentenceEnd + 1]
|
||||
punctuations = punctuations[0 : sentenceEnd + 1]
|
||||
|
||||
# if len(punctuations) == 0:
|
||||
# continue
|
||||
|
||||
punctuations_np = punctuations.cpu().numpy()
|
||||
new_mini_sentence_punc += [int(x) for x in punctuations_np]
|
||||
words_with_punc = []
|
||||
for i in range(len(mini_sentence)):
|
||||
if (
|
||||
i == 0
|
||||
or self.punc_list[punctuations[i - 1]] == "。"
|
||||
or self.punc_list[punctuations[i - 1]] == "?"
|
||||
) and len(mini_sentence[i][0].encode()) == 1:
|
||||
mini_sentence[i] = mini_sentence[i].capitalize()
|
||||
if i == 0:
|
||||
if len(mini_sentence[i][0].encode()) == 1:
|
||||
mini_sentence[i] = " " + mini_sentence[i]
|
||||
if i > 0:
|
||||
if (
|
||||
len(mini_sentence[i][0].encode()) == 1
|
||||
and len(mini_sentence[i - 1][0].encode()) == 1
|
||||
):
|
||||
mini_sentence[i] = " " + mini_sentence[i]
|
||||
words_with_punc.append(mini_sentence[i])
|
||||
if self.punc_list[punctuations[i]] != "_":
|
||||
punc_res = self.punc_list[punctuations[i]]
|
||||
if len(mini_sentence[i][0].encode()) == 1:
|
||||
if punc_res == ",":
|
||||
punc_res = ","
|
||||
elif punc_res == "。":
|
||||
punc_res = "."
|
||||
elif punc_res == "?":
|
||||
punc_res = "?"
|
||||
words_with_punc.append(punc_res)
|
||||
new_mini_sentence += "".join(words_with_punc)
|
||||
# Add Period for the end of the sentence
|
||||
new_mini_sentence_out = new_mini_sentence
|
||||
new_mini_sentence_punc_out = new_mini_sentence_punc
|
||||
if mini_sentence_i == len(mini_sentences) - 1:
|
||||
if new_mini_sentence[-1] == "," or new_mini_sentence[-1] == "、":
|
||||
new_mini_sentence_out = new_mini_sentence[:-1] + "。"
|
||||
new_mini_sentence_punc_out = new_mini_sentence_punc[:-1] + [
|
||||
self.sentence_end_id
|
||||
]
|
||||
elif new_mini_sentence[-1] == ",":
|
||||
new_mini_sentence_out = new_mini_sentence[:-1] + "."
|
||||
new_mini_sentence_punc_out = new_mini_sentence_punc[:-1] + [
|
||||
self.sentence_end_id
|
||||
]
|
||||
elif (
|
||||
new_mini_sentence[-1] != "。"
|
||||
and new_mini_sentence[-1] != "?"
|
||||
and len(new_mini_sentence[-1].encode()) != 1
|
||||
):
|
||||
new_mini_sentence_out = new_mini_sentence + "。"
|
||||
new_mini_sentence_punc_out = new_mini_sentence_punc[:-1] + [
|
||||
self.sentence_end_id
|
||||
]
|
||||
if len(punctuations):
|
||||
punctuations[-1] = 2
|
||||
elif (
|
||||
new_mini_sentence[-1] != "."
|
||||
and new_mini_sentence[-1] != "?"
|
||||
and len(new_mini_sentence[-1].encode()) == 1
|
||||
):
|
||||
new_mini_sentence_out = new_mini_sentence + "."
|
||||
new_mini_sentence_punc_out = new_mini_sentence_punc[:-1] + [
|
||||
self.sentence_end_id
|
||||
]
|
||||
if len(punctuations):
|
||||
punctuations[-1] = 2
|
||||
# keep a punctuations array for punc segment
|
||||
if punc_array is None:
|
||||
punc_array = punctuations
|
||||
else:
|
||||
punc_array = torch.cat([punc_array, punctuations], dim=0)
|
||||
|
||||
# post processing when using word level punc model
|
||||
if self.jieba_usr_dict is not None:
|
||||
punc_array = punc_array.reshape(-1)
|
||||
len_tokens = len(tokens)
|
||||
new_punc_array = copy.copy(punc_array).tolist()
|
||||
# for i, (token, punc_id) in enumerate(zip(tokens[::-1], punc_array.tolist()[::-1])):
|
||||
for i, token in enumerate(tokens[::-1]):
|
||||
if "\u0e00" <= token[0] <= "\u9fa5": # ignore en words
|
||||
if len(token) > 1:
|
||||
num_append = len(token) - 1
|
||||
ind_append = len_tokens - i - 1
|
||||
for _ in range(num_append):
|
||||
new_punc_array.insert(ind_append, 1)
|
||||
punc_array = torch.tensor(new_punc_array)
|
||||
|
||||
result_i = {"key": key[0], "text": new_mini_sentence_out, "punc_array": punc_array}
|
||||
results.append(result_i)
|
||||
return results, meta_data
|
||||
|
||||
def export(self, **kwargs):
|
||||
|
||||
"""Export.
|
||||
|
||||
Args:
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
from .export_meta import export_rebuild_model
|
||||
|
||||
models = export_rebuild_model(model=self, **kwargs)
|
||||
return models
|
||||
@@ -0,0 +1,53 @@
|
||||
# This is an example that demonstrates how to configure a model file.
|
||||
# You can modify the configuration according to your own requirements.
|
||||
|
||||
# to print the register_table:
|
||||
# from funasr.register import tables
|
||||
# tables.print()
|
||||
|
||||
model: CTTransformer
|
||||
model_conf:
|
||||
ignore_id: 0
|
||||
embed_unit: 256
|
||||
att_unit: 256
|
||||
dropout_rate: 0.1
|
||||
punc_list:
|
||||
- <unk>
|
||||
- _
|
||||
- ','
|
||||
- 。
|
||||
- '?'
|
||||
- 、
|
||||
punc_weight:
|
||||
- 1.0
|
||||
- 1.0
|
||||
- 1.0
|
||||
- 1.0
|
||||
- 1.0
|
||||
- 1.0
|
||||
sentence_end_id: 3
|
||||
|
||||
encoder: SANMEncoder
|
||||
encoder_conf:
|
||||
input_size: 256
|
||||
output_size: 256
|
||||
attention_heads: 8
|
||||
linear_units: 1024
|
||||
num_blocks: 4
|
||||
dropout_rate: 0.1
|
||||
positional_dropout_rate: 0.1
|
||||
attention_dropout_rate: 0.0
|
||||
input_layer: pe
|
||||
pos_enc_class: SinusoidalPositionEncoder
|
||||
normalize_before: true
|
||||
kernel_size: 11
|
||||
sanm_shfit: 0
|
||||
selfattention_layer_type: sanm
|
||||
padding_idx: 0
|
||||
|
||||
tokenizer: CharTokenizer
|
||||
tokenizer_conf:
|
||||
unk_symbol: <unk>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
import re
|
||||
|
||||
|
||||
def split_to_mini_sentence(words: list, word_limit: int = 20):
|
||||
"""Split to mini sentence.
|
||||
|
||||
Args:
|
||||
words: TODO.
|
||||
word_limit: TODO.
|
||||
"""
|
||||
assert word_limit > 1
|
||||
if len(words) <= word_limit:
|
||||
return [words]
|
||||
sentences = []
|
||||
length = len(words)
|
||||
sentence_len = length // word_limit
|
||||
for i in range(sentence_len):
|
||||
sentences.append(words[i * word_limit : (i + 1) * word_limit])
|
||||
if length % word_limit > 0:
|
||||
sentences.append(words[sentence_len * word_limit :])
|
||||
return sentences
|
||||
|
||||
|
||||
def split_words(text: str, jieba_usr_dict=None, **kwargs):
|
||||
"""Split words.
|
||||
|
||||
Args:
|
||||
text: Text tensor or string input.
|
||||
jieba_usr_dict: TODO.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
if jieba_usr_dict:
|
||||
input_list = text.split()
|
||||
token_list_all = []
|
||||
langauge_list = []
|
||||
token_list_tmp = []
|
||||
language_flag = None
|
||||
for token in input_list:
|
||||
if isEnglish(token) and language_flag == "Chinese":
|
||||
token_list_all.append(token_list_tmp)
|
||||
langauge_list.append("Chinese")
|
||||
token_list_tmp = []
|
||||
elif not isEnglish(token) and language_flag == "English":
|
||||
token_list_all.append(token_list_tmp)
|
||||
langauge_list.append("English")
|
||||
token_list_tmp = []
|
||||
|
||||
token_list_tmp.append(token)
|
||||
|
||||
if isEnglish(token):
|
||||
language_flag = "English"
|
||||
else:
|
||||
language_flag = "Chinese"
|
||||
|
||||
if token_list_tmp:
|
||||
token_list_all.append(token_list_tmp)
|
||||
langauge_list.append(language_flag)
|
||||
|
||||
result_list = []
|
||||
for token_list_tmp, language_flag in zip(token_list_all, langauge_list):
|
||||
if language_flag == "English":
|
||||
result_list.extend(token_list_tmp)
|
||||
else:
|
||||
seg_list = jieba_usr_dict.cut(join_chinese_and_english(token_list_tmp), HMM=False)
|
||||
result_list.extend(seg_list)
|
||||
|
||||
return result_list
|
||||
|
||||
else:
|
||||
words = []
|
||||
segs = text.split()
|
||||
for seg in segs:
|
||||
# There is no space in seg.
|
||||
current_word = ""
|
||||
for c in seg:
|
||||
if len(c.encode()) == 1:
|
||||
# This is an ASCII char.
|
||||
current_word += c
|
||||
else:
|
||||
# This is a Chinese char.
|
||||
if len(current_word) > 0:
|
||||
words.append(current_word)
|
||||
current_word = ""
|
||||
words.append(c)
|
||||
if len(current_word) > 0:
|
||||
words.append(current_word)
|
||||
return words
|
||||
|
||||
|
||||
def isEnglish(text: str):
|
||||
"""Isenglish.
|
||||
|
||||
Args:
|
||||
text: Text tensor or string input.
|
||||
"""
|
||||
if re.search("^[a-zA-Z']+$", text):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def join_chinese_and_english(input_list):
|
||||
"""Join chinese and english.
|
||||
|
||||
Args:
|
||||
input_list: TODO.
|
||||
"""
|
||||
line = ""
|
||||
for token in input_list:
|
||||
if isEnglish(token):
|
||||
line = line + " " + token
|
||||
else:
|
||||
line = line + token
|
||||
|
||||
line = line.strip()
|
||||
return line
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
import torch
|
||||
from funasr.models.sanm.attention import MultiHeadedAttentionSANM
|
||||
|
||||
|
||||
class MultiHeadedAttentionSANMwithMask(MultiHeadedAttentionSANM):
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize MultiHeadedAttentionSANMwithMask.
|
||||
|
||||
Args:
|
||||
*args: Variable positional arguments.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def forward(self, x, mask, mask_shfit_chunk=None, mask_att_chunk_encoder=None):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
mask: TODO.
|
||||
mask_shfit_chunk: TODO.
|
||||
mask_att_chunk_encoder: TODO.
|
||||
"""
|
||||
q_h, k_h, v_h, v = self.forward_qkv(x)
|
||||
fsmn_memory = self.forward_fsmn(v, mask[0], mask_shfit_chunk)
|
||||
q_h = q_h * self.d_k ** (-0.5)
|
||||
scores = torch.matmul(q_h, k_h.transpose(-2, -1))
|
||||
att_outs = self.forward_attention(v_h, scores, mask[1], mask_att_chunk_encoder)
|
||||
return att_outs + fsmn_memory
|
||||
@@ -0,0 +1,561 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
import torch
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from funasr.register import tables
|
||||
from funasr.models.ctc.ctc import CTC
|
||||
from funasr.models.transformer.utils.repeat import repeat
|
||||
from funasr.models.transformer.layer_norm import LayerNorm
|
||||
from funasr.models.sanm.attention import MultiHeadedAttention
|
||||
from funasr.models.transformer.utils.nets_utils import make_pad_mask
|
||||
from funasr.models.transformer.utils.subsampling import check_short_utt
|
||||
from funasr.models.transformer.utils.subsampling import TooShortUttError
|
||||
from funasr.models.transformer.embedding import SinusoidalPositionEncoder
|
||||
from funasr.models.transformer.utils.multi_layer_conv import Conv1dLinear
|
||||
from funasr.models.transformer.utils.mask import subsequent_mask, vad_mask
|
||||
from funasr.models.transformer.utils.multi_layer_conv import MultiLayeredConv1d
|
||||
from funasr.models.transformer.positionwise_feed_forward import PositionwiseFeedForward
|
||||
from funasr.models.ct_transformer_streaming.attention import MultiHeadedAttentionSANMwithMask
|
||||
from funasr.models.transformer.utils.subsampling import (
|
||||
Conv2dSubsampling,
|
||||
Conv2dSubsampling2,
|
||||
Conv2dSubsampling6,
|
||||
Conv2dSubsampling8,
|
||||
)
|
||||
|
||||
|
||||
class EncoderLayerSANM(torch.nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
in_size,
|
||||
size,
|
||||
self_attn,
|
||||
feed_forward,
|
||||
dropout_rate,
|
||||
normalize_before=True,
|
||||
concat_after=False,
|
||||
stochastic_depth_rate=0.0,
|
||||
):
|
||||
"""Construct an EncoderLayer object."""
|
||||
super(EncoderLayerSANM, self).__init__()
|
||||
self.self_attn = self_attn
|
||||
self.feed_forward = feed_forward
|
||||
self.norm1 = LayerNorm(in_size)
|
||||
self.norm2 = LayerNorm(size)
|
||||
self.dropout = torch.nn.Dropout(dropout_rate)
|
||||
self.in_size = in_size
|
||||
self.size = size
|
||||
self.normalize_before = normalize_before
|
||||
self.concat_after = concat_after
|
||||
if self.concat_after:
|
||||
self.concat_linear = torch.nn.Linear(size + size, size)
|
||||
self.stochastic_depth_rate = stochastic_depth_rate
|
||||
self.dropout_rate = dropout_rate
|
||||
|
||||
def forward(self, x, mask, cache=None, mask_shfit_chunk=None, mask_att_chunk_encoder=None):
|
||||
"""Compute encoded features.
|
||||
|
||||
Args:
|
||||
x_input (torch.Tensor): Input tensor (#batch, time, size).
|
||||
mask (torch.Tensor): Mask tensor for the input (#batch, time).
|
||||
cache (torch.Tensor): Cache tensor of the input (#batch, time - 1, size).
|
||||
|
||||
Returns:
|
||||
torch.Tensor: Output tensor (#batch, time, size).
|
||||
torch.Tensor: Mask tensor (#batch, time).
|
||||
|
||||
"""
|
||||
skip_layer = False
|
||||
# with stochastic depth, residual connection `x + f(x)` becomes
|
||||
# `x <- x + 1 / (1 - p) * f(x)` at training time.
|
||||
stoch_layer_coeff = 1.0
|
||||
if self.training and self.stochastic_depth_rate > 0:
|
||||
skip_layer = torch.rand(1).item() < self.stochastic_depth_rate
|
||||
stoch_layer_coeff = 1.0 / (1 - self.stochastic_depth_rate)
|
||||
|
||||
if skip_layer:
|
||||
if cache is not None:
|
||||
x = torch.cat([cache, x], dim=1)
|
||||
return x, mask
|
||||
|
||||
residual = x
|
||||
if self.normalize_before:
|
||||
x = self.norm1(x)
|
||||
|
||||
if self.concat_after:
|
||||
x_concat = torch.cat(
|
||||
(
|
||||
x,
|
||||
self.self_attn(
|
||||
x,
|
||||
mask,
|
||||
mask_shfit_chunk=mask_shfit_chunk,
|
||||
mask_att_chunk_encoder=mask_att_chunk_encoder,
|
||||
),
|
||||
),
|
||||
dim=-1,
|
||||
)
|
||||
if self.in_size == self.size:
|
||||
x = residual + stoch_layer_coeff * self.concat_linear(x_concat)
|
||||
else:
|
||||
x = stoch_layer_coeff * self.concat_linear(x_concat)
|
||||
else:
|
||||
if self.in_size == self.size:
|
||||
x = residual + stoch_layer_coeff * self.dropout(
|
||||
self.self_attn(
|
||||
x,
|
||||
mask,
|
||||
mask_shfit_chunk=mask_shfit_chunk,
|
||||
mask_att_chunk_encoder=mask_att_chunk_encoder,
|
||||
)
|
||||
)
|
||||
else:
|
||||
x = stoch_layer_coeff * self.dropout(
|
||||
self.self_attn(
|
||||
x,
|
||||
mask,
|
||||
mask_shfit_chunk=mask_shfit_chunk,
|
||||
mask_att_chunk_encoder=mask_att_chunk_encoder,
|
||||
)
|
||||
)
|
||||
if not self.normalize_before:
|
||||
x = self.norm1(x)
|
||||
|
||||
residual = x
|
||||
if self.normalize_before:
|
||||
x = self.norm2(x)
|
||||
x = residual + stoch_layer_coeff * self.dropout(self.feed_forward(x))
|
||||
if not self.normalize_before:
|
||||
x = self.norm2(x)
|
||||
|
||||
return x, mask, cache, mask_shfit_chunk, mask_att_chunk_encoder
|
||||
|
||||
def forward_chunk(self, x, cache=None, chunk_size=None, look_back=0):
|
||||
"""Compute encoded features.
|
||||
|
||||
Args:
|
||||
x_input (torch.Tensor): Input tensor (#batch, time, size).
|
||||
mask (torch.Tensor): Mask tensor for the input (#batch, time).
|
||||
cache (torch.Tensor): Cache tensor of the input (#batch, time - 1, size).
|
||||
|
||||
Returns:
|
||||
torch.Tensor: Output tensor (#batch, time, size).
|
||||
torch.Tensor: Mask tensor (#batch, time).
|
||||
|
||||
"""
|
||||
|
||||
residual = x
|
||||
if self.normalize_before:
|
||||
x = self.norm1(x)
|
||||
|
||||
if self.in_size == self.size:
|
||||
attn, cache = self.self_attn.forward_chunk(x, cache, chunk_size, look_back)
|
||||
x = residual + attn
|
||||
else:
|
||||
x, cache = self.self_attn.forward_chunk(x, cache, chunk_size, look_back)
|
||||
|
||||
if not self.normalize_before:
|
||||
x = self.norm1(x)
|
||||
|
||||
residual = x
|
||||
if self.normalize_before:
|
||||
x = self.norm2(x)
|
||||
x = residual + self.feed_forward(x)
|
||||
if not self.normalize_before:
|
||||
x = self.norm2(x)
|
||||
|
||||
return x, cache
|
||||
|
||||
|
||||
@tables.register("encoder_classes", "SANMVadEncoder")
|
||||
class SANMVadEncoder(torch.nn.Module):
|
||||
"""
|
||||
Author: Speech Lab of DAMO Academy, Alibaba Group
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_size: int,
|
||||
output_size: int = 256,
|
||||
attention_heads: int = 4,
|
||||
linear_units: int = 2048,
|
||||
num_blocks: int = 6,
|
||||
dropout_rate: float = 0.1,
|
||||
positional_dropout_rate: float = 0.1,
|
||||
attention_dropout_rate: float = 0.0,
|
||||
input_layer: Optional[str] = "conv2d",
|
||||
pos_enc_class=SinusoidalPositionEncoder,
|
||||
normalize_before: bool = True,
|
||||
concat_after: bool = False,
|
||||
positionwise_layer_type: str = "linear",
|
||||
positionwise_conv_kernel_size: int = 1,
|
||||
padding_idx: int = -1,
|
||||
interctc_layer_idx: List[int] = [],
|
||||
interctc_use_conditioning: bool = False,
|
||||
kernel_size: int = 11,
|
||||
sanm_shfit: int = 0,
|
||||
selfattention_layer_type: str = "sanm",
|
||||
):
|
||||
"""Initialize SANMVadEncoder.
|
||||
|
||||
Args:
|
||||
input_size: Size/dimension parameter.
|
||||
output_size: Size/dimension parameter.
|
||||
attention_heads: TODO.
|
||||
linear_units: TODO.
|
||||
num_blocks: TODO.
|
||||
dropout_rate: TODO.
|
||||
positional_dropout_rate: TODO.
|
||||
attention_dropout_rate: TODO.
|
||||
input_layer: TODO.
|
||||
pos_enc_class: TODO.
|
||||
normalize_before: TODO.
|
||||
concat_after: TODO.
|
||||
positionwise_layer_type: TODO.
|
||||
positionwise_conv_kernel_size: Size/dimension parameter.
|
||||
padding_idx: TODO.
|
||||
interctc_layer_idx: TODO.
|
||||
interctc_use_conditioning: TODO.
|
||||
kernel_size: Size/dimension parameter.
|
||||
sanm_shfit: TODO.
|
||||
selfattention_layer_type: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
self._output_size = output_size
|
||||
|
||||
if input_layer == "linear":
|
||||
self.embed = torch.nn.Sequential(
|
||||
torch.nn.Linear(input_size, output_size),
|
||||
torch.nn.LayerNorm(output_size),
|
||||
torch.nn.Dropout(dropout_rate),
|
||||
torch.nn.ReLU(),
|
||||
pos_enc_class(output_size, positional_dropout_rate),
|
||||
)
|
||||
elif input_layer == "conv2d":
|
||||
self.embed = Conv2dSubsampling(input_size, output_size, dropout_rate)
|
||||
elif input_layer == "conv2d2":
|
||||
self.embed = Conv2dSubsampling2(input_size, output_size, dropout_rate)
|
||||
elif input_layer == "conv2d6":
|
||||
self.embed = Conv2dSubsampling6(input_size, output_size, dropout_rate)
|
||||
elif input_layer == "conv2d8":
|
||||
self.embed = Conv2dSubsampling8(input_size, output_size, dropout_rate)
|
||||
elif input_layer == "embed":
|
||||
self.embed = torch.nn.Sequential(
|
||||
torch.nn.Embedding(input_size, output_size, padding_idx=padding_idx),
|
||||
SinusoidalPositionEncoder(),
|
||||
)
|
||||
elif input_layer is None:
|
||||
if input_size == output_size:
|
||||
self.embed = None
|
||||
else:
|
||||
self.embed = torch.nn.Linear(input_size, output_size)
|
||||
elif input_layer == "pe":
|
||||
self.embed = SinusoidalPositionEncoder()
|
||||
else:
|
||||
raise ValueError("unknown input_layer: " + input_layer)
|
||||
self.normalize_before = normalize_before
|
||||
if positionwise_layer_type == "linear":
|
||||
positionwise_layer = PositionwiseFeedForward
|
||||
positionwise_layer_args = (
|
||||
output_size,
|
||||
linear_units,
|
||||
dropout_rate,
|
||||
)
|
||||
elif positionwise_layer_type == "conv1d":
|
||||
positionwise_layer = MultiLayeredConv1d
|
||||
positionwise_layer_args = (
|
||||
output_size,
|
||||
linear_units,
|
||||
positionwise_conv_kernel_size,
|
||||
dropout_rate,
|
||||
)
|
||||
elif positionwise_layer_type == "conv1d-linear":
|
||||
positionwise_layer = Conv1dLinear
|
||||
positionwise_layer_args = (
|
||||
output_size,
|
||||
linear_units,
|
||||
positionwise_conv_kernel_size,
|
||||
dropout_rate,
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError("Support only linear or conv1d.")
|
||||
|
||||
if selfattention_layer_type == "selfattn":
|
||||
encoder_selfattn_layer = MultiHeadedAttention
|
||||
encoder_selfattn_layer_args = (
|
||||
attention_heads,
|
||||
output_size,
|
||||
attention_dropout_rate,
|
||||
)
|
||||
|
||||
elif selfattention_layer_type == "sanm":
|
||||
self.encoder_selfattn_layer = MultiHeadedAttentionSANMwithMask
|
||||
encoder_selfattn_layer_args0 = (
|
||||
attention_heads,
|
||||
input_size,
|
||||
output_size,
|
||||
attention_dropout_rate,
|
||||
kernel_size,
|
||||
sanm_shfit,
|
||||
)
|
||||
|
||||
encoder_selfattn_layer_args = (
|
||||
attention_heads,
|
||||
output_size,
|
||||
output_size,
|
||||
attention_dropout_rate,
|
||||
kernel_size,
|
||||
sanm_shfit,
|
||||
)
|
||||
|
||||
self.encoders0 = repeat(
|
||||
1,
|
||||
lambda lnum: EncoderLayerSANM(
|
||||
input_size,
|
||||
output_size,
|
||||
self.encoder_selfattn_layer(*encoder_selfattn_layer_args0),
|
||||
positionwise_layer(*positionwise_layer_args),
|
||||
dropout_rate,
|
||||
normalize_before,
|
||||
concat_after,
|
||||
),
|
||||
)
|
||||
|
||||
self.encoders = repeat(
|
||||
num_blocks - 1,
|
||||
lambda lnum: EncoderLayerSANM(
|
||||
output_size,
|
||||
output_size,
|
||||
self.encoder_selfattn_layer(*encoder_selfattn_layer_args),
|
||||
positionwise_layer(*positionwise_layer_args),
|
||||
dropout_rate,
|
||||
normalize_before,
|
||||
concat_after,
|
||||
),
|
||||
)
|
||||
if self.normalize_before:
|
||||
self.after_norm = LayerNorm(output_size)
|
||||
|
||||
self.interctc_layer_idx = interctc_layer_idx
|
||||
if len(interctc_layer_idx) > 0:
|
||||
assert 0 < min(interctc_layer_idx) and max(interctc_layer_idx) < num_blocks
|
||||
self.interctc_use_conditioning = interctc_use_conditioning
|
||||
self.conditioning_layer = None
|
||||
self.dropout = torch.nn.Dropout(dropout_rate)
|
||||
|
||||
def output_size(self) -> int:
|
||||
"""Output size."""
|
||||
return self._output_size
|
||||
|
||||
def forward(
|
||||
self,
|
||||
xs_pad: torch.Tensor,
|
||||
ilens: torch.Tensor,
|
||||
vad_indexes: torch.Tensor,
|
||||
prev_states: torch.Tensor = None,
|
||||
ctc: CTC = None,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor, Optional[torch.Tensor]]:
|
||||
"""Embed positions in tensor.
|
||||
|
||||
Args:
|
||||
xs_pad: input tensor (B, L, D)
|
||||
ilens: input length (B)
|
||||
prev_states: Not to be used now.
|
||||
Returns:
|
||||
position embedded tensor and mask
|
||||
"""
|
||||
masks = (~make_pad_mask(ilens)[:, None, :]).to(xs_pad.device)
|
||||
sub_masks = subsequent_mask(masks.size(-1), device=xs_pad.device).unsqueeze(0)
|
||||
no_future_masks = masks & sub_masks
|
||||
xs_pad *= self.output_size() ** 0.5
|
||||
if self.embed is None:
|
||||
xs_pad = xs_pad
|
||||
elif (
|
||||
isinstance(self.embed, Conv2dSubsampling)
|
||||
or isinstance(self.embed, Conv2dSubsampling2)
|
||||
or isinstance(self.embed, Conv2dSubsampling6)
|
||||
or isinstance(self.embed, Conv2dSubsampling8)
|
||||
):
|
||||
short_status, limit_size = check_short_utt(self.embed, xs_pad.size(1))
|
||||
if short_status:
|
||||
raise TooShortUttError(
|
||||
f"has {xs_pad.size(1)} frames and is too short for subsampling "
|
||||
+ f"(it needs more than {limit_size} frames), return empty results",
|
||||
xs_pad.size(1),
|
||||
limit_size,
|
||||
)
|
||||
xs_pad, masks = self.embed(xs_pad, masks)
|
||||
else:
|
||||
xs_pad = self.embed(xs_pad)
|
||||
|
||||
# xs_pad = self.dropout(xs_pad)
|
||||
mask_tup0 = [masks, no_future_masks]
|
||||
encoder_outs = self.encoders0(xs_pad, mask_tup0)
|
||||
xs_pad, _ = encoder_outs[0], encoder_outs[1]
|
||||
intermediate_outs = []
|
||||
|
||||
for layer_idx, encoder_layer in enumerate(self.encoders):
|
||||
if layer_idx + 1 == len(self.encoders):
|
||||
# This is last layer.
|
||||
coner_mask = torch.ones(
|
||||
masks.size(0),
|
||||
masks.size(-1),
|
||||
masks.size(-1),
|
||||
device=xs_pad.device,
|
||||
dtype=torch.bool,
|
||||
)
|
||||
for word_index, length in enumerate(ilens):
|
||||
coner_mask[word_index, :, :] = vad_mask(
|
||||
masks.size(-1), vad_indexes[word_index], device=xs_pad.device
|
||||
)
|
||||
layer_mask = masks & coner_mask
|
||||
else:
|
||||
layer_mask = no_future_masks
|
||||
mask_tup1 = [masks, layer_mask]
|
||||
encoder_outs = encoder_layer(xs_pad, mask_tup1)
|
||||
xs_pad, layer_mask = encoder_outs[0], encoder_outs[1]
|
||||
|
||||
if self.normalize_before:
|
||||
xs_pad = self.after_norm(xs_pad)
|
||||
|
||||
olens = masks.squeeze(1).sum(1)
|
||||
if len(intermediate_outs) > 0:
|
||||
return (xs_pad, intermediate_outs), olens, None
|
||||
return xs_pad, olens, None
|
||||
|
||||
|
||||
class EncoderLayerSANMExport(torch.nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
model,
|
||||
):
|
||||
"""Construct an EncoderLayer object."""
|
||||
super().__init__()
|
||||
self.self_attn = model.self_attn
|
||||
self.feed_forward = model.feed_forward
|
||||
self.norm1 = model.norm1
|
||||
self.norm2 = model.norm2
|
||||
self.in_size = model.in_size
|
||||
self.size = model.size
|
||||
|
||||
def forward(self, x, mask):
|
||||
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
mask: TODO.
|
||||
"""
|
||||
residual = x
|
||||
x = self.norm1(x)
|
||||
x = self.self_attn(x, mask)
|
||||
if self.in_size == self.size:
|
||||
x = x + residual
|
||||
residual = x
|
||||
x = self.norm2(x)
|
||||
x = self.feed_forward(x)
|
||||
x = x + residual
|
||||
|
||||
return x, mask
|
||||
|
||||
|
||||
@tables.register("encoder_classes", "SANMVadEncoderExport")
|
||||
class SANMVadEncoderExport(torch.nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
model,
|
||||
max_seq_len=512,
|
||||
feats_dim=560,
|
||||
model_name="encoder",
|
||||
onnx: bool = True,
|
||||
):
|
||||
"""Initialize SANMVadEncoderExport.
|
||||
|
||||
Args:
|
||||
model: Model instance or model name.
|
||||
max_seq_len: TODO.
|
||||
feats_dim: Size/dimension parameter.
|
||||
model_name: TODO.
|
||||
onnx: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
self.embed = model.embed
|
||||
self.model = model
|
||||
self._output_size = model._output_size
|
||||
|
||||
from funasr.utils.torch_function import sequence_mask
|
||||
|
||||
self.make_pad_mask = sequence_mask(max_seq_len, flip=False)
|
||||
|
||||
from funasr.models.sanm.attention import MultiHeadedAttentionSANMExport
|
||||
|
||||
if hasattr(model, "encoders0"):
|
||||
for i, d in enumerate(self.model.encoders0):
|
||||
if isinstance(d.self_attn, MultiHeadedAttentionSANMwithMask):
|
||||
d.self_attn = MultiHeadedAttentionSANMExport(d.self_attn)
|
||||
self.model.encoders0[i] = EncoderLayerSANMExport(d)
|
||||
|
||||
for i, d in enumerate(self.model.encoders):
|
||||
if isinstance(d.self_attn, MultiHeadedAttentionSANMwithMask):
|
||||
d.self_attn = MultiHeadedAttentionSANMExport(d.self_attn)
|
||||
self.model.encoders[i] = EncoderLayerSANMExport(d)
|
||||
|
||||
def prepare_mask(self, mask, sub_masks):
|
||||
"""Prepare mask.
|
||||
|
||||
Args:
|
||||
mask: TODO.
|
||||
sub_masks: TODO.
|
||||
"""
|
||||
mask_3d_btd = mask[:, :, None]
|
||||
mask_4d_bhlt = (1 - sub_masks) * -10000.0
|
||||
|
||||
return mask_3d_btd, mask_4d_bhlt
|
||||
|
||||
def forward(
|
||||
self,
|
||||
speech: torch.Tensor,
|
||||
speech_lengths: torch.Tensor,
|
||||
vad_masks: torch.Tensor,
|
||||
sub_masks: torch.Tensor,
|
||||
):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
speech: Speech audio tensor, shape (batch, time).
|
||||
speech_lengths: Length of each speech sample.
|
||||
vad_masks: TODO.
|
||||
sub_masks: TODO.
|
||||
"""
|
||||
speech = speech * self._output_size**0.5
|
||||
mask = self.make_pad_mask(speech_lengths)
|
||||
vad_masks = self.prepare_mask(mask, vad_masks)
|
||||
mask = self.prepare_mask(mask, sub_masks)
|
||||
|
||||
if self.embed is None:
|
||||
xs_pad = speech
|
||||
else:
|
||||
xs_pad = self.embed(speech)
|
||||
|
||||
encoder_outs = self.model.encoders0(xs_pad, mask)
|
||||
xs_pad, masks = encoder_outs[0], encoder_outs[1]
|
||||
|
||||
# encoder_outs = self.model.encoders(xs_pad, mask)
|
||||
for layer_idx, encoder_layer in enumerate(self.model.encoders):
|
||||
if layer_idx == len(self.model.encoders) - 1:
|
||||
mask = vad_masks
|
||||
encoder_outs = encoder_layer(xs_pad, mask)
|
||||
xs_pad, masks = encoder_outs[0], encoder_outs[1]
|
||||
|
||||
xs_pad = self.model.after_norm(xs_pad)
|
||||
|
||||
return xs_pad, speech_lengths
|
||||
|
||||
def get_output_size(self):
|
||||
"""Get output size."""
|
||||
return self.model.encoders[0].size
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
import types
|
||||
import torch
|
||||
from funasr.register import tables
|
||||
|
||||
|
||||
def export_rebuild_model(model, **kwargs):
|
||||
|
||||
"""Export rebuild model.
|
||||
|
||||
Args:
|
||||
model: Model instance or model name.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
is_onnx = kwargs.get("type", "onnx") == "onnx"
|
||||
encoder_class = tables.encoder_classes.get(kwargs["encoder"] + "Export")
|
||||
model.encoder = encoder_class(model.encoder, onnx=is_onnx)
|
||||
|
||||
model.forward = types.MethodType(export_forward, model)
|
||||
model.export_dummy_inputs = types.MethodType(export_dummy_inputs, model)
|
||||
model.export_input_names = types.MethodType(export_input_names, model)
|
||||
model.export_output_names = types.MethodType(export_output_names, model)
|
||||
model.export_dynamic_axes = types.MethodType(export_dynamic_axes, model)
|
||||
model.export_name = types.MethodType(export_name, model)
|
||||
|
||||
return model
|
||||
|
||||
|
||||
def export_forward(
|
||||
self,
|
||||
inputs: torch.Tensor,
|
||||
text_lengths: torch.Tensor,
|
||||
vad_indexes: torch.Tensor,
|
||||
sub_masks: torch.Tensor,
|
||||
):
|
||||
"""Compute loss value from buffer sequences.
|
||||
|
||||
Args:
|
||||
input (torch.Tensor): Input ids. (batch, len)
|
||||
hidden (torch.Tensor): Target ids. (batch, len)
|
||||
|
||||
"""
|
||||
x = self.embed(inputs)
|
||||
# mask = self._target_mask(input)
|
||||
h, _ = self.encoder(x, text_lengths, vad_indexes, sub_masks)
|
||||
y = self.decoder(h)
|
||||
return y
|
||||
|
||||
|
||||
def export_dummy_inputs(self):
|
||||
"""Export dummy inputs."""
|
||||
length = 120
|
||||
text_indexes = torch.randint(0, self.embed.num_embeddings, (1, length)).type(torch.int32)
|
||||
text_lengths = torch.tensor([length], dtype=torch.int32)
|
||||
vad_mask = torch.ones(length, length, dtype=torch.float32)[None, None, :, :]
|
||||
sub_masks = torch.ones(length, length, dtype=torch.float32)
|
||||
sub_masks = torch.tril(sub_masks).type(torch.float32)
|
||||
return (text_indexes, text_lengths, vad_mask, sub_masks[None, None, :, :])
|
||||
|
||||
|
||||
def export_input_names(self):
|
||||
"""Export input names."""
|
||||
return ["inputs", "text_lengths", "vad_masks", "sub_masks"]
|
||||
|
||||
|
||||
def export_output_names(self):
|
||||
"""Export output names."""
|
||||
return ["logits"]
|
||||
|
||||
|
||||
def export_dynamic_axes(self):
|
||||
"""Export dynamic axes."""
|
||||
return {
|
||||
"inputs": {1: "feats_length"},
|
||||
"vad_masks": {2: "feats_length1", 3: "feats_length2"},
|
||||
"sub_masks": {2: "feats_length1", 3: "feats_length2"},
|
||||
"logits": {1: "logits_length"},
|
||||
}
|
||||
|
||||
|
||||
def export_name(self):
|
||||
"""Export name."""
|
||||
return "model.onnx"
|
||||
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
import torch
|
||||
import numpy as np
|
||||
from contextlib import contextmanager
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from funasr.register import tables
|
||||
from funasr.train_utils.device_funcs import to_device
|
||||
from funasr.models.ct_transformer.model import CTTransformer
|
||||
from funasr.utils.load_utils import load_audio_text_image_video
|
||||
from funasr.models.ct_transformer.utils import split_to_mini_sentence, split_words
|
||||
|
||||
|
||||
if LooseVersion(torch.__version__) >= LooseVersion("1.6.0"):
|
||||
from torch.cuda.amp import autocast
|
||||
else:
|
||||
# Nothing to do if torch<1.6.0
|
||||
@contextmanager
|
||||
def autocast(enabled=True):
|
||||
"""Autocast.
|
||||
|
||||
Args:
|
||||
enabled: TODO.
|
||||
"""
|
||||
yield
|
||||
|
||||
|
||||
@tables.register("model_classes", "CTTransformerStreaming")
|
||||
class CTTransformerStreaming(CTTransformer):
|
||||
"""CT-Transformer Streaming: Online punctuation restoration.
|
||||
|
||||
Processes text incrementally with a sliding window, maintaining cache
|
||||
of previous context for consistent punctuation decisions across chunks.
|
||||
Used as punc_model in streaming ASR pipelines.
|
||||
|
||||
Supports VAD-aware punctuation: uses VAD boundaries to improve sentence segmentation.
|
||||
|
||||
Reference: https://arxiv.org/pdf/2003.01309.pdf
|
||||
Output: {"key": str, "text": str, "punc_array": Tensor}
|
||||
|
||||
Author: Speech Lab of DAMO Academy, Alibaba Group
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize CTTransformerStreaming.
|
||||
|
||||
Args:
|
||||
*args: Variable positional arguments.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def punc_forward(
|
||||
self, text: torch.Tensor, text_lengths: torch.Tensor, vad_indexes: torch.Tensor, **kwargs
|
||||
):
|
||||
"""Compute loss value from buffer sequences.
|
||||
|
||||
Args:
|
||||
input (torch.Tensor): Input ids. (batch, len)
|
||||
hidden (torch.Tensor): Target ids. (batch, len)
|
||||
|
||||
"""
|
||||
x = self.embed(text)
|
||||
# mask = self._target_mask(input)
|
||||
h, _, _ = self.encoder(x, text_lengths, vad_indexes=vad_indexes)
|
||||
y = self.decoder(h)
|
||||
return y, None
|
||||
|
||||
def with_vad(self):
|
||||
"""With vad."""
|
||||
return True
|
||||
|
||||
def inference(
|
||||
self,
|
||||
data_in,
|
||||
data_lengths=None,
|
||||
key: list = None,
|
||||
tokenizer=None,
|
||||
frontend=None,
|
||||
cache: dict = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Run inference on input data.
|
||||
|
||||
Args:
|
||||
data_in: Input data (audio samples, file paths, or text).
|
||||
data_lengths: Lengths of each input sample in the batch.
|
||||
key: Sample identifiers.
|
||||
tokenizer: Tokenizer instance for text encoding/decoding.
|
||||
frontend: Audio frontend for feature extraction.
|
||||
cache: State cache dict for streaming inference.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
if cache is None:
|
||||
cache = {}
|
||||
assert len(data_in) == 1
|
||||
|
||||
if len(cache) == 0:
|
||||
cache["pre_text"] = []
|
||||
text = load_audio_text_image_video(data_in, data_type=kwargs.get("kwargs", "text"))[0]
|
||||
text = "".join(cache["pre_text"]) + " " + text
|
||||
|
||||
split_size = kwargs.get("split_size", 20)
|
||||
|
||||
tokens = split_words(text)
|
||||
tokens_int = tokenizer.encode(tokens)
|
||||
|
||||
mini_sentences = split_to_mini_sentence(tokens, split_size)
|
||||
mini_sentences_id = split_to_mini_sentence(tokens_int, split_size)
|
||||
assert len(mini_sentences) == len(mini_sentences_id)
|
||||
cache_sent = []
|
||||
cache_sent_id = torch.from_numpy(np.array([], dtype="int32"))
|
||||
skip_num = 0
|
||||
sentence_punc_list = []
|
||||
sentence_words_list = []
|
||||
cache_pop_trigger_limit = 200
|
||||
results = []
|
||||
meta_data = {}
|
||||
punc_array = None
|
||||
for mini_sentence_i in range(len(mini_sentences)):
|
||||
mini_sentence = mini_sentences[mini_sentence_i]
|
||||
mini_sentence_id = mini_sentences_id[mini_sentence_i]
|
||||
mini_sentence = cache_sent + mini_sentence
|
||||
mini_sentence_id = np.concatenate((cache_sent_id, mini_sentence_id), axis=0)
|
||||
data = {
|
||||
"text": torch.unsqueeze(torch.from_numpy(mini_sentence_id), 0),
|
||||
"text_lengths": torch.from_numpy(np.array([len(mini_sentence_id)], dtype="int32")),
|
||||
"vad_indexes": torch.from_numpy(np.array([len(cache["pre_text"])], dtype="int32")),
|
||||
}
|
||||
data = to_device(data, kwargs["device"])
|
||||
# y, _ = self.wrapped_model(**data)
|
||||
y, _ = self.punc_forward(**data)
|
||||
_, indices = y.view(-1, y.shape[-1]).topk(1, dim=1)
|
||||
punctuations = indices
|
||||
if indices.size()[0] != 1:
|
||||
punctuations = torch.squeeze(indices)
|
||||
assert punctuations.size()[0] == len(mini_sentence)
|
||||
|
||||
# Search for the last Period/QuestionMark as cache
|
||||
if mini_sentence_i < len(mini_sentences) - 1:
|
||||
sentenceEnd = -1
|
||||
last_comma_index = -1
|
||||
for i in range(len(punctuations) - 2, 1, -1):
|
||||
if (
|
||||
self.punc_list[punctuations[i]] == "。"
|
||||
or self.punc_list[punctuations[i]] == "?"
|
||||
):
|
||||
sentenceEnd = i
|
||||
break
|
||||
if last_comma_index < 0 and self.punc_list[punctuations[i]] == ",":
|
||||
last_comma_index = i
|
||||
|
||||
if (
|
||||
sentenceEnd < 0
|
||||
and len(mini_sentence) > cache_pop_trigger_limit
|
||||
and last_comma_index >= 0
|
||||
):
|
||||
# The sentence it too long, cut off at a comma.
|
||||
sentenceEnd = last_comma_index
|
||||
punctuations[sentenceEnd] = self.sentence_end_id
|
||||
cache_sent = mini_sentence[sentenceEnd + 1 :]
|
||||
cache_sent_id = mini_sentence_id[sentenceEnd + 1 :]
|
||||
mini_sentence = mini_sentence[0 : sentenceEnd + 1]
|
||||
punctuations = punctuations[0 : sentenceEnd + 1]
|
||||
|
||||
# if len(punctuations) == 0:
|
||||
# continue
|
||||
|
||||
punctuations_np = punctuations.cpu().numpy()
|
||||
sentence_punc_list += [self.punc_list[int(x)] for x in punctuations_np]
|
||||
sentence_words_list += mini_sentence
|
||||
|
||||
assert len(sentence_punc_list) == len(sentence_words_list)
|
||||
words_with_punc = []
|
||||
sentence_punc_list_out = []
|
||||
for i in range(0, len(sentence_words_list)):
|
||||
if i > 0:
|
||||
if (
|
||||
len(sentence_words_list[i][0].encode()) == 1
|
||||
and len(sentence_words_list[i - 1][-1].encode()) == 1
|
||||
):
|
||||
sentence_words_list[i] = " " + sentence_words_list[i]
|
||||
if skip_num < len(cache["pre_text"]):
|
||||
skip_num += 1
|
||||
else:
|
||||
words_with_punc.append(sentence_words_list[i])
|
||||
if skip_num >= len(cache["pre_text"]):
|
||||
sentence_punc_list_out.append(sentence_punc_list[i])
|
||||
if sentence_punc_list[i] != "_":
|
||||
words_with_punc.append(sentence_punc_list[i])
|
||||
sentence_out = "".join(words_with_punc)
|
||||
|
||||
sentenceEnd = -1
|
||||
for i in range(len(sentence_punc_list) - 2, 1, -1):
|
||||
if sentence_punc_list[i] == "。" or sentence_punc_list[i] == "?":
|
||||
sentenceEnd = i
|
||||
break
|
||||
cache["pre_text"] = sentence_words_list[sentenceEnd + 1 :]
|
||||
if sentence_out[-1] in self.punc_list:
|
||||
sentence_out = sentence_out[:-1]
|
||||
sentence_punc_list_out[-1] = "_"
|
||||
# keep a punctuations array for punc segment
|
||||
if punc_array is None:
|
||||
punc_array = punctuations
|
||||
else:
|
||||
punc_array = torch.cat([punc_array, punctuations], dim=0)
|
||||
|
||||
result_i = {"key": key[0], "text": sentence_out, "punc_array": punc_array}
|
||||
results.append(result_i)
|
||||
|
||||
return results, meta_data
|
||||
|
||||
def export(self, **kwargs):
|
||||
|
||||
"""Export.
|
||||
|
||||
Args:
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
from .export_meta import export_rebuild_model
|
||||
|
||||
models = export_rebuild_model(model=self, **kwargs)
|
||||
return models
|
||||
@@ -0,0 +1,50 @@
|
||||
# This is an example that demonstrates how to configure a model file.
|
||||
# You can modify the configuration according to your own requirements.
|
||||
|
||||
# to print the register_table:
|
||||
# from funasr.register import tables
|
||||
# tables.print()
|
||||
|
||||
model: CTTransformerStreaming
|
||||
model_conf:
|
||||
ignore_id: 0
|
||||
embed_unit: 256
|
||||
att_unit: 256
|
||||
dropout_rate: 0.1
|
||||
punc_list:
|
||||
- <unk>
|
||||
- _
|
||||
- ,
|
||||
- 。
|
||||
- ?
|
||||
- 、
|
||||
punc_weight:
|
||||
- 1.0
|
||||
- 1.0
|
||||
- 1.0
|
||||
- 1.0
|
||||
- 1.0
|
||||
- 1.0
|
||||
sentence_end_id: 3
|
||||
|
||||
encoder: SANMVadEncoder
|
||||
encoder_conf:
|
||||
input_size: 256
|
||||
output_size: 256
|
||||
attention_heads: 8
|
||||
linear_units: 1024
|
||||
num_blocks: 3
|
||||
dropout_rate: 0.1
|
||||
positional_dropout_rate: 0.1
|
||||
attention_dropout_rate: 0.0
|
||||
input_layer: pe
|
||||
pos_enc_class: SinusoidalPositionEncoder
|
||||
normalize_before: true
|
||||
kernel_size: 11
|
||||
sanm_shfit: 5
|
||||
selfattention_layer_type: sanm
|
||||
padding_idx: 0
|
||||
|
||||
tokenizer: CharTokenizer
|
||||
tokenizer_conf:
|
||||
unk_symbol: <unk>
|
||||
@@ -0,0 +1,216 @@
|
||||
import logging
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
class CTC(torch.nn.Module):
|
||||
"""CTC module.
|
||||
|
||||
Args:
|
||||
odim: dimension of outputs
|
||||
encoder_output_size: number of encoder projection units
|
||||
dropout_rate: dropout rate (0.0 ~ 1.0)
|
||||
ctc_type: builtin or warpctc
|
||||
reduce: reduce the CTC loss into a scalar
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
odim: int,
|
||||
encoder_output_size: int,
|
||||
dropout_rate: float = 0.0,
|
||||
ctc_type: str = "builtin",
|
||||
reduce: bool = True,
|
||||
ignore_nan_grad: bool = True,
|
||||
extra_linear: bool = True,
|
||||
):
|
||||
"""Initialize CTC.
|
||||
|
||||
Args:
|
||||
odim: TODO.
|
||||
encoder_output_size: Size/dimension parameter.
|
||||
dropout_rate: TODO.
|
||||
ctc_type: TODO.
|
||||
reduce: TODO.
|
||||
ignore_nan_grad: TODO.
|
||||
extra_linear: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
eprojs = encoder_output_size
|
||||
self.dropout_rate = dropout_rate
|
||||
|
||||
if extra_linear:
|
||||
self.ctc_lo = torch.nn.Linear(eprojs, odim)
|
||||
else:
|
||||
self.ctc_lo = None
|
||||
|
||||
self.ctc_type = ctc_type
|
||||
self.ignore_nan_grad = ignore_nan_grad
|
||||
|
||||
if self.ctc_type == "builtin":
|
||||
self.ctc_loss = torch.nn.CTCLoss(reduction="none")
|
||||
elif self.ctc_type == "warpctc":
|
||||
import warpctc_pytorch as warp_ctc
|
||||
|
||||
if ignore_nan_grad:
|
||||
logging.warning("ignore_nan_grad option is not supported for warp_ctc")
|
||||
self.ctc_loss = warp_ctc.CTCLoss(size_average=True, reduce=reduce)
|
||||
else:
|
||||
raise ValueError(f'ctc_type must be "builtin" or "warpctc": {self.ctc_type}')
|
||||
|
||||
self.reduce = reduce
|
||||
|
||||
def loss_fn(self, th_pred, th_target, th_ilen, th_olen) -> torch.Tensor:
|
||||
"""Loss fn.
|
||||
|
||||
Args:
|
||||
th_pred: TODO.
|
||||
th_target: TODO.
|
||||
th_ilen: TODO.
|
||||
th_olen: TODO.
|
||||
"""
|
||||
if self.ctc_type == "builtin":
|
||||
th_pred = th_pred.log_softmax(2)
|
||||
loss = self.ctc_loss(th_pred, th_target, th_ilen, th_olen)
|
||||
|
||||
if loss.requires_grad and self.ignore_nan_grad:
|
||||
# ctc_grad: (L, B, O)
|
||||
ctc_grad = loss.grad_fn(torch.ones_like(loss))
|
||||
ctc_grad = ctc_grad.sum([0, 2])
|
||||
indices = torch.isfinite(ctc_grad)
|
||||
size = indices.long().sum()
|
||||
if size == 0:
|
||||
# Return as is
|
||||
logging.warning(
|
||||
"All samples in this mini-batch got nan grad."
|
||||
" Returning nan value instead of CTC loss"
|
||||
)
|
||||
elif size != th_pred.size(1):
|
||||
logging.warning(
|
||||
f"{th_pred.size(1) - size}/{th_pred.size(1)}"
|
||||
" samples got nan grad."
|
||||
" These were ignored for CTC loss."
|
||||
)
|
||||
|
||||
# Create mask for target
|
||||
target_mask = torch.full(
|
||||
[th_target.size(0)],
|
||||
1,
|
||||
dtype=torch.bool,
|
||||
device=th_target.device,
|
||||
)
|
||||
s = 0
|
||||
for ind, le in enumerate(th_olen):
|
||||
if not indices[ind]:
|
||||
target_mask[s : s + le] = 0
|
||||
s += le
|
||||
|
||||
# Calc loss again using maksed data
|
||||
loss = self.ctc_loss(
|
||||
th_pred[:, indices, :],
|
||||
th_target[target_mask],
|
||||
th_ilen[indices],
|
||||
th_olen[indices],
|
||||
)
|
||||
else:
|
||||
size = th_pred.size(1)
|
||||
|
||||
if self.reduce:
|
||||
# Batch-size average
|
||||
loss = loss.sum() / size
|
||||
else:
|
||||
loss = loss / size
|
||||
return loss
|
||||
|
||||
elif self.ctc_type == "warpctc":
|
||||
# warpctc only supports float32
|
||||
th_pred = th_pred.to(dtype=torch.float32)
|
||||
|
||||
th_target = th_target.cpu().int()
|
||||
th_ilen = th_ilen.cpu().int()
|
||||
th_olen = th_olen.cpu().int()
|
||||
loss = self.ctc_loss(th_pred, th_target, th_ilen, th_olen)
|
||||
if self.reduce:
|
||||
# NOTE: sum() is needed to keep consistency since warpctc
|
||||
# return as tensor w/ shape (1,)
|
||||
# but builtin return as tensor w/o shape (scalar).
|
||||
loss = loss.sum()
|
||||
return loss
|
||||
|
||||
elif self.ctc_type == "gtnctc":
|
||||
log_probs = torch.nn.functional.log_softmax(th_pred, dim=2)
|
||||
return self.ctc_loss(log_probs, th_target, th_ilen, 0, "none")
|
||||
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
def forward(self, hs_pad, hlens, ys_pad, ys_lens):
|
||||
"""Calculate CTC loss.
|
||||
|
||||
Args:
|
||||
hs_pad: batch of padded hidden state sequences (B, Tmax, D)
|
||||
hlens: batch of lengths of hidden state sequences (B)
|
||||
ys_pad: batch of padded character id sequence tensor (B, Lmax)
|
||||
ys_lens: batch of lengths of character sequence (B)
|
||||
"""
|
||||
# hs_pad: (B, L, NProj) -> ys_hat: (B, L, Nvocab)
|
||||
if self.ctc_lo is not None:
|
||||
ys_hat = self.ctc_lo(F.dropout(hs_pad, p=self.dropout_rate))
|
||||
else:
|
||||
ys_hat = hs_pad
|
||||
|
||||
if self.ctc_type == "gtnctc":
|
||||
# gtn expects list form for ys
|
||||
ys_true = [y[y != -1] for y in ys_pad] # parse padded ys
|
||||
else:
|
||||
# ys_hat: (B, L, D) -> (L, B, D)
|
||||
ys_hat = ys_hat.transpose(0, 1)
|
||||
# (B, L) -> (BxL,)
|
||||
ys_true = torch.cat([ys_pad[i, :l] for i, l in enumerate(ys_lens)])
|
||||
|
||||
hlens = hlens.to(hs_pad.device)
|
||||
loss = self.loss_fn(ys_hat, ys_true, hlens, ys_lens).to(
|
||||
device=hs_pad.device, dtype=hs_pad.dtype
|
||||
)
|
||||
|
||||
return loss
|
||||
|
||||
def softmax(self, hs_pad):
|
||||
"""softmax of frame activations
|
||||
|
||||
Args:
|
||||
Tensor hs_pad: 3d tensor (B, Tmax, eprojs)
|
||||
Returns:
|
||||
torch.Tensor: softmax applied 3d tensor (B, Tmax, odim)
|
||||
"""
|
||||
if self.ctc_lo is not None:
|
||||
return F.softmax(self.ctc_lo(hs_pad), dim=2)
|
||||
else:
|
||||
return F.softmax(hs_pad, dim=2)
|
||||
|
||||
def log_softmax(self, hs_pad):
|
||||
"""log_softmax of frame activations
|
||||
|
||||
Args:
|
||||
Tensor hs_pad: 3d tensor (B, Tmax, eprojs)
|
||||
Returns:
|
||||
torch.Tensor: log softmax applied 3d tensor (B, Tmax, odim)
|
||||
"""
|
||||
if self.ctc_lo is not None:
|
||||
return F.log_softmax(self.ctc_lo(hs_pad), dim=2)
|
||||
else:
|
||||
return F.log_softmax(hs_pad, dim=2)
|
||||
|
||||
def argmax(self, hs_pad):
|
||||
"""argmax of frame activations
|
||||
|
||||
Args:
|
||||
torch.Tensor hs_pad: 3d tensor (B, Tmax, eprojs)
|
||||
Returns:
|
||||
torch.Tensor: argmax applied 2d tensor (B, Tmax)
|
||||
"""
|
||||
if self.ctc_lo is not None:
|
||||
return torch.argmax(self.ctc_lo(hs_pad), dim=2)
|
||||
else:
|
||||
return torch.argmax(hs_pad, dim=2)
|
||||
@@ -0,0 +1,299 @@
|
||||
import logging
|
||||
from typing import Union, Dict, List, Tuple, Optional
|
||||
|
||||
import time
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from funasr.models.ctc.ctc import CTC
|
||||
from funasr.train_utils.device_funcs import force_gatherable
|
||||
from funasr.utils.load_utils import load_audio_text_image_video, extract_fbank
|
||||
from funasr.utils import postprocess_utils
|
||||
from funasr.utils.datadir_writer import DatadirWriter
|
||||
from funasr.register import tables
|
||||
from funasr.models.paraformer.search import Hypothesis
|
||||
|
||||
|
||||
@tables.register("model_classes", "CTC")
|
||||
class Transformer(nn.Module):
|
||||
"""CTC-attention hybrid Encoder-Decoder model"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
specaug: str = None,
|
||||
specaug_conf: dict = None,
|
||||
normalize: str = None,
|
||||
normalize_conf: dict = None,
|
||||
encoder: str = None,
|
||||
encoder_conf: dict = None,
|
||||
ctc_conf: dict = None,
|
||||
input_size: int = 80,
|
||||
vocab_size: int = -1,
|
||||
ignore_id: int = -1,
|
||||
blank_id: int = 0,
|
||||
sos: int = 1,
|
||||
eos: int = 2,
|
||||
length_normalized_loss: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
|
||||
"""Initialize Transformer.
|
||||
|
||||
Args:
|
||||
specaug: TODO.
|
||||
specaug_conf: Configuration dict for specaug.
|
||||
normalize: TODO.
|
||||
normalize_conf: Configuration dict for normalize.
|
||||
encoder: TODO.
|
||||
encoder_conf: Configuration dict for encoder.
|
||||
ctc_conf: Configuration dict for ctc.
|
||||
input_size: Size/dimension parameter.
|
||||
vocab_size: Size/dimension parameter.
|
||||
ignore_id: TODO.
|
||||
blank_id: TODO.
|
||||
sos: TODO.
|
||||
eos: TODO.
|
||||
length_normalized_loss: TODO.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
if specaug is not None:
|
||||
specaug_class = tables.specaug_classes.get(specaug)
|
||||
specaug = specaug_class(**specaug_conf)
|
||||
if normalize is not None:
|
||||
normalize_class = tables.normalize_classes.get(normalize)
|
||||
normalize = normalize_class(**normalize_conf)
|
||||
encoder_class = tables.encoder_classes.get(encoder)
|
||||
encoder = encoder_class(input_size=input_size, **encoder_conf)
|
||||
encoder_output_size = encoder.output_size()
|
||||
|
||||
if ctc_conf is None:
|
||||
ctc_conf = {}
|
||||
ctc = CTC(odim=vocab_size, encoder_output_size=encoder_output_size, **ctc_conf)
|
||||
|
||||
self.blank_id = blank_id
|
||||
self.sos = sos if sos is not None else vocab_size - 1
|
||||
self.eos = eos if eos is not None else vocab_size - 1
|
||||
self.vocab_size = vocab_size
|
||||
self.ignore_id = ignore_id
|
||||
self.specaug = specaug
|
||||
self.normalize = normalize
|
||||
self.encoder = encoder
|
||||
self.error_calculator = None
|
||||
|
||||
self.ctc = ctc
|
||||
|
||||
self.length_normalized_loss = length_normalized_loss
|
||||
|
||||
def forward(
|
||||
self,
|
||||
speech: torch.Tensor,
|
||||
speech_lengths: torch.Tensor,
|
||||
text: torch.Tensor,
|
||||
text_lengths: torch.Tensor,
|
||||
**kwargs,
|
||||
) -> Tuple[torch.Tensor, Dict[str, torch.Tensor], torch.Tensor]:
|
||||
"""Encoder + Decoder + Calc loss
|
||||
Args:
|
||||
speech: (Batch, Length, ...)
|
||||
speech_lengths: (Batch, )
|
||||
text: (Batch, Length)
|
||||
text_lengths: (Batch,)
|
||||
"""
|
||||
# import pdb;
|
||||
# pdb.set_trace()
|
||||
if len(text_lengths.size()) > 1:
|
||||
text_lengths = text_lengths[:, 0]
|
||||
if len(speech_lengths.size()) > 1:
|
||||
speech_lengths = speech_lengths[:, 0]
|
||||
|
||||
batch_size = speech.shape[0]
|
||||
|
||||
# 1. Encoder
|
||||
encoder_out, encoder_out_lens = self.encode(speech, speech_lengths)
|
||||
|
||||
loss_ctc, cer_ctc = None, None
|
||||
stats = dict()
|
||||
|
||||
loss_ctc, cer_ctc = self._calc_ctc_loss(
|
||||
encoder_out, encoder_out_lens, text, text_lengths
|
||||
)
|
||||
|
||||
loss = loss_ctc
|
||||
|
||||
# Collect total loss stats
|
||||
stats["loss"] = torch.clone(loss.detach())
|
||||
|
||||
# force_gatherable: to-device and to-tensor if scalar for DataParallel
|
||||
if self.length_normalized_loss:
|
||||
batch_size = int((text_lengths + 1).sum())
|
||||
loss, stats, weight = force_gatherable((loss, stats, batch_size), loss.device)
|
||||
return loss, stats, weight
|
||||
|
||||
def encode(
|
||||
self,
|
||||
speech: torch.Tensor,
|
||||
speech_lengths: torch.Tensor,
|
||||
**kwargs,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Frontend + Encoder. Note that this method is used by asr_inference.py
|
||||
Args:
|
||||
speech: (Batch, Length, ...)
|
||||
speech_lengths: (Batch, )
|
||||
ind: int
|
||||
"""
|
||||
|
||||
# Data augmentation
|
||||
if self.specaug is not None and self.training:
|
||||
speech, speech_lengths = self.specaug(speech, speech_lengths)
|
||||
|
||||
# Normalization for feature: e.g. Global-CMVN, Utterance-CMVN
|
||||
if self.normalize is not None:
|
||||
speech, speech_lengths = self.normalize(speech, speech_lengths)
|
||||
|
||||
# Forward encoder
|
||||
# feats: (Batch, Length, Dim)
|
||||
# -> encoder_out: (Batch, Length2, Dim2)
|
||||
encoder_out, encoder_out_lens = self.encoder(speech, speech_lengths)
|
||||
|
||||
return encoder_out, encoder_out_lens
|
||||
|
||||
|
||||
def _calc_ctc_loss(
|
||||
self,
|
||||
encoder_out: torch.Tensor,
|
||||
encoder_out_lens: torch.Tensor,
|
||||
ys_pad: torch.Tensor,
|
||||
ys_pad_lens: torch.Tensor,
|
||||
):
|
||||
# Calc CTC loss
|
||||
"""Internal: calc ctc loss.
|
||||
|
||||
Args:
|
||||
encoder_out: Encoder output tensor.
|
||||
encoder_out_lens: Encoder output lengths.
|
||||
ys_pad: TODO.
|
||||
ys_pad_lens: Lengths of ys_pad.
|
||||
"""
|
||||
loss_ctc = self.ctc(encoder_out, encoder_out_lens, ys_pad, ys_pad_lens)
|
||||
|
||||
# Calc CER using CTC
|
||||
cer_ctc = None
|
||||
if not self.training and self.error_calculator is not None:
|
||||
ys_hat = self.ctc.argmax(encoder_out).data
|
||||
cer_ctc = self.error_calculator(ys_hat.cpu(), ys_pad.cpu(), is_ctc=True)
|
||||
return loss_ctc, cer_ctc
|
||||
|
||||
|
||||
def inference(
|
||||
self,
|
||||
data_in,
|
||||
data_lengths=None,
|
||||
key: list = None,
|
||||
tokenizer=None,
|
||||
frontend=None,
|
||||
**kwargs,
|
||||
):
|
||||
|
||||
"""Run inference on input data.
|
||||
|
||||
Args:
|
||||
data_in: Input data (audio samples, file paths, or text).
|
||||
data_lengths: Lengths of each input sample in the batch.
|
||||
key: Sample identifiers.
|
||||
tokenizer: Tokenizer instance for text encoding/decoding.
|
||||
frontend: Audio frontend for feature extraction.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
if kwargs.get("batch_size", 1) > 1:
|
||||
raise NotImplementedError("batch decoding is not implemented")
|
||||
|
||||
meta_data = {}
|
||||
if (
|
||||
isinstance(data_in, torch.Tensor) and kwargs.get("data_type", "sound") == "fbank"
|
||||
): # fbank
|
||||
speech, speech_lengths = data_in, data_lengths
|
||||
if len(speech.shape) < 3:
|
||||
speech = speech[None, :, :]
|
||||
if speech_lengths is None:
|
||||
speech_lengths = speech.shape[1]
|
||||
else:
|
||||
# extract fbank feats
|
||||
time1 = time.perf_counter()
|
||||
audio_sample_list = load_audio_text_image_video(
|
||||
data_in,
|
||||
fs=frontend.fs,
|
||||
audio_fs=kwargs.get("fs", 16000),
|
||||
data_type=kwargs.get("data_type", "sound"),
|
||||
tokenizer=tokenizer,
|
||||
)
|
||||
time2 = time.perf_counter()
|
||||
meta_data["load_data"] = f"{time2 - time1:0.3f}"
|
||||
speech, speech_lengths = extract_fbank(
|
||||
audio_sample_list, data_type=kwargs.get("data_type", "sound"), frontend=frontend
|
||||
)
|
||||
time3 = time.perf_counter()
|
||||
meta_data["extract_feat"] = f"{time3 - time2:0.3f}"
|
||||
meta_data["batch_data_time"] = (
|
||||
speech_lengths.sum().item() * frontend.frame_shift * frontend.lfr_n / 1000
|
||||
)
|
||||
|
||||
speech = speech.to(device=kwargs["device"])
|
||||
speech_lengths = speech_lengths.to(device=kwargs["device"])
|
||||
# Encoder
|
||||
encoder_out, encoder_out_lens = self.encode(speech, speech_lengths)
|
||||
if isinstance(encoder_out, tuple):
|
||||
encoder_out = encoder_out[0]
|
||||
|
||||
# c. Passed the encoder result and the beam search
|
||||
ctc_logits = self.ctc.log_softmax(encoder_out)
|
||||
|
||||
results = []
|
||||
b, n, d = encoder_out.size()
|
||||
if isinstance(key[0], (list, tuple)):
|
||||
key = key[0]
|
||||
if len(key) < b:
|
||||
key = key * b
|
||||
for i in range(b):
|
||||
x = ctc_logits[i, :encoder_out_lens[i], :]
|
||||
yseq = x.argmax(dim=-1)
|
||||
yseq = torch.unique_consecutive(yseq, dim=-1)
|
||||
yseq = torch.tensor([self.sos] + yseq.tolist() + [self.eos], device=yseq.device)
|
||||
nbest_hyps = [Hypothesis(yseq=yseq)]
|
||||
|
||||
for nbest_idx, hyp in enumerate(nbest_hyps):
|
||||
ibest_writer = None
|
||||
if kwargs.get("output_dir") is not None:
|
||||
if not hasattr(self, "writer"):
|
||||
self.writer = DatadirWriter(kwargs.get("output_dir"))
|
||||
ibest_writer = self.writer[f"{nbest_idx + 1}best_recog"]
|
||||
|
||||
# remove sos/eos and get results
|
||||
last_pos = -1
|
||||
if isinstance(hyp.yseq, list):
|
||||
token_int = hyp.yseq[1:last_pos]
|
||||
else:
|
||||
token_int = hyp.yseq[1:last_pos].tolist()
|
||||
|
||||
# remove blank symbol id, which is assumed to be 0
|
||||
token_int = list(
|
||||
filter(
|
||||
lambda x: x != self.eos and x != self.sos and x != self.blank_id, token_int
|
||||
)
|
||||
)
|
||||
|
||||
# Change integer-ids to tokens
|
||||
token = tokenizer.ids2tokens(token_int)
|
||||
text = tokenizer.tokens2text(token)
|
||||
|
||||
text_postprocessed, _ = postprocess_utils.sentence_postprocess(token)
|
||||
result_i = {"key": key[i], "token": token, "text": text_postprocessed}
|
||||
results.append(result_i)
|
||||
|
||||
if ibest_writer is not None:
|
||||
ibest_writer["token"][key[i]] = " ".join(token)
|
||||
ibest_writer["text"][key[i]] = text_postprocessed
|
||||
|
||||
return results, meta_data
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
from contextlib import contextmanager
|
||||
from distutils.version import LooseVersion
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
# from funasr.layers.abs_normalize import AbsNormalize
|
||||
# from funasr.models.base_model import FunASRModel
|
||||
# from funasr.models.encoder.abs_encoder import AbsEncoder
|
||||
from funasr.frontends.abs_frontend import AbsFrontend
|
||||
|
||||
# from funasr.models.preencoder.abs_preencoder import AbsPreEncoder
|
||||
# from funasr.models.specaug.abs_specaug import AbsSpecAug
|
||||
from funasr.train_utils.device_funcs import force_gatherable
|
||||
|
||||
if LooseVersion(torch.__version__) >= LooseVersion("1.6.0"):
|
||||
from torch.cuda.amp import autocast
|
||||
else:
|
||||
# Nothing to do if torch<1.6.0
|
||||
@contextmanager
|
||||
def autocast(enabled=True):
|
||||
"""Autocast.
|
||||
|
||||
Args:
|
||||
enabled: TODO.
|
||||
"""
|
||||
yield
|
||||
|
||||
|
||||
class Data2VecPretrainModel(nn.Module):
|
||||
"""Data2Vec Pretrain model"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
frontend=None,
|
||||
specaug=None,
|
||||
normalize=None,
|
||||
encoder=None,
|
||||
preencoder=None,
|
||||
):
|
||||
|
||||
"""Initialize Data2VecPretrainModel.
|
||||
|
||||
Args:
|
||||
frontend: Audio frontend for feature extraction.
|
||||
specaug: TODO.
|
||||
normalize: TODO.
|
||||
encoder: TODO.
|
||||
preencoder: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.frontend = frontend
|
||||
self.specaug = specaug
|
||||
self.normalize = normalize
|
||||
self.preencoder = preencoder
|
||||
self.encoder = encoder
|
||||
self.num_updates = 0
|
||||
|
||||
def forward(
|
||||
self,
|
||||
speech: torch.Tensor,
|
||||
speech_lengths: torch.Tensor,
|
||||
) -> Tuple[torch.Tensor, Dict[str, torch.Tensor], torch.Tensor]:
|
||||
"""Frontend + Encoder + Calc loss
|
||||
Args:
|
||||
speech: (Batch, Length, ...)
|
||||
speech_lengths: (Batch, )
|
||||
"""
|
||||
# Check that batch_size is unified
|
||||
assert speech.shape[0] == speech_lengths.shape[0], (speech.shape, speech_lengths.shape)
|
||||
|
||||
self.encoder.set_num_updates(self.num_updates)
|
||||
|
||||
# 1. Encoder
|
||||
encoder_out = self.encode(speech, speech_lengths)
|
||||
|
||||
losses = encoder_out["losses"]
|
||||
loss = sum(losses.values())
|
||||
sample_size = encoder_out["sample_size"]
|
||||
loss = loss.sum() / sample_size
|
||||
|
||||
target_var = float(encoder_out["target_var"])
|
||||
pred_var = float(encoder_out["pred_var"])
|
||||
ema_decay = float(encoder_out["ema_decay"])
|
||||
|
||||
stats = dict(
|
||||
loss=torch.clone(loss.detach()),
|
||||
target_var=target_var,
|
||||
pred_var=pred_var,
|
||||
ema_decay=ema_decay,
|
||||
)
|
||||
|
||||
loss, stats, weight = force_gatherable((loss, stats, sample_size), loss.device)
|
||||
return loss, stats, weight
|
||||
|
||||
def collect_feats(
|
||||
self, speech: torch.Tensor, speech_lengths: torch.Tensor
|
||||
) -> Dict[str, torch.Tensor]:
|
||||
"""Collect feats.
|
||||
|
||||
Args:
|
||||
speech: Speech audio tensor, shape (batch, time).
|
||||
speech_lengths: Length of each speech sample.
|
||||
"""
|
||||
feats, feats_lengths = self._extract_feats(speech, speech_lengths)
|
||||
return {"feats": feats, "feats_lengths": feats_lengths}
|
||||
|
||||
def encode(
|
||||
self,
|
||||
speech: torch.Tensor,
|
||||
speech_lengths: torch.Tensor,
|
||||
):
|
||||
"""Frontend + Encoder.
|
||||
Args:
|
||||
speech: (Batch, Length, ...)
|
||||
speech_lengths: (Batch, )
|
||||
"""
|
||||
with autocast(False):
|
||||
# 1. Extract feats
|
||||
feats, feats_lengths = self._extract_feats(speech, speech_lengths)
|
||||
|
||||
# 2. Data augmentation
|
||||
if self.specaug is not None and self.training:
|
||||
feats, feats_lengths = self.specaug(feats, feats_lengths)
|
||||
|
||||
# 3. Normalization for feature: e.g. Global-CMVN, Utterance-CMVN
|
||||
if self.normalize is not None:
|
||||
feats, feats_lengths = self.normalize(feats, feats_lengths)
|
||||
|
||||
# Pre-encoder, e.g. used for raw input data
|
||||
if self.preencoder is not None:
|
||||
feats, feats_lengths = self.preencoder(feats, feats_lengths)
|
||||
|
||||
# 4. Forward encoder
|
||||
if min(speech_lengths) == max(speech_lengths): # for clipping, set speech_lengths as None
|
||||
speech_lengths = None
|
||||
encoder_out = self.encoder(feats, speech_lengths, mask=True, features_only=False)
|
||||
|
||||
return encoder_out
|
||||
|
||||
def _extract_feats(
|
||||
self, speech: torch.Tensor, speech_lengths: torch.Tensor
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Internal: extract feats.
|
||||
|
||||
Args:
|
||||
speech: Speech audio tensor, shape (batch, time).
|
||||
speech_lengths: Length of each speech sample.
|
||||
"""
|
||||
assert speech_lengths.dim() == 1, speech_lengths.shape
|
||||
|
||||
# for data-parallel
|
||||
speech = speech[:, : speech_lengths.max()]
|
||||
|
||||
if self.frontend is not None:
|
||||
# Frontend
|
||||
# e.g. STFT and Feature extract
|
||||
# data_loader may send time-domain signal in this case
|
||||
# speech (Batch, NSamples) -> feats: (Batch, NFrames, Dim)
|
||||
feats, feats_lengths = self.frontend(speech, speech_lengths)
|
||||
else:
|
||||
# No frontend and no feature extract
|
||||
feats, feats_lengths = speech, speech_lengths
|
||||
return feats, feats_lengths
|
||||
|
||||
def set_num_updates(self, num_updates):
|
||||
"""Set num updates.
|
||||
|
||||
Args:
|
||||
num_updates: TODO.
|
||||
"""
|
||||
self.num_updates = num_updates
|
||||
|
||||
def get_num_updates(self):
|
||||
"""Get num updates."""
|
||||
return self.num_updates
|
||||
@@ -0,0 +1,680 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import logging
|
||||
import math
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from funasr.models.data2vec.data_utils import compute_mask_indices
|
||||
from funasr.models.data2vec.ema_module import EMAModule
|
||||
from funasr.models.data2vec.grad_multiply import GradMultiply
|
||||
from funasr.models.data2vec.wav2vec2 import (
|
||||
ConvFeatureExtractionModel,
|
||||
TransformerEncoder,
|
||||
)
|
||||
from funasr.models.transformer.utils.nets_utils import make_pad_mask
|
||||
|
||||
|
||||
def get_annealed_rate(start, end, curr_step, total_steps):
|
||||
"""Get annealed rate.
|
||||
|
||||
Args:
|
||||
start: TODO.
|
||||
end: TODO.
|
||||
curr_step: TODO.
|
||||
total_steps: TODO.
|
||||
"""
|
||||
r = end - start
|
||||
pct_remaining = 1 - curr_step / total_steps
|
||||
return end - r * pct_remaining
|
||||
|
||||
|
||||
class Data2VecEncoder(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
# for ConvFeatureExtractionModel
|
||||
input_size: int = None,
|
||||
extractor_mode: str = None,
|
||||
conv_feature_layers: str = "[(512,2,2)] + [(512,2,2)]",
|
||||
# for Transformer Encoder
|
||||
## model architecture
|
||||
layer_type: str = "transformer",
|
||||
layer_norm_first: bool = False,
|
||||
encoder_layers: int = 12,
|
||||
encoder_embed_dim: int = 768,
|
||||
encoder_ffn_embed_dim: int = 3072,
|
||||
encoder_attention_heads: int = 12,
|
||||
activation_fn: str = "gelu",
|
||||
## dropouts
|
||||
dropout: float = 0.1,
|
||||
attention_dropout: float = 0.1,
|
||||
activation_dropout: float = 0.0,
|
||||
encoder_layerdrop: float = 0.0,
|
||||
dropout_input: float = 0.0,
|
||||
dropout_features: float = 0.0,
|
||||
## grad settings
|
||||
feature_grad_mult: float = 1.0,
|
||||
## masking
|
||||
mask_prob: float = 0.65,
|
||||
mask_length: int = 10,
|
||||
mask_selection: str = "static",
|
||||
mask_other: int = 0,
|
||||
no_mask_overlap: bool = False,
|
||||
mask_min_space: int = 1,
|
||||
require_same_masks: bool = True, # if set as True, collate_fn should be clipping
|
||||
mask_dropout: float = 0.0,
|
||||
## channel masking
|
||||
mask_channel_length: int = 10,
|
||||
mask_channel_prob: float = 0.0,
|
||||
mask_channel_before: bool = False,
|
||||
mask_channel_selection: str = "static",
|
||||
mask_channel_other: int = 0,
|
||||
no_mask_channel_overlap: bool = False,
|
||||
mask_channel_min_space: int = 1,
|
||||
## positional embeddings
|
||||
conv_pos: int = 128,
|
||||
conv_pos_groups: int = 16,
|
||||
pos_conv_depth: int = 1,
|
||||
max_positions: int = 100000,
|
||||
# EMA module
|
||||
average_top_k_layers: int = 8,
|
||||
layer_norm_target_layer: bool = False,
|
||||
instance_norm_target_layer: bool = False,
|
||||
instance_norm_targets: bool = False,
|
||||
layer_norm_targets: bool = False,
|
||||
batch_norm_target_layer: bool = False,
|
||||
group_norm_target_layer: bool = False,
|
||||
ema_decay: float = 0.999,
|
||||
ema_end_decay: float = 0.9999,
|
||||
ema_anneal_end_step: int = 100000,
|
||||
ema_transformer_only: bool = True,
|
||||
ema_layers_only: bool = True,
|
||||
min_target_var: float = 0.1,
|
||||
min_pred_var: float = 0.01,
|
||||
# Loss
|
||||
loss_beta: float = 0.0,
|
||||
loss_scale: float = None,
|
||||
# FP16 optimization
|
||||
required_seq_len_multiple: int = 2,
|
||||
):
|
||||
"""Initialize Data2VecEncoder.
|
||||
|
||||
Args:
|
||||
input_size: Size/dimension parameter.
|
||||
extractor_mode: TODO.
|
||||
conv_feature_layers: TODO.
|
||||
layer_type: TODO.
|
||||
layer_norm_first: TODO.
|
||||
encoder_layers: TODO.
|
||||
encoder_embed_dim: Size/dimension parameter.
|
||||
encoder_ffn_embed_dim: Size/dimension parameter.
|
||||
encoder_attention_heads: TODO.
|
||||
activation_fn: TODO.
|
||||
dropout: TODO.
|
||||
attention_dropout: TODO.
|
||||
activation_dropout: TODO.
|
||||
encoder_layerdrop: TODO.
|
||||
dropout_input: TODO.
|
||||
dropout_features: TODO.
|
||||
feature_grad_mult: TODO.
|
||||
mask_prob: TODO.
|
||||
mask_length: TODO.
|
||||
mask_selection: TODO.
|
||||
mask_other: TODO.
|
||||
no_mask_overlap: TODO.
|
||||
mask_min_space: TODO.
|
||||
require_same_masks: TODO.
|
||||
mask_dropout: TODO.
|
||||
mask_channel_length: TODO.
|
||||
mask_channel_prob: TODO.
|
||||
mask_channel_before: TODO.
|
||||
mask_channel_selection: TODO.
|
||||
mask_channel_other: TODO.
|
||||
no_mask_channel_overlap: TODO.
|
||||
mask_channel_min_space: TODO.
|
||||
conv_pos: TODO.
|
||||
conv_pos_groups: TODO.
|
||||
pos_conv_depth: TODO.
|
||||
max_positions: TODO.
|
||||
average_top_k_layers: TODO.
|
||||
layer_norm_target_layer: TODO.
|
||||
instance_norm_target_layer: TODO.
|
||||
instance_norm_targets: TODO.
|
||||
layer_norm_targets: TODO.
|
||||
batch_norm_target_layer: TODO.
|
||||
group_norm_target_layer: TODO.
|
||||
ema_decay: TODO.
|
||||
ema_end_decay: TODO.
|
||||
ema_anneal_end_step: TODO.
|
||||
ema_transformer_only: TODO.
|
||||
ema_layers_only: TODO.
|
||||
min_target_var: TODO.
|
||||
min_pred_var: TODO.
|
||||
loss_beta: TODO.
|
||||
loss_scale: TODO.
|
||||
required_seq_len_multiple: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
# ConvFeatureExtractionModel
|
||||
self.conv_feature_layers = conv_feature_layers
|
||||
feature_enc_layers = eval(conv_feature_layers)
|
||||
self.extractor_embed = feature_enc_layers[-1][0]
|
||||
self.feature_extractor = ConvFeatureExtractionModel(
|
||||
conv_layers=feature_enc_layers,
|
||||
dropout=0.0,
|
||||
mode=extractor_mode,
|
||||
in_d=input_size,
|
||||
)
|
||||
|
||||
# Transformer Encoder
|
||||
## model architecture
|
||||
self.layer_type = layer_type
|
||||
self.layer_norm_first = layer_norm_first
|
||||
self.encoder_layers = encoder_layers
|
||||
self.encoder_embed_dim = encoder_embed_dim
|
||||
self.encoder_ffn_embed_dim = encoder_ffn_embed_dim
|
||||
self.encoder_attention_heads = encoder_attention_heads
|
||||
self.activation_fn = activation_fn
|
||||
## dropout
|
||||
self.dropout = dropout
|
||||
self.attention_dropout = attention_dropout
|
||||
self.activation_dropout = activation_dropout
|
||||
self.encoder_layerdrop = encoder_layerdrop
|
||||
self.dropout_input = dropout_input
|
||||
self.dropout_features = dropout_features
|
||||
## grad settings
|
||||
self.feature_grad_mult = feature_grad_mult
|
||||
## masking
|
||||
self.mask_prob = mask_prob
|
||||
self.mask_length = mask_length
|
||||
self.mask_selection = mask_selection
|
||||
self.mask_other = mask_other
|
||||
self.no_mask_overlap = no_mask_overlap
|
||||
self.mask_min_space = mask_min_space
|
||||
self.require_same_masks = (
|
||||
require_same_masks # if set as True, collate_fn should be clipping
|
||||
)
|
||||
self.mask_dropout = mask_dropout
|
||||
## channel masking
|
||||
self.mask_channel_length = mask_channel_length
|
||||
self.mask_channel_prob = mask_channel_prob
|
||||
self.mask_channel_before = mask_channel_before
|
||||
self.mask_channel_selection = mask_channel_selection
|
||||
self.mask_channel_other = mask_channel_other
|
||||
self.no_mask_channel_overlap = no_mask_channel_overlap
|
||||
self.mask_channel_min_space = mask_channel_min_space
|
||||
## positional embeddings
|
||||
self.conv_pos = conv_pos
|
||||
self.conv_pos_groups = conv_pos_groups
|
||||
self.pos_conv_depth = pos_conv_depth
|
||||
self.max_positions = max_positions
|
||||
self.mask_emb = nn.Parameter(torch.FloatTensor(self.encoder_embed_dim).uniform_())
|
||||
self.encoder = TransformerEncoder(
|
||||
dropout=self.dropout,
|
||||
encoder_embed_dim=self.encoder_embed_dim,
|
||||
required_seq_len_multiple=required_seq_len_multiple,
|
||||
pos_conv_depth=self.pos_conv_depth,
|
||||
conv_pos=self.conv_pos,
|
||||
conv_pos_groups=self.conv_pos_groups,
|
||||
# transformer layers
|
||||
layer_type=self.layer_type,
|
||||
encoder_layers=self.encoder_layers,
|
||||
encoder_ffn_embed_dim=self.encoder_ffn_embed_dim,
|
||||
encoder_attention_heads=self.encoder_attention_heads,
|
||||
attention_dropout=self.attention_dropout,
|
||||
activation_dropout=self.activation_dropout,
|
||||
activation_fn=self.activation_fn,
|
||||
layer_norm_first=self.layer_norm_first,
|
||||
encoder_layerdrop=self.encoder_layerdrop,
|
||||
max_positions=self.max_positions,
|
||||
)
|
||||
## projections and dropouts
|
||||
self.post_extract_proj = nn.Linear(self.extractor_embed, self.encoder_embed_dim)
|
||||
self.dropout_input = nn.Dropout(self.dropout_input)
|
||||
self.dropout_features = nn.Dropout(self.dropout_features)
|
||||
self.layer_norm = torch.nn.LayerNorm(self.extractor_embed)
|
||||
self.final_proj = nn.Linear(self.encoder_embed_dim, self.encoder_embed_dim)
|
||||
|
||||
# EMA module
|
||||
self.average_top_k_layers = average_top_k_layers
|
||||
self.layer_norm_target_layer = layer_norm_target_layer
|
||||
self.instance_norm_target_layer = instance_norm_target_layer
|
||||
self.instance_norm_targets = instance_norm_targets
|
||||
self.layer_norm_targets = layer_norm_targets
|
||||
self.batch_norm_target_layer = batch_norm_target_layer
|
||||
self.group_norm_target_layer = group_norm_target_layer
|
||||
self.ema_decay = ema_decay
|
||||
self.ema_end_decay = ema_end_decay
|
||||
self.ema_anneal_end_step = ema_anneal_end_step
|
||||
self.ema_transformer_only = ema_transformer_only
|
||||
self.ema_layers_only = ema_layers_only
|
||||
self.min_target_var = min_target_var
|
||||
self.min_pred_var = min_pred_var
|
||||
self.ema = None
|
||||
|
||||
# Loss
|
||||
self.loss_beta = loss_beta
|
||||
self.loss_scale = loss_scale
|
||||
|
||||
# FP16 optimization
|
||||
self.required_seq_len_multiple = required_seq_len_multiple
|
||||
|
||||
self.num_updates = 0
|
||||
|
||||
logging.info("Data2VecEncoder settings: {}".format(self.__dict__))
|
||||
|
||||
def make_ema_teacher(self):
|
||||
"""Make ema teacher."""
|
||||
skip_keys = set()
|
||||
if self.ema_layers_only:
|
||||
self.ema_transformer_only = True
|
||||
for k, _ in self.encoder.pos_conv.named_parameters():
|
||||
skip_keys.add(f"pos_conv.{k}")
|
||||
|
||||
self.ema = EMAModule(
|
||||
self.encoder if self.ema_transformer_only else self,
|
||||
ema_decay=self.ema_decay,
|
||||
ema_fp32=True,
|
||||
skip_keys=skip_keys,
|
||||
)
|
||||
|
||||
def set_num_updates(self, num_updates):
|
||||
"""Set num updates.
|
||||
|
||||
Args:
|
||||
num_updates: TODO.
|
||||
"""
|
||||
if self.ema is None and self.final_proj is not None:
|
||||
logging.info("Making EMA Teacher")
|
||||
self.make_ema_teacher()
|
||||
elif self.training and self.ema is not None:
|
||||
if self.ema_decay != self.ema_end_decay:
|
||||
if num_updates >= self.ema_anneal_end_step:
|
||||
decay = self.ema_end_decay
|
||||
else:
|
||||
decay = get_annealed_rate(
|
||||
self.ema_decay,
|
||||
self.ema_end_decay,
|
||||
num_updates,
|
||||
self.ema_anneal_end_step,
|
||||
)
|
||||
self.ema.set_decay(decay)
|
||||
if self.ema.get_decay() < 1:
|
||||
self.ema.step(self.encoder if self.ema_transformer_only else self)
|
||||
|
||||
self.num_updates = num_updates
|
||||
|
||||
def apply_mask(
|
||||
self,
|
||||
x,
|
||||
padding_mask,
|
||||
mask_indices=None,
|
||||
mask_channel_indices=None,
|
||||
):
|
||||
"""Apply mask.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
padding_mask: TODO.
|
||||
mask_indices: TODO.
|
||||
mask_channel_indices: TODO.
|
||||
"""
|
||||
B, T, C = x.shape
|
||||
|
||||
if self.mask_channel_prob > 0 and self.mask_channel_before:
|
||||
mask_channel_indices = compute_mask_indices(
|
||||
(B, C),
|
||||
None,
|
||||
self.mask_channel_prob,
|
||||
self.mask_channel_length,
|
||||
self.mask_channel_selection,
|
||||
self.mask_channel_other,
|
||||
no_overlap=self.no_mask_channel_overlap,
|
||||
min_space=self.mask_channel_min_space,
|
||||
)
|
||||
mask_channel_indices = (
|
||||
torch.from_numpy(mask_channel_indices).to(x.device).unsqueeze(1).expand(-1, T, -1)
|
||||
)
|
||||
x[mask_channel_indices] = 0
|
||||
|
||||
if self.mask_prob > 0:
|
||||
if mask_indices is None:
|
||||
mask_indices = compute_mask_indices(
|
||||
(B, T),
|
||||
padding_mask,
|
||||
self.mask_prob,
|
||||
self.mask_length,
|
||||
self.mask_selection,
|
||||
self.mask_other,
|
||||
min_masks=1,
|
||||
no_overlap=self.no_mask_overlap,
|
||||
min_space=self.mask_min_space,
|
||||
require_same_masks=self.require_same_masks,
|
||||
mask_dropout=self.mask_dropout,
|
||||
)
|
||||
mask_indices = torch.from_numpy(mask_indices).to(x.device)
|
||||
x[mask_indices] = self.mask_emb
|
||||
else:
|
||||
mask_indices = None
|
||||
|
||||
if self.mask_channel_prob > 0 and not self.mask_channel_before:
|
||||
if mask_channel_indices is None:
|
||||
mask_channel_indices = compute_mask_indices(
|
||||
(B, C),
|
||||
None,
|
||||
self.mask_channel_prob,
|
||||
self.mask_channel_length,
|
||||
self.mask_channel_selection,
|
||||
self.mask_channel_other,
|
||||
no_overlap=self.no_mask_channel_overlap,
|
||||
min_space=self.mask_channel_min_space,
|
||||
)
|
||||
mask_channel_indices = (
|
||||
torch.from_numpy(mask_channel_indices)
|
||||
.to(x.device)
|
||||
.unsqueeze(1)
|
||||
.expand(-1, T, -1)
|
||||
)
|
||||
x[mask_channel_indices] = 0
|
||||
|
||||
return x, mask_indices
|
||||
|
||||
def _get_feat_extract_output_lengths(self, input_lengths: torch.LongTensor):
|
||||
"""
|
||||
Computes the output length of the convolutional layers
|
||||
"""
|
||||
|
||||
def _conv_out_length(input_length, kernel_size, stride):
|
||||
"""Internal: conv out length.
|
||||
|
||||
Args:
|
||||
input_length: TODO.
|
||||
kernel_size: Size/dimension parameter.
|
||||
stride: TODO.
|
||||
"""
|
||||
return torch.floor((input_length - kernel_size).to(torch.float32) / stride + 1)
|
||||
|
||||
conv_cfg_list = eval(self.conv_feature_layers)
|
||||
|
||||
for i in range(len(conv_cfg_list)):
|
||||
input_lengths = _conv_out_length(
|
||||
input_lengths, conv_cfg_list[i][1], conv_cfg_list[i][2]
|
||||
)
|
||||
|
||||
return input_lengths.to(torch.long)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
xs_pad,
|
||||
ilens=None,
|
||||
mask=False,
|
||||
features_only=True,
|
||||
layer=None,
|
||||
mask_indices=None,
|
||||
mask_channel_indices=None,
|
||||
padding_count=None,
|
||||
):
|
||||
# create padding_mask by ilens
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
xs_pad: TODO.
|
||||
ilens: TODO.
|
||||
mask: TODO.
|
||||
features_only: TODO.
|
||||
layer: TODO.
|
||||
mask_indices: TODO.
|
||||
mask_channel_indices: TODO.
|
||||
padding_count: TODO.
|
||||
"""
|
||||
if ilens is not None:
|
||||
padding_mask = make_pad_mask(lengths=ilens).to(xs_pad.device)
|
||||
else:
|
||||
padding_mask = None
|
||||
|
||||
features = xs_pad
|
||||
|
||||
if self.feature_grad_mult > 0:
|
||||
features = self.feature_extractor(features)
|
||||
if self.feature_grad_mult != 1.0:
|
||||
features = GradMultiply.apply(features, self.feature_grad_mult)
|
||||
else:
|
||||
with torch.no_grad():
|
||||
features = self.feature_extractor(features)
|
||||
|
||||
features = features.transpose(1, 2)
|
||||
|
||||
features = self.layer_norm(features)
|
||||
|
||||
orig_padding_mask = padding_mask
|
||||
|
||||
if padding_mask is not None:
|
||||
input_lengths = (1 - padding_mask.long()).sum(-1)
|
||||
# apply conv formula to get real output_lengths
|
||||
output_lengths = self._get_feat_extract_output_lengths(input_lengths)
|
||||
|
||||
padding_mask = torch.zeros(
|
||||
features.shape[:2], dtype=features.dtype, device=features.device
|
||||
)
|
||||
# these two operations makes sure that all values
|
||||
# before the output lengths indices are attended to
|
||||
padding_mask[
|
||||
(
|
||||
torch.arange(padding_mask.shape[0], device=padding_mask.device),
|
||||
output_lengths - 1,
|
||||
)
|
||||
] = 1
|
||||
padding_mask = (1 - padding_mask.flip([-1]).cumsum(-1).flip([-1])).bool()
|
||||
else:
|
||||
padding_mask = None
|
||||
|
||||
if self.post_extract_proj is not None:
|
||||
features = self.post_extract_proj(features)
|
||||
|
||||
pre_encoder_features = None
|
||||
if self.ema_transformer_only:
|
||||
pre_encoder_features = features.clone()
|
||||
|
||||
features = self.dropout_input(features)
|
||||
|
||||
if mask:
|
||||
x, mask_indices = self.apply_mask(
|
||||
features,
|
||||
padding_mask,
|
||||
mask_indices=mask_indices,
|
||||
mask_channel_indices=mask_channel_indices,
|
||||
)
|
||||
else:
|
||||
x = features
|
||||
mask_indices = None
|
||||
|
||||
x, layer_results = self.encoder(
|
||||
x,
|
||||
padding_mask=padding_mask,
|
||||
layer=layer,
|
||||
)
|
||||
|
||||
if features_only:
|
||||
encoder_out_lens = (1 - padding_mask.long()).sum(1)
|
||||
return x, encoder_out_lens, None
|
||||
|
||||
result = {
|
||||
"losses": {},
|
||||
"padding_mask": padding_mask,
|
||||
"x": x,
|
||||
}
|
||||
|
||||
with torch.no_grad():
|
||||
self.ema.model.eval()
|
||||
|
||||
if self.ema_transformer_only:
|
||||
y, layer_results = self.ema.model.extract_features(
|
||||
pre_encoder_features,
|
||||
padding_mask=padding_mask,
|
||||
min_layer=self.encoder_layers - self.average_top_k_layers,
|
||||
)
|
||||
y = {
|
||||
"x": y,
|
||||
"padding_mask": padding_mask,
|
||||
"layer_results": layer_results,
|
||||
}
|
||||
else:
|
||||
y = self.ema.model.extract_features(
|
||||
source=xs_pad,
|
||||
padding_mask=orig_padding_mask,
|
||||
mask=False,
|
||||
)
|
||||
|
||||
target_layer_results = [l[2] for l in y["layer_results"]]
|
||||
|
||||
permuted = False
|
||||
if self.instance_norm_target_layer or self.batch_norm_target_layer:
|
||||
target_layer_results = [
|
||||
tl.permute(1, 2, 0) for tl in target_layer_results # TBC -> BCT
|
||||
]
|
||||
permuted = True
|
||||
|
||||
if self.batch_norm_target_layer:
|
||||
target_layer_results = [
|
||||
F.batch_norm(tl.float(), running_mean=None, running_var=None, training=True)
|
||||
for tl in target_layer_results
|
||||
]
|
||||
|
||||
if self.instance_norm_target_layer:
|
||||
target_layer_results = [F.instance_norm(tl.float()) for tl in target_layer_results]
|
||||
|
||||
if permuted:
|
||||
target_layer_results = [
|
||||
tl.transpose(1, 2) for tl in target_layer_results # BCT -> BTC
|
||||
]
|
||||
|
||||
if self.group_norm_target_layer:
|
||||
target_layer_results = [
|
||||
F.layer_norm(tl.float(), tl.shape[-2:]) for tl in target_layer_results
|
||||
]
|
||||
|
||||
if self.layer_norm_target_layer:
|
||||
target_layer_results = [
|
||||
F.layer_norm(tl.float(), tl.shape[-1:]) for tl in target_layer_results
|
||||
]
|
||||
|
||||
y = sum(target_layer_results) / len(target_layer_results)
|
||||
|
||||
if self.layer_norm_targets:
|
||||
y = F.layer_norm(y.float(), y.shape[-1:])
|
||||
|
||||
if self.instance_norm_targets:
|
||||
y = F.instance_norm(y.float().transpose(1, 2)).transpose(1, 2)
|
||||
|
||||
if not permuted:
|
||||
y = y.transpose(0, 1)
|
||||
|
||||
y = y[mask_indices]
|
||||
|
||||
x = x[mask_indices]
|
||||
x = self.final_proj(x)
|
||||
|
||||
sz = x.size(-1)
|
||||
|
||||
if self.loss_beta == 0:
|
||||
loss = F.mse_loss(x.float(), y.float(), reduction="none").sum(dim=-1)
|
||||
else:
|
||||
loss = F.smooth_l1_loss(
|
||||
x.float(), y.float(), reduction="none", beta=self.loss_beta
|
||||
).sum(dim=-1)
|
||||
|
||||
if self.loss_scale is not None:
|
||||
scale = self.loss_scale
|
||||
else:
|
||||
scale = 1 / math.sqrt(sz)
|
||||
|
||||
result["losses"]["regression"] = loss.sum() * scale
|
||||
|
||||
if "sample_size" not in result:
|
||||
result["sample_size"] = loss.numel()
|
||||
|
||||
with torch.no_grad():
|
||||
result["target_var"] = self.compute_var(y)
|
||||
result["pred_var"] = self.compute_var(x.float())
|
||||
|
||||
if self.num_updates > 5000 and result["target_var"] < self.min_target_var:
|
||||
logging.error(
|
||||
f"target var is {result['target_var'].item()} < {self.min_target_var}, exiting"
|
||||
)
|
||||
raise Exception(
|
||||
f"target var is {result['target_var'].item()} < {self.min_target_var}, exiting"
|
||||
)
|
||||
if self.num_updates > 5000 and result["pred_var"] < self.min_pred_var:
|
||||
logging.error(f"pred var is {result['pred_var'].item()} < {self.min_pred_var}, exiting")
|
||||
raise Exception(
|
||||
f"pred var is {result['pred_var'].item()} < {self.min_pred_var}, exiting"
|
||||
)
|
||||
|
||||
if self.ema is not None:
|
||||
result["ema_decay"] = self.ema.get_decay() * 1000
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def compute_var(y):
|
||||
"""Compute var.
|
||||
|
||||
Args:
|
||||
y: TODO.
|
||||
"""
|
||||
y = y.view(-1, y.size(-1))
|
||||
if dist.is_initialized():
|
||||
zc = torch.tensor(y.size(0)).cuda()
|
||||
zs = y.sum(dim=0)
|
||||
zss = (y**2).sum(dim=0)
|
||||
|
||||
dist.all_reduce(zc)
|
||||
dist.all_reduce(zs)
|
||||
dist.all_reduce(zss)
|
||||
|
||||
var = zss / (zc - 1) - (zs**2) / (zc * (zc - 1))
|
||||
return torch.sqrt(var + 1e-6).mean()
|
||||
else:
|
||||
return torch.sqrt(y.var(dim=0) + 1e-6).mean()
|
||||
|
||||
def extract_features(self, xs_pad, ilens, mask=False, layer=None):
|
||||
"""Extract features.
|
||||
|
||||
Args:
|
||||
xs_pad: TODO.
|
||||
ilens: TODO.
|
||||
mask: TODO.
|
||||
layer: TODO.
|
||||
"""
|
||||
res = self.forward(
|
||||
xs_pad,
|
||||
ilens,
|
||||
mask=mask,
|
||||
features_only=True,
|
||||
layer=layer,
|
||||
)
|
||||
return res
|
||||
|
||||
def remove_pretraining_modules(self, last_layer=None):
|
||||
"""Remove pretraining modules.
|
||||
|
||||
Args:
|
||||
last_layer: TODO.
|
||||
"""
|
||||
self.final_proj = None
|
||||
self.ema = None
|
||||
if last_layer is not None:
|
||||
self.encoder.layers = nn.ModuleList(
|
||||
l for i, l in enumerate(self.encoder.layers) if i <= last_layer
|
||||
)
|
||||
|
||||
def output_size(self) -> int:
|
||||
"""Output size."""
|
||||
return self.encoder_embed_dim
|
||||
@@ -0,0 +1,149 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
|
||||
def compute_mask_indices(
|
||||
shape: Tuple[int, int],
|
||||
padding_mask: Optional[torch.Tensor],
|
||||
mask_prob: float,
|
||||
mask_length: int,
|
||||
mask_type: str = "static",
|
||||
mask_other: float = 0.0,
|
||||
min_masks: int = 0,
|
||||
no_overlap: bool = False,
|
||||
min_space: int = 0,
|
||||
require_same_masks: bool = True,
|
||||
mask_dropout: float = 0.0,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Computes random mask spans for a given shape
|
||||
|
||||
Args:
|
||||
shape: the the shape for which to compute masks.
|
||||
should be of size 2 where first element is batch size and 2nd is timesteps
|
||||
padding_mask: optional padding mask of the same size as shape, which will prevent masking padded elements
|
||||
mask_prob: probability for each token to be chosen as start of the span to be masked. this will be multiplied by
|
||||
number of timesteps divided by length of mask span to mask approximately this percentage of all elements.
|
||||
however due to overlaps, the actual number will be smaller (unless no_overlap is True)
|
||||
mask_type: how to compute mask lengths
|
||||
static = fixed size
|
||||
uniform = sample from uniform distribution [mask_other, mask_length*2]
|
||||
normal = sample from normal distribution with mean mask_length and stdev mask_other. mask is min 1 element
|
||||
poisson = sample from possion distribution with lambda = mask length
|
||||
min_masks: minimum number of masked spans
|
||||
no_overlap: if false, will switch to an alternative recursive algorithm that prevents spans from overlapping
|
||||
min_space: only used if no_overlap is True, this is how many elements to keep unmasked between spans
|
||||
require_same_masks: if true, will randomly drop out masks until same amount of masks remains in each sample
|
||||
mask_dropout: randomly dropout this percentage of masks in each example
|
||||
"""
|
||||
|
||||
bsz, all_sz = shape
|
||||
mask = np.full((bsz, all_sz), False)
|
||||
|
||||
all_num_mask = int(
|
||||
# add a random number for probabilistic rounding
|
||||
mask_prob * all_sz / float(mask_length)
|
||||
+ np.random.rand()
|
||||
)
|
||||
|
||||
all_num_mask = max(min_masks, all_num_mask)
|
||||
|
||||
mask_idcs = []
|
||||
for i in range(bsz):
|
||||
if padding_mask is not None:
|
||||
sz = all_sz - padding_mask[i].long().sum().item()
|
||||
num_mask = int(
|
||||
# add a random number for probabilistic rounding
|
||||
mask_prob * sz / float(mask_length)
|
||||
+ np.random.rand()
|
||||
)
|
||||
num_mask = max(min_masks, num_mask)
|
||||
else:
|
||||
sz = all_sz
|
||||
num_mask = all_num_mask
|
||||
|
||||
if mask_type == "static":
|
||||
lengths = np.full(num_mask, mask_length)
|
||||
elif mask_type == "uniform":
|
||||
lengths = np.random.randint(mask_other, mask_length * 2 + 1, size=num_mask)
|
||||
elif mask_type == "normal":
|
||||
lengths = np.random.normal(mask_length, mask_other, size=num_mask)
|
||||
lengths = [max(1, int(round(x))) for x in lengths]
|
||||
elif mask_type == "poisson":
|
||||
lengths = np.random.poisson(mask_length, size=num_mask)
|
||||
lengths = [int(round(x)) for x in lengths]
|
||||
else:
|
||||
raise Exception("unknown mask selection " + mask_type)
|
||||
|
||||
if sum(lengths) == 0:
|
||||
lengths[0] = min(mask_length, sz - 1)
|
||||
|
||||
if no_overlap:
|
||||
mask_idc = []
|
||||
|
||||
def arrange(s, e, length, keep_length):
|
||||
"""Arrange.
|
||||
|
||||
Args:
|
||||
s: TODO.
|
||||
e: TODO.
|
||||
length: TODO.
|
||||
keep_length: TODO.
|
||||
"""
|
||||
span_start = np.random.randint(s, e - length)
|
||||
mask_idc.extend(span_start + i for i in range(length))
|
||||
|
||||
new_parts = []
|
||||
if span_start - s - min_space >= keep_length:
|
||||
new_parts.append((s, span_start - min_space + 1))
|
||||
if e - span_start - length - min_space > keep_length:
|
||||
new_parts.append((span_start + length + min_space, e))
|
||||
return new_parts
|
||||
|
||||
parts = [(0, sz)]
|
||||
min_length = min(lengths)
|
||||
for length in sorted(lengths, reverse=True):
|
||||
lens = np.fromiter(
|
||||
(e - s if e - s >= length + min_space else 0 for s, e in parts),
|
||||
np.int32,
|
||||
)
|
||||
l_sum = np.sum(lens)
|
||||
if l_sum == 0:
|
||||
break
|
||||
probs = lens / np.sum(lens)
|
||||
c = np.random.choice(len(parts), p=probs)
|
||||
s, e = parts.pop(c)
|
||||
parts.extend(arrange(s, e, length, min_length))
|
||||
mask_idc = np.asarray(mask_idc)
|
||||
else:
|
||||
min_len = min(lengths)
|
||||
if sz - min_len <= num_mask:
|
||||
min_len = sz - num_mask - 1
|
||||
|
||||
mask_idc = np.random.choice(sz - min_len, num_mask, replace=False)
|
||||
|
||||
mask_idc = np.asarray(
|
||||
[mask_idc[j] + offset for j in range(len(mask_idc)) for offset in range(lengths[j])]
|
||||
)
|
||||
|
||||
mask_idcs.append(np.unique(mask_idc[mask_idc < sz]))
|
||||
|
||||
min_len = min([len(m) for m in mask_idcs])
|
||||
for i, mask_idc in enumerate(mask_idcs):
|
||||
if len(mask_idc) > min_len and require_same_masks:
|
||||
mask_idc = np.random.choice(mask_idc, min_len, replace=False)
|
||||
if mask_dropout > 0:
|
||||
num_holes = np.rint(len(mask_idc) * mask_dropout).astype(int)
|
||||
mask_idc = np.random.choice(mask_idc, len(mask_idc) - num_holes, replace=False)
|
||||
|
||||
mask[i, mask_idc] = True
|
||||
|
||||
return mask
|
||||
@@ -0,0 +1,146 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
"""
|
||||
Used for EMA tracking a given pytorch module. The user is responsible for calling step()
|
||||
and setting the appropriate decay
|
||||
"""
|
||||
|
||||
import copy
|
||||
import logging
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
class EMAModule:
|
||||
"""Exponential Moving Average of Fairseq Models"""
|
||||
|
||||
def __init__(self, model, ema_decay=0.9999, ema_fp32=False, device=None, skip_keys=None):
|
||||
"""
|
||||
@param model model to initialize the EMA with
|
||||
@param config EMAConfig object with configuration like
|
||||
ema_decay, ema_update_freq, ema_fp32
|
||||
@param device If provided, copy EMA to this device (e.g. gpu).
|
||||
Otherwise EMA is in the same device as the model.
|
||||
"""
|
||||
|
||||
self.decay = ema_decay
|
||||
self.ema_fp32 = ema_fp32
|
||||
self.model = copy.deepcopy(model)
|
||||
self.model.requires_grad_(False)
|
||||
self.skip_keys = skip_keys or set()
|
||||
self.fp32_params = {}
|
||||
|
||||
if device is not None:
|
||||
logging.info(f"Copying EMA model to device {device}")
|
||||
self.model = self.model.to(device=device)
|
||||
|
||||
if self.ema_fp32:
|
||||
self.build_fp32_params()
|
||||
|
||||
self.update_freq_counter = 0
|
||||
|
||||
def build_fp32_params(self, state_dict=None):
|
||||
"""
|
||||
Store a copy of the EMA params in fp32.
|
||||
If state dict is passed, the EMA params is copied from
|
||||
the provided state dict. Otherwise, it is copied from the
|
||||
current EMA model parameters.
|
||||
"""
|
||||
if not self.ema_fp32:
|
||||
raise RuntimeError(
|
||||
"build_fp32_params should not be called if ema_fp32=False. "
|
||||
"Use ema_fp32=True if this is really intended."
|
||||
)
|
||||
|
||||
if state_dict is None:
|
||||
state_dict = self.model.state_dict()
|
||||
|
||||
def _to_float(t):
|
||||
"""Internal: to float.
|
||||
|
||||
Args:
|
||||
t: TODO.
|
||||
"""
|
||||
return t.float() if torch.is_floating_point(t) else t
|
||||
|
||||
for param_key in state_dict:
|
||||
if param_key in self.fp32_params:
|
||||
self.fp32_params[param_key].copy_(state_dict[param_key])
|
||||
else:
|
||||
self.fp32_params[param_key] = _to_float(state_dict[param_key])
|
||||
|
||||
def restore(self, state_dict, build_fp32_params=False):
|
||||
"""Load data from a model spec into EMA model"""
|
||||
self.model.load_state_dict(state_dict, strict=False)
|
||||
if build_fp32_params:
|
||||
self.build_fp32_params(state_dict)
|
||||
|
||||
def set_decay(self, decay):
|
||||
"""Set decay.
|
||||
|
||||
Args:
|
||||
decay: TODO.
|
||||
"""
|
||||
self.decay = decay
|
||||
|
||||
def get_decay(self):
|
||||
"""Get decay."""
|
||||
return self.decay
|
||||
|
||||
def _step_internal(self, new_model):
|
||||
"""One update of the EMA model based on new model weights"""
|
||||
decay = self.decay
|
||||
|
||||
ema_state_dict = {}
|
||||
ema_params = self.fp32_params if self.ema_fp32 else self.model.state_dict()
|
||||
for key, param in new_model.state_dict().items():
|
||||
if isinstance(param, dict):
|
||||
continue
|
||||
try:
|
||||
ema_param = ema_params[key]
|
||||
except KeyError:
|
||||
ema_param = param.float().clone() if param.ndim == 1 else copy.deepcopy(param)
|
||||
|
||||
if param.shape != ema_param.shape:
|
||||
raise ValueError(
|
||||
"incompatible tensor shapes between model param and ema param"
|
||||
+ "{} vs. {}".format(param.shape, ema_param.shape)
|
||||
)
|
||||
|
||||
if "version" in key:
|
||||
# Do not decay a model.version pytorch param
|
||||
continue
|
||||
|
||||
if key in self.skip_keys or (
|
||||
"num_batches_tracked" in key and ema_param.dtype == torch.int64
|
||||
):
|
||||
ema_param = param.to(dtype=ema_param.dtype).clone()
|
||||
ema_params[key].copy_(ema_param)
|
||||
else:
|
||||
ema_param.mul_(decay)
|
||||
ema_param.add_(param.to(dtype=ema_param.dtype), alpha=1 - decay)
|
||||
ema_state_dict[key] = ema_param
|
||||
self.restore(ema_state_dict, build_fp32_params=False)
|
||||
|
||||
def step(self, new_model):
|
||||
"""Step.
|
||||
|
||||
Args:
|
||||
new_model: New Model instance.
|
||||
"""
|
||||
self._step_internal(new_model)
|
||||
|
||||
def reverse(self, model):
|
||||
"""
|
||||
Load the model parameters from EMA model.
|
||||
Useful for inference or fine-tuning from the EMA model.
|
||||
"""
|
||||
d = self.model.state_dict()
|
||||
if "_ema" in d:
|
||||
del d["_ema"]
|
||||
|
||||
model.load_state_dict(d, strict=False)
|
||||
return model
|
||||
@@ -0,0 +1,31 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
class GradMultiply(torch.autograd.Function):
|
||||
@staticmethod
|
||||
def forward(ctx, x, scale):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
ctx: TODO.
|
||||
x: TODO.
|
||||
scale: TODO.
|
||||
"""
|
||||
ctx.scale = scale
|
||||
res = x.new(x)
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, grad):
|
||||
"""Backward.
|
||||
|
||||
Args:
|
||||
ctx: TODO.
|
||||
grad: TODO.
|
||||
"""
|
||||
return grad * ctx.scale, None
|
||||
@@ -0,0 +1,692 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import logging
|
||||
import math
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from torch import Tensor, nn
|
||||
from torch.nn import Parameter
|
||||
|
||||
from funasr.models.data2vec.quant_noise import quant_noise
|
||||
|
||||
|
||||
class FairseqDropout(nn.Module):
|
||||
def __init__(self, p, module_name=None):
|
||||
"""Initialize FairseqDropout.
|
||||
|
||||
Args:
|
||||
p: TODO.
|
||||
module_name: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
self.p = p
|
||||
self.module_name = module_name
|
||||
self.apply_during_inference = False
|
||||
|
||||
def forward(self, x, inplace: bool = False):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
inplace: TODO.
|
||||
"""
|
||||
if self.p > 0 and (self.training or self.apply_during_inference):
|
||||
return F.dropout(x, p=self.p, training=True, inplace=inplace)
|
||||
else:
|
||||
return x
|
||||
|
||||
def make_generation_fast_(
|
||||
self,
|
||||
name: str,
|
||||
retain_dropout: bool = False,
|
||||
retain_dropout_modules: Optional[List[str]] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Make generation fast .
|
||||
|
||||
Args:
|
||||
name: TODO.
|
||||
retain_dropout: TODO.
|
||||
retain_dropout_modules: TODO.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
if retain_dropout:
|
||||
if retain_dropout_modules is not None and self.module_name is None:
|
||||
logging.warning(
|
||||
"Cannot enable dropout during inference for module {} "
|
||||
"because module_name was not set".format(name)
|
||||
)
|
||||
elif (
|
||||
retain_dropout_modules is None # if None, apply to all modules
|
||||
or self.module_name in retain_dropout_modules
|
||||
):
|
||||
logging.info("Enabling dropout during inference for module: {}".format(name))
|
||||
self.apply_during_inference = True
|
||||
else:
|
||||
logging.info("Disabling dropout for module: {}".format(name))
|
||||
|
||||
|
||||
class MultiheadAttention(nn.Module):
|
||||
"""Multi-headed attention.
|
||||
|
||||
See "Attention Is All You Need" for more details.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
embed_dim,
|
||||
num_heads,
|
||||
kdim=None,
|
||||
vdim=None,
|
||||
dropout=0.0,
|
||||
bias=True,
|
||||
add_bias_kv=False,
|
||||
add_zero_attn=False,
|
||||
self_attention=False,
|
||||
encoder_decoder_attention=False,
|
||||
q_noise=0.0,
|
||||
qn_block_size=8,
|
||||
):
|
||||
"""Initialize MultiheadAttention.
|
||||
|
||||
Args:
|
||||
embed_dim: Size/dimension parameter.
|
||||
num_heads: TODO.
|
||||
kdim: TODO.
|
||||
vdim: TODO.
|
||||
dropout: TODO.
|
||||
bias: TODO.
|
||||
add_bias_kv: TODO.
|
||||
add_zero_attn: TODO.
|
||||
self_attention: TODO.
|
||||
encoder_decoder_attention: TODO.
|
||||
q_noise: TODO.
|
||||
qn_block_size: Size/dimension parameter.
|
||||
"""
|
||||
super().__init__()
|
||||
self.embed_dim = embed_dim
|
||||
self.kdim = kdim if kdim is not None else embed_dim
|
||||
self.vdim = vdim if vdim is not None else embed_dim
|
||||
self.qkv_same_dim = self.kdim == embed_dim and self.vdim == embed_dim
|
||||
|
||||
self.num_heads = num_heads
|
||||
self.dropout_module = FairseqDropout(dropout, module_name=self.__class__.__name__)
|
||||
|
||||
self.head_dim = embed_dim // num_heads
|
||||
assert (
|
||||
self.head_dim * num_heads == self.embed_dim
|
||||
), "embed_dim must be divisible by num_heads"
|
||||
self.scaling = self.head_dim**-0.5
|
||||
|
||||
self.self_attention = self_attention
|
||||
self.encoder_decoder_attention = encoder_decoder_attention
|
||||
|
||||
assert not self.self_attention or self.qkv_same_dim, (
|
||||
"Self-attention requires query, key and " "value to be of the same size"
|
||||
)
|
||||
|
||||
self.k_proj = quant_noise(
|
||||
nn.Linear(self.kdim, embed_dim, bias=bias), q_noise, qn_block_size
|
||||
)
|
||||
self.v_proj = quant_noise(
|
||||
nn.Linear(self.vdim, embed_dim, bias=bias), q_noise, qn_block_size
|
||||
)
|
||||
self.q_proj = quant_noise(
|
||||
nn.Linear(embed_dim, embed_dim, bias=bias), q_noise, qn_block_size
|
||||
)
|
||||
|
||||
self.out_proj = quant_noise(
|
||||
nn.Linear(embed_dim, embed_dim, bias=bias), q_noise, qn_block_size
|
||||
)
|
||||
|
||||
if add_bias_kv:
|
||||
self.bias_k = Parameter(torch.Tensor(1, 1, embed_dim))
|
||||
self.bias_v = Parameter(torch.Tensor(1, 1, embed_dim))
|
||||
else:
|
||||
self.bias_k = self.bias_v = None
|
||||
|
||||
self.add_zero_attn = add_zero_attn
|
||||
|
||||
self.reset_parameters()
|
||||
|
||||
self.onnx_trace = False
|
||||
self.skip_embed_dim_check = False
|
||||
|
||||
def prepare_for_onnx_export_(self):
|
||||
"""Prepare for onnx export ."""
|
||||
self.onnx_trace = True
|
||||
|
||||
def reset_parameters(self):
|
||||
"""Reset parameters."""
|
||||
if self.qkv_same_dim:
|
||||
# Empirically observed the convergence to be much better with
|
||||
# the scaled initialization
|
||||
nn.init.xavier_uniform_(self.k_proj.weight, gain=1 / math.sqrt(2))
|
||||
nn.init.xavier_uniform_(self.v_proj.weight, gain=1 / math.sqrt(2))
|
||||
nn.init.xavier_uniform_(self.q_proj.weight, gain=1 / math.sqrt(2))
|
||||
else:
|
||||
nn.init.xavier_uniform_(self.k_proj.weight)
|
||||
nn.init.xavier_uniform_(self.v_proj.weight)
|
||||
nn.init.xavier_uniform_(self.q_proj.weight)
|
||||
|
||||
nn.init.xavier_uniform_(self.out_proj.weight)
|
||||
if self.out_proj.bias is not None:
|
||||
nn.init.constant_(self.out_proj.bias, 0.0)
|
||||
if self.bias_k is not None:
|
||||
nn.init.xavier_normal_(self.bias_k)
|
||||
if self.bias_v is not None:
|
||||
nn.init.xavier_normal_(self.bias_v)
|
||||
|
||||
def _get_reserve_head_index(self, num_heads_to_keep: int):
|
||||
"""Internal: get reserve head index.
|
||||
|
||||
Args:
|
||||
num_heads_to_keep: TODO.
|
||||
"""
|
||||
k_proj_heads_norm = []
|
||||
q_proj_heads_norm = []
|
||||
v_proj_heads_norm = []
|
||||
|
||||
for i in range(self.num_heads):
|
||||
start_idx = i * self.head_dim
|
||||
end_idx = (i + 1) * self.head_dim
|
||||
k_proj_heads_norm.append(
|
||||
torch.sum(torch.abs(self.k_proj.weight[start_idx:end_idx,])).tolist()
|
||||
+ torch.sum(torch.abs(self.k_proj.bias[start_idx:end_idx])).tolist()
|
||||
)
|
||||
q_proj_heads_norm.append(
|
||||
torch.sum(torch.abs(self.q_proj.weight[start_idx:end_idx,])).tolist()
|
||||
+ torch.sum(torch.abs(self.q_proj.bias[start_idx:end_idx])).tolist()
|
||||
)
|
||||
v_proj_heads_norm.append(
|
||||
torch.sum(torch.abs(self.v_proj.weight[start_idx:end_idx,])).tolist()
|
||||
+ torch.sum(torch.abs(self.v_proj.bias[start_idx:end_idx])).tolist()
|
||||
)
|
||||
|
||||
heads_norm = []
|
||||
for i in range(self.num_heads):
|
||||
heads_norm.append(k_proj_heads_norm[i] + q_proj_heads_norm[i] + v_proj_heads_norm[i])
|
||||
|
||||
sorted_head_index = sorted(range(self.num_heads), key=lambda k: heads_norm[k], reverse=True)
|
||||
reserve_head_index = []
|
||||
for i in range(num_heads_to_keep):
|
||||
start = sorted_head_index[i] * self.head_dim
|
||||
end = (sorted_head_index[i] + 1) * self.head_dim
|
||||
reserve_head_index.append((start, end))
|
||||
return reserve_head_index
|
||||
|
||||
def _adaptive_prune_heads(self, reserve_head_index: List[Tuple[int, int]]):
|
||||
"""Internal: adaptive prune heads.
|
||||
|
||||
Args:
|
||||
reserve_head_index: TODO.
|
||||
"""
|
||||
new_q_weight = []
|
||||
new_q_bias = []
|
||||
new_k_weight = []
|
||||
new_k_bias = []
|
||||
new_v_weight = []
|
||||
new_v_bias = []
|
||||
new_out_proj_weight = []
|
||||
|
||||
for ele in reserve_head_index:
|
||||
start_idx, end_idx = ele
|
||||
new_q_weight.append(self.q_proj.weight[start_idx:end_idx,])
|
||||
new_q_bias.append(self.q_proj.bias[start_idx:end_idx])
|
||||
|
||||
new_k_weight.append(self.k_proj.weight[start_idx:end_idx,])
|
||||
|
||||
new_k_bias.append(self.k_proj.bias[start_idx:end_idx])
|
||||
|
||||
new_v_weight.append(self.v_proj.weight[start_idx:end_idx,])
|
||||
new_v_bias.append(self.v_proj.bias[start_idx:end_idx])
|
||||
|
||||
new_out_proj_weight.append(self.out_proj.weight[:, start_idx:end_idx])
|
||||
|
||||
new_q_weight = torch.cat(new_q_weight).detach()
|
||||
new_k_weight = torch.cat(new_k_weight).detach()
|
||||
new_v_weight = torch.cat(new_v_weight).detach()
|
||||
new_out_proj_weight = torch.cat(new_out_proj_weight, dim=-1).detach()
|
||||
new_q_weight.requires_grad = True
|
||||
new_k_weight.requires_grad = True
|
||||
new_v_weight.requires_grad = True
|
||||
new_out_proj_weight.requires_grad = True
|
||||
|
||||
new_q_bias = torch.cat(new_q_bias).detach()
|
||||
new_q_bias.requires_grad = True
|
||||
|
||||
new_k_bias = torch.cat(new_k_bias).detach()
|
||||
new_k_bias.requires_grad = True
|
||||
|
||||
new_v_bias = torch.cat(new_v_bias).detach()
|
||||
new_v_bias.requires_grad = True
|
||||
|
||||
self.q_proj.weight = torch.nn.Parameter(new_q_weight)
|
||||
self.q_proj.bias = torch.nn.Parameter(new_q_bias)
|
||||
|
||||
self.k_proj.weight = torch.nn.Parameter(new_k_weight)
|
||||
self.k_proj.bias = torch.nn.Parameter(new_k_bias)
|
||||
|
||||
self.v_proj.weight = torch.nn.Parameter(new_v_weight)
|
||||
self.v_proj.bias = torch.nn.Parameter(new_v_bias)
|
||||
|
||||
self.out_proj.weight = torch.nn.Parameter(new_out_proj_weight)
|
||||
|
||||
self.num_heads = len(reserve_head_index)
|
||||
self.embed_dim = self.head_dim * self.num_heads
|
||||
self.q_proj.out_features = self.embed_dim
|
||||
self.k_proj.out_features = self.embed_dim
|
||||
self.v_proj.out_features = self.embed_dim
|
||||
|
||||
def _set_skip_embed_dim_check(self):
|
||||
"""Internal: set skip embed dim check."""
|
||||
self.skip_embed_dim_check = True
|
||||
|
||||
def forward(
|
||||
self,
|
||||
query,
|
||||
key: Optional[Tensor],
|
||||
value: Optional[Tensor],
|
||||
key_padding_mask: Optional[Tensor] = None,
|
||||
incremental_state: Optional[Dict[str, Dict[str, Optional[Tensor]]]] = None,
|
||||
need_weights: bool = True,
|
||||
static_kv: bool = False,
|
||||
attn_mask: Optional[Tensor] = None,
|
||||
before_softmax: bool = False,
|
||||
need_head_weights: bool = False,
|
||||
) -> Tuple[Tensor, Optional[Tensor]]:
|
||||
"""Input shape: Time x Batch x Channel
|
||||
|
||||
Args:
|
||||
key_padding_mask (ByteTensor, optional): mask to exclude
|
||||
keys that are pads, of shape `(batch, src_len)`, where
|
||||
padding elements are indicated by 1s.
|
||||
need_weights (bool, optional): return the attention weights,
|
||||
averaged over heads (default: False).
|
||||
attn_mask (ByteTensor, optional): typically used to
|
||||
implement causal attention, where the mask prevents the
|
||||
attention from looking forward in time (default: None).
|
||||
before_softmax (bool, optional): return the raw attention
|
||||
weights and values before the attention softmax.
|
||||
need_head_weights (bool, optional): return the attention
|
||||
weights for each head. Implies *need_weights*. Default:
|
||||
return the average attention weights over all heads.
|
||||
"""
|
||||
if need_head_weights:
|
||||
need_weights = True
|
||||
|
||||
is_tpu = query.device.type == "xla"
|
||||
|
||||
tgt_len, bsz, embed_dim = query.size()
|
||||
src_len = tgt_len
|
||||
if not self.skip_embed_dim_check:
|
||||
assert embed_dim == self.embed_dim, f"query dim {embed_dim} != {self.embed_dim}"
|
||||
assert list(query.size()) == [tgt_len, bsz, embed_dim]
|
||||
if key is not None:
|
||||
src_len, key_bsz, _ = key.size()
|
||||
if not torch.jit.is_scripting():
|
||||
assert key_bsz == bsz
|
||||
assert value is not None
|
||||
assert src_len, bsz == value.shape[:2]
|
||||
|
||||
if (
|
||||
not self.onnx_trace
|
||||
and not is_tpu # don't use PyTorch version on TPUs
|
||||
and incremental_state is None
|
||||
and not static_kv
|
||||
# A workaround for quantization to work. Otherwise JIT compilation
|
||||
# treats bias in linear module as method.
|
||||
and not torch.jit.is_scripting()
|
||||
# The Multihead attention implemented in pytorch forces strong dimension check
|
||||
# for input embedding dimention and K,Q,V projection dimension.
|
||||
# Since pruning will break the dimension check and it is not easy to modify the pytorch API,
|
||||
# it is preferred to bypass the pytorch MHA when we need to skip embed_dim_check
|
||||
and not self.skip_embed_dim_check
|
||||
):
|
||||
assert key is not None and value is not None
|
||||
return F.multi_head_attention_forward(
|
||||
query,
|
||||
key,
|
||||
value,
|
||||
self.embed_dim,
|
||||
self.num_heads,
|
||||
torch.empty([0]),
|
||||
torch.cat((self.q_proj.bias, self.k_proj.bias, self.v_proj.bias)),
|
||||
self.bias_k,
|
||||
self.bias_v,
|
||||
self.add_zero_attn,
|
||||
self.dropout_module.p,
|
||||
self.out_proj.weight,
|
||||
self.out_proj.bias,
|
||||
self.training or self.dropout_module.apply_during_inference,
|
||||
key_padding_mask,
|
||||
need_weights,
|
||||
attn_mask,
|
||||
use_separate_proj_weight=True,
|
||||
q_proj_weight=self.q_proj.weight,
|
||||
k_proj_weight=self.k_proj.weight,
|
||||
v_proj_weight=self.v_proj.weight,
|
||||
)
|
||||
|
||||
if incremental_state is not None:
|
||||
saved_state = self._get_input_buffer(incremental_state)
|
||||
if saved_state is not None and "prev_key" in saved_state:
|
||||
# previous time steps are cached - no need to recompute
|
||||
# key and value if they are static
|
||||
if static_kv:
|
||||
assert self.encoder_decoder_attention and not self.self_attention
|
||||
key = value = None
|
||||
else:
|
||||
saved_state = None
|
||||
|
||||
if self.self_attention:
|
||||
q = self.q_proj(query)
|
||||
k = self.k_proj(query)
|
||||
v = self.v_proj(query)
|
||||
elif self.encoder_decoder_attention:
|
||||
# encoder-decoder attention
|
||||
q = self.q_proj(query)
|
||||
if key is None:
|
||||
assert value is None
|
||||
k = v = None
|
||||
else:
|
||||
k = self.k_proj(key)
|
||||
v = self.v_proj(key)
|
||||
|
||||
else:
|
||||
assert key is not None and value is not None
|
||||
q = self.q_proj(query)
|
||||
k = self.k_proj(key)
|
||||
v = self.v_proj(value)
|
||||
q *= self.scaling
|
||||
|
||||
if self.bias_k is not None:
|
||||
assert self.bias_v is not None
|
||||
k = torch.cat([k, self.bias_k.repeat(1, bsz, 1)])
|
||||
v = torch.cat([v, self.bias_v.repeat(1, bsz, 1)])
|
||||
if attn_mask is not None:
|
||||
attn_mask = torch.cat([attn_mask, attn_mask.new_zeros(attn_mask.size(0), 1)], dim=1)
|
||||
if key_padding_mask is not None:
|
||||
key_padding_mask = torch.cat(
|
||||
[
|
||||
key_padding_mask,
|
||||
key_padding_mask.new_zeros(key_padding_mask.size(0), 1),
|
||||
],
|
||||
dim=1,
|
||||
)
|
||||
|
||||
q = q.contiguous().view(tgt_len, bsz * self.num_heads, self.head_dim).transpose(0, 1)
|
||||
if k is not None:
|
||||
k = k.contiguous().view(-1, bsz * self.num_heads, self.head_dim).transpose(0, 1)
|
||||
if v is not None:
|
||||
v = v.contiguous().view(-1, bsz * self.num_heads, self.head_dim).transpose(0, 1)
|
||||
|
||||
if saved_state is not None:
|
||||
# saved states are stored with shape (bsz, num_heads, seq_len, head_dim)
|
||||
if "prev_key" in saved_state:
|
||||
_prev_key = saved_state["prev_key"]
|
||||
assert _prev_key is not None
|
||||
prev_key = _prev_key.view(bsz * self.num_heads, -1, self.head_dim)
|
||||
if static_kv:
|
||||
k = prev_key
|
||||
else:
|
||||
assert k is not None
|
||||
k = torch.cat([prev_key, k], dim=1)
|
||||
src_len = k.size(1)
|
||||
if "prev_value" in saved_state:
|
||||
_prev_value = saved_state["prev_value"]
|
||||
assert _prev_value is not None
|
||||
prev_value = _prev_value.view(bsz * self.num_heads, -1, self.head_dim)
|
||||
if static_kv:
|
||||
v = prev_value
|
||||
else:
|
||||
assert v is not None
|
||||
v = torch.cat([prev_value, v], dim=1)
|
||||
prev_key_padding_mask: Optional[Tensor] = None
|
||||
if "prev_key_padding_mask" in saved_state:
|
||||
prev_key_padding_mask = saved_state["prev_key_padding_mask"]
|
||||
assert k is not None and v is not None
|
||||
key_padding_mask = MultiheadAttention._append_prev_key_padding_mask(
|
||||
key_padding_mask=key_padding_mask,
|
||||
prev_key_padding_mask=prev_key_padding_mask,
|
||||
batch_size=bsz,
|
||||
src_len=k.size(1),
|
||||
static_kv=static_kv,
|
||||
)
|
||||
|
||||
saved_state["prev_key"] = k.view(bsz, self.num_heads, -1, self.head_dim)
|
||||
saved_state["prev_value"] = v.view(bsz, self.num_heads, -1, self.head_dim)
|
||||
saved_state["prev_key_padding_mask"] = key_padding_mask
|
||||
# In this branch incremental_state is never None
|
||||
assert incremental_state is not None
|
||||
incremental_state = self._set_input_buffer(incremental_state, saved_state)
|
||||
assert k is not None
|
||||
assert k.size(1) == src_len
|
||||
|
||||
# This is part of a workaround to get around fork/join parallelism
|
||||
# not supporting Optional types.
|
||||
if key_padding_mask is not None and key_padding_mask.dim() == 0:
|
||||
key_padding_mask = None
|
||||
|
||||
if key_padding_mask is not None:
|
||||
assert key_padding_mask.size(0) == bsz
|
||||
assert key_padding_mask.size(1) == src_len
|
||||
|
||||
if self.add_zero_attn:
|
||||
assert v is not None
|
||||
src_len += 1
|
||||
k = torch.cat([k, k.new_zeros((k.size(0), 1) + k.size()[2:])], dim=1)
|
||||
v = torch.cat([v, v.new_zeros((v.size(0), 1) + v.size()[2:])], dim=1)
|
||||
if attn_mask is not None:
|
||||
attn_mask = torch.cat([attn_mask, attn_mask.new_zeros(attn_mask.size(0), 1)], dim=1)
|
||||
if key_padding_mask is not None:
|
||||
key_padding_mask = torch.cat(
|
||||
[
|
||||
key_padding_mask,
|
||||
torch.zeros(key_padding_mask.size(0), 1).type_as(key_padding_mask),
|
||||
],
|
||||
dim=1,
|
||||
)
|
||||
|
||||
attn_weights = torch.bmm(q, k.transpose(1, 2))
|
||||
attn_weights = self.apply_sparse_mask(attn_weights, tgt_len, src_len, bsz)
|
||||
|
||||
assert list(attn_weights.size()) == [bsz * self.num_heads, tgt_len, src_len]
|
||||
|
||||
if attn_mask is not None:
|
||||
attn_mask = attn_mask.unsqueeze(0)
|
||||
if self.onnx_trace:
|
||||
attn_mask = attn_mask.repeat(attn_weights.size(0), 1, 1)
|
||||
attn_weights += attn_mask
|
||||
|
||||
if key_padding_mask is not None:
|
||||
# don't attend to padding symbols
|
||||
attn_weights = attn_weights.view(bsz, self.num_heads, tgt_len, src_len)
|
||||
if not is_tpu:
|
||||
attn_weights = attn_weights.masked_fill(
|
||||
key_padding_mask.unsqueeze(1).unsqueeze(2).to(torch.bool),
|
||||
float("-inf"),
|
||||
)
|
||||
else:
|
||||
attn_weights = attn_weights.transpose(0, 2)
|
||||
attn_weights = attn_weights.masked_fill(key_padding_mask, float("-inf"))
|
||||
attn_weights = attn_weights.transpose(0, 2)
|
||||
attn_weights = attn_weights.view(bsz * self.num_heads, tgt_len, src_len)
|
||||
|
||||
if before_softmax:
|
||||
return attn_weights, v
|
||||
|
||||
attn_weights_float = F.softmax(attn_weights, dim=-1, dtype=torch.float32)
|
||||
attn_weights = attn_weights_float.type_as(attn_weights)
|
||||
attn_probs = self.dropout_module(attn_weights)
|
||||
|
||||
assert v is not None
|
||||
attn = torch.bmm(attn_probs, v)
|
||||
assert list(attn.size()) == [bsz * self.num_heads, tgt_len, self.head_dim]
|
||||
if self.onnx_trace and attn.size(1) == 1:
|
||||
# when ONNX tracing a single decoder step (sequence length == 1)
|
||||
# the transpose is a no-op copy before view, thus unnecessary
|
||||
attn = attn.contiguous().view(tgt_len, bsz, self.embed_dim)
|
||||
else:
|
||||
attn = attn.transpose(0, 1).contiguous().view(tgt_len, bsz, self.embed_dim)
|
||||
attn = self.out_proj(attn)
|
||||
attn_weights: Optional[Tensor] = None
|
||||
if need_weights:
|
||||
attn_weights = attn_weights_float.view(bsz, self.num_heads, tgt_len, src_len).transpose(
|
||||
1, 0
|
||||
)
|
||||
if not need_head_weights:
|
||||
# average attention weights over heads
|
||||
attn_weights = attn_weights.mean(dim=0)
|
||||
|
||||
return attn, attn_weights
|
||||
|
||||
@staticmethod
|
||||
def _append_prev_key_padding_mask(
|
||||
key_padding_mask: Optional[Tensor],
|
||||
prev_key_padding_mask: Optional[Tensor],
|
||||
batch_size: int,
|
||||
src_len: int,
|
||||
static_kv: bool,
|
||||
) -> Optional[Tensor]:
|
||||
# saved key padding masks have shape (bsz, seq_len)
|
||||
"""Internal: append prev key padding mask.
|
||||
|
||||
Args:
|
||||
key_padding_mask: TODO.
|
||||
prev_key_padding_mask: TODO.
|
||||
batch_size: Number of samples per batch.
|
||||
src_len: TODO.
|
||||
static_kv: TODO.
|
||||
"""
|
||||
if prev_key_padding_mask is not None and static_kv:
|
||||
new_key_padding_mask = prev_key_padding_mask
|
||||
elif prev_key_padding_mask is not None and key_padding_mask is not None:
|
||||
new_key_padding_mask = torch.cat(
|
||||
[prev_key_padding_mask.float(), key_padding_mask.float()], dim=1
|
||||
)
|
||||
# During incremental decoding, as the padding token enters and
|
||||
# leaves the frame, there will be a time when prev or current
|
||||
# is None
|
||||
elif prev_key_padding_mask is not None:
|
||||
if src_len > prev_key_padding_mask.size(1):
|
||||
filler = torch.zeros(
|
||||
(batch_size, src_len - prev_key_padding_mask.size(1)),
|
||||
device=prev_key_padding_mask.device,
|
||||
)
|
||||
new_key_padding_mask = torch.cat(
|
||||
[prev_key_padding_mask.float(), filler.float()], dim=1
|
||||
)
|
||||
else:
|
||||
new_key_padding_mask = prev_key_padding_mask.float()
|
||||
elif key_padding_mask is not None:
|
||||
if src_len > key_padding_mask.size(1):
|
||||
filler = torch.zeros(
|
||||
(batch_size, src_len - key_padding_mask.size(1)),
|
||||
device=key_padding_mask.device,
|
||||
)
|
||||
new_key_padding_mask = torch.cat([filler.float(), key_padding_mask.float()], dim=1)
|
||||
else:
|
||||
new_key_padding_mask = key_padding_mask.float()
|
||||
else:
|
||||
new_key_padding_mask = prev_key_padding_mask
|
||||
return new_key_padding_mask
|
||||
|
||||
@torch.jit.export
|
||||
def reorder_incremental_state(
|
||||
self,
|
||||
incremental_state: Dict[str, Dict[str, Optional[Tensor]]],
|
||||
new_order: Tensor,
|
||||
):
|
||||
"""Reorder buffered internal state (for incremental generation)."""
|
||||
input_buffer = self._get_input_buffer(incremental_state)
|
||||
if input_buffer is not None:
|
||||
for k in input_buffer.keys():
|
||||
input_buffer_k = input_buffer[k]
|
||||
if input_buffer_k is not None:
|
||||
if self.encoder_decoder_attention and input_buffer_k.size(0) == new_order.size(
|
||||
0
|
||||
):
|
||||
break
|
||||
input_buffer[k] = input_buffer_k.index_select(0, new_order)
|
||||
incremental_state = self._set_input_buffer(incremental_state, input_buffer)
|
||||
return incremental_state
|
||||
|
||||
def _get_input_buffer(
|
||||
self, incremental_state: Optional[Dict[str, Dict[str, Optional[Tensor]]]]
|
||||
) -> Dict[str, Optional[Tensor]]:
|
||||
"""Internal: get input buffer.
|
||||
|
||||
Args:
|
||||
incremental_state: TODO.
|
||||
"""
|
||||
result = self.get_incremental_state(incremental_state, "attn_state")
|
||||
if result is not None:
|
||||
return result
|
||||
else:
|
||||
empty_result: Dict[str, Optional[Tensor]] = {}
|
||||
return empty_result
|
||||
|
||||
def _set_input_buffer(
|
||||
self,
|
||||
incremental_state: Dict[str, Dict[str, Optional[Tensor]]],
|
||||
buffer: Dict[str, Optional[Tensor]],
|
||||
):
|
||||
"""Internal: set input buffer.
|
||||
|
||||
Args:
|
||||
incremental_state: TODO.
|
||||
buffer: TODO.
|
||||
"""
|
||||
return self.set_incremental_state(incremental_state, "attn_state", buffer)
|
||||
|
||||
def apply_sparse_mask(self, attn_weights, tgt_len: int, src_len: int, bsz: int):
|
||||
"""Apply sparse mask.
|
||||
|
||||
Args:
|
||||
attn_weights: TODO.
|
||||
tgt_len: TODO.
|
||||
src_len: TODO.
|
||||
bsz: TODO.
|
||||
"""
|
||||
return attn_weights
|
||||
|
||||
def upgrade_state_dict_named(self, state_dict, name):
|
||||
"""Upgrade state dict named.
|
||||
|
||||
Args:
|
||||
state_dict: TODO.
|
||||
name: TODO.
|
||||
"""
|
||||
prefix = name + "." if name != "" else ""
|
||||
items_to_add = {}
|
||||
keys_to_remove = []
|
||||
for k in state_dict.keys():
|
||||
if k.endswith(prefix + "in_proj_weight"):
|
||||
# in_proj_weight used to be q + k + v with same dimensions
|
||||
dim = int(state_dict[k].shape[0] / 3)
|
||||
items_to_add[prefix + "q_proj.weight"] = state_dict[k][:dim]
|
||||
items_to_add[prefix + "k_proj.weight"] = state_dict[k][dim : 2 * dim]
|
||||
items_to_add[prefix + "v_proj.weight"] = state_dict[k][2 * dim :]
|
||||
|
||||
keys_to_remove.append(k)
|
||||
|
||||
k_bias = prefix + "in_proj_bias"
|
||||
if k_bias in state_dict.keys():
|
||||
dim = int(state_dict[k].shape[0] / 3)
|
||||
items_to_add[prefix + "q_proj.bias"] = state_dict[k_bias][:dim]
|
||||
items_to_add[prefix + "k_proj.bias"] = state_dict[k_bias][dim : 2 * dim]
|
||||
items_to_add[prefix + "v_proj.bias"] = state_dict[k_bias][2 * dim :]
|
||||
|
||||
keys_to_remove.append(prefix + "in_proj_bias")
|
||||
|
||||
for k in keys_to_remove:
|
||||
del state_dict[k]
|
||||
|
||||
for key, value in items_to_add.items():
|
||||
state_dict[key] = value
|
||||
@@ -0,0 +1,107 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
|
||||
def quant_noise(module, p, block_size):
|
||||
"""
|
||||
Wraps modules and applies quantization noise to the weights for
|
||||
subsequent quantization with Iterative Product Quantization as
|
||||
described in "Training with Quantization Noise for Extreme Model Compression"
|
||||
|
||||
Args:
|
||||
- module: nn.Module
|
||||
- p: amount of Quantization Noise
|
||||
- block_size: size of the blocks for subsequent quantization with iPQ
|
||||
|
||||
Remarks:
|
||||
- Module weights must have the right sizes wrt the block size
|
||||
- Only Linear, Embedding and Conv2d modules are supported for the moment
|
||||
- For more detail on how to quantize by blocks with convolutional weights,
|
||||
see "And the Bit Goes Down: Revisiting the Quantization of Neural Networks"
|
||||
- We implement the simplest form of noise here as stated in the paper
|
||||
which consists in randomly dropping blocks
|
||||
"""
|
||||
|
||||
# if no quantization noise, don't register hook
|
||||
if p <= 0:
|
||||
return module
|
||||
|
||||
# supported modules
|
||||
assert isinstance(module, (nn.Linear, nn.Embedding, nn.Conv2d))
|
||||
|
||||
# test whether module.weight has the right sizes wrt block_size
|
||||
is_conv = module.weight.ndim == 4
|
||||
|
||||
# 2D matrix
|
||||
if not is_conv:
|
||||
assert (
|
||||
module.weight.size(1) % block_size == 0
|
||||
), "Input features must be a multiple of block sizes"
|
||||
|
||||
# 4D matrix
|
||||
else:
|
||||
# 1x1 convolutions
|
||||
if module.kernel_size == (1, 1):
|
||||
assert (
|
||||
module.in_channels % block_size == 0
|
||||
), "Input channels must be a multiple of block sizes"
|
||||
# regular convolutions
|
||||
else:
|
||||
k = module.kernel_size[0] * module.kernel_size[1]
|
||||
assert k % block_size == 0, "Kernel size must be a multiple of block size"
|
||||
|
||||
def _forward_pre_hook(mod, input):
|
||||
# no noise for evaluation
|
||||
"""Internal: forward pre hook.
|
||||
|
||||
Args:
|
||||
mod: TODO.
|
||||
input: Input audio/text data.
|
||||
"""
|
||||
if mod.training:
|
||||
if not is_conv:
|
||||
# gather weight and sizes
|
||||
weight = mod.weight
|
||||
in_features = weight.size(1)
|
||||
out_features = weight.size(0)
|
||||
|
||||
# split weight matrix into blocks and randomly drop selected blocks
|
||||
mask = torch.zeros(in_features // block_size * out_features, device=weight.device)
|
||||
mask.bernoulli_(p)
|
||||
mask = mask.repeat_interleave(block_size, -1).view(-1, in_features)
|
||||
|
||||
else:
|
||||
# gather weight and sizes
|
||||
weight = mod.weight
|
||||
in_channels = mod.in_channels
|
||||
out_channels = mod.out_channels
|
||||
|
||||
# split weight matrix into blocks and randomly drop selected blocks
|
||||
if mod.kernel_size == (1, 1):
|
||||
mask = torch.zeros(
|
||||
int(in_channels // block_size * out_channels),
|
||||
device=weight.device,
|
||||
)
|
||||
mask.bernoulli_(p)
|
||||
mask = mask.repeat_interleave(block_size, -1).view(-1, in_channels)
|
||||
else:
|
||||
mask = torch.zeros(weight.size(0), weight.size(1), device=weight.device)
|
||||
mask.bernoulli_(p)
|
||||
mask = (
|
||||
mask.unsqueeze(2)
|
||||
.unsqueeze(3)
|
||||
.repeat(1, 1, mod.kernel_size[0], mod.kernel_size[1])
|
||||
)
|
||||
|
||||
# scale weights and apply mask
|
||||
mask = mask.to(torch.bool) # x.bool() is not currently supported in TorchScript
|
||||
s = 1 / (1 - p)
|
||||
mod.weight.data = s * weight.masked_fill(mask, 0)
|
||||
|
||||
module.register_forward_pre_hook(_forward_pre_hook)
|
||||
return module
|
||||
@@ -0,0 +1,221 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import math
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from funasr.models.data2vec.multihead_attention import MultiheadAttention
|
||||
|
||||
|
||||
class Fp32LayerNorm(nn.LayerNorm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize Fp32LayerNorm.
|
||||
|
||||
Args:
|
||||
*args: Variable positional arguments.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def forward(self, input):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
input: Input audio/text data.
|
||||
"""
|
||||
output = F.layer_norm(
|
||||
input.float(),
|
||||
self.normalized_shape,
|
||||
self.weight.float() if self.weight is not None else None,
|
||||
self.bias.float() if self.bias is not None else None,
|
||||
self.eps,
|
||||
)
|
||||
return output.type_as(input)
|
||||
|
||||
|
||||
class Fp32GroupNorm(nn.GroupNorm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize Fp32GroupNorm.
|
||||
|
||||
Args:
|
||||
*args: Variable positional arguments.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def forward(self, input):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
input: Input audio/text data.
|
||||
"""
|
||||
output = F.group_norm(
|
||||
input.float(),
|
||||
self.num_groups,
|
||||
self.weight.float() if self.weight is not None else None,
|
||||
self.bias.float() if self.bias is not None else None,
|
||||
self.eps,
|
||||
)
|
||||
return output.type_as(input)
|
||||
|
||||
|
||||
class TransposeLast(nn.Module):
|
||||
def __init__(self, deconstruct_idx=None):
|
||||
"""Initialize TransposeLast.
|
||||
|
||||
Args:
|
||||
deconstruct_idx: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
self.deconstruct_idx = deconstruct_idx
|
||||
|
||||
def forward(self, x):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
if self.deconstruct_idx is not None:
|
||||
x = x[self.deconstruct_idx]
|
||||
return x.transpose(-2, -1)
|
||||
|
||||
|
||||
class SamePad(nn.Module):
|
||||
def __init__(self, kernel_size, causal=False):
|
||||
"""Initialize SamePad.
|
||||
|
||||
Args:
|
||||
kernel_size: Size/dimension parameter.
|
||||
causal: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
if causal:
|
||||
self.remove = kernel_size - 1
|
||||
else:
|
||||
self.remove = 1 if kernel_size % 2 == 0 else 0
|
||||
|
||||
def forward(self, x):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
if self.remove > 0:
|
||||
x = x[:, :, : -self.remove]
|
||||
return x
|
||||
|
||||
|
||||
def pad_to_multiple(x, multiple, dim=-1, value=0):
|
||||
# Inspired from https://github.com/lucidrains/local-attention/blob/master/local_attention/local_attention.py#L41
|
||||
"""Pad to multiple.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
multiple: TODO.
|
||||
dim: TODO.
|
||||
value: TODO.
|
||||
"""
|
||||
if x is None:
|
||||
return None, 0
|
||||
tsz = x.size(dim)
|
||||
m = tsz / multiple
|
||||
remainder = math.ceil(m) * multiple - tsz
|
||||
if m.is_integer():
|
||||
return x, 0
|
||||
pad_offset = (0,) * (-1 - dim) * 2
|
||||
|
||||
return F.pad(x, (*pad_offset, 0, remainder), value=value), remainder
|
||||
|
||||
|
||||
def gelu_accurate(x):
|
||||
"""Gelu accurate.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
if not hasattr(gelu_accurate, "_a"):
|
||||
gelu_accurate._a = math.sqrt(2 / math.pi)
|
||||
return 0.5 * x * (1 + torch.tanh(gelu_accurate._a * (x + 0.044715 * torch.pow(x, 3))))
|
||||
|
||||
|
||||
def gelu(x: torch.Tensor) -> torch.Tensor:
|
||||
"""Gelu.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
return torch.nn.functional.gelu(x.float()).type_as(x)
|
||||
|
||||
|
||||
def get_available_activation_fns():
|
||||
"""Get available activation fns."""
|
||||
return [
|
||||
"relu",
|
||||
"gelu",
|
||||
"gelu_fast", # deprecated
|
||||
"gelu_accurate",
|
||||
"tanh",
|
||||
"linear",
|
||||
]
|
||||
|
||||
|
||||
def get_activation_fn(activation: str):
|
||||
"""Returns the activation function corresponding to `activation`"""
|
||||
|
||||
if activation == "relu":
|
||||
return F.relu
|
||||
elif activation == "gelu":
|
||||
return gelu
|
||||
elif activation == "gelu_accurate":
|
||||
return gelu_accurate
|
||||
elif activation == "tanh":
|
||||
return torch.tanh
|
||||
elif activation == "linear":
|
||||
return lambda x: x
|
||||
elif activation == "swish":
|
||||
return torch.nn.SiLU
|
||||
else:
|
||||
raise RuntimeError("--activation-fn {} not supported".format(activation))
|
||||
|
||||
|
||||
def init_bert_params(module):
|
||||
"""
|
||||
Initialize the weights specific to the BERT Model.
|
||||
This overrides the default initializations depending on the specified arguments.
|
||||
1. If normal_init_linear_weights is set then weights of linear
|
||||
layer will be initialized using the normal distribution and
|
||||
bais will be set to the specified value.
|
||||
2. If normal_init_embed_weights is set then weights of embedding
|
||||
layer will be initialized using the normal distribution.
|
||||
3. If normal_init_proj_weights is set then weights of
|
||||
in_project_weight for MultiHeadAttention initialized using
|
||||
the normal distribution (to be validated).
|
||||
"""
|
||||
|
||||
def normal_(data):
|
||||
# with FSDP, module params will be on CUDA, so we cast them back to CPU
|
||||
# so that the RNG is consistent with and without FSDP
|
||||
"""Normal .
|
||||
|
||||
Args:
|
||||
data: TODO.
|
||||
"""
|
||||
data.copy_(data.cpu().normal_(mean=0.0, std=0.02).to(data.device))
|
||||
|
||||
if isinstance(module, nn.Linear):
|
||||
normal_(module.weight.data)
|
||||
if module.bias is not None:
|
||||
module.bias.data.zero_()
|
||||
if isinstance(module, nn.Embedding):
|
||||
normal_(module.weight.data)
|
||||
if module.padding_idx is not None:
|
||||
module.weight.data[module.padding_idx].zero_()
|
||||
if isinstance(module, MultiheadAttention):
|
||||
normal_(module.q_proj.weight.data)
|
||||
normal_(module.k_proj.weight.data)
|
||||
normal_(module.v_proj.weight.data)
|
||||
@@ -0,0 +1,497 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import logging
|
||||
import math
|
||||
from typing import List, Tuple
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from funasr.models.data2vec import utils
|
||||
from funasr.models.data2vec.multihead_attention import MultiheadAttention
|
||||
|
||||
|
||||
class ConvFeatureExtractionModel(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
conv_layers: List[Tuple[int, int, int]],
|
||||
dropout: float = 0.0,
|
||||
mode: str = "default",
|
||||
conv_bias: bool = False,
|
||||
in_d: int = 1,
|
||||
):
|
||||
"""Initialize ConvFeatureExtractionModel.
|
||||
|
||||
Args:
|
||||
conv_layers: TODO.
|
||||
dropout: TODO.
|
||||
mode: TODO.
|
||||
conv_bias: TODO.
|
||||
in_d: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
assert mode in {"default", "layer_norm"}
|
||||
|
||||
def block(
|
||||
n_in,
|
||||
n_out,
|
||||
k,
|
||||
stride,
|
||||
is_layer_norm=False,
|
||||
is_group_norm=False,
|
||||
conv_bias=False,
|
||||
):
|
||||
"""Block.
|
||||
|
||||
Args:
|
||||
n_in: TODO.
|
||||
n_out: TODO.
|
||||
k: TODO.
|
||||
stride: TODO.
|
||||
is_layer_norm: Boolean flag for layer norm.
|
||||
is_group_norm: Boolean flag for group norm.
|
||||
conv_bias: TODO.
|
||||
"""
|
||||
def make_conv():
|
||||
"""Make conv."""
|
||||
conv = nn.Conv1d(n_in, n_out, k, stride=stride, bias=conv_bias)
|
||||
nn.init.kaiming_normal_(conv.weight)
|
||||
return conv
|
||||
|
||||
assert (
|
||||
is_layer_norm and is_group_norm
|
||||
) == False, "layer norm and group norm are exclusive"
|
||||
|
||||
if is_layer_norm:
|
||||
return nn.Sequential(
|
||||
make_conv(),
|
||||
nn.Dropout(p=dropout),
|
||||
nn.Sequential(
|
||||
utils.TransposeLast(),
|
||||
utils.Fp32LayerNorm(dim, elementwise_affine=True),
|
||||
utils.TransposeLast(),
|
||||
),
|
||||
nn.GELU(),
|
||||
)
|
||||
elif is_group_norm:
|
||||
return nn.Sequential(
|
||||
make_conv(),
|
||||
nn.Dropout(p=dropout),
|
||||
utils.Fp32GroupNorm(dim, dim, affine=True),
|
||||
nn.GELU(),
|
||||
)
|
||||
else:
|
||||
return nn.Sequential(make_conv(), nn.Dropout(p=dropout), nn.GELU())
|
||||
|
||||
self.conv_layers = nn.ModuleList()
|
||||
for i, cl in enumerate(conv_layers):
|
||||
assert len(cl) == 3, "invalid conv definition: " + str(cl)
|
||||
(dim, k, stride) = cl
|
||||
|
||||
self.conv_layers.append(
|
||||
block(
|
||||
in_d,
|
||||
dim,
|
||||
k,
|
||||
stride,
|
||||
is_layer_norm=mode == "layer_norm",
|
||||
is_group_norm=mode == "default" and i == 0,
|
||||
conv_bias=conv_bias,
|
||||
)
|
||||
)
|
||||
in_d = dim
|
||||
|
||||
def forward(self, x):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
if len(x.shape) == 2:
|
||||
x = x.unsqueeze(1)
|
||||
else:
|
||||
x = x.transpose(1, 2)
|
||||
|
||||
for conv in self.conv_layers:
|
||||
x = conv(x)
|
||||
return x
|
||||
|
||||
|
||||
def make_conv_pos(e, k, g):
|
||||
"""Make conv pos.
|
||||
|
||||
Args:
|
||||
e: TODO.
|
||||
k: TODO.
|
||||
g: TODO.
|
||||
"""
|
||||
pos_conv = nn.Conv1d(
|
||||
e,
|
||||
e,
|
||||
kernel_size=k,
|
||||
padding=k // 2,
|
||||
groups=g,
|
||||
)
|
||||
dropout = 0
|
||||
std = math.sqrt((4 * (1.0 - dropout)) / (k * e))
|
||||
nn.init.normal_(pos_conv.weight, mean=0, std=std)
|
||||
nn.init.constant_(pos_conv.bias, 0)
|
||||
|
||||
pos_conv = nn.utils.weight_norm(pos_conv, name="weight", dim=2)
|
||||
pos_conv = nn.Sequential(pos_conv, utils.SamePad(k), nn.GELU())
|
||||
|
||||
return pos_conv
|
||||
|
||||
|
||||
class TransformerEncoder(nn.Module):
|
||||
def build_encoder_layer(self):
|
||||
"""Build encoder layer."""
|
||||
if self.layer_type == "transformer":
|
||||
layer = TransformerSentenceEncoderLayer(
|
||||
embedding_dim=self.embedding_dim,
|
||||
ffn_embedding_dim=self.encoder_ffn_embed_dim,
|
||||
num_attention_heads=self.encoder_attention_heads,
|
||||
dropout=self.dropout,
|
||||
attention_dropout=self.attention_dropout,
|
||||
activation_dropout=self.activation_dropout,
|
||||
activation_fn=self.activation_fn,
|
||||
layer_norm_first=self.layer_norm_first,
|
||||
)
|
||||
else:
|
||||
logging.error("Only transformer is supported for data2vec now")
|
||||
return layer
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
# position
|
||||
dropout,
|
||||
encoder_embed_dim,
|
||||
required_seq_len_multiple,
|
||||
pos_conv_depth,
|
||||
conv_pos,
|
||||
conv_pos_groups,
|
||||
# transformer layers
|
||||
layer_type,
|
||||
encoder_layers,
|
||||
encoder_ffn_embed_dim,
|
||||
encoder_attention_heads,
|
||||
attention_dropout,
|
||||
activation_dropout,
|
||||
activation_fn,
|
||||
layer_norm_first,
|
||||
encoder_layerdrop,
|
||||
max_positions,
|
||||
):
|
||||
"""Initialize TransformerEncoder.
|
||||
|
||||
Args:
|
||||
dropout: TODO.
|
||||
encoder_embed_dim: Size/dimension parameter.
|
||||
required_seq_len_multiple: TODO.
|
||||
pos_conv_depth: TODO.
|
||||
conv_pos: TODO.
|
||||
conv_pos_groups: TODO.
|
||||
layer_type: TODO.
|
||||
encoder_layers: TODO.
|
||||
encoder_ffn_embed_dim: Size/dimension parameter.
|
||||
encoder_attention_heads: TODO.
|
||||
attention_dropout: TODO.
|
||||
activation_dropout: TODO.
|
||||
activation_fn: TODO.
|
||||
layer_norm_first: TODO.
|
||||
encoder_layerdrop: TODO.
|
||||
max_positions: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
# position
|
||||
self.dropout = dropout
|
||||
self.embedding_dim = encoder_embed_dim
|
||||
self.required_seq_len_multiple = required_seq_len_multiple
|
||||
if pos_conv_depth > 1:
|
||||
num_layers = pos_conv_depth
|
||||
k = max(3, conv_pos // num_layers)
|
||||
|
||||
def make_conv_block(e, k, g, l):
|
||||
"""Make conv block.
|
||||
|
||||
Args:
|
||||
e: TODO.
|
||||
k: TODO.
|
||||
g: TODO.
|
||||
l: TODO.
|
||||
"""
|
||||
return nn.Sequential(
|
||||
*[
|
||||
nn.Sequential(
|
||||
nn.Conv1d(
|
||||
e,
|
||||
e,
|
||||
kernel_size=k,
|
||||
padding=k // 2,
|
||||
groups=g,
|
||||
),
|
||||
utils.SamePad(k),
|
||||
utils.TransposeLast(),
|
||||
torch.nn.LayerNorm(e, elementwise_affine=False),
|
||||
utils.TransposeLast(),
|
||||
nn.GELU(),
|
||||
)
|
||||
for _ in range(l)
|
||||
]
|
||||
)
|
||||
|
||||
self.pos_conv = make_conv_block(self.embedding_dim, k, conv_pos_groups, num_layers)
|
||||
|
||||
else:
|
||||
self.pos_conv = make_conv_pos(
|
||||
self.embedding_dim,
|
||||
conv_pos,
|
||||
conv_pos_groups,
|
||||
)
|
||||
|
||||
# transformer layers
|
||||
self.layer_type = layer_type
|
||||
self.encoder_ffn_embed_dim = encoder_ffn_embed_dim
|
||||
self.encoder_attention_heads = encoder_attention_heads
|
||||
self.attention_dropout = attention_dropout
|
||||
self.activation_dropout = activation_dropout
|
||||
self.activation_fn = activation_fn
|
||||
self.layer_norm_first = layer_norm_first
|
||||
self.layerdrop = encoder_layerdrop
|
||||
self.max_positions = max_positions
|
||||
self.layers = nn.ModuleList([self.build_encoder_layer() for _ in range(encoder_layers)])
|
||||
self.layer_norm = torch.nn.LayerNorm(self.embedding_dim)
|
||||
|
||||
self.apply(utils.init_bert_params)
|
||||
|
||||
def forward(self, x, padding_mask=None, layer=None):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
padding_mask: TODO.
|
||||
layer: TODO.
|
||||
"""
|
||||
x, layer_results = self.extract_features(x, padding_mask, layer)
|
||||
|
||||
if self.layer_norm_first and layer is None:
|
||||
x = self.layer_norm(x)
|
||||
|
||||
return x, layer_results
|
||||
|
||||
def extract_features(
|
||||
self,
|
||||
x,
|
||||
padding_mask=None,
|
||||
tgt_layer=None,
|
||||
min_layer=0,
|
||||
):
|
||||
|
||||
"""Extract features.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
padding_mask: TODO.
|
||||
tgt_layer: TODO.
|
||||
min_layer: TODO.
|
||||
"""
|
||||
if padding_mask is not None:
|
||||
x[padding_mask] = 0
|
||||
|
||||
x_conv = self.pos_conv(x.transpose(1, 2))
|
||||
x_conv = x_conv.transpose(1, 2)
|
||||
x = x + x_conv
|
||||
|
||||
if not self.layer_norm_first:
|
||||
x = self.layer_norm(x)
|
||||
|
||||
# pad to the sequence length dimension
|
||||
x, pad_length = utils.pad_to_multiple(x, self.required_seq_len_multiple, dim=-2, value=0)
|
||||
if pad_length > 0 and padding_mask is None:
|
||||
padding_mask = x.new_zeros((x.size(0), x.size(1)), dtype=torch.bool)
|
||||
padding_mask[:, -pad_length:] = True
|
||||
else:
|
||||
padding_mask, _ = utils.pad_to_multiple(
|
||||
padding_mask, self.required_seq_len_multiple, dim=-1, value=True
|
||||
)
|
||||
x = F.dropout(x, p=self.dropout, training=self.training)
|
||||
|
||||
# B x T x C -> T x B x C
|
||||
x = x.transpose(0, 1)
|
||||
|
||||
layer_results = []
|
||||
r = None
|
||||
for i, layer in enumerate(self.layers):
|
||||
dropout_probability = np.random.random() if self.layerdrop > 0 else 1
|
||||
if not self.training or (dropout_probability > self.layerdrop):
|
||||
x, (z, lr) = layer(x, self_attn_padding_mask=padding_mask)
|
||||
if i >= min_layer:
|
||||
layer_results.append((x, z, lr))
|
||||
if i == tgt_layer:
|
||||
r = x
|
||||
break
|
||||
|
||||
if r is not None:
|
||||
x = r
|
||||
|
||||
# T x B x C -> B x T x C
|
||||
x = x.transpose(0, 1)
|
||||
|
||||
# undo paddding
|
||||
if pad_length > 0:
|
||||
x = x[:, :-pad_length]
|
||||
|
||||
def undo_pad(a, b, c):
|
||||
"""Undo pad.
|
||||
|
||||
Args:
|
||||
a: TODO.
|
||||
b: TODO.
|
||||
c: TODO.
|
||||
"""
|
||||
return (
|
||||
a[:-pad_length],
|
||||
b[:-pad_length] if b is not None else b,
|
||||
c[:-pad_length],
|
||||
)
|
||||
|
||||
layer_results = [undo_pad(*u) for u in layer_results]
|
||||
|
||||
return x, layer_results
|
||||
|
||||
def max_positions(self):
|
||||
"""Maximum output length supported by the encoder."""
|
||||
return self.max_positions
|
||||
|
||||
def upgrade_state_dict_named(self, state_dict, name):
|
||||
"""Upgrade a (possibly old) state dict for new versions of fairseq."""
|
||||
return state_dict
|
||||
|
||||
|
||||
class TransformerSentenceEncoderLayer(nn.Module):
|
||||
"""
|
||||
Implements a Transformer Encoder Layer used in BERT/XLM style pre-trained
|
||||
models.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
embedding_dim: int = 768,
|
||||
ffn_embedding_dim: int = 3072,
|
||||
num_attention_heads: int = 8,
|
||||
dropout: float = 0.1,
|
||||
attention_dropout: float = 0.1,
|
||||
activation_dropout: float = 0.1,
|
||||
activation_fn: str = "relu",
|
||||
layer_norm_first: bool = False,
|
||||
) -> None:
|
||||
|
||||
"""Initialize TransformerSentenceEncoderLayer.
|
||||
|
||||
Args:
|
||||
embedding_dim: Size/dimension parameter.
|
||||
ffn_embedding_dim: Size/dimension parameter.
|
||||
num_attention_heads: TODO.
|
||||
dropout: TODO.
|
||||
attention_dropout: TODO.
|
||||
activation_dropout: TODO.
|
||||
activation_fn: TODO.
|
||||
layer_norm_first: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
# Initialize parameters
|
||||
self.embedding_dim = embedding_dim
|
||||
self.dropout = dropout
|
||||
self.activation_dropout = activation_dropout
|
||||
|
||||
# Initialize blocks
|
||||
self.activation_fn = utils.get_activation_fn(activation_fn)
|
||||
self.self_attn = MultiheadAttention(
|
||||
self.embedding_dim,
|
||||
num_attention_heads,
|
||||
dropout=attention_dropout,
|
||||
self_attention=True,
|
||||
)
|
||||
|
||||
self.dropout1 = nn.Dropout(dropout)
|
||||
self.dropout2 = nn.Dropout(self.activation_dropout)
|
||||
self.dropout3 = nn.Dropout(dropout)
|
||||
|
||||
self.layer_norm_first = layer_norm_first
|
||||
|
||||
# layer norm associated with the self attention layer
|
||||
self.self_attn_layer_norm = torch.nn.LayerNorm(self.embedding_dim)
|
||||
self.fc1 = nn.Linear(self.embedding_dim, ffn_embedding_dim)
|
||||
self.fc2 = nn.Linear(ffn_embedding_dim, self.embedding_dim)
|
||||
|
||||
# layer norm associated with the position wise feed-forward NN
|
||||
self.final_layer_norm = torch.nn.LayerNorm(self.embedding_dim)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
x: torch.Tensor, # (T, B, C)
|
||||
self_attn_mask: torch.Tensor = None,
|
||||
self_attn_padding_mask: torch.Tensor = None,
|
||||
):
|
||||
"""
|
||||
LayerNorm is applied either before or after the self-attention/ffn
|
||||
modules similar to the original Transformer imlementation.
|
||||
"""
|
||||
residual = x
|
||||
|
||||
if self.layer_norm_first:
|
||||
x = self.self_attn_layer_norm(x)
|
||||
x, attn = self.self_attn(
|
||||
query=x,
|
||||
key=x,
|
||||
value=x,
|
||||
key_padding_mask=self_attn_padding_mask,
|
||||
attn_mask=self_attn_mask,
|
||||
need_weights=False,
|
||||
)
|
||||
x = self.dropout1(x)
|
||||
x = residual + x
|
||||
|
||||
residual = x
|
||||
x = self.final_layer_norm(x)
|
||||
x = self.activation_fn(self.fc1(x))
|
||||
x = self.dropout2(x)
|
||||
x = self.fc2(x)
|
||||
|
||||
layer_result = x
|
||||
|
||||
x = self.dropout3(x)
|
||||
x = residual + x
|
||||
else:
|
||||
x, attn = self.self_attn(
|
||||
query=x,
|
||||
key=x,
|
||||
value=x,
|
||||
key_padding_mask=self_attn_padding_mask,
|
||||
need_weights=False,
|
||||
)
|
||||
|
||||
x = self.dropout1(x)
|
||||
x = residual + x
|
||||
|
||||
x = self.self_attn_layer_norm(x)
|
||||
|
||||
residual = x
|
||||
x = self.activation_fn(self.fc1(x))
|
||||
x = self.dropout2(x)
|
||||
x = self.fc2(x)
|
||||
|
||||
layer_result = x
|
||||
|
||||
x = self.dropout3(x)
|
||||
x = residual + x
|
||||
x = self.final_layer_norm(x)
|
||||
|
||||
return x, (attn, layer_result)
|
||||
@@ -0,0 +1,503 @@
|
||||
# Copyright 2022 Kwangyoun Kim (ASAPP inc.)
|
||||
# Apache 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
"""E-Branchformer encoder definition.
|
||||
Reference:
|
||||
Kwangyoun Kim, Felix Wu, Yifan Peng, Jing Pan,
|
||||
Prashant Sridhar, Kyu J. Han, Shinji Watanabe,
|
||||
"E-Branchformer: Branchformer with Enhanced merging
|
||||
for speech recognition," in SLT 2022.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from funasr.models.ctc.ctc import CTC
|
||||
from funasr.models.branchformer.cgmlp import ConvolutionalGatingMLP
|
||||
from funasr.models.branchformer.fastformer import FastSelfAttention
|
||||
from funasr.models.transformer.utils.nets_utils import get_activation, make_pad_mask
|
||||
from funasr.models.transformer.attention import ( # noqa: H301
|
||||
LegacyRelPositionMultiHeadedAttention,
|
||||
MultiHeadedAttention,
|
||||
RelPositionMultiHeadedAttention,
|
||||
)
|
||||
from funasr.models.transformer.embedding import ( # noqa: H301
|
||||
LegacyRelPositionalEncoding,
|
||||
PositionalEncoding,
|
||||
RelPositionalEncoding,
|
||||
ScaledPositionalEncoding,
|
||||
)
|
||||
from funasr.models.transformer.layer_norm import LayerNorm
|
||||
from funasr.models.transformer.positionwise_feed_forward import (
|
||||
PositionwiseFeedForward,
|
||||
)
|
||||
from funasr.models.transformer.utils.repeat import repeat
|
||||
from funasr.models.transformer.utils.subsampling import (
|
||||
Conv2dSubsampling,
|
||||
Conv2dSubsampling2,
|
||||
Conv2dSubsampling6,
|
||||
Conv2dSubsampling8,
|
||||
TooShortUttError,
|
||||
check_short_utt,
|
||||
)
|
||||
from funasr.register import tables
|
||||
|
||||
|
||||
class EBranchformerEncoderLayer(torch.nn.Module):
|
||||
"""E-Branchformer encoder layer module.
|
||||
|
||||
Args:
|
||||
size (int): model dimension
|
||||
attn: standard self-attention or efficient attention
|
||||
cgmlp: ConvolutionalGatingMLP
|
||||
feed_forward: feed-forward module, optional
|
||||
feed_forward: macaron-style feed-forward module, optional
|
||||
dropout_rate (float): dropout probability
|
||||
merge_conv_kernel (int): kernel size of the depth-wise conv in merge module
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
size: int,
|
||||
attn: torch.nn.Module,
|
||||
cgmlp: torch.nn.Module,
|
||||
feed_forward: Optional[torch.nn.Module],
|
||||
feed_forward_macaron: Optional[torch.nn.Module],
|
||||
dropout_rate: float,
|
||||
merge_conv_kernel: int = 3,
|
||||
):
|
||||
"""Initialize EBranchformerEncoderLayer.
|
||||
|
||||
Args:
|
||||
size: TODO.
|
||||
attn: TODO.
|
||||
cgmlp: TODO.
|
||||
feed_forward: TODO.
|
||||
feed_forward_macaron: TODO.
|
||||
dropout_rate: TODO.
|
||||
merge_conv_kernel: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.size = size
|
||||
self.attn = attn
|
||||
self.cgmlp = cgmlp
|
||||
|
||||
self.feed_forward = feed_forward
|
||||
self.feed_forward_macaron = feed_forward_macaron
|
||||
self.ff_scale = 1.0
|
||||
if self.feed_forward is not None:
|
||||
self.norm_ff = LayerNorm(size)
|
||||
if self.feed_forward_macaron is not None:
|
||||
self.ff_scale = 0.5
|
||||
self.norm_ff_macaron = LayerNorm(size)
|
||||
|
||||
self.norm_mha = LayerNorm(size) # for the MHA module
|
||||
self.norm_mlp = LayerNorm(size) # for the MLP module
|
||||
self.norm_final = LayerNorm(size) # for the final output of the block
|
||||
|
||||
self.dropout = torch.nn.Dropout(dropout_rate)
|
||||
|
||||
self.depthwise_conv_fusion = torch.nn.Conv1d(
|
||||
size + size,
|
||||
size + size,
|
||||
kernel_size=merge_conv_kernel,
|
||||
stride=1,
|
||||
padding=(merge_conv_kernel - 1) // 2,
|
||||
groups=size + size,
|
||||
bias=True,
|
||||
)
|
||||
self.merge_proj = torch.nn.Linear(size + size, size)
|
||||
|
||||
def forward(self, x_input, mask, cache=None):
|
||||
"""Compute encoded features.
|
||||
|
||||
Args:
|
||||
x_input (Union[Tuple, torch.Tensor]): Input tensor w/ or w/o pos emb.
|
||||
- w/ pos emb: Tuple of tensors [(#batch, time, size), (1, time, size)].
|
||||
- w/o pos emb: Tensor (#batch, time, size).
|
||||
mask (torch.Tensor): Mask tensor for the input (#batch, 1, time).
|
||||
cache (torch.Tensor): Cache tensor of the input (#batch, time - 1, size).
|
||||
Returns:
|
||||
torch.Tensor: Output tensor (#batch, time, size).
|
||||
torch.Tensor: Mask tensor (#batch, time).
|
||||
"""
|
||||
|
||||
if cache is not None:
|
||||
raise NotImplementedError("cache is not None, which is not tested")
|
||||
|
||||
if isinstance(x_input, tuple):
|
||||
x, pos_emb = x_input[0], x_input[1]
|
||||
else:
|
||||
x, pos_emb = x_input, None
|
||||
|
||||
if self.feed_forward_macaron is not None:
|
||||
residual = x
|
||||
x = self.norm_ff_macaron(x)
|
||||
x = residual + self.ff_scale * self.dropout(self.feed_forward_macaron(x))
|
||||
|
||||
# Two branches
|
||||
x1 = x
|
||||
x2 = x
|
||||
|
||||
# Branch 1: multi-headed attention module
|
||||
x1 = self.norm_mha(x1)
|
||||
|
||||
if isinstance(self.attn, FastSelfAttention):
|
||||
x_att = self.attn(x1, mask)
|
||||
else:
|
||||
if pos_emb is not None:
|
||||
x_att = self.attn(x1, x1, x1, pos_emb, mask)
|
||||
else:
|
||||
x_att = self.attn(x1, x1, x1, mask)
|
||||
|
||||
x1 = self.dropout(x_att)
|
||||
|
||||
# Branch 2: convolutional gating mlp
|
||||
x2 = self.norm_mlp(x2)
|
||||
|
||||
if pos_emb is not None:
|
||||
x2 = (x2, pos_emb)
|
||||
x2 = self.cgmlp(x2, mask)
|
||||
if isinstance(x2, tuple):
|
||||
x2 = x2[0]
|
||||
|
||||
x2 = self.dropout(x2)
|
||||
|
||||
# Merge two branches
|
||||
x_concat = torch.cat([x1, x2], dim=-1)
|
||||
x_tmp = x_concat.transpose(1, 2)
|
||||
x_tmp = self.depthwise_conv_fusion(x_tmp)
|
||||
x_tmp = x_tmp.transpose(1, 2)
|
||||
x = x + self.dropout(self.merge_proj(x_concat + x_tmp))
|
||||
|
||||
if self.feed_forward is not None:
|
||||
# feed forward module
|
||||
residual = x
|
||||
x = self.norm_ff(x)
|
||||
x = residual + self.ff_scale * self.dropout(self.feed_forward(x))
|
||||
|
||||
x = self.norm_final(x)
|
||||
|
||||
if pos_emb is not None:
|
||||
return (x, pos_emb), mask
|
||||
|
||||
return x, mask
|
||||
|
||||
|
||||
@tables.register("encoder_classes", "EBranchformerEncoder")
|
||||
class EBranchformerEncoder(nn.Module):
|
||||
"""E-Branchformer encoder module."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_size: int,
|
||||
output_size: int = 256,
|
||||
attention_heads: int = 4,
|
||||
attention_layer_type: str = "rel_selfattn",
|
||||
pos_enc_layer_type: str = "rel_pos",
|
||||
rel_pos_type: str = "latest",
|
||||
cgmlp_linear_units: int = 2048,
|
||||
cgmlp_conv_kernel: int = 31,
|
||||
use_linear_after_conv: bool = False,
|
||||
gate_activation: str = "identity",
|
||||
num_blocks: int = 12,
|
||||
dropout_rate: float = 0.1,
|
||||
positional_dropout_rate: float = 0.1,
|
||||
attention_dropout_rate: float = 0.0,
|
||||
input_layer: Optional[str] = "conv2d",
|
||||
zero_triu: bool = False,
|
||||
padding_idx: int = -1,
|
||||
layer_drop_rate: float = 0.0,
|
||||
max_pos_emb_len: int = 5000,
|
||||
use_ffn: bool = False,
|
||||
macaron_ffn: bool = False,
|
||||
ffn_activation_type: str = "swish",
|
||||
linear_units: int = 2048,
|
||||
positionwise_layer_type: str = "linear",
|
||||
merge_conv_kernel: int = 3,
|
||||
interctc_layer_idx=None,
|
||||
interctc_use_conditioning: bool = False,
|
||||
):
|
||||
"""Initialize EBranchformerEncoder.
|
||||
|
||||
Args:
|
||||
input_size: Size/dimension parameter.
|
||||
output_size: Size/dimension parameter.
|
||||
attention_heads: TODO.
|
||||
attention_layer_type: TODO.
|
||||
pos_enc_layer_type: TODO.
|
||||
rel_pos_type: TODO.
|
||||
cgmlp_linear_units: TODO.
|
||||
cgmlp_conv_kernel: TODO.
|
||||
use_linear_after_conv: TODO.
|
||||
gate_activation: TODO.
|
||||
num_blocks: TODO.
|
||||
dropout_rate: TODO.
|
||||
positional_dropout_rate: TODO.
|
||||
attention_dropout_rate: TODO.
|
||||
input_layer: TODO.
|
||||
zero_triu: TODO.
|
||||
padding_idx: TODO.
|
||||
layer_drop_rate: TODO.
|
||||
max_pos_emb_len: TODO.
|
||||
use_ffn: TODO.
|
||||
macaron_ffn: TODO.
|
||||
ffn_activation_type: TODO.
|
||||
linear_units: TODO.
|
||||
positionwise_layer_type: TODO.
|
||||
merge_conv_kernel: TODO.
|
||||
interctc_layer_idx: TODO.
|
||||
interctc_use_conditioning: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
self._output_size = output_size
|
||||
|
||||
if rel_pos_type == "legacy":
|
||||
if pos_enc_layer_type == "rel_pos":
|
||||
pos_enc_layer_type = "legacy_rel_pos"
|
||||
if attention_layer_type == "rel_selfattn":
|
||||
attention_layer_type = "legacy_rel_selfattn"
|
||||
elif rel_pos_type == "latest":
|
||||
assert attention_layer_type != "legacy_rel_selfattn"
|
||||
assert pos_enc_layer_type != "legacy_rel_pos"
|
||||
else:
|
||||
raise ValueError("unknown rel_pos_type: " + rel_pos_type)
|
||||
|
||||
if pos_enc_layer_type == "abs_pos":
|
||||
pos_enc_class = PositionalEncoding
|
||||
elif pos_enc_layer_type == "scaled_abs_pos":
|
||||
pos_enc_class = ScaledPositionalEncoding
|
||||
elif pos_enc_layer_type == "rel_pos":
|
||||
assert attention_layer_type == "rel_selfattn"
|
||||
pos_enc_class = RelPositionalEncoding
|
||||
elif pos_enc_layer_type == "legacy_rel_pos":
|
||||
assert attention_layer_type == "legacy_rel_selfattn"
|
||||
pos_enc_class = LegacyRelPositionalEncoding
|
||||
logging.warning("Using legacy_rel_pos and it will be deprecated in the future.")
|
||||
else:
|
||||
raise ValueError("unknown pos_enc_layer: " + pos_enc_layer_type)
|
||||
|
||||
if input_layer == "linear":
|
||||
self.embed = torch.nn.Sequential(
|
||||
torch.nn.Linear(input_size, output_size),
|
||||
torch.nn.LayerNorm(output_size),
|
||||
torch.nn.Dropout(dropout_rate),
|
||||
pos_enc_class(output_size, positional_dropout_rate, max_pos_emb_len),
|
||||
)
|
||||
elif input_layer == "conv2d":
|
||||
self.embed = Conv2dSubsampling(
|
||||
input_size,
|
||||
output_size,
|
||||
dropout_rate,
|
||||
pos_enc_class(output_size, positional_dropout_rate, max_pos_emb_len),
|
||||
)
|
||||
elif input_layer == "conv2d2":
|
||||
self.embed = Conv2dSubsampling2(
|
||||
input_size,
|
||||
output_size,
|
||||
dropout_rate,
|
||||
pos_enc_class(output_size, positional_dropout_rate, max_pos_emb_len),
|
||||
)
|
||||
elif input_layer == "conv2d6":
|
||||
self.embed = Conv2dSubsampling6(
|
||||
input_size,
|
||||
output_size,
|
||||
dropout_rate,
|
||||
pos_enc_class(output_size, positional_dropout_rate, max_pos_emb_len),
|
||||
)
|
||||
elif input_layer == "conv2d8":
|
||||
self.embed = Conv2dSubsampling8(
|
||||
input_size,
|
||||
output_size,
|
||||
dropout_rate,
|
||||
pos_enc_class(output_size, positional_dropout_rate, max_pos_emb_len),
|
||||
)
|
||||
elif input_layer == "embed":
|
||||
self.embed = torch.nn.Sequential(
|
||||
torch.nn.Embedding(input_size, output_size, padding_idx=padding_idx),
|
||||
pos_enc_class(output_size, positional_dropout_rate, max_pos_emb_len),
|
||||
)
|
||||
elif isinstance(input_layer, torch.nn.Module):
|
||||
self.embed = torch.nn.Sequential(
|
||||
input_layer,
|
||||
pos_enc_class(output_size, positional_dropout_rate, max_pos_emb_len),
|
||||
)
|
||||
elif input_layer is None:
|
||||
if input_size == output_size:
|
||||
self.embed = None
|
||||
else:
|
||||
self.embed = torch.nn.Linear(input_size, output_size)
|
||||
else:
|
||||
raise ValueError("unknown input_layer: " + input_layer)
|
||||
|
||||
activation = get_activation(ffn_activation_type)
|
||||
if positionwise_layer_type == "linear":
|
||||
positionwise_layer = PositionwiseFeedForward
|
||||
positionwise_layer_args = (
|
||||
output_size,
|
||||
linear_units,
|
||||
dropout_rate,
|
||||
activation,
|
||||
)
|
||||
elif positionwise_layer_type is None:
|
||||
logging.warning("no macaron ffn")
|
||||
else:
|
||||
raise ValueError("Support only linear.")
|
||||
|
||||
if attention_layer_type == "selfattn":
|
||||
encoder_selfattn_layer = MultiHeadedAttention
|
||||
encoder_selfattn_layer_args = (
|
||||
attention_heads,
|
||||
output_size,
|
||||
attention_dropout_rate,
|
||||
)
|
||||
elif attention_layer_type == "legacy_rel_selfattn":
|
||||
assert pos_enc_layer_type == "legacy_rel_pos"
|
||||
encoder_selfattn_layer = LegacyRelPositionMultiHeadedAttention
|
||||
encoder_selfattn_layer_args = (
|
||||
attention_heads,
|
||||
output_size,
|
||||
attention_dropout_rate,
|
||||
)
|
||||
logging.warning("Using legacy_rel_selfattn and it will be deprecated in the future.")
|
||||
elif attention_layer_type == "rel_selfattn":
|
||||
assert pos_enc_layer_type == "rel_pos"
|
||||
encoder_selfattn_layer = RelPositionMultiHeadedAttention
|
||||
encoder_selfattn_layer_args = (
|
||||
attention_heads,
|
||||
output_size,
|
||||
attention_dropout_rate,
|
||||
zero_triu,
|
||||
)
|
||||
elif attention_layer_type == "fast_selfattn":
|
||||
assert pos_enc_layer_type in ["abs_pos", "scaled_abs_pos"]
|
||||
encoder_selfattn_layer = FastSelfAttention
|
||||
encoder_selfattn_layer_args = (
|
||||
output_size,
|
||||
attention_heads,
|
||||
attention_dropout_rate,
|
||||
)
|
||||
else:
|
||||
raise ValueError("unknown encoder_attn_layer: " + attention_layer_type)
|
||||
|
||||
cgmlp_layer = ConvolutionalGatingMLP
|
||||
cgmlp_layer_args = (
|
||||
output_size,
|
||||
cgmlp_linear_units,
|
||||
cgmlp_conv_kernel,
|
||||
dropout_rate,
|
||||
use_linear_after_conv,
|
||||
gate_activation,
|
||||
)
|
||||
|
||||
self.encoders = repeat(
|
||||
num_blocks,
|
||||
lambda lnum: EBranchformerEncoderLayer(
|
||||
output_size,
|
||||
encoder_selfattn_layer(*encoder_selfattn_layer_args),
|
||||
cgmlp_layer(*cgmlp_layer_args),
|
||||
positionwise_layer(*positionwise_layer_args) if use_ffn else None,
|
||||
positionwise_layer(*positionwise_layer_args) if use_ffn and macaron_ffn else None,
|
||||
dropout_rate,
|
||||
merge_conv_kernel,
|
||||
),
|
||||
layer_drop_rate,
|
||||
)
|
||||
self.after_norm = LayerNorm(output_size)
|
||||
|
||||
if interctc_layer_idx is None:
|
||||
interctc_layer_idx = []
|
||||
self.interctc_layer_idx = interctc_layer_idx
|
||||
if len(interctc_layer_idx) > 0:
|
||||
assert 0 < min(interctc_layer_idx) and max(interctc_layer_idx) < num_blocks
|
||||
self.interctc_use_conditioning = interctc_use_conditioning
|
||||
self.conditioning_layer = None
|
||||
|
||||
def output_size(self) -> int:
|
||||
"""Output size."""
|
||||
return self._output_size
|
||||
|
||||
def forward(
|
||||
self,
|
||||
xs_pad: torch.Tensor,
|
||||
ilens: torch.Tensor,
|
||||
prev_states: torch.Tensor = None,
|
||||
ctc: CTC = None,
|
||||
max_layer: int = None,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor, Optional[torch.Tensor]]:
|
||||
"""Calculate forward propagation.
|
||||
|
||||
Args:
|
||||
xs_pad (torch.Tensor): Input tensor (#batch, L, input_size).
|
||||
ilens (torch.Tensor): Input length (#batch).
|
||||
prev_states (torch.Tensor): Not to be used now.
|
||||
ctc (CTC): Intermediate CTC module.
|
||||
max_layer (int): Layer depth below which InterCTC is applied.
|
||||
Returns:
|
||||
torch.Tensor: Output tensor (#batch, L, output_size).
|
||||
torch.Tensor: Output length (#batch).
|
||||
torch.Tensor: Not to be used now.
|
||||
"""
|
||||
|
||||
masks = (~make_pad_mask(ilens)[:, None, :]).to(xs_pad.device)
|
||||
|
||||
if (
|
||||
isinstance(self.embed, Conv2dSubsampling)
|
||||
or isinstance(self.embed, Conv2dSubsampling2)
|
||||
or isinstance(self.embed, Conv2dSubsampling6)
|
||||
or isinstance(self.embed, Conv2dSubsampling8)
|
||||
):
|
||||
short_status, limit_size = check_short_utt(self.embed, xs_pad.size(1))
|
||||
if short_status:
|
||||
raise TooShortUttError(
|
||||
f"has {xs_pad.size(1)} frames and is too short for subsampling "
|
||||
+ f"(it needs more than {limit_size} frames), return empty results",
|
||||
xs_pad.size(1),
|
||||
limit_size,
|
||||
)
|
||||
xs_pad, masks = self.embed(xs_pad, masks)
|
||||
elif self.embed is not None:
|
||||
xs_pad = self.embed(xs_pad)
|
||||
|
||||
intermediate_outs = []
|
||||
if len(self.interctc_layer_idx) == 0:
|
||||
if max_layer is not None and 0 <= max_layer < len(self.encoders):
|
||||
for layer_idx, encoder_layer in enumerate(self.encoders):
|
||||
xs_pad, masks = encoder_layer(xs_pad, masks)
|
||||
if layer_idx >= max_layer:
|
||||
break
|
||||
else:
|
||||
xs_pad, masks = self.encoders(xs_pad, masks)
|
||||
else:
|
||||
for layer_idx, encoder_layer in enumerate(self.encoders):
|
||||
xs_pad, masks = encoder_layer(xs_pad, masks)
|
||||
|
||||
if layer_idx + 1 in self.interctc_layer_idx:
|
||||
encoder_out = xs_pad
|
||||
|
||||
if isinstance(encoder_out, tuple):
|
||||
encoder_out = encoder_out[0]
|
||||
|
||||
intermediate_outs.append((layer_idx + 1, encoder_out))
|
||||
|
||||
if self.interctc_use_conditioning:
|
||||
ctc_out = ctc.softmax(encoder_out)
|
||||
|
||||
if isinstance(xs_pad, tuple):
|
||||
xs_pad = list(xs_pad)
|
||||
xs_pad[0] = xs_pad[0] + self.conditioning_layer(ctc_out)
|
||||
xs_pad = tuple(xs_pad)
|
||||
else:
|
||||
xs_pad = xs_pad + self.conditioning_layer(ctc_out)
|
||||
|
||||
if isinstance(xs_pad, tuple):
|
||||
xs_pad = xs_pad[0]
|
||||
|
||||
xs_pad = self.after_norm(xs_pad)
|
||||
olens = masks.squeeze(1).sum(1)
|
||||
if len(intermediate_outs) > 0:
|
||||
return (xs_pad, intermediate_outs), olens, None
|
||||
return xs_pad, olens, None
|
||||
@@ -0,0 +1,29 @@
|
||||
import logging
|
||||
|
||||
from funasr.models.transformer.model import Transformer
|
||||
from funasr.register import tables
|
||||
|
||||
|
||||
@tables.register("model_classes", "EBranchformer")
|
||||
class EBranchformer(Transformer):
|
||||
"""E-Branchformer: Enhanced Branchformer with improved merging.
|
||||
|
||||
Uses element-wise merging instead of concatenation for parallel branches,
|
||||
resulting in better parameter efficiency.
|
||||
|
||||
Inherits Transformer pipeline.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
|
||||
"""Initialize EBranchformer.
|
||||
|
||||
Args:
|
||||
*args: Variable positional arguments.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -0,0 +1,116 @@
|
||||
# This is an example that demonstrates how to configure a model file.
|
||||
# You can modify the configuration according to your own requirements.
|
||||
|
||||
# to print the register_table:
|
||||
# from funasr.register import tables
|
||||
# tables.print()
|
||||
|
||||
# network architecture
|
||||
model: Branchformer
|
||||
model_conf:
|
||||
ctc_weight: 0.3
|
||||
lsm_weight: 0.1 # label smoothing option
|
||||
length_normalized_loss: false
|
||||
|
||||
# encoder
|
||||
encoder: EBranchformerEncoder
|
||||
encoder_conf:
|
||||
output_size: 256
|
||||
attention_heads: 4
|
||||
attention_layer_type: rel_selfattn
|
||||
pos_enc_layer_type: rel_pos
|
||||
rel_pos_type: latest
|
||||
cgmlp_linear_units: 1024
|
||||
cgmlp_conv_kernel: 31
|
||||
use_linear_after_conv: false
|
||||
gate_activation: identity
|
||||
num_blocks: 12
|
||||
dropout_rate: 0.1
|
||||
positional_dropout_rate: 0.1
|
||||
attention_dropout_rate: 0.1
|
||||
input_layer: conv2d
|
||||
layer_drop_rate: 0.0
|
||||
linear_units: 1024
|
||||
positionwise_layer_type: linear
|
||||
use_ffn: true
|
||||
macaron_ffn: true
|
||||
merge_conv_kernel: 31
|
||||
|
||||
# decoder
|
||||
decoder: TransformerDecoder
|
||||
decoder_conf:
|
||||
attention_heads: 4
|
||||
linear_units: 2048
|
||||
num_blocks: 6
|
||||
dropout_rate: 0.1
|
||||
positional_dropout_rate: 0.1
|
||||
self_attention_dropout_rate: 0.
|
||||
src_attention_dropout_rate: 0.
|
||||
|
||||
|
||||
# frontend related
|
||||
frontend: WavFrontend
|
||||
frontend_conf:
|
||||
fs: 16000
|
||||
window: hamming
|
||||
n_mels: 80
|
||||
frame_length: 25
|
||||
frame_shift: 10
|
||||
dither: 0.0
|
||||
lfr_m: 1
|
||||
lfr_n: 1
|
||||
|
||||
specaug: SpecAug
|
||||
specaug_conf:
|
||||
apply_time_warp: true
|
||||
time_warp_window: 5
|
||||
time_warp_mode: bicubic
|
||||
apply_freq_mask: true
|
||||
freq_mask_width_range:
|
||||
- 0
|
||||
- 30
|
||||
num_freq_mask: 2
|
||||
apply_time_mask: true
|
||||
time_mask_width_range:
|
||||
- 0
|
||||
- 40
|
||||
num_time_mask: 2
|
||||
|
||||
train_conf:
|
||||
accum_grad: 1
|
||||
grad_clip: 5
|
||||
max_epoch: 180
|
||||
keep_nbest_models: 10
|
||||
log_interval: 50
|
||||
|
||||
optim: adam
|
||||
optim_conf:
|
||||
lr: 0.001
|
||||
weight_decay: 0.000001
|
||||
scheduler: warmuplr
|
||||
scheduler_conf:
|
||||
warmup_steps: 35000
|
||||
|
||||
dataset: AudioDataset
|
||||
dataset_conf:
|
||||
index_ds: IndexDSJsonl
|
||||
batch_sampler: BatchSampler
|
||||
batch_type: example # example or length
|
||||
batch_size: 1 # if batch_type is example, batch_size is the numbers of samples; if length, batch_size is source_token_len+target_token_len;
|
||||
max_token_length: 2048 # filter samples if source_token_len+target_token_len > max_token_length,
|
||||
buffer_size: 500
|
||||
shuffle: True
|
||||
num_workers: 4
|
||||
|
||||
tokenizer: CharTokenizer
|
||||
tokenizer_conf:
|
||||
unk_symbol: <unk>
|
||||
split_with_space: true
|
||||
|
||||
|
||||
ctc_conf:
|
||||
dropout_rate: 0.0
|
||||
ctc_type: builtin
|
||||
reduce: true
|
||||
ignore_nan_grad: true
|
||||
normalize: null
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
import types
|
||||
import torch
|
||||
from funasr.register import tables
|
||||
|
||||
|
||||
def export_rebuild_model(model, **kwargs):
|
||||
"""Export rebuild model.
|
||||
|
||||
Args:
|
||||
model: Model instance or model name.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
model.device = kwargs.get("device")
|
||||
is_onnx = kwargs.get("type", "onnx") == "onnx"
|
||||
encoder_class = tables.encoder_classes.get(kwargs["encoder"] + "Export")
|
||||
model.encoder = encoder_class(model.encoder, onnx=is_onnx)
|
||||
|
||||
predictor_class = tables.predictor_classes.get(kwargs["predictor"] + "Export")
|
||||
model.predictor = predictor_class(model.predictor, onnx=is_onnx)
|
||||
|
||||
decoder_class = tables.decoder_classes.get(kwargs["decoder"] + "Export")
|
||||
model.decoder = decoder_class(model.decoder, onnx=is_onnx)
|
||||
|
||||
from funasr.utils.torch_function import sequence_mask
|
||||
|
||||
model.make_pad_mask = sequence_mask(kwargs["max_seq_len"], flip=False)
|
||||
|
||||
model.forward = types.MethodType(export_forward, model)
|
||||
model.export_dummy_inputs = types.MethodType(export_dummy_inputs, model)
|
||||
model.export_input_names = types.MethodType(export_input_names, model)
|
||||
model.export_output_names = types.MethodType(export_output_names, model)
|
||||
model.export_dynamic_axes = types.MethodType(export_dynamic_axes, model)
|
||||
model.export_name = types.MethodType(export_name, model)
|
||||
|
||||
model.export_name = 'model'
|
||||
return model
|
||||
|
||||
|
||||
def export_forward(
|
||||
self,
|
||||
speech: torch.Tensor,
|
||||
speech_lengths: torch.Tensor,
|
||||
):
|
||||
# a. To device
|
||||
"""Export forward.
|
||||
|
||||
Args:
|
||||
speech: Speech audio tensor, shape (batch, time).
|
||||
speech_lengths: Length of each speech sample.
|
||||
"""
|
||||
batch = {"speech": speech, "speech_lengths": speech_lengths}
|
||||
# batch = to_device(batch, device=self.device)
|
||||
|
||||
enc, enc_len = self.encoder(**batch)
|
||||
mask = self.make_pad_mask(enc_len)[:, None, :]
|
||||
pre_acoustic_embeds, pre_token_length, alphas, pre_peak_index = self.predictor(enc, mask)
|
||||
pre_token_length = pre_token_length.floor().type(torch.int32)
|
||||
|
||||
decoder_out, _ = self.decoder(enc, enc_len, pre_acoustic_embeds, pre_token_length)
|
||||
decoder_out = torch.log_softmax(decoder_out, dim=-1)
|
||||
# sample_ids = decoder_out.argmax(dim=-1)
|
||||
|
||||
return decoder_out, pre_token_length
|
||||
|
||||
|
||||
def export_dummy_inputs(self):
|
||||
"""Export dummy inputs."""
|
||||
speech = torch.randn(2, 30, 560)
|
||||
speech_lengths = torch.tensor([6, 30], dtype=torch.int32)
|
||||
return (speech, speech_lengths)
|
||||
|
||||
|
||||
def export_input_names(self):
|
||||
"""Export input names."""
|
||||
return ["speech", "speech_lengths"]
|
||||
|
||||
|
||||
def export_output_names(self):
|
||||
"""Export output names."""
|
||||
return ["logits", "token_num"]
|
||||
|
||||
|
||||
def export_dynamic_axes(self):
|
||||
"""Export dynamic axes."""
|
||||
return {
|
||||
"speech": {0: "batch_size", 1: "feats_length"},
|
||||
"speech_lengths": {
|
||||
0: "batch_size",
|
||||
},
|
||||
"logits": {0: "batch_size", 1: "logits_length"},
|
||||
}
|
||||
|
||||
|
||||
def export_name(
|
||||
self,
|
||||
):
|
||||
"""Export name."""
|
||||
return "model.onnx"
|
||||
@@ -0,0 +1,776 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# Copyright 2024 Kun Zou (chinazoukun@gmail.com). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
import time
|
||||
import copy
|
||||
import torch
|
||||
import logging
|
||||
from torch.cuda.amp import autocast
|
||||
from typing import Union, Dict, List, Tuple, Optional
|
||||
|
||||
from funasr.register import tables
|
||||
from funasr.models.ctc.ctc import CTC
|
||||
from funasr.utils import postprocess_utils
|
||||
from funasr.metrics.compute_acc import th_accuracy
|
||||
from funasr.train_utils.device_funcs import to_device
|
||||
from funasr.utils.datadir_writer import DatadirWriter
|
||||
from funasr.models.paraformer.search import Hypothesis
|
||||
from funasr.models.paraformer.cif_predictor import mae_loss
|
||||
from funasr.train_utils.device_funcs import force_gatherable
|
||||
from funasr.losses.label_smoothing_loss import LabelSmoothingLoss
|
||||
from funasr.models.transformer.utils.add_sos_eos import add_sos_eos, add_sos_and_eos
|
||||
from funasr.models.transformer.utils.nets_utils import make_pad_mask
|
||||
from funasr.utils.timestamp_tools import ts_prediction_lfr6_standard
|
||||
from funasr.utils.load_utils import load_audio_text_image_video, extract_fbank
|
||||
|
||||
|
||||
@tables.register("model_classes", "EParaformer")
|
||||
class EParaformer(torch.nn.Module):
|
||||
"""E-Paraformer: Enhanced Paraformer with streaming support.
|
||||
|
||||
Extended Paraformer supporting both offline and streaming modes
|
||||
through dynamic masking in the encoder. Used for 2-pass decoding
|
||||
where first pass provides streaming results and second pass refines.
|
||||
|
||||
Output: {"key": str, "text": str, "timestamp": [[start_ms, end_ms], ...]}
|
||||
|
||||
Author: Speech Lab of DAMO Academy, Alibaba Group
|
||||
Paraformer: Fast and Accurate Parallel Transformer for Non-autoregressive End-to-End Speech Recognition
|
||||
https://arxiv.org/abs/2206.08317
|
||||
Author: Kun Zou, chinazoukun@gmail.com
|
||||
E-Paraformer: A Faster and Better Parallel Transformer for Non-autoregressive End-to-End Mandarin Speech Recognition
|
||||
https://www.isca-archive.org/interspeech_2024/zou24_interspeech.pdf
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
specaug: Optional[str] = None,
|
||||
specaug_conf: Optional[Dict] = None,
|
||||
normalize: str = None,
|
||||
normalize_conf: Optional[Dict] = None,
|
||||
encoder: str = None,
|
||||
encoder_conf: Optional[Dict] = None,
|
||||
decoder: str = None,
|
||||
decoder_conf: Optional[Dict] = None,
|
||||
ctc: str = None,
|
||||
ctc_conf: Optional[Dict] = None,
|
||||
predictor: str = None,
|
||||
predictor_conf: Optional[Dict] = None,
|
||||
ctc_weight: float = 0.5,
|
||||
input_size: int = 80,
|
||||
vocab_size: int = -1,
|
||||
ignore_id: int = -1,
|
||||
blank_id: int = 0,
|
||||
sos: int = 1,
|
||||
eos: int = 2,
|
||||
lsm_weight: float = 0.0,
|
||||
length_normalized_loss: bool = False,
|
||||
# report_cer: bool = True,
|
||||
# report_wer: bool = True,
|
||||
# sym_space: str = "<space>",
|
||||
# sym_blank: str = "<blank>",
|
||||
# extract_feats_in_collect_stats: bool = True,
|
||||
# predictor=None,
|
||||
predictor_weight: float = 0.0,
|
||||
predictor_bias: int = 2,
|
||||
sampling_ratio: float = 0.2,
|
||||
share_embedding: bool = False,
|
||||
# preencoder: Optional[AbsPreEncoder] = None,
|
||||
# postencoder: Optional[AbsPostEncoder] = None,
|
||||
use_1st_decoder_loss: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
|
||||
"""Initialize EParaformer.
|
||||
|
||||
Args:
|
||||
specaug: TODO.
|
||||
specaug_conf: Configuration dict for specaug.
|
||||
normalize: TODO.
|
||||
normalize_conf: Configuration dict for normalize.
|
||||
encoder: TODO.
|
||||
encoder_conf: Configuration dict for encoder.
|
||||
decoder: TODO.
|
||||
decoder_conf: Configuration dict for decoder.
|
||||
ctc: TODO.
|
||||
ctc_conf: Configuration dict for ctc.
|
||||
predictor: TODO.
|
||||
predictor_conf: Configuration dict for predictor.
|
||||
ctc_weight: TODO.
|
||||
input_size: Size/dimension parameter.
|
||||
vocab_size: Size/dimension parameter.
|
||||
ignore_id: TODO.
|
||||
blank_id: TODO.
|
||||
sos: TODO.
|
||||
eos: TODO.
|
||||
lsm_weight: TODO.
|
||||
length_normalized_loss: TODO.
|
||||
predictor_weight: TODO.
|
||||
predictor_bias: TODO.
|
||||
sampling_ratio: TODO.
|
||||
share_embedding: TODO.
|
||||
use_1st_decoder_loss: TODO.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
if specaug is not None:
|
||||
specaug_class = tables.specaug_classes.get(specaug)
|
||||
specaug = specaug_class(**specaug_conf)
|
||||
if normalize is not None:
|
||||
normalize_class = tables.normalize_classes.get(normalize)
|
||||
normalize = normalize_class(**normalize_conf)
|
||||
encoder_class = tables.encoder_classes.get(encoder)
|
||||
encoder = encoder_class(input_size=input_size, **encoder_conf)
|
||||
encoder_output_size = encoder.output_size()
|
||||
|
||||
if decoder is not None:
|
||||
decoder_class = tables.decoder_classes.get(decoder)
|
||||
decoder = decoder_class(
|
||||
vocab_size=vocab_size,
|
||||
encoder_output_size=encoder_output_size,
|
||||
**decoder_conf,
|
||||
)
|
||||
if ctc_weight > 0.0:
|
||||
|
||||
if ctc_conf is None:
|
||||
ctc_conf = {}
|
||||
|
||||
ctc = CTC(odim=vocab_size, encoder_output_size=encoder_output_size, **ctc_conf)
|
||||
if predictor is not None:
|
||||
predictor_class = tables.predictor_classes.get(predictor)
|
||||
predictor = predictor_class(**predictor_conf)
|
||||
|
||||
# note that eos is the same as sos (equivalent ID)
|
||||
self.blank_id = blank_id
|
||||
self.sos = sos if sos is not None else vocab_size - 1
|
||||
self.eos = eos if eos is not None else vocab_size - 1
|
||||
self.vocab_size = vocab_size
|
||||
self.ignore_id = ignore_id
|
||||
self.ctc_weight = ctc_weight
|
||||
# self.token_list = token_list.copy()
|
||||
#
|
||||
# self.frontend = frontend
|
||||
self.specaug = specaug
|
||||
self.normalize = normalize
|
||||
# self.preencoder = preencoder
|
||||
# self.postencoder = postencoder
|
||||
self.encoder = encoder
|
||||
#
|
||||
# if not hasattr(self.encoder, "interctc_use_conditioning"):
|
||||
# self.encoder.interctc_use_conditioning = False
|
||||
# if self.encoder.interctc_use_conditioning:
|
||||
# self.encoder.conditioning_layer = torch.nn.Linear(
|
||||
# vocab_size, self.encoder.output_size()
|
||||
# )
|
||||
#
|
||||
# self.error_calculator = None
|
||||
#
|
||||
if ctc_weight == 1.0:
|
||||
self.decoder = None
|
||||
else:
|
||||
self.decoder = decoder
|
||||
|
||||
self.criterion_att = LabelSmoothingLoss(
|
||||
size=vocab_size,
|
||||
padding_idx=ignore_id,
|
||||
smoothing=lsm_weight,
|
||||
normalize_length=length_normalized_loss,
|
||||
)
|
||||
|
||||
if use_1st_decoder_loss:
|
||||
self.criterion_att_1st = LabelSmoothingLoss(
|
||||
size=vocab_size,
|
||||
padding_idx=ignore_id,
|
||||
smoothing=lsm_weight,
|
||||
normalize_length=length_normalized_loss,
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# if report_cer or report_wer:
|
||||
# self.error_calculator = ErrorCalculator(
|
||||
# token_list, sym_space, sym_blank, report_cer, report_wer
|
||||
# )
|
||||
#
|
||||
if ctc_weight == 0.0:
|
||||
self.ctc = None
|
||||
else:
|
||||
self.ctc = ctc
|
||||
#
|
||||
# self.extract_feats_in_collect_stats = extract_feats_in_collect_stats
|
||||
self.predictor = predictor
|
||||
self.predictor_weight = predictor_weight
|
||||
self.predictor_bias = predictor_bias
|
||||
self.sampling_ratio = sampling_ratio
|
||||
self.criterion_pre = mae_loss(normalize_length=length_normalized_loss)
|
||||
|
||||
self.share_embedding = share_embedding
|
||||
if self.share_embedding:
|
||||
self.decoder.embed = None
|
||||
|
||||
self.use_1st_decoder_loss = use_1st_decoder_loss
|
||||
self.length_normalized_loss = length_normalized_loss
|
||||
self.beam_search = None
|
||||
self.error_calculator = None
|
||||
|
||||
def forward(
|
||||
self,
|
||||
speech: torch.Tensor,
|
||||
speech_lengths: torch.Tensor,
|
||||
text: torch.Tensor,
|
||||
text_lengths: torch.Tensor,
|
||||
**kwargs,
|
||||
) -> Tuple[torch.Tensor, Dict[str, torch.Tensor], torch.Tensor]:
|
||||
"""Encoder + Decoder + Calc loss
|
||||
Args:
|
||||
speech: (Batch, Length, ...)
|
||||
speech_lengths: (Batch, )
|
||||
text: (Batch, Length)
|
||||
text_lengths: (Batch,)
|
||||
"""
|
||||
if len(text_lengths.size()) > 1:
|
||||
text_lengths = text_lengths[:, 0]
|
||||
if len(speech_lengths.size()) > 1:
|
||||
speech_lengths = speech_lengths[:, 0]
|
||||
|
||||
batch_size = speech.shape[0]
|
||||
|
||||
# Encoder
|
||||
encoder_out, encoder_out_lens = self.encode(speech, speech_lengths)
|
||||
|
||||
loss_ctc, cer_ctc = None, None
|
||||
loss_pre = None
|
||||
stats = dict()
|
||||
|
||||
# decoder: CTC branch
|
||||
if self.ctc_weight != 0.0:
|
||||
loss_ctc, cer_ctc = self._calc_ctc_loss(
|
||||
encoder_out, encoder_out_lens, text, text_lengths
|
||||
)
|
||||
|
||||
# Collect CTC branch stats
|
||||
stats["loss_ctc"] = loss_ctc.detach() if loss_ctc is not None else None
|
||||
stats["cer_ctc"] = cer_ctc
|
||||
|
||||
# decoder: Attention decoder branch
|
||||
loss_att, acc_att, cer_att, wer_att, loss_pre, pre_loss_att = self._calc_att_loss(
|
||||
encoder_out, encoder_out_lens, text, text_lengths
|
||||
)
|
||||
|
||||
# 3. CTC-Att loss definition
|
||||
if self.ctc_weight == 0.0:
|
||||
loss = loss_att + loss_pre * self.predictor_weight
|
||||
else:
|
||||
loss = (
|
||||
self.ctc_weight * loss_ctc
|
||||
+ (1 - self.ctc_weight) * loss_att
|
||||
+ loss_pre * self.predictor_weight
|
||||
)
|
||||
if pre_loss_att is not None:
|
||||
loss += pre_loss_att
|
||||
# Collect Attn branch stats
|
||||
stats["loss_att"] = loss_att.detach() if loss_att is not None else None
|
||||
stats["pre_loss_att"] = pre_loss_att.detach() if pre_loss_att is not None else None
|
||||
stats["acc"] = acc_att
|
||||
stats["cer"] = cer_att
|
||||
stats["wer"] = wer_att
|
||||
stats["loss_pre"] = loss_pre.detach().cpu() if loss_pre is not None else None
|
||||
|
||||
stats["loss"] = torch.clone(loss.detach())
|
||||
stats["batch_size"] = batch_size
|
||||
|
||||
# force_gatherable: to-device and to-tensor if scalar for DataParallel
|
||||
if self.length_normalized_loss:
|
||||
batch_size = (text_lengths + self.predictor_bias).sum()
|
||||
loss, stats, weight = force_gatherable((loss, stats, batch_size), loss.device)
|
||||
return loss, stats, weight
|
||||
|
||||
def encode(
|
||||
self,
|
||||
speech: torch.Tensor,
|
||||
speech_lengths: torch.Tensor,
|
||||
**kwargs,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Encoder. Note that this method is used by asr_inference.py
|
||||
Args:
|
||||
speech: (Batch, Length, ...)
|
||||
speech_lengths: (Batch, )
|
||||
ind: int
|
||||
"""
|
||||
with autocast(False):
|
||||
|
||||
# Data augmentation
|
||||
if self.specaug is not None and self.training:
|
||||
speech, speech_lengths = self.specaug(speech, speech_lengths)
|
||||
|
||||
# Normalization for feature: e.g. Global-CMVN, Utterance-CMVN
|
||||
if self.normalize is not None:
|
||||
speech, speech_lengths = self.normalize(speech, speech_lengths)
|
||||
|
||||
# Forward encoder
|
||||
encoder_out, encoder_out_lens, _ = self.encoder(speech, speech_lengths)
|
||||
if isinstance(encoder_out, tuple):
|
||||
encoder_out = encoder_out[0]
|
||||
|
||||
return encoder_out, encoder_out_lens
|
||||
|
||||
def calc_predictor(self, encoder_out, encoder_out_lens):
|
||||
|
||||
"""Calc predictor.
|
||||
|
||||
Args:
|
||||
encoder_out: Encoder output tensor.
|
||||
encoder_out_lens: Encoder output lengths.
|
||||
"""
|
||||
encoder_out_mask = (
|
||||
~make_pad_mask(encoder_out_lens, maxlen=encoder_out.size(1))[:, None, :]
|
||||
).to(encoder_out.device)
|
||||
pre_acoustic_embeds, pre_token_length, alphas, pre_peak_index = self.predictor(
|
||||
encoder_out, None, encoder_out_mask, ignore_id=self.ignore_id
|
||||
)
|
||||
return pre_acoustic_embeds, pre_token_length, alphas, pre_peak_index
|
||||
|
||||
def cal_decoder_with_predictor(
|
||||
self, encoder_out, encoder_out_lens, sematic_embeds, ys_pad_lens
|
||||
):
|
||||
|
||||
"""Cal decoder with predictor.
|
||||
|
||||
Args:
|
||||
encoder_out: Encoder output tensor.
|
||||
encoder_out_lens: Encoder output lengths.
|
||||
sematic_embeds: TODO.
|
||||
ys_pad_lens: Lengths of ys_pad.
|
||||
"""
|
||||
decoder_outs = self.decoder(encoder_out, encoder_out_lens, sematic_embeds, ys_pad_lens)
|
||||
decoder_out = decoder_outs[0]
|
||||
decoder_out = torch.log_softmax(decoder_out, dim=-1)
|
||||
return decoder_out, ys_pad_lens
|
||||
|
||||
def _calc_att_loss(
|
||||
self,
|
||||
encoder_out: torch.Tensor,
|
||||
encoder_out_lens: torch.Tensor,
|
||||
ys_pad: torch.Tensor,
|
||||
ys_pad_lens: torch.Tensor,
|
||||
):
|
||||
"""Internal: calc att loss.
|
||||
|
||||
Args:
|
||||
encoder_out: Encoder output tensor.
|
||||
encoder_out_lens: Encoder output lengths.
|
||||
ys_pad: TODO.
|
||||
ys_pad_lens: Lengths of ys_pad.
|
||||
"""
|
||||
encoder_out_mask = (
|
||||
~make_pad_mask(encoder_out_lens, maxlen=encoder_out.size(1))[:, None, :]
|
||||
).to(encoder_out.device)
|
||||
if self.predictor_bias == 1:
|
||||
_, ys_pad = add_sos_eos(ys_pad, self.sos, self.eos, self.ignore_id)
|
||||
ys_pad_lens = ys_pad_lens + self.predictor_bias
|
||||
if self.predictor_bias == 2:
|
||||
_, ys_pad = add_sos_and_eos(ys_pad, self.sos, self.eos, self.ignore_id)
|
||||
ys_pad_lens = ys_pad_lens + self.predictor_bias
|
||||
|
||||
pre_acoustic_embeds, pre_token_length, _, pre_peak_index = self.predictor(
|
||||
encoder_out, ys_pad, encoder_out_mask, ignore_id=self.ignore_id
|
||||
)
|
||||
|
||||
# 0. sampler
|
||||
decoder_out_1st = None
|
||||
pre_loss_att = None
|
||||
if self.sampling_ratio > 0.0:
|
||||
if self.use_1st_decoder_loss:
|
||||
sematic_embeds, decoder_out_1st = self.sampler_with_grad(
|
||||
encoder_out, encoder_out_lens, ys_pad, ys_pad_lens, pre_acoustic_embeds
|
||||
)
|
||||
else:
|
||||
|
||||
sematic_embeds, decoder_out_1st = self.sampler(
|
||||
encoder_out, encoder_out_lens, ys_pad, ys_pad_lens, pre_acoustic_embeds
|
||||
)
|
||||
else:
|
||||
sematic_embeds = pre_acoustic_embeds
|
||||
|
||||
# 1. Forward decoder
|
||||
decoder_outs = self.decoder(encoder_out, encoder_out_lens, sematic_embeds, ys_pad_lens)
|
||||
decoder_out, _ = decoder_outs[0], decoder_outs[1]
|
||||
|
||||
if decoder_out_1st is None:
|
||||
decoder_out_1st = decoder_out
|
||||
# 2. Compute attention loss
|
||||
if self.use_1st_decoder_loss:
|
||||
pre_loss_att = self.criterion_att_1st(decoder_out_1st, ys_pad)
|
||||
loss_att = self.criterion_att(decoder_out, ys_pad)
|
||||
acc_att = th_accuracy(
|
||||
decoder_out_1st.view(-1, self.vocab_size),
|
||||
ys_pad,
|
||||
ignore_label=self.ignore_id,
|
||||
)
|
||||
loss_pre = self.criterion_pre(ys_pad_lens.type_as(pre_token_length), pre_token_length)
|
||||
|
||||
# Compute cer/wer using attention-decoder
|
||||
if self.training or self.error_calculator is None:
|
||||
cer_att, wer_att = None, None
|
||||
else:
|
||||
ys_hat = decoder_out_1st.argmax(dim=-1)
|
||||
cer_att, wer_att = self.error_calculator(ys_hat.cpu(), ys_pad.cpu())
|
||||
|
||||
return loss_att, acc_att, cer_att, wer_att, loss_pre, pre_loss_att
|
||||
|
||||
def sampler(self, encoder_out, encoder_out_lens, ys_pad, ys_pad_lens, pre_acoustic_embeds):
|
||||
|
||||
"""Sampler.
|
||||
|
||||
Args:
|
||||
encoder_out: Encoder output tensor.
|
||||
encoder_out_lens: Encoder output lengths.
|
||||
ys_pad: TODO.
|
||||
ys_pad_lens: Lengths of ys_pad.
|
||||
pre_acoustic_embeds: TODO.
|
||||
"""
|
||||
tgt_mask = (~make_pad_mask(ys_pad_lens, maxlen=ys_pad_lens.max())[:, :, None]).to(
|
||||
ys_pad.device
|
||||
)
|
||||
ys_pad_masked = ys_pad * tgt_mask[:, :, 0]
|
||||
if self.share_embedding:
|
||||
ys_pad_embed = self.decoder.output_layer.weight[ys_pad_masked]
|
||||
else:
|
||||
ys_pad_embed = self.decoder.embed(ys_pad_masked)
|
||||
with torch.no_grad():
|
||||
decoder_outs = self.decoder(
|
||||
encoder_out, encoder_out_lens, pre_acoustic_embeds, ys_pad_lens
|
||||
)
|
||||
decoder_out, _ = decoder_outs[0], decoder_outs[1]
|
||||
pred_tokens = decoder_out.argmax(-1)
|
||||
nonpad_positions = ys_pad.ne(self.ignore_id)
|
||||
seq_lens = (nonpad_positions).sum(1)
|
||||
same_num = ((pred_tokens == ys_pad) & nonpad_positions).sum(1)
|
||||
input_mask = torch.ones_like(nonpad_positions)
|
||||
bsz, seq_len = ys_pad.size()
|
||||
for li in range(bsz):
|
||||
target_num = (
|
||||
((seq_lens[li] - same_num[li].sum()).float()) * self.sampling_ratio
|
||||
).long()
|
||||
if target_num > 0:
|
||||
input_mask[li].scatter_(
|
||||
dim=0,
|
||||
index=torch.randperm(seq_lens[li])[:target_num].to(input_mask.device),
|
||||
value=0,
|
||||
)
|
||||
input_mask = input_mask.eq(1)
|
||||
input_mask = input_mask.masked_fill(~nonpad_positions, False)
|
||||
input_mask_expand_dim = input_mask.unsqueeze(2).to(pre_acoustic_embeds.device)
|
||||
|
||||
sematic_embeds = pre_acoustic_embeds.masked_fill(
|
||||
~input_mask_expand_dim, 0
|
||||
) + ys_pad_embed.masked_fill(input_mask_expand_dim, 0)
|
||||
return sematic_embeds * tgt_mask, decoder_out * tgt_mask
|
||||
|
||||
def sampler_with_grad(self, encoder_out, encoder_out_lens, ys_pad, ys_pad_lens, pre_acoustic_embeds):
|
||||
|
||||
"""Sampler with grad.
|
||||
|
||||
Args:
|
||||
encoder_out: Encoder output tensor.
|
||||
encoder_out_lens: Encoder output lengths.
|
||||
ys_pad: TODO.
|
||||
ys_pad_lens: Lengths of ys_pad.
|
||||
pre_acoustic_embeds: TODO.
|
||||
"""
|
||||
tgt_mask = (~make_pad_mask(ys_pad_lens, maxlen=ys_pad_lens.max())[:, :, None]).to(
|
||||
ys_pad.device
|
||||
)
|
||||
ys_pad_masked = ys_pad * tgt_mask[:, :, 0]
|
||||
if self.share_embedding:
|
||||
ys_pad_embed = self.decoder.output_layer.weight[ys_pad_masked]
|
||||
else:
|
||||
ys_pad_embed = self.decoder.embed(ys_pad_masked)
|
||||
decoder_outs = self.decoder(
|
||||
encoder_out, encoder_out_lens, pre_acoustic_embeds, ys_pad_lens
|
||||
)
|
||||
decoder_out, _ = decoder_outs[0], decoder_outs[1]
|
||||
pred_tokens = decoder_out.argmax(-1)
|
||||
nonpad_positions = ys_pad.ne(self.ignore_id)
|
||||
seq_lens = (nonpad_positions).sum(1)
|
||||
same_num = ((pred_tokens == ys_pad) & nonpad_positions).sum(1)
|
||||
input_mask = torch.ones_like(nonpad_positions)
|
||||
bsz, seq_len = ys_pad.size()
|
||||
for li in range(bsz):
|
||||
target_num = (
|
||||
((seq_lens[li] - same_num[li].sum()).float()) * self.sampling_ratio
|
||||
).long()
|
||||
if target_num > 0:
|
||||
input_mask[li].scatter_(
|
||||
dim=0,
|
||||
index=torch.randperm(seq_lens[li])[:target_num].to(input_mask.device),
|
||||
value=0,
|
||||
)
|
||||
input_mask = input_mask.eq(1)
|
||||
input_mask = input_mask.masked_fill(~nonpad_positions, False)
|
||||
input_mask_expand_dim = input_mask.unsqueeze(2).to(pre_acoustic_embeds.device)
|
||||
|
||||
sematic_embeds = pre_acoustic_embeds.masked_fill(
|
||||
~input_mask_expand_dim, 0
|
||||
) + ys_pad_embed.masked_fill(input_mask_expand_dim, 0)
|
||||
return sematic_embeds * tgt_mask, decoder_out * tgt_mask
|
||||
|
||||
|
||||
def _calc_ctc_loss(
|
||||
self,
|
||||
encoder_out: torch.Tensor,
|
||||
encoder_out_lens: torch.Tensor,
|
||||
ys_pad: torch.Tensor,
|
||||
ys_pad_lens: torch.Tensor,
|
||||
):
|
||||
# Calc CTC loss
|
||||
"""Internal: calc ctc loss.
|
||||
|
||||
Args:
|
||||
encoder_out: Encoder output tensor.
|
||||
encoder_out_lens: Encoder output lengths.
|
||||
ys_pad: TODO.
|
||||
ys_pad_lens: Lengths of ys_pad.
|
||||
"""
|
||||
loss_ctc = self.ctc(encoder_out, encoder_out_lens, ys_pad, ys_pad_lens)
|
||||
|
||||
# Calc CER using CTC
|
||||
cer_ctc = None
|
||||
if not self.training and self.error_calculator is not None:
|
||||
ys_hat = self.ctc.argmax(encoder_out).data
|
||||
cer_ctc = self.error_calculator(ys_hat.cpu(), ys_pad.cpu(), is_ctc=True)
|
||||
return loss_ctc, cer_ctc
|
||||
|
||||
def init_beam_search(
|
||||
self,
|
||||
**kwargs,
|
||||
):
|
||||
"""Init beam search.
|
||||
|
||||
Args:
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
from funasr.models.paraformer.search import BeamSearchPara
|
||||
from funasr.models.transformer.scorers.ctc import CTCPrefixScorer
|
||||
from funasr.models.transformer.scorers.length_bonus import LengthBonus
|
||||
|
||||
# 1. Build ASR model
|
||||
scorers = {}
|
||||
|
||||
if self.ctc != None:
|
||||
ctc = CTCPrefixScorer(ctc=self.ctc, eos=self.eos)
|
||||
scorers.update(ctc=ctc)
|
||||
token_list = kwargs.get("token_list")
|
||||
scorers.update(
|
||||
length_bonus=LengthBonus(len(token_list)),
|
||||
)
|
||||
|
||||
# 3. Build ngram model
|
||||
# ngram is not supported now
|
||||
ngram = None
|
||||
scorers["ngram"] = ngram
|
||||
|
||||
weights = dict(
|
||||
decoder=1.0 - kwargs.get("decoding_ctc_weight"),
|
||||
ctc=kwargs.get("decoding_ctc_weight", 0.0),
|
||||
lm=kwargs.get("lm_weight", 0.0),
|
||||
ngram=kwargs.get("ngram_weight", 0.0),
|
||||
length_bonus=kwargs.get("penalty", 0.0),
|
||||
)
|
||||
beam_search = BeamSearchPara(
|
||||
beam_size=kwargs.get("beam_size", 2),
|
||||
weights=weights,
|
||||
scorers=scorers,
|
||||
sos=self.sos,
|
||||
eos=self.eos,
|
||||
vocab_size=len(token_list),
|
||||
token_list=token_list,
|
||||
pre_beam_score_key=None if self.ctc_weight == 1.0 else "full",
|
||||
)
|
||||
# beam_search.to(device=kwargs.get("device", "cpu"), dtype=getattr(torch, kwargs.get("dtype", "float32"))).eval()
|
||||
# for scorer in scorers.values():
|
||||
# if isinstance(scorer, torch.nn.Module):
|
||||
# scorer.to(device=kwargs.get("device", "cpu"), dtype=getattr(torch, kwargs.get("dtype", "float32"))).eval()
|
||||
self.beam_search = beam_search
|
||||
|
||||
def inference(
|
||||
self,
|
||||
data_in,
|
||||
data_lengths=None,
|
||||
key: list = None,
|
||||
tokenizer=None,
|
||||
frontend=None,
|
||||
**kwargs,
|
||||
):
|
||||
# init beamsearch
|
||||
"""Run inference on input data.
|
||||
|
||||
Args:
|
||||
data_in: Input data (audio samples, file paths, or text).
|
||||
data_lengths: Lengths of each input sample in the batch.
|
||||
key: Sample identifiers.
|
||||
tokenizer: Tokenizer instance for text encoding/decoding.
|
||||
frontend: Audio frontend for feature extraction.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
is_use_ctc = kwargs.get("decoding_ctc_weight", 0.0) > 0.00001 and self.ctc != None
|
||||
is_use_lm = (
|
||||
kwargs.get("lm_weight", 0.0) > 0.00001 and kwargs.get("lm_file", None) is not None
|
||||
)
|
||||
pred_timestamp = kwargs.get("pred_timestamp", False)
|
||||
if self.beam_search is None and (is_use_lm or is_use_ctc):
|
||||
logging.info("enable beam_search")
|
||||
self.init_beam_search(**kwargs)
|
||||
self.nbest = kwargs.get("nbest", 1)
|
||||
|
||||
meta_data = {}
|
||||
if (
|
||||
isinstance(data_in, torch.Tensor) and kwargs.get("data_type", "sound") == "fbank"
|
||||
): # fbank
|
||||
speech, speech_lengths = data_in, data_lengths
|
||||
if len(speech.shape) < 3:
|
||||
speech = speech[None, :, :]
|
||||
if speech_lengths is not None:
|
||||
speech_lengths = speech_lengths.squeeze(-1)
|
||||
else:
|
||||
speech_lengths = speech.shape[1]
|
||||
else:
|
||||
# extract fbank feats
|
||||
time1 = time.perf_counter()
|
||||
audio_sample_list = load_audio_text_image_video(
|
||||
data_in,
|
||||
fs=frontend.fs,
|
||||
audio_fs=kwargs.get("fs", 16000),
|
||||
data_type=kwargs.get("data_type", "sound"),
|
||||
tokenizer=tokenizer,
|
||||
)
|
||||
time2 = time.perf_counter()
|
||||
meta_data["load_data"] = f"{time2 - time1:0.3f}"
|
||||
speech, speech_lengths = extract_fbank(
|
||||
audio_sample_list, data_type=kwargs.get("data_type", "sound"), frontend=frontend
|
||||
)
|
||||
time3 = time.perf_counter()
|
||||
meta_data["extract_feat"] = f"{time3 - time2:0.3f}"
|
||||
meta_data["batch_data_time"] = (
|
||||
speech_lengths.sum().item() * frontend.frame_shift * frontend.lfr_n / 1000
|
||||
)
|
||||
|
||||
speech = speech.to(device=kwargs["device"])
|
||||
speech_lengths = speech_lengths.to(device=kwargs["device"])
|
||||
# Encoder
|
||||
if kwargs.get("fp16", False):
|
||||
speech = speech.half()
|
||||
encoder_out, encoder_out_lens = self.encode(speech, speech_lengths)
|
||||
if isinstance(encoder_out, tuple):
|
||||
encoder_out = encoder_out[0]
|
||||
|
||||
# predictor
|
||||
predictor_outs = self.calc_predictor(encoder_out, encoder_out_lens)
|
||||
pre_acoustic_embeds, pre_token_length, alphas, pre_peak_index = (
|
||||
predictor_outs[0],
|
||||
predictor_outs[1],
|
||||
predictor_outs[2],
|
||||
predictor_outs[3],
|
||||
)
|
||||
|
||||
pre_token_length = pre_token_length.round().long()
|
||||
if torch.max(pre_token_length) < 1:
|
||||
return []
|
||||
decoder_outs = self.cal_decoder_with_predictor(
|
||||
encoder_out, encoder_out_lens, pre_acoustic_embeds, pre_token_length
|
||||
)
|
||||
decoder_out, ys_pad_lens = decoder_outs[0], decoder_outs[1]
|
||||
|
||||
results = []
|
||||
b, n, d = decoder_out.size()
|
||||
if isinstance(key[0], (list, tuple)):
|
||||
key = key[0]
|
||||
if len(key) < b:
|
||||
key = key * b
|
||||
for i in range(b):
|
||||
x = encoder_out[i, : encoder_out_lens[i], :]
|
||||
am_scores = decoder_out[i, : pre_token_length[i], :]
|
||||
if self.beam_search is not None:
|
||||
nbest_hyps = self.beam_search(
|
||||
x=x,
|
||||
am_scores=am_scores,
|
||||
maxlenratio=kwargs.get("maxlenratio", 0.0),
|
||||
minlenratio=kwargs.get("minlenratio", 0.0),
|
||||
)
|
||||
|
||||
nbest_hyps = nbest_hyps[: self.nbest]
|
||||
else:
|
||||
|
||||
yseq = am_scores.argmax(dim=-1)
|
||||
score = am_scores.max(dim=-1)[0]
|
||||
score = torch.sum(score, dim=-1)
|
||||
# pad with mask tokens to ensure compatibility with sos/eos tokens
|
||||
yseq = torch.tensor([self.sos] + yseq.tolist() + [self.eos], device=yseq.device)
|
||||
nbest_hyps = [Hypothesis(yseq=yseq, score=score)]
|
||||
for nbest_idx, hyp in enumerate(nbest_hyps):
|
||||
ibest_writer = None
|
||||
if kwargs.get("output_dir") is not None:
|
||||
if not hasattr(self, "writer"):
|
||||
self.writer = DatadirWriter(kwargs.get("output_dir"))
|
||||
ibest_writer = self.writer[f"{nbest_idx+1}best_recog"]
|
||||
# remove sos/eos and get results
|
||||
last_pos = -1
|
||||
if isinstance(hyp.yseq, list):
|
||||
token_int = hyp.yseq[1:last_pos]
|
||||
else:
|
||||
token_int = hyp.yseq[1:last_pos].tolist()
|
||||
|
||||
# remove blank symbol id, which is assumed to be 0
|
||||
token_int = list(
|
||||
filter(
|
||||
lambda x: x != self.eos and x != self.sos and x != self.blank_id, token_int
|
||||
)
|
||||
)
|
||||
|
||||
if tokenizer is not None:
|
||||
# Change integer-ids to tokens
|
||||
token = tokenizer.ids2tokens(token_int)
|
||||
text_postprocessed = tokenizer.tokens2text(token)
|
||||
|
||||
if pred_timestamp:
|
||||
timestamp_str, timestamp = ts_prediction_lfr6_standard(
|
||||
pre_peak_index[i],
|
||||
alphas[i],
|
||||
copy.copy(token),
|
||||
vad_offset=kwargs.get("begin_time", 0),
|
||||
upsample_rate=1,
|
||||
)
|
||||
if not hasattr(tokenizer, "bpemodel"):
|
||||
text_postprocessed, time_stamp_postprocessed, _ = postprocess_utils.sentence_postprocess(token, timestamp)
|
||||
result_i = {"key": key[i], "text": text_postprocessed, "timestamp": time_stamp_postprocessed,}
|
||||
else:
|
||||
if not hasattr(tokenizer, "bpemodel"):
|
||||
text_postprocessed, _ = postprocess_utils.sentence_postprocess(token)
|
||||
result_i = {"key": key[i], "text": text_postprocessed}
|
||||
|
||||
if ibest_writer is not None:
|
||||
ibest_writer["token"][key[i]] = " ".join(token)
|
||||
# ibest_writer["text"][key[i]] = text
|
||||
ibest_writer["text"][key[i]] = text_postprocessed
|
||||
else:
|
||||
result_i = {"key": key[i], "token_int": token_int}
|
||||
results.append(result_i)
|
||||
|
||||
return results, meta_data
|
||||
|
||||
def export(self, **kwargs):
|
||||
"""Export.
|
||||
|
||||
Args:
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
from .export_meta import export_rebuild_model
|
||||
|
||||
if "max_seq_len" not in kwargs:
|
||||
kwargs["max_seq_len"] = 512
|
||||
models = export_rebuild_model(model=self, **kwargs)
|
||||
return models
|
||||
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# Copyright 2024 Kun Zou (chinazoukun@gmail.com). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
import torch
|
||||
import logging
|
||||
import numpy as np
|
||||
|
||||
from funasr.register import tables
|
||||
from funasr.train_utils.device_funcs import to_device
|
||||
from funasr.models.transformer.utils.nets_utils import make_pad_mask
|
||||
from torch.cuda.amp import autocast
|
||||
|
||||
|
||||
@tables.register("predictor_classes", "PifPredictor")
|
||||
class PifPredictor(torch.nn.Module):
|
||||
"""
|
||||
Author: Kun Zou, chinazoukun@gmail.com
|
||||
E-Paraformer: A Faster and Better Parallel Transformer for Non-autoregressive End-to-End Mandarin Speech Recognition
|
||||
https://www.isca-archive.org/interspeech_2024/zou24_interspeech.pdf
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
idim,
|
||||
l_order,
|
||||
r_order,
|
||||
threshold=1.0,
|
||||
dropout=0.1,
|
||||
smooth_factor=1.0,
|
||||
noise_threshold=0,
|
||||
sigma=0.5,
|
||||
bias=0.0,
|
||||
sigma_heads=4,
|
||||
):
|
||||
"""Initialize PifPredictor.
|
||||
|
||||
Args:
|
||||
idim: TODO.
|
||||
l_order: TODO.
|
||||
r_order: TODO.
|
||||
threshold: TODO.
|
||||
dropout: TODO.
|
||||
smooth_factor: TODO.
|
||||
noise_threshold: TODO.
|
||||
sigma: TODO.
|
||||
bias: TODO.
|
||||
sigma_heads: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.pad = torch.nn.ConstantPad1d((l_order, r_order), 0)
|
||||
self.cif_conv1d = torch.nn.Conv1d(idim, idim, l_order + r_order + 1, groups=idim)
|
||||
self.cif_output = torch.nn.Linear(idim, 1)
|
||||
self.dropout = torch.nn.Dropout(p=dropout)
|
||||
self.threshold = threshold
|
||||
self.smooth_factor = smooth_factor
|
||||
self.noise_threshold = noise_threshold
|
||||
self.sigma = torch.nn.Parameter(torch.tensor([sigma]*sigma_heads))
|
||||
self.bias = torch.nn.Parameter(torch.tensor([bias]*sigma_heads))
|
||||
self.sigma_heads = sigma_heads
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden,
|
||||
target_label=None,
|
||||
mask=None,
|
||||
ignore_id=-1,
|
||||
mask_chunk_predictor=None,
|
||||
target_label_length=None,
|
||||
):
|
||||
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
hidden: TODO.
|
||||
target_label: TODO.
|
||||
mask: TODO.
|
||||
ignore_id: TODO.
|
||||
mask_chunk_predictor: TODO.
|
||||
target_label_length: TODO.
|
||||
"""
|
||||
with autocast(False):
|
||||
h = hidden
|
||||
context = h.transpose(1, 2)
|
||||
queries = self.pad(context)
|
||||
memory = self.cif_conv1d(queries)
|
||||
output = memory + context
|
||||
output = self.dropout(output)
|
||||
output = output.transpose(1, 2)
|
||||
output = torch.relu(output)
|
||||
output = self.cif_output(output)
|
||||
alphas = torch.sigmoid(output)
|
||||
alphas = torch.nn.functional.relu(alphas * self.smooth_factor - self.noise_threshold)
|
||||
if mask is not None:
|
||||
mask = mask.transpose(-1, -2).float()
|
||||
alphas = alphas * mask
|
||||
if mask_chunk_predictor is not None:
|
||||
alphas = alphas * mask_chunk_predictor
|
||||
alphas = alphas.squeeze(-1)
|
||||
mask = mask.squeeze(-1)
|
||||
if target_label_length is not None:
|
||||
target_length = target_label_length
|
||||
elif target_label is not None:
|
||||
target_mask = (target_label != ignore_id).float()
|
||||
target_length = target_mask.sum(-1)
|
||||
else:
|
||||
target_mask = None
|
||||
target_length = None
|
||||
token_num = alphas.sum(-1)
|
||||
if target_length is not None:
|
||||
alphas *= (target_length / token_num)[:, None].repeat(1, alphas.size(1))
|
||||
max_token_num = torch.max(target_length)
|
||||
else:
|
||||
token_num_int = token_num.round()
|
||||
alphas *=(token_num_int / token_num)[:, None]
|
||||
max_token_num = torch.max(token_num_int)
|
||||
alignment = torch.cumsum(alphas, dim=-1)
|
||||
fire_positions = (torch.arange(max_token_num) + 0.5).type_as(alphas).unsqueeze(0)
|
||||
scores = - ((fire_positions[:, None, :, None] - alignment[:, None, None, :]) * self.sigma[None, :, None, None]) **2 + self.bias[None, :, None, None]
|
||||
scores = scores.masked_fill(~(mask[:, None, None, :].to(torch.bool)), float("-inf"))
|
||||
weights = torch.softmax(scores, dim=-1)
|
||||
n_hidden = hidden.view(hidden.size(0), -1, self.sigma_heads, hidden.size(-1) // self.sigma_heads).transpose(1, 2)
|
||||
acoustic_embeds = torch.matmul(weights, n_hidden).transpose(1,2).contiguous().view(hidden.size(0), -1, hidden.size(-1))
|
||||
|
||||
if target_mask is not None:
|
||||
acoustic_embeds *= target_mask[:, :, None]
|
||||
cif_peak = None
|
||||
return acoustic_embeds, token_num, alphas, cif_peak
|
||||
|
||||
@@ -0,0 +1,451 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
import torch
|
||||
import logging
|
||||
from itertools import chain
|
||||
from typing import Any, Dict, List, NamedTuple, Tuple, Union
|
||||
|
||||
from funasr.metrics.common import end_detect
|
||||
from funasr.models.transformer.scorers.scorer_interface import (
|
||||
PartialScorerInterface,
|
||||
ScorerInterface,
|
||||
)
|
||||
|
||||
|
||||
class Hypothesis(NamedTuple):
|
||||
"""Hypothesis data type."""
|
||||
|
||||
yseq: torch.Tensor
|
||||
score: Union[float, torch.Tensor] = 0
|
||||
scores: Dict[str, Union[float, torch.Tensor]] = dict()
|
||||
states: Dict[str, Any] = dict()
|
||||
|
||||
def asdict(self) -> dict:
|
||||
"""Convert data to JSON-friendly dict."""
|
||||
return self._replace(
|
||||
yseq=self.yseq.tolist(),
|
||||
score=float(self.score),
|
||||
scores={k: float(v) for k, v in self.scores.items()},
|
||||
)._asdict()
|
||||
|
||||
|
||||
class BeamSearchPara(torch.nn.Module):
|
||||
"""Beam search implementation."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
scorers: Dict[str, ScorerInterface],
|
||||
weights: Dict[str, float],
|
||||
beam_size: int,
|
||||
vocab_size: int,
|
||||
sos: int,
|
||||
eos: int,
|
||||
token_list: List[str] = None,
|
||||
pre_beam_ratio: float = 1.5,
|
||||
pre_beam_score_key: str = None,
|
||||
):
|
||||
"""Initialize beam search.
|
||||
|
||||
Args:
|
||||
scorers (dict[str, ScorerInterface]): Dict of decoder modules
|
||||
e.g., Decoder, CTCPrefixScorer, LM
|
||||
The scorer will be ignored if it is `None`
|
||||
weights (dict[str, float]): Dict of weights for each scorers
|
||||
The scorer will be ignored if its weight is 0
|
||||
beam_size (int): The number of hypotheses kept during search
|
||||
vocab_size (int): The number of vocabulary
|
||||
sos (int): Start of sequence id
|
||||
eos (int): End of sequence id
|
||||
token_list (list[str]): List of tokens for debug log
|
||||
pre_beam_score_key (str): key of scores to perform pre-beam search
|
||||
pre_beam_ratio (float): beam size in the pre-beam search
|
||||
will be `int(pre_beam_ratio * beam_size)`
|
||||
|
||||
"""
|
||||
super().__init__()
|
||||
# set scorers
|
||||
self.weights = weights
|
||||
self.scorers = dict()
|
||||
self.full_scorers = dict()
|
||||
self.part_scorers = dict()
|
||||
# this module dict is required for recursive cast
|
||||
# `self.to(device, dtype)` in `recog.py`
|
||||
self.nn_dict = torch.nn.ModuleDict()
|
||||
for k, v in scorers.items():
|
||||
w = weights.get(k, 0)
|
||||
if w == 0 or v is None:
|
||||
continue
|
||||
assert isinstance(
|
||||
v, ScorerInterface
|
||||
), f"{k} ({type(v)}) does not implement ScorerInterface"
|
||||
self.scorers[k] = v
|
||||
if isinstance(v, PartialScorerInterface):
|
||||
self.part_scorers[k] = v
|
||||
else:
|
||||
self.full_scorers[k] = v
|
||||
if isinstance(v, torch.nn.Module):
|
||||
self.nn_dict[k] = v
|
||||
|
||||
# set configurations
|
||||
self.sos = sos
|
||||
self.eos = eos
|
||||
self.token_list = token_list
|
||||
self.pre_beam_size = int(pre_beam_ratio * beam_size)
|
||||
self.beam_size = beam_size
|
||||
self.n_vocab = vocab_size
|
||||
if (
|
||||
pre_beam_score_key is not None
|
||||
and pre_beam_score_key != "full"
|
||||
and pre_beam_score_key not in self.full_scorers
|
||||
):
|
||||
raise KeyError(f"{pre_beam_score_key} is not found in {self.full_scorers}")
|
||||
self.pre_beam_score_key = pre_beam_score_key
|
||||
self.do_pre_beam = (
|
||||
self.pre_beam_score_key is not None
|
||||
and self.pre_beam_size < self.n_vocab
|
||||
and len(self.part_scorers) > 0
|
||||
)
|
||||
|
||||
def init_hyp(self, x: torch.Tensor) -> List[Hypothesis]:
|
||||
"""Get an initial hypothesis data.
|
||||
|
||||
Args:
|
||||
x (torch.Tensor): The encoder output feature
|
||||
|
||||
Returns:
|
||||
Hypothesis: The initial hypothesis.
|
||||
|
||||
"""
|
||||
init_states = dict()
|
||||
init_scores = dict()
|
||||
for k, d in self.scorers.items():
|
||||
init_states[k] = d.init_state(x)
|
||||
init_scores[k] = 0.0
|
||||
return [
|
||||
Hypothesis(
|
||||
score=0.0,
|
||||
scores=init_scores,
|
||||
states=init_states,
|
||||
yseq=torch.tensor([self.sos], device=x.device),
|
||||
)
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def append_token(xs: torch.Tensor, x: int) -> torch.Tensor:
|
||||
"""Append new token to prefix tokens.
|
||||
|
||||
Args:
|
||||
xs (torch.Tensor): The prefix token
|
||||
x (int): The new token to append
|
||||
|
||||
Returns:
|
||||
torch.Tensor: New tensor contains: xs + [x] with xs.dtype and xs.device
|
||||
|
||||
"""
|
||||
x = torch.tensor([x], dtype=xs.dtype, device=xs.device)
|
||||
return torch.cat((xs, x))
|
||||
|
||||
def score_full(
|
||||
self, hyp: Hypothesis, x: torch.Tensor
|
||||
) -> Tuple[Dict[str, torch.Tensor], Dict[str, Any]]:
|
||||
"""Score new hypothesis by `self.full_scorers`.
|
||||
|
||||
Args:
|
||||
hyp (Hypothesis): Hypothesis with prefix tokens to score
|
||||
x (torch.Tensor): Corresponding input feature
|
||||
|
||||
Returns:
|
||||
Tuple[Dict[str, torch.Tensor], Dict[str, Any]]: Tuple of
|
||||
score dict of `hyp` that has string keys of `self.full_scorers`
|
||||
and tensor score values of shape: `(self.n_vocab,)`,
|
||||
and state dict that has string keys
|
||||
and state values of `self.full_scorers`
|
||||
|
||||
"""
|
||||
scores = dict()
|
||||
states = dict()
|
||||
for k, d in self.full_scorers.items():
|
||||
scores[k], states[k] = d.score(hyp.yseq, hyp.states[k], x)
|
||||
return scores, states
|
||||
|
||||
def score_partial(
|
||||
self, hyp: Hypothesis, ids: torch.Tensor, x: torch.Tensor
|
||||
) -> Tuple[Dict[str, torch.Tensor], Dict[str, Any]]:
|
||||
"""Score new hypothesis by `self.part_scorers`.
|
||||
|
||||
Args:
|
||||
hyp (Hypothesis): Hypothesis with prefix tokens to score
|
||||
ids (torch.Tensor): 1D tensor of new partial tokens to score
|
||||
x (torch.Tensor): Corresponding input feature
|
||||
|
||||
Returns:
|
||||
Tuple[Dict[str, torch.Tensor], Dict[str, Any]]: Tuple of
|
||||
score dict of `hyp` that has string keys of `self.part_scorers`
|
||||
and tensor score values of shape: `(len(ids),)`,
|
||||
and state dict that has string keys
|
||||
and state values of `self.part_scorers`
|
||||
|
||||
"""
|
||||
scores = dict()
|
||||
states = dict()
|
||||
for k, d in self.part_scorers.items():
|
||||
scores[k], states[k] = d.score_partial(hyp.yseq, ids, hyp.states[k], x)
|
||||
return scores, states
|
||||
|
||||
def beam(
|
||||
self, weighted_scores: torch.Tensor, ids: torch.Tensor
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Compute topk full token ids and partial token ids.
|
||||
|
||||
Args:
|
||||
weighted_scores (torch.Tensor): The weighted sum scores for each tokens.
|
||||
Its shape is `(self.n_vocab,)`.
|
||||
ids (torch.Tensor): The partial token ids to compute topk
|
||||
|
||||
Returns:
|
||||
Tuple[torch.Tensor, torch.Tensor]:
|
||||
The topk full token ids and partial token ids.
|
||||
Their shapes are `(self.beam_size,)`
|
||||
|
||||
"""
|
||||
# no pre beam performed
|
||||
if weighted_scores.size(0) == ids.size(0):
|
||||
top_ids = weighted_scores.topk(self.beam_size)[1]
|
||||
return top_ids, top_ids
|
||||
|
||||
# mask pruned in pre-beam not to select in topk
|
||||
tmp = weighted_scores[ids]
|
||||
weighted_scores[:] = -float("inf")
|
||||
weighted_scores[ids] = tmp
|
||||
top_ids = weighted_scores.topk(self.beam_size)[1]
|
||||
local_ids = weighted_scores[ids].topk(self.beam_size)[1]
|
||||
return top_ids, local_ids
|
||||
|
||||
@staticmethod
|
||||
def merge_scores(
|
||||
prev_scores: Dict[str, float],
|
||||
next_full_scores: Dict[str, torch.Tensor],
|
||||
full_idx: int,
|
||||
next_part_scores: Dict[str, torch.Tensor],
|
||||
part_idx: int,
|
||||
) -> Dict[str, torch.Tensor]:
|
||||
"""Merge scores for new hypothesis.
|
||||
|
||||
Args:
|
||||
prev_scores (Dict[str, float]):
|
||||
The previous hypothesis scores by `self.scorers`
|
||||
next_full_scores (Dict[str, torch.Tensor]): scores by `self.full_scorers`
|
||||
full_idx (int): The next token id for `next_full_scores`
|
||||
next_part_scores (Dict[str, torch.Tensor]):
|
||||
scores of partial tokens by `self.part_scorers`
|
||||
part_idx (int): The new token id for `next_part_scores`
|
||||
|
||||
Returns:
|
||||
Dict[str, torch.Tensor]: The new score dict.
|
||||
Its keys are names of `self.full_scorers` and `self.part_scorers`.
|
||||
Its values are scalar tensors by the scorers.
|
||||
|
||||
"""
|
||||
new_scores = dict()
|
||||
for k, v in next_full_scores.items():
|
||||
new_scores[k] = prev_scores[k] + v[full_idx]
|
||||
for k, v in next_part_scores.items():
|
||||
new_scores[k] = prev_scores[k] + v[part_idx]
|
||||
return new_scores
|
||||
|
||||
def merge_states(self, states: Any, part_states: Any, part_idx: int) -> Any:
|
||||
"""Merge states for new hypothesis.
|
||||
|
||||
Args:
|
||||
states: states of `self.full_scorers`
|
||||
part_states: states of `self.part_scorers`
|
||||
part_idx (int): The new token id for `part_scores`
|
||||
|
||||
Returns:
|
||||
Dict[str, torch.Tensor]: The new score dict.
|
||||
Its keys are names of `self.full_scorers` and `self.part_scorers`.
|
||||
Its values are states of the scorers.
|
||||
|
||||
"""
|
||||
new_states = dict()
|
||||
for k, v in states.items():
|
||||
new_states[k] = v
|
||||
for k, d in self.part_scorers.items():
|
||||
new_states[k] = d.select_state(part_states[k], part_idx)
|
||||
return new_states
|
||||
|
||||
def search(
|
||||
self, running_hyps: List[Hypothesis], x: torch.Tensor, am_score: torch.Tensor
|
||||
) -> List[Hypothesis]:
|
||||
"""Search new tokens for running hypotheses and encoded speech x.
|
||||
|
||||
Args:
|
||||
running_hyps (List[Hypothesis]): Running hypotheses on beam
|
||||
x (torch.Tensor): Encoded speech feature (T, D)
|
||||
|
||||
Returns:
|
||||
List[Hypotheses]: Best sorted hypotheses
|
||||
|
||||
"""
|
||||
best_hyps = []
|
||||
part_ids = torch.arange(self.n_vocab, device=x.device) # no pre-beam
|
||||
for hyp in running_hyps:
|
||||
# scoring
|
||||
weighted_scores = torch.zeros(self.n_vocab, dtype=x.dtype, device=x.device)
|
||||
weighted_scores += am_score
|
||||
scores, states = self.score_full(hyp, x)
|
||||
for k in self.full_scorers:
|
||||
weighted_scores += self.weights[k] * scores[k]
|
||||
# partial scoring
|
||||
if self.do_pre_beam:
|
||||
pre_beam_scores = (
|
||||
weighted_scores
|
||||
if self.pre_beam_score_key == "full"
|
||||
else scores[self.pre_beam_score_key]
|
||||
)
|
||||
part_ids = torch.topk(pre_beam_scores, self.pre_beam_size)[1]
|
||||
part_scores, part_states = self.score_partial(hyp, part_ids, x)
|
||||
for k in self.part_scorers:
|
||||
weighted_scores[part_ids] += self.weights[k] * part_scores[k]
|
||||
# add previous hyp score
|
||||
weighted_scores += hyp.score
|
||||
|
||||
# update hyps
|
||||
for j, part_j in zip(*self.beam(weighted_scores, part_ids)):
|
||||
# will be (2 x beam at most)
|
||||
best_hyps.append(
|
||||
Hypothesis(
|
||||
score=weighted_scores[j],
|
||||
yseq=self.append_token(hyp.yseq, j),
|
||||
scores=self.merge_scores(hyp.scores, scores, j, part_scores, part_j),
|
||||
states=self.merge_states(states, part_states, part_j),
|
||||
)
|
||||
)
|
||||
|
||||
# sort and prune 2 x beam -> beam
|
||||
best_hyps = sorted(best_hyps, key=lambda x: x.score, reverse=True)[
|
||||
: min(len(best_hyps), self.beam_size)
|
||||
]
|
||||
return best_hyps
|
||||
|
||||
def forward(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
am_scores: torch.Tensor,
|
||||
maxlenratio: float = 0.0,
|
||||
minlenratio: float = 0.0,
|
||||
) -> List[Hypothesis]:
|
||||
"""Perform beam search.
|
||||
|
||||
Args:
|
||||
x (torch.Tensor): Encoded speech feature (T, D)
|
||||
maxlenratio (float): Input length ratio to obtain max output length.
|
||||
If maxlenratio=0.0 (default), it uses a end-detect function
|
||||
to automatically find maximum hypothesis lengths
|
||||
If maxlenratio<0.0, its absolute value is interpreted
|
||||
as a constant max output length.
|
||||
minlenratio (float): Input length ratio to obtain min output length.
|
||||
|
||||
Returns:
|
||||
list[Hypothesis]: N-best decoding results
|
||||
|
||||
"""
|
||||
# set length bounds
|
||||
maxlen = am_scores.shape[0]
|
||||
logging.info("decoder input length: " + str(x.shape[0]))
|
||||
logging.info("max output length: " + str(maxlen))
|
||||
|
||||
# main loop of prefix search
|
||||
running_hyps = self.init_hyp(x)
|
||||
ended_hyps = []
|
||||
for i in range(maxlen):
|
||||
logging.debug("position " + str(i))
|
||||
best = self.search(running_hyps, x, am_scores[i])
|
||||
# post process of one iteration
|
||||
running_hyps = self.post_process(i, maxlen, maxlenratio, best, ended_hyps)
|
||||
# end detection
|
||||
if maxlenratio == 0.0 and end_detect([h.asdict() for h in ended_hyps], i):
|
||||
logging.info(f"end detected at {i}")
|
||||
break
|
||||
if len(running_hyps) == 0:
|
||||
logging.info("no hypothesis. Finish decoding.")
|
||||
break
|
||||
else:
|
||||
logging.debug(f"remained hypotheses: {len(running_hyps)}")
|
||||
|
||||
nbest_hyps = sorted(ended_hyps, key=lambda x: x.score, reverse=True)
|
||||
# check the number of hypotheses reaching to eos
|
||||
if len(nbest_hyps) == 0:
|
||||
logging.warning(
|
||||
"there is no N-best results, perform recognition " "again with smaller minlenratio."
|
||||
)
|
||||
return (
|
||||
[]
|
||||
if minlenratio < 0.1
|
||||
else self.forward(x, maxlenratio, max(0.0, minlenratio - 0.1))
|
||||
)
|
||||
|
||||
# report the best result
|
||||
best = nbest_hyps[0]
|
||||
for k, v in best.scores.items():
|
||||
logging.info(f"{v:6.2f} * {self.weights[k]:3} = {v * self.weights[k]:6.2f} for {k}")
|
||||
logging.info(f"total log probability: {best.score:.2f}")
|
||||
logging.info(f"normalized log probability: {best.score / len(best.yseq):.2f}")
|
||||
logging.info(f"total number of ended hypotheses: {len(nbest_hyps)}")
|
||||
if self.token_list is not None:
|
||||
logging.info(
|
||||
"best hypo: " + "".join([self.token_list[x.item()] for x in best.yseq[1:-1]]) + "\n"
|
||||
)
|
||||
return nbest_hyps
|
||||
|
||||
def post_process(
|
||||
self,
|
||||
i: int,
|
||||
maxlen: int,
|
||||
maxlenratio: float,
|
||||
running_hyps: List[Hypothesis],
|
||||
ended_hyps: List[Hypothesis],
|
||||
) -> List[Hypothesis]:
|
||||
"""Perform post-processing of beam search iterations.
|
||||
|
||||
Args:
|
||||
i (int): The length of hypothesis tokens.
|
||||
maxlen (int): The maximum length of tokens in beam search.
|
||||
maxlenratio (int): The maximum length ratio in beam search.
|
||||
running_hyps (List[Hypothesis]): The running hypotheses in beam search.
|
||||
ended_hyps (List[Hypothesis]): The ended hypotheses in beam search.
|
||||
|
||||
Returns:
|
||||
List[Hypothesis]: The new running hypotheses.
|
||||
|
||||
"""
|
||||
logging.debug(f"the number of running hypotheses: {len(running_hyps)}")
|
||||
if self.token_list is not None:
|
||||
logging.debug(
|
||||
"best hypo: "
|
||||
+ "".join([self.token_list[x.item()] for x in running_hyps[0].yseq[1:]])
|
||||
)
|
||||
# add eos in the final loop to avoid that there are no ended hyps
|
||||
if i == maxlen - 1:
|
||||
logging.info("adding <eos> in the last position in the loop")
|
||||
running_hyps = [
|
||||
h._replace(yseq=self.append_token(h.yseq, self.eos)) for h in running_hyps
|
||||
]
|
||||
|
||||
# add ended hypotheses to a final list, and removed them from current hypotheses
|
||||
# (this will be a problem, number of hyps < beam)
|
||||
remained_hyps = []
|
||||
for hyp in running_hyps:
|
||||
if hyp.yseq[-1] == self.eos:
|
||||
# e.g., Word LM needs to add final <eos> score
|
||||
for k, d in chain(self.full_scorers.items(), self.part_scorers.items()):
|
||||
s = d.final_score(hyp.states[k])
|
||||
hyp.scores[k] += s
|
||||
hyp = hyp._replace(score=hyp.score + self.weights[k] * s)
|
||||
ended_hyps.append(hyp)
|
||||
else:
|
||||
remained_hyps.append(hyp)
|
||||
return remained_hyps
|
||||
@@ -0,0 +1,343 @@
|
||||
from contextlib import contextmanager
|
||||
from distutils.version import LooseVersion
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from funasr.frontends.wav_frontend import WavFrontendMel23
|
||||
from funasr.models.eend.encoder import EENDOLATransformerEncoder
|
||||
from funasr.models.eend.encoder_decoder_attractor import EncoderDecoderAttractor
|
||||
from funasr.models.eend.utils.losses import (
|
||||
standard_loss,
|
||||
cal_power_loss,
|
||||
fast_batch_pit_n_speaker_loss,
|
||||
)
|
||||
from funasr.models.eend.utils.power import create_powerlabel
|
||||
from funasr.models.eend.utils.power import generate_mapping_dict
|
||||
from funasr.train_utils.device_funcs import force_gatherable
|
||||
|
||||
if LooseVersion(torch.__version__) >= LooseVersion("1.6.0"):
|
||||
pass
|
||||
else:
|
||||
# Nothing to do if torch<1.6.0
|
||||
@contextmanager
|
||||
def autocast(enabled=True):
|
||||
"""Autocast.
|
||||
|
||||
Args:
|
||||
enabled: TODO.
|
||||
"""
|
||||
yield
|
||||
|
||||
|
||||
def pad_attractor(att, max_n_speakers):
|
||||
"""Pad attractor.
|
||||
|
||||
Args:
|
||||
att: TODO.
|
||||
max_n_speakers: TODO.
|
||||
"""
|
||||
C, D = att.shape
|
||||
if C < max_n_speakers:
|
||||
att = torch.cat(
|
||||
[att, torch.zeros(max_n_speakers - C, D).to(torch.float32).to(att.device)], dim=0
|
||||
)
|
||||
return att
|
||||
|
||||
|
||||
def pad_labels(ts, out_size):
|
||||
"""Pad labels.
|
||||
|
||||
Args:
|
||||
ts: TODO.
|
||||
out_size: Size/dimension parameter.
|
||||
"""
|
||||
for i, t in enumerate(ts):
|
||||
if t.shape[1] < out_size:
|
||||
ts[i] = F.pad(t, (0, out_size - t.shape[1], 0, 0), mode="constant", value=0.0)
|
||||
return ts
|
||||
|
||||
|
||||
def pad_results(ys, out_size):
|
||||
"""Pad results.
|
||||
|
||||
Args:
|
||||
ys: TODO.
|
||||
out_size: Size/dimension parameter.
|
||||
"""
|
||||
ys_padded = []
|
||||
for i, y in enumerate(ys):
|
||||
if y.shape[1] < out_size:
|
||||
ys_padded.append(
|
||||
torch.cat(
|
||||
[
|
||||
y,
|
||||
torch.zeros(y.shape[0], out_size - y.shape[1])
|
||||
.to(torch.float32)
|
||||
.to(y.device),
|
||||
],
|
||||
dim=1,
|
||||
)
|
||||
)
|
||||
else:
|
||||
ys_padded.append(y)
|
||||
return ys_padded
|
||||
|
||||
|
||||
class DiarEENDOLAModel(nn.Module):
|
||||
"""EEND-OLA diarization model"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
frontend: Optional[WavFrontendMel23],
|
||||
encoder: EENDOLATransformerEncoder,
|
||||
encoder_decoder_attractor: EncoderDecoderAttractor,
|
||||
n_units: int = 256,
|
||||
max_n_speaker: int = 8,
|
||||
attractor_loss_weight: float = 1.0,
|
||||
mapping_dict=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize DiarEENDOLAModel.
|
||||
|
||||
Args:
|
||||
frontend: Audio frontend for feature extraction.
|
||||
encoder: TODO.
|
||||
encoder_decoder_attractor: TODO.
|
||||
n_units: TODO.
|
||||
max_n_speaker: TODO.
|
||||
attractor_loss_weight: TODO.
|
||||
mapping_dict: TODO.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__()
|
||||
self.frontend = frontend
|
||||
self.enc = encoder
|
||||
self.encoder_decoder_attractor = encoder_decoder_attractor
|
||||
self.attractor_loss_weight = attractor_loss_weight
|
||||
self.max_n_speaker = max_n_speaker
|
||||
if mapping_dict is None:
|
||||
mapping_dict = generate_mapping_dict(max_speaker_num=self.max_n_speaker)
|
||||
self.mapping_dict = mapping_dict
|
||||
# PostNet
|
||||
self.postnet = nn.LSTM(self.max_n_speaker, n_units, 1, batch_first=True)
|
||||
self.output_layer = nn.Linear(n_units, mapping_dict["oov"] + 1)
|
||||
|
||||
def forward_encoder(self, xs, ilens):
|
||||
"""Forward encoder.
|
||||
|
||||
Args:
|
||||
xs: TODO.
|
||||
ilens: TODO.
|
||||
"""
|
||||
xs = nn.utils.rnn.pad_sequence(xs, batch_first=True, padding_value=-1)
|
||||
pad_shape = xs.shape
|
||||
xs_mask = [torch.ones(ilen).to(xs.device) for ilen in ilens]
|
||||
xs_mask = torch.nn.utils.rnn.pad_sequence(
|
||||
xs_mask, batch_first=True, padding_value=0
|
||||
).unsqueeze(-2)
|
||||
emb = self.enc(xs, xs_mask)
|
||||
emb = torch.split(emb.view(pad_shape[0], pad_shape[1], -1), 1, dim=0)
|
||||
emb = [e[0][:ilen] for e, ilen in zip(emb, ilens)]
|
||||
return emb
|
||||
|
||||
def forward_post_net(self, logits, ilens):
|
||||
"""Forward post net.
|
||||
|
||||
Args:
|
||||
logits: TODO.
|
||||
ilens: TODO.
|
||||
"""
|
||||
maxlen = torch.max(ilens).to(torch.int).item()
|
||||
logits = nn.utils.rnn.pad_sequence(logits, batch_first=True, padding_value=-1)
|
||||
logits = nn.utils.rnn.pack_padded_sequence(
|
||||
logits, ilens.cpu().to(torch.int64), batch_first=True, enforce_sorted=False
|
||||
)
|
||||
outputs, (_, _) = self.postnet(logits)
|
||||
outputs = nn.utils.rnn.pad_packed_sequence(
|
||||
outputs, batch_first=True, padding_value=-1, total_length=maxlen
|
||||
)[0]
|
||||
outputs = [output[: ilens[i].to(torch.int).item()] for i, output in enumerate(outputs)]
|
||||
outputs = [self.output_layer(output) for output in outputs]
|
||||
return outputs
|
||||
|
||||
def forward(
|
||||
self,
|
||||
speech: List[torch.Tensor],
|
||||
speaker_labels: List[torch.Tensor],
|
||||
orders: torch.Tensor,
|
||||
) -> Tuple[torch.Tensor, Dict[str, torch.Tensor], torch.Tensor]:
|
||||
|
||||
# Check that batch_size is unified
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
speech: Speech audio tensor, shape (batch, time).
|
||||
speaker_labels: TODO.
|
||||
orders: TODO.
|
||||
"""
|
||||
assert len(speech) == len(speaker_labels), (len(speech), len(speaker_labels))
|
||||
speech_lengths = torch.tensor([len(sph) for sph in speech]).to(torch.int64)
|
||||
speaker_labels_lengths = torch.tensor([spk.shape[-1] for spk in speaker_labels]).to(
|
||||
torch.int64
|
||||
)
|
||||
batch_size = len(speech)
|
||||
|
||||
# Encoder
|
||||
encoder_out = self.forward_encoder(speech, speech_lengths)
|
||||
|
||||
# Encoder-decoder attractor
|
||||
attractor_loss, attractors = self.encoder_decoder_attractor(
|
||||
[e[order] for e, order in zip(encoder_out, orders)], speaker_labels_lengths
|
||||
)
|
||||
speaker_logits = [
|
||||
torch.matmul(e, att.permute(1, 0)) for e, att in zip(encoder_out, attractors)
|
||||
]
|
||||
|
||||
# pit loss
|
||||
pit_speaker_labels = fast_batch_pit_n_speaker_loss(speaker_logits, speaker_labels)
|
||||
pit_loss = standard_loss(speaker_logits, pit_speaker_labels)
|
||||
|
||||
# pse loss
|
||||
with torch.no_grad():
|
||||
power_ts = [
|
||||
create_powerlabel(label.cpu().numpy(), self.mapping_dict, self.max_n_speaker).to(
|
||||
encoder_out[0].device, non_blocking=True
|
||||
)
|
||||
for label in pit_speaker_labels
|
||||
]
|
||||
pad_attractors = [pad_attractor(att, self.max_n_speaker) for att in attractors]
|
||||
pse_speaker_logits = [
|
||||
torch.matmul(e, pad_att.permute(1, 0))
|
||||
for e, pad_att in zip(encoder_out, pad_attractors)
|
||||
]
|
||||
pse_speaker_logits = self.forward_post_net(pse_speaker_logits, speech_lengths)
|
||||
pse_loss = cal_power_loss(pse_speaker_logits, power_ts)
|
||||
|
||||
loss = pse_loss + pit_loss + self.attractor_loss_weight * attractor_loss
|
||||
|
||||
stats = dict()
|
||||
stats["pse_loss"] = pse_loss.detach()
|
||||
stats["pit_loss"] = pit_loss.detach()
|
||||
stats["attractor_loss"] = attractor_loss.detach()
|
||||
stats["batch_size"] = batch_size
|
||||
|
||||
# Collect total loss stats
|
||||
stats["loss"] = torch.clone(loss.detach())
|
||||
|
||||
# force_gatherable: to-device and to-tensor if scalar for DataParallel
|
||||
loss, stats, weight = force_gatherable((loss, stats, batch_size), loss.device)
|
||||
return loss, stats, weight
|
||||
|
||||
def estimate_sequential(
|
||||
self,
|
||||
speech: torch.Tensor,
|
||||
n_speakers: int = None,
|
||||
shuffle: bool = True,
|
||||
threshold: float = 0.5,
|
||||
**kwargs,
|
||||
):
|
||||
"""Estimate sequential.
|
||||
|
||||
Args:
|
||||
speech: Speech audio tensor, shape (batch, time).
|
||||
n_speakers: TODO.
|
||||
shuffle: TODO.
|
||||
threshold: TODO.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
speech_lengths = torch.tensor([len(sph) for sph in speech]).to(torch.int64)
|
||||
emb = self.forward_encoder(speech, speech_lengths)
|
||||
if shuffle:
|
||||
orders = [np.arange(e.shape[0]) for e in emb]
|
||||
for order in orders:
|
||||
np.random.shuffle(order)
|
||||
attractors, probs = self.encoder_decoder_attractor.estimate(
|
||||
[
|
||||
e[torch.from_numpy(order).to(torch.long).to(speech[0].device)]
|
||||
for e, order in zip(emb, orders)
|
||||
]
|
||||
)
|
||||
else:
|
||||
attractors, probs = self.encoder_decoder_attractor.estimate(emb)
|
||||
attractors_active = []
|
||||
for p, att, e in zip(probs, attractors, emb):
|
||||
if n_speakers and n_speakers >= 0:
|
||||
att = att[:n_speakers,]
|
||||
attractors_active.append(att)
|
||||
elif threshold is not None:
|
||||
silence = torch.nonzero(p < threshold)[0]
|
||||
n_spk = silence[0] if silence.size else None
|
||||
att = att[:n_spk,]
|
||||
attractors_active.append(att)
|
||||
else:
|
||||
NotImplementedError("n_speakers or threshold has to be given.")
|
||||
raw_n_speakers = [att.shape[0] for att in attractors_active]
|
||||
attractors = [
|
||||
(
|
||||
pad_attractor(att, self.max_n_speaker)
|
||||
if att.shape[0] <= self.max_n_speaker
|
||||
else att[: self.max_n_speaker]
|
||||
)
|
||||
for att in attractors_active
|
||||
]
|
||||
ys = [torch.matmul(e, att.permute(1, 0)) for e, att in zip(emb, attractors)]
|
||||
logits = self.forward_post_net(ys, speech_lengths)
|
||||
ys = [
|
||||
self.recover_y_from_powerlabel(logit, raw_n_speaker)
|
||||
for logit, raw_n_speaker in zip(logits, raw_n_speakers)
|
||||
]
|
||||
|
||||
return ys, emb, attractors, raw_n_speakers
|
||||
|
||||
def recover_y_from_powerlabel(self, logit, n_speaker):
|
||||
"""Recover y from powerlabel.
|
||||
|
||||
Args:
|
||||
logit: TODO.
|
||||
n_speaker: TODO.
|
||||
"""
|
||||
pred = torch.argmax(torch.softmax(logit, dim=-1), dim=-1)
|
||||
oov_index = torch.where(pred == self.mapping_dict["oov"])[0]
|
||||
for i in oov_index:
|
||||
if i > 0:
|
||||
pred[i] = pred[i - 1]
|
||||
else:
|
||||
pred[i] = 0
|
||||
pred = [self.inv_mapping_func(i) for i in pred]
|
||||
decisions = [bin(num)[2:].zfill(self.max_n_speaker)[::-1] for num in pred]
|
||||
decisions = (
|
||||
torch.from_numpy(
|
||||
np.stack([np.array([int(i) for i in dec]) for dec in decisions], axis=0)
|
||||
)
|
||||
.to(logit.device)
|
||||
.to(torch.float32)
|
||||
)
|
||||
decisions = decisions[:, :n_speaker]
|
||||
return decisions
|
||||
|
||||
def inv_mapping_func(self, label):
|
||||
|
||||
"""Inv mapping func.
|
||||
|
||||
Args:
|
||||
label: TODO.
|
||||
"""
|
||||
if not isinstance(label, int):
|
||||
label = int(label)
|
||||
if label in self.mapping_dict["label2dec"].keys():
|
||||
num = self.mapping_dict["label2dec"][label]
|
||||
else:
|
||||
num = -1
|
||||
return num
|
||||
|
||||
def collect_feats(self, **batch: torch.Tensor) -> Dict[str, torch.Tensor]:
|
||||
"""Collect feats.
|
||||
|
||||
Args:
|
||||
**batch: Additional keyword arguments.
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,86 @@
|
||||
import logging
|
||||
|
||||
import kaldiio
|
||||
import numpy as np
|
||||
import torch
|
||||
from torch.utils.data import DataLoader
|
||||
from torch.utils.data import Dataset
|
||||
|
||||
|
||||
def custom_collate(batch):
|
||||
"""Custom collate.
|
||||
|
||||
Args:
|
||||
batch: TODO.
|
||||
"""
|
||||
keys, speech, speaker_labels, orders = zip(*batch)
|
||||
speech = [torch.from_numpy(np.copy(sph)).to(torch.float32) for sph in speech]
|
||||
speaker_labels = [torch.from_numpy(np.copy(spk)).to(torch.float32) for spk in speaker_labels]
|
||||
orders = [torch.from_numpy(np.copy(o)).to(torch.int64) for o in orders]
|
||||
batch = dict(speech=speech, speaker_labels=speaker_labels, orders=orders)
|
||||
|
||||
return keys, batch
|
||||
|
||||
|
||||
class EENDOLADataset(Dataset):
|
||||
def __init__(
|
||||
self,
|
||||
data_file,
|
||||
):
|
||||
"""Initialize EENDOLADataset.
|
||||
|
||||
Args:
|
||||
data_file: TODO.
|
||||
"""
|
||||
self.data_file = data_file
|
||||
with open(data_file) as f:
|
||||
lines = f.readlines()
|
||||
self.samples = [line.strip().split() for line in lines]
|
||||
logging.info("total samples: {}".format(len(self.samples)))
|
||||
|
||||
def __len__(self):
|
||||
"""Internal: len ."""
|
||||
return len(self.samples)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
"""Internal: getitem .
|
||||
|
||||
Args:
|
||||
idx: TODO.
|
||||
"""
|
||||
key, speech_path, speaker_label_path = self.samples[idx]
|
||||
speech = kaldiio.load_mat(speech_path)
|
||||
speaker_label = kaldiio.load_mat(speaker_label_path).reshape(speech.shape[0], -1)
|
||||
|
||||
order = np.arange(speech.shape[0])
|
||||
np.random.shuffle(order)
|
||||
|
||||
return key, speech, speaker_label, order
|
||||
|
||||
|
||||
class EENDOLADataLoader:
|
||||
def __init__(self, data_file, batch_size, shuffle=True, num_workers=8):
|
||||
"""Initialize EENDOLADataLoader.
|
||||
|
||||
Args:
|
||||
data_file: TODO.
|
||||
batch_size: Number of samples per batch.
|
||||
shuffle: TODO.
|
||||
num_workers: TODO.
|
||||
"""
|
||||
dataset = EENDOLADataset(data_file)
|
||||
self.data_loader = DataLoader(
|
||||
dataset,
|
||||
batch_size=batch_size,
|
||||
collate_fn=custom_collate,
|
||||
shuffle=shuffle,
|
||||
num_workers=num_workers,
|
||||
)
|
||||
|
||||
def build_iter(self, epoch):
|
||||
"""Build iter.
|
||||
|
||||
Args:
|
||||
epoch: TODO.
|
||||
"""
|
||||
return self.data_loader
|
||||
@@ -0,0 +1,179 @@
|
||||
import math
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from torch import nn
|
||||
|
||||
|
||||
class MultiHeadSelfAttention(nn.Module):
|
||||
def __init__(self, n_units, h=8, dropout_rate=0.1):
|
||||
"""Initialize MultiHeadSelfAttention.
|
||||
|
||||
Args:
|
||||
n_units: TODO.
|
||||
h: TODO.
|
||||
dropout_rate: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
self.linearQ = nn.Linear(n_units, n_units)
|
||||
self.linearK = nn.Linear(n_units, n_units)
|
||||
self.linearV = nn.Linear(n_units, n_units)
|
||||
self.linearO = nn.Linear(n_units, n_units)
|
||||
self.d_k = n_units // h
|
||||
self.h = h
|
||||
self.dropout = nn.Dropout(dropout_rate)
|
||||
|
||||
def __call__(self, x, batch_size, x_mask):
|
||||
"""Internal: call .
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
batch_size: Number of samples per batch.
|
||||
x_mask: TODO.
|
||||
"""
|
||||
q = self.linearQ(x).view(batch_size, -1, self.h, self.d_k)
|
||||
k = self.linearK(x).view(batch_size, -1, self.h, self.d_k)
|
||||
v = self.linearV(x).view(batch_size, -1, self.h, self.d_k)
|
||||
scores = torch.matmul(q.permute(0, 2, 1, 3), k.permute(0, 2, 3, 1)) / math.sqrt(self.d_k)
|
||||
if x_mask is not None:
|
||||
x_mask = x_mask.unsqueeze(1)
|
||||
scores = scores.masked_fill(x_mask == 0, -1e9)
|
||||
self.att = F.softmax(scores, dim=3)
|
||||
p_att = self.dropout(self.att)
|
||||
x = torch.matmul(p_att, v.permute(0, 2, 1, 3))
|
||||
x = x.permute(0, 2, 1, 3).contiguous().view(-1, self.h * self.d_k)
|
||||
return self.linearO(x)
|
||||
|
||||
|
||||
class PositionwiseFeedForward(nn.Module):
|
||||
def __init__(self, n_units, d_units, dropout_rate):
|
||||
"""Initialize PositionwiseFeedForward.
|
||||
|
||||
Args:
|
||||
n_units: TODO.
|
||||
d_units: TODO.
|
||||
dropout_rate: TODO.
|
||||
"""
|
||||
super(PositionwiseFeedForward, self).__init__()
|
||||
self.linear1 = nn.Linear(n_units, d_units)
|
||||
self.linear2 = nn.Linear(d_units, n_units)
|
||||
self.dropout = nn.Dropout(dropout_rate)
|
||||
|
||||
def __call__(self, x):
|
||||
"""Internal: call .
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
return self.linear2(self.dropout(F.relu(self.linear1(x))))
|
||||
|
||||
|
||||
class PositionalEncoding(torch.nn.Module):
|
||||
def __init__(self, d_model, dropout_rate, max_len=5000, reverse=False):
|
||||
"""Initialize PositionalEncoding.
|
||||
|
||||
Args:
|
||||
d_model: D Model instance.
|
||||
dropout_rate: TODO.
|
||||
max_len: TODO.
|
||||
reverse: TODO.
|
||||
"""
|
||||
super(PositionalEncoding, self).__init__()
|
||||
self.d_model = d_model
|
||||
self.reverse = reverse
|
||||
self.xscale = math.sqrt(self.d_model)
|
||||
self.dropout = torch.nn.Dropout(p=dropout_rate)
|
||||
self.pe = None
|
||||
self.extend_pe(torch.tensor(0.0).expand(1, max_len))
|
||||
|
||||
def extend_pe(self, x):
|
||||
"""Extend pe.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
if self.pe is not None:
|
||||
if self.pe.size(1) >= x.size(1):
|
||||
if self.pe.dtype != x.dtype or self.pe.device != x.device:
|
||||
self.pe = self.pe.to(dtype=x.dtype, device=x.device)
|
||||
return
|
||||
pe = torch.zeros(x.size(1), self.d_model)
|
||||
if self.reverse:
|
||||
position = torch.arange(x.size(1) - 1, -1, -1.0, dtype=torch.float32).unsqueeze(1)
|
||||
else:
|
||||
position = torch.arange(0, x.size(1), dtype=torch.float32).unsqueeze(1)
|
||||
div_term = torch.exp(
|
||||
torch.arange(0, self.d_model, 2, dtype=torch.float32)
|
||||
* -(math.log(10000.0) / self.d_model)
|
||||
)
|
||||
pe[:, 0::2] = torch.sin(position * div_term)
|
||||
pe[:, 1::2] = torch.cos(position * div_term)
|
||||
pe = pe.unsqueeze(0)
|
||||
self.pe = pe.to(device=x.device, dtype=x.dtype)
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
self.extend_pe(x)
|
||||
x = x * self.xscale + self.pe[:, : x.size(1)]
|
||||
return self.dropout(x)
|
||||
|
||||
|
||||
class EENDOLATransformerEncoder(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
idim: int,
|
||||
n_layers: int,
|
||||
n_units: int,
|
||||
e_units: int = 2048,
|
||||
h: int = 4,
|
||||
dropout_rate: float = 0.1,
|
||||
use_pos_emb: bool = False,
|
||||
):
|
||||
"""Initialize EENDOLATransformerEncoder.
|
||||
|
||||
Args:
|
||||
idim: TODO.
|
||||
n_layers: TODO.
|
||||
n_units: TODO.
|
||||
e_units: TODO.
|
||||
h: TODO.
|
||||
dropout_rate: TODO.
|
||||
use_pos_emb: TODO.
|
||||
"""
|
||||
super(EENDOLATransformerEncoder, self).__init__()
|
||||
self.linear_in = nn.Linear(idim, n_units)
|
||||
self.lnorm_in = nn.LayerNorm(n_units)
|
||||
self.n_layers = n_layers
|
||||
self.dropout = nn.Dropout(dropout_rate)
|
||||
for i in range(n_layers):
|
||||
setattr(self, "{}{:d}".format("lnorm1_", i), nn.LayerNorm(n_units))
|
||||
setattr(self, "{}{:d}".format("self_att_", i), MultiHeadSelfAttention(n_units, h))
|
||||
setattr(self, "{}{:d}".format("lnorm2_", i), nn.LayerNorm(n_units))
|
||||
setattr(
|
||||
self,
|
||||
"{}{:d}".format("ff_", i),
|
||||
PositionwiseFeedForward(n_units, e_units, dropout_rate),
|
||||
)
|
||||
self.lnorm_out = nn.LayerNorm(n_units)
|
||||
|
||||
def __call__(self, x, x_mask=None):
|
||||
"""Internal: call .
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
x_mask: TODO.
|
||||
"""
|
||||
BT_size = x.shape[0] * x.shape[1]
|
||||
e = self.linear_in(x.reshape(BT_size, -1))
|
||||
for i in range(self.n_layers):
|
||||
e = getattr(self, "{}{:d}".format("lnorm1_", i))(e)
|
||||
s = getattr(self, "{}{:d}".format("self_att_", i))(e, x.shape[0], x_mask)
|
||||
e = e + self.dropout(s)
|
||||
e = getattr(self, "{}{:d}".format("lnorm2_", i))(e)
|
||||
s = getattr(self, "{}{:d}".format("ff_", i))(e)
|
||||
e = e + self.dropout(s)
|
||||
return self.lnorm_out(e)
|
||||
@@ -0,0 +1,89 @@
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from torch import nn
|
||||
|
||||
|
||||
class EncoderDecoderAttractor(nn.Module):
|
||||
|
||||
def __init__(self, n_units, encoder_dropout=0.1, decoder_dropout=0.1):
|
||||
"""Initialize EncoderDecoderAttractor.
|
||||
|
||||
Args:
|
||||
n_units: TODO.
|
||||
encoder_dropout: TODO.
|
||||
decoder_dropout: TODO.
|
||||
"""
|
||||
super(EncoderDecoderAttractor, self).__init__()
|
||||
self.enc0_dropout = nn.Dropout(encoder_dropout)
|
||||
self.encoder = nn.LSTM(n_units, n_units, 1, batch_first=True, dropout=encoder_dropout)
|
||||
self.dec0_dropout = nn.Dropout(decoder_dropout)
|
||||
self.decoder = nn.LSTM(n_units, n_units, 1, batch_first=True, dropout=decoder_dropout)
|
||||
self.counter = nn.Linear(n_units, 1)
|
||||
self.n_units = n_units
|
||||
|
||||
def forward_core(self, xs, zeros):
|
||||
"""Forward core.
|
||||
|
||||
Args:
|
||||
xs: TODO.
|
||||
zeros: TODO.
|
||||
"""
|
||||
ilens = torch.from_numpy(np.array([x.shape[0] for x in xs])).to(torch.int64)
|
||||
xs = [self.enc0_dropout(x) for x in xs]
|
||||
xs = nn.utils.rnn.pad_sequence(xs, batch_first=True, padding_value=-1)
|
||||
xs = nn.utils.rnn.pack_padded_sequence(xs, ilens, batch_first=True, enforce_sorted=False)
|
||||
_, (hx, cx) = self.encoder(xs)
|
||||
zlens = torch.from_numpy(np.array([z.shape[0] for z in zeros])).to(torch.int64)
|
||||
max_zlen = torch.max(zlens).to(torch.int).item()
|
||||
zeros = [self.enc0_dropout(z) for z in zeros]
|
||||
zeros = nn.utils.rnn.pad_sequence(zeros, batch_first=True, padding_value=-1)
|
||||
zeros = nn.utils.rnn.pack_padded_sequence(
|
||||
zeros, zlens, batch_first=True, enforce_sorted=False
|
||||
)
|
||||
attractors, (_, _) = self.decoder(zeros, (hx, cx))
|
||||
attractors = nn.utils.rnn.pad_packed_sequence(
|
||||
attractors, batch_first=True, padding_value=-1, total_length=max_zlen
|
||||
)[0]
|
||||
attractors = [att[: zlens[i].to(torch.int).item()] for i, att in enumerate(attractors)]
|
||||
return attractors
|
||||
|
||||
def forward(self, xs, n_speakers):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
xs: TODO.
|
||||
n_speakers: TODO.
|
||||
"""
|
||||
zeros = [
|
||||
torch.zeros(n_spk + 1, self.n_units).to(torch.float32).to(xs[0].device)
|
||||
for n_spk in n_speakers
|
||||
]
|
||||
attractors = self.forward_core(xs, zeros)
|
||||
labels = torch.cat(
|
||||
[torch.from_numpy(np.array([[1] * n_spk + [0]], np.float32)) for n_spk in n_speakers],
|
||||
dim=1,
|
||||
)
|
||||
labels = labels.to(xs[0].device)
|
||||
logit = torch.cat(
|
||||
[self.counter(att).view(-1, n_spk + 1) for att, n_spk in zip(attractors, n_speakers)],
|
||||
dim=1,
|
||||
)
|
||||
loss = F.binary_cross_entropy(torch.sigmoid(logit), labels)
|
||||
|
||||
attractors = [att[slice(0, att.shape[0] - 1)] for att in attractors]
|
||||
return loss, attractors
|
||||
|
||||
def estimate(self, xs, max_n_speakers=15):
|
||||
"""Estimate.
|
||||
|
||||
Args:
|
||||
xs: TODO.
|
||||
max_n_speakers: TODO.
|
||||
"""
|
||||
zeros = [
|
||||
torch.zeros(max_n_speakers, self.n_units).to(torch.float32).to(xs[0].device) for _ in xs
|
||||
]
|
||||
attractors = self.forward_core(xs, zeros)
|
||||
probs = [torch.sigmoid(torch.flatten(self.counter(att))) for att in attractors]
|
||||
return attractors, probs
|
||||
@@ -0,0 +1,276 @@
|
||||
# Copyright 2019 Hitachi, Ltd. (author: Yusuke Fujita)
|
||||
# Licensed under the MIT license.
|
||||
#
|
||||
# This module is for computing audio features
|
||||
|
||||
import numpy as np
|
||||
import librosa
|
||||
|
||||
|
||||
def get_input_dim(
|
||||
frame_size,
|
||||
context_size,
|
||||
transform_type,
|
||||
):
|
||||
"""Get input dim.
|
||||
|
||||
Args:
|
||||
frame_size: Size/dimension parameter.
|
||||
context_size: Size/dimension parameter.
|
||||
transform_type: TODO.
|
||||
"""
|
||||
if transform_type.startswith("logmel23"):
|
||||
frame_size = 23
|
||||
elif transform_type.startswith("logmel"):
|
||||
frame_size = 40
|
||||
else:
|
||||
fft_size = 1 << (frame_size - 1).bit_length()
|
||||
frame_size = int(fft_size / 2) + 1
|
||||
input_dim = (2 * context_size + 1) * frame_size
|
||||
return input_dim
|
||||
|
||||
|
||||
def transform(Y, transform_type=None, dtype=np.float32):
|
||||
"""Transform STFT feature
|
||||
|
||||
Args:
|
||||
Y: STFT
|
||||
(n_frames, n_bins)-shaped np.complex array
|
||||
transform_type:
|
||||
None, "log"
|
||||
dtype: output data type
|
||||
np.float32 is expected
|
||||
Returns:
|
||||
Y (numpy.array): transformed feature
|
||||
"""
|
||||
Y = np.abs(Y)
|
||||
if not transform_type:
|
||||
pass
|
||||
elif transform_type == "log":
|
||||
Y = np.log(np.maximum(Y, 1e-10))
|
||||
elif transform_type == "logmel":
|
||||
n_fft = 2 * (Y.shape[1] - 1)
|
||||
sr = 16000
|
||||
n_mels = 40
|
||||
mel_basis = librosa.filters.mel(sr, n_fft, n_mels)
|
||||
Y = np.dot(Y**2, mel_basis.T)
|
||||
Y = np.log10(np.maximum(Y, 1e-10))
|
||||
elif transform_type == "logmel23":
|
||||
n_fft = 2 * (Y.shape[1] - 1)
|
||||
sr = 8000
|
||||
n_mels = 23
|
||||
mel_basis = librosa.filters.mel(sr, n_fft, n_mels)
|
||||
Y = np.dot(Y**2, mel_basis.T)
|
||||
Y = np.log10(np.maximum(Y, 1e-10))
|
||||
elif transform_type == "logmel23_mn":
|
||||
n_fft = 2 * (Y.shape[1] - 1)
|
||||
sr = 8000
|
||||
n_mels = 23
|
||||
mel_basis = librosa.filters.mel(sr, n_fft, n_mels)
|
||||
Y = np.dot(Y**2, mel_basis.T)
|
||||
Y = np.log10(np.maximum(Y, 1e-10))
|
||||
mean = np.mean(Y, axis=0)
|
||||
Y = Y - mean
|
||||
elif transform_type == "logmel23_swn":
|
||||
n_fft = 2 * (Y.shape[1] - 1)
|
||||
sr = 8000
|
||||
n_mels = 23
|
||||
mel_basis = librosa.filters.mel(sr, n_fft, n_mels)
|
||||
Y = np.dot(Y**2, mel_basis.T)
|
||||
Y = np.log10(np.maximum(Y, 1e-10))
|
||||
# b = np.ones(300)/300
|
||||
# mean = scipy.signal.convolve2d(Y, b[:, None], mode='same')
|
||||
|
||||
# simple 2-means based threshoding for mean calculation
|
||||
powers = np.sum(Y, axis=1)
|
||||
th = (np.max(powers) + np.min(powers)) / 2.0
|
||||
for i in range(10):
|
||||
th = (np.mean(powers[powers >= th]) + np.mean(powers[powers < th])) / 2
|
||||
mean = np.mean(Y[powers > th, :], axis=0)
|
||||
Y = Y - mean
|
||||
elif transform_type == "logmel23_mvn":
|
||||
n_fft = 2 * (Y.shape[1] - 1)
|
||||
sr = 8000
|
||||
n_mels = 23
|
||||
mel_basis = librosa.filters.mel(sr, n_fft, n_mels)
|
||||
Y = np.dot(Y**2, mel_basis.T)
|
||||
Y = np.log10(np.maximum(Y, 1e-10))
|
||||
mean = np.mean(Y, axis=0)
|
||||
Y = Y - mean
|
||||
std = np.maximum(np.std(Y, axis=0), 1e-10)
|
||||
Y = Y / std
|
||||
else:
|
||||
raise ValueError("Unknown transform_type: %s" % transform_type)
|
||||
return Y.astype(dtype)
|
||||
|
||||
|
||||
def subsample(Y, T, subsampling=1):
|
||||
"""Frame subsampling"""
|
||||
Y_ss = Y[::subsampling]
|
||||
T_ss = T[::subsampling]
|
||||
return Y_ss, T_ss
|
||||
|
||||
|
||||
def splice(Y, context_size=0):
|
||||
"""Frame splicing
|
||||
|
||||
Args:
|
||||
Y: feature
|
||||
(n_frames, n_featdim)-shaped numpy array
|
||||
context_size:
|
||||
number of frames concatenated on left-side
|
||||
if context_size = 5, 11 frames are concatenated.
|
||||
|
||||
Returns:
|
||||
Y_spliced: spliced feature
|
||||
(n_frames, n_featdim * (2 * context_size + 1))-shaped
|
||||
"""
|
||||
Y_pad = np.pad(Y, [(context_size, context_size), (0, 0)], "constant")
|
||||
Y_spliced = np.lib.stride_tricks.as_strided(
|
||||
np.ascontiguousarray(Y_pad),
|
||||
(Y.shape[0], Y.shape[1] * (2 * context_size + 1)),
|
||||
(Y.itemsize * Y.shape[1], Y.itemsize),
|
||||
writeable=False,
|
||||
)
|
||||
return Y_spliced
|
||||
|
||||
|
||||
def stft(data, frame_size=1024, frame_shift=256):
|
||||
"""Compute STFT features
|
||||
|
||||
Args:
|
||||
data: audio signal
|
||||
(n_samples,)-shaped np.float32 array
|
||||
frame_size: number of samples in a frame (must be a power of two)
|
||||
frame_shift: number of samples between frames
|
||||
|
||||
Returns:
|
||||
stft: STFT frames
|
||||
(n_frames, n_bins)-shaped np.complex64 array
|
||||
"""
|
||||
# round up to nearest power of 2
|
||||
fft_size = 1 << (frame_size - 1).bit_length()
|
||||
# HACK: The last frame is ommited
|
||||
# as librosa.stft produces such an excessive frame
|
||||
if len(data) % frame_shift == 0:
|
||||
return librosa.stft(data, n_fft=fft_size, win_length=frame_size, hop_length=frame_shift).T[
|
||||
:-1
|
||||
]
|
||||
else:
|
||||
return librosa.stft(data, n_fft=fft_size, win_length=frame_size, hop_length=frame_shift).T
|
||||
|
||||
|
||||
def _count_frames(data_len, size, shift):
|
||||
# HACK: Assuming librosa.stft(..., center=True)
|
||||
"""Internal: count frames.
|
||||
|
||||
Args:
|
||||
data_len: TODO.
|
||||
size: TODO.
|
||||
shift: TODO.
|
||||
"""
|
||||
n_frames = 1 + int(data_len / shift)
|
||||
if data_len % shift == 0:
|
||||
n_frames = n_frames - 1
|
||||
return n_frames
|
||||
|
||||
|
||||
def get_frame_labels(
|
||||
kaldi_obj, rec, start=0, end=None, frame_size=1024, frame_shift=256, n_speakers=None
|
||||
):
|
||||
"""Get frame-aligned labels of given recording
|
||||
Args:
|
||||
kaldi_obj (KaldiData)
|
||||
rec (str): recording id
|
||||
start (int): start frame index
|
||||
end (int): end frame index
|
||||
None means the last frame of recording
|
||||
frame_size (int): number of frames in a frame
|
||||
frame_shift (int): number of shift samples
|
||||
n_speakers (int): number of speakers
|
||||
if None, the value is given from data
|
||||
Returns:
|
||||
T: label
|
||||
(n_frames, n_speakers)-shaped np.int32 array
|
||||
"""
|
||||
filtered_segments = kaldi_obj.segments[kaldi_obj.segments["rec"] == rec]
|
||||
speakers = np.unique([kaldi_obj.utt2spk[seg["utt"]] for seg in filtered_segments]).tolist()
|
||||
if n_speakers is None:
|
||||
n_speakers = len(speakers)
|
||||
es = end * frame_shift if end is not None else None
|
||||
data, rate = kaldi_obj.load_wav(rec, start * frame_shift, es)
|
||||
n_frames = _count_frames(len(data), frame_size, frame_shift)
|
||||
T = np.zeros((n_frames, n_speakers), dtype=np.int32)
|
||||
if end is None:
|
||||
end = n_frames
|
||||
|
||||
for seg in filtered_segments:
|
||||
speaker_index = speakers.index(kaldi_obj.utt2spk[seg["utt"]])
|
||||
start_frame = np.rint(seg["st"] * rate / frame_shift).astype(int)
|
||||
end_frame = np.rint(seg["et"] * rate / frame_shift).astype(int)
|
||||
rel_start = rel_end = None
|
||||
if start <= start_frame and start_frame < end:
|
||||
rel_start = start_frame - start
|
||||
if start < end_frame and end_frame <= end:
|
||||
rel_end = end_frame - start
|
||||
if rel_start is not None or rel_end is not None:
|
||||
T[rel_start:rel_end, speaker_index] = 1
|
||||
return T
|
||||
|
||||
|
||||
def get_labeledSTFT(
|
||||
kaldi_obj, rec, start, end, frame_size, frame_shift, n_speakers=None, use_speaker_id=False
|
||||
):
|
||||
"""Extracts STFT and corresponding labels
|
||||
|
||||
Extracts STFT and corresponding diarization labels for
|
||||
given recording id and start/end times
|
||||
|
||||
Args:
|
||||
kaldi_obj (KaldiData)
|
||||
rec (str): recording id
|
||||
start (int): start frame index
|
||||
end (int): end frame index
|
||||
frame_size (int): number of samples in a frame
|
||||
frame_shift (int): number of shift samples
|
||||
n_speakers (int): number of speakers
|
||||
if None, the value is given from data
|
||||
Returns:
|
||||
Y: STFT
|
||||
(n_frames, n_bins)-shaped np.complex64 array,
|
||||
T: label
|
||||
(n_frmaes, n_speakers)-shaped np.int32 array.
|
||||
"""
|
||||
data, rate = kaldi_obj.load_wav(rec, start * frame_shift, end * frame_shift)
|
||||
Y = stft(data, frame_size, frame_shift)
|
||||
filtered_segments = kaldi_obj.segments[rec]
|
||||
# filtered_segments = kaldi_obj.segments[kaldi_obj.segments['rec'] == rec]
|
||||
speakers = np.unique([kaldi_obj.utt2spk[seg["utt"]] for seg in filtered_segments]).tolist()
|
||||
if n_speakers is None:
|
||||
n_speakers = len(speakers)
|
||||
T = np.zeros((Y.shape[0], n_speakers), dtype=np.int32)
|
||||
|
||||
if use_speaker_id:
|
||||
all_speakers = sorted(kaldi_obj.spk2utt.keys())
|
||||
S = np.zeros((Y.shape[0], len(all_speakers)), dtype=np.int32)
|
||||
|
||||
for seg in filtered_segments:
|
||||
speaker_index = speakers.index(kaldi_obj.utt2spk[seg["utt"]])
|
||||
if use_speaker_id:
|
||||
all_speaker_index = all_speakers.index(kaldi_obj.utt2spk[seg["utt"]])
|
||||
start_frame = np.rint(seg["st"] * rate / frame_shift).astype(int)
|
||||
end_frame = np.rint(seg["et"] * rate / frame_shift).astype(int)
|
||||
rel_start = rel_end = None
|
||||
if start <= start_frame and start_frame < end:
|
||||
rel_start = start_frame - start
|
||||
if start < end_frame and end_frame <= end:
|
||||
rel_end = end_frame - start
|
||||
if rel_start is not None or rel_end is not None:
|
||||
T[rel_start:rel_end, speaker_index] = 1
|
||||
if use_speaker_id:
|
||||
S[rel_start:rel_end, all_speaker_index] = 1
|
||||
|
||||
if use_speaker_id:
|
||||
return Y, T, S
|
||||
else:
|
||||
return Y, T
|
||||
@@ -0,0 +1,174 @@
|
||||
# Copyright 2019 Hitachi, Ltd. (author: Yusuke Fujita)
|
||||
# Licensed under the MIT license.
|
||||
#
|
||||
# This library provides utilities for kaldi-style data directory.
|
||||
|
||||
|
||||
from __future__ import print_function
|
||||
import os
|
||||
import sys
|
||||
import numpy as np
|
||||
import subprocess
|
||||
import librosa as sf
|
||||
import io
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
def load_segments(segments_file):
|
||||
"""load segments file as array"""
|
||||
if not os.path.exists(segments_file):
|
||||
return None
|
||||
return np.loadtxt(
|
||||
segments_file,
|
||||
dtype=[("utt", "object"), ("rec", "object"), ("st", "f"), ("et", "f")],
|
||||
ndmin=1,
|
||||
)
|
||||
|
||||
|
||||
def load_segments_hash(segments_file):
|
||||
"""Load segments hash.
|
||||
|
||||
Args:
|
||||
segments_file: TODO.
|
||||
"""
|
||||
ret = {}
|
||||
if not os.path.exists(segments_file):
|
||||
return None
|
||||
for line in open(segments_file):
|
||||
utt, rec, st, et = line.strip().split()
|
||||
ret[utt] = (rec, float(st), float(et))
|
||||
return ret
|
||||
|
||||
|
||||
def load_segments_rechash(segments_file):
|
||||
"""Load segments rechash.
|
||||
|
||||
Args:
|
||||
segments_file: TODO.
|
||||
"""
|
||||
ret = {}
|
||||
if not os.path.exists(segments_file):
|
||||
return None
|
||||
for line in open(segments_file):
|
||||
utt, rec, st, et = line.strip().split()
|
||||
if rec not in ret:
|
||||
ret[rec] = []
|
||||
ret[rec].append({"utt": utt, "st": float(st), "et": float(et)})
|
||||
return ret
|
||||
|
||||
|
||||
def load_wav_scp(wav_scp_file):
|
||||
"""return dictionary { rec: wav_rxfilename }"""
|
||||
lines = [line.strip().split(None, 1) for line in open(wav_scp_file)]
|
||||
return {x[0]: x[1] for x in lines}
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def load_wav(wav_rxfilename, start=0, end=None):
|
||||
"""This function reads audio file and return data in numpy.float32 array.
|
||||
"lru_cache" holds recently loaded audio so that can be called
|
||||
many times on the same audio file.
|
||||
OPTIMIZE: controls lru_cache size for random access,
|
||||
considering memory size
|
||||
"""
|
||||
if wav_rxfilename.endswith("|"):
|
||||
# input piped command
|
||||
p = subprocess.Popen(wav_rxfilename[:-1], shell=True, stdout=subprocess.PIPE)
|
||||
data, samplerate = sf.load(io.BytesIO(p.stdout.read()), dtype="float32")
|
||||
# cannot seek
|
||||
data = data[start:end]
|
||||
elif wav_rxfilename == "-":
|
||||
# stdin
|
||||
data, samplerate = sf.load(sys.stdin, dtype="float32")
|
||||
# cannot seek
|
||||
data = data[start:end]
|
||||
else:
|
||||
# normal wav file
|
||||
data, samplerate = sf.load(wav_rxfilename, start=start, stop=end)
|
||||
return data, samplerate
|
||||
|
||||
|
||||
def load_utt2spk(utt2spk_file):
|
||||
"""returns dictionary { uttid: spkid }"""
|
||||
lines = [line.strip().split(None, 1) for line in open(utt2spk_file)]
|
||||
return {x[0]: x[1] for x in lines}
|
||||
|
||||
|
||||
def load_spk2utt(spk2utt_file):
|
||||
"""returns dictionary { spkid: list of uttids }"""
|
||||
if not os.path.exists(spk2utt_file):
|
||||
return None
|
||||
lines = [line.strip().split() for line in open(spk2utt_file)]
|
||||
return {x[0]: x[1:] for x in lines}
|
||||
|
||||
|
||||
def load_reco2dur(reco2dur_file):
|
||||
"""returns dictionary { recid: duration }"""
|
||||
if not os.path.exists(reco2dur_file):
|
||||
return None
|
||||
lines = [line.strip().split(None, 1) for line in open(reco2dur_file)]
|
||||
return {x[0]: float(x[1]) for x in lines}
|
||||
|
||||
|
||||
def process_wav(wav_rxfilename, process):
|
||||
"""This function returns preprocessed wav_rxfilename
|
||||
Args:
|
||||
wav_rxfilename: input
|
||||
process: command which can be connected via pipe,
|
||||
use stdin and stdout
|
||||
Returns:
|
||||
wav_rxfilename: output piped command
|
||||
"""
|
||||
if wav_rxfilename.endswith("|"):
|
||||
# input piped command
|
||||
return wav_rxfilename + process + "|"
|
||||
else:
|
||||
# stdin "-" or normal file
|
||||
return "cat {} | {} |".format(wav_rxfilename, process)
|
||||
|
||||
|
||||
def extract_segments(wavs, segments=None):
|
||||
"""This function returns generator of segmented audio as
|
||||
(utterance id, numpy.float32 array)
|
||||
TODO?: sampling rate is not converted.
|
||||
"""
|
||||
if segments is not None:
|
||||
# segments should be sorted by rec-id
|
||||
for seg in segments:
|
||||
wav = wavs[seg["rec"]]
|
||||
data, samplerate = load_wav(wav)
|
||||
st_sample = np.rint(seg["st"] * samplerate).astype(int)
|
||||
et_sample = np.rint(seg["et"] * samplerate).astype(int)
|
||||
yield seg["utt"], data[st_sample:et_sample]
|
||||
else:
|
||||
# segments file not found,
|
||||
# wav.scp is used as segmented audio list
|
||||
for rec in wavs:
|
||||
data, samplerate = load_wav(wavs[rec])
|
||||
yield rec, data
|
||||
|
||||
|
||||
class KaldiData:
|
||||
def __init__(self, data_dir):
|
||||
"""Initialize KaldiData.
|
||||
|
||||
Args:
|
||||
data_dir: TODO.
|
||||
"""
|
||||
self.data_dir = data_dir
|
||||
self.segments = load_segments_rechash(os.path.join(self.data_dir, "segments"))
|
||||
self.utt2spk = load_utt2spk(os.path.join(self.data_dir, "utt2spk"))
|
||||
self.wavs = load_wav_scp(os.path.join(self.data_dir, "wav.scp"))
|
||||
self.reco2dur = load_reco2dur(os.path.join(self.data_dir, "reco2dur"))
|
||||
self.spk2utt = load_spk2utt(os.path.join(self.data_dir, "spk2utt"))
|
||||
|
||||
def load_wav(self, recid, start=0, end=None):
|
||||
"""Load wav.
|
||||
|
||||
Args:
|
||||
recid: TODO.
|
||||
start: TODO.
|
||||
end: TODO.
|
||||
"""
|
||||
data, rate = load_wav(self.wavs[recid], start, end)
|
||||
return data, rate
|
||||
@@ -0,0 +1,67 @@
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from scipy.optimize import linear_sum_assignment
|
||||
|
||||
|
||||
def standard_loss(ys, ts):
|
||||
"""Standard loss.
|
||||
|
||||
Args:
|
||||
ys: TODO.
|
||||
ts: TODO.
|
||||
"""
|
||||
losses = [F.binary_cross_entropy(torch.sigmoid(y), t) * len(y) for y, t in zip(ys, ts)]
|
||||
loss = torch.sum(torch.stack(losses))
|
||||
n_frames = (
|
||||
torch.from_numpy(np.array(np.sum([t.shape[0] for t in ts])))
|
||||
.to(torch.float32)
|
||||
.to(ys[0].device)
|
||||
)
|
||||
loss = loss / n_frames
|
||||
return loss
|
||||
|
||||
|
||||
def fast_batch_pit_n_speaker_loss(ys, ts):
|
||||
"""Fast batch pit n speaker loss.
|
||||
|
||||
Args:
|
||||
ys: TODO.
|
||||
ts: TODO.
|
||||
"""
|
||||
with torch.no_grad():
|
||||
bs = len(ys)
|
||||
indices = []
|
||||
for b in range(bs):
|
||||
y = ys[b].transpose(0, 1)
|
||||
t = ts[b].transpose(0, 1)
|
||||
C, _ = t.shape
|
||||
y = y[:, None, :].repeat(1, C, 1)
|
||||
t = t[None, :, :].repeat(C, 1, 1)
|
||||
bce_loss = F.binary_cross_entropy(torch.sigmoid(y), t, reduction="none").mean(-1)
|
||||
C = bce_loss.cpu()
|
||||
indices.append(linear_sum_assignment(C))
|
||||
labels_perm = [t[:, idx[1]] for t, idx in zip(ts, indices)]
|
||||
|
||||
return labels_perm
|
||||
|
||||
|
||||
def cal_power_loss(logits, power_ts):
|
||||
"""Cal power loss.
|
||||
|
||||
Args:
|
||||
logits: TODO.
|
||||
power_ts: TODO.
|
||||
"""
|
||||
losses = [
|
||||
F.cross_entropy(input=logit, target=power_t.to(torch.long)) * len(logit)
|
||||
for logit, power_t in zip(logits, power_ts)
|
||||
]
|
||||
loss = torch.sum(torch.stack(losses))
|
||||
n_frames = (
|
||||
torch.from_numpy(np.array(np.sum([power_t.shape[0] for power_t in power_ts])))
|
||||
.to(torch.float32)
|
||||
.to(power_ts[0].device)
|
||||
)
|
||||
loss = loss / n_frames
|
||||
return loss
|
||||
@@ -0,0 +1,156 @@
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.multiprocessing
|
||||
import torch.nn.functional as F
|
||||
from itertools import combinations
|
||||
from itertools import permutations
|
||||
|
||||
|
||||
def generate_mapping_dict(max_speaker_num=6, max_olp_speaker_num=3):
|
||||
"""Generate mapping dict.
|
||||
|
||||
Args:
|
||||
max_speaker_num: TODO.
|
||||
max_olp_speaker_num: TODO.
|
||||
"""
|
||||
all_kinds = []
|
||||
all_kinds.append(0)
|
||||
for i in range(max_olp_speaker_num):
|
||||
selected_num = i + 1
|
||||
coms = np.array(list(combinations(np.arange(max_speaker_num), selected_num)))
|
||||
for com in coms:
|
||||
tmp = np.zeros(max_speaker_num)
|
||||
tmp[com] = 1
|
||||
item = int(raw_dec_trans(tmp.reshape(1, -1), max_speaker_num)[0])
|
||||
all_kinds.append(item)
|
||||
all_kinds_order = sorted(all_kinds)
|
||||
|
||||
mapping_dict = {}
|
||||
mapping_dict["dec2label"] = {}
|
||||
mapping_dict["label2dec"] = {}
|
||||
for i in range(len(all_kinds_order)):
|
||||
dec = all_kinds_order[i]
|
||||
mapping_dict["dec2label"][dec] = i
|
||||
mapping_dict["label2dec"][i] = dec
|
||||
oov_id = len(all_kinds_order)
|
||||
mapping_dict["oov"] = oov_id
|
||||
return mapping_dict
|
||||
|
||||
|
||||
def raw_dec_trans(x, max_speaker_num):
|
||||
"""Raw dec trans.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
max_speaker_num: TODO.
|
||||
"""
|
||||
num_list = []
|
||||
for i in range(max_speaker_num):
|
||||
num_list.append(x[:, i])
|
||||
base = 1
|
||||
T = x.shape[0]
|
||||
res = np.zeros((T))
|
||||
for num in num_list:
|
||||
res += num * base
|
||||
base = base * 2
|
||||
return res
|
||||
|
||||
|
||||
def mapping_func(num, mapping_dict):
|
||||
"""Mapping func.
|
||||
|
||||
Args:
|
||||
num: TODO.
|
||||
mapping_dict: TODO.
|
||||
"""
|
||||
if num in mapping_dict["dec2label"].keys():
|
||||
label = mapping_dict["dec2label"][num]
|
||||
else:
|
||||
label = mapping_dict["oov"]
|
||||
return label
|
||||
|
||||
|
||||
def dec_trans(x, max_speaker_num, mapping_dict):
|
||||
"""Dec trans.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
max_speaker_num: TODO.
|
||||
mapping_dict: TODO.
|
||||
"""
|
||||
num_list = []
|
||||
for i in range(max_speaker_num):
|
||||
num_list.append(x[:, i])
|
||||
base = 1
|
||||
T = x.shape[0]
|
||||
res = np.zeros((T))
|
||||
for num in num_list:
|
||||
res += num * base
|
||||
base = base * 2
|
||||
res = np.array([mapping_func(i, mapping_dict) for i in res])
|
||||
return res
|
||||
|
||||
|
||||
def create_powerlabel(label, mapping_dict, max_speaker_num=6, max_olp_speaker_num=3):
|
||||
"""Create powerlabel.
|
||||
|
||||
Args:
|
||||
label: TODO.
|
||||
mapping_dict: TODO.
|
||||
max_speaker_num: TODO.
|
||||
max_olp_speaker_num: TODO.
|
||||
"""
|
||||
T, C = label.shape
|
||||
padding_label = np.zeros((T, max_speaker_num))
|
||||
padding_label[:, :C] = label
|
||||
out_label = dec_trans(padding_label, max_speaker_num, mapping_dict)
|
||||
out_label = torch.from_numpy(out_label)
|
||||
return out_label
|
||||
|
||||
|
||||
def generate_perm_pse(label, n_speaker, mapping_dict, max_speaker_num, max_olp_speaker_num=3):
|
||||
"""Generate perm pse.
|
||||
|
||||
Args:
|
||||
label: TODO.
|
||||
n_speaker: TODO.
|
||||
mapping_dict: TODO.
|
||||
max_speaker_num: TODO.
|
||||
max_olp_speaker_num: TODO.
|
||||
"""
|
||||
perms = np.array(list(permutations(range(n_speaker)))).astype(np.float32)
|
||||
perms = torch.from_numpy(perms).to(label.device).to(torch.int64)
|
||||
perm_labels = [label[:, perm] for perm in perms]
|
||||
perm_pse_labels = [
|
||||
create_powerlabel(perm_label.cpu().numpy(), mapping_dict, max_speaker_num).to(
|
||||
perm_label.device, non_blocking=True
|
||||
)
|
||||
for perm_label in perm_labels
|
||||
]
|
||||
return perm_labels, perm_pse_labels
|
||||
|
||||
|
||||
def generate_min_pse(
|
||||
label, n_speaker, mapping_dict, max_speaker_num, pse_logit, max_olp_speaker_num=3
|
||||
):
|
||||
"""Generate min pse.
|
||||
|
||||
Args:
|
||||
label: TODO.
|
||||
n_speaker: TODO.
|
||||
mapping_dict: TODO.
|
||||
max_speaker_num: TODO.
|
||||
pse_logit: TODO.
|
||||
max_olp_speaker_num: TODO.
|
||||
"""
|
||||
perm_labels, perm_pse_labels = generate_perm_pse(
|
||||
label, n_speaker, mapping_dict, max_speaker_num, max_olp_speaker_num=max_olp_speaker_num
|
||||
)
|
||||
losses = [
|
||||
F.cross_entropy(input=pse_logit, target=perm_pse_label.to(torch.long)) * len(pse_logit)
|
||||
for perm_pse_label in perm_pse_labels
|
||||
]
|
||||
loss = torch.stack(losses)
|
||||
min_index = torch.argmin(loss)
|
||||
selected_perm_label, selected_pse_label = perm_labels[min_index], perm_pse_labels[min_index]
|
||||
return selected_perm_label, selected_pse_label
|
||||
@@ -0,0 +1,229 @@
|
||||
import copy
|
||||
import numpy as np
|
||||
import time
|
||||
import torch
|
||||
from funasr.models.eend.utils.power import create_powerlabel
|
||||
from itertools import combinations
|
||||
|
||||
metrics = [
|
||||
("diarization_error", "speaker_scored", "DER"),
|
||||
("speech_miss", "speech_scored", "SAD_MR"),
|
||||
("speech_falarm", "speech_scored", "SAD_FR"),
|
||||
("speaker_miss", "speaker_scored", "MI"),
|
||||
("speaker_falarm", "speaker_scored", "FA"),
|
||||
("speaker_error", "speaker_scored", "CF"),
|
||||
("correct", "frames", "accuracy"),
|
||||
]
|
||||
|
||||
|
||||
def recover_prediction(y, n_speaker):
|
||||
"""Recover prediction.
|
||||
|
||||
Args:
|
||||
y: TODO.
|
||||
n_speaker: TODO.
|
||||
"""
|
||||
if n_speaker <= 1:
|
||||
return y
|
||||
elif n_speaker == 2:
|
||||
com_index = torch.from_numpy(np.array(list(combinations(np.arange(n_speaker), 2)))).to(
|
||||
y.dtype
|
||||
)
|
||||
num_coms = com_index.shape[0]
|
||||
y_single = y[:, :-num_coms]
|
||||
y_olp = y[:, -num_coms:]
|
||||
olp_map_index = torch.where(y_olp > 0.5)
|
||||
olp_map_index = torch.stack(olp_map_index, dim=1)
|
||||
com_map_index = com_index[olp_map_index[:, -1]]
|
||||
speaker_map_index = torch.from_numpy(np.array(com_map_index)).view(-1).to(torch.int64)
|
||||
frame_map_index = olp_map_index[:, 0][:, None].repeat([1, 2]).view(-1).to(torch.int64)
|
||||
y_single[frame_map_index] = 0
|
||||
y_single[frame_map_index, speaker_map_index] = 1
|
||||
return y_single
|
||||
else:
|
||||
olp2_com_index = torch.from_numpy(np.array(list(combinations(np.arange(n_speaker), 2)))).to(
|
||||
y.dtype
|
||||
)
|
||||
olp2_num_coms = olp2_com_index.shape[0]
|
||||
olp3_com_index = torch.from_numpy(np.array(list(combinations(np.arange(n_speaker), 3)))).to(
|
||||
y.dtype
|
||||
)
|
||||
olp3_num_coms = olp3_com_index.shape[0]
|
||||
y_single = y[:, :n_speaker]
|
||||
y_olp2 = y[:, n_speaker : n_speaker + olp2_num_coms]
|
||||
y_olp3 = y[:, -olp3_num_coms:]
|
||||
|
||||
olp3_map_index = torch.where(y_olp3 > 0.5)
|
||||
olp3_map_index = torch.stack(olp3_map_index, dim=1)
|
||||
olp3_com_map_index = olp3_com_index[olp3_map_index[:, -1]]
|
||||
olp3_speaker_map_index = (
|
||||
torch.from_numpy(np.array(olp3_com_map_index)).view(-1).to(torch.int64)
|
||||
)
|
||||
olp3_frame_map_index = olp3_map_index[:, 0][:, None].repeat([1, 3]).view(-1).to(torch.int64)
|
||||
y_single[olp3_frame_map_index] = 0
|
||||
y_single[olp3_frame_map_index, olp3_speaker_map_index] = 1
|
||||
y_olp2[olp3_frame_map_index] = 0
|
||||
|
||||
olp2_map_index = torch.where(y_olp2 > 0.5)
|
||||
olp2_map_index = torch.stack(olp2_map_index, dim=1)
|
||||
olp2_com_map_index = olp2_com_index[olp2_map_index[:, -1]]
|
||||
olp2_speaker_map_index = (
|
||||
torch.from_numpy(np.array(olp2_com_map_index)).view(-1).to(torch.int64)
|
||||
)
|
||||
olp2_frame_map_index = olp2_map_index[:, 0][:, None].repeat([1, 2]).view(-1).to(torch.int64)
|
||||
y_single[olp2_frame_map_index] = 0
|
||||
y_single[olp2_frame_map_index, olp2_speaker_map_index] = 1
|
||||
return y_single
|
||||
|
||||
|
||||
class PowerReporter:
|
||||
def __init__(self, valid_data_loader, mapping_dict, max_n_speaker):
|
||||
"""Initialize PowerReporter.
|
||||
|
||||
Args:
|
||||
valid_data_loader: TODO.
|
||||
mapping_dict: TODO.
|
||||
max_n_speaker: TODO.
|
||||
"""
|
||||
valid_data_loader_cp = copy.deepcopy(valid_data_loader)
|
||||
self.valid_data_loader = valid_data_loader_cp
|
||||
del valid_data_loader
|
||||
self.mapping_dict = mapping_dict
|
||||
self.max_n_speaker = max_n_speaker
|
||||
|
||||
def report(self, model, eidx, device):
|
||||
"""Report.
|
||||
|
||||
Args:
|
||||
model: Model instance or model name.
|
||||
eidx: TODO.
|
||||
device: Target device ("cuda:0", "cpu", etc.).
|
||||
"""
|
||||
self.report_val(model, eidx, device)
|
||||
|
||||
def report_val(self, model, eidx, device):
|
||||
"""Report val.
|
||||
|
||||
Args:
|
||||
model: Model instance or model name.
|
||||
eidx: TODO.
|
||||
device: Target device ("cuda:0", "cpu", etc.).
|
||||
"""
|
||||
model.eval()
|
||||
ud_valid_start = time.time()
|
||||
valid_res, valid_loss, stats_keys, vad_valid_accuracy = self.report_core(
|
||||
model, self.valid_data_loader, device
|
||||
)
|
||||
|
||||
# Epoch Display
|
||||
valid_der = valid_res["diarization_error"] / valid_res["speaker_scored"]
|
||||
valid_accuracy = valid_res["correct"].to(torch.float32) / valid_res["frames"] * 100
|
||||
vad_valid_accuracy = vad_valid_accuracy * 100
|
||||
print(
|
||||
"Epoch ",
|
||||
eidx + 1,
|
||||
"Valid Loss ",
|
||||
valid_loss,
|
||||
"Valid_DER %.5f" % valid_der,
|
||||
"Valid_Accuracy %.5f%% " % valid_accuracy,
|
||||
"VAD_Valid_Accuracy %.5f%% " % vad_valid_accuracy,
|
||||
)
|
||||
ud_valid = (time.time() - ud_valid_start) / 60.0
|
||||
print("Valid cost time ... ", ud_valid)
|
||||
|
||||
def inv_mapping_func(self, label, mapping_dict):
|
||||
"""Inv mapping func.
|
||||
|
||||
Args:
|
||||
label: TODO.
|
||||
mapping_dict: TODO.
|
||||
"""
|
||||
if not isinstance(label, int):
|
||||
label = int(label)
|
||||
if label in mapping_dict["label2dec"].keys():
|
||||
num = mapping_dict["label2dec"][label]
|
||||
else:
|
||||
num = -1
|
||||
return num
|
||||
|
||||
def report_core(self, model, data_loader, device):
|
||||
"""Report core.
|
||||
|
||||
Args:
|
||||
model: Model instance or model name.
|
||||
data_loader: TODO.
|
||||
device: Target device ("cuda:0", "cpu", etc.).
|
||||
"""
|
||||
res = {}
|
||||
for item in metrics:
|
||||
res[item[0]] = 0.0
|
||||
res[item[1]] = 0.0
|
||||
with torch.no_grad():
|
||||
loss_s = 0.0
|
||||
uidx = 0
|
||||
for xs, ts, orders in data_loader:
|
||||
xs = [x.to(device) for x in xs]
|
||||
ts = [t.to(device) for t in ts]
|
||||
orders = [o.to(device) for o in orders]
|
||||
loss, pit_loss, mpit_loss, att_loss, ys, logits, labels, attractors = model(
|
||||
xs, ts, orders
|
||||
)
|
||||
loss_s += loss.item()
|
||||
uidx += 1
|
||||
|
||||
for logit, t, att in zip(logits, labels, attractors):
|
||||
pred = torch.argmax(torch.softmax(logit, dim=-1), dim=-1) # (T, )
|
||||
oov_index = torch.where(pred == self.mapping_dict["oov"])[0]
|
||||
for i in oov_index:
|
||||
if i > 0:
|
||||
pred[i] = pred[i - 1]
|
||||
else:
|
||||
pred[i] = 0
|
||||
pred = [self.inv_mapping_func(i, self.mapping_dict) for i in pred]
|
||||
decisions = [bin(num)[2:].zfill(self.max_n_speaker)[::-1] for num in pred]
|
||||
decisions = (
|
||||
torch.from_numpy(
|
||||
np.stack([np.array([int(i) for i in dec]) for dec in decisions], axis=0)
|
||||
)
|
||||
.to(att.device)
|
||||
.to(torch.float32)
|
||||
)
|
||||
decisions = decisions[:, : att.shape[0]]
|
||||
|
||||
stats = self.calc_diarization_error(decisions, t)
|
||||
res["speaker_scored"] += stats["speaker_scored"]
|
||||
res["speech_scored"] += stats["speech_scored"]
|
||||
res["frames"] += stats["frames"]
|
||||
for item in metrics:
|
||||
res[item[0]] += stats[item[0]]
|
||||
loss_s /= uidx
|
||||
vad_acc = 0
|
||||
|
||||
return res, loss_s, stats.keys(), vad_acc
|
||||
|
||||
def calc_diarization_error(self, decisions, label, label_delay=0):
|
||||
"""Calc diarization error.
|
||||
|
||||
Args:
|
||||
decisions: TODO.
|
||||
label: TODO.
|
||||
label_delay: TODO.
|
||||
"""
|
||||
label = label[: len(label) - label_delay, ...]
|
||||
n_ref = torch.sum(label, dim=-1)
|
||||
n_sys = torch.sum(decisions, dim=-1)
|
||||
res = {}
|
||||
res["speech_scored"] = torch.sum(n_ref > 0)
|
||||
res["speech_miss"] = torch.sum((n_ref > 0) & (n_sys == 0))
|
||||
res["speech_falarm"] = torch.sum((n_ref == 0) & (n_sys > 0))
|
||||
res["speaker_scored"] = torch.sum(n_ref)
|
||||
res["speaker_miss"] = torch.sum(torch.max(n_ref - n_sys, torch.zeros_like(n_ref)))
|
||||
res["speaker_falarm"] = torch.sum(torch.max(n_sys - n_ref, torch.zeros_like(n_ref)))
|
||||
n_map = torch.sum(((label == 1) & (decisions == 1)), dim=-1).to(torch.float32)
|
||||
res["speaker_error"] = torch.sum(torch.min(n_ref, n_sys) - n_map)
|
||||
res["correct"] = torch.sum(label == decisions) / label.shape[1]
|
||||
res["diarization_error"] = (
|
||||
res["speaker_miss"] + res["speaker_falarm"] + res["speaker_error"]
|
||||
)
|
||||
res["frames"] = len(label)
|
||||
return res
|
||||
@@ -0,0 +1,2 @@
|
||||
from .eres2net import ERes2Net
|
||||
from .eres2net_aug import ERes2NetAug
|
||||
@@ -0,0 +1,481 @@
|
||||
# Copyright 3D-Speaker (https://github.com/alibaba-damo-academy/3D-Speaker). All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
""" Res2Net implementation is adapted from https://github.com/wenet-e2e/wespeaker.
|
||||
ERes2Net incorporates both local and global feature fusion techniques to improve the performance.
|
||||
The local feature fusion (LFF) fuses the features within one single residual block to extract the local signal.
|
||||
The global feature fusion (GFF) takes acoustic features of different scales as input to aggregate global signal.
|
||||
ERes2Net-Large is an upgraded version of ERes2Net that uses a larger number of parameters to achieve better
|
||||
recognition performance. Parameters expansion, baseWidth, and scale can be modified to obtain optimal performance.
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
import funasr.models.sond.pooling.pooling_layers as pooling_layers
|
||||
|
||||
from funasr.models.eres2net.fusion import AFF
|
||||
|
||||
|
||||
class ReLU(nn.Hardtanh):
|
||||
|
||||
def __init__(self, inplace=False):
|
||||
"""Initialize ReLU.
|
||||
|
||||
Args:
|
||||
inplace: TODO.
|
||||
"""
|
||||
super(ReLU, self).__init__(0, 20, inplace)
|
||||
|
||||
def __repr__(self):
|
||||
"""Internal: repr ."""
|
||||
inplace_str = "inplace" if self.inplace else ""
|
||||
return self.__class__.__name__ + " (" + inplace_str + ")"
|
||||
|
||||
|
||||
def conv1x1(in_planes, out_planes, stride=1):
|
||||
"1x1 convolution without padding"
|
||||
return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, padding=0, bias=False)
|
||||
|
||||
|
||||
def conv3x3(in_planes, out_planes, stride=1):
|
||||
"3x3 convolution with padding"
|
||||
return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride, padding=1, bias=False)
|
||||
|
||||
|
||||
class BasicBlockERes2Net(nn.Module):
|
||||
expansion = 2
|
||||
|
||||
def __init__(self, in_planes, planes, stride=1, baseWidth=32, scale=2):
|
||||
"""Initialize BasicBlockERes2Net.
|
||||
|
||||
Args:
|
||||
in_planes: TODO.
|
||||
planes: TODO.
|
||||
stride: TODO.
|
||||
baseWidth: TODO.
|
||||
scale: TODO.
|
||||
"""
|
||||
super(BasicBlockERes2Net, self).__init__()
|
||||
width = int(math.floor(planes * (baseWidth / 64.0)))
|
||||
self.conv1 = conv1x1(in_planes, width * scale, stride)
|
||||
self.bn1 = nn.BatchNorm2d(width * scale)
|
||||
self.nums = scale
|
||||
|
||||
convs = []
|
||||
bns = []
|
||||
for i in range(self.nums):
|
||||
convs.append(conv3x3(width, width))
|
||||
bns.append(nn.BatchNorm2d(width))
|
||||
self.convs = nn.ModuleList(convs)
|
||||
self.bns = nn.ModuleList(bns)
|
||||
self.relu = ReLU(inplace=True)
|
||||
|
||||
self.conv3 = conv1x1(width * scale, planes * self.expansion)
|
||||
self.bn3 = nn.BatchNorm2d(planes * self.expansion)
|
||||
self.shortcut = nn.Sequential()
|
||||
if stride != 1 or in_planes != self.expansion * planes:
|
||||
self.shortcut = nn.Sequential(
|
||||
nn.Conv2d(
|
||||
in_planes, self.expansion * planes, kernel_size=1, stride=stride, bias=False
|
||||
),
|
||||
nn.BatchNorm2d(self.expansion * planes),
|
||||
)
|
||||
self.stride = stride
|
||||
self.width = width
|
||||
self.scale = scale
|
||||
|
||||
def forward(self, x):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
residual = x
|
||||
|
||||
out = self.conv1(x)
|
||||
out = self.bn1(out)
|
||||
out = self.relu(out)
|
||||
spx = torch.split(out, self.width, 1)
|
||||
for i in range(self.nums):
|
||||
if i == 0:
|
||||
sp = spx[i]
|
||||
else:
|
||||
sp = sp + spx[i]
|
||||
sp = self.convs[i](sp)
|
||||
sp = self.relu(self.bns[i](sp))
|
||||
if i == 0:
|
||||
out = sp
|
||||
else:
|
||||
out = torch.cat((out, sp), 1)
|
||||
|
||||
out = self.conv3(out)
|
||||
out = self.bn3(out)
|
||||
|
||||
residual = self.shortcut(x)
|
||||
out += residual
|
||||
out = self.relu(out)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
class BasicBlockERes2Net_diff_AFF(nn.Module):
|
||||
expansion = 2
|
||||
|
||||
def __init__(self, in_planes, planes, stride=1, baseWidth=32, scale=2):
|
||||
"""Initialize BasicBlockERes2Net_diff_AFF.
|
||||
|
||||
Args:
|
||||
in_planes: TODO.
|
||||
planes: TODO.
|
||||
stride: TODO.
|
||||
baseWidth: TODO.
|
||||
scale: TODO.
|
||||
"""
|
||||
super(BasicBlockERes2Net_diff_AFF, self).__init__()
|
||||
width = int(math.floor(planes * (baseWidth / 64.0)))
|
||||
self.conv1 = conv1x1(in_planes, width * scale, stride)
|
||||
self.bn1 = nn.BatchNorm2d(width * scale)
|
||||
self.nums = scale
|
||||
|
||||
convs = []
|
||||
fuse_models = []
|
||||
bns = []
|
||||
for i in range(self.nums):
|
||||
convs.append(conv3x3(width, width))
|
||||
bns.append(nn.BatchNorm2d(width))
|
||||
for j in range(self.nums - 1):
|
||||
fuse_models.append(AFF(channels=width))
|
||||
|
||||
self.convs = nn.ModuleList(convs)
|
||||
self.bns = nn.ModuleList(bns)
|
||||
self.fuse_models = nn.ModuleList(fuse_models)
|
||||
self.relu = ReLU(inplace=True)
|
||||
|
||||
self.conv3 = conv1x1(width * scale, planes * self.expansion)
|
||||
self.bn3 = nn.BatchNorm2d(planes * self.expansion)
|
||||
self.shortcut = nn.Sequential()
|
||||
if stride != 1 or in_planes != self.expansion * planes:
|
||||
self.shortcut = nn.Sequential(
|
||||
nn.Conv2d(
|
||||
in_planes, self.expansion * planes, kernel_size=1, stride=stride, bias=False
|
||||
),
|
||||
nn.BatchNorm2d(self.expansion * planes),
|
||||
)
|
||||
self.stride = stride
|
||||
self.width = width
|
||||
self.scale = scale
|
||||
|
||||
def forward(self, x):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
residual = x
|
||||
|
||||
out = self.conv1(x)
|
||||
out = self.bn1(out)
|
||||
out = self.relu(out)
|
||||
spx = torch.split(out, self.width, 1)
|
||||
for i in range(self.nums):
|
||||
if i == 0:
|
||||
sp = spx[i]
|
||||
else:
|
||||
sp = self.fuse_models[i - 1](sp, spx[i])
|
||||
|
||||
sp = self.convs[i](sp)
|
||||
sp = self.relu(self.bns[i](sp))
|
||||
if i == 0:
|
||||
out = sp
|
||||
else:
|
||||
out = torch.cat((out, sp), 1)
|
||||
|
||||
out = self.conv3(out)
|
||||
out = self.bn3(out)
|
||||
|
||||
residual = self.shortcut(x)
|
||||
out += residual
|
||||
out = self.relu(out)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
class ERes2Net(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
block=BasicBlockERes2Net,
|
||||
block_fuse=BasicBlockERes2Net_diff_AFF,
|
||||
num_blocks=[3, 4, 6, 3],
|
||||
m_channels=32,
|
||||
feat_dim=80,
|
||||
embedding_size=192,
|
||||
pooling_func="TSTP",
|
||||
two_emb_layer=False,
|
||||
):
|
||||
"""Initialize ERes2Net.
|
||||
|
||||
Args:
|
||||
block: TODO.
|
||||
block_fuse: TODO.
|
||||
num_blocks: TODO.
|
||||
m_channels: TODO.
|
||||
feat_dim: Size/dimension parameter.
|
||||
embedding_size: Size/dimension parameter.
|
||||
pooling_func: TODO.
|
||||
two_emb_layer: TODO.
|
||||
"""
|
||||
super(ERes2Net, self).__init__()
|
||||
self.in_planes = m_channels
|
||||
self.feat_dim = feat_dim
|
||||
self.embedding_size = embedding_size
|
||||
self.stats_dim = int(feat_dim / 8) * m_channels * 8
|
||||
self.two_emb_layer = two_emb_layer
|
||||
|
||||
self.conv1 = nn.Conv2d(1, m_channels, kernel_size=3, stride=1, padding=1, bias=False)
|
||||
self.bn1 = nn.BatchNorm2d(m_channels)
|
||||
self.layer1 = self._make_layer(block, m_channels, num_blocks[0], stride=1)
|
||||
self.layer2 = self._make_layer(block, m_channels * 2, num_blocks[1], stride=2)
|
||||
self.layer3 = self._make_layer(block_fuse, m_channels * 4, num_blocks[2], stride=2)
|
||||
self.layer4 = self._make_layer(block_fuse, m_channels * 8, num_blocks[3], stride=2)
|
||||
|
||||
# Downsampling module for each layer
|
||||
self.layer1_downsample = nn.Conv2d(
|
||||
m_channels * 2, m_channels * 4, kernel_size=3, stride=2, padding=1, bias=False
|
||||
)
|
||||
self.layer2_downsample = nn.Conv2d(
|
||||
m_channels * 4, m_channels * 8, kernel_size=3, padding=1, stride=2, bias=False
|
||||
)
|
||||
self.layer3_downsample = nn.Conv2d(
|
||||
m_channels * 8, m_channels * 16, kernel_size=3, padding=1, stride=2, bias=False
|
||||
)
|
||||
|
||||
# Bottom-up fusion module
|
||||
self.fuse_mode12 = AFF(channels=m_channels * 4)
|
||||
self.fuse_mode123 = AFF(channels=m_channels * 8)
|
||||
self.fuse_mode1234 = AFF(channels=m_channels * 16)
|
||||
|
||||
self.n_stats = 1 if pooling_func == "TAP" or pooling_func == "TSDP" else 2
|
||||
self.pool = getattr(pooling_layers, pooling_func)(in_dim=self.stats_dim * block.expansion)
|
||||
self.seg_1 = nn.Linear(self.stats_dim * block.expansion * self.n_stats, embedding_size)
|
||||
if self.two_emb_layer:
|
||||
self.seg_bn_1 = nn.BatchNorm1d(embedding_size, affine=False)
|
||||
self.seg_2 = nn.Linear(embedding_size, embedding_size)
|
||||
else:
|
||||
self.seg_bn_1 = nn.Identity()
|
||||
self.seg_2 = nn.Identity()
|
||||
|
||||
def _make_layer(self, block, planes, num_blocks, stride):
|
||||
"""Internal: make layer.
|
||||
|
||||
Args:
|
||||
block: TODO.
|
||||
planes: TODO.
|
||||
num_blocks: TODO.
|
||||
stride: TODO.
|
||||
"""
|
||||
strides = [stride] + [1] * (num_blocks - 1)
|
||||
layers = []
|
||||
for stride in strides:
|
||||
layers.append(block(self.in_planes, planes, stride))
|
||||
self.in_planes = planes * block.expansion
|
||||
return nn.Sequential(*layers)
|
||||
|
||||
def forward(self, x):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
x = x.permute(0, 2, 1) # (B,T,F) => (B,F,T)
|
||||
x = x.unsqueeze_(1)
|
||||
out = F.relu(self.bn1(self.conv1(x)))
|
||||
out1 = self.layer1(out)
|
||||
out2 = self.layer2(out1)
|
||||
out1_downsample = self.layer1_downsample(out1)
|
||||
fuse_out12 = self.fuse_mode12(out2, out1_downsample)
|
||||
out3 = self.layer3(out2)
|
||||
fuse_out12_downsample = self.layer2_downsample(fuse_out12)
|
||||
fuse_out123 = self.fuse_mode123(out3, fuse_out12_downsample)
|
||||
out4 = self.layer4(out3)
|
||||
fuse_out123_downsample = self.layer3_downsample(fuse_out123)
|
||||
fuse_out1234 = self.fuse_mode1234(out4, fuse_out123_downsample)
|
||||
stats = self.pool(fuse_out1234)
|
||||
|
||||
embed_a = self.seg_1(stats)
|
||||
if self.two_emb_layer:
|
||||
out = F.relu(embed_a)
|
||||
out = self.seg_bn_1(out)
|
||||
embed_b = self.seg_2(out)
|
||||
return embed_b
|
||||
else:
|
||||
return embed_a
|
||||
|
||||
|
||||
class BasicBlockRes2Net(nn.Module):
|
||||
expansion = 2
|
||||
|
||||
def __init__(self, in_planes, planes, stride=1, baseWidth=32, scale=2):
|
||||
"""Initialize BasicBlockRes2Net.
|
||||
|
||||
Args:
|
||||
in_planes: TODO.
|
||||
planes: TODO.
|
||||
stride: TODO.
|
||||
baseWidth: TODO.
|
||||
scale: TODO.
|
||||
"""
|
||||
super(BasicBlockRes2Net, self).__init__()
|
||||
width = int(math.floor(planes * (baseWidth / 64.0)))
|
||||
self.conv1 = conv1x1(in_planes, width * scale, stride)
|
||||
self.bn1 = nn.BatchNorm2d(width * scale)
|
||||
self.nums = scale - 1
|
||||
convs = []
|
||||
bns = []
|
||||
for i in range(self.nums):
|
||||
convs.append(conv3x3(width, width))
|
||||
bns.append(nn.BatchNorm2d(width))
|
||||
self.convs = nn.ModuleList(convs)
|
||||
self.bns = nn.ModuleList(bns)
|
||||
self.relu = ReLU(inplace=True)
|
||||
|
||||
self.conv3 = conv1x1(width * scale, planes * self.expansion)
|
||||
self.bn3 = nn.BatchNorm2d(planes * self.expansion)
|
||||
self.shortcut = nn.Sequential()
|
||||
if stride != 1 or in_planes != self.expansion * planes:
|
||||
self.shortcut = nn.Sequential(
|
||||
nn.Conv2d(
|
||||
in_planes, self.expansion * planes, kernel_size=1, stride=stride, bias=False
|
||||
),
|
||||
nn.BatchNorm2d(self.expansion * planes),
|
||||
)
|
||||
self.stride = stride
|
||||
self.width = width
|
||||
self.scale = scale
|
||||
|
||||
def forward(self, x):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
residual = x
|
||||
|
||||
out = self.conv1(x)
|
||||
out = self.bn1(out)
|
||||
out = self.relu(out)
|
||||
spx = torch.split(out, self.width, 1)
|
||||
for i in range(self.nums):
|
||||
if i == 0:
|
||||
sp = spx[i]
|
||||
else:
|
||||
sp = sp + spx[i]
|
||||
sp = self.convs[i](sp)
|
||||
sp = self.relu(self.bns[i](sp))
|
||||
if i == 0:
|
||||
out = sp
|
||||
else:
|
||||
out = torch.cat((out, sp), 1)
|
||||
|
||||
out = torch.cat((out, spx[self.nums]), 1)
|
||||
|
||||
out = self.conv3(out)
|
||||
out = self.bn3(out)
|
||||
|
||||
residual = self.shortcut(x)
|
||||
out += residual
|
||||
out = self.relu(out)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
class Res2Net(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
block=BasicBlockRes2Net,
|
||||
num_blocks=[3, 4, 6, 3],
|
||||
m_channels=32,
|
||||
feat_dim=80,
|
||||
embedding_size=192,
|
||||
pooling_func="TSTP",
|
||||
two_emb_layer=False,
|
||||
):
|
||||
"""Initialize Res2Net.
|
||||
|
||||
Args:
|
||||
block: TODO.
|
||||
num_blocks: TODO.
|
||||
m_channels: TODO.
|
||||
feat_dim: Size/dimension parameter.
|
||||
embedding_size: Size/dimension parameter.
|
||||
pooling_func: TODO.
|
||||
two_emb_layer: TODO.
|
||||
"""
|
||||
super(Res2Net, self).__init__()
|
||||
self.in_planes = m_channels
|
||||
self.feat_dim = feat_dim
|
||||
self.embedding_size = embedding_size
|
||||
self.stats_dim = int(feat_dim / 8) * m_channels * 8
|
||||
self.two_emb_layer = two_emb_layer
|
||||
|
||||
self.conv1 = nn.Conv2d(1, m_channels, kernel_size=3, stride=1, padding=1, bias=False)
|
||||
self.bn1 = nn.BatchNorm2d(m_channels)
|
||||
self.layer1 = self._make_layer(block, m_channels, num_blocks[0], stride=1)
|
||||
self.layer2 = self._make_layer(block, m_channels * 2, num_blocks[1], stride=2)
|
||||
self.layer3 = self._make_layer(block, m_channels * 4, num_blocks[2], stride=2)
|
||||
self.layer4 = self._make_layer(block, m_channels * 8, num_blocks[3], stride=2)
|
||||
|
||||
self.n_stats = 1 if pooling_func == "TAP" or pooling_func == "TSDP" else 2
|
||||
self.pool = getattr(pooling_layers, pooling_func)(in_dim=self.stats_dim * block.expansion)
|
||||
self.seg_1 = nn.Linear(self.stats_dim * block.expansion * self.n_stats, embedding_size)
|
||||
if self.two_emb_layer:
|
||||
self.seg_bn_1 = nn.BatchNorm1d(embedding_size, affine=False)
|
||||
self.seg_2 = nn.Linear(embedding_size, embedding_size)
|
||||
else:
|
||||
self.seg_bn_1 = nn.Identity()
|
||||
self.seg_2 = nn.Identity()
|
||||
|
||||
def _make_layer(self, block, planes, num_blocks, stride):
|
||||
"""Internal: make layer.
|
||||
|
||||
Args:
|
||||
block: TODO.
|
||||
planes: TODO.
|
||||
num_blocks: TODO.
|
||||
stride: TODO.
|
||||
"""
|
||||
strides = [stride] + [1] * (num_blocks - 1)
|
||||
layers = []
|
||||
for stride in strides:
|
||||
layers.append(block(self.in_planes, planes, stride))
|
||||
self.in_planes = planes * block.expansion
|
||||
return nn.Sequential(*layers)
|
||||
|
||||
def forward(self, x):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
x = x.permute(0, 2, 1) # (B,T,F) => (B,F,T)
|
||||
|
||||
x = x.unsqueeze_(1)
|
||||
out = F.relu(self.bn1(self.conv1(x)))
|
||||
out = self.layer1(out)
|
||||
out = self.layer2(out)
|
||||
out = self.layer3(out)
|
||||
out = self.layer4(out)
|
||||
|
||||
stats = self.pool(out)
|
||||
|
||||
embed_a = self.seg_1(stats)
|
||||
if self.two_emb_layer:
|
||||
out = F.relu(embed_a)
|
||||
out = self.seg_bn_1(out)
|
||||
embed_b = self.seg_2(out)
|
||||
return embed_b
|
||||
else:
|
||||
return embed_a
|
||||
@@ -0,0 +1,314 @@
|
||||
# Copyright 3D-Speaker (https://github.com/alibaba-damo-academy/3D-Speaker). All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
""" Res2Net implementation is adapted from https://github.com/wenet-e2e/wespeaker.
|
||||
ERes2Net incorporates both local and global feature fusion techniques to improve the performance.
|
||||
The local feature fusion (LFF) fuses the features within one single residual block to extract the local signal.
|
||||
The global feature fusion (GFF) takes acoustic features of different scales as input to aggregate global signal.
|
||||
ERes2Net-Large is an upgraded version of ERes2Net that uses a larger number of parameters to achieve better
|
||||
recognition performance. Parameters expansion, baseWidth, and scale can be modified to obtain optimal performance.
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
import funasr.models.sond.pooling.pooling_layers as pooling_layers
|
||||
|
||||
from funasr.models.eres2net.fusion import AFF
|
||||
|
||||
|
||||
class ReLU(nn.Hardtanh):
|
||||
|
||||
def __init__(self, inplace=False):
|
||||
"""Initialize ReLU.
|
||||
|
||||
Args:
|
||||
inplace: TODO.
|
||||
"""
|
||||
super(ReLU, self).__init__(0, 20, inplace)
|
||||
|
||||
def __repr__(self):
|
||||
"""Internal: repr ."""
|
||||
inplace_str = "inplace" if self.inplace else ""
|
||||
return self.__class__.__name__ + " (" + inplace_str + ")"
|
||||
|
||||
|
||||
def conv1x1(in_planes, out_planes, stride=1):
|
||||
"1x1 convolution without padding"
|
||||
return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, padding=0, bias=False)
|
||||
|
||||
|
||||
def conv3x3(in_planes, out_planes, stride=1):
|
||||
"3x3 convolution with padding"
|
||||
return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride, padding=1, bias=False)
|
||||
|
||||
|
||||
class BasicBlockERes2Net(nn.Module):
|
||||
expansion = 4
|
||||
|
||||
def __init__(self, in_planes, planes, stride=1, baseWidth=24, scale=3):
|
||||
"""Initialize BasicBlockERes2Net.
|
||||
|
||||
Args:
|
||||
in_planes: TODO.
|
||||
planes: TODO.
|
||||
stride: TODO.
|
||||
baseWidth: TODO.
|
||||
scale: TODO.
|
||||
"""
|
||||
super(BasicBlockERes2Net, self).__init__()
|
||||
width = int(math.floor(planes * (baseWidth / 64.0)))
|
||||
self.conv1 = conv1x1(in_planes, width * scale, stride)
|
||||
self.bn1 = nn.BatchNorm2d(width * scale)
|
||||
self.nums = scale
|
||||
|
||||
convs = []
|
||||
bns = []
|
||||
for i in range(self.nums):
|
||||
convs.append(conv3x3(width, width))
|
||||
bns.append(nn.BatchNorm2d(width))
|
||||
self.convs = nn.ModuleList(convs)
|
||||
self.bns = nn.ModuleList(bns)
|
||||
self.relu = ReLU(inplace=True)
|
||||
|
||||
self.conv3 = conv1x1(width * scale, planes * self.expansion)
|
||||
self.bn3 = nn.BatchNorm2d(planes * self.expansion)
|
||||
self.shortcut = nn.Sequential()
|
||||
if stride != 1 or in_planes != self.expansion * planes:
|
||||
self.shortcut = nn.Sequential(
|
||||
nn.Conv2d(
|
||||
in_planes, self.expansion * planes, kernel_size=1, stride=stride, bias=False
|
||||
),
|
||||
nn.BatchNorm2d(self.expansion * planes),
|
||||
)
|
||||
self.stride = stride
|
||||
self.width = width
|
||||
self.scale = scale
|
||||
|
||||
def forward(self, x):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
residual = x
|
||||
|
||||
out = self.conv1(x)
|
||||
out = self.bn1(out)
|
||||
out = self.relu(out)
|
||||
spx = torch.split(out, self.width, 1)
|
||||
for i in range(self.nums):
|
||||
if i == 0:
|
||||
sp = spx[i]
|
||||
else:
|
||||
sp = sp + spx[i]
|
||||
sp = self.convs[i](sp)
|
||||
sp = self.relu(self.bns[i](sp))
|
||||
if i == 0:
|
||||
out = sp
|
||||
else:
|
||||
out = torch.cat((out, sp), 1)
|
||||
|
||||
out = self.conv3(out)
|
||||
out = self.bn3(out)
|
||||
|
||||
residual = self.shortcut(x)
|
||||
out += residual
|
||||
out = self.relu(out)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
class BasicBlockERes2Net_diff_AFF(nn.Module):
|
||||
expansion = 4
|
||||
|
||||
def __init__(self, in_planes, planes, stride=1, baseWidth=24, scale=3):
|
||||
"""Initialize BasicBlockERes2Net_diff_AFF.
|
||||
|
||||
Args:
|
||||
in_planes: TODO.
|
||||
planes: TODO.
|
||||
stride: TODO.
|
||||
baseWidth: TODO.
|
||||
scale: TODO.
|
||||
"""
|
||||
super(BasicBlockERes2Net_diff_AFF, self).__init__()
|
||||
width = int(math.floor(planes * (baseWidth / 64.0)))
|
||||
self.conv1 = conv1x1(in_planes, width * scale, stride)
|
||||
self.bn1 = nn.BatchNorm2d(width * scale)
|
||||
|
||||
self.nums = scale
|
||||
|
||||
convs = []
|
||||
fuse_models = []
|
||||
bns = []
|
||||
for i in range(self.nums):
|
||||
convs.append(conv3x3(width, width))
|
||||
bns.append(nn.BatchNorm2d(width))
|
||||
for j in range(self.nums - 1):
|
||||
fuse_models.append(AFF(channels=width))
|
||||
|
||||
self.convs = nn.ModuleList(convs)
|
||||
self.bns = nn.ModuleList(bns)
|
||||
self.fuse_models = nn.ModuleList(fuse_models)
|
||||
self.relu = ReLU(inplace=True)
|
||||
|
||||
self.conv3 = conv1x1(width * scale, planes * self.expansion)
|
||||
self.bn3 = nn.BatchNorm2d(planes * self.expansion)
|
||||
self.shortcut = nn.Sequential()
|
||||
if stride != 1 or in_planes != self.expansion * planes:
|
||||
self.shortcut = nn.Sequential(
|
||||
nn.Conv2d(
|
||||
in_planes, self.expansion * planes, kernel_size=1, stride=stride, bias=False
|
||||
),
|
||||
nn.BatchNorm2d(self.expansion * planes),
|
||||
)
|
||||
self.stride = stride
|
||||
self.width = width
|
||||
self.scale = scale
|
||||
|
||||
def forward(self, x):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
residual = x
|
||||
|
||||
out = self.conv1(x)
|
||||
out = self.bn1(out)
|
||||
out = self.relu(out)
|
||||
spx = torch.split(out, self.width, 1)
|
||||
for i in range(self.nums):
|
||||
if i == 0:
|
||||
sp = spx[i]
|
||||
else:
|
||||
sp = self.fuse_models[i - 1](sp, spx[i])
|
||||
|
||||
sp = self.convs[i](sp)
|
||||
sp = self.relu(self.bns[i](sp))
|
||||
if i == 0:
|
||||
out = sp
|
||||
else:
|
||||
out = torch.cat((out, sp), 1)
|
||||
|
||||
out = self.conv3(out)
|
||||
out = self.bn3(out)
|
||||
|
||||
residual = self.shortcut(x)
|
||||
out += residual
|
||||
out = self.relu(out)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
class ERes2NetAug(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
block=BasicBlockERes2Net,
|
||||
block_fuse=BasicBlockERes2Net_diff_AFF,
|
||||
num_blocks=[3, 4, 6, 3],
|
||||
m_channels=64,
|
||||
feat_dim=80,
|
||||
embedding_size=192,
|
||||
pooling_func="TSTP",
|
||||
two_emb_layer=False,
|
||||
):
|
||||
"""Initialize ERes2NetAug.
|
||||
|
||||
Args:
|
||||
block: TODO.
|
||||
block_fuse: TODO.
|
||||
num_blocks: TODO.
|
||||
m_channels: TODO.
|
||||
feat_dim: Size/dimension parameter.
|
||||
embedding_size: Size/dimension parameter.
|
||||
pooling_func: TODO.
|
||||
two_emb_layer: TODO.
|
||||
"""
|
||||
super(ERes2NetAug, self).__init__()
|
||||
self.in_planes = m_channels
|
||||
self.feat_dim = feat_dim
|
||||
self.embedding_size = embedding_size
|
||||
self.stats_dim = int(feat_dim / 8) * m_channels * 8
|
||||
self.two_emb_layer = two_emb_layer
|
||||
|
||||
self.conv1 = nn.Conv2d(1, m_channels, kernel_size=3, stride=1, padding=1, bias=False)
|
||||
self.bn1 = nn.BatchNorm2d(m_channels)
|
||||
self.layer1 = self._make_layer(block, m_channels, num_blocks[0], stride=1)
|
||||
self.layer2 = self._make_layer(block, m_channels * 2, num_blocks[1], stride=2)
|
||||
self.layer3 = self._make_layer(block_fuse, m_channels * 4, num_blocks[2], stride=2)
|
||||
self.layer4 = self._make_layer(block_fuse, m_channels * 8, num_blocks[3], stride=2)
|
||||
|
||||
self.layer1_downsample = nn.Conv2d(
|
||||
m_channels * 4, m_channels * 8, kernel_size=3, padding=1, stride=2, bias=False
|
||||
)
|
||||
self.layer2_downsample = nn.Conv2d(
|
||||
m_channels * 8, m_channels * 16, kernel_size=3, padding=1, stride=2, bias=False
|
||||
)
|
||||
self.layer3_downsample = nn.Conv2d(
|
||||
m_channels * 16, m_channels * 32, kernel_size=3, padding=1, stride=2, bias=False
|
||||
)
|
||||
self.fuse_mode12 = AFF(channels=m_channels * 8)
|
||||
self.fuse_mode123 = AFF(channels=m_channels * 16)
|
||||
self.fuse_mode1234 = AFF(channels=m_channels * 32)
|
||||
|
||||
self.n_stats = 1 if pooling_func == "TAP" or pooling_func == "TSDP" else 2
|
||||
self.pool = getattr(pooling_layers, pooling_func)(in_dim=self.stats_dim * block.expansion)
|
||||
self.seg_1 = nn.Linear(self.stats_dim * block.expansion * self.n_stats, embedding_size)
|
||||
if self.two_emb_layer:
|
||||
self.seg_bn_1 = nn.BatchNorm1d(embedding_size, affine=False)
|
||||
self.seg_2 = nn.Linear(embedding_size, embedding_size)
|
||||
else:
|
||||
self.seg_bn_1 = nn.Identity()
|
||||
self.seg_2 = nn.Identity()
|
||||
|
||||
def _make_layer(self, block, planes, num_blocks, stride):
|
||||
"""Internal: make layer.
|
||||
|
||||
Args:
|
||||
block: TODO.
|
||||
planes: TODO.
|
||||
num_blocks: TODO.
|
||||
stride: TODO.
|
||||
"""
|
||||
strides = [stride] + [1] * (num_blocks - 1)
|
||||
layers = []
|
||||
for stride in strides:
|
||||
layers.append(block(self.in_planes, planes, stride))
|
||||
self.in_planes = planes * block.expansion
|
||||
return nn.Sequential(*layers)
|
||||
|
||||
def forward(self, x):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
x = x.permute(0, 2, 1) # (B,T,F) => (B,F,T)
|
||||
|
||||
x = x.unsqueeze_(1)
|
||||
out = F.relu(self.bn1(self.conv1(x)))
|
||||
out1 = self.layer1(out)
|
||||
out2 = self.layer2(out1)
|
||||
out1_downsample = self.layer1_downsample(out1)
|
||||
fuse_out12 = self.fuse_mode12(out2, out1_downsample)
|
||||
out3 = self.layer3(out2)
|
||||
fuse_out12_downsample = self.layer2_downsample(fuse_out12)
|
||||
fuse_out123 = self.fuse_mode123(out3, fuse_out12_downsample)
|
||||
out4 = self.layer4(out3)
|
||||
fuse_out123_downsample = self.layer3_downsample(fuse_out123)
|
||||
fuse_out1234 = self.fuse_mode1234(out4, fuse_out123_downsample)
|
||||
stats = self.pool(fuse_out1234)
|
||||
|
||||
embed_a = self.seg_1(stats)
|
||||
if self.two_emb_layer:
|
||||
out = F.relu(embed_a)
|
||||
out = self.seg_bn_1(out)
|
||||
embed_b = self.seg_2(out)
|
||||
return embed_b
|
||||
else:
|
||||
return embed_a
|
||||
@@ -0,0 +1,290 @@
|
||||
# Copyright 3D-Speaker (https://github.com/alibaba-damo-academy/3D-Speaker). All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
import math
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
import funasr.models.sond.pooling.pooling_layers as pooling_layers
|
||||
from funasr.models.eres2net.fusion import AFF
|
||||
|
||||
|
||||
class ReLU(nn.Hardtanh):
|
||||
|
||||
def __init__(self, inplace=False):
|
||||
"""Initialize ReLU.
|
||||
|
||||
Args:
|
||||
inplace: TODO.
|
||||
"""
|
||||
super(ReLU, self).__init__(0, 20, inplace)
|
||||
|
||||
def __repr__(self):
|
||||
"""Internal: repr ."""
|
||||
inplace_str = "inplace" if self.inplace else ""
|
||||
return self.__class__.__name__ + " (" + inplace_str + ")"
|
||||
|
||||
|
||||
class BasicBlockERes2NetV2(nn.Module):
|
||||
|
||||
def __init__(self, in_planes, planes, stride=1, baseWidth=26, scale=2, expansion=2):
|
||||
"""Initialize BasicBlockERes2NetV2.
|
||||
|
||||
Args:
|
||||
in_planes: TODO.
|
||||
planes: TODO.
|
||||
stride: TODO.
|
||||
baseWidth: TODO.
|
||||
scale: TODO.
|
||||
expansion: TODO.
|
||||
"""
|
||||
super(BasicBlockERes2NetV2, self).__init__()
|
||||
width = int(math.floor(planes * (baseWidth / 64.0)))
|
||||
self.conv1 = nn.Conv2d(in_planes, width * scale, kernel_size=1, stride=stride, bias=False)
|
||||
self.bn1 = nn.BatchNorm2d(width * scale)
|
||||
self.nums = scale
|
||||
self.expansion = expansion
|
||||
|
||||
convs = []
|
||||
bns = []
|
||||
for i in range(self.nums):
|
||||
convs.append(nn.Conv2d(width, width, kernel_size=3, padding=1, bias=False))
|
||||
bns.append(nn.BatchNorm2d(width))
|
||||
self.convs = nn.ModuleList(convs)
|
||||
self.bns = nn.ModuleList(bns)
|
||||
self.relu = ReLU(inplace=True)
|
||||
|
||||
self.conv3 = nn.Conv2d(width * scale, planes * self.expansion, kernel_size=1, bias=False)
|
||||
self.bn3 = nn.BatchNorm2d(planes * self.expansion)
|
||||
self.shortcut = nn.Sequential()
|
||||
if stride != 1 or in_planes != self.expansion * planes:
|
||||
self.shortcut = nn.Sequential(
|
||||
nn.Conv2d(in_planes, self.expansion * planes, kernel_size=1, stride=stride, bias=False),
|
||||
nn.BatchNorm2d(self.expansion * planes),
|
||||
)
|
||||
self.stride = stride
|
||||
self.width = width
|
||||
self.scale = scale
|
||||
|
||||
def forward(self, x):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
residual = x
|
||||
|
||||
out = self.conv1(x)
|
||||
out = self.bn1(out)
|
||||
out = self.relu(out)
|
||||
spx = torch.split(out, self.width, 1)
|
||||
for i in range(self.nums):
|
||||
if i == 0:
|
||||
sp = spx[i]
|
||||
else:
|
||||
sp = sp + spx[i]
|
||||
sp = self.convs[i](sp)
|
||||
sp = self.relu(self.bns[i](sp))
|
||||
if i == 0:
|
||||
out = sp
|
||||
else:
|
||||
out = torch.cat((out, sp), 1)
|
||||
|
||||
out = self.conv3(out)
|
||||
out = self.bn3(out)
|
||||
|
||||
residual = self.shortcut(x)
|
||||
out += residual
|
||||
out = self.relu(out)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
class BasicBlockERes2NetV2AFF(nn.Module):
|
||||
|
||||
def __init__(self, in_planes, planes, stride=1, baseWidth=26, scale=2, expansion=2):
|
||||
"""Initialize BasicBlockERes2NetV2AFF.
|
||||
|
||||
Args:
|
||||
in_planes: TODO.
|
||||
planes: TODO.
|
||||
stride: TODO.
|
||||
baseWidth: TODO.
|
||||
scale: TODO.
|
||||
expansion: TODO.
|
||||
"""
|
||||
super(BasicBlockERes2NetV2AFF, self).__init__()
|
||||
width = int(math.floor(planes * (baseWidth / 64.0)))
|
||||
self.conv1 = nn.Conv2d(in_planes, width * scale, kernel_size=1, stride=stride, bias=False)
|
||||
self.bn1 = nn.BatchNorm2d(width * scale)
|
||||
self.nums = scale
|
||||
self.expansion = expansion
|
||||
|
||||
convs = []
|
||||
fuse_models = []
|
||||
bns = []
|
||||
for i in range(self.nums):
|
||||
convs.append(nn.Conv2d(width, width, kernel_size=3, padding=1, bias=False))
|
||||
bns.append(nn.BatchNorm2d(width))
|
||||
for j in range(self.nums - 1):
|
||||
fuse_models.append(AFF(channels=width, r=4))
|
||||
|
||||
self.convs = nn.ModuleList(convs)
|
||||
self.bns = nn.ModuleList(bns)
|
||||
self.fuse_models = nn.ModuleList(fuse_models)
|
||||
self.relu = ReLU(inplace=True)
|
||||
|
||||
self.conv3 = nn.Conv2d(width * scale, planes * self.expansion, kernel_size=1, bias=False)
|
||||
self.bn3 = nn.BatchNorm2d(planes * self.expansion)
|
||||
self.shortcut = nn.Sequential()
|
||||
if stride != 1 or in_planes != self.expansion * planes:
|
||||
self.shortcut = nn.Sequential(
|
||||
nn.Conv2d(in_planes, self.expansion * planes, kernel_size=1, stride=stride, bias=False),
|
||||
nn.BatchNorm2d(self.expansion * planes),
|
||||
)
|
||||
self.stride = stride
|
||||
self.width = width
|
||||
self.scale = scale
|
||||
|
||||
def forward(self, x):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
residual = x
|
||||
|
||||
out = self.conv1(x)
|
||||
out = self.bn1(out)
|
||||
out = self.relu(out)
|
||||
spx = torch.split(out, self.width, 1)
|
||||
for i in range(self.nums):
|
||||
if i == 0:
|
||||
sp = spx[i]
|
||||
else:
|
||||
sp = self.fuse_models[i - 1](sp, spx[i])
|
||||
sp = self.convs[i](sp)
|
||||
sp = self.relu(self.bns[i](sp))
|
||||
if i == 0:
|
||||
out = sp
|
||||
else:
|
||||
out = torch.cat((out, sp), 1)
|
||||
|
||||
out = self.conv3(out)
|
||||
out = self.bn3(out)
|
||||
|
||||
residual = self.shortcut(x)
|
||||
out += residual
|
||||
out = self.relu(out)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
class ERes2NetV2(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
block=BasicBlockERes2NetV2,
|
||||
block_fuse=BasicBlockERes2NetV2AFF,
|
||||
num_blocks=[3, 4, 6, 3],
|
||||
m_channels=64,
|
||||
feat_dim=80,
|
||||
embedding_size=192,
|
||||
baseWidth=26,
|
||||
scale=2,
|
||||
expansion=2,
|
||||
pooling_func="TSTP",
|
||||
two_emb_layer=False,
|
||||
):
|
||||
"""Initialize ERes2NetV2.
|
||||
|
||||
Args:
|
||||
block: TODO.
|
||||
block_fuse: TODO.
|
||||
num_blocks: TODO.
|
||||
m_channels: TODO.
|
||||
feat_dim: Size/dimension parameter.
|
||||
embedding_size: Size/dimension parameter.
|
||||
baseWidth: TODO.
|
||||
scale: TODO.
|
||||
expansion: TODO.
|
||||
pooling_func: TODO.
|
||||
two_emb_layer: TODO.
|
||||
"""
|
||||
super(ERes2NetV2, self).__init__()
|
||||
self.in_planes = m_channels
|
||||
self.feat_dim = feat_dim
|
||||
self.embedding_size = embedding_size
|
||||
self.stats_dim = int(feat_dim / 8) * m_channels * 8
|
||||
self.two_emb_layer = two_emb_layer
|
||||
self.baseWidth = baseWidth
|
||||
self.scale = scale
|
||||
self.expansion = expansion
|
||||
|
||||
self.conv1 = nn.Conv2d(1, m_channels, kernel_size=3, stride=1, padding=1, bias=False)
|
||||
self.bn1 = nn.BatchNorm2d(m_channels)
|
||||
self.layer1 = self._make_layer(block, m_channels, num_blocks[0], stride=1)
|
||||
self.layer2 = self._make_layer(block, m_channels * 2, num_blocks[1], stride=2)
|
||||
self.layer3 = self._make_layer(block_fuse, m_channels * 4, num_blocks[2], stride=2)
|
||||
self.layer4 = self._make_layer(block_fuse, m_channels * 8, num_blocks[3], stride=2)
|
||||
|
||||
self.layer3_ds = nn.Conv2d(
|
||||
m_channels * 4 * self.expansion, m_channels * 8 * self.expansion,
|
||||
kernel_size=3, padding=1, stride=2, bias=False,
|
||||
)
|
||||
self.fuse34 = AFF(channels=m_channels * 8 * self.expansion, r=4)
|
||||
|
||||
self.n_stats = 1 if pooling_func == "TAP" or pooling_func == "TSDP" else 2
|
||||
self.pool = getattr(pooling_layers, pooling_func)(in_dim=self.stats_dim * self.expansion)
|
||||
self.seg_1 = nn.Linear(self.stats_dim * self.expansion * self.n_stats, embedding_size)
|
||||
if self.two_emb_layer:
|
||||
self.seg_bn_1 = nn.BatchNorm1d(embedding_size, affine=False)
|
||||
self.seg_2 = nn.Linear(embedding_size, embedding_size)
|
||||
else:
|
||||
self.seg_bn_1 = nn.Identity()
|
||||
self.seg_2 = nn.Identity()
|
||||
|
||||
def _make_layer(self, block, planes, num_blocks, stride):
|
||||
"""Internal: make layer.
|
||||
|
||||
Args:
|
||||
block: TODO.
|
||||
planes: TODO.
|
||||
num_blocks: TODO.
|
||||
stride: TODO.
|
||||
"""
|
||||
strides = [stride] + [1] * (num_blocks - 1)
|
||||
layers = []
|
||||
for stride in strides:
|
||||
layers.append(
|
||||
block(self.in_planes, planes, stride, baseWidth=self.baseWidth, scale=self.scale, expansion=self.expansion)
|
||||
)
|
||||
self.in_planes = planes * self.expansion
|
||||
return nn.Sequential(*layers)
|
||||
|
||||
def forward(self, x):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
x = x.permute(0, 2, 1) # (B,T,F) => (B,F,T)
|
||||
x = x.unsqueeze_(1)
|
||||
out = F.relu(self.bn1(self.conv1(x)))
|
||||
out1 = self.layer1(out)
|
||||
out2 = self.layer2(out1)
|
||||
out3 = self.layer3(out2)
|
||||
out4 = self.layer4(out3)
|
||||
out3_ds = self.layer3_ds(out3)
|
||||
fuse_out34 = self.fuse34(out4, out3_ds)
|
||||
stats = self.pool(fuse_out34)
|
||||
|
||||
embed_a = self.seg_1(stats)
|
||||
if self.two_emb_layer:
|
||||
out = F.relu(embed_a)
|
||||
out = self.seg_bn_1(out)
|
||||
embed_b = self.seg_2(out)
|
||||
return embed_b
|
||||
else:
|
||||
return embed_a
|
||||
@@ -0,0 +1,40 @@
|
||||
# Copyright 3D-Speaker (https://github.com/alibaba-damo-academy/3D-Speaker). All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
|
||||
class AFF(nn.Module):
|
||||
|
||||
def __init__(self, channels=64, r=4):
|
||||
"""Initialize AFF.
|
||||
|
||||
Args:
|
||||
channels: TODO.
|
||||
r: TODO.
|
||||
"""
|
||||
super(AFF, self).__init__()
|
||||
inter_channels = int(channels // r)
|
||||
|
||||
self.local_att = nn.Sequential(
|
||||
nn.Conv2d(channels * 2, inter_channels, kernel_size=1, stride=1, padding=0),
|
||||
nn.BatchNorm2d(inter_channels),
|
||||
nn.SiLU(inplace=True),
|
||||
nn.Conv2d(inter_channels, channels, kernel_size=1, stride=1, padding=0),
|
||||
nn.BatchNorm2d(channels),
|
||||
)
|
||||
|
||||
def forward(self, x, ds_y):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
ds_y: TODO.
|
||||
"""
|
||||
xa = torch.cat((x, ds_y), dim=1)
|
||||
x_att = self.local_att(xa)
|
||||
x_att = 1.0 + torch.tanh(x_att)
|
||||
xo = torch.mul(x, x_att) + torch.mul(ds_y, 2.0 - x_att)
|
||||
|
||||
return xo
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
# Modified from 3D-Speaker (https://github.com/alibaba-damo-academy/3D-Speaker)
|
||||
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
import torch
|
||||
import numpy as np
|
||||
|
||||
from funasr.register import tables
|
||||
from funasr.models.campplus.utils import extract_feature
|
||||
from funasr.utils.load_utils import load_audio_text_image_video
|
||||
from funasr.models.eres2net.eres2netv2 import ERes2NetV2
|
||||
|
||||
|
||||
@tables.register("model_classes", "ERes2NetV2")
|
||||
@tables.register("model_classes", "iic/speech_eres2netv2_sv_zh-cn_16k-common")
|
||||
class ERes2NetV2SV(torch.nn.Module):
|
||||
"""ERes2NetV2: Enhanced Res2Net v2 for Speaker Verification.
|
||||
|
||||
Improved speaker embedding model based on Res2Net architecture with
|
||||
multi-scale feature aggregation. Provides 192-dim speaker embeddings
|
||||
for speaker verification and diarization.
|
||||
|
||||
Better than CAM++ for short-duration audio (< 3s) speaker feature extraction.
|
||||
|
||||
Output: {"spk_embedding": Tensor of shape (1, 192)}
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
feat_dim=80,
|
||||
embedding_size=192,
|
||||
m_channels=64,
|
||||
baseWidth=26,
|
||||
scale=2,
|
||||
expansion=2,
|
||||
num_blocks=[3, 4, 6, 3],
|
||||
pooling_func="TSTP",
|
||||
two_emb_layer=False,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize ERes2NetV2SV.
|
||||
|
||||
Args:
|
||||
feat_dim: Size/dimension parameter.
|
||||
embedding_size: Size/dimension parameter.
|
||||
m_channels: TODO.
|
||||
baseWidth: TODO.
|
||||
scale: TODO.
|
||||
expansion: TODO.
|
||||
num_blocks: TODO.
|
||||
pooling_func: TODO.
|
||||
two_emb_layer: TODO.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__()
|
||||
self.model = ERes2NetV2(
|
||||
feat_dim=feat_dim,
|
||||
embedding_size=embedding_size,
|
||||
m_channels=m_channels,
|
||||
baseWidth=baseWidth,
|
||||
scale=scale,
|
||||
expansion=expansion,
|
||||
num_blocks=num_blocks,
|
||||
pooling_func=pooling_func,
|
||||
two_emb_layer=two_emb_layer,
|
||||
)
|
||||
self.embedding_size = embedding_size
|
||||
|
||||
model_path = kwargs.get("model_path", None)
|
||||
init_param = kwargs.get("init_param", None)
|
||||
if init_param is None and model_path is not None:
|
||||
ckpt = os.path.join(model_path, "pretrained_eres2netv2.ckpt")
|
||||
if os.path.exists(ckpt):
|
||||
init_param = ckpt
|
||||
if init_param is not None and os.path.exists(init_param):
|
||||
self._load_pretrained(init_param)
|
||||
|
||||
def _load_pretrained(self, path):
|
||||
"""Internal: load pretrained.
|
||||
|
||||
Args:
|
||||
path: TODO.
|
||||
"""
|
||||
state_dict = torch.load(path, map_location="cpu")
|
||||
if "state_dict" in state_dict:
|
||||
state_dict = state_dict["state_dict"]
|
||||
missing, unexpected = self.model.load_state_dict(state_dict, strict=False)
|
||||
if missing:
|
||||
logging.warning(f"ERes2NetV2 missing keys: {missing[:5]}...")
|
||||
logging.info(f"ERes2NetV2 loaded pretrained weights from {path}")
|
||||
|
||||
def forward(self, x):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
x: TODO.
|
||||
"""
|
||||
return self.model(x)
|
||||
|
||||
def inference(
|
||||
self,
|
||||
data_in,
|
||||
data_lengths=None,
|
||||
key: list = None,
|
||||
tokenizer=None,
|
||||
frontend=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Run inference on input data.
|
||||
|
||||
Args:
|
||||
data_in: Input data (audio samples, file paths, or text).
|
||||
data_lengths: Lengths of each input sample in the batch.
|
||||
key: Sample identifiers.
|
||||
tokenizer: Tokenizer instance for text encoding/decoding.
|
||||
frontend: Audio frontend for feature extraction.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
meta_data = {}
|
||||
time1 = time.perf_counter()
|
||||
audio_sample_list = load_audio_text_image_video(
|
||||
data_in, fs=16000, audio_fs=kwargs.get("fs", 16000), data_type="sound"
|
||||
)
|
||||
time2 = time.perf_counter()
|
||||
meta_data["load_data"] = f"{time2 - time1:0.3f}"
|
||||
speech, speech_lengths, speech_times = extract_feature(audio_sample_list)
|
||||
speech = speech.to(device=kwargs["device"])
|
||||
time3 = time.perf_counter()
|
||||
meta_data["extract_feat"] = f"{time3 - time2:0.3f}"
|
||||
meta_data["batch_data_time"] = np.array(speech_times).sum().item() / 16000.0
|
||||
results = [{"spk_embedding": self.forward(speech.to(torch.float32))}]
|
||||
return results, meta_data
|
||||
Executable
+686
@@ -0,0 +1,686 @@
|
||||
from typing import Tuple, Dict
|
||||
import copy
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from funasr.register import tables
|
||||
|
||||
|
||||
def toKaldiMatrix(np_mat):
|
||||
"""Tokaldimatrix.
|
||||
|
||||
Args:
|
||||
np_mat: TODO.
|
||||
"""
|
||||
np.set_printoptions(threshold=np.inf, linewidth=np.nan)
|
||||
out_str = str(np_mat)
|
||||
out_str = out_str.replace('[', '')
|
||||
out_str = out_str.replace(']', '')
|
||||
return '[ %s ]\n' % out_str
|
||||
|
||||
|
||||
class LinearTransform(nn.Module):
|
||||
|
||||
def __init__(self, input_dim, output_dim):
|
||||
"""Initialize LinearTransform.
|
||||
|
||||
Args:
|
||||
input_dim: Size/dimension parameter.
|
||||
output_dim: Size/dimension parameter.
|
||||
"""
|
||||
super(LinearTransform, self).__init__()
|
||||
self.input_dim = input_dim
|
||||
self.output_dim = output_dim
|
||||
self.linear = nn.Linear(input_dim, output_dim, bias=False)
|
||||
|
||||
def forward(self, input):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
input: Input audio/text data.
|
||||
"""
|
||||
output = self.linear(input)
|
||||
|
||||
return output
|
||||
|
||||
def to_kaldi_net(self):
|
||||
"""To kaldi net."""
|
||||
re_str = ''
|
||||
re_str += '<LinearTransform> %d %d\n' % (self.output_dim,
|
||||
self.input_dim)
|
||||
re_str += '<LearnRateCoef> 1\n'
|
||||
|
||||
linear_weights = self.state_dict()['linear.weight']
|
||||
x = linear_weights.squeeze().numpy()
|
||||
re_str += toKaldiMatrix(x)
|
||||
|
||||
return re_str
|
||||
|
||||
def to_pytorch_net(self, fread):
|
||||
"""To pytorch net.
|
||||
|
||||
Args:
|
||||
fread: TODO.
|
||||
"""
|
||||
linear_line = fread.readline()
|
||||
linear_split = linear_line.strip().split()
|
||||
assert len(linear_split) == 3
|
||||
assert linear_split[0] == '<LinearTransform>'
|
||||
self.output_dim = int(linear_split[1])
|
||||
self.input_dim = int(linear_split[2])
|
||||
|
||||
learn_rate_line = fread.readline()
|
||||
assert learn_rate_line.find('LearnRateCoef') != -1
|
||||
|
||||
self.linear.reset_parameters()
|
||||
|
||||
linear_weights = self.state_dict()['linear.weight']
|
||||
#print(linear_weights.shape)
|
||||
new_weights = torch.zeros((self.output_dim, self.input_dim),
|
||||
dtype=torch.float32)
|
||||
for i in range(self.output_dim):
|
||||
line = fread.readline()
|
||||
splits = line.strip().strip('\[\]').strip().split()
|
||||
assert len(splits) == self.input_dim
|
||||
cols = torch.tensor([float(item) for item in splits],
|
||||
dtype=torch.float32)
|
||||
new_weights[i, :] = cols
|
||||
|
||||
self.linear.weight.data = new_weights
|
||||
|
||||
|
||||
class AffineTransform(nn.Module):
|
||||
|
||||
def __init__(self, input_dim, output_dim):
|
||||
"""Initialize AffineTransform.
|
||||
|
||||
Args:
|
||||
input_dim: Size/dimension parameter.
|
||||
output_dim: Size/dimension parameter.
|
||||
"""
|
||||
super(AffineTransform, self).__init__()
|
||||
self.input_dim = input_dim
|
||||
self.output_dim = output_dim
|
||||
self.linear = nn.Linear(input_dim, output_dim)
|
||||
|
||||
def forward(self, input):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
input: Input audio/text data.
|
||||
"""
|
||||
output = self.linear(input)
|
||||
|
||||
return output
|
||||
|
||||
def to_kaldi_net(self):
|
||||
"""To kaldi net."""
|
||||
re_str = ''
|
||||
re_str += '<AffineTransform> %d %d\n' % (self.output_dim,
|
||||
self.input_dim)
|
||||
re_str += '<LearnRateCoef> 1 <BiasLearnRateCoef> 1 <MaxNorm> 0\n'
|
||||
|
||||
linear_weights = self.state_dict()['linear.weight']
|
||||
x = linear_weights.squeeze().numpy()
|
||||
re_str += toKaldiMatrix(x)
|
||||
|
||||
linear_bias = self.state_dict()['linear.bias']
|
||||
x = linear_bias.squeeze().numpy()
|
||||
re_str += toKaldiMatrix(x)
|
||||
|
||||
return re_str
|
||||
|
||||
def to_pytorch_net(self, fread):
|
||||
"""To pytorch net.
|
||||
|
||||
Args:
|
||||
fread: TODO.
|
||||
"""
|
||||
affine_line = fread.readline()
|
||||
affine_split = affine_line.strip().split()
|
||||
assert len(affine_split) == 3
|
||||
assert affine_split[0] == '<AffineTransform>'
|
||||
self.output_dim = int(affine_split[1])
|
||||
self.input_dim = int(affine_split[2])
|
||||
print('AffineTransform output/input dim: %d %d' %
|
||||
(self.output_dim, self.input_dim))
|
||||
|
||||
learn_rate_line = fread.readline()
|
||||
assert learn_rate_line.find('LearnRateCoef') != -1
|
||||
|
||||
#linear_weights = self.state_dict()['linear.weight']
|
||||
#print(linear_weights.shape)
|
||||
self.linear.reset_parameters()
|
||||
|
||||
new_weights = torch.zeros((self.output_dim, self.input_dim),
|
||||
dtype=torch.float32)
|
||||
for i in range(self.output_dim):
|
||||
line = fread.readline()
|
||||
splits = line.strip().strip('\[\]').strip().split()
|
||||
assert len(splits) == self.input_dim
|
||||
cols = torch.tensor([float(item) for item in splits],
|
||||
dtype=torch.float32)
|
||||
new_weights[i, :] = cols
|
||||
|
||||
self.linear.weight.data = new_weights
|
||||
|
||||
linear_bias = self.state_dict()['linear.bias']
|
||||
#print(linear_bias.shape)
|
||||
bias_line = fread.readline()
|
||||
splits = bias_line.strip().strip('\[\]').strip().split()
|
||||
assert len(splits) == self.output_dim
|
||||
new_bias = torch.tensor([float(item) for item in splits],
|
||||
dtype=torch.float32)
|
||||
|
||||
self.linear.bias.data = new_bias
|
||||
|
||||
|
||||
class RectifiedLinear(nn.Module):
|
||||
|
||||
def __init__(self, input_dim, output_dim):
|
||||
"""Initialize RectifiedLinear.
|
||||
|
||||
Args:
|
||||
input_dim: Size/dimension parameter.
|
||||
output_dim: Size/dimension parameter.
|
||||
"""
|
||||
super(RectifiedLinear, self).__init__()
|
||||
self.dim = input_dim
|
||||
self.relu = nn.ReLU()
|
||||
self.dropout = nn.Dropout(0.1)
|
||||
|
||||
def forward(self, input):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
input: Input audio/text data.
|
||||
"""
|
||||
out = self.relu(input)
|
||||
return out
|
||||
|
||||
def to_kaldi_net(self):
|
||||
"""To kaldi net."""
|
||||
re_str = ''
|
||||
re_str += '<RectifiedLinear> %d %d\n' % (self.dim, self.dim)
|
||||
return re_str
|
||||
|
||||
def to_pytorch_net(self, fread):
|
||||
"""To pytorch net.
|
||||
|
||||
Args:
|
||||
fread: TODO.
|
||||
"""
|
||||
line = fread.readline()
|
||||
splits = line.strip().split()
|
||||
assert len(splits) == 3
|
||||
assert splits[0] == '<RectifiedLinear>'
|
||||
assert int(splits[1]) == int(splits[2])
|
||||
assert int(splits[1]) == self.dim
|
||||
self.dim = int(splits[1])
|
||||
|
||||
|
||||
class FSMNBlock(nn.Module):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_dim: int,
|
||||
output_dim: int,
|
||||
lorder=None,
|
||||
rorder=None,
|
||||
lstride=1,
|
||||
rstride=1,
|
||||
):
|
||||
"""Initialize FSMNBlock.
|
||||
|
||||
Args:
|
||||
input_dim: Size/dimension parameter.
|
||||
output_dim: Size/dimension parameter.
|
||||
lorder: TODO.
|
||||
rorder: TODO.
|
||||
lstride: TODO.
|
||||
rstride: TODO.
|
||||
"""
|
||||
super(FSMNBlock, self).__init__()
|
||||
|
||||
self.dim = input_dim
|
||||
|
||||
if lorder is None:
|
||||
return
|
||||
|
||||
self.lorder = lorder
|
||||
self.rorder = rorder
|
||||
self.lstride = lstride
|
||||
self.rstride = rstride
|
||||
|
||||
self.conv_left = nn.Conv2d(
|
||||
self.dim, self.dim, [lorder, 1], dilation=[lstride, 1], groups=self.dim, bias=False
|
||||
)
|
||||
|
||||
if self.rorder > 0:
|
||||
self.conv_right = nn.Conv2d(
|
||||
self.dim, self.dim, [rorder, 1], dilation=[rstride, 1], groups=self.dim, bias=False
|
||||
)
|
||||
else:
|
||||
self.conv_right = None
|
||||
|
||||
def forward(self, input: torch.Tensor, cache: torch.Tensor = None):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
input: Input audio/text data.
|
||||
cache: State cache dict for streaming inference.
|
||||
"""
|
||||
x = torch.unsqueeze(input, 1)
|
||||
x_per = x.permute(0, 3, 2, 1) # B D T C
|
||||
|
||||
if cache is not None:
|
||||
cache = cache.to(x_per.device)
|
||||
y_left = torch.cat((cache, x_per), dim=2)
|
||||
cache = y_left[:, :, -(self.lorder - 1) * self.lstride :, :]
|
||||
else:
|
||||
y_left = F.pad(x_per, [0, 0, (self.lorder - 1) * self.lstride, 0])
|
||||
|
||||
y_left = self.conv_left(y_left)
|
||||
out = x_per + y_left
|
||||
|
||||
if self.conv_right is not None:
|
||||
# maybe need to check
|
||||
y_right = F.pad(x_per, [0, 0, 0, self.rorder * self.rstride])
|
||||
y_right = y_right[:, :, self.rstride :, :]
|
||||
y_right = self.conv_right(y_right)
|
||||
out += y_right
|
||||
|
||||
out_per = out.permute(0, 3, 2, 1)
|
||||
output = out_per.squeeze(1)
|
||||
|
||||
return output, cache
|
||||
|
||||
def to_kaldi_net(self):
|
||||
"""To kaldi net."""
|
||||
re_str = ''
|
||||
re_str += '<Fsmn> %d %d\n' % (self.dim, self.dim)
|
||||
re_str += '<LearnRateCoef> %d <LOrder> %d <ROrder> %d <LStride> %d <RStride> %d <MaxNorm> 0\n' % (
|
||||
1, self.lorder, self.rorder, self.lstride, self.rstride)
|
||||
|
||||
#print(self.conv_left.weight,self.conv_right.weight)
|
||||
lfiters = self.state_dict()['conv_left.weight']
|
||||
x = np.flipud(lfiters.squeeze().numpy().T)
|
||||
re_str += toKaldiMatrix(x)
|
||||
|
||||
if self.conv_right is not None:
|
||||
rfiters = self.state_dict()['conv_right.weight']
|
||||
x = (rfiters.squeeze().numpy().T)
|
||||
re_str += toKaldiMatrix(x)
|
||||
|
||||
return re_str
|
||||
|
||||
def to_pytorch_net(self, fread):
|
||||
"""To pytorch net.
|
||||
|
||||
Args:
|
||||
fread: TODO.
|
||||
"""
|
||||
fsmn_line = fread.readline()
|
||||
fsmn_split = fsmn_line.strip().split()
|
||||
assert len(fsmn_split) == 3
|
||||
assert fsmn_split[0] == '<Fsmn>'
|
||||
self.dim = int(fsmn_split[1])
|
||||
|
||||
params_line = fread.readline()
|
||||
params_split = params_line.strip().strip('\[\]').strip().split()
|
||||
assert len(params_split) == 12
|
||||
assert params_split[0] == '<LearnRateCoef>'
|
||||
assert params_split[2] == '<LOrder>'
|
||||
self.lorder = int(params_split[3])
|
||||
assert params_split[4] == '<ROrder>'
|
||||
self.rorder = int(params_split[5])
|
||||
assert params_split[6] == '<LStride>'
|
||||
self.lstride = int(params_split[7])
|
||||
assert params_split[8] == '<RStride>'
|
||||
self.rstride = int(params_split[9])
|
||||
assert params_split[10] == '<MaxNorm>'
|
||||
|
||||
#lfilters = self.state_dict()['conv_left.weight']
|
||||
#print(lfilters.shape)
|
||||
print('read conv_left weight')
|
||||
new_lfilters = torch.zeros((self.lorder, 1, self.dim, 1),
|
||||
dtype=torch.float32)
|
||||
for i in range(self.lorder):
|
||||
print('read conv_left weight -- %d' % i)
|
||||
line = fread.readline()
|
||||
splits = line.strip().strip('\[\]').strip().split()
|
||||
assert len(splits) == self.dim
|
||||
cols = torch.tensor([float(item) for item in splits],
|
||||
dtype=torch.float32)
|
||||
new_lfilters[self.lorder - 1 - i, 0, :, 0] = cols
|
||||
|
||||
new_lfilters = torch.transpose(new_lfilters, 0, 2)
|
||||
#print(new_lfilters.shape)
|
||||
|
||||
self.conv_left.reset_parameters()
|
||||
self.conv_left.weight.data = new_lfilters
|
||||
#print(self.conv_left.weight.shape)
|
||||
|
||||
if self.rorder > 0:
|
||||
#rfilters = self.state_dict()['conv_right.weight']
|
||||
#print(rfilters.shape)
|
||||
print('read conv_right weight')
|
||||
new_rfilters = torch.zeros((self.rorder, 1, self.dim, 1),
|
||||
dtype=torch.float32)
|
||||
line = fread.readline()
|
||||
for i in range(self.rorder):
|
||||
print('read conv_right weight -- %d' % i)
|
||||
line = fread.readline()
|
||||
splits = line.strip().strip('\[\]').strip().split()
|
||||
assert len(splits) == self.dim
|
||||
cols = torch.tensor([float(item) for item in splits],
|
||||
dtype=torch.float32)
|
||||
new_rfilters[i, 0, :, 0] = cols
|
||||
|
||||
new_rfilters = torch.transpose(new_rfilters, 0, 2)
|
||||
#print(new_rfilters.shape)
|
||||
self.conv_right.reset_parameters()
|
||||
self.conv_right.weight.data = new_rfilters
|
||||
#print(self.conv_right.weight.shape)
|
||||
|
||||
class BasicBlock(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
linear_dim: int,
|
||||
proj_dim: int,
|
||||
lorder: int,
|
||||
rorder: int,
|
||||
lstride: int,
|
||||
rstride: int,
|
||||
stack_layer: int,
|
||||
):
|
||||
"""Initialize BasicBlock.
|
||||
|
||||
Args:
|
||||
linear_dim: Size/dimension parameter.
|
||||
proj_dim: Size/dimension parameter.
|
||||
lorder: TODO.
|
||||
rorder: TODO.
|
||||
lstride: TODO.
|
||||
rstride: TODO.
|
||||
stack_layer: TODO.
|
||||
"""
|
||||
super(BasicBlock, self).__init__()
|
||||
self.lorder = lorder
|
||||
self.rorder = rorder
|
||||
self.lstride = lstride
|
||||
self.rstride = rstride
|
||||
self.stack_layer = stack_layer
|
||||
self.linear = LinearTransform(linear_dim, proj_dim)
|
||||
self.fsmn_block = FSMNBlock(proj_dim, proj_dim, lorder, rorder, lstride, rstride)
|
||||
self.affine = AffineTransform(proj_dim, linear_dim)
|
||||
self.relu = RectifiedLinear(linear_dim, linear_dim)
|
||||
|
||||
def forward(self, input: torch.Tensor, cache: Dict[str, torch.Tensor] = None):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
input: Input audio/text data.
|
||||
cache: State cache dict for streaming inference.
|
||||
"""
|
||||
x1 = self.linear(input) # B T D
|
||||
|
||||
if cache is not None:
|
||||
cache_layer_name = 'cache_layer_{}'.format(self.stack_layer)
|
||||
if cache_layer_name not in cache:
|
||||
cache[cache_layer_name] = torch.zeros(
|
||||
x1.shape[0], x1.shape[-1], (self.lorder - 1) * self.lstride, 1
|
||||
)
|
||||
x2, cache[cache_layer_name] = self.fsmn_block(x1, cache[cache_layer_name])
|
||||
else:
|
||||
x2, _ = self.fsmn_block(x1, None)
|
||||
x3 = self.affine(x2)
|
||||
x4 = self.relu(x3)
|
||||
return x4
|
||||
|
||||
def to_kaldi_net(self):
|
||||
"""To kaldi net."""
|
||||
re_str = ''
|
||||
re_str += self.linear.to_kaldi_net()
|
||||
re_str += self.fsmn_block.to_kaldi_net()
|
||||
re_str += self.affine.to_kaldi_net()
|
||||
re_str += self.relu.to_kaldi_net()
|
||||
|
||||
return re_str
|
||||
|
||||
def to_pytorch_net(self, fread):
|
||||
"""To pytorch net.
|
||||
|
||||
Args:
|
||||
fread: TODO.
|
||||
"""
|
||||
self.linear.to_pytorch_net(fread)
|
||||
self.fsmn_block.to_pytorch_net(fread)
|
||||
self.affine.to_pytorch_net(fread)
|
||||
self.relu.to_pytorch_net(fread)
|
||||
|
||||
|
||||
class BasicBlock_export(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
model,
|
||||
):
|
||||
"""Initialize BasicBlock_export.
|
||||
|
||||
Args:
|
||||
model: Model instance or model name.
|
||||
"""
|
||||
super(BasicBlock_export, self).__init__()
|
||||
self.linear = model.linear
|
||||
self.fsmn_block = model.fsmn_block
|
||||
self.affine = model.affine
|
||||
self.relu = model.relu
|
||||
|
||||
def forward(self, input: torch.Tensor, in_cache: torch.Tensor):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
input: Input audio/text data.
|
||||
in_cache: TODO.
|
||||
"""
|
||||
x = self.linear(input) # B T D
|
||||
# cache_layer_name = 'cache_layer_{}'.format(self.stack_layer)
|
||||
# if cache_layer_name not in in_cache:
|
||||
# in_cache[cache_layer_name] = torch.zeros(x1.shape[0], x1.shape[-1], (self.lorder - 1) * self.lstride, 1)
|
||||
x, out_cache = self.fsmn_block(x, in_cache)
|
||||
x = self.affine(x)
|
||||
x = self.relu(x)
|
||||
return x, out_cache
|
||||
|
||||
|
||||
class FsmnStack(nn.Sequential):
|
||||
def __init__(self, *args):
|
||||
"""Initialize FsmnStack.
|
||||
|
||||
Args:
|
||||
*args: Variable positional arguments.
|
||||
"""
|
||||
super(FsmnStack, self).__init__(*args)
|
||||
|
||||
def forward(self, input: torch.Tensor, cache: Dict[str, torch.Tensor]):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
input: Input audio/text data.
|
||||
cache: State cache dict for streaming inference.
|
||||
"""
|
||||
x = input
|
||||
for module in self._modules.values():
|
||||
x = module(x, cache)
|
||||
return x
|
||||
|
||||
def to_kaldi_net(self):
|
||||
"""To kaldi net."""
|
||||
re_str = ''
|
||||
for module in self._modules.values():
|
||||
re_str += module.to_kaldi_net()
|
||||
|
||||
return re_str
|
||||
|
||||
def to_pytorch_net(self, fread):
|
||||
"""To pytorch net.
|
||||
|
||||
Args:
|
||||
fread: TODO.
|
||||
"""
|
||||
for module in self._modules.values():
|
||||
module.to_pytorch_net(fread)
|
||||
|
||||
|
||||
"""
|
||||
FSMN net for keyword spotting
|
||||
input_dim: input dimension
|
||||
linear_dim: fsmn input dimensionll
|
||||
proj_dim: fsmn projection dimension
|
||||
lorder: fsmn left order
|
||||
rorder: fsmn right order
|
||||
num_syn: output dimension
|
||||
fsmn_layers: no. of sequential fsmn layers
|
||||
"""
|
||||
|
||||
|
||||
@tables.register("encoder_classes", "FSMNConvert")
|
||||
class FSMNConvert(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
input_dim: int,
|
||||
input_affine_dim: int,
|
||||
fsmn_layers: int,
|
||||
linear_dim: int,
|
||||
proj_dim: int,
|
||||
lorder: int,
|
||||
rorder: int,
|
||||
lstride: int,
|
||||
rstride: int,
|
||||
output_affine_dim: int,
|
||||
output_dim: int,
|
||||
use_softmax: bool = True,
|
||||
):
|
||||
"""Initialize FSMNConvert.
|
||||
|
||||
Args:
|
||||
input_dim: Size/dimension parameter.
|
||||
input_affine_dim: Size/dimension parameter.
|
||||
fsmn_layers: TODO.
|
||||
linear_dim: Size/dimension parameter.
|
||||
proj_dim: Size/dimension parameter.
|
||||
lorder: TODO.
|
||||
rorder: TODO.
|
||||
lstride: TODO.
|
||||
rstride: TODO.
|
||||
output_affine_dim: Size/dimension parameter.
|
||||
output_dim: Size/dimension parameter.
|
||||
use_softmax: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.input_dim = input_dim
|
||||
self.input_affine_dim = input_affine_dim
|
||||
self.fsmn_layers = fsmn_layers
|
||||
self.linear_dim = linear_dim
|
||||
self.proj_dim = proj_dim
|
||||
self.output_affine_dim = output_affine_dim
|
||||
self.output_dim = output_dim
|
||||
|
||||
self.in_linear1 = AffineTransform(input_dim, input_affine_dim)
|
||||
self.in_linear2 = AffineTransform(input_affine_dim, linear_dim)
|
||||
self.relu = RectifiedLinear(linear_dim, linear_dim)
|
||||
self.fsmn = FsmnStack(
|
||||
*[
|
||||
BasicBlock(linear_dim, proj_dim, lorder, rorder, lstride, rstride, i)
|
||||
for i in range(fsmn_layers)
|
||||
]
|
||||
)
|
||||
self.out_linear1 = AffineTransform(linear_dim, output_affine_dim)
|
||||
self.out_linear2 = AffineTransform(output_affine_dim, output_dim)
|
||||
|
||||
self.use_softmax = use_softmax
|
||||
if self.use_softmax:
|
||||
self.softmax = nn.Softmax(dim=-1)
|
||||
|
||||
def output_size(self) -> int:
|
||||
"""Output size."""
|
||||
return self.output_dim
|
||||
|
||||
def forward(
|
||||
self,
|
||||
input: torch.Tensor,
|
||||
cache: Dict[str, torch.Tensor] = None
|
||||
) -> Tuple[torch.Tensor, Dict[str, torch.Tensor]]:
|
||||
"""
|
||||
Args:
|
||||
input (torch.Tensor): Input tensor (B, T, D)
|
||||
cache: when cache is not None, the forward is in streaming. The type of cache is a dict, egs,
|
||||
{'cache_layer_1': torch.Tensor(B, T1, D)}, T1 is equal to self.lorder. It is {} for the 1st frame
|
||||
"""
|
||||
|
||||
x1 = self.in_linear1(input)
|
||||
x2 = self.in_linear2(x1)
|
||||
x3 = self.relu(x2)
|
||||
x4 = self.fsmn(x3, cache) # self.cache will update automatically in self.fsmn
|
||||
x5 = self.out_linear1(x4)
|
||||
x6 = self.out_linear2(x5)
|
||||
|
||||
if self.use_softmax:
|
||||
x7 = self.softmax(x6)
|
||||
return x7
|
||||
|
||||
return x6
|
||||
|
||||
def to_kaldi_net(self):
|
||||
"""To kaldi net."""
|
||||
re_str = ''
|
||||
re_str += '<Nnet>\n'
|
||||
re_str += self.in_linear1.to_kaldi_net()
|
||||
re_str += self.in_linear2.to_kaldi_net()
|
||||
re_str += self.relu.to_kaldi_net()
|
||||
|
||||
for fsmn in self.fsmn:
|
||||
re_str += fsmn.to_kaldi_net()
|
||||
|
||||
re_str += self.out_linear1.to_kaldi_net()
|
||||
re_str += self.out_linear2.to_kaldi_net()
|
||||
re_str += '<Softmax> %d %d\n' % (self.output_dim, self.output_dim)
|
||||
re_str += '</Nnet>\n'
|
||||
|
||||
return re_str
|
||||
|
||||
def to_pytorch_net(self, kaldi_file):
|
||||
"""To pytorch net.
|
||||
|
||||
Args:
|
||||
kaldi_file: TODO.
|
||||
"""
|
||||
with open(kaldi_file, 'r', encoding='utf8') as fread:
|
||||
fread = open(kaldi_file, 'r')
|
||||
nnet_start_line = fread.readline()
|
||||
assert nnet_start_line.strip() == '<Nnet>'
|
||||
|
||||
self.in_linear1.to_pytorch_net(fread)
|
||||
self.in_linear2.to_pytorch_net(fread)
|
||||
self.relu.to_pytorch_net(fread)
|
||||
|
||||
for fsmn in self.fsmn:
|
||||
fsmn.to_pytorch_net(fread)
|
||||
|
||||
self.out_linear1.to_pytorch_net(fread)
|
||||
self.out_linear2.to_pytorch_net(fread)
|
||||
|
||||
softmax_line = fread.readline()
|
||||
softmax_split = softmax_line.strip().split()
|
||||
assert softmax_split[0].strip() == '<Softmax>'
|
||||
assert int(softmax_split[1]) == self.output_dim
|
||||
assert int(softmax_split[2]) == self.output_dim
|
||||
|
||||
nnet_end_line = fread.readline()
|
||||
assert nnet_end_line.strip() == '</Nnet>'
|
||||
fread.close()
|
||||
@@ -0,0 +1,342 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
import time
|
||||
import torch
|
||||
import logging
|
||||
from torch.cuda.amp import autocast
|
||||
from typing import Union, Dict, List, Tuple, Optional
|
||||
|
||||
from funasr.register import tables
|
||||
from funasr.models.ctc.ctc import CTC
|
||||
from funasr.utils import postprocess_utils
|
||||
from funasr.metrics.compute_acc import th_accuracy
|
||||
from funasr.utils.datadir_writer import DatadirWriter
|
||||
from funasr.models.paraformer.search import Hypothesis
|
||||
from funasr.models.paraformer.cif_predictor import mae_loss
|
||||
from funasr.train_utils.device_funcs import force_gatherable
|
||||
from funasr.losses.label_smoothing_loss import LabelSmoothingLoss
|
||||
from funasr.models.transformer.utils.add_sos_eos import add_sos_eos
|
||||
from funasr.models.transformer.utils.nets_utils import make_pad_mask, pad_list
|
||||
from funasr.utils.load_utils import load_audio_text_image_video, extract_fbank
|
||||
|
||||
|
||||
@tables.register("model_classes", "FsmnKWS")
|
||||
class FsmnKWS(torch.nn.Module):
|
||||
"""FSMN-KWS: Keyword Spotting model using FSMN architecture.
|
||||
|
||||
Detects predefined keywords/wake words in audio streams.
|
||||
Supports both offline and streaming operation.
|
||||
|
||||
Output: {"key": str, "value": detected_keyword_info}
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
specaug: Optional[str] = None,
|
||||
specaug_conf: Optional[Dict] = None,
|
||||
normalize: str = None,
|
||||
normalize_conf: Optional[Dict] = None,
|
||||
encoder: str = None,
|
||||
encoder_conf: Optional[Dict] = None,
|
||||
ctc: str = None,
|
||||
ctc_conf: Optional[Dict] = None,
|
||||
ctc_weight: float = 1.0,
|
||||
input_size: int = 360,
|
||||
vocab_size: int = -1,
|
||||
ignore_id: int = -1,
|
||||
blank_id: int = 0,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize FsmnKWS.
|
||||
|
||||
Args:
|
||||
specaug: TODO.
|
||||
specaug_conf: Configuration dict for specaug.
|
||||
normalize: TODO.
|
||||
normalize_conf: Configuration dict for normalize.
|
||||
encoder: TODO.
|
||||
encoder_conf: Configuration dict for encoder.
|
||||
ctc: TODO.
|
||||
ctc_conf: Configuration dict for ctc.
|
||||
ctc_weight: TODO.
|
||||
input_size: Size/dimension parameter.
|
||||
vocab_size: Size/dimension parameter.
|
||||
ignore_id: TODO.
|
||||
blank_id: TODO.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
if specaug is not None:
|
||||
specaug_class = tables.specaug_classes.get(specaug)
|
||||
specaug = specaug_class(**specaug_conf)
|
||||
|
||||
if normalize is not None:
|
||||
normalize_class = tables.normalize_classes.get(normalize)
|
||||
normalize = normalize_class(**normalize_conf)
|
||||
|
||||
encoder_class = tables.encoder_classes.get(encoder)
|
||||
encoder = encoder_class(**encoder_conf)
|
||||
encoder_output_size = encoder.output_size()
|
||||
|
||||
if ctc_conf is None:
|
||||
ctc_conf = {}
|
||||
ctc = CTC(
|
||||
odim=vocab_size, encoder_output_size=encoder_output_size, **ctc_conf
|
||||
)
|
||||
|
||||
self.blank_id = blank_id
|
||||
self.vocab_size = vocab_size
|
||||
self.ignore_id = ignore_id
|
||||
self.ctc_weight = ctc_weight
|
||||
|
||||
# self.frontend = frontend
|
||||
self.specaug = specaug
|
||||
self.normalize = normalize
|
||||
self.encoder = encoder
|
||||
self.ctc = ctc
|
||||
|
||||
self.error_calculator = None
|
||||
|
||||
def forward(
|
||||
self,
|
||||
speech: torch.Tensor,
|
||||
speech_lengths: torch.Tensor,
|
||||
text: torch.Tensor,
|
||||
text_lengths: torch.Tensor,
|
||||
**kwargs,
|
||||
) -> Tuple[torch.Tensor, Dict[str, torch.Tensor], torch.Tensor]:
|
||||
"""Encoder + Decoder + Calc loss
|
||||
Args:
|
||||
speech: (Batch, Length, ...)
|
||||
speech_lengths: (Batch, )
|
||||
text: (Batch, Length)
|
||||
text_lengths: (Batch,)
|
||||
"""
|
||||
if len(text_lengths.size()) > 1:
|
||||
text_lengths = text_lengths[:, 0]
|
||||
if len(speech_lengths.size()) > 1:
|
||||
speech_lengths = speech_lengths[:, 0]
|
||||
batch_size = speech.shape[0]
|
||||
|
||||
# Encoder
|
||||
encoder_out, encoder_out_lens = self.encode(speech, speech_lengths)
|
||||
loss_ctc, cer_ctc = self._calc_ctc_loss(
|
||||
encoder_out, encoder_out_lens, text, text_lengths
|
||||
)
|
||||
|
||||
# Collect CTC branch stats
|
||||
stats = dict()
|
||||
stats["loss_ctc"] = loss_ctc.detach() if loss_ctc is not None else None
|
||||
stats["cer_ctc"] = cer_ctc
|
||||
|
||||
loss = self.ctc_weight * loss_ctc
|
||||
|
||||
stats["cer"] = cer_ctc
|
||||
stats["loss"] = torch.clone(loss.detach())
|
||||
|
||||
# force_gatherable: to-device and to-tensor if scalar for DataParallel
|
||||
loss, stats, weight = force_gatherable((loss, stats, batch_size), loss.device)
|
||||
return loss, stats, weight
|
||||
|
||||
|
||||
def encode(
|
||||
self, speech: torch.Tensor, speech_lengths: torch.Tensor, **kwargs,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Encoder. Note that this method is used by asr_inference.py
|
||||
Args:
|
||||
speech: (Batch, Length, ...)
|
||||
speech_lengths: (Batch, )
|
||||
ind: int
|
||||
"""
|
||||
with autocast(False):
|
||||
# Data augmentation
|
||||
if self.specaug is not None and self.training:
|
||||
speech, speech_lengths = self.specaug(speech, speech_lengths)
|
||||
|
||||
# Normalization for feature: e.g. Global-CMVN, Utterance-CMVN
|
||||
if self.normalize is not None:
|
||||
speech, speech_lengths = self.normalize(speech, speech_lengths)
|
||||
|
||||
# Forward encoder
|
||||
encoder_out = self.encoder(speech)
|
||||
encoder_out_lens = speech_lengths
|
||||
|
||||
if isinstance(encoder_out, tuple):
|
||||
encoder_out = encoder_out[0]
|
||||
|
||||
return encoder_out, encoder_out_lens
|
||||
|
||||
def _calc_ctc_loss(
|
||||
self,
|
||||
encoder_out: torch.Tensor,
|
||||
encoder_out_lens: torch.Tensor,
|
||||
ys_pad: torch.Tensor,
|
||||
ys_pad_lens: torch.Tensor,
|
||||
):
|
||||
# Calc CTC loss
|
||||
"""Internal: calc ctc loss.
|
||||
|
||||
Args:
|
||||
encoder_out: Encoder output tensor.
|
||||
encoder_out_lens: Encoder output lengths.
|
||||
ys_pad: TODO.
|
||||
ys_pad_lens: Lengths of ys_pad.
|
||||
"""
|
||||
loss_ctc = self.ctc(encoder_out, encoder_out_lens, ys_pad, ys_pad_lens)
|
||||
|
||||
# Calc CER using CTC
|
||||
cer_ctc = None
|
||||
if not self.training and self.error_calculator is not None:
|
||||
ys_hat = self.ctc.argmax(encoder_out).data
|
||||
cer_ctc = self.error_calculator(ys_hat.cpu(), ys_pad.cpu(), is_ctc=True)
|
||||
|
||||
return loss_ctc, cer_ctc
|
||||
|
||||
def inference(
|
||||
self,
|
||||
data_in,
|
||||
data_lengths=None,
|
||||
key: list=None,
|
||||
tokenizer=None,
|
||||
frontend=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Run inference on input data.
|
||||
|
||||
Args:
|
||||
data_in: Input data (audio samples, file paths, or text).
|
||||
data_lengths: Lengths of each input sample in the batch.
|
||||
key: Sample identifiers.
|
||||
tokenizer: Tokenizer instance for text encoding/decoding.
|
||||
frontend: Audio frontend for feature extraction.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
keywords = kwargs.get("keywords")
|
||||
from funasr.utils.kws_utils import KwsCtcPrefixDecoder
|
||||
self.kws_decoder = KwsCtcPrefixDecoder(
|
||||
ctc=self.ctc,
|
||||
keywords=keywords,
|
||||
token_list=tokenizer.token_list,
|
||||
seg_dict=tokenizer.seg_dict,
|
||||
)
|
||||
|
||||
meta_data = {}
|
||||
if isinstance(data_in, torch.Tensor) and kwargs.get("data_type", "sound") == "fbank": # fbank
|
||||
speech, speech_lengths = data_in, data_lengths
|
||||
if len(speech.shape) < 3:
|
||||
speech = speech[None, :, :]
|
||||
if speech_lengths is not None:
|
||||
speech_lengths = speech_lengths.squeeze(-1)
|
||||
else:
|
||||
speech_lengths = speech.shape[1]
|
||||
else:
|
||||
# extract fbank feats
|
||||
time1 = time.perf_counter()
|
||||
audio_sample_list = load_audio_text_image_video(data_in, fs=frontend.fs, audio_fs=kwargs.get("fs", 16000), data_type=kwargs.get("data_type", "sound"), tokenizer=tokenizer)
|
||||
time2 = time.perf_counter()
|
||||
meta_data["load_data"] = f"{time2 - time1:0.3f}"
|
||||
speech, speech_lengths = extract_fbank(audio_sample_list, data_type=kwargs.get("data_type", "sound"), frontend=frontend)
|
||||
time3 = time.perf_counter()
|
||||
meta_data["extract_feat"] = f"{time3 - time2:0.3f}"
|
||||
meta_data["batch_data_time"] = speech_lengths.sum().item() * frontend.frame_shift * frontend.lfr_n / 1000
|
||||
|
||||
speech = speech.to(device=kwargs["device"])
|
||||
speech_lengths = speech_lengths.to(device=kwargs["device"])
|
||||
|
||||
# Encoder
|
||||
encoder_out, encoder_out_lens = self.encode(speech, speech_lengths)
|
||||
if isinstance(encoder_out, tuple):
|
||||
encoder_out = encoder_out[0]
|
||||
|
||||
results = []
|
||||
if kwargs.get("output_dir") is not None:
|
||||
if not hasattr(self, "writer"):
|
||||
self.writer = DatadirWriter(kwargs.get("output_dir"))
|
||||
|
||||
for i in range(encoder_out.size(0)):
|
||||
x = encoder_out[i, :encoder_out_lens[i], :]
|
||||
detect_result = self.kws_decoder.decode(x)
|
||||
is_deted, det_keyword, det_score = detect_result[0], detect_result[1], detect_result[2]
|
||||
|
||||
if is_deted:
|
||||
self.writer["detect"][key[i]] = "detected " + det_keyword + " " + str(det_score)
|
||||
det_info = "detected " + det_keyword + " " + str(det_score)
|
||||
else:
|
||||
self.writer["detect"][key[i]] = "rejected"
|
||||
det_info = "rejected"
|
||||
|
||||
result_i = {"key": key[i], "text": det_info}
|
||||
results.append(result_i)
|
||||
|
||||
return results, meta_data
|
||||
|
||||
|
||||
@tables.register("model_classes", "FsmnKWSConvert")
|
||||
class FsmnKWSConvert(torch.nn.Module):
|
||||
"""
|
||||
Author: Speech Lab of DAMO Academy, Alibaba Group
|
||||
Deep-FSMN for Large Vocabulary Continuous Speech Recognition
|
||||
https://arxiv.org/abs/1803.05030
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
encoder: str = None,
|
||||
encoder_conf: Optional[Dict] = None,
|
||||
ctc: str = None,
|
||||
ctc_conf: Optional[Dict] = None,
|
||||
ctc_weight: float = 1.0,
|
||||
input_size: int = 360,
|
||||
vocab_size: int = -1,
|
||||
blank_id: int = 0,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize FsmnKWSConvert.
|
||||
|
||||
Args:
|
||||
encoder: TODO.
|
||||
encoder_conf: Configuration dict for encoder.
|
||||
ctc: TODO.
|
||||
ctc_conf: Configuration dict for ctc.
|
||||
ctc_weight: TODO.
|
||||
input_size: Size/dimension parameter.
|
||||
vocab_size: Size/dimension parameter.
|
||||
blank_id: TODO.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
encoder_class = tables.encoder_classes.get(encoder)
|
||||
encoder = encoder_class(**encoder_conf)
|
||||
encoder_output_size = encoder.output_size()
|
||||
|
||||
if ctc_conf is None:
|
||||
ctc_conf = {}
|
||||
ctc = CTC(
|
||||
odim=vocab_size, encoder_output_size=encoder_output_size, **ctc_conf
|
||||
)
|
||||
|
||||
self.blank_id = blank_id
|
||||
self.vocab_size = vocab_size
|
||||
self.ctc_weight = ctc_weight
|
||||
self.encoder = encoder
|
||||
self.ctc = ctc
|
||||
|
||||
self.error_calculator = None
|
||||
|
||||
def to_kaldi_net(self):
|
||||
"""To kaldi net."""
|
||||
return self.encoder.to_kaldi_net()
|
||||
|
||||
|
||||
def to_pytorch_net(self, kaldi_file):
|
||||
"""To pytorch net.
|
||||
|
||||
Args:
|
||||
kaldi_file: TODO.
|
||||
"""
|
||||
return self.encoder.to_pytorch_net(kaldi_file)
|
||||
Executable
+258
@@ -0,0 +1,258 @@
|
||||
from typing import Tuple, Dict
|
||||
import copy
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from funasr.models.fsmn_kws.encoder import (toKaldiMatrix, LinearTransform, AffineTransform, RectifiedLinear, FSMNBlock, FsmnStack, BasicBlock)
|
||||
|
||||
|
||||
from funasr.register import tables
|
||||
|
||||
|
||||
'''
|
||||
FSMN net for keyword spotting
|
||||
input_dim: input dimension
|
||||
linear_dim: fsmn input dimensionll
|
||||
proj_dim: fsmn projection dimension
|
||||
lorder: fsmn left order
|
||||
rorder: fsmn right order
|
||||
num_syn: output dimension
|
||||
fsmn_layers: no. of sequential fsmn layers
|
||||
'''
|
||||
|
||||
@tables.register("encoder_classes", "FSMNMT")
|
||||
class FSMNMT(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
input_dim: int,
|
||||
input_affine_dim: int,
|
||||
fsmn_layers: int,
|
||||
linear_dim: int,
|
||||
proj_dim: int,
|
||||
lorder: int,
|
||||
rorder: int,
|
||||
lstride: int,
|
||||
rstride: int,
|
||||
output_affine_dim: int,
|
||||
output_dim: int,
|
||||
output_dim2: int,
|
||||
use_softmax: bool = True,
|
||||
):
|
||||
"""Initialize FSMNMT.
|
||||
|
||||
Args:
|
||||
input_dim: Size/dimension parameter.
|
||||
input_affine_dim: Size/dimension parameter.
|
||||
fsmn_layers: TODO.
|
||||
linear_dim: Size/dimension parameter.
|
||||
proj_dim: Size/dimension parameter.
|
||||
lorder: TODO.
|
||||
rorder: TODO.
|
||||
lstride: TODO.
|
||||
rstride: TODO.
|
||||
output_affine_dim: Size/dimension parameter.
|
||||
output_dim: Size/dimension parameter.
|
||||
output_dim2: TODO.
|
||||
use_softmax: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.input_dim = input_dim
|
||||
self.input_affine_dim = input_affine_dim
|
||||
self.fsmn_layers = fsmn_layers
|
||||
self.linear_dim = linear_dim
|
||||
self.proj_dim = proj_dim
|
||||
self.output_affine_dim = output_affine_dim
|
||||
self.output_dim = output_dim
|
||||
self.output_dim2 = output_dim2
|
||||
|
||||
self.in_linear1 = AffineTransform(input_dim, input_affine_dim)
|
||||
self.in_linear2 = AffineTransform(input_affine_dim, linear_dim)
|
||||
self.relu = RectifiedLinear(linear_dim, linear_dim)
|
||||
self.fsmn = FsmnStack(*[BasicBlock(linear_dim, proj_dim, lorder, rorder, lstride, rstride, i) for i in
|
||||
range(fsmn_layers)])
|
||||
self.out_linear1 = AffineTransform(linear_dim, output_affine_dim)
|
||||
self.out_linear1_2 = AffineTransform(linear_dim, output_affine_dim)
|
||||
self.out_linear2 = AffineTransform(output_affine_dim, output_dim)
|
||||
self.out_linear2_2 = AffineTransform(output_affine_dim, output_dim2)
|
||||
|
||||
self.use_softmax = use_softmax
|
||||
if self.use_softmax:
|
||||
self.softmax = nn.Softmax(dim=-1)
|
||||
|
||||
def output_size(self) -> int:
|
||||
"""Output size."""
|
||||
return self.output_dim
|
||||
|
||||
def output_size2(self) -> int:
|
||||
"""Output size2."""
|
||||
return self.output_dim2
|
||||
|
||||
def forward(
|
||||
self,
|
||||
input: torch.Tensor,
|
||||
cache: Dict[str, torch.Tensor] = None
|
||||
) -> Tuple[torch.Tensor, Dict[str, torch.Tensor]]:
|
||||
"""
|
||||
Args:
|
||||
input (torch.Tensor): Input tensor (B, T, D)
|
||||
cache: when cache is not None, the forward is in streaming. The type of cache is a dict, egs,
|
||||
{'cache_layer_1': torch.Tensor(B, T1, D)}, T1 is equal to self.lorder. It is {} for the 1st frame
|
||||
"""
|
||||
|
||||
x1 = self.in_linear1(input)
|
||||
x2 = self.in_linear2(x1)
|
||||
x3 = self.relu(x2)
|
||||
x4 = self.fsmn(x3, cache) # self.cache will update automatically in self.fsmn
|
||||
x5 = self.out_linear1(x4)
|
||||
x6 = self.out_linear2(x5)
|
||||
|
||||
x5_2 = self.out_linear1_2(x4)
|
||||
x6_2 = self.out_linear2_2(x5_2)
|
||||
|
||||
if self.use_softmax:
|
||||
x7 = self.softmax(x6)
|
||||
x7_2 = self.softmax(x6_2)
|
||||
return x7, x7_2
|
||||
|
||||
return x6, x6_2
|
||||
|
||||
|
||||
@tables.register("encoder_classes", "FSMNMTConvert")
|
||||
class FSMNMTConvert(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
input_dim: int,
|
||||
input_affine_dim: int,
|
||||
fsmn_layers: int,
|
||||
linear_dim: int,
|
||||
proj_dim: int,
|
||||
lorder: int,
|
||||
rorder: int,
|
||||
lstride: int,
|
||||
rstride: int,
|
||||
output_affine_dim: int,
|
||||
output_dim: int,
|
||||
output_dim2: int,
|
||||
use_softmax: bool = True,
|
||||
):
|
||||
"""Initialize FSMNMTConvert.
|
||||
|
||||
Args:
|
||||
input_dim: Size/dimension parameter.
|
||||
input_affine_dim: Size/dimension parameter.
|
||||
fsmn_layers: TODO.
|
||||
linear_dim: Size/dimension parameter.
|
||||
proj_dim: Size/dimension parameter.
|
||||
lorder: TODO.
|
||||
rorder: TODO.
|
||||
lstride: TODO.
|
||||
rstride: TODO.
|
||||
output_affine_dim: Size/dimension parameter.
|
||||
output_dim: Size/dimension parameter.
|
||||
output_dim2: TODO.
|
||||
use_softmax: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.input_dim = input_dim
|
||||
self.input_affine_dim = input_affine_dim
|
||||
self.fsmn_layers = fsmn_layers
|
||||
self.linear_dim = linear_dim
|
||||
self.proj_dim = proj_dim
|
||||
self.output_affine_dim = output_affine_dim
|
||||
self.output_dim = output_dim
|
||||
self.output_dim2 = output_dim2
|
||||
|
||||
self.in_linear1 = AffineTransform(input_dim, input_affine_dim)
|
||||
self.in_linear2 = AffineTransform(input_affine_dim, linear_dim)
|
||||
self.relu = RectifiedLinear(linear_dim, linear_dim)
|
||||
self.fsmn = FsmnStack(*[BasicBlock(linear_dim, proj_dim, lorder, rorder, lstride, rstride, i) for i in
|
||||
range(fsmn_layers)])
|
||||
self.out_linear1 = AffineTransform(linear_dim, output_affine_dim)
|
||||
self.out_linear1_2 = AffineTransform(linear_dim, output_affine_dim)
|
||||
self.out_linear2 = AffineTransform(output_affine_dim, output_dim)
|
||||
self.out_linear2_2 = AffineTransform(output_affine_dim, output_dim2)
|
||||
|
||||
self.use_softmax = use_softmax
|
||||
if self.use_softmax:
|
||||
self.softmax = nn.Softmax(dim=-1)
|
||||
|
||||
def output_size(self) -> int:
|
||||
"""Output size."""
|
||||
return self.output_dim
|
||||
|
||||
def output_size2(self) -> int:
|
||||
"""Output size2."""
|
||||
return self.output_dim2
|
||||
|
||||
def to_kaldi_net(self):
|
||||
"""To kaldi net."""
|
||||
re_str = ''
|
||||
re_str += '<Nnet>\n'
|
||||
re_str += self.in_linear1.to_kaldi_net()
|
||||
re_str += self.in_linear2.to_kaldi_net()
|
||||
re_str += self.relu.to_kaldi_net()
|
||||
|
||||
for fsmn in self.fsmn:
|
||||
re_str += fsmn.to_kaldi_net()
|
||||
|
||||
re_str += self.out_linear1.to_kaldi_net()
|
||||
re_str += self.out_linear2.to_kaldi_net()
|
||||
re_str += '<Softmax> %d %d\n' % (self.output_dim, self.output_dim)
|
||||
re_str += '</Nnet>\n'
|
||||
|
||||
return re_str
|
||||
|
||||
def to_kaldi_net2(self):
|
||||
"""To kaldi net2."""
|
||||
re_str = ''
|
||||
re_str += '<Nnet>\n'
|
||||
re_str += self.in_linear1.to_kaldi_net()
|
||||
re_str += self.in_linear2.to_kaldi_net()
|
||||
re_str += self.relu.to_kaldi_net()
|
||||
|
||||
for fsmn in self.fsmn:
|
||||
re_str += fsmn.to_kaldi_net()
|
||||
|
||||
re_str += self.out_linear1_2.to_kaldi_net()
|
||||
re_str += self.out_linear2_2.to_kaldi_net()
|
||||
re_str += '<Softmax> %d %d\n' % (self.output_dim2, self.output_dim2)
|
||||
re_str += '</Nnet>\n'
|
||||
|
||||
return re_str
|
||||
|
||||
def to_pytorch_net(self, kaldi_file):
|
||||
"""To pytorch net.
|
||||
|
||||
Args:
|
||||
kaldi_file: TODO.
|
||||
"""
|
||||
with open(kaldi_file, 'r', encoding='utf8') as fread:
|
||||
fread = open(kaldi_file, 'r')
|
||||
nnet_start_line = fread.readline()
|
||||
assert nnet_start_line.strip() == '<Nnet>'
|
||||
|
||||
self.in_linear1.to_pytorch_net(fread)
|
||||
self.in_linear2.to_pytorch_net(fread)
|
||||
self.relu.to_pytorch_net(fread)
|
||||
|
||||
for fsmn in self.fsmn:
|
||||
fsmn.to_pytorch_net(fread)
|
||||
|
||||
self.out_linear1.to_pytorch_net(fread)
|
||||
self.out_linear2.to_pytorch_net(fread)
|
||||
|
||||
softmax_line = fread.readline()
|
||||
softmax_split = softmax_line.strip().split()
|
||||
assert softmax_split[0].strip() == '<Softmax>'
|
||||
assert int(softmax_split[1]) == self.output_dim
|
||||
assert int(softmax_split[2]) == self.output_dim
|
||||
|
||||
nnet_end_line = fread.readline()
|
||||
assert nnet_end_line.strip() == '</Nnet>'
|
||||
fread.close()
|
||||
@@ -0,0 +1,404 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
import time
|
||||
import torch
|
||||
import logging
|
||||
from torch.cuda.amp import autocast
|
||||
from typing import Union, Dict, List, Tuple, Optional
|
||||
|
||||
from funasr.register import tables
|
||||
from funasr.models.ctc.ctc import CTC
|
||||
from funasr.utils import postprocess_utils
|
||||
from funasr.metrics.compute_acc import th_accuracy
|
||||
from funasr.utils.datadir_writer import DatadirWriter
|
||||
from funasr.models.paraformer.search import Hypothesis
|
||||
from funasr.models.paraformer.cif_predictor import mae_loss
|
||||
from funasr.train_utils.device_funcs import force_gatherable
|
||||
from funasr.losses.label_smoothing_loss import LabelSmoothingLoss
|
||||
from funasr.models.transformer.utils.add_sos_eos import add_sos_eos
|
||||
from funasr.models.transformer.utils.nets_utils import make_pad_mask, pad_list
|
||||
from funasr.utils.load_utils import load_audio_text_image_video, extract_fbank
|
||||
|
||||
|
||||
@tables.register("model_classes", "FsmnKWSMT")
|
||||
class FsmnKWSMT(torch.nn.Module):
|
||||
"""FSMN-KWS-MT: Multi-Task FSMN Keyword Spotting.
|
||||
|
||||
Keyword spotting with multi-task learning: simultaneously
|
||||
detects keywords and performs filler token classification.
|
||||
Improves keyword detection robustness through auxiliary tasks.
|
||||
|
||||
Output: {"key": str, "value": keyword_detection_result}
|
||||
|
||||
Author: Speech Lab of DAMO Academy, Alibaba Group
|
||||
Deep-FSMN for Large Vocabulary Continuous Speech Recognition
|
||||
https://arxiv.org/abs/1803.05030
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
specaug: Optional[str] = None,
|
||||
specaug_conf: Optional[Dict] = None,
|
||||
normalize: str = None,
|
||||
normalize_conf: Optional[Dict] = None,
|
||||
encoder: str = None,
|
||||
encoder_conf: Optional[Dict] = None,
|
||||
ctc_conf: Optional[Dict] = None,
|
||||
input_size: int = 360,
|
||||
vocab_size: list = [],
|
||||
ignore_id: int = -1,
|
||||
blank_id: int = 0,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize FsmnKWSMT.
|
||||
|
||||
Args:
|
||||
specaug: TODO.
|
||||
specaug_conf: Configuration dict for specaug.
|
||||
normalize: TODO.
|
||||
normalize_conf: Configuration dict for normalize.
|
||||
encoder: TODO.
|
||||
encoder_conf: Configuration dict for encoder.
|
||||
ctc_conf: Configuration dict for ctc.
|
||||
input_size: Size/dimension parameter.
|
||||
vocab_size: Size/dimension parameter.
|
||||
ignore_id: TODO.
|
||||
blank_id: TODO.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
if specaug is not None:
|
||||
specaug_class = tables.specaug_classes.get(specaug)
|
||||
specaug = specaug_class(**specaug_conf)
|
||||
|
||||
if normalize is not None:
|
||||
normalize_class = tables.normalize_classes.get(normalize)
|
||||
normalize = normalize_class(**normalize_conf)
|
||||
|
||||
encoder_class = tables.encoder_classes.get(encoder)
|
||||
encoder = encoder_class(**encoder_conf)
|
||||
encoder_output_size = encoder.output_size()
|
||||
encoder_output_size2 = encoder.output_size2()
|
||||
|
||||
ctc = CTC(
|
||||
odim=vocab_size[0], encoder_output_size=encoder_output_size, **ctc_conf
|
||||
)
|
||||
ctc2 = CTC(
|
||||
odim=vocab_size[1], encoder_output_size=encoder_output_size2, **ctc_conf
|
||||
)
|
||||
|
||||
self.blank_id = blank_id
|
||||
self.ignore_id = ignore_id
|
||||
|
||||
# self.frontend = frontend
|
||||
self.specaug = specaug
|
||||
self.normalize = normalize
|
||||
self.encoder = encoder
|
||||
self.ctc = ctc
|
||||
self.ctc2 = ctc2
|
||||
|
||||
self.error_calculator = None
|
||||
|
||||
def forward(
|
||||
self,
|
||||
speech: torch.Tensor,
|
||||
speech_lengths: torch.Tensor,
|
||||
text: torch.Tensor,
|
||||
text_lengths: torch.Tensor,
|
||||
text2: torch.Tensor,
|
||||
text2_lengths: torch.Tensor,
|
||||
**kwargs,
|
||||
) -> Tuple[torch.Tensor, Dict[str, torch.Tensor], torch.Tensor]:
|
||||
"""Encoder + Decoder + Calc loss
|
||||
Args:
|
||||
speech: (Batch, Length, ...)
|
||||
speech_lengths: (Batch, )
|
||||
text: (Batch, Length)
|
||||
text_lengths: (Batch,)
|
||||
text2: (Batch, Length)
|
||||
text2_lengths: (Batch,)
|
||||
"""
|
||||
if len(text_lengths.size()) > 1:
|
||||
text_lengths = text_lengths[:, 0]
|
||||
if len(speech_lengths.size()) > 1:
|
||||
speech_lengths = speech_lengths[:, 0]
|
||||
batch_size = speech.shape[0]
|
||||
|
||||
# Encoder
|
||||
encoder_out, encoder_out2, encoder_out_lens = self.encode(speech, speech_lengths)
|
||||
|
||||
loss_ctc, cer_ctc = self._calc_ctc_loss(
|
||||
encoder_out, encoder_out_lens, text, text_lengths
|
||||
)
|
||||
loss_ctc2, cer_ctc2 = self._calc_ctc_loss(
|
||||
encoder_out2, encoder_out_lens, text2, text2_lengths
|
||||
)
|
||||
|
||||
# Collect CTC branch stats
|
||||
stats = dict()
|
||||
stats["loss_ctc"] = loss_ctc.detach() if loss_ctc is not None else None
|
||||
stats["cer_ctc"] = cer_ctc
|
||||
stats["loss_ctc2"] = loss_ctc2.detach() if loss_ctc2 is not None else None
|
||||
stats["cer_ctc2"] = cer_ctc2
|
||||
|
||||
loss = 0.5 * loss_ctc + 0.5 * loss_ctc2
|
||||
|
||||
stats["cer"] = cer_ctc
|
||||
stats["cer2"] = cer_ctc2
|
||||
stats["loss"] = torch.clone(loss.detach())
|
||||
|
||||
# force_gatherable: to-device and to-tensor if scalar for DataParallel
|
||||
loss, stats, weight = force_gatherable((loss, stats, batch_size), loss.device)
|
||||
return loss, stats, weight
|
||||
|
||||
def encode(
|
||||
self,
|
||||
speech: torch.Tensor,
|
||||
speech_lengths: torch.Tensor,
|
||||
**kwargs,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Encoder. Note that this method is used by asr_inference.py
|
||||
Args:
|
||||
speech: (Batch, Length, ...)
|
||||
speech_lengths: (Batch, )
|
||||
ind: int
|
||||
"""
|
||||
with autocast(False):
|
||||
# Data augmentation
|
||||
if self.specaug is not None and self.training:
|
||||
speech, speech_lengths = self.specaug(speech, speech_lengths)
|
||||
|
||||
# Normalization for feature: e.g. Global-CMVN, Utterance-CMVN
|
||||
if self.normalize is not None:
|
||||
speech, speech_lengths = self.normalize(speech, speech_lengths)
|
||||
|
||||
# Forward encoder
|
||||
encoder_out, encoder_out2 = self.encoder(speech)
|
||||
encoder_out_lens = speech_lengths
|
||||
|
||||
if isinstance(encoder_out, tuple):
|
||||
encoder_out = encoder_out[0]
|
||||
|
||||
if isinstance(encoder_out2, tuple):
|
||||
encoder_out2 = encoder_out2[0]
|
||||
|
||||
return encoder_out, encoder_out2, encoder_out_lens
|
||||
|
||||
def _calc_ctc_loss(
|
||||
self,
|
||||
encoder_out: torch.Tensor,
|
||||
encoder_out_lens: torch.Tensor,
|
||||
ys_pad: torch.Tensor,
|
||||
ys_pad_lens: torch.Tensor,
|
||||
):
|
||||
# Calc CTC loss
|
||||
"""Internal: calc ctc loss.
|
||||
|
||||
Args:
|
||||
encoder_out: Encoder output tensor.
|
||||
encoder_out_lens: Encoder output lengths.
|
||||
ys_pad: TODO.
|
||||
ys_pad_lens: Lengths of ys_pad.
|
||||
"""
|
||||
loss_ctc = self.ctc(encoder_out, encoder_out_lens, ys_pad, ys_pad_lens)
|
||||
|
||||
# Calc CER using CTC
|
||||
cer_ctc = None
|
||||
if not self.training and self.error_calculator is not None:
|
||||
ys_hat = self.ctc.argmax(encoder_out).data
|
||||
cer_ctc = self.error_calculator(ys_hat.cpu(), ys_pad.cpu(), is_ctc=True)
|
||||
return loss_ctc, cer_ctc
|
||||
|
||||
def _calc_ctc2_loss(
|
||||
self,
|
||||
encoder_out: torch.Tensor,
|
||||
encoder_out_lens: torch.Tensor,
|
||||
ys_pad: torch.Tensor,
|
||||
ys_pad_lens: torch.Tensor,
|
||||
):
|
||||
# Calc CTC loss
|
||||
"""Internal: calc ctc2 loss.
|
||||
|
||||
Args:
|
||||
encoder_out: Encoder output tensor.
|
||||
encoder_out_lens: Encoder output lengths.
|
||||
ys_pad: TODO.
|
||||
ys_pad_lens: Lengths of ys_pad.
|
||||
"""
|
||||
loss_ctc = self.ctc2(encoder_out, encoder_out_lens, ys_pad, ys_pad_lens)
|
||||
|
||||
# Calc CER using CTC
|
||||
cer_ctc = None
|
||||
if not self.training and self.error_calculator is not None:
|
||||
ys_hat = self.ctc2.argmax(encoder_out).data
|
||||
cer_ctc = self.error_calculator(ys_hat.cpu(), ys_pad.cpu(), is_ctc=True)
|
||||
return loss_ctc, cer_ctc
|
||||
|
||||
|
||||
def inference(
|
||||
self,
|
||||
data_in,
|
||||
data_lengths=None,
|
||||
key: list=None,
|
||||
tokenizer=None,
|
||||
frontend=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Run inference on input data.
|
||||
|
||||
Args:
|
||||
data_in: Input data (audio samples, file paths, or text).
|
||||
data_lengths: Lengths of each input sample in the batch.
|
||||
key: Sample identifiers.
|
||||
tokenizer: Tokenizer instance for text encoding/decoding.
|
||||
frontend: Audio frontend for feature extraction.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
keywords = kwargs.get("keywords")
|
||||
from funasr.utils.kws_utils import KwsCtcPrefixDecoder
|
||||
self.kws_decoder = KwsCtcPrefixDecoder(
|
||||
ctc=self.ctc,
|
||||
keywords=keywords,
|
||||
token_list=tokenizer[0].token_list,
|
||||
seg_dict=tokenizer[0].seg_dict,
|
||||
)
|
||||
self.kws_decoder2 = KwsCtcPrefixDecoder(
|
||||
ctc=self.ctc2,
|
||||
keywords=keywords,
|
||||
token_list=tokenizer[1].token_list,
|
||||
seg_dict=tokenizer[1].seg_dict,
|
||||
)
|
||||
|
||||
meta_data = {}
|
||||
if isinstance(data_in, torch.Tensor) and kwargs.get("data_type", "sound") == "fbank": # fbank
|
||||
speech, speech_lengths = data_in, data_lengths
|
||||
if len(speech.shape) < 3:
|
||||
speech = speech[None, :, :]
|
||||
if speech_lengths is not None:
|
||||
speech_lengths = speech_lengths.squeeze(-1)
|
||||
else:
|
||||
speech_lengths = speech.shape[1]
|
||||
else:
|
||||
# extract fbank feats
|
||||
time1 = time.perf_counter()
|
||||
audio_sample_list = load_audio_text_image_video(
|
||||
data_in,
|
||||
fs=frontend.fs,
|
||||
audio_fs=kwargs.get("fs", 16000),
|
||||
data_type=kwargs.get("data_type", "sound"),
|
||||
tokenizer=tokenizer
|
||||
)
|
||||
time2 = time.perf_counter()
|
||||
meta_data["load_data"] = f"{time2 - time1:0.3f}"
|
||||
speech, speech_lengths = extract_fbank(
|
||||
audio_sample_list,
|
||||
data_type=kwargs.get("data_type", "sound"),
|
||||
frontend=frontend
|
||||
)
|
||||
time3 = time.perf_counter()
|
||||
meta_data["extract_feat"] = f"{time3 - time2:0.3f}"
|
||||
meta_data["batch_data_time"] = speech_lengths.sum().item() * frontend.frame_shift * frontend.lfr_n / 1000
|
||||
|
||||
speech = speech.to(device=kwargs["device"])
|
||||
speech_lengths = speech_lengths.to(device=kwargs["device"])
|
||||
|
||||
# Encoder
|
||||
encoder_out, encoder_out2, encoder_out_lens = self.encode(speech, speech_lengths)
|
||||
if isinstance(encoder_out, tuple):
|
||||
encoder_out = encoder_out[0]
|
||||
|
||||
if isinstance(encoder_out2, tuple):
|
||||
encoder_out2 = encoder_out2[0]
|
||||
|
||||
results = []
|
||||
if kwargs.get("output_dir") is not None:
|
||||
if not hasattr(self, "writer"):
|
||||
self.writer = DatadirWriter(kwargs.get("output_dir"))
|
||||
|
||||
for i in range(encoder_out.size(0)):
|
||||
x = encoder_out[i, :encoder_out_lens[i], :]
|
||||
detect_result = self.kws_decoder.decode(x)
|
||||
is_deted, det_keyword, det_score = detect_result[0], detect_result[1], detect_result[2]
|
||||
|
||||
if is_deted:
|
||||
self.writer["detect"][key[i]] = "detected " + det_keyword + " " + str(det_score)
|
||||
det_info = "detected " + det_keyword + " " + str(det_score)
|
||||
else:
|
||||
self.writer["detect"][key[i]] = "rejected"
|
||||
det_info = "rejected"
|
||||
|
||||
x2 = encoder_out2[i, :encoder_out_lens[i], :]
|
||||
detect_result2 = self.kws_decoder2.decode(x2)
|
||||
is_deted2, det_keyword2, det_score2 = detect_result2[0], detect_result2[1], detect_result2[2]
|
||||
|
||||
if is_deted2:
|
||||
self.writer["detect2"][key[i]] = "detected " + det_keyword2 + " " + str(det_score2)
|
||||
det_info2 = "detected " + det_keyword2 + " " + str(det_score2)
|
||||
else:
|
||||
self.writer["detect2"][key[i]] = "rejected"
|
||||
det_info2 = "rejected"
|
||||
|
||||
result_i = {"key": key[i], "text": det_info, "text2": det_info2}
|
||||
results.append(result_i)
|
||||
|
||||
return results, meta_data
|
||||
|
||||
|
||||
@tables.register("model_classes", "FsmnKWSMTConvert")
|
||||
class FsmnKWSMTConvert(torch.nn.Module):
|
||||
"""
|
||||
Author: Speech Lab of DAMO Academy, Alibaba Group
|
||||
Deep-FSMN for Large Vocabulary Continuous Speech Recognition
|
||||
https://arxiv.org/abs/1803.05030
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
encoder: str = None,
|
||||
encoder_conf: Optional[Dict] = None,
|
||||
ctc_conf: Optional[Dict] = None,
|
||||
ctc_weight: float = 1.0,
|
||||
input_size: int = 360,
|
||||
blank_id: int = 0,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize FsmnKWSMTConvert.
|
||||
|
||||
Args:
|
||||
encoder: TODO.
|
||||
encoder_conf: Configuration dict for encoder.
|
||||
ctc_conf: Configuration dict for ctc.
|
||||
ctc_weight: TODO.
|
||||
input_size: Size/dimension parameter.
|
||||
blank_id: TODO.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
encoder_class = tables.encoder_classes.get(encoder)
|
||||
encoder = encoder_class(**encoder_conf)
|
||||
encoder_output_size = encoder.output_size()
|
||||
self.blank_id = blank_id
|
||||
self.encoder = encoder
|
||||
|
||||
self.error_calculator = None
|
||||
|
||||
def to_kaldi_net(self):
|
||||
"""To kaldi net."""
|
||||
return self.encoder.to_kaldi_net()
|
||||
|
||||
def to_kaldi_net2(self):
|
||||
"""To kaldi net2."""
|
||||
return self.encoder.to_kaldi_net2()
|
||||
|
||||
def to_pytorch_net(self, kaldi_file):
|
||||
"""To pytorch net.
|
||||
|
||||
Args:
|
||||
kaldi_file: TODO.
|
||||
"""
|
||||
return self.encoder.to_pytorch_net(kaldi_file)
|
||||
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env python3
|
||||
"""DynamicStreamingVAD — 动态阈值流式 VAD 封装。
|
||||
|
||||
在 fsmn-vad 基础上,根据当前语音段的累积时长动态调整静音切分阈值:
|
||||
短句等待更长静音(避免切碎),长句快速切分(避免堆积)。
|
||||
|
||||
支持流式(逐帧喂入)和非流式(一次性处理完整音频)两种调用方式。
|
||||
|
||||
Usage (流式):
|
||||
from funasr import AutoModel
|
||||
from funasr.models.fsmn_vad_streaming.dynamic_vad import DynamicStreamingVAD
|
||||
|
||||
vad_model = AutoModel(model="fsmn-vad", device="cuda:0")
|
||||
vad = DynamicStreamingVAD(vad_model)
|
||||
|
||||
for audio_chunk in audio_stream:
|
||||
segments = vad.feed(audio_chunk)
|
||||
for seg in segments:
|
||||
print(f"Speech: {seg[0]}-{seg[1]}ms")
|
||||
|
||||
# 结束时
|
||||
final_segments = vad.finalize()
|
||||
|
||||
Usage (非流式):
|
||||
segments = vad.process(full_audio_tensor)
|
||||
for seg in segments:
|
||||
print(f"Speech: {seg[0]}-{seg[1]}ms")
|
||||
"""
|
||||
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import torch
|
||||
import numpy as np
|
||||
|
||||
|
||||
# 默认动态阈值配置:(累积时长上限ms, 静音阈值ms)
|
||||
DEFAULT_SILENCE_SCHEDULE = [
|
||||
(5000, 2000),
|
||||
(10000, 1500),
|
||||
(15000, 1000),
|
||||
(30000, 800),
|
||||
(45000, 400),
|
||||
(float('inf'), 100),
|
||||
]
|
||||
|
||||
|
||||
class DynamicStreamingVAD:
|
||||
"""动态阈值流式 VAD。
|
||||
|
||||
在 fsmn-vad 的流式推理基础上,根据当前语音段已累积的时长
|
||||
动态调整静音切分阈值,实现「短句不切碎、长句快切分」。
|
||||
|
||||
Args:
|
||||
vad_model: FunASR AutoModel 加载的 fsmn-vad 模型实例。
|
||||
chunk_size_ms: 每次喂入 VAD 的 chunk 大小(毫秒),默认 60。
|
||||
speech_noise_thres: 语音/噪声判别阈值,默认 0.5。
|
||||
speech_to_sil_thres_ms: 语音转静音的基础时间(毫秒),默认 150。
|
||||
silence_schedule: 动态阈值配置表,格式为
|
||||
[(累积时长上限ms, 对应的静音阈值ms), ...]。
|
||||
当累积时长 <= 上限时,使用对应的静音阈值。
|
||||
默认值适合实时对话场景。设为 None 禁用动态调整(使用固定阈值)。
|
||||
sample_rate: 采样率,默认 16000。
|
||||
|
||||
Example:
|
||||
# 自定义阈值:更激进的切分
|
||||
vad = DynamicStreamingVAD(
|
||||
vad_model,
|
||||
silence_schedule=[
|
||||
(3000, 1500),
|
||||
(8000, 800),
|
||||
(15000, 400),
|
||||
(float('inf'), 200),
|
||||
],
|
||||
)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
vad_model,
|
||||
chunk_size_ms: int = 60,
|
||||
speech_noise_thres: float = 0.5,
|
||||
speech_to_sil_thres_ms: int = 150,
|
||||
silence_schedule: Optional[List[Tuple[float, int]]] = None,
|
||||
sample_rate: int = 16000,
|
||||
):
|
||||
self.model = vad_model
|
||||
self.chunk_size_ms = chunk_size_ms
|
||||
self.speech_noise_thres = speech_noise_thres
|
||||
self.speech_to_sil_thres_ms = speech_to_sil_thres_ms
|
||||
self.silence_schedule = silence_schedule if silence_schedule is not None else DEFAULT_SILENCE_SCHEDULE
|
||||
self.sample_rate = sample_rate
|
||||
|
||||
self.cache = {}
|
||||
self.confirmed_segments: List[List[int]] = []
|
||||
self.current_speech_start: Optional[int] = None
|
||||
self.accumulated_since_cut_ms: int = 0
|
||||
|
||||
def _get_silence_threshold(self) -> int:
|
||||
"""根据当前累积时长,从 schedule 中查询静音阈值。"""
|
||||
for limit_ms, silence_ms in self.silence_schedule:
|
||||
if self.accumulated_since_cut_ms <= limit_ms:
|
||||
return silence_ms
|
||||
return self.silence_schedule[-1][1]
|
||||
|
||||
def _apply_dynamic_threshold(self):
|
||||
"""将动态阈值应用到 VAD 内部 cache。"""
|
||||
if "stats" not in self.cache:
|
||||
return
|
||||
stats = self.cache["stats"]
|
||||
stats.speech_noise_thres = self.speech_noise_thres
|
||||
desired_silence_ms = self._get_silence_threshold()
|
||||
stats.max_end_sil_frame_cnt_thresh = max(desired_silence_ms - self.speech_to_sil_thres_ms, 0)
|
||||
|
||||
def feed(self, audio_chunk: torch.Tensor, is_final: bool = False) -> List[List[int]]:
|
||||
"""喂入一段音频,返回新确认的语音段。
|
||||
|
||||
Args:
|
||||
audio_chunk: 音频数据(float32 tensor,16kHz)。
|
||||
可以是任意长度,内部按 chunk_size_ms 处理。
|
||||
is_final: 是否为最后一段音频。设为 True 时会强制结束当前语音段。
|
||||
|
||||
Returns:
|
||||
新确认的语音段列表,每段为 [start_ms, end_ms]。
|
||||
仅在检测到语音结束时返回非空列表。
|
||||
"""
|
||||
if audio_chunk.dim() > 1:
|
||||
audio_chunk = audio_chunk.squeeze()
|
||||
|
||||
chunk_samples = len(audio_chunk)
|
||||
self.accumulated_since_cut_ms += int(chunk_samples * 1000 / self.sample_rate)
|
||||
|
||||
self._apply_dynamic_threshold()
|
||||
|
||||
res = self.model.generate(
|
||||
input=[audio_chunk], cache=self.cache,
|
||||
is_final=is_final, chunk_size=self.chunk_size_ms,
|
||||
)
|
||||
|
||||
signals = res[0].get("value", [])
|
||||
new_confirmed = []
|
||||
|
||||
for sig in signals:
|
||||
if sig[0] >= 0 and sig[1] == -1:
|
||||
self.current_speech_start = sig[0]
|
||||
elif sig[0] == -1 and sig[1] >= 0:
|
||||
start = self.current_speech_start if self.current_speech_start is not None else 0
|
||||
seg = [start, sig[1]]
|
||||
self.confirmed_segments.append(seg)
|
||||
new_confirmed.append(seg)
|
||||
self.current_speech_start = None
|
||||
self.accumulated_since_cut_ms = 0
|
||||
elif sig[0] >= 0 and sig[1] >= 0:
|
||||
self.confirmed_segments.append(sig)
|
||||
new_confirmed.append(sig)
|
||||
self.current_speech_start = None
|
||||
self.accumulated_since_cut_ms = 0
|
||||
|
||||
return new_confirmed
|
||||
|
||||
def finalize(self) -> List[List[int]]:
|
||||
"""结束流式处理,返回最后可能未结束的语音段。
|
||||
|
||||
调用此方法后,VAD 状态会被重置。
|
||||
如果当前有正在进行的语音段,会被强制结束。
|
||||
|
||||
Returns:
|
||||
最后确认的语音段列表。
|
||||
"""
|
||||
# Feed empty with is_final=True to flush
|
||||
empty = torch.zeros(int(self.sample_rate * 0.01), dtype=torch.float32)
|
||||
return self.feed(empty, is_final=True)
|
||||
|
||||
def process(self, audio: torch.Tensor) -> List[List[int]]:
|
||||
"""非流式接口:一次性处理完整音频,返回所有语音段。
|
||||
|
||||
Args:
|
||||
audio: 完整音频(float32 tensor,16kHz)。
|
||||
|
||||
Returns:
|
||||
所有检测到的语音段 [[start_ms, end_ms], ...]。
|
||||
"""
|
||||
self.reset()
|
||||
|
||||
if isinstance(audio, np.ndarray):
|
||||
audio = torch.from_numpy(audio).float()
|
||||
if audio.dim() > 1:
|
||||
audio = audio.squeeze()
|
||||
|
||||
# 分 chunk 喂入
|
||||
chunk_samples = int(self.sample_rate * self.chunk_size_ms / 1000)
|
||||
total = len(audio)
|
||||
all_segments = []
|
||||
|
||||
for i in range(0, total, chunk_samples):
|
||||
chunk = audio[i:i + chunk_samples]
|
||||
is_last = (i + chunk_samples >= total)
|
||||
segs = self.feed(chunk, is_final=is_last)
|
||||
all_segments.extend(segs)
|
||||
|
||||
return all_segments
|
||||
|
||||
@property
|
||||
def is_speaking(self) -> bool:
|
||||
"""当前是否在语音状态中。"""
|
||||
return self.current_speech_start is not None
|
||||
|
||||
@property
|
||||
def current_duration_ms(self) -> int:
|
||||
"""当前段已累积的时长(毫秒)。"""
|
||||
return self.accumulated_since_cut_ms
|
||||
|
||||
@property
|
||||
def current_threshold_ms(self) -> int:
|
||||
"""当前使用的静音阈值(毫秒)。"""
|
||||
return self._get_silence_threshold()
|
||||
|
||||
def reset(self):
|
||||
"""重置所有状态,开始新一轮检测。"""
|
||||
self.cache = {}
|
||||
self.confirmed_segments = []
|
||||
self.current_speech_start = None
|
||||
self.accumulated_since_cut_ms = 0
|
||||
Executable
+453
@@ -0,0 +1,453 @@
|
||||
from typing import Tuple, Dict
|
||||
import copy
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from funasr.register import tables
|
||||
|
||||
|
||||
class LinearTransform(nn.Module):
|
||||
|
||||
def __init__(self, input_dim, output_dim):
|
||||
"""Initialize LinearTransform.
|
||||
|
||||
Args:
|
||||
input_dim: Size/dimension parameter.
|
||||
output_dim: Size/dimension parameter.
|
||||
"""
|
||||
super(LinearTransform, self).__init__()
|
||||
self.input_dim = input_dim
|
||||
self.output_dim = output_dim
|
||||
self.linear = nn.Linear(input_dim, output_dim, bias=False)
|
||||
|
||||
def forward(self, input):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
input: Input audio/text data.
|
||||
"""
|
||||
output = self.linear(input)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
class AffineTransform(nn.Module):
|
||||
|
||||
def __init__(self, input_dim, output_dim):
|
||||
"""Initialize AffineTransform.
|
||||
|
||||
Args:
|
||||
input_dim: Size/dimension parameter.
|
||||
output_dim: Size/dimension parameter.
|
||||
"""
|
||||
super(AffineTransform, self).__init__()
|
||||
self.input_dim = input_dim
|
||||
self.output_dim = output_dim
|
||||
self.linear = nn.Linear(input_dim, output_dim)
|
||||
|
||||
def forward(self, input):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
input: Input audio/text data.
|
||||
"""
|
||||
output = self.linear(input)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
class RectifiedLinear(nn.Module):
|
||||
|
||||
def __init__(self, input_dim, output_dim):
|
||||
"""Initialize RectifiedLinear.
|
||||
|
||||
Args:
|
||||
input_dim: Size/dimension parameter.
|
||||
output_dim: Size/dimension parameter.
|
||||
"""
|
||||
super(RectifiedLinear, self).__init__()
|
||||
self.dim = input_dim
|
||||
self.relu = nn.ReLU()
|
||||
self.dropout = nn.Dropout(0.1)
|
||||
|
||||
def forward(self, input):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
input: Input audio/text data.
|
||||
"""
|
||||
out = self.relu(input)
|
||||
return out
|
||||
|
||||
|
||||
class FSMNBlock(nn.Module):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_dim: int,
|
||||
output_dim: int,
|
||||
lorder=None,
|
||||
rorder=None,
|
||||
lstride=1,
|
||||
rstride=1,
|
||||
):
|
||||
"""Initialize FSMNBlock.
|
||||
|
||||
Args:
|
||||
input_dim: Size/dimension parameter.
|
||||
output_dim: Size/dimension parameter.
|
||||
lorder: TODO.
|
||||
rorder: TODO.
|
||||
lstride: TODO.
|
||||
rstride: TODO.
|
||||
"""
|
||||
super(FSMNBlock, self).__init__()
|
||||
|
||||
self.dim = input_dim
|
||||
|
||||
if lorder is None:
|
||||
return
|
||||
|
||||
self.lorder = lorder
|
||||
self.rorder = rorder
|
||||
self.lstride = lstride
|
||||
self.rstride = rstride
|
||||
|
||||
self.conv_left = nn.Conv2d(
|
||||
self.dim, self.dim, [lorder, 1], dilation=[lstride, 1], groups=self.dim, bias=False
|
||||
)
|
||||
|
||||
if self.rorder > 0:
|
||||
self.conv_right = nn.Conv2d(
|
||||
self.dim, self.dim, [rorder, 1], dilation=[rstride, 1], groups=self.dim, bias=False
|
||||
)
|
||||
else:
|
||||
self.conv_right = None
|
||||
|
||||
def forward(self, input: torch.Tensor, cache: torch.Tensor = None):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
input: Input audio/text data.
|
||||
cache: State cache dict for streaming inference.
|
||||
"""
|
||||
x = torch.unsqueeze(input, 1)
|
||||
x_per = x.permute(0, 3, 2, 1) # B D T C
|
||||
|
||||
if cache is not None:
|
||||
cache = cache.to(x_per.device)
|
||||
y_left = torch.cat((cache, x_per), dim=2)
|
||||
cache = y_left[:, :, -(self.lorder - 1) * self.lstride :, :]
|
||||
else:
|
||||
y_left = F.pad(x_per, [0, 0, (self.lorder - 1) * self.lstride, 0])
|
||||
|
||||
y_left = self.conv_left(y_left)
|
||||
out = x_per + y_left
|
||||
|
||||
if self.conv_right is not None:
|
||||
# maybe need to check
|
||||
y_right = F.pad(x_per, [0, 0, 0, self.rorder * self.rstride])
|
||||
y_right = y_right[:, :, self.rstride :, :]
|
||||
y_right = self.conv_right(y_right)
|
||||
out += y_right
|
||||
|
||||
out_per = out.permute(0, 3, 2, 1)
|
||||
output = out_per.squeeze(1)
|
||||
|
||||
return output, cache
|
||||
|
||||
|
||||
class BasicBlock(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
linear_dim: int,
|
||||
proj_dim: int,
|
||||
lorder: int,
|
||||
rorder: int,
|
||||
lstride: int,
|
||||
rstride: int,
|
||||
stack_layer: int,
|
||||
):
|
||||
"""Initialize BasicBlock.
|
||||
|
||||
Args:
|
||||
linear_dim: Size/dimension parameter.
|
||||
proj_dim: Size/dimension parameter.
|
||||
lorder: TODO.
|
||||
rorder: TODO.
|
||||
lstride: TODO.
|
||||
rstride: TODO.
|
||||
stack_layer: TODO.
|
||||
"""
|
||||
super(BasicBlock, self).__init__()
|
||||
self.lorder = lorder
|
||||
self.rorder = rorder
|
||||
self.lstride = lstride
|
||||
self.rstride = rstride
|
||||
self.stack_layer = stack_layer
|
||||
self.linear = LinearTransform(linear_dim, proj_dim)
|
||||
self.fsmn_block = FSMNBlock(proj_dim, proj_dim, lorder, rorder, lstride, rstride)
|
||||
self.affine = AffineTransform(proj_dim, linear_dim)
|
||||
self.relu = RectifiedLinear(linear_dim, linear_dim)
|
||||
|
||||
def forward(self, input: torch.Tensor, cache: Dict[str, torch.Tensor] = None):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
input: Input audio/text data.
|
||||
cache: State cache dict for streaming inference.
|
||||
"""
|
||||
x1 = self.linear(input) # B T D
|
||||
|
||||
if cache is not None:
|
||||
cache_layer_name = 'cache_layer_{}'.format(self.stack_layer)
|
||||
if cache_layer_name not in cache:
|
||||
cache[cache_layer_name] = torch.zeros(
|
||||
x1.shape[0], x1.shape[-1], (self.lorder - 1) * self.lstride, 1
|
||||
)
|
||||
x2, cache[cache_layer_name] = self.fsmn_block(x1, cache[cache_layer_name])
|
||||
else:
|
||||
x2, _ = self.fsmn_block(x1, None)
|
||||
x3 = self.affine(x2)
|
||||
x4 = self.relu(x3)
|
||||
return x4
|
||||
|
||||
|
||||
class BasicBlock_export(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
model,
|
||||
):
|
||||
"""Initialize BasicBlock_export.
|
||||
|
||||
Args:
|
||||
model: Model instance or model name.
|
||||
"""
|
||||
super(BasicBlock_export, self).__init__()
|
||||
self.linear = model.linear
|
||||
self.fsmn_block = model.fsmn_block
|
||||
self.affine = model.affine
|
||||
self.relu = model.relu
|
||||
|
||||
def forward(self, input: torch.Tensor, in_cache: torch.Tensor):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
input: Input audio/text data.
|
||||
in_cache: TODO.
|
||||
"""
|
||||
x = self.linear(input) # B T D
|
||||
# cache_layer_name = 'cache_layer_{}'.format(self.stack_layer)
|
||||
# if cache_layer_name not in in_cache:
|
||||
# in_cache[cache_layer_name] = torch.zeros(x1.shape[0], x1.shape[-1], (self.lorder - 1) * self.lstride, 1)
|
||||
x, out_cache = self.fsmn_block(x, in_cache)
|
||||
x = self.affine(x)
|
||||
x = self.relu(x)
|
||||
return x, out_cache
|
||||
|
||||
|
||||
class FsmnStack(nn.Sequential):
|
||||
def __init__(self, *args):
|
||||
"""Initialize FsmnStack.
|
||||
|
||||
Args:
|
||||
*args: Variable positional arguments.
|
||||
"""
|
||||
super(FsmnStack, self).__init__(*args)
|
||||
|
||||
def forward(self, input: torch.Tensor, cache: Dict[str, torch.Tensor]):
|
||||
"""Forward pass for training.
|
||||
|
||||
Args:
|
||||
input: Input audio/text data.
|
||||
cache: State cache dict for streaming inference.
|
||||
"""
|
||||
x = input
|
||||
for module in self._modules.values():
|
||||
x = module(x, cache)
|
||||
return x
|
||||
|
||||
|
||||
"""
|
||||
FSMN net for keyword spotting
|
||||
input_dim: input dimension
|
||||
linear_dim: fsmn input dimensionll
|
||||
proj_dim: fsmn projection dimension
|
||||
lorder: fsmn left order
|
||||
rorder: fsmn right order
|
||||
num_syn: output dimension
|
||||
fsmn_layers: no. of sequential fsmn layers
|
||||
"""
|
||||
|
||||
|
||||
@tables.register("encoder_classes", "FSMN")
|
||||
class FSMN(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
input_dim: int,
|
||||
input_affine_dim: int,
|
||||
fsmn_layers: int,
|
||||
linear_dim: int,
|
||||
proj_dim: int,
|
||||
lorder: int,
|
||||
rorder: int,
|
||||
lstride: int,
|
||||
rstride: int,
|
||||
output_affine_dim: int,
|
||||
output_dim: int,
|
||||
use_softmax: bool = True,
|
||||
):
|
||||
"""Initialize FSMN.
|
||||
|
||||
Args:
|
||||
input_dim: Size/dimension parameter.
|
||||
input_affine_dim: Size/dimension parameter.
|
||||
fsmn_layers: TODO.
|
||||
linear_dim: Size/dimension parameter.
|
||||
proj_dim: Size/dimension parameter.
|
||||
lorder: TODO.
|
||||
rorder: TODO.
|
||||
lstride: TODO.
|
||||
rstride: TODO.
|
||||
output_affine_dim: Size/dimension parameter.
|
||||
output_dim: Size/dimension parameter.
|
||||
use_softmax: TODO.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.input_dim = input_dim
|
||||
self.input_affine_dim = input_affine_dim
|
||||
self.fsmn_layers = fsmn_layers
|
||||
self.linear_dim = linear_dim
|
||||
self.proj_dim = proj_dim
|
||||
self.output_affine_dim = output_affine_dim
|
||||
self.output_dim = output_dim
|
||||
|
||||
self.in_linear1 = AffineTransform(input_dim, input_affine_dim)
|
||||
self.in_linear2 = AffineTransform(input_affine_dim, linear_dim)
|
||||
self.relu = RectifiedLinear(linear_dim, linear_dim)
|
||||
self.fsmn = FsmnStack(
|
||||
*[
|
||||
BasicBlock(linear_dim, proj_dim, lorder, rorder, lstride, rstride, i)
|
||||
for i in range(fsmn_layers)
|
||||
]
|
||||
)
|
||||
self.out_linear1 = AffineTransform(linear_dim, output_affine_dim)
|
||||
self.out_linear2 = AffineTransform(output_affine_dim, output_dim)
|
||||
|
||||
self.use_softmax = use_softmax
|
||||
if self.use_softmax:
|
||||
self.softmax = nn.Softmax(dim=-1)
|
||||
|
||||
def fuse_modules(self):
|
||||
"""Fuse modules."""
|
||||
pass
|
||||
|
||||
def output_size(self) -> int:
|
||||
"""Output size."""
|
||||
return self.output_dim
|
||||
|
||||
def forward(
|
||||
self,
|
||||
input: torch.Tensor,
|
||||
cache: Dict[str, torch.Tensor] = None
|
||||
) -> Tuple[torch.Tensor, Dict[str, torch.Tensor]]:
|
||||
"""
|
||||
Args:
|
||||
input (torch.Tensor): Input tensor (B, T, D)
|
||||
cache: when cache is not None, the forward is in streaming. The type of cache is a dict, egs,
|
||||
{'cache_layer_1': torch.Tensor(B, T1, D)}, T1 is equal to self.lorder. It is {} for the 1st frame
|
||||
"""
|
||||
|
||||
x1 = self.in_linear1(input)
|
||||
x2 = self.in_linear2(x1)
|
||||
x3 = self.relu(x2)
|
||||
x4 = self.fsmn(x3, cache) # self.cache will update automatically in self.fsmn
|
||||
x5 = self.out_linear1(x4)
|
||||
x6 = self.out_linear2(x5)
|
||||
|
||||
if self.use_softmax:
|
||||
x7 = self.softmax(x6)
|
||||
return x7
|
||||
|
||||
return x6
|
||||
|
||||
|
||||
@tables.register("encoder_classes", "FSMNExport")
|
||||
class FSMNExport(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
model,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize FSMNExport.
|
||||
|
||||
Args:
|
||||
model: Model instance or model name.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
# self.input_dim = input_dim
|
||||
# self.input_affine_dim = input_affine_dim
|
||||
# self.fsmn_layers = fsmn_layers
|
||||
# self.linear_dim = linear_dim
|
||||
# self.proj_dim = proj_dim
|
||||
# self.output_affine_dim = output_affine_dim
|
||||
# self.output_dim = output_dim
|
||||
#
|
||||
# self.in_linear1 = AffineTransform(input_dim, input_affine_dim)
|
||||
# self.in_linear2 = AffineTransform(input_affine_dim, linear_dim)
|
||||
# self.relu = RectifiedLinear(linear_dim, linear_dim)
|
||||
# self.fsmn = FsmnStack(*[BasicBlock(linear_dim, proj_dim, lorder, rorder, lstride, rstride, i) for i in
|
||||
# range(fsmn_layers)])
|
||||
# self.out_linear1 = AffineTransform(linear_dim, output_affine_dim)
|
||||
# self.out_linear2 = AffineTransform(output_affine_dim, output_dim)
|
||||
# self.softmax = nn.Softmax(dim=-1)
|
||||
|
||||
self.in_linear1 = model.in_linear1
|
||||
self.in_linear2 = model.in_linear2
|
||||
self.relu = model.relu
|
||||
# self.fsmn = model.fsmn
|
||||
self.out_linear1 = model.out_linear1
|
||||
self.out_linear2 = model.out_linear2
|
||||
self.softmax = model.softmax
|
||||
self.fsmn = model.fsmn
|
||||
for i, d in enumerate(model.fsmn):
|
||||
if isinstance(d, BasicBlock):
|
||||
self.fsmn[i] = BasicBlock_export(d)
|
||||
|
||||
def fuse_modules(self):
|
||||
"""Fuse modules."""
|
||||
pass
|
||||
|
||||
def forward(
|
||||
self,
|
||||
input: torch.Tensor,
|
||||
*args,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
input (torch.Tensor): Input tensor (B, T, D)
|
||||
in_cache: when in_cache is not None, the forward is in streaming. The type of in_cache is a dict, egs,
|
||||
{'cache_layer_1': torch.Tensor(B, T1, D)}, T1 is equal to self.lorder. It is {} for the 1st frame
|
||||
"""
|
||||
|
||||
x = self.in_linear1(input)
|
||||
x = self.in_linear2(x)
|
||||
x = self.relu(x)
|
||||
# x4 = self.fsmn(x3, in_cache) # self.in_cache will update automatically in self.fsmn
|
||||
out_caches = list()
|
||||
for i, d in enumerate(self.fsmn):
|
||||
in_cache = args[i]
|
||||
x, out_cache = d(x, in_cache)
|
||||
out_caches.append(out_cache)
|
||||
x = self.out_linear1(x)
|
||||
x = self.out_linear2(x)
|
||||
x = self.softmax(x)
|
||||
|
||||
return x, out_caches
|
||||
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
import types
|
||||
import torch
|
||||
from funasr.register import tables
|
||||
|
||||
|
||||
def export_rebuild_model(model, **kwargs):
|
||||
"""Export rebuild model.
|
||||
|
||||
Args:
|
||||
model: Model instance or model name.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
is_onnx = kwargs.get("type", "onnx") == "onnx"
|
||||
encoder_class = tables.encoder_classes.get(kwargs["encoder"] + "Export")
|
||||
model.encoder = encoder_class(model.encoder, onnx=is_onnx)
|
||||
|
||||
model.forward = types.MethodType(export_forward, model)
|
||||
model.export_dummy_inputs = types.MethodType(export_dummy_inputs, model)
|
||||
model.export_input_names = types.MethodType(export_input_names, model)
|
||||
model.export_output_names = types.MethodType(export_output_names, model)
|
||||
model.export_dynamic_axes = types.MethodType(export_dynamic_axes, model)
|
||||
model.export_name = types.MethodType(export_name, model)
|
||||
|
||||
return model
|
||||
|
||||
|
||||
def export_forward(self, feats: torch.Tensor, *args, **kwargs):
|
||||
|
||||
"""Export forward.
|
||||
|
||||
Args:
|
||||
feats: Feature tensor (e.g., fbank), shape (batch, frames, dim).
|
||||
*args: Variable positional arguments.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
scores, out_caches = self.encoder(feats, *args)
|
||||
|
||||
return scores, out_caches
|
||||
|
||||
|
||||
def export_dummy_inputs(self, data_in=None, frame=30):
|
||||
"""Export dummy inputs.
|
||||
|
||||
Args:
|
||||
data_in: Input data (audio samples, file paths, or text).
|
||||
frame: TODO.
|
||||
"""
|
||||
if data_in is None:
|
||||
speech = torch.randn(1, frame, self.encoder_conf.get("input_dim"))
|
||||
else:
|
||||
speech = None # Undo
|
||||
|
||||
cache_frames = self.encoder_conf.get("lorder") + self.encoder_conf.get("rorder") - 1
|
||||
in_cache0 = torch.randn(1, self.encoder_conf.get("proj_dim"), cache_frames, 1)
|
||||
in_cache1 = torch.randn(1, self.encoder_conf.get("proj_dim"), cache_frames, 1)
|
||||
in_cache2 = torch.randn(1, self.encoder_conf.get("proj_dim"), cache_frames, 1)
|
||||
in_cache3 = torch.randn(1, self.encoder_conf.get("proj_dim"), cache_frames, 1)
|
||||
|
||||
return (speech, in_cache0, in_cache1, in_cache2, in_cache3)
|
||||
|
||||
|
||||
def export_input_names(self):
|
||||
"""Export input names."""
|
||||
return ["speech", "in_cache0", "in_cache1", "in_cache2", "in_cache3"]
|
||||
|
||||
|
||||
def export_output_names(self):
|
||||
"""Export output names."""
|
||||
return ["logits", "out_cache0", "out_cache1", "out_cache2", "out_cache3"]
|
||||
|
||||
|
||||
def export_dynamic_axes(self):
|
||||
"""Export dynamic axes."""
|
||||
return {
|
||||
"speech": {1: "feats_length"},
|
||||
}
|
||||
|
||||
|
||||
def export_name(
|
||||
self,
|
||||
):
|
||||
"""Export name."""
|
||||
return "model.onnx"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
||||
# This is an example that demonstrates how to configure a model file.
|
||||
# You can modify the configuration according to your own requirements.
|
||||
|
||||
# to print the register_table:
|
||||
# from funasr.register import tables
|
||||
# tables.print()
|
||||
|
||||
# network architecture
|
||||
model: FsmnVADStreaming
|
||||
model_conf:
|
||||
sample_rate: 16000
|
||||
detect_mode: 1
|
||||
snr_mode: 0
|
||||
max_end_silence_time: 800
|
||||
max_start_silence_time: 3000
|
||||
do_start_point_detection: True
|
||||
do_end_point_detection: True
|
||||
window_size_ms: 200
|
||||
sil_to_speech_time_thres: 150
|
||||
speech_to_sil_time_thres: 150
|
||||
speech_2_noise_ratio: 1.0
|
||||
do_extend: 1
|
||||
lookback_time_start_point: 200
|
||||
lookahead_time_end_point: 100
|
||||
max_single_segment_time: 60000
|
||||
snr_thres: -100.0
|
||||
noise_frame_num_used_for_snr: 100
|
||||
decibel_thres: -100.0
|
||||
speech_noise_thres: 0.6
|
||||
fe_prior_thres: 0.0001
|
||||
silence_pdf_num: 1
|
||||
sil_pdf_ids: [0]
|
||||
speech_noise_thresh_low: -0.1
|
||||
speech_noise_thresh_high: 0.3
|
||||
output_frame_probs: False
|
||||
frame_in_ms: 10
|
||||
frame_length_ms: 25
|
||||
|
||||
encoder: FSMN
|
||||
encoder_conf:
|
||||
input_dim: 400
|
||||
input_affine_dim: 140
|
||||
fsmn_layers: 4
|
||||
linear_dim: 250
|
||||
proj_dim: 128
|
||||
lorder: 20
|
||||
rorder: 0
|
||||
lstride: 1
|
||||
rstride: 0
|
||||
output_affine_dim: 140
|
||||
output_dim: 248
|
||||
|
||||
frontend: WavFrontend
|
||||
frontend_conf:
|
||||
fs: 16000
|
||||
window: hamming
|
||||
n_mels: 80
|
||||
frame_length: 25
|
||||
frame_shift: 10
|
||||
dither: 0.0
|
||||
lfr_m: 5
|
||||
lfr_n: 1
|
||||
@@ -0,0 +1,70 @@
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
class CTC(torch.nn.Module):
|
||||
"""CTC module.
|
||||
|
||||
Args:
|
||||
odim: dimension of outputs
|
||||
encoder_output_size: number of encoder projection units
|
||||
dropout_rate: dropout rate (0.0 ~ 1.0)
|
||||
reduce: reduce the CTC loss into a scalar
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
odim: int,
|
||||
encoder_output_size: int,
|
||||
dropout_rate: float = 0.0,
|
||||
reduce: bool = True,
|
||||
blank_id: int = 0,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize CTC.
|
||||
|
||||
Args:
|
||||
odim: TODO.
|
||||
encoder_output_size: Size/dimension parameter.
|
||||
dropout_rate: TODO.
|
||||
reduce: TODO.
|
||||
blank_id: TODO.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__()
|
||||
eprojs = encoder_output_size
|
||||
self.dropout_rate = dropout_rate
|
||||
self.ctc_lo = torch.nn.Linear(eprojs, odim)
|
||||
self.blank_id = blank_id
|
||||
self.ctc_loss = torch.nn.CTCLoss(reduction="none", blank=blank_id)
|
||||
self.reduce = reduce
|
||||
|
||||
def softmax(self, hs_pad):
|
||||
"""softmax of frame activations
|
||||
|
||||
Args:
|
||||
Tensor hs_pad: 3d tensor (B, Tmax, eprojs)
|
||||
Returns:
|
||||
torch.Tensor: softmax applied 3d tensor (B, Tmax, odim)
|
||||
"""
|
||||
return F.softmax(self.ctc_lo(hs_pad), dim=2)
|
||||
|
||||
def log_softmax(self, hs_pad):
|
||||
"""log_softmax of frame activations
|
||||
|
||||
Args:
|
||||
Tensor hs_pad: 3d tensor (B, Tmax, eprojs)
|
||||
Returns:
|
||||
torch.Tensor: log softmax applied 3d tensor (B, Tmax, odim)
|
||||
"""
|
||||
return F.log_softmax(self.ctc_lo(hs_pad), dim=2)
|
||||
|
||||
def argmax(self, hs_pad):
|
||||
"""argmax of frame activations
|
||||
|
||||
Args:
|
||||
torch.Tensor hs_pad: 3d tensor (B, Tmax, eprojs)
|
||||
Returns:
|
||||
torch.Tensor: argmax applied 2d tensor (B, Tmax)
|
||||
"""
|
||||
return torch.argmax(self.ctc_lo(hs_pad), dim=2)
|
||||
@@ -0,0 +1,34 @@
|
||||
# Copyright FunASR (https://github.com/modelscope/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
"""Device helpers for Fun-ASR-Nano runtime paths."""
|
||||
|
||||
_SUPPORTED_AUTOCAST_DEVICE_TYPES = {"cuda", "xpu", "mps", "npu"}
|
||||
|
||||
|
||||
def _device_type_from_value(device):
|
||||
"""Resolve a device type without requiring optional backend registration."""
|
||||
if device is None:
|
||||
return "cpu"
|
||||
|
||||
device_type = getattr(device, "type", None)
|
||||
if device_type:
|
||||
return str(device_type).lower()
|
||||
|
||||
if isinstance(device, str):
|
||||
return device.split(":", 1)[0].lower()
|
||||
|
||||
return str(device).split(":", 1)[0].lower()
|
||||
|
||||
|
||||
def resolve_autocast_device_type(device):
|
||||
"""Return the torch.autocast device_type for a Fun-ASR-Nano device.
|
||||
|
||||
PyTorch builds without torch_npu may reject ``torch.device("npu:0")`` before
|
||||
torch_npu registers the backend. Parse strings directly so NPU requests do
|
||||
not fall back to CPU autocast, which only supports bf16 and caused #3034.
|
||||
"""
|
||||
device_type = _device_type_from_value(device)
|
||||
if device_type in _SUPPORTED_AUTOCAST_DEVICE_TYPES:
|
||||
return device_type
|
||||
return "cpu"
|
||||
@@ -0,0 +1,728 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
|
||||
# MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
"""
|
||||
Fun-ASR-Nano vLLM inference engine.
|
||||
|
||||
Uses vLLM for high-throughput LLM decoding while keeping the audio encoder
|
||||
and adaptor in PyTorch. Supports batch inference and tensor-parallel for
|
||||
multi-GPU acceleration.
|
||||
|
||||
Usage:
|
||||
from funasr.models.fun_asr_nano.inference_vllm import FunASRNanoVLLM
|
||||
|
||||
engine = FunASRNanoVLLM.from_pretrained(
|
||||
model="FunAudioLLM/Fun-ASR-Nano-2512",
|
||||
tensor_parallel_size=2,
|
||||
)
|
||||
results = engine.generate(["audio1.wav", "audio2.wav"])
|
||||
"""
|
||||
|
||||
import glob
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import time
|
||||
from typing import List, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
dtype_map = {"bf16": torch.bfloat16, "fp16": torch.float16, "fp32": torch.float32}
|
||||
|
||||
|
||||
def prepare_vllm_model_dir(model_dir: str, output_dir: str = None) -> str:
|
||||
"""Extract LLM weights from Fun-ASR-Nano model.pt and save in HuggingFace format.
|
||||
|
||||
Fun-ASR-Nano stores all weights (audio encoder + adaptor + LLM) in a single
|
||||
model.pt file. vLLM needs the LLM weights in standard HuggingFace format.
|
||||
This function extracts LLM weights and saves them alongside the config/tokenizer
|
||||
files from the Qwen3-0.6B subdirectory.
|
||||
|
||||
Args:
|
||||
model_dir: Path to the Fun-ASR-Nano model directory.
|
||||
output_dir: Where to save the extracted LLM. Defaults to model_dir/Qwen3-0.6B-vllm.
|
||||
|
||||
Returns:
|
||||
Path to the directory containing the vLLM-ready LLM model.
|
||||
"""
|
||||
if output_dir is None:
|
||||
output_dir = os.path.join(model_dir, "Qwen3-0.6B-vllm")
|
||||
|
||||
# Check if already prepared
|
||||
safetensors_files = glob.glob(os.path.join(output_dir, "*.safetensors"))
|
||||
bin_files = glob.glob(os.path.join(output_dir, "model*.bin"))
|
||||
if safetensors_files or bin_files:
|
||||
logger.info(f"vLLM model already prepared at {output_dir}")
|
||||
return output_dir
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Copy config and tokenizer from Qwen3-0.6B
|
||||
qwen_dir = os.path.join(model_dir, "Qwen3-0.6B")
|
||||
if not os.path.isdir(qwen_dir):
|
||||
raise FileNotFoundError(f"Qwen3-0.6B config directory not found at {qwen_dir}")
|
||||
|
||||
for fname in os.listdir(qwen_dir):
|
||||
src = os.path.join(qwen_dir, fname)
|
||||
dst = os.path.join(output_dir, fname)
|
||||
if os.path.isfile(src) and not os.path.exists(dst):
|
||||
shutil.copy2(src, dst)
|
||||
|
||||
# Load model.pt and extract LLM weights
|
||||
model_pt = os.path.join(model_dir, "model.pt")
|
||||
if not os.path.exists(model_pt):
|
||||
raise FileNotFoundError(
|
||||
f"model.pt not found at {model_pt}. Make sure the model is fully downloaded."
|
||||
)
|
||||
|
||||
logger.info(f"Loading model.pt from {model_pt}...")
|
||||
checkpoint = torch.load(model_pt, map_location="cpu")
|
||||
if "state_dict" in checkpoint:
|
||||
state_dict = checkpoint["state_dict"]
|
||||
else:
|
||||
state_dict = checkpoint
|
||||
|
||||
# Extract LLM weights (prefixed with "llm.")
|
||||
llm_state = {}
|
||||
for key, value in state_dict.items():
|
||||
if key.startswith("llm."):
|
||||
new_key = key[len("llm."):]
|
||||
llm_state[new_key] = value
|
||||
|
||||
if not llm_state:
|
||||
raise RuntimeError("No LLM weights found in model.pt (expected prefix 'llm.')")
|
||||
|
||||
logger.info(f"Extracted {len(llm_state)} LLM weight tensors")
|
||||
|
||||
# Save in safetensors format (preferred by vLLM)
|
||||
try:
|
||||
from safetensors.torch import save_file
|
||||
|
||||
save_path = os.path.join(output_dir, "model.safetensors")
|
||||
save_file(llm_state, save_path)
|
||||
logger.info(f"Saved LLM weights to {save_path}")
|
||||
|
||||
# Create model index
|
||||
index = {
|
||||
"metadata": {"total_size": sum(v.numel() * v.element_size() for v in llm_state.values())},
|
||||
"weight_map": {k: "model.safetensors" for k in llm_state.keys()},
|
||||
}
|
||||
with open(os.path.join(output_dir, "model.safetensors.index.json"), "w") as f:
|
||||
json.dump(index, f, indent=2)
|
||||
except ImportError:
|
||||
save_path = os.path.join(output_dir, "model.bin")
|
||||
torch.save(llm_state, save_path)
|
||||
logger.info(f"Saved LLM weights to {save_path} (install safetensors for faster loading)")
|
||||
|
||||
return output_dir
|
||||
|
||||
|
||||
class FunASRNanoVLLM:
|
||||
"""Fun-ASR-Nano with vLLM backend for high-throughput inference.
|
||||
|
||||
Architecture:
|
||||
Audio -> WavFrontend -> SenseVoiceEncoder -> AudioAdaptor -> audio embeddings
|
||||
Text tokens -> LLM embedding layer -> text embeddings
|
||||
Combined embeddings -> vLLM (Qwen3-0.6B) -> generated text
|
||||
|
||||
The audio encoder and adaptor run in PyTorch on a single GPU,
|
||||
while vLLM handles the LLM inference with optional tensor parallelism.
|
||||
|
||||
Args:
|
||||
model_dir: Path to the Fun-ASR-Nano model directory.
|
||||
device: Device for audio encoder/adaptor (e.g. "cuda:0").
|
||||
dtype: Dtype for audio processing ("bf16", "fp16", "fp32").
|
||||
tensor_parallel_size: Number of GPUs for vLLM tensor parallelism.
|
||||
gpu_memory_utilization: Fraction of GPU memory for vLLM KV cache.
|
||||
max_model_len: Maximum sequence length for vLLM.
|
||||
enforce_eager: Disable CUDA graph for debugging.
|
||||
|
||||
Example:
|
||||
>>> engine = FunASRNanoVLLM(
|
||||
... model_dir="/path/to/Fun-ASR-Nano-2512",
|
||||
... tensor_parallel_size=2,
|
||||
... )
|
||||
>>> results = engine.generate(["audio1.wav", "audio2.wav"])
|
||||
>>> for r in results:
|
||||
... print(r["text"])
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model_dir: str,
|
||||
device: str = "cuda:0",
|
||||
dtype: str = "bf16",
|
||||
tensor_parallel_size: int = 1,
|
||||
gpu_memory_utilization: float = 0.8,
|
||||
max_model_len: int = 2048,
|
||||
enforce_eager: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
from vllm import LLM, SamplingParams
|
||||
try:
|
||||
from vllm.inputs import EmbedsPrompt
|
||||
except ImportError:
|
||||
from vllm.inputs.data import EmbedsPrompt
|
||||
|
||||
self.device = device
|
||||
self.dtype = dtype
|
||||
self.torch_dtype = dtype_map.get(dtype, torch.bfloat16)
|
||||
if self.torch_dtype == torch.float16:
|
||||
logger.warning(
|
||||
"dtype='fp16' can produce degraded or garbage transcription for "
|
||||
"Fun-ASR-Nano (numerical overflow in the audio embedding path). "
|
||||
"Use dtype='bf16' (recommended) or dtype='fp32'. On GPUs without "
|
||||
"bfloat16 support (e.g. NVIDIA V100), use 'fp32'."
|
||||
)
|
||||
self.model_dir = model_dir
|
||||
|
||||
# Step 1: Prepare LLM weights for vLLM (extract from model.pt if needed)
|
||||
vllm_model_dir = prepare_vllm_model_dir(model_dir)
|
||||
|
||||
# Step 2: Load audio components (encoder + adaptor + frontend)
|
||||
self._load_audio_components(model_dir, **kwargs)
|
||||
|
||||
# Step 3: Initialize vLLM engine
|
||||
logger.info(f"Initializing vLLM with model: {vllm_model_dir}")
|
||||
logger.info(f" tensor_parallel_size={tensor_parallel_size}")
|
||||
logger.info(f" gpu_memory_utilization={gpu_memory_utilization}")
|
||||
|
||||
vllm_kwargs = kwargs.get("vllm_kwargs", {})
|
||||
self.vllm_engine = LLM(
|
||||
enable_prompt_embeds=True,
|
||||
model=vllm_model_dir,
|
||||
tensor_parallel_size=tensor_parallel_size,
|
||||
gpu_memory_utilization=gpu_memory_utilization,
|
||||
max_model_len=max_model_len,
|
||||
enforce_eager=enforce_eager,
|
||||
dtype={"bf16": "bfloat16", "fp16": "float16", "fp32": "auto"}.get(dtype, dtype),
|
||||
trust_remote_code=True,
|
||||
**vllm_kwargs,
|
||||
)
|
||||
|
||||
# Step 4: Get tokenizer and LLM embedding layer
|
||||
self.tokenizer = self.vllm_engine.get_tokenizer()
|
||||
self._load_embedding_layer(model_dir)
|
||||
|
||||
def _load_audio_components(self, model_dir: str, **kwargs):
|
||||
"""Load audio encoder, adaptor, frontend, and CTC from checkpoint."""
|
||||
from omegaconf import OmegaConf
|
||||
from funasr.register import tables
|
||||
|
||||
config_path = os.path.join(model_dir, "config.yaml")
|
||||
config = OmegaConf.load(config_path)
|
||||
self._config = OmegaConf.to_container(config, resolve=True)
|
||||
|
||||
# --- Frontend ---
|
||||
frontend_class = tables.frontend_classes.get(config["frontend"])
|
||||
frontend_conf = OmegaConf.to_container(config.get("frontend_conf", {}), resolve=True)
|
||||
cmvn_file = frontend_conf.get("cmvn_file")
|
||||
if cmvn_file and not os.path.isabs(cmvn_file):
|
||||
frontend_conf["cmvn_file"] = os.path.join(model_dir, cmvn_file)
|
||||
self.frontend = frontend_class(**frontend_conf)
|
||||
self.frontend.eval()
|
||||
|
||||
# --- Audio Encoder ---
|
||||
encoder_conf = OmegaConf.to_container(config.get("audio_encoder_conf", {}), resolve=True)
|
||||
hub = encoder_conf.get("hub", None)
|
||||
if hub == "ms":
|
||||
from funasr import AutoModel as FunAutoModel
|
||||
|
||||
enc_model = FunAutoModel(
|
||||
model=config["audio_encoder"], model_revision="master", disable_update=True
|
||||
)
|
||||
self.audio_encoder_output_size = (
|
||||
enc_model.model.encoder_output_size
|
||||
if hasattr(enc_model.model, "encoder_output_size")
|
||||
else -1
|
||||
)
|
||||
self.audio_encoder = (
|
||||
enc_model.model.model.encoder
|
||||
if hasattr(enc_model.model, "model")
|
||||
else enc_model.model.encoder
|
||||
)
|
||||
else:
|
||||
encoder_class = tables.encoder_classes.get(config["audio_encoder"])
|
||||
input_size = self.frontend.output_size()
|
||||
self.audio_encoder = encoder_class(input_size=input_size, **encoder_conf)
|
||||
self.audio_encoder_output_size = self.audio_encoder.output_size()
|
||||
|
||||
self.audio_encoder.eval()
|
||||
for p in self.audio_encoder.parameters():
|
||||
p.requires_grad = False
|
||||
|
||||
# --- Audio Adaptor ---
|
||||
adaptor_conf = OmegaConf.to_container(config.get("audio_adaptor_conf", {}), resolve=True)
|
||||
adaptor_class = tables.adaptor_classes.get(config["audio_adaptor"])
|
||||
if self.audio_encoder_output_size > 0:
|
||||
adaptor_conf["encoder_dim"] = self.audio_encoder_output_size
|
||||
self.audio_adaptor = adaptor_class(**adaptor_conf)
|
||||
self.audio_adaptor.eval()
|
||||
for p in self.audio_adaptor.parameters():
|
||||
p.requires_grad = False
|
||||
self.use_low_frame_rate = adaptor_conf.get("use_low_frame_rate", False)
|
||||
|
||||
# --- CTC Decoder (optional, for timestamps) ---
|
||||
self.ctc_decoder = None
|
||||
self.ctc = None
|
||||
self.ctc_tokenizer = None
|
||||
self.blank_id = None
|
||||
|
||||
ctc_decoder_name = self._config.get("ctc_decoder", None)
|
||||
if ctc_decoder_name:
|
||||
ctc_decoder_class = tables.adaptor_classes.get(ctc_decoder_name)
|
||||
ctc_decoder_conf = self._config.get("ctc_decoder_conf", {})
|
||||
if self.audio_encoder_output_size > 0:
|
||||
ctc_decoder_conf["encoder_dim"] = self.audio_encoder_output_size
|
||||
self.ctc_decoder = ctc_decoder_class(**ctc_decoder_conf)
|
||||
self.ctc_decoder.eval()
|
||||
for p in self.ctc_decoder.parameters():
|
||||
p.requires_grad = False
|
||||
|
||||
from funasr.models.fun_asr_nano.ctc import CTC
|
||||
|
||||
ctc_conf = self._config.get("ctc_conf", {})
|
||||
ctc_vocab_size = self._config.get("ctc_vocab_size", 60515)
|
||||
self.blank_id = ctc_conf.get("blank_id", ctc_vocab_size - 1)
|
||||
self.ctc = CTC(
|
||||
odim=ctc_vocab_size,
|
||||
encoder_output_size=self.audio_encoder_output_size,
|
||||
blank_id=self.blank_id,
|
||||
**ctc_conf,
|
||||
)
|
||||
|
||||
# CTC tokenizer
|
||||
ds_conf = self._config.get("dataset_conf", {})
|
||||
ctc_tokenizer_name = ds_conf.get("ctc_tokenizer", None)
|
||||
ctc_tokenizer_conf = ds_conf.get("ctc_tokenizer_conf", {})
|
||||
if ctc_tokenizer_name:
|
||||
ctc_tokenizer_class = tables.tokenizer_classes.get(ctc_tokenizer_name)
|
||||
vocab_path = ctc_tokenizer_conf.get("vocab_path")
|
||||
if vocab_path is None or not os.path.isabs(vocab_path):
|
||||
multilingual_path = os.path.join(model_dir, "multilingual.tiktoken")
|
||||
if os.path.exists(multilingual_path):
|
||||
ctc_tokenizer_conf["vocab_path"] = multilingual_path
|
||||
elif vocab_path and not os.path.isabs(vocab_path):
|
||||
ctc_tokenizer_conf["vocab_path"] = os.path.join(model_dir, vocab_path)
|
||||
self.ctc_tokenizer = ctc_tokenizer_class(**ctc_tokenizer_conf)
|
||||
|
||||
# --- Load weights from model.pt ---
|
||||
model_pt = os.path.join(model_dir, "model.pt")
|
||||
if os.path.exists(model_pt):
|
||||
logger.info(f"Loading audio component weights from {model_pt}")
|
||||
checkpoint = torch.load(model_pt, map_location="cpu")
|
||||
state_dict = checkpoint.get("state_dict", checkpoint)
|
||||
|
||||
# Audio encoder
|
||||
enc_state = {
|
||||
k[len("audio_encoder."):]: v
|
||||
for k, v in state_dict.items()
|
||||
if k.startswith("audio_encoder.")
|
||||
}
|
||||
if enc_state:
|
||||
self.audio_encoder.load_state_dict(enc_state, strict=False)
|
||||
logger.info(f" Loaded audio_encoder: {len(enc_state)} params")
|
||||
|
||||
# Audio adaptor
|
||||
adp_state = {
|
||||
k[len("audio_adaptor."):]: v
|
||||
for k, v in state_dict.items()
|
||||
if k.startswith("audio_adaptor.")
|
||||
}
|
||||
if adp_state:
|
||||
self.audio_adaptor.load_state_dict(adp_state, strict=False)
|
||||
logger.info(f" Loaded audio_adaptor: {len(adp_state)} params")
|
||||
|
||||
# CTC decoder
|
||||
if self.ctc_decoder is not None:
|
||||
ctc_dec_state = {
|
||||
k[len("ctc_decoder."):]: v
|
||||
for k, v in state_dict.items()
|
||||
if k.startswith("ctc_decoder.")
|
||||
}
|
||||
if ctc_dec_state:
|
||||
self.ctc_decoder.load_state_dict(ctc_dec_state, strict=False)
|
||||
ctc_state = {
|
||||
k[len("ctc."):]: v
|
||||
for k, v in state_dict.items()
|
||||
if k.startswith("ctc.") and not k.startswith("ctc_decoder.")
|
||||
}
|
||||
if ctc_state:
|
||||
self.ctc.load_state_dict(ctc_state, strict=False)
|
||||
|
||||
# Move to device
|
||||
self.audio_encoder = self.audio_encoder.to(self.device, dtype=torch.float32)
|
||||
self.audio_adaptor = self.audio_adaptor.to(self.device, dtype=self.torch_dtype)
|
||||
if self.ctc_decoder is not None:
|
||||
self.ctc_decoder = self.ctc_decoder.to(self.device, dtype=torch.float32)
|
||||
self.ctc = self.ctc.to(self.device, dtype=torch.float32)
|
||||
|
||||
def _load_embedding_layer(self, model_dir: str):
|
||||
"""Load the LLM embedding layer for text token embedding computation."""
|
||||
model_pt = os.path.join(model_dir, "model.pt")
|
||||
checkpoint = torch.load(model_pt, map_location="cpu")
|
||||
state_dict = checkpoint.get("state_dict", checkpoint)
|
||||
|
||||
# Look for embedding weights
|
||||
embed_key = None
|
||||
for key in state_dict.keys():
|
||||
if "embed_tokens.weight" in key and key.startswith("llm."):
|
||||
embed_key = key
|
||||
break
|
||||
|
||||
if embed_key is None:
|
||||
raise RuntimeError("Could not find LLM embedding weights in model.pt")
|
||||
|
||||
embed_weight = state_dict[embed_key]
|
||||
self.embed_tokens = nn.Embedding.from_pretrained(embed_weight, freeze=True)
|
||||
self.embed_tokens = self.embed_tokens.to(self.device, dtype=self.torch_dtype)
|
||||
logger.info(f"Loaded embedding layer: {embed_weight.shape}")
|
||||
|
||||
@torch.no_grad()
|
||||
def _encode_audio(self, audio_input: Union[str, torch.Tensor, np.ndarray]):
|
||||
"""Encode audio through frontend -> encoder -> adaptor.
|
||||
|
||||
Returns:
|
||||
adaptor_out: (1, T', D_llm) audio embeddings for LLM input
|
||||
adaptor_out_lens: (1,) lengths
|
||||
encoder_out: (1, T, D_enc) encoder output for CTC
|
||||
encoder_out_lens: (1,) encoder output lengths
|
||||
"""
|
||||
from funasr.utils.load_utils import load_audio_text_image_video, extract_fbank
|
||||
|
||||
if isinstance(audio_input, str):
|
||||
data_src = load_audio_text_image_video(audio_input, fs=self.frontend.fs)
|
||||
elif isinstance(audio_input, np.ndarray):
|
||||
data_src = torch.from_numpy(audio_input).float()
|
||||
elif isinstance(audio_input, torch.Tensor):
|
||||
data_src = audio_input.float()
|
||||
else:
|
||||
raise ValueError(f"Unsupported audio input type: {type(audio_input)}")
|
||||
|
||||
speech, speech_lengths = extract_fbank(
|
||||
data_src, data_type="sound", frontend=self.frontend, is_final=True
|
||||
)
|
||||
speech = speech.to(self.device, dtype=torch.float32)
|
||||
speech_lengths = speech_lengths.to(self.device)
|
||||
|
||||
encoder_out, encoder_out_lens = self.audio_encoder(speech, speech_lengths)
|
||||
encoder_out_for_adaptor = encoder_out.to(dtype=self.torch_dtype)
|
||||
adaptor_out, adaptor_out_lens = self.audio_adaptor(encoder_out_for_adaptor, encoder_out_lens)
|
||||
|
||||
# Apply low frame rate: compute effective token count from fbank length
|
||||
# Matches PyTorch model.py data_load_speech formula exactly
|
||||
if self.use_low_frame_rate:
|
||||
for i in range(adaptor_out.shape[0]):
|
||||
fbank_len = speech_lengths[i].item()
|
||||
olens = 1 + (fbank_len - 3 + 2 * 1) // 2
|
||||
olens = 1 + (olens - 3 + 2 * 1) // 2
|
||||
fake_token_len = (olens - 1) // 2 + 1
|
||||
adaptor_out_lens[i] = fake_token_len
|
||||
|
||||
return adaptor_out, adaptor_out_lens, encoder_out, encoder_out_lens
|
||||
|
||||
def _build_prompt_text(
|
||||
self,
|
||||
hotwords: List[str] = None,
|
||||
language: str = None,
|
||||
itn: bool = True,
|
||||
) -> str:
|
||||
"""Build the ASR prompt string."""
|
||||
hotwords = hotwords or []
|
||||
if len(hotwords) > 0:
|
||||
hotwords_str = ", ".join(hotwords)
|
||||
prompt = (
|
||||
"请结合上下文信息,更加准确地完成语音转写任务。"
|
||||
"如果没有相关信息,我们会留空。\n\n\n**上下文信息:**\n\n\n"
|
||||
)
|
||||
prompt += f"热词列表:[{hotwords_str}]\n"
|
||||
else:
|
||||
prompt = ""
|
||||
if language is None:
|
||||
prompt += "语音转写"
|
||||
else:
|
||||
prompt += f"语音转写成{language}"
|
||||
if not itn:
|
||||
prompt += ",不进行文本规整"
|
||||
return prompt + ":"
|
||||
|
||||
@torch.no_grad()
|
||||
def _build_input_embeds(
|
||||
self,
|
||||
audio_embeds: torch.Tensor,
|
||||
audio_embed_lens: torch.Tensor,
|
||||
hotwords: List[str] = None,
|
||||
language: str = None,
|
||||
itn: bool = True,
|
||||
system_prompt: str = "You are a helpful assistant.",
|
||||
) -> torch.Tensor:
|
||||
"""Build the full input embedding sequence with audio inserted.
|
||||
|
||||
Returns:
|
||||
Tensor of shape (seq_len, D_llm)
|
||||
"""
|
||||
prompt = self._build_prompt_text(hotwords, language, itn)
|
||||
|
||||
# ChatML format with speech markers and thinking prefix
|
||||
prefix_text = (
|
||||
f"<|im_start|>system\n{system_prompt}<|im_end|>\n"
|
||||
f"<|im_start|>user\n{prompt}<|startofspeech|>"
|
||||
)
|
||||
suffix_text = "<|endofspeech|><|im_end|>\n<|im_start|>assistant\n"
|
||||
|
||||
# Tokenize
|
||||
prefix_ids = self.tokenizer.encode(prefix_text, add_special_tokens=False)
|
||||
suffix_ids = self.tokenizer.encode(suffix_text, add_special_tokens=False)
|
||||
|
||||
# Embed text tokens
|
||||
prefix_tensor = torch.tensor(prefix_ids, dtype=torch.long, device=self.device)
|
||||
suffix_tensor = torch.tensor(suffix_ids, dtype=torch.long, device=self.device)
|
||||
prefix_embeds = self.embed_tokens(prefix_tensor)
|
||||
suffix_embeds = self.embed_tokens(suffix_tensor)
|
||||
|
||||
# Audio embeddings
|
||||
audio_len = audio_embed_lens[0].item()
|
||||
audio_emb = audio_embeds[0, :audio_len, :]
|
||||
|
||||
# Concat: [prefix_text_emb | audio_emb | suffix_text_emb]
|
||||
inputs_embeds = torch.cat([prefix_embeds, audio_emb, suffix_embeds], dim=0)
|
||||
return inputs_embeds
|
||||
|
||||
def generate(
|
||||
self,
|
||||
inputs: Union[str, List[str], np.ndarray, torch.Tensor, List],
|
||||
hotwords: List[str] = None,
|
||||
language: str = None,
|
||||
itn: bool = True,
|
||||
max_new_tokens: int = 512,
|
||||
temperature: float = 0.0,
|
||||
top_p: float = 1.0,
|
||||
top_k: int = -1,
|
||||
repetition_penalty: float = 1.0,
|
||||
**kwargs,
|
||||
) -> List[dict]:
|
||||
"""Run batch ASR inference using vLLM.
|
||||
|
||||
Args:
|
||||
inputs: Audio input(s). Accepts:
|
||||
- str: single file path
|
||||
- List[str]: batch of file paths
|
||||
- np.ndarray / torch.Tensor: raw audio samples (16kHz)
|
||||
hotwords: Keywords to boost recognition accuracy.
|
||||
language: Language hint (e.g. "中文", "英文", "日文").
|
||||
itn: Apply inverse text normalization (default True).
|
||||
max_new_tokens: Maximum tokens to generate per sample.
|
||||
temperature: Sampling temperature (0 = greedy decoding).
|
||||
top_p: Nucleus sampling parameter.
|
||||
top_k: Top-k sampling (-1 = disabled).
|
||||
repetition_penalty: Repetition penalty factor.
|
||||
|
||||
Returns:
|
||||
List of result dicts: [{"key": str, "text": str, "timestamps": [...]}]
|
||||
"""
|
||||
from vllm import SamplingParams
|
||||
try:
|
||||
from vllm.inputs import EmbedsPrompt
|
||||
except ImportError:
|
||||
from vllm.inputs.data import EmbedsPrompt
|
||||
|
||||
from funasr.models.fun_asr_nano.vllm_utils import resolve_repetition_penalty
|
||||
|
||||
if isinstance(inputs, (str, np.ndarray, torch.Tensor)):
|
||||
inputs = [inputs]
|
||||
|
||||
sampling_params = SamplingParams(
|
||||
max_tokens=max_new_tokens,
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
top_k=top_k if top_k > 0 else -1,
|
||||
# Prompt-embeds mode has no token IDs to penalize; see #2948.
|
||||
repetition_penalty=resolve_repetition_penalty(repetition_penalty),
|
||||
skip_special_tokens=True,
|
||||
)
|
||||
|
||||
# Batch encode audio and build embedding prompts
|
||||
prompts = []
|
||||
encoder_outputs = []
|
||||
|
||||
t0 = time.perf_counter()
|
||||
|
||||
# Pre-compute text embeddings (shared across batch)
|
||||
prompt_text = self._build_prompt_text(hotwords, language, itn)
|
||||
prefix_text = f"<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n<|im_start|>user\n{prompt_text}"
|
||||
suffix_text = "<|im_end|>\n<|im_start|>assistant\n"
|
||||
prefix_ids = self.tokenizer.encode(prefix_text, add_special_tokens=False)
|
||||
suffix_ids = self.tokenizer.encode(suffix_text, add_special_tokens=False)
|
||||
prefix_emb = self.embed_tokens(torch.tensor(prefix_ids, dtype=torch.long, device=self.device))
|
||||
suffix_emb = self.embed_tokens(torch.tensor(suffix_ids, dtype=torch.long, device=self.device))
|
||||
|
||||
# Batch encode audio (groups of 8 for memory efficiency)
|
||||
batch_size_enc = 8
|
||||
all_adaptor_outs = []
|
||||
all_adaptor_lens = []
|
||||
for i in range(0, len(inputs), batch_size_enc):
|
||||
batch_inputs = inputs[i:i+batch_size_enc]
|
||||
# Load and extract fbank for batch
|
||||
from funasr.utils.load_utils import load_audio_text_image_video, extract_fbank
|
||||
audio_tensors = []
|
||||
for audio_input in batch_inputs:
|
||||
if isinstance(audio_input, str):
|
||||
data_src = load_audio_text_image_video(audio_input, fs=self.frontend.fs)
|
||||
elif isinstance(audio_input, np.ndarray):
|
||||
data_src = torch.from_numpy(audio_input).float()
|
||||
elif isinstance(audio_input, torch.Tensor):
|
||||
data_src = audio_input.float()
|
||||
else:
|
||||
raise ValueError(f"Unsupported audio input type: {type(audio_input)}")
|
||||
audio_tensors.append(data_src)
|
||||
|
||||
speech, speech_lengths = extract_fbank(
|
||||
audio_tensors, data_type="sound", frontend=self.frontend, is_final=True
|
||||
)
|
||||
speech = speech.to(self.device, dtype=torch.float32)
|
||||
speech_lengths = speech_lengths.to(self.device)
|
||||
|
||||
with torch.no_grad():
|
||||
enc_out, enc_lens = self.audio_encoder(speech, speech_lengths)
|
||||
adp_out, adp_lens = self.audio_adaptor(enc_out.to(dtype=self.torch_dtype), enc_lens)
|
||||
|
||||
# Apply low frame rate token length correction
|
||||
if self.use_low_frame_rate:
|
||||
for j in range(len(batch_inputs)):
|
||||
fbank_len = speech_lengths[j].item()
|
||||
olens = 1 + (fbank_len - 3 + 2 * 1) // 2
|
||||
olens = 1 + (olens - 3 + 2 * 1) // 2
|
||||
adp_lens[j] = (olens - 1) // 2 + 1
|
||||
|
||||
for j in range(len(batch_inputs)):
|
||||
all_adaptor_outs.append(adp_out[j, :adp_lens[j], :])
|
||||
all_adaptor_lens.append(adp_lens[j])
|
||||
encoder_outputs.append((enc_out[j:j+1, :enc_lens[j], :], enc_lens[j:j+1]))
|
||||
|
||||
# Build prompts
|
||||
for audio_emb in all_adaptor_outs:
|
||||
input_embeds = torch.cat([prefix_emb, audio_emb, suffix_emb], dim=0)
|
||||
prompts.append(EmbedsPrompt(prompt_embeds=input_embeds.float()))
|
||||
|
||||
t1 = time.perf_counter()
|
||||
logger.info(f"Audio encoding: {len(inputs)} samples in {t1 - t0:.3f}s")
|
||||
|
||||
# vLLM batch generation
|
||||
outputs = self.vllm_engine.generate(prompts, sampling_params, use_tqdm=len(inputs) > 1)
|
||||
|
||||
t2 = time.perf_counter()
|
||||
logger.info(f"vLLM generation: {t2 - t1:.3f}s")
|
||||
|
||||
# Process results
|
||||
results = []
|
||||
for i, output in enumerate(outputs):
|
||||
token_ids = list(output.outputs[0].token_ids)
|
||||
text = self.tokenizer.decode(token_ids, skip_special_tokens=True)
|
||||
# Clean vLLM artifacts: remove garbage prefix/tags
|
||||
text = re.sub(r'<[^>]*>', '', text)
|
||||
text = re.sub(r'\[[^\]]*\]', '', text)
|
||||
text = re.sub(r'endofpatch|/sil|FFFF|</strong>', '', text)
|
||||
# Strip non-CJK/non-alnum prefix garbage
|
||||
text = re.sub(r'^[^\w一-鿿]+', '', text)
|
||||
text_clean = re.sub(r"\s+", " ", text).strip()
|
||||
|
||||
key = (
|
||||
os.path.splitext(os.path.basename(inputs[i]))[0]
|
||||
if isinstance(inputs[i], str)
|
||||
else f"sample_{i}"
|
||||
)
|
||||
result = {"key": key, "text": text_clean}
|
||||
|
||||
# Timestamps via CTC forced alignment
|
||||
if self.ctc_decoder is not None and self.ctc_tokenizer is not None:
|
||||
try:
|
||||
timestamps = self._compute_timestamps(
|
||||
encoder_outputs[i][0], encoder_outputs[i][1], text_clean
|
||||
)
|
||||
if timestamps:
|
||||
result["timestamps"] = timestamps
|
||||
except Exception as e:
|
||||
logger.debug(f"Timestamp computation failed for {key}: {e}")
|
||||
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
@torch.no_grad()
|
||||
def _compute_timestamps(self, encoder_out, encoder_out_lens, text):
|
||||
"""CTC forced alignment for character-level timestamps."""
|
||||
from funasr.models.fun_asr_nano.tools.utils import forced_align
|
||||
|
||||
decoder_out, decoder_out_lens = self.ctc_decoder(encoder_out, encoder_out_lens)
|
||||
ctc_logits = self.ctc.log_softmax(decoder_out)
|
||||
x = ctc_logits[0, : encoder_out_lens[0].item(), :]
|
||||
|
||||
target_ids = torch.tensor(self.ctc_tokenizer.encode(text), dtype=torch.int64)
|
||||
if len(target_ids) == 0:
|
||||
return []
|
||||
|
||||
timestamps = forced_align(x, target_ids, self.blank_id)
|
||||
for ts in timestamps:
|
||||
ts["token"] = self.ctc_tokenizer.decode([ts["token"]])
|
||||
ts["start_time"] = ts["start_time"] * 6 * 10 / 1000
|
||||
ts["end_time"] = ts["end_time"] * 6 * 10 / 1000
|
||||
return timestamps
|
||||
|
||||
@classmethod
|
||||
def from_pretrained(
|
||||
cls,
|
||||
model: str = "FunAudioLLM/Fun-ASR-Nano-2512",
|
||||
hub: str = "ms",
|
||||
device: str = "cuda:0",
|
||||
dtype: str = "bf16",
|
||||
tensor_parallel_size: int = 1,
|
||||
gpu_memory_utilization: float = 0.8,
|
||||
max_model_len: int = 2048,
|
||||
**kwargs,
|
||||
) -> "FunASRNanoVLLM":
|
||||
"""Load model from hub or local path.
|
||||
|
||||
Args:
|
||||
model: Model name or local directory path.
|
||||
hub: "ms" (ModelScope) or "hf" (HuggingFace).
|
||||
device: Device for audio encoder/adaptor.
|
||||
dtype: Compute dtype ("bf16", "fp16", "fp32").
|
||||
tensor_parallel_size: GPUs for vLLM tensor parallel.
|
||||
gpu_memory_utilization: GPU memory fraction for vLLM.
|
||||
max_model_len: Maximum sequence length.
|
||||
|
||||
Returns:
|
||||
Initialized FunASRNanoVLLM engine.
|
||||
"""
|
||||
if os.path.isdir(model):
|
||||
model_dir = model
|
||||
else:
|
||||
if hub in ("ms", "modelscope"):
|
||||
from modelscope.hub.snapshot_download import snapshot_download
|
||||
|
||||
model_dir = snapshot_download(model, revision=kwargs.pop("revision", "master"))
|
||||
elif hub in ("hf", "huggingface"):
|
||||
from huggingface_hub import snapshot_download
|
||||
|
||||
model_dir = snapshot_download(model)
|
||||
else:
|
||||
raise ValueError(f"Unsupported hub: {hub}. Use 'ms' or 'hf'.")
|
||||
|
||||
logger.info(f"Model directory: {model_dir}")
|
||||
return cls(
|
||||
model_dir=model_dir,
|
||||
device=device,
|
||||
dtype=dtype,
|
||||
tensor_parallel_size=tensor_parallel_size,
|
||||
gpu_memory_utilization=gpu_memory_utilization,
|
||||
max_model_len=max_model_len,
|
||||
**kwargs,
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user