TensorFlow搭建的ASR模型训练100批次后CTC Loss突变为无穷
我正在用TensorFlow搭建基于Hugging Face Mozilla Common Voice 11数据集的自动语音识别(ASR)神经网络,但模型训练约100批次后,CTC Loss突然变为无穷大。
数据预处理代码
dataset = datasets.load_dataset("mozilla-foundation/common_voice_11_0", "en") dataset = dataset.remove_columns(['client_id', 'audio', 'up_votes', 'down_votes', 'age', 'gender', 'accent', 'locale', 'segment']) def prepare_dataset(batch): wav_file = batch['path'] # Remove file name split = wav_file.split("\\") joined = "\\".join(split[:-1]) + "\\" # Get the train number complete_path = glob.glob(joined + "*") # Combine all the parts file = complete_path[0] + "\\" + split[-1] batch['path'] = file return batch train_dataset = dataset['train'].map(prepare_dataset).shuffle(len(dataset['train'])) val_dataset = dataset['validation'].map(prepare_dataset).shuffle(len(dataset['validation'])) frame_length = 256 frame_step = 160 fft_length = 384 def load_mp3(wav_file): audio = tfio.audio.AudioIOTensor(wav_file, dtype=tf.float32) sample_rate = tf.cast(audio.rate, dtype=tf.int64) audio = tf.squeeze(audio.to_tensor()) audio = tfio.audio.resample(audio, rate_in=sample_rate, rate_out=8000) audio = tfio.audio.fade(audio, fade_in=1000, fade_out=2000, mode="logarithmic") return audio def convert_to_spect(audio): spectrogram = tf.signal.stft( audio, frame_length=frame_length, frame_step=frame_step, fft_length=fft_length ) spectrogram = tf.abs(spectrogram) spectrogram = tf.math.pow(spectrogram, 0.5) spectrogram = tfio.audio.freq_mask(spectrogram, param=25) spectrogram = tfio.audio.time_mask(spectrogram, param=25) spectrogram = tfio.audio.freq_mask(spectrogram, param=25) spectrogram = tfio.audio.time_mask(spectrogram, param=25) means = tf.math.reduce_mean(spectrogram, 1, keepdims=True) stddevs = tf.math.reduce_std(spectrogram, 1, keepdims=True) spectrogram = (spectrogram - means) / (stddevs + 1e-10) return spectrogram def process_text(label): label = tf.strings.lower(label) label = tf.strings.unicode_split(label, input_encoding="UTF-8") label = char_to_num(label) return label def encode_mozilla_sample(wav_file, label): audio = load_mp3(wav_file) spectrogram = convert_to_spect(audio) label = process_text(label) return spectrogram, label
模型代码
def CTCLoss(y_true, y_pred): # Compute the training-time loss value batch_len = tf.cast(tf.shape(y_true)[0], dtype="int64") input_length = tf.cast(tf.shape(y_pred)[1], dtype="int64") label_length = tf.cast(tf.shape(y_true)[1], dtype="int64") input_length = input_length * tf.ones(shape=(batch_len, 1), dtype="int64") label_length = label_length * tf.ones(shape=(batch_len, 1), dtype="int64") loss = tf.keras.backend.ctc_batch_cost(y_true, y_pred, input_length, label_length) return loss def build_model(input_dim, output_dim, rnn_layers=5, conv_units=128, rnn_units=128, dropout=0.5): input_spectrogram = tf.keras.layers.Input((None, input_dim), name="input") x = tf.keras.layers.Reshape((-1, input_dim, 1), name="expand_dim")(input_spectrogram) # Conv layers x = tf.keras.layers.Conv2D( filters=conv_units, kernel_size=[11, 41], strides=[2, 2], padding="same", use_bias=False, name="conv_1", )(x) x = tf.keras.layers.BatchNormalization(name="conv_1_bn")(x) x = tf.keras.layers.ReLU(name="conv_1_relu")(x) x = tf.keras.layers.Conv2D( filters=conv_units, kernel_size=[11, 21], strides=[1, 2], padding="same", use_bias=False, name="conv_2", )(x) x = tf.keras.layers.BatchNormalization(name="conv_2_bn")(x) x = tf.keras.layers.ReLU(name="conv_2_relu")(x) x = tf.keras.layers.Reshape((-1, x.shape[-2] * x.shape[-1]))(x) # RNN layers for i in range(1, rnn_layers + 1): recurrent = tf.keras.layers.GRU( units=rnn_units, activation="tanh", recurrent_activation="sigmoid", use_bias=True, return_sequences=True, reset_after=True, name=f"gru_{i}", ) x = tf.keras.layers.Bidirectional( recurrent, name=f"bidirectional_{i}", merge_mode="concat" )(x) x = tf.keras.layers.BatchNormalization(name=f"rnn_{i}_bn")(x) if i < rnn_layers: x = tf.keras.layers.Dropout(rate=dropout)(x) # Dense layer x = tf.keras.layers.Dense(units=rnn_units * 2, activation="gelu", name="dense_1")(x) x = tf.keras.layers.Dropout(rate=dropout)(x) # Classification layer output = tf.keras.layers.Dense(units=output_dim + 1, activation="softmax", name="output_layer")(x) # Model model = tf.keras.Model(input_spectrogram, output, name="DeepSpeech_2") # Optimizer opt = tf.keras.optimizers.Adam(learning_rate=0.01) # Compile the model and return model.compile(optimizer=opt, loss=CTCLoss) return model # Get the model model = build_model( input_dim=fft_length // 2 + 1, output_dim=char_to_num.vocabulary_size(), rnn_units=32, conv_units=32, rnn_layers=5, dropout=0.5 )
环境配置
- tensorflow: 2.10.1
- python: 3.9.12
- gpu: Nvidia GeForce RTX 3080
- OS: Windows 11
- cuDNN: 8.1
- CUDA: 11.2
已尝试的解决方案
- 调整batch size:256及以上会GPU内存不足,128及以下仍出现Loss突变为无穷的问题
- 减少训练数据量、缩小模型规模
- 更换激活函数:ReLU、LeakyReLU、GELU
- 更换优化器:SGD、Adam、AdamW
- 调整RNN/Conv层数
- 替换为1D Conv层、取消GRU双向设置、换回AdamW(更新后尝试)
补充疑问
我希望从零实现ASR,用现有RTX 3080和Mozilla Common Voice 11数据集能否得到尚可的结果,还是必须使用wav2vec?
解答
1. CTC Loss突变为无穷大的原因及解决办法
CTC Loss出现无穷大,核心原因通常是模型输出的概率分布中,对应真实标签的路径概率趋近于0,导致计算时触发log(0)的情况,进而引发无穷大。结合代码和尝试的操作,给出具体排查方向:
修正CTC Loss的长度计算逻辑
当前Loss函数里,直接用tf.shape(y_pred)[1]作为所有样本的输入长度,tf.shape(y_true)[1]作为所有样本的标签长度,但实际每个样本的输入(频谱图的时间步长)和标签长度各不相同——强制让一个batch内所有样本共享相同长度,会导致部分样本的输入长度小于标签长度,触发CTC的无效路径计算,直接导致Loss爆无穷。解决办法:数据预处理时单独记录每个样本的输入长度(频谱图的时间维度)和标签长度,训练时将这两个作为额外输入传入Loss函数,而非使用batch的统一长度。
降低初始学习率
目前使用的Adam初始学习率0.01对于ASR模型来说过高,容易导致训练后期权重更新幅度过大,模型输出出现极端值(比如某类概率趋近于0)。建议将学习率降到1e-4甚至1e-5,配合学习率衰减策略(如ReduceLROnPlateau)使用。过滤预处理中的异常值
convert_to_spect函数中的掩码操作可能导致部分频谱图的标准差趋近于0,虽然添加了1e-10的偏移,但如果数据本身存在问题(比如静音片段过长),归一化后仍可能出现NaN或极端值。可以在预处理后添加数值限制:spectrogram = tf.clip_by_value(spectrogram, -10, 10)把数值约束在合理范围,避免梯度爆炸。
检查标签处理逻辑
确认char_to_num的词汇表包含所有需要的字符,且标签中没有超出词汇表的内容。另外,CTC要求标签不能有连续重复字符(除非中间有空白符,但当前预处理未处理此情况),这虽不会直接导致Loss无穷,但会影响模型收敛效率。
2. 从零实现ASR的可行性
用RTX 3080和Mozilla Common Voice 11完全可以得到尚可的结果,不需要依赖wav2vec。当前模型框架参考DeepSpeech结构是合理的,解决Loss爆无穷问题后,再调整训练策略即可:
- 使用更小的学习率,延长训练周期
- 增加数据增强手段(如速度扰动、音量扰动)
- 过滤过长/过短的音频和标签数据
- 启用梯度裁剪(
tf.keras.optimizers.Adam(clipnorm=1.0))防止梯度爆炸
训练完成后,在Common Voice测试集上达到80%左右的词错误率(WER)是完全可行的,进一步调优后WER可降至60%以下。
内容的提问来源于stack exchange,提问作者Nathan Montanez

