LLM训练实战手册:Loss解读、学习率调优与梯度问题排查
·
LLM训练实战手册:Loss解读、学习率调优与梯度问题排查
从训练日志到模型收敛,手把手教你成为LLM训练诊断专家
前言
在大模型训练中,看懂训练日志比写训练代码更重要。当你启动一个千亿参数的训练任务,看着满屏滚动的loss数值,你是否能准确判断:
- 这个loss下降速度正常吗?
- 学习率该调大还是调小?
- 为什么loss突然变成NaN了?
- 我的模型真的在"学习"还是在"死记硬背"?
本文将从Loss动力学、学习率策略、梯度异常诊断三个维度,结合真实训练日志和代码示例,帮你建立一套完整的训练监控与故障排查体系。
一、Loss深度解读:听懂模型的"心跳"
1.1 Loss的本质与分类
Loss是模型训练的"体温计",不同任务有不同的Loss函数和解读方式:
| 任务类型 | 常用Loss | 正常收敛范围 | 异常信号 |
|---|---|---|---|
| 因果语言建模 (CLM) | CrossEntropyLoss | 1.0 ~ 3.0 | > 5.0 或 < 0.5 |
| 文本分类 | CrossEntropyLoss | 0.1 ~ 1.0 | 持续 > 2.0 |
| 回归任务 | MSELoss | 依赖数据尺度 | 突然阶跃变化 |
| 多任务学习 | 加权组合Loss | 各分量需单独监控 | 某分量异常飙升 |
1.2 训练日志的"四层解读法"
一份典型的LLM训练日志长这样:
{'loss': 2.3456, 'grad_norm': 1.23, 'learning_rate': 1.5e-4, 'epoch': 0.5, 'step': 1000}
{'loss': 2.1234, 'grad_norm': 0.98, 'learning_rate': 1.8e-4, 'epoch': 0.8, 'step': 1500}
{'loss': 1.9876, 'grad_norm': 0.76, 'learning_rate': 2.0e-4, 'epoch': 1.0, 'step': 2000}
我们需要从四个层次解读:
class TrainingLogAnalyzer:
"""
训练日志分析器 - 四层解读法
"""
def __init__(self):
self.metrics_history = {
'loss': [],
'grad_norm': [],
'lr': [],
'epoch': [],
'step': []
}
def add_log(self, log_entry: dict):
"""添加一条训练日志"""
for key in self.metrics_history:
if key in log_entry:
self.metrics_history[key].append(log_entry[key])
def analyze(self) -> dict:
"""
四层分析:
Layer 1: 绝对值判断 - 数值在合理范围吗?
Layer 2: 趋势判断 - 方向对吗?速度正常吗?
Layer 3: 波动判断 - 稳定吗?有异常尖刺吗?
Layer 4: 关联判断 - loss和grad_norm、lr的关系合理吗?
"""
analysis = {
'layer1_abs': self._check_absolute_values(),
'layer2_trend': self._check_trend(),
'layer3_volatility': self._check_volatility(),
'layer4_correlation': self._check_correlation()
}
return analysis
def _check_absolute_values(self) -> dict:
"""Layer 1: 绝对值检查"""
recent_loss = self.metrics_history['loss'][-10:] if self.metrics_history['loss'] else []
if not recent_loss:
return {"status": "unknown", "message": "暂无数据"}
avg_loss = sum(recent_loss) / len(recent_loss)
if avg_loss > 5.0:
return {"status": "warning", "message": f"Loss偏高 ({avg_loss:.2f}),可能学习率过大或数据有问题"}
elif avg_loss < 0.1:
return {"status": "warning", "message": f"Loss过低 ({avg_loss:.2f}),可能过拟合或标签泄露"}
elif 0.5 <= avg_loss <= 3.0:
return {"status": "good", "message": f"Loss在合理范围 ({avg_loss:.2f})"}
else:
return {"status": "info", "message": f"当前Loss = {avg_loss:.2f}"}
def _check_trend(self) -> dict:
"""Layer 2: 趋势检查"""
if len(self.metrics_history['loss']) < 20:
return {"status": "info", "message": "数据不足,无法判断趋势"}
# 计算最近20步和之前20步的平均loss
recent = self.metrics_history['loss'][-20:]
previous = self.metrics_history['loss'][-40:-20]
recent_avg = sum(recent) / len(recent)
previous_avg = sum(previous) / len(previous)
# 计算下降比例
decrease_rate = (previous_avg - recent_avg) / previous_avg if previous_avg > 0 else 0
if decrease_rate > 0.05:
return {"status": "good", "message": f"Loss持续下降 ({decrease_rate*100:.1f}%),模型在学习 ✅"}
elif -0.02 <= decrease_rate <= 0.05:
return {"status": "warning", "message": f"Loss趋于平稳 ({decrease_rate*100:.1f}%),可能已收敛或陷入局部最优"}
elif decrease_rate < -0.02:
return {"status": "danger", "message": f"Loss不降反升 ({decrease_rate*100:.1f}%),训练不稳定!"}
else:
return {"status": "info", "message": f"Loss变化率 = {decrease_rate*100:.1f}%"}
def _check_volatility(self) -> dict:
"""Layer 3: 波动检查"""
if len(self.metrics_history['loss']) < 20:
return {"status": "info", "message": "数据不足"}
recent = self.metrics_history['loss'][-50:]
mean = sum(recent) / len(recent)
variance = sum((x - mean) ** 2 for x in recent) / len(recent)
std = variance ** 0.5
# 计算相对波动系数 (CV)
cv = std / mean if mean > 0 else 0
# 检测异常尖刺(超过均值+3倍标准差)
outliers = [x for x in recent if x > mean + 3 * std]
if cv < 0.05:
return {"status": "good", "message": f"Loss非常平稳 (CV={cv:.3f})"}
elif cv < 0.15:
return {"status": "good", "message": f"Loss波动正常 (CV={cv:.3f})"}
elif cv < 0.3:
return {"status": "warning", "message": f"Loss波动较大 (CV={cv:.3f}),检查batch size或学习率"}
else:
return {
"status": "danger",
"message": f"Loss剧烈波动 (CV={cv:.3f}),发现 {len(outliers)} 个异常尖刺!可能梯度爆炸"
}
def _check_correlation(self) -> dict:
"""Layer 4: 关联检查 - loss与grad_norm/lr的关系"""
if len(self.metrics_history['loss']) < 10:
return {"status": "info", "message": "数据不足"}
issues = []
# 检查grad_norm
if 'grad_norm' in self.metrics_history:
recent_grad = self.metrics_history['grad_norm'][-20:]
avg_grad = sum(recent_grad) / len(recent_grad)
if avg_grad > 10.0:
issues.append(f"梯度范数过高 ({avg_grad:.1f}),可能梯度爆炸")
elif avg_grad < 0.01:
issues.append(f"梯度范数过低 ({avg_grad:.3f}),可能梯度消失")
elif 0.1 <= avg_grad <= 5.0:
pass # 正常范围
# 检查学习率
if 'lr' in self.metrics_history:
recent_lr = self.metrics_history['lr'][-5:]
if len(recent_lr) > 1 and all(abs(recent_lr[i] - recent_lr[i-1]) < 1e-8 for i in range(1, len(recent_lr))):
# 学习率没变化,检查是否在warmup阶段
pass
if issues:
return {"status": "warning", "message": "; ".join(issues)}
return {"status": "good", "message": "loss与梯度、学习率关系正常"}
1.3 Loss曲线的五大经典模式
import matplotlib.pyplot as plt
import numpy as np
def plot_loss_patterns():
"""
五种经典Loss曲线及诊断
"""
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
steps = np.arange(1000)
# 模式1: 理想收敛 ✅
axes[0,0].plot(steps, 3.0 * np.exp(-steps/300) + 0.8 + 0.05*np.random.randn(1000))
axes[0,0].set_title("✅ 理想收敛:指数下降后平稳")
axes[0,0].set_ylabel("Loss")
# 模式2: 学习率过大 🔥
axes[0,1].plot(steps, 2.0 + 0.5*np.sin(steps/50) + 0.3*np.random.randn(1000))
axes[0,1].set_title("🔥 震荡不收敛:学习率过大")
# 模式3: 过拟合 ⚠️
train_loss = 2.0 * np.exp(-steps/200) + 0.3 + 0.02*np.random.randn(1000)
val_loss = 2.0 * np.exp(-steps/300) + 0.8 + 0.05*np.random.randn(1000)
val_loss[600:] = val_loss[600:] + 0.5 * np.arange(400) / 400
axes[0,2].plot(steps, train_loss, label='Train')
axes[0,2].plot(steps, val_loss, label='Val')
axes[0,2].set_title("⚠️ 过拟合:训练loss下降,验证loss上升")
axes[0,2].legend()
# 模式4: 梯度爆炸 💥
axes[1,0].plot(steps[:900], 2.0 * np.exp(-steps[:900]/200) + 0.5)
axes[1,0].plot(steps[900:], 2.0 * np.exp(-steps[900:]/200) + 0.5 + 100*np.random.randn(100))
axes[1,0].set_title("💥 梯度爆炸:loss突然飙升")
# 模式5: 欠拟合 📉
axes[1,1].plot(steps, 3.0 - 0.3 * (1 - np.exp(-steps/500)) + 0.1*np.random.randn(1000))
axes[1,1].set_title("📉 欠拟合:loss下降太慢")
# 模式6: 正常收敛但有噪声
axes[1,2].plot(steps, 2.8 * np.exp(-steps/250) + 0.6 + 0.08*np.random.randn(1000))
axes[1,2].set_title("✅ 正常收敛(带噪声):batch size较小")
plt.tight_layout()
plt.savefig('loss_patterns.png', dpi=150)
plt.show()
二、学习率调度:训练的"油门与刹车"
2.1 主流学习率调度策略对比
from transformers import get_scheduler
from torch.optim import AdamW
import torch
class LRSchedulerMaster:
"""
学习率调度器全面掌握
"""
@staticmethod
def get_scheduler_examples():
"""
展示五种主流调度策略
"""
# 模拟训练步数
num_steps = 10000
warmup_steps = 500
# 1. 线性衰减 (最常用)
lr_linear = [1e-4 * (1 - i/num_steps) for i in range(num_steps)]
# 2. 余弦退火 (推荐用于LLM)
lr_cosine = [1e-4 * 0.5 * (1 + np.cos(np.pi * i / num_steps)) for i in range(num_steps)]
# 3. 带warmup的余弦 (最佳实践)
def cosine_with_warmup(step, warmup_steps, total_steps, max_lr):
if step < warmup_steps:
return max_lr * step / warmup_steps
progress = (step - warmup_steps) / (total_steps - warmup_steps)
return max_lr * 0.5 * (1 + np.cos(np.pi * progress))
lr_warmup_cosine = [cosine_with_warmup(i, warmup_steps, num_steps, 1e-4) for i in range(num_steps)]
# 4. 恒定学习率 (慎用)
lr_constant = [1e-4] * num_steps
# 5. 多项式衰减
lr_poly = [1e-4 * (1 - i/num_steps)**0.5 for i in range(num_steps)]
# 可视化对比
plt.figure(figsize=(12, 6))
plt.plot(lr_linear, label='Linear Decay')
plt.plot(lr_cosine, label='Cosine Decay')
plt.plot(lr_warmup_cosine, label='Cosine with Warmup ⭐')
plt.plot(lr_constant, label='Constant', linestyle='--')
plt.plot(lr_poly, label='Polynomial Decay')
plt.axvline(x=warmup_steps, color='red', linestyle=':', label='Warmup End')
plt.xlabel('Training Steps')
plt.ylabel('Learning Rate')
plt.title('学习率调度策略对比')
plt.legend()
plt.grid(True, alpha=0.3)
plt.savefig('lr_schedulers.png', dpi=150)
plt.show()
@staticmethod
def detect_lr_issues(history_lr: list, history_loss: list) -> dict:
"""
从训练历史中检测学习率相关问题
"""
issues = []
# 1. 检查学习率是否过小(loss下降太慢)
if len(history_loss) > 100:
recent_loss_avg = sum(history_loss[-50:]) / 50
early_loss_avg = sum(history_loss[:50]) / 50
decrease = (early_loss_avg - recent_loss_avg) / early_loss_avg
if decrease < 0.1 and min(history_lr) < 1e-6:
issues.append("学习率过小,Loss下降缓慢")
# 2. 检查学习率是否过大(loss震荡)
if len(history_loss) > 100:
# 计算后50步的波动
recent = history_loss[-50:]
std = np.std(recent)
mean = np.mean(recent)
if std / mean > 0.2 and max(history_lr) > 1e-3:
issues.append("学习率过大,Loss剧烈震荡")
# 3. 检查warmup是否合理(初始loss是否快速下降)
if len(history_loss) > 50:
first_loss = history_loss[0]
loss_after_warmup = history_loss[50] if len(history_loss) > 50 else history_loss[-1]
if loss_after_warmup > first_loss * 0.9:
issues.append("Warmup可能不足,Loss下降不明显")
return {
"issues": issues,
"status": "danger" if len(issues) > 1 else "warning" if len(issues) == 1 else "good",
"current_lr": history_lr[-1] if history_lr else None,
"suggestion": self._suggest_lr_adjustment(issues, history_lr, history_loss)
}
@staticmethod
def _suggest_lr_adjustment(issues, history_lr, history_loss):
"""根据问题给出学习率调整建议"""
if not issues:
return "当前学习率策略正常,继续训练"
suggestions = []
if any("过大" in issue for issue in issues):
suggestions.append("建议将学习率降低到当前的 1/3 到 1/10")
if any("过小" in issue for issue in issues):
suggestions.append("建议将学习率提高 2-3 倍")
if any("Warmup" in issue for issue in issues):
suggestions.append("建议增加warmup_steps到总步数的10%")
return "; ".join(suggestions)
2.2 实战:使用transformers调度器
from transformers import get_scheduler
from torch.optim import AdamW
from torch.utils.data import DataLoader
from tqdm import tqdm
def train_with_scheduler(
model,
train_dataloader: DataLoader,
num_epochs: int,
lr: float = 1e-4,
warmup_ratio: float = 0.05,
scheduler_type: str = "cosine"
):
"""
完整的训练循环 - 包含学习率调度
"""
# 优化器
optimizer = AdamW(model.parameters(), lr=lr, weight_decay=0.01)
# 计算总训练步数
num_training_steps = num_epochs * len(train_dataloader)
num_warmup_steps = int(num_training_steps * warmup_ratio)
# 学习率调度器
scheduler = get_scheduler(
name=scheduler_type,
optimizer=optimizer,
num_warmup_steps=num_warmup_steps,
num_training_steps=num_training_steps
)
# 训练日志记录
training_log = {
'step': [],
'loss': [],
'lr': [],
'grad_norm': []
}
model.train()
progress_bar = tqdm(range(num_training_steps))
for epoch in range(num_epochs):
for batch in train_dataloader:
# 前向传播
outputs = model(**batch)
loss = outputs.loss
# 反向传播
loss.backward()
# 梯度裁剪(防止梯度爆炸)
grad_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# 优化器步进
optimizer.step()
scheduler.step()
optimizer.zero_grad()
# 记录日志
step = len(training_log['step'])
training_log['step'].append(step)
training_log['loss'].append(loss.item())
training_log['lr'].append(scheduler.get_last_lr()[0])
training_log['grad_norm'].append(grad_norm.item())
# 更新进度条
progress_bar.update(1)
progress_bar.set_postfix({
'loss': f"{loss.item():.4f}",
'lr': f"{scheduler.get_last_lr()[0]:.2e}",
'grad': f"{grad_norm.item():.3f}"
})
# 每100步输出详细日志
if step % 100 == 0:
print(f"\n[Step {step}] loss={loss.item():.4f}, "
f"lr={scheduler.get_last_lr()[0]:.2e}, "
f"grad_norm={grad_norm.item():.3f}")
return training_log
# 使用分析器检查训练日志
def monitor_training(training_log: dict):
"""
实时监控训练状态
"""
analyzer = TrainingLogAnalyzer()
for i in range(len(training_log['step'])):
log_entry = {
'step': training_log['step'][i],
'loss': training_log['loss'][i],
'lr': training_log['lr'][i],
'grad_norm': training_log['grad_norm'][i]
}
analyzer.add_log(log_entry)
# 每200步输出一次分析报告
if i % 200 == 0 and i > 0:
report = analyzer.analyze()
print(f"\n📊 训练状态报告 (Step {i}):")
for layer, result in report.items():
emoji = "🟢" if result['status'] == 'good' else "🟡" if result['status'] == 'warning' else "🔴"
print(f" {emoji} {layer}: {result['message']}")
三、梯度问题诊断:消失、爆炸与NaN
3.1 梯度问题的根源
class GradientDetective:
"""
梯度问题诊断工具
"""
@staticmethod
def diagnose_gradient_issues(
model,
loss,
grad_norm: float,
grad_stats: dict = None
) -> dict:
"""
诊断三类梯度问题
Args:
model: 模型对象
loss: 当前loss值
grad_norm: 梯度范数
grad_stats: 各层梯度统计 {layer_name: {mean, std, max, min}}
"""
diagnosis = {
'gradient_explosion': False,
'gradient_vanishing': False,
'nan_detected': False,
'layer_issues': [],
'suggestions': []
}
# 1. 检查梯度爆炸
if grad_norm > 10.0:
diagnosis['gradient_explosion'] = True
diagnosis['suggestions'].append(
f"梯度范数过高 ({grad_norm:.2f}),建议:\n"
" - 降低学习率\n"
" - 使用梯度裁剪 (clip_grad_norm_)\n"
" - 检查输入数据是否标准化"
)
# 2. 检查梯度消失
if grad_norm < 1e-4:
diagnosis['gradient_vanishing'] = True
diagnosis['suggestions'].append(
f"梯度范数过低 ({grad_norm:.6f}),建议:\n"
" - 检查是否有大量饱和的激活函数(如Sigmoid)\n"
" - 考虑使用残差连接\n"
" - 尝试增加学习率或使用AdamW优化器"
)
# 3. 检查NaN
if np.isnan(grad_norm) or np.isnan(loss.item()):
diagnosis['nan_detected'] = True
diagnosis['suggestions'].append(
"检测到NaN!立即停止训练,建议:\n"
" - 检查学习率是否过大\n"
" - 检查输入数据是否包含NaN或Inf\n"
" - 在loss.backward()前添加 loss = (loss / gradient_accumulation_steps)\n"
" - 使用 torch.autograd.set_detect_anomaly(True) 定位问题"
)
# 4. 逐层检查
if grad_stats:
for layer_name, stats in grad_stats.items():
if stats['std'] / stats['mean'] > 3.0:
diagnosis['layer_issues'].append(
f"{layer_name}: 梯度方差过大 (std/mean={stats['std']/stats['mean']:.2f})"
)
if abs(stats['mean']) < 1e-6:
diagnosis['layer_issues'].append(
f"{layer_name}: 梯度均值接近0,可能梯度消失"
)
return diagnosis
@staticmethod
def gradient_clipping_strategies():
"""
梯度裁剪策略对比
"""
# 1. 全局裁剪 (推荐)
# torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# 2. 逐层裁剪
# for param in model.parameters():
# if param.grad is not None:
# torch.nn.utils.clip_grad_norm_(param, max_norm=1.0)
# 3. 自适应裁剪 (根据历史梯度均值)
# 示例代码见下方
pass
3.2 实战:梯度监控与可视化
class GradientMonitor:
"""
训练过程中实时监控梯度
"""
def __init__(self, model, log_interval: int = 100):
self.model = model
self.log_interval = log_interval
self.history = {
'step': [],
'grad_norm': [],
'grad_max': [],
'grad_mean': [],
'layer_gradients': {}
}
# 注册hook来捕获每层的梯度
self.hooks = []
for name, param in model.named_parameters():
if param.requires_grad:
hook = param.register_hook(
lambda grad, n=name: self._grad_hook(n, grad)
)
self.hooks.append(hook)
self.history['layer_gradients'][name] = []
def _grad_hook(self, name: str, grad):
"""梯度hook函数"""
if len(self.history['layer_gradients'][name]) < 1000: # 限制存储数量
self.history['layer_gradients'][name].append(grad.norm().item())
def step(self, step: int):
"""每步调用,记录全局梯度信息"""
total_norm = 0
max_grad = 0
mean_grad = 0
count = 0
for param in self.model.parameters():
if param.grad is not None:
grad_norm = param.grad.data.norm(2).item()
total_norm += grad_norm ** 2
max_grad = max(max_grad, grad_norm)
mean_grad += grad_norm
count += 1
total_norm = total_norm ** 0.5
mean_grad = mean_grad / count if count > 0 else 0
self.history['step'].append(step)
self.history['grad_norm'].append(total_norm)
self.history['grad_max'].append(max_grad)
self.history['grad_mean'].append(mean_grad)
def plot_gradient_heatmap(self, save_path: str = "gradient_heatmap.png"):
"""
绘制各层梯度分布热力图
"""
layer_names = list(self.history['layer_gradients'].keys())
gradient_data = []
for name in layer_names:
# 取最近100步的平均梯度作为热力图数据
values = self.history['layer_gradients'][name][-100:]
gradient_data.append(values)
# 对齐长度
min_len = min(len(d) for d in gradient_data)
gradient_data = [d[:min_len] for d in gradient_data]
plt.figure(figsize=(12, 8))
im = plt.imshow(gradient_data, aspect='auto', cmap='hot', interpolation='nearest')
plt.colorbar(label='Gradient Norm')
plt.xlabel('Step (last 100)')
plt.ylabel('Layer Index')
plt.title('各层梯度热力图 (🔥 = 大梯度, ⚫ = 小梯度)')
# 标注层名
plt.yticks(range(len(layer_names)), [name[:20] for name in layer_names], fontsize=8)
plt.tight_layout()
plt.savefig(save_path, dpi=150)
plt.show()
def report(self) -> dict:
"""生成梯度报告"""
recent_norms = self.history['grad_norm'][-20:] if self.history['grad_norm'] else []
if not recent_norms:
return {"status": "info", "message": "暂无梯度数据"}
avg_norm = sum(recent_norms) / len(recent_norms)
std_norm = (sum((x - avg_norm) ** 2 for x in recent_norms) / len(recent_norms)) ** 0.5
# 检测异常层
anomaly_layers = []
for name, grads in self.history['layer_gradients'].items():
if grads and len(grads) > 10:
recent_avg = sum(grads[-10:]) / 10
if recent_avg > 10.0:
anomaly_layers.append((name, 'explosion', recent_avg))
elif recent_avg < 1e-5:
anomaly_layers.append((name, 'vanishing', recent_avg))
return {
"status": "good" if not anomaly_layers and avg_norm < 5.0 else "warning",
"avg_grad_norm": avg_norm,
"std_grad_norm": std_norm,
"anomaly_layers": anomaly_layers,
"message": f"平均梯度范数: {avg_norm:.4f}±{std_norm:.4f}"
}
3.3 梯度问题快速修复手册
class QuickFix:
"""
梯度问题快速修复方案
"""
@staticmethod
def fix_nan_issue():
"""NaN问题修复代码模板"""
# 方案1: 启用异常检测
import torch
torch.autograd.set_detect_anomaly(True)
# 方案2: 在loss前添加数值稳定措施
# loss = loss + 1e-8 # 防止log(0)
# 方案3: 使用amp混合精度时添加梯度缩放
# from torch.cuda.amp import GradScaler
# scaler = GradScaler()
# with torch.cuda.amp.autocast():
# loss = model(**batch).loss
# scaler.scale(loss).backward()
# scaler.unscale_(optimizer)
# scaler.step(optimizer)
# scaler.update()
# 方案4: 检查输入数据
# assert not torch.isnan(batch['input_ids']).any()
# assert not torch.isinf(batch['input_ids']).any()
pass
@staticmethod
def fix_gradient_explosion():
"""梯度爆炸修复模板"""
# 方案1: 梯度裁剪
# torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# 方案2: 降低学习率
# optimizer.param_groups[0]['lr'] *= 0.5
# 方案3: 使用更好的初始化
# for layer in model.modules():
# if isinstance(layer, nn.Linear):
# nn.init.xavier_uniform_(layer.weight)
pass
@staticmethod
def fix_gradient_vanishing():
"""梯度消失修复模板"""
# 方案1: 使用残差连接
# 方案2: 避免使用Sigmoid/Tanh,改用ReLU/GELU
# 方案3: 使用BatchNorm/LayerNorm
# 方案4: 增大学习率
pass
四、实战:完整的训练监控系统
import wandb
from datetime import datetime
class TrainingMonitor:
"""
完整的训练监控系统 - 整合所有诊断功能
"""
def __init__(self, model, project_name: str = "llm-training"):
self.model = model
self.analyzer = TrainingLogAnalyzer()
self.gradient_monitor = GradientMonitor(model)
self.lr_scheduler_master = LRSchedulerMaster()
# 初始化wandb日志(可选)
# wandb.init(project=project_name)
self.start_time = datetime.now()
self.alerts = []
def on_step_end(self, step: int, loss: float, grad_norm: float, lr: float):
"""
每步结束时的监控逻辑
"""
# 记录日志
log_entry = {
'step': step,
'loss': loss,
'grad_norm': grad_norm,
'lr': lr
}
self.analyzer.add_log(log_entry)
self.gradient_monitor.step(step)
# 定期检查(每100步)
if step % 100 == 0 and step > 0:
self._check_and_alert(step)
# 记录到wandb
# wandb.log({
# 'loss': loss,
# 'grad_norm': grad_norm,
# 'lr': lr,
# 'step': step
# })
def _check_and_alert(self, step: int):
"""检查并发出警报"""
analysis = self.analyzer.analyze()
grad_report = self.gradient_monitor.report()
alerts = []
# 检查loss异常
if analysis['layer1_abs']['status'] == 'warning':
alerts.append(f"⚠️ Loss异常: {analysis['layer1_abs']['message']}")
if analysis['layer2_trend']['status'] == 'danger':
alerts.append(f"🔴 Loss趋势异常: {analysis['layer2_trend']['message']}")
# 检查梯度异常
if grad_report['status'] == 'warning':
alerts.append(f"⚠️ 梯度异常: {grad_report['message']}")
# 输出警报
if alerts:
print(f"\n🚨 训练警报 (Step {step}):")
for alert in alerts:
print(f" {alert}")
self.alerts.append({'step': step, 'alerts': alerts})
def generate_final_report(self) -> str:
"""
生成最终训练报告
"""
elapsed = (datetime.now() - self.start_time).total_seconds()
report = f"""
╔══════════════════════════════════════════════════════════╗
║ 训练完成报告 ║
╠══════════════════════════════════════════════════════════╣
║ 训练时长: {elapsed/60:.1f} 分钟
║ 总步数: {len(self.analyzer.metrics_history['step'])}
║ 最终Loss: {self.analyzer.metrics_history['loss'][-1] if self.analyzer.metrics_history['loss'] else 'N/A'}
║ 最终LR: {self.analyzer.metrics_history['lr'][-1] if self.analyzer.metrics_history['lr'] else 'N/A'}
╠══════════════════════════════════════════════════════════╣
║ 警报数量: {len(self.alerts)}
║ 收敛状态: {self.analyzer.analyze()['layer2_trend']['message']}
╠══════════════════════════════════════════════════════════╣
║ 📌 建议:
"""
# 添加最终建议
final_analysis = self.analyzer.analyze()
if final_analysis['layer1_abs']['status'] == 'warning':
report += f"\n - {final_analysis['layer1_abs']['message']}"
if final_analysis['layer2_trend']['status'] == 'warning':
report += f"\n - {final_analysis['layer2_trend']['message']}"
report += "\n╚══════════════════════════════════════════════════════════╝"
return report
五、常见故障速查表
| 故障现象 | 可能原因 | 快速诊断 | 修复方案 |
|---|---|---|---|
| Loss = NaN | 学习率过大、数据包含NaN、梯度爆炸 | 检查torch.isnan(loss) |
降低lr 10倍,启用梯度裁剪 |
| Loss不下降 | 学习率过小、数据问题、模型初始化差 | 对比初始loss和当前loss | 提高lr 3倍,检查数据预处理 |
| Loss震荡剧烈 | 学习率过大、batch size太小 | 计算loss的CV值 | 降低lr至1/3,增大batch size |
| 验证Loss上升 | 过拟合 | 对比train/val loss | 增加weight_decay,减少epochs |
| 梯度范数>10 | 梯度爆炸 | 监控grad_norm | 梯度裁剪,降低lr |
| 梯度范数<1e-6 | 梯度消失 | 检查各层梯度分布 | 使用残差连接,更换激活函数 |
| 训练过早收敛 | 陷入局部最优 | loss曲线平坦 | 使用余弦退火重启学习率 |
| GPU OOM | batch size过大、内存碎片 | 监控显存使用 | 减小batch size,使用gradient accumulation |
六、总结:训练监控的黄金法则
6.1 每日检查清单
daily_checklist = """
□ Loss值在合理范围(1-3 for CLM)
□ Loss趋势持续下降(最近100步下降>5%)
□ Loss波动正常(CV < 0.15)
□ 梯度范数在0.1-5.0之间
□ 学习率按计划调度
□ 无NaN/Inf出现
□ 显存使用稳定
□ 训练速度正常(it/s)
"""
6.2 一句话总结
训练监控的本质,是建立对"正常状态"的直觉。 当loss、梯度、学习率三者处于动态平衡时,模型就在健康地学习。任何一方的异常,都会在另外两者上留下痕迹。
📚 扩展资源
原创声明:本文为CSDN博主原创文章,基于真实LLM训练经验总结,欢迎分享交流。遇到具体训练问题,欢迎在评论区留言讨论!
最后更新:2026年6月
更多推荐



所有评论(0)