训练Chatbot模型出现Epoch损失为0异常,求问题排查方案
自定义Chatbot训练损失归零问题分析与解决思路
问题背景
作为深度学习领域新手,训练自定义Chatbot模型时遇到异常:100轮Epoch后损失变为0,模型完全失去训练价值。已自行编写preprocessing.py代码及training_data.json数据集,训练代码如下:
import tensorflow as tf import json import numpy as np from tensorflow import keras from keras.preprocessing.text import Tokenizer from keras.utils.data_utils import pad_sequences from keras.layers import Input, LSTM, Dense, Attention, Embedding, Dropout, LayerNormalization from keras.models import Model from keras.optimizers import Adam from keras.callbacks import EarlyStopping from keras.initializers import GlorotUniform def create_chatbot_model(vocab_size, max_seq_length): input_layer = Input(shape=(max_seq_length,)) embedding_layer = Embedding(vocab_size, 512, embeddings_initializer=GlorotUniform(seed=42))(input_layer) lstm_layer = LSTM(1024, return_sequences=True, dropout=0.2, recurrent_dropout=0.2)(embedding_layer) attention_layer = Attention(use_scale=True)([lstm_layer, lstm_layer]) normalized_attention = LayerNormalization()(attention_layer) dropout_layer = Dropout(0.2)(normalized_attention) output_layer = Dense(vocab_size, activation='softmax')(dropout_layer) model = Model(inputs=input_layer, outputs=output_layer) model.compile(loss='categorical_crossentropy', optimizer=Adam(learning_rate=0.0005)) return model def train_chatbot_model(model, x_train, y_train): optimizer = Adam(learning_rate=0.0005) loss_function = tf.keras.losses.CategoricalCrossentropy() train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train)).batch(128) for epoch in range(100): epoch_loss = tf.keras.metrics.Mean() for x_batch, y_batch in train_dataset: with tf.GradientTape() as tape: logits = model(x_batch) loss_value = loss_function(y_batch, logits) gradients = tape.gradient(loss_value, model.trainable_variables) optimizer.apply_gradients(zip(gradients, model.trainable_variables)) epoch_loss(loss_value) print(f'Epoch {epoch+1}, Loss: {epoch_loss.result()}') # Load and preprocess the training data def prepare_training_data(): with open('training_data.json', 'r') as file: training_data = json.load(file) user_messages = [] aim_responses = [] for dialogues in training_data: user_dialogue = [] aim_dialogue = [] for turn in dialogues: if turn['role'] == 'user': user_dialogue.append(turn['content']) else: aim_dialogue.append(turn['content']) user_messages.append(user_dialogue) aim_responses.append(aim_dialogue) tokenizer = Tokenizer() tokenizer.fit_on_texts(np.concatenate(user_messages)) max_seq_length = 30 x_train = [] y_train = [] for user_dialogue, aim_dialogue in zip(user_messages, aim_responses): user_sequences = tokenizer.texts_to_sequences(user_dialogue) aim_sequences = tokenizer.texts_to_sequences(aim_dialogue) for i in range(1, len(user_sequences)): x_train.append(user_sequences[:i]) y_train.append(aim_sequences[i]) x_train = pad_sequences(x_train, maxlen=max_seq_length, padding='post') y_train = pad_sequences(y_train, maxlen=max_seq_length, padding='post') vocab_size = len(tokenizer.word_index) + 1 return x_train, y_train, vocab_size, max_seq_length # Main code execution if __name__ == '__main__': x_train, y_train, vocab_size, max_seq_length = prepare_training_data() chatbot_model = create_chatbot_model(vocab_size, max_seq_length) train_chatbot_model(chatbot_model, x_train, y_train) chatbot_model.save('trained_chatbot_model.h5')
问题原因分析
- 标签与损失函数不匹配:模型使用
categorical_crossentropy损失,但y_train是整数索引序列,未做one-hot编码。该损失函数要求标签为one-hot格式,模型会错误将整数索引当作有效one-hot向量学习,最终输出对应位置的1,损失直接归零,但完全不具备泛化能力。 - 数据预处理逻辑混乱:
- 遍历对话时,假设用户消息和助手回复轮次完全匹配,若存在轮次不匹配的对话,会导致数据错位;
x_train.append(user_sequences[:i])将前i轮用户消息的序列列表直接作为输入,pad_sequences会把整个列表当成单个序列处理,输入格式完全错误;- Tokenizer仅在用户消息上训练,未包含助手回复的词汇,导致回复中的未知词汇被忽略,编码信息缺失。
- 训练与模型结构的缺陷:
- 未对pad的0位置做掩码处理,这些无效位置参与损失计算,模型会倾向于预测0来快速降低损失;
- 自定义训练循环中重复定义优化器和损失函数,与模型compile时的配置冲突,可能导致优化状态异常;
- 未使用EarlyStopping等回调,即使过拟合仍继续训练到100轮,加剧损失异常。
解决思路
修正标签与损失函数匹配问题:
改用SparseCategoricalCrossentropy损失(适配整数标签),同时移除输出层的softmax激活,使用from_logits=True让损失计算包含softmax,数值更稳定:# 修改模型输出层 output_layer = Dense(vocab_size)(dropout_layer) # 修改损失函数 loss_function = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)修复数据预处理逻辑:
- 确保用户消息与助手回复轮次匹配,过滤掉不匹配的对话:
for user_dialogue, aim_dialogue in zip(user_messages, aim_responses): # 只保留轮次一致的对话 if len(user_dialogue) != len(aim_dialogue): continue # 单轮对话配对 for user_msg, aim_msg in zip(user_dialogue, aim_dialogue): x_train.append(user_msg) y_train.append(aim_msg) - Tokenizer同时拟合用户消息和助手回复的文本,覆盖全量词汇:
# 展平所有文本 user_flat = [msg for dialog in user_messages for msg in dialog] aim_flat = [msg for dialog in aim_responses for msg in dialog] tokenizer.fit_on_texts(user_flat + aim_flat)
- 确保用户消息与助手回复轮次匹配,过滤掉不匹配的对话:
优化模型与训练逻辑:
- 在Embedding层添加
mask_zero=True,自动忽略pad的0位置:embedding_layer = Embedding(vocab_size, 512, mask_zero=True, embeddings_initializer=GlorotUniform(seed=42))(input_layer) - 使用
model.fit()替代自定义训练循环,避免配置冲突,同时加入EarlyStopping防止过拟合:early_stopping = EarlyStopping(monitor='loss', patience=5, restore_best_weights=True) model.fit(x_train, y_train, batch_size=128, epochs=100, callbacks=[early_stopping])
- 在Embedding层添加
内容的提问来源于stack exchange,提问作者tasisotop
相关产品推荐
相关产品推荐

