Buckets:
| import os | |
| import glob | |
| import math | |
| import csv | |
| import torch | |
| from torch.utils.data import Dataset, DataLoader | |
| from torch.nn.utils.rnn import pad_sequence | |
| from tqdm import tqdm | |
| from torch.amp import autocast | |
| from config import (PAD_TOKEN_ID, START_OF_SPEECH_TOKEN_ID, | |
| END_OF_SPEECH_TOKEN_ID, AUDIO_OFFSET) | |
| from model import create_model, save_checkpoint | |
| from tokenizer import TTSTokenizer | |
| # ── Хиперпараметри ─────────────────────────────────────────────── | |
| PEAK_LR = 7e-5 | |
| START_LR = 0 | |
| MIN_LR = 5e-6 | |
| WEIGHT_DECAY = 0.01 | |
| EPOCHS = 20 | |
| BATCH_SIZE = 64 | |
| ACCUM_STEPS = 1 # Без accumulation | |
| GRAD_CLIP = 1.0 | |
| CKPT_EVERY = 1000 # Checkpoint на всеки N optimizer стъпки | |
| LOG_FILE = "train_log.csv" | |
| # ── Dataset ────────────────────────────────────────────────────── | |
| class ShardedTTSDataset(Dataset): | |
| def __init__(self, data_dir): | |
| self.shard_files = sorted(glob.glob(os.path.join(data_dir, "*.pt"))) | |
| self.samples = [] | |
| print(f"Зареждане на {len(self.shard_files)} шарда...") | |
| for sf in self.shard_files: | |
| self.samples.extend(torch.load(sf, weights_only=False)) | |
| print(f"Общо записи: {len(self.samples):,}") | |
| def __len__(self): | |
| return len(self.samples) | |
| def __getitem__(self, idx): | |
| item = self.samples[idx] | |
| return { | |
| 'text_ids': item['text_ids'].clone().detach().long(), | |
| 'audio_codes': item['audio_codes'].clone().detach().long(), | |
| 'speaker_emb': item['speaker_emb'].clone().detach().float(), | |
| } | |
| def collate_fn(batch): | |
| enc_ids_list, dec_ids_list, labels_list, speaker_embs = [], [], [], [] | |
| for item in batch: | |
| enc_ids_list.append(item['text_ids']) | |
| audio_codes = item['audio_codes'] + AUDIO_OFFSET | |
| # GPT-style: model.py вътрешно shift-ва logits[:, :-1] vs labels[:, 1:] | |
| # Затова dec_ids и labels трябва да са подравнени, а model-ът сам измества. | |
| dec_ids_list.append(torch.cat([torch.tensor([START_OF_SPEECH_TOKEN_ID]), audio_codes, torch.tensor([END_OF_SPEECH_TOKEN_ID])])) | |
| labels_list.append(torch.cat([torch.tensor([-100]), audio_codes, torch.tensor([END_OF_SPEECH_TOKEN_ID])])) | |
| speaker_embs.append(item['speaker_emb']) | |
| enc_ids = pad_sequence(enc_ids_list, batch_first=True, padding_value=PAD_TOKEN_ID) | |
| dec_ids = pad_sequence(dec_ids_list, batch_first=True, padding_value=PAD_TOKEN_ID) | |
| labels = pad_sequence(labels_list, batch_first=True, padding_value=-100) | |
| enc_mask = (enc_ids != PAD_TOKEN_ID).long() | |
| speaker_emb = torch.stack(speaker_embs) | |
| return enc_ids, dec_ids, enc_mask, labels, speaker_emb | |
| # ── LR Scheduler: Warmup + Cosine Decay ───────────────────────── | |
| def get_lr(step: int, warmup_steps: int, total_steps: int) -> float: | |
| if step < warmup_steps: | |
| return START_LR + (PEAK_LR - START_LR) * (step / max(1, warmup_steps)) | |
| else: | |
| progress = (step - warmup_steps) / max(1, total_steps - warmup_steps) | |
| cosine = 0.5 * (1.0 + math.cos(math.pi * progress)) | |
| return MIN_LR + (PEAK_LR - MIN_LR) * cosine | |
| # ── Основен тренировъчен цикъл ─────────────────────────────────── | |
| def train(): | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| print(f"Устройство: {device}") | |
| processed_dir = os.path.abspath("../data/processed") | |
| if not os.path.exists(processed_dir): | |
| print(f"[ГРЕШКА] {processed_dir} не съществува!"); return | |
| dataset = ShardedTTSDataset(processed_dir) | |
| dataloader = DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True, | |
| collate_fn=collate_fn, num_workers=4, pin_memory=True) | |
| steps_per_epoch = len(dataloader) // ACCUM_STEPS # optimizer стъпки на епоха | |
| warmup_steps = steps_per_epoch * 2 # Warmup = 2 епохи | |
| total_steps = steps_per_epoch * EPOCHS | |
| print(f"Батчове/епоха: {len(dataloader):,} | Optimizer стъпки/епоха: {steps_per_epoch:,} | Accum: {ACCUM_STEPS}") | |
| print(f"Warmup: {warmup_steps:,} стъпки (2 епохи) | Общо: {total_steps:,}") | |
| print(f"Peak LR: {PEAK_LR}, Min LR: {MIN_LR}, Weight Decay: {WEIGHT_DECAY}, Epochs: {EPOCHS}") | |
| print(f"Ефективен batch size: {BATCH_SIZE * ACCUM_STEPS}") | |
| model = create_model(device=device) | |
| model.train() | |
| optimizer = torch.optim.AdamW(model.parameters(), lr=PEAK_LR, weight_decay=WEIGHT_DECAY, | |
| betas=(0.9, 0.999), eps=1e-8) | |
| # BF16 — без GradScaler (не е нужен при bfloat16) | |
| os.makedirs("checkpoints", exist_ok=True) | |
| # CSV лог за реално наблюдение | |
| log_path = LOG_FILE | |
| log_f = open(log_path, "w", newline="") | |
| writer = csv.writer(log_f) | |
| writer.writerow(["step", "batch_loss", "avg_loss", "lr"]) | |
| log_f.flush() | |
| print(f"Loss лог: {log_path} (следи с: tail -f {log_path})\n") | |
| step = 0 | |
| running_loss = 0.0 | |
| running_count = 0 | |
| for epoch in range(EPOCHS): | |
| loop = tqdm(total=steps_per_epoch, desc=f"Епоха {epoch+1}/{EPOCHS}") | |
| epoch_loss_sum, valid_batches = 0.0, 0 | |
| optimizer.zero_grad(set_to_none=True) | |
| for i, (enc_ids, dec_ids, enc_mask, labels, spk_emb) in enumerate(dataloader): | |
| enc_ids = enc_ids.to(device) | |
| dec_ids = dec_ids.to(device) | |
| enc_mask = enc_mask.to(device) | |
| labels = labels.to(device) | |
| spk_emb = spk_emb.to(device) | |
| with autocast('cuda', dtype=torch.bfloat16): | |
| out = model(enc_ids=enc_ids, dec_ids=dec_ids, | |
| enc_mask=enc_mask, dec_labels=labels, | |
| speaker_emb=spk_emb) | |
| loss = out['loss'] / ACCUM_STEPS | |
| loss.backward() | |
| batch_loss = loss.item() * ACCUM_STEPS # реалният loss | |
| epoch_loss_sum += batch_loss | |
| valid_batches += 1 | |
| if (i + 1) % ACCUM_STEPS == 0: | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), GRAD_CLIP) | |
| optimizer.step() | |
| optimizer.zero_grad(set_to_none=True) | |
| step += 1 | |
| current_lr = get_lr(step, warmup_steps, total_steps) | |
| for pg in optimizer.param_groups: | |
| pg['lr'] = current_lr | |
| running_loss += batch_loss | |
| running_count += 1 | |
| avg_loss = running_loss / running_count | |
| writer.writerow([step, f"{batch_loss:.4f}", f"{avg_loss:.4f}", f"{current_lr:.2e}"]) | |
| log_f.flush() | |
| loop.update(1) | |
| loop.set_postfix(step=step, loss=f"{batch_loss:.4f}", | |
| avg=f"{avg_loss:.4f}", lr=f"{current_lr:.2e}") | |
| if step % CKPT_EVERY == 0: | |
| ckpt_dir = f"checkpoints/step_{step:06d}" | |
| save_checkpoint(model, optimizer, None, step, | |
| avg_loss, ckpt_dir, best_val_loss=None) | |
| tqdm.write(f" ✓ Checkpoint запазен: {ckpt_dir} | step={step} | avg_loss={avg_loss:.4f}") | |
| loop.close() | |
| epoch_avg = epoch_loss_sum / max(1, valid_batches) | |
| ckpt_dir = f"checkpoints/epoch_{epoch+1}_final" | |
| save_checkpoint(model, optimizer, None, step, epoch_avg, ckpt_dir, best_val_loss=None) | |
| print(f"\n✓ Епоха {epoch+1} завърши. Средна загуба: {epoch_avg:.4f}") | |
| print(f" Checkpoint: {ckpt_dir}") | |
| log_f.close() | |
| print("\n[КРАЙ] Обучението приключи успешно!") | |
| if __name__ == "__main__": | |
| train() | |
Xet Storage Details
- Size:
- 8.43 kB
- Xet hash:
- 3de82cccd1d2867a1ff724e1ae7214473e7e9bcfc65395bc4e03f16cd89f8cf4
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.