Text Classification
PEFT
Safetensors
English
gemma4
lora
multi-label-classification
text-only
research-prototype
Instructions to use mts-the-architect/open-sensations-gemma4-e2b-bf16-lora with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use mts-the-architect/open-sensations-gemma4-e2b-bf16-lora with PEFT:
from peft import PeftModel from transformers import AutoModel base_model = AutoModel.from_pretrained("google/gemma-4-E2B") model = PeftModel.from_pretrained(base_model, "mts-the-architect/open-sensations-gemma4-e2b-bf16-lora") - Notebooks
- Google Colab
- Kaggle
Download train_gemma.py from mts-the-architect/open-sensations-gemma4-e2b-bf16-lora: direct link, hf CLI and curl.
- Browser
- Download file 9.84 kB
-
https://huggingface.co/mts-the-architect/open-sensations-gemma4-e2b-bf16-lora/resolve/main/train_gemma.py
- Command line
-
hf download hf://mts-the-architect/open-sensations-gemma4-e2b-bf16-lora/train_gemma.py
-
curl -L -o train_gemma.py https://huggingface.co/mts-the-architect/open-sensations-gemma4-e2b-bf16-lora/resolve/main/train_gemma.py
9.84 kB
| """Text-only Gemma adapters: human baseline, replay control and synthetic arm.""" | |
| import argparse | |
| import hashlib | |
| import importlib.metadata | |
| import json | |
| import math | |
| import os | |
| import random | |
| import time | |
| from pathlib import Path | |
| os.environ.setdefault('HF_HOME',str(Path(__file__).resolve().parent/'.cache/huggingface')) | |
| import numpy as np | |
| import torch | |
| from torch.utils.data import DataLoader | |
| from transformers import AutoTokenizer, set_seed, get_linear_schedule_with_warmup | |
| from gemma_classifier import BASE, REVISION, new_classifier, load_classifier | |
| from metrics_gemma import classification_metrics | |
| ROOT=Path(__file__).resolve().parent | |
| SEED=20261004 | |
| def write(path,data): | |
| path=Path(path);path.parent.mkdir(parents=True,exist_ok=True) | |
| path.write_text(json.dumps(data,indent=2)+'\n',encoding='utf-8') | |
| def rows(path):return [json.loads(line) for line in path.read_text(encoding='utf-8').splitlines()] | |
| def train(task): | |
| out=ROOT/'outputs/gemma'/task | |
| if out.exists():raise RuntimeError(f'Refusing output overwrite: {out}') | |
| out.mkdir(parents=True) | |
| data=ROOT/'data'/('open-emotions-gemma' if task=='open-emotions' else task) | |
| meta=json.loads((data/'data_manifest.json').read_text()) | |
| for name,digest in meta['files_sha256'].items(): | |
| assert hashlib.sha256((data/name).read_bytes()).hexdigest()==digest | |
| names=meta['labels'];parts={s:rows(data/f'{s}.jsonl') for s in ['train','validation','test']} | |
| generated=rows(data/'synthetic.jsonl') | |
| # Bound continuation cost while keeping identical human replay in both arms. | |
| rng=random.Random(SEED);replay=rng.sample(parts['train'],min(6000,len(parts['train']))) | |
| epochs=10 if task=='open-sensations' else 1 | |
| limit=256 if task=='open-morals' else 128 | |
| batch_size=8;accumulation=4 | |
| settings=dict(seed=SEED,epochs=epochs,microbatch_size=batch_size,gradient_accumulation_steps=accumulation, | |
| effective_batch_size=32,max_length=limit,lora_rank=8,lora_alpha=16,lora_dropout=.05, | |
| lora_targets=['q_proj','v_proj'],learning_rate=1e-4,head_learning_rate=1e-3, | |
| continuation_epochs=1,continuation_learning_rate=5e-5,continuation_head_learning_rate=5e-4, | |
| precision='BF16 frozen backbone; FP32 adapters/head',optimizer='AdamW',weight_decay=.01, | |
| warmup_ratio=.06,max_grad_norm=1.,attention='eager',pooling='last_non_padding_token') | |
| plan=dict(task=task,mode='full',completed=False,base_model=BASE,base_revision=REVISION,labels=names,settings=settings, | |
| data=meta,text_only=True,generation_enabled=False,tool_calling=False,multimodality=False, | |
| gpu=torch.cuda.get_device_name(0),hardware='ASUS ROG Flow Z13 2025 GZ302EA; Ryzen AI Max+395; Radeon8060S; 128GB unified RAM; Ubuntu24.04 WSL2', | |
| versions={n:importlib.metadata.version(n) for n in ['torch','transformers','peft','accelerate','numpy']}, | |
| source_sha256={n:hashlib.sha256((ROOT/n).read_bytes()).hexdigest() for n in ['gemma_classifier.py','train_gemma.py','prepare_gemma_data.py']}, | |
| continuation_replay_ids=[r['id'] for r in replay], | |
| selection_rule='Candidate validation macro F1 must exceed both baseline/control by >=0.005, with micro F1 loss <=0.01 against each; otherwise baseline. Freeze before test.', | |
| comparison_limits='Same human splits and label definitions as RoBERTa; different tokenizers, model sizes, precision and training/adaptation budgets. Not a controlled architecture-only ablation.', | |
| sensory_limits='Target-in-context associations, not a whole-sentence presence classifier; tiny held-out word-group sample.', | |
| synthetic_compute_note='Identical sampled human replay in both continuations; synthetic arm has extra examples/optimizer steps.') | |
| write(out/'experiment_plan.json',plan) | |
| started=time.monotonic() | |
| tokenizer=AutoTokenizer.from_pretrained(BASE,revision=REVISION);tokenizer.padding_side='right' | |
| def collate(items): | |
| encoded=tokenizer([r['text'] for r in items],padding=True,truncation=True,max_length=limit,return_tensors='pt') | |
| encoded={k:v.cuda() for k,v in encoded.items() if k in ['input_ids','attention_mask']} | |
| encoded['labels']=torch.tensor([r['targets'] for r in items],device='cuda',dtype=torch.float32) | |
| return encoded | |
| def predict(model,items): | |
| model.eval();scores=[];truth=[] | |
| for batch in DataLoader(items,batch_size=batch_size,collate_fn=collate): | |
| truth.append(batch.pop('labels').cpu().numpy()>=.5) | |
| scores.append(model(**batch).logits.sigmoid().cpu().numpy()) | |
| return np.concatenate(scores),np.concatenate(truth) | |
| def calibrate(model): | |
| scores,truth=predict(model,parts['validation']) | |
| objective='micro_f1' if task=='open-emotions' else 'macro_f1' | |
| threshold=float(max(np.arange(.1,.81,.05),key=lambda t:classification_metrics(truth,scores>=t,names)[objective])) | |
| return threshold,classification_metrics(truth,scores>=threshold,names),objective | |
| def fit(model,items,count,arm,lr,head_lr): | |
| set_seed(SEED);loader=DataLoader(items,batch_size=batch_size,shuffle=True,collate_fn=collate) | |
| adapter_params=[p for p in model.backbone.parameters() if p.requires_grad] | |
| opt=torch.optim.AdamW([{'params':adapter_params,'lr':lr},{'params':model.head.parameters(),'lr':head_lr}],weight_decay=.01) | |
| updates=count*math.ceil(len(loader)/accumulation) | |
| scheduler=get_linear_schedule_with_warmup(opt,math.ceil(updates*.06),updates) | |
| best=-1;history=[];arm_start=time.monotonic() | |
| for epoch in range(1,count+1): | |
| model.train();loss_sum=0.;opt.zero_grad(set_to_none=True) | |
| for step,batch in enumerate(loader,1): | |
| loss=model(**batch).loss | |
| if not torch.isfinite(loss):raise RuntimeError('Non-finite loss') | |
| group=min(accumulation,len(loader)-((step-1)//accumulation)*accumulation) | |
| (loss/group).backward() | |
| if step%accumulation==0 or step==len(loader): | |
| torch.nn.utils.clip_grad_norm_([*adapter_params,*model.head.parameters()],1.,error_if_nonfinite=True) | |
| opt.step();scheduler.step();opt.zero_grad(set_to_none=True) | |
| loss_sum+=loss.item() | |
| if step==1 or step%50==0 or step==len(loader): | |
| print(task,arm,'epoch',epoch,'step',step,'/',len(loader),'loss',round(loss.item(),5),'seconds',round(time.monotonic()-arm_start,1),flush=True) | |
| threshold,metrics,objective=calibrate(model) | |
| history.append(dict(epoch=epoch,mean_train_loss=loss_sum/len(loader),threshold=threshold,validation=metrics)) | |
| print(task,arm,'validation',metrics['micro_f1'],metrics['macro_f1'],flush=True) | |
| if metrics[objective]>best: | |
| best=metrics[objective] | |
| model.save(out/arm/'model',tokenizer,dict(global_threshold=threshold,max_length=limit,selected_on='validation',objective=objective)) | |
| write(out/arm/'history.json',history) | |
| selected=max(history,key=lambda r:r['validation'][objective]) | |
| return dict(examples=len(items),epochs=count,optimizer_steps=updates,elapsed_seconds=time.monotonic()-arm_start, | |
| history=history,selected_epoch=selected['epoch'],validation=selected['validation'],threshold=selected['threshold']) | |
| set_seed(SEED) | |
| torch.cuda.reset_peak_memory_stats() | |
| model=new_classifier(names) | |
| plan['parameter_counts']=dict(total=sum(p.numel() for p in model.parameters()),trainable=sum(p.numel() for p in model.parameters() if p.requires_grad)) | |
| write(out/'experiment_plan.json',plan) | |
| arms={'baseline':fit(model,parts['train'],epochs,'baseline',1e-4,1e-3)} | |
| del model;torch.cuda.empty_cache() | |
| for arm,items in [('real-only',replay),('synthetic',replay+generated)]: | |
| model,_,_=load_classifier(out/'baseline/model',trainable=True) | |
| arms[arm]=fit(model,items,1,arm,5e-5,5e-4) | |
| del model;torch.cuda.empty_cache() | |
| candidate=arms['synthetic']['validation'] | |
| accepted=all(candidate['macro_f1']>=arms[a]['validation']['macro_f1']+.005 and candidate['micro_f1']>=arms[a]['validation']['micro_f1']-.01 for a in ['baseline','real-only']) | |
| decision=dict(selected_arm='synthetic' if accepted else 'baseline',synthetic_accepted=accepted,selection='validation only; frozen before test evaluation') | |
| write(out/'decision.json',decision) | |
| for arm in arms: | |
| model,_,_=load_classifier(out/arm/'model') | |
| scores,truth=predict(model,parts['test']) | |
| arms[arm]['test']=classification_metrics(truth,scores>=arms[arm]['threshold'],names) | |
| write(out/arm/'metrics.json',arms[arm]) | |
| del model;torch.cuda.empty_cache() | |
| plan.update(completed=True,decision=decision,arms=arms,elapsed_seconds=time.monotonic()-started, | |
| peak_gpu_allocated_bytes=torch.cuda.max_memory_allocated(),peak_gpu_reserved_bytes=torch.cuda.max_memory_reserved()) | |
| write(out/'run.json',plan) | |
| for arm in arms:write(out/arm/'model/training_run.json',{**plan,'arm':arm}) | |
| print('COMPLETE',task,decision,arms[decision['selected_arm']]['test'],flush=True) | |
| def main(): | |
| parser=argparse.ArgumentParser();parser.add_argument('task',choices=['all','open-emotions','open-morals','open-sensations']);args=parser.parse_args() | |
| torch.set_num_threads(8) | |
| if not torch.cuda.is_available():raise RuntimeError('GPU required') | |
| for task in (['open-sensations','open-morals','open-emotions'] if args.task=='all' else [args.task]):train(task) | |
| if __name__=='__main__':main() | |