Download code/speaker_v4/train.py from laion/tyger-speaker-predictor: direct link, hf CLI and curl.
- Browser
- Download file 15.1 kB
-
https://huggingface.co/laion/tyger-speaker-predictor/resolve/main/code/speaker_v4/train.py
- Command line
-
hf download hf://laion/tyger-speaker-predictor/code/speaker_v4/train.py
-
curl -L -o train.py https://huggingface.co/laion/tyger-speaker-predictor/resolve/main/code/speaker_v4/train.py
15.1 kB
| """Four-frame specialization and a matched layer/hard-example factorial.""" | |
| import json,time | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| from torch import nn | |
| from torch.nn import functional as F | |
| from speaker_v3.train import Student,metrics,checkpoint | |
| from .common import OLD,WORK,FEATURES,SEEDS,atomic,guard,update,report | |
| from . import data | |
| class MixedStudent(Student): | |
| def __init__(self,mix_layers=False): | |
| super().__init__('gru',128);self.mix_layers=mix_layers | |
| if mix_layers:self.layer_logits=nn.Parameter(torch.tensor([-1.5,-1.5,-1.5,3.])) | |
| else:self.register_parameter('layer_logits',None) | |
| def forward(self,x,energy): | |
| if self.mix_layers: | |
| x=(F.layer_norm(x.float(),(1024,))*self.layer_logits.softmax(0)[None,:,None,None]).sum(1) | |
| else:x=x[:,-1].float() | |
| pool=torch.zeros(len(x),2,1024,device=x.device,dtype=x.dtype) | |
| return super().forward(x,pool,energy,False) | |
| class Resident: | |
| """Bounded GPU int8 cache: no repeated random disk I/O per epoch.""" | |
| def __init__(self,indices,with_layers=False,device='cuda:0'): | |
| self.indices=np.asarray(indices,np.int64);self.device=device;self.arrays=data.arrays();self.layers=with_layers | |
| n=len(indices);channels=4 if with_layers else 1 | |
| self.q=torch.empty(n,channels,4,1024,dtype=torch.int8,device=device) | |
| self.scales=torch.empty(n,channels,4,1,dtype=torch.float16,device=device) | |
| extra_q=np.load(FEATURES/'q.npy',mmap_mode='r') if with_layers else None | |
| extra_s=np.load(FEATURES/'scale.npy',mmap_mode='r') if with_layers else None | |
| for start in range(0,n,8192): | |
| end=min(n,start+8192);g=self.indices[start:end] | |
| self.q[start:end,-1]=torch.from_numpy(self.arrays['q'][g,-4:].copy()).to(device) | |
| self.scales[start:end,-1]=torch.from_numpy(self.arrays['scale'][g,-4:].copy()).to(device) | |
| if with_layers: | |
| self.q[start:end,:3]=torch.from_numpy(extra_q[start:end].copy()).to(device) | |
| self.scales[start:end,:3]=torch.from_numpy(extra_s[start:end].copy()).to(device) | |
| self.energy=torch.from_numpy(self.arrays['energy'][self.indices,-4:].copy()).to(device) | |
| self.target=torch.from_numpy(self.arrays['local_target'][self.indices].copy()).to(device) | |
| self.ids_cpu=np.asarray(self.arrays['speaker'][self.indices],np.int64) | |
| self.classes_cpu=np.asarray(self.arrays['activity'][self.indices],np.int64) | |
| self.emotions_cpu=np.asarray(self.arrays['emotion'][self.indices],np.int64) | |
| self.ids=torch.tensor(self.ids_cpu,device=device);self.classes=torch.tensor(self.classes_cpu,device=device) | |
| masks=data.splits();self.split={s:np.flatnonzero(np.isin(self.indices,v)) for s,v in masks.items()} | |
| def batch(self,indices): | |
| index=torch.as_tensor(indices,dtype=torch.long,device=self.device) | |
| x=self.q[index].float()*self.scales[index].float() | |
| return x,self.energy[index].float(),self.target[index].float(),self.classes[index],self.ids[index] | |
| def predict(model,resident,local_indices): | |
| embeddings=[];states=[];model.eval() | |
| for start in range(0,len(local_indices),512): | |
| x,e,*_=resident.batch(local_indices[start:start+512]);emb,logits=model(x,e) | |
| embeddings.append(emb.cpu().numpy());states.append(logits.argmax(-1).cpu().numpy()) | |
| return np.concatenate(embeddings),np.concatenate(states) | |
| def evaluate(model,resident,split,prototypes,threshold=None): | |
| local=resident.split[split] | |
| return metrics(predict(model,resident,local),resident.arrays,resident.indices[local],prototypes,threshold) | |
| class Pairs: | |
| """Training-only, source-aware positives and nearest-identity negatives.""" | |
| def __init__(self,resident,stable): | |
| self.r=resident;train=resident.split['train'];train=train[resident.classes_cpu[train]==0] | |
| self.voices=np.unique(resident.ids_cpu[train]);self.banks={int(s):train[resident.ids_cpu[train]==s] for s in self.voices} | |
| centers=stable[self.voices];scores=centers@centers.T;np.fill_diagonal(scores,-10) | |
| self.neighbors={int(s):self.voices[np.argsort(scores[i])[-8:][::-1]] for i,s in enumerate(self.voices)} | |
| self.positive={};self.transcripts={};clip_idx,info=data.metadata();self.signature={} | |
| for s,bank in self.banks.items(): | |
| for emotion in np.unique(resident.emotions_cpu[bank]): | |
| other=bank[resident.emotions_cpu[bank]!=emotion] | |
| self.positive[(s,int(emotion))]=other if len(other) else bank | |
| for i in bank: | |
| c=int(clip_idx[resident.indices[i]]) | |
| if c<0:raise RuntimeError('Missing training source metadata') | |
| signature=info[c]['text_hash'];self.signature[int(i)]=signature | |
| self.transcripts.setdefault((s,signature),[]).append(int(i)) | |
| self.matched=0;self.pairs=0 | |
| def sample(self,indices,rng,hard=True): | |
| pos=indices.copy();neg=indices.copy() | |
| for j,index in enumerate(indices): | |
| s=int(self.r.ids_cpu[index]) | |
| if s<0:continue | |
| candidates=self.positive[(s,int(self.r.emotions_cpu[index]))] if hard else self.banks[s] | |
| pos[j]=candidates[int(rng.integers(len(candidates)))];match=None | |
| neighbors=self.neighbors[s] if hard else self.voices[self.voices!=s] | |
| if hard: | |
| for other in neighbors: | |
| same=self.transcripts.get((int(other),self.signature[int(index)])) | |
| if same:match=same[int(rng.integers(len(same)))];self.matched+=1;break | |
| if match is None: | |
| other=int(neighbors[int(rng.integers(len(neighbors)))]);bank=self.banks[other];match=bank[int(rng.integers(len(bank)))] | |
| neg[j]=match;self.pairs+=1 | |
| return pos,neg | |
| def objective(m):return m['macro_speaker_accuracy']-.5*m['eer']+.1*m['source_utterance_teacher_cosine'] | |
| def regression(embedding,target,mean): | |
| target=F.normalize(target,dim=-1) | |
| raw=(1-(embedding*target).sum(-1)).mean() | |
| center=(1-(F.normalize(embedding-mean,dim=-1)*F.normalize(target-mean,dim=-1)).sum(-1)).mean() | |
| return .4*raw+.6*center+.05*F.mse_loss(embedding,target)*192 | |
| def train_one(resident,arm,seed,alpha,warm,mix_layers=False,pair_mode=None,epochs=8,pairs=None): | |
| guard();folder=WORK/'models';folder.mkdir(exist_ok=True);name=f'{arm}-s{seed}' | |
| done=folder/(name+'.json');bestfile=folder/(name+'-best.pt');resume=folder/(name+'-resume.pt') | |
| if done.exists():return json.loads(done.read_text()) | |
| torch.manual_seed(seed);model=MixedStudent(mix_layers).to(resident.device) | |
| saved=torch.load(warm,map_location=resident.device,weights_only=False) | |
| missing,unexpected=model.load_state_dict(saved['model'],strict=False) | |
| if unexpected or any(k!='layer_logits' for k in missing):raise RuntimeError('Warm checkpoint topology mismatch') | |
| groups=[{'params':[p for n,p in model.named_parameters() if n!='layer_logits'],'lr':2e-4}] | |
| if mix_layers:groups.append({'params':[model.layer_logits],'lr':.003}) | |
| optimizer=torch.optim.AdamW(groups,weight_decay=.003) | |
| train=resident.split['train'];prototypes=np.load(OLD/'prototypes.npy').astype(np.float32) | |
| prototypes/=np.maximum(np.linalg.norm(prototypes,axis=-1,keepdims=True),1e-8) | |
| stable=data.stable_targets();stable_tensor=torch.from_numpy(stable).to(resident.device) | |
| voices=np.unique(resident.ids_cpu[train]);voices=voices[voices>=0] | |
| proto=torch.from_numpy(prototypes).to(resident.device);mean=model.teacher_mean | |
| contrast=F.normalize(proto[voices]-mean,dim=-1) | |
| lookup=torch.full((500,),-1,dtype=torch.long,device=resident.device);lookup[torch.tensor(voices,device=resident.device)]=torch.arange(len(voices),device=resident.device) | |
| counts=np.bincount(resident.classes_cpu[train],minlength=3).clip(1) | |
| weights=torch.tensor(counts**-.5,dtype=torch.float32,device=resident.device);weights/=weights.mean() | |
| best=-1e9;best_epoch=0;history=[];first_epoch=0;examples=0;batch_size=512 | |
| prior_matches=prior_pairs=0;pair_start=(pairs.matched,pairs.pairs) if pair_mode else (0,0) | |
| if resume.exists(): | |
| z=torch.load(resume,map_location=resident.device,weights_only=False);model.load_state_dict(z['model']);optimizer.load_state_dict(z['optimizer']) | |
| first_epoch=z['epoch'];history=z['history'];best=z['best'];best_epoch=z['best_epoch'];examples=z['examples'] | |
| prior_matches=z.get('matched_pairs',0);prior_pairs=z.get('total_pairs',0) | |
| torch.set_rng_state(z['torch_rng'].cpu());torch.cuda.set_rng_state(z['cuda_rng'].cpu()) | |
| start=time.perf_counter() | |
| update(phase='Training 320-ms ablations',arm=arm,seed=seed,centroid_weight=alpha,epoch=first_epoch, | |
| validation_macro_accuracy=None,validation_eer=None) | |
| for epoch in range(first_epoch,epochs): | |
| model.train();order=np.random.default_rng(seed*100+epoch).permutation(train);rng=np.random.default_rng(seed*200+epoch) | |
| loss_sum=0.;epoch_start=time.perf_counter() | |
| for step,offset in enumerate(range(0,len(order),batch_size)): | |
| if step%100==0:guard() | |
| anchor=order[offset:offset+batch_size];n=len(anchor);chosen=anchor | |
| if pair_mode: | |
| pos,neg=pairs.sample(anchor,rng,hard=pair_mode=='hard');chosen=np.r_[anchor,pos,neg] | |
| x,e,target,activity,speaker=resident.batch(chosen);optimizer.zero_grad(set_to_none=True) | |
| with torch.autocast('cuda',dtype=torch.bfloat16): | |
| embedding,logits=model(x,e);valid=activity==0;reg=embedding.sum()*0 | |
| if valid.any(): | |
| a=embedding[valid];b=target[valid];s=speaker[valid] | |
| reg=(1-alpha)*regression(a,b,mean)+alpha*regression(a,stable_tensor[s],mean) | |
| reg=reg+.2*F.cross_entropy(F.normalize(a-mean,dim=-1)@contrast.T/.12,lookup[s]) | |
| loss=reg+.2*F.cross_entropy(logits,activity,weight=weights) | |
| if pair_mode: | |
| active=activity[:n]==0 | |
| if active.any(): | |
| center=F.normalize(embedding-mean,dim=-1);a=center[:n][active];p=center[n:2*n][active];negative=center[2*n:][active] | |
| positive=(a*p).sum(-1);negative_cos=(a*negative).sum(-1) | |
| loss=loss+.1*(1-positive).mean()+.1*F.relu(negative_cos-positive+.15).mean() | |
| if not torch.isfinite(loss):raise RuntimeError('Nonfinite objective; stop without overwriting baseline') | |
| loss.backward();nn.utils.clip_grad_norm_(model.parameters(),2);optimizer.step() | |
| loss_sum+=float(loss.detach())*n;examples+=len(chosen) | |
| if step%200==0:update(phase='Training 320-ms ablations',arm=arm,seed=seed,epoch=epoch+1,batch=step,centroid_weight=alpha) | |
| validation=evaluate(model,resident,'validation',prototypes);score=objective(validation) | |
| history.append({'epoch':epoch+1,'loss':loss_sum/len(train),'seconds':time.perf_counter()-epoch_start,'validation':validation}) | |
| if score>best: | |
| best=score;best_epoch=epoch+1 | |
| checkpoint({'model':model.state_dict(),'arm':arm,'seed':seed,'alpha':alpha,'mix_layers':mix_layers,'pair_mode':pair_mode, | |
| 'epoch':best_epoch,'validation':validation,'format_version':4},bestfile) | |
| checkpoint({'model':model.state_dict(),'optimizer':optimizer.state_dict(),'epoch':epoch+1,'history':history,'best':best, | |
| 'best_epoch':best_epoch,'examples':examples,'torch_rng':torch.get_rng_state(),'cuda_rng':torch.cuda.get_rng_state(), | |
| 'matched_pairs':prior_matches+pairs.matched-pair_start[0] if pair_mode else 0, | |
| 'total_pairs':prior_pairs+pairs.pairs-pair_start[1] if pair_mode else 0},resume) | |
| if pair_mode: | |
| # Save a separate tiny counter record without duplicating optimizer weights. | |
| atomic(folder/(name+'-pairs.json'),{'matched_pairs':prior_matches+pairs.matched-pair_start[0], | |
| 'total_pairs':prior_pairs+pairs.pairs-pair_start[1]}) | |
| update(phase='Training 320-ms ablations',arm=arm,seed=seed,epoch=epoch+1,validation_macro_accuracy=validation['macro_speaker_accuracy'],validation_eer=validation['eer']) | |
| if epoch>=4 and epoch+1-best_epoch>=3:break | |
| saved=torch.load(bestfile,map_location=resident.device,weights_only=False);model.load_state_dict(saved['model']);model.eval() | |
| local=resident.split['validation'][:1];x,e,*_=resident.batch(local) | |
| with torch.inference_mode(): | |
| for _ in range(10):model(x,e) | |
| torch.cuda.synchronize();t=time.perf_counter() | |
| for _ in range(100):model(x,e) | |
| torch.cuda.synchronize();latency=(time.perf_counter()-t)*10 | |
| result={'arm':arm,'seed':seed,'alpha':alpha,'mix_layers':mix_layers,'pair_mode':pair_mode,'best_epoch':saved['epoch'], | |
| 'parameters':sum(p.numel() for p in model.parameters()),'validation':saved['validation'],'history':history, | |
| 'checkpoint':bestfile.name,'training_seconds_this_run':time.perf_counter()-start,'effective_examples_seen':examples, | |
| 'committed_training_epoch_seconds':sum(h['seconds'] for h in history), | |
| 'student_gpu_batch1_ms':latency,'layer_weights':model.layer_logits.softmax(0).detach().cpu().tolist() if mix_layers else [0,0,0,1]} | |
| if pair_mode: | |
| counts=folder/(name+'-pairs.json') | |
| counters=json.loads(counts.read_text()) if counts.exists() else {} | |
| result['full_transcript_negative_match_rate']=(prior_matches+pairs.matched-pair_start[0])/max(prior_pairs+pairs.pairs-pair_start[1],1) | |
| result['pair_counts']=counters | |
| atomic(done,result) | |
| # Completed best checkpoint + history are retained; only this redundant | |
| # optimizer resume cache is removed after verified successful publication. | |
| if resume.exists():resume.unlink() | |
| del model,optimizer;torch.cuda.empty_cache();return result | |
| def final_tests(results,resident,stage): | |
| prototypes=np.load(OLD/'prototypes.npy').astype(np.float32);prototypes/=np.linalg.norm(prototypes,axis=-1,keepdims=True) | |
| for row in results[stage]: | |
| if row.get('test'):continue | |
| z=torch.load(WORK/'models'/row['checkpoint'],map_location=resident.device,weights_only=False) | |
| model=MixedStudent(row['mix_layers']).to(resident.device);model.load_state_dict(z['model']);model.eval() | |
| row['test']=evaluate(model,resident,'test',prototypes,row['validation']['far05_threshold']) | |
| del model | |
| report(results) | |
| def aggregates(records): | |
| result={} | |
| for arm in sorted({r['arm'] for r in records}): | |
| items=[r for r in records if r['arm']==arm];values={} | |
| for split in ('validation','test'): | |
| if not all(split in r for r in items):continue | |
| values[split]={metric:{'mean':float(np.mean([r[split][metric] for r in items])), | |
| 'std_across_seeds':float(np.std([r[split][metric] for r in items]))} for metric in | |
| ('macro_speaker_accuracy','eer','positive_cosine','impostor_cosine','source_utterance_teacher_cosine','activity_accuracy')} | |
| result[arm]={'seeds':len(items),**values} | |
| return result | |