File size: 6,656 Bytes
656ef99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
caefe89
 
 
656ef99
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import os,json,time,random,gc,hashlib
from pathlib import Path
import torch
from datasets import load_dataset
from transformers import AutoTokenizer,AutoModelForCausalLM,Trainer,TrainingArguments,set_seed
from peft import LoraConfig,get_peft_model
from huggingface_hub import HfApi,hf_hub_download
from sacrebleu.metrics import CHRF,BLEU
BASE='utter-project/EuroLLM-1.7B';REPO='SEE7/AstheralTranslate';DATA='Helsinki-NLP/opus-100'
api=HfApi();rev=api.model_info(BASE).sha;datarev=api.dataset_info(DATA).sha
out=Path('/tmp/translate');out.mkdir(exist_ok=True)
set_seed(42);rng=random.Random(42);start=time.monotonic()
langs={'de':'German','fr':'French','es':'Spanish','it':'Italian','pt':'Portuguese'}
tok=AutoTokenizer.from_pretrained(BASE,revision=rev);tok.pad_token=tok.eos_token;tok.padding_side='left'
def prompt(text,lang):return 'Translate from English to '+langs[lang]+'. Return only the translation.\nEnglish: '+text.strip()+'\n'+langs[lang]+':'
train=[];tests=[];seen=set()
for lang in langs:
 conf='-'.join(sorted(['en',lang]))
 ds=load_dataset(DATA,conf,split='train',revision=datarev)
 ds=ds.shuffle(seed=42)
 count=0
 for row in ds:
  x=row['translation'];a=x['en'].strip();b=x[lang].strip()
  if not (8<=len(a)<=600 and 4<=len(b)<=700):continue
  key=(lang,a,b)
  if key in seen:continue
  pre=tok(prompt(a,lang),add_special_tokens=True)['input_ids']
  answer=tok(' '+b+tok.eos_token,add_special_tokens=False)['input_ids']
  if len(pre)+len(answer)>256:continue
  seen.add(key);train.append({'input_ids':pre+answer,'labels':[-100]*len(pre)+answer,'source':a,'target':b,'language':lang})
  count+=1
  if count==2000:break
 print('DATA',lang,count,flush=True)
 test=load_dataset(DATA,conf,split='test',revision=datarev)
 for row in test:
  x=row['translation'];a=x['en'].strip();b=x[lang].strip()
  if 8<=len(a)<=300 and 4<=len(b)<=400 and (lang,a,b) not in seen:
   tests.append({'source':a,'reference':b,'language':lang})
   if sum(t['language']==lang for t in tests)==25:break
assert len(tests)==125
rng.shuffle(train)
(out/'training_manifest.json').write_text(json.dumps({'base':BASE,'revision':rev,'dataset':DATA,'dataset_revision':datarev,'examples':len(train),'per_language':2000,'seed':42,'sha256':hashlib.sha256(json.dumps(train,sort_keys=True).encode()).hexdigest()},indent=2))
model=AutoModelForCausalLM.from_pretrained(BASE,revision=rev,torch_dtype=torch.bfloat16,device_map={'':'cuda'},attn_implementation='sdpa').eval()
report={'base':BASE,'base_revision':rev,'dataset_revision':datarev,'parameters':sum(p.numel() for p in model.parameters()),'languages':langs,'hardware':torch.cuda.get_device_name(),'training_examples':len(train)}
@torch.inference_mode()
def evaluate(label):
 rows=[]
 for i,t in enumerate(tests):
  x=tok(prompt(t['source'],t['language']),return_tensors='pt').to('cuda')
  y=model.generate(**x,max_new_tokens=128,do_sample=False,pad_token_id=tok.pad_token_id,eos_token_id=tok.eos_token_id)
  raw=tok.decode(y[0,x['input_ids'].shape[1]:],skip_special_tokens=True)
  pred=raw.strip().split('\n')[0].strip()
  rows.append({**t,'prediction':pred,'raw':raw,'generated_tokens':int(y.shape[1]-x['input_ids'].shape[1])})
  if i%10==0:print('EVAL',label,i,flush=True)
 scores={}
 for lang in langs:
  r=[r for r in rows if r['language']==lang];hyp=[r['prediction'] for r in r];refs=[[r['reference'] for r in r]]
  scores[lang]={'chrf':CHRF().corpus_score(hyp,refs).score,'bleu':BLEU().corpus_score(hyp,refs).score,'n':len(r)}
 (out/(label+'_outputs.json')).write_text(json.dumps(rows,ensure_ascii=False,indent=2))
 print('SCORES',label,json.dumps(scores),flush=True)
 return scores
report['base_scores']=evaluate('base')
model=get_peft_model(model,LoraConfig(r=32,lora_alpha=64,lora_dropout=0.05,target_modules=['q_proj','k_proj','v_proj','o_proj','gate_proj','up_proj','down_proj'],task_type='CAUSAL_LM'))
report['trainable_parameters']=sum(p.numel() for p in model.parameters() if p.requires_grad)
model.config.use_cache=False
def collate(rows):
 length=max(len(r['input_ids']) for r in rows)
 return {'input_ids':torch.tensor([[tok.pad_token_id]*(length-len(r['input_ids']))+r['input_ids'] for r in rows]),'labels':torch.tensor([[-100]*(length-len(r['labels']))+r['labels'] for r in rows]),'attention_mask':torch.tensor([[0]*(length-len(r['input_ids']))+[1]*len(r['input_ids']) for r in rows])}
args=TrainingArguments(output_dir='/tmp/checkpoints',max_steps=500,per_device_train_batch_size=4,gradient_accumulation_steps=4,learning_rate=1e-4,warmup_ratio=0.05,bf16=True,gradient_checkpointing=True,gradient_checkpointing_kwargs={'use_reentrant':False},save_strategy='no',logging_steps=20,report_to='none',remove_unused_columns=False,seed=42)
trainer=Trainer(model=model,args=args,train_dataset=train,data_collator=collate)
result=trainer.train();report['training']=result.metrics;report['optimizer_steps']=trainer.state.global_step
model.save_pretrained(out/'adapter');model.gradient_checkpointing_disable();model.config.use_cache=True;model.eval()
model=model.merge_and_unload(safe_merge=True);model.config.astheral_version='translate-1'
model.save_pretrained(out/'model',safe_serialization=True,max_shard_size='4GB');tok.save_pretrained(out/'model')
del trainer,model;gc.collect();torch.cuda.empty_cache()
model=AutoModelForCausalLM.from_pretrained(out/'model',torch_dtype=torch.bfloat16,device_map={'':'cuda'},attn_implementation='sdpa').eval()
report['finetuned_scores']=evaluate('finetuned')
report['mean_chrf_base']=sum(v['chrf'] for v in report['base_scores'].values())/5
report['mean_chrf_finetuned']=sum(v['chrf'] for v in report['finetuned_scores'].values())/5
report['improved']=report['mean_chrf_finetuned']>report['mean_chrf_base']
report['elapsed_seconds']=time.monotonic()-start
(out/'evaluation.json').write_text(json.dumps(report,indent=2))
(out/'train.py').write_text(Path(__file__).read_text())
api.upload_folder(repo_id=REPO,folder_path=str(out/'model'),commit_message='Publish actual merged AstheralTranslate weights after reload and translation evaluation')
api.upload_folder(repo_id=REPO,folder_path=str(out/'adapter'),path_in_repo='adapter',commit_message='Publish trained rank32 translation adapter')
api.upload_folder(repo_id=REPO,folder_path=str(out),allow_patterns=['*.json','*.py'],commit_message='Publish translation training provenance and before/after test outputs')
import urllib.request
license_path=str(out/'LICENSE')
Path(license_path).write_bytes(urllib.request.urlopen('https://www.apache.org/licenses/LICENSE-2.0.txt',timeout=20).read())
api.upload_file(repo_id=REPO,path_or_fileobj=license_path,path_in_repo='LICENSE')
print('TRANSLATE_COMPLETE',json.dumps(report),flush=True)