Allenare Llama2 e Mistral con Python sui propri dati
Questo manuale è un punto di partenza per coloro che desiderano personalizzare potenti modelli linguistici come Llama 2 e Mistral per i propri progetti.
Attraverso l'utilizzo di QLoRA, esploreremo passo dopo passo il finetuning di questi ampi modelli linguistici (LLM), anche nel caso in cui non si disponga di un supercomputer a disposizione.
Punto Chiave: La base per un modello efficace è la qualità dei dati.
Esamineremo l'addestramento su dati esistenti e la creazione di un proprio dataset.
Imparerai come formattare i tuoi dati per l'addestramento, specificamente per il formato ChatML. Il codice è mantenuto semplice, senza l'utilizzo di scatole nere aggiuntive o strumenti di addestramento complessi, utilizzando solamente i pacchetti base di PyTorch e Hugging Face.
Se sei curioso della tecnologia dietro le quinte, ecco alcune letture consigliate:
Cosa Imparerai in questa Guida per fare il Finetuning di Llama2 e Mistral con Python :
Come trovare e preparare dataset adeguati
Trasformare i dataset nel formato ChatML per l'addestramento
Caricare una versione quantizzata del tuo modello base e collegare gli adattatori LoRA
La scelta delle impostazioni di addestramento più adatte
Questo viaggio esplorerà le fondamenta dell'addestramento di modelli linguistici avanzati, fornendo una panoramica chiara e pratica delle tecniche e strumenti necessari per ottimizzare e adattare questi modelli di IA per specifici ambiti di interesse.
Prerequisiti
Prima di iniziare, avrai bisogno degli ultimi strumenti di Hugging Face. Esegui il seguente comando nel tuo terminale per installare o aggiornare questi pacchetti:
pip install -U accelerate bitsandbytes datasets peft transformers tokenizers
#se stai usando colab
!pip install -U accelerate bitsandbytes datasets peft transformers tokenizers
A titolo di riferimento, queste sono le versioni specifiche utilizzate per creare questo tutorial:
accelerate 0.24.1
bitsandbytes 0.41.1
datasets 2.14.6
peft 0.6.0
transformers 4.35.0
tokenizers 0.14.1
torch 2.1.0
1. Datasets: Esistenti o Crea il Tuo
Questa sezione è dedicata al processo cruciale di caricamento o creazione di un dataset, e successivamente alla formattazione secondo la struttura ChatML. In seguito, approfondiremo le tecniche di tokenizzazione e batching nella prossima sezione.
Tieni presente che la qualità del tuo dataset è essenziale: avrà un impatto significativo sulle prestazioni del tuo modello. È essenziale che il dataset sia ben adattato al tuo compito.
Strategia generale
I dataset possono essere composti da varie fonti. Prendiamo ad esempio il fine-tuning di Open Hermes 2 su Mistral, che è stato addestrato su ~900.000 esempi da una molteplicità di dataset.
Questi dataset spesso comprendono coppie domanda-risposta, formattate sia come coppie isolate (un singolo esempio equivale a una singola domanda e risposta) o concatenate in una sequenza dialogica (formattate come Q/A, Q/A, Q/A).
Questa sezione mira a guidarti nella conversione di questi dataset in un formato uniforme compatibile con gli schemi di addestramento. Per prepararsi all'addestramento, è necessario selezionare un formato. Qui ho scelto il ChatML di OpenAI perché è stato adottato frequentemente nelle recenti versioni dei modelli e potrebbe diventare il nuovo standard.
Di seguito è riportato un esempio di dialogo formattato in ChatML (dal dataset Open Orca):
<|im_start|>system
You are an AI assistant. User will you give you a task. Your goal is to
complete the task as faithfully as you can. While performing the task
think step-by-step and justify your steps.<|im_end|>
<|im_start|>user
Premise: A man is inline skating in front of a wooden bench. Hypothesis:
A man is having fun skating in front of a bench. .Choose the correct
answer: Given the premise, can we conclude the hypothesis?
Select from: a). yes b). it is not possible to tell c). no<|im_end|>
<|im_start|>assistant
b). it is not possible to tell Justification: Although the man is inline
skating in front of the wooden bench, we cannot conclude whether he is
having fun or not, as his emotions are not explicitly mentioned.<|im_end|>
L'esempio sopra può essere tokenizzato, raggruppato e inserito nell'algoritmo di addestramento. Tuttavia, prima di procedere, esamineremo alcuni dataset ben noti e come prepararli e formattarli.
Come caricare i dati di Open Assistant
Cominciamo con il dataset Open Assistant.
from datasets import load_dataset
# Carica il dataset "OpenAssistant/oasst_top1_2023-08-25"
dataset = load_dataset("OpenAssistant/oasst_top1_2023-08-25")
Dopo il caricamento, il dataset è prediviso in suddivisioni per l'addestramento (13.000 voci) e il test (700 voci).
>>> dataset
DatasetDict({
train: Dataset({
features: ['text'],
num_rows: 12947
})
test: Dataset({
features: ['text'],
num_rows: 690
})
})
Diamo uno sguardo alla prima voce:
>>> print(dataset["train"][0]["text"])
<|im_start|>user
Consigliami 5 nomi per il mio cucciolo di dobberman<|im_end|>
<|im_start|>assistant
Ecco 5 nomi per il tuo cucciolo di dobermann:
- Zeus
- Apollo
- Thor
- Athena
- Odin<|im_end|>
Che comodità!
Questo è già in formato ChatML, quindi non dobbiamo fare nulla.
Tranne dire al tokenizzatore e al modello che le stringhe <|im_start|> e <|im_end|> sono token, non devono essere divise, ed <|im_end|> è un token speciale ( eos, "fine sequenza") che segna la fine di una risposta da parte del modello, altrimenti il modello genererà per sempre e mai smettere.
Come integrare questi token con modelli base come llama2 e mistral sarà approfondito nella Sezione 3. L'uso di dataset già strutturati come il dataset Open Assistant semplifica il processo di adattamento e formattazione per l'addestramento dei modelli di linguaggio, permettendo di concentrarsi maggiormente sull'ottimizzazione delle prestazioni dei modelli.
Come caricare i dati di Open Orca
Passiamo a Open Orca, un dataset che comprende 4,2 milioni di voci e richiede una suddivisione in train/test dopo il caricamento, che può essere ottenuta utilizzando train_test_split.
from datasets import load_dataset
# Carica il dataset "Open-Orca/OpenOrca"
dataset = load_dataset("Open-Orca/OpenOrca")
dataset = dataset["train"].train_test_split(test_size=0.1)
Esaminiamo la struttura del dataset. Ecco la prima voce:
{
'id': 'flan.2020759',
'system_prompt': 'Tu sei un assistente AI. Ti verrà assegnato un compito.
Devi generare una risposta dettagliata e lunga.',
'question': 'Ülke, bildirgeyi uygulamaya başlayan son ülkeler
arasında olmasına rağmen 46 ülke arasında 24. sırayı
aldı. Potresti tradurlo in inglese, per favore?',
'response': 'Nonostante sia uno degli ultimi paesi ad
attuare la dichiarazione, si è classificato al 24° su 46 paesi.'
}
Si tratta di una coppia domanda+risposta e di un messaggio del sistema che descrive il contesto in cui deve essere data la risposta alla domanda.
A differenza del dataset Open Assistant, dobbiamo formattare i dati di Open Orca come ChatML da soli.
def format_conversation(row):
template="""<|im_start|>system
{sys}<|im_end|>
<|im_start|>user
{q}<|im_end|>
<|im_start|>assistant
{a}<|im_end|>"""
conversation=template.format(
sys=row["system_prompt"],
q=row["question"],
a=row["response"],
)
return {"text": conversation}
import os
dataset = dataset.map(
format_conversation,
remove_columns=dataset["train"].column_names, # rimuovi tutte le colonne; resterà solo "text"
num_proc=os.cpu_count() # multithreaded
)
Ora, il dataset è pronto per essere tokenizzato e inserito nel processo di addestramento.
Creazione di un dataset basato su un transcript di un podcast
Precedentemente ho addestrato llama1 sui transcript del podcast di Lex Friedman. Questo compito ha coinvolto la trasformazione di un podcast noto per le sue discussioni approfondite in un set di addestramento che permette a un'intelligenza artificiale di imitare il modo di parlare di Lex.
from datasets import load_dataset
import json
# Carica il dataset "g-ronimo/lfpodcast"
dataset = load_dataset("g-ronimo/lfpodcast")
dataset = dataset["train"].train_test_split(test_size=0.1)
# Ispezione della prima voce nel set di addestramentoprint(json.dumps(dataset["train"][0], indent=2))
Questa struttura cattura l'essenza di ogni episodio del podcast, ma per prepararlo per l'addestramento del modello, la conversazione deve essere trasformata nel formato ChatML. Dovremo passare attraverso ogni turno di messaggio, applicare la formattazione ChatML e concatenare i messaggi per memorizzare l'intero transcript dell'episodio in un singolo campo di testo. I ruoli di Ospite e Lex saranno riassunti rispettivamente come utente e assistente, per condizionare il modello linguistico ad adottare la personalità curiosa e competente di Lex.
def format_conversation(row):
# Template per i turni di conversazione nel formato ChatML
template="""<|im_start|>system
{sys}<|im_end|>
<|im_start|>user
{q}<|im_end|>
<|im_start|>assistant
{a}<|im_end|>"""
turns=row["conversation"]
# Se Lex è il primo speaker, salta il suo turno per iniziare con la domanda dell'Ospiteif turns[0]["from"]=="Lex":
turns=turns[1:]
conversation=[]
for i in range(0, len(turns), 2):
# Si assume che la conversazione alterna sempre tra Ospite e Lex
question=turns[i] # Ospite
answer=turns[i+1] # Lex
conversation.append(
template.format(
q=question["text"],
a=answer["text"],
))
return {"text": "\n".join(conversation)}
import os
dataset = dataset.map(
format_conversation,
remove_columns=dataset["train"].column_names,
num_proc=os.cpu_count()
)
Applicando questi cambiamenti, il dataset risultante sarà pronto per essere tokenizzato e alimentare il processo di addestramento, insegnando quindi al modello linguistico a conversare in modo simile alle discussioni del podcast di Lex Fridman. Se sei curioso, prova llama-friedman.
Creare il tuo dataset per fare il finetuning di modelli LLM
Penso che tu abbia già compreso l'idea. Ecco alcune ulteriori idee per la creazione di dataset creative che GPT-4 ha suggerito:
Dataset dei Discorsi delle Figure Storiche
Raccogli discorsi, lettere e opere scritte di figure storiche per creare un dataset che rifletta il loro stile di parlare e scrivere. Questo potrebbe essere utilizzato per generare contenuti educativi, come interviste simulate con figure storiche, o per creare esperienze narrative in cui queste figure forniscono commenti sugli eventi odierni.
Enciclopedia di Mondi Fittizi
Crea un dataset da vari romanzi fantasy e di fantascienza che dettagli gli elementi di costruzione del mondo all'interno di queste storie, come geografia, sistemi politici, specie e tecnologia. Ciò potrebbe essere utilizzato per addestrare un'intelligenza artificiale a generare nuovi mondi fantasy o per fornire informazioni contestuali ricche per lo sviluppo dei giochi.
Dataset di Dialoghi Emotivi
Analizza script di film, opere teatrali e romanzi per creare un dataset di dialoghi contrassegnati con toni emotivi corrispondenti. Questo dataset potrebbe essere utilizzato per addestrare un sistema AI che riconosce e genera dialoghi con sottotoni emotivi sfumati, utile per migliorare le risposte empatiche di chatbot e assistenti virtuali.
Dataset di Recensioni e Specifiche dei Prodotti Tecnologici
Raccogli un dataset completo di recensioni di prodotti tecnologici, specifiche e commenti degli utenti da varie fonti. Questo dataset potrebbe alimentare un motore di raccomandazione o un sistema AI progettato per fornire consigli d'acquisto ai consumatori.
Caricare e preparare il modello e il tokenizzatore
Prima di iniziare a elaborare i dati che abbiamo appena preparato, dobbiamo caricare il modello e il tokenizzatore e assicurarci che elaborino correttamente i tag ChatML <|im_start|>e <|im_end|>siano consapevoli <|im_end|>del (nuovo) token eos.
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer, BitsAndBytesConfig
from peft import prepare_model_for_kbit_training, LoraConfig, get_peft_model
modelpath="models/Mistral-7B-v0.1"
# Load 4-bit quantized model
model = AutoModelForCausalLM.from_pretrained(
modelpath,
device_map="auto",
quantization_config=BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_quant_type="nf4",
),
torch_dtype=torch.bfloat16,
)
# Load (slow) Tokenizer, fast tokenizer sometimes ignores added tokens
tokenizer = AutoTokenizer.from_pretrained(modelpath, use_fast=False)
# Add tokens <|im_start|> and <|im_end|>, latter is special eos token
tokenizer.pad_token = "</s>"
tokenizer.add_tokens(["<|im_start|>"])
tokenizer.add_special_tokens(dict(eos_token="<|im_end|>"))
model.resize_token_embeddings(len(tokenizer))
model.config.eos_token_id = tokenizer.eos_token_id
Poiché non stiamo addestrando tutti i parametri ma solo un sottoinsieme, dobbiamo aggiungere gli adattatori LoRA al modello utilizzando Huggingface peft. Assicurati di utilizzare peft>= 0,6, altrimenti 1) get_peft_modelsarà molto lento e 2) l'allenamento fallirà con Mistral.
# Aggiungi adattatori LoRA al modello
model = prepare_model_for_kbit_training(model)
config = LoraConfig(
r= 64 ,
lora_alpha= 16 ,
target_modules = [ 'q_proj' , 'k_proj' , 'down_proj' , 'v_proj' , 'gate_proj' , 'o_proj' , 'up_proj' ],
lora_dropout= 0.1 ,
bias= "none" ,
moduli_da_salvare = [ "lm_head" , "embed_tokens" ], # necessario perché abbiamo aggiunto nuovi token al tokenizzatore/modello
task_type= "CAUSAL_LM"
)
model = get_peft_model(model , config)
model.config.use_cache = False
rank LoRA r: specifica su quanti parametri eseguire l'addestramento. Più alto è il rango, più parametri verranno addestrati e più grandi saranno i file dell'adattatore. Di solito un numero compreso tra 8 e 128. Il valore massimo possibile, ad es. addestrando tutti i parametri, sarebbe 4096 per llama2-7b e Mistral (= hidden_sizein config.json) e vanificherebbe lo scopo dell'aggiunta di adattatori. Il documento QLoRA suggerisce 64Guanaco (set di dati Open Assistant) che funziona bene per me.
target_modules: Un altro suggerimento/scoperta degli autori di QLoRA nel loro articolo :
scopriamo che l'iperparametro LoRA più critico è il numero di adattatori LoRA utilizzati in totale e che i LoRA su tutti gli strati di blocchi del trasformatore lineare sono necessari per soddisfare le prestazioni di regolazione fine complete
modules_to_save: specifica i moduli oltre ai livelli LoRA da impostare come addestrabili e salvati nel checkpoint finale. Dato che abbiamo aggiunto i tag ChatML come token al vocabolario, dobbiamo addestrare e salvare anche il livello lineare lm_heade la matrice di incorporamento embed_tokens. Ciò sarà rilevante per unire nuovamente l'adattatore nel modello base in un secondo momento.
Tokenizzazione
Tokenizziamo il campo di testo nel nostro dataset senza aggiungere token speciali o padding, poiché lo faremo manualmente.
def tokenize(element):
return tokenizer(
element["text"],
truncation=True,
max_length=2048,
add_special_tokens=False,
)
dataset_tokenized = dataset.map(
tokenize,
batched=True,
num_proc=os.cpu_count(), # multithreaded
remove_columns=["text"] # non necessitiamo più delle stringhe, da qui in poi abbiamo solo i token
)
max_length: specifica la lunghezza massima di un campione (in numero di token). Tutto ciò che supera i 2048 token verrà troncato e non sarà utilizzato per l'addestramento. Se il dataset ha solo brevi coppie di domande/risposte in un singolo campione (ad es. Open Orca), questo sarà più che sufficiente. Se i campioni sono più lunghi (ad es. una trascrizione di un podcast), idealmente dovresti aumentare max_length (consuma VRAM) o dividere i campioni in più parti più piccole. Il valore massimo per llama2 è 4096.
Creazione di batch
Il trainer di Hugging Face richiede una funzione di collazione per trasformare una lista di campioni in un dizionario che contiene un batch con:
input_ids (testo tokenizzato)
labels (testo obiettivo, identico a input_ids)
attention_masks (tensore di zeri e uni).
Adotteremo una versione semplificata di DataCollatorForCausalLM dal repository QLoRA per questo scopo.
def collate(elements):
tokenlist = [e["input_ids"] for e in elements]
tokens_maxlen = max([len(t) for t in tokenlist]) # lunghezza del più lungo input
input_ids, labels, attention_masks = [], [], []
for tokens in tokenlist:
pad_len = tokens_maxlen - len(tokens) # quanti token di padding aggiungere per questo campione# pad di input_ids con pad_token, di labels con ignore_index (-100) e set di attention_mask a 1 dove c'è contenuto, altrimenti 0
input_ids.append(tokens + [tokenizer.pad_token_id] * pad_len)
labels.append(tokens + [-100] * pad_len)
attention_masks.append([1] * len(tokens) + [0] * pad_len)
batch = {
"input_ids": torch.tensor(input_ids),
"labels": torch.tensor(labels),
"attention_mask": torch.tensor(attention_masks)
}
return batch
Iperparametri di Addestramento
La scelta degli iperparametri può incidere notevolmente sulle prestazioni del modello. Ecco gli iperparametri che abbiamo selezionato per il nostro addestramento:
bs=8: Dimensione del batch. Cerchiamo di massimizzarla per aumentare la velocità, ma riduciamo se c'è mancanza di memoria.
ga_steps=1: Passi di accumulo del gradiente. Aumenta la dimensione effettiva del batch senza consumare più memoria ma rende l'addestramento più lento. La dimensione effettiva del batch è data da batch_size * gradient_accumulation_steps.
epochs=5: Numero di epoche. Dipende dal dataset e dall'andamento della valutazione durante l'addestramento.
steps_per_epoch=len(dataset_tokenized["train"])//(bs*ga_steps): Numero di passi per epoca, considerando la dimensione del batch e gli step di accumulo del gradiente.
args = TrainingArguments(
output_dir="out",
per_device_train_batch_size=bs,
per_device_eval_batch_size=bs,
evaluation_strategy="steps",
logging_steps=1,
eval_steps=steps_per_epoch, # valutazione e salvataggio una volta per epoca
save_steps=steps_per_epoch,
gradient_accumulation_steps=ga_steps,
num_train_epochs=epochs,
lr_scheduler_type="constant",
optim="paged_adamw_32bit",
learning_rate=0.0002,
group_by_length=True,
fp16=True,
ddp_find_unused_parameters=False, # necessario per l'addestramento con accelerate
)
num_train_epochs: Il numero di epoche da addestrare dipende dal dataset. Idealmente, la perdita sulla suddivisione di valutazione indica quando interrompere l'addestramento e quale checkpoint è il migliore.
learning_rate: Utilizzeremo il tasso di apprendimento predefinito suggerito dagli autori di QLoRA, 0.0002 per un modello da 7B (o 13 B). Per modelli con più parametri, sono suggeriti tassi di apprendimento inferiori.
trainer = Trainer(
model=model,
tokenizer=tokenizer,
data_collator=collate,
train_dataset=dataset_tokenized["train"],
eval_dataset=dataset_tokenized["test"],
args=args,
)
trainer.train()
In sintesi, sarà necessario valutare quale checkpoint funziona meglio per il tuo specifico compito. Di solito, inizia con 3-4 epoche per avere un buon punto di partenza.
Esempio di Esecuzione dell'Addestramento
Perdita nell'Addestramento e nella Valutazione
Ecco un grafico di wandb di un tipico ciclo di addestramento per il dataset Open Assistant (OA), confrontando il fine-tuning di llama2-7b e Mistral-7b.
Tempo di Addestramento e Utilizzo di VRAM
Il fine-tuning di Llama2-7B e Mistral-7B sul dataset Open Assistant su una singola GPU con 24GB di VRAM richiede circa 100 minuti per epoca.
GPU: NVIDIA GeForce RTX 3090
Dataset "OpenAssistant/oasst_top1_2023-08-25"
Dimensione del batch 16, passi di accumulo del gradiente 1
Lunghezza massima del campione 512
Fondere gli Adattatori LoRA con il Modello di Base
Il seguente codice è leggermente diverso da altri script (come quello fornito da TheBloke, ad esempio) perché abbiamo aggiunto dei token per ChatML prima dell'addestramento.
Tuttavia, non abbiamo modificato il modello di base; ecco perché, prima di caricare gli adattatori, dobbiamo aggiungere i nuovi token al modello di base e al tokenizer; altrimenti proveremmo a fondere gli adattatori con due token aggiuntivi su un modello senza questi token (e questo fallirebbe).
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
import torch
base_path="models/Mistral-7B-v0.1" # input: base model
adapter_path="out/checkpoint-606" # input: adapters
save_to="models/Mistral-7B-finetuned" # out: merged model ready for inference
base_model = AutoModelForCausalLM.from_pretrained(
base_path,
return_dict=True,
torch_dtype=torch.bfloat16,
device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained(base_path)
# Aggiungi/imposta i token (stessi 5 passaggi di codice utilizzati prima dell'addestramento)
tokenizer.pad_token = "</s>"
tokenizer.add_tokens([""])
tokenizer.add_special_tokens(dict(eos_token=""))
base_model.resize_token_embeddings(len(tokenizer))
base_model.config.eos_token_id = tokenizer.eos_token_id
# Carica LoRA adapter e fonda
model = PeftModel.from_pretrained(base_model, adapter_path)
model = model.merge_and_unload()
model.save_pretrained(save_to, safe_serialization=True, max_shard_size='4GB')
tokenizer.save_pretrained(save_to)
Risoluzione dei Problemi
Le sfide fanno parte integrante dell'addestramento del modello. Discutiamo di alcuni problemi comuni e delle loro soluzioni.
OOM (Out of Memory)
Se incontri un errore di memoria esaurita (OOM):
Considera la riduzione delle dimensioni del batch.
Accorcia i campioni di addestramento riducendo la lunghezza del contesto (max_length in tokenize()).
Addestramento Troppo Lento
Se l'addestramento sembra lento:
Aumenta le dimensioni del batch.
Usa più GPU, acquistale o noleggiale (su runpod, ad esempio). Il codice fornito è pronto per l'accelerazione e può essere utilizzato per addestrare in impostazioni multi-GPU, avvia semplicemente con accelerate launch qlora.py anziché python qlora.py
Cattiva Qualità del Modello Finale
La qualità del tuo modello è il riflesso della qualità del tuo dataset. Per migliorare la qualità del modello:
Assicurati che il tuo dataset sia ricco e pertinente.
Regola gli iperparametri: learning_rate, epochs, rank r, lora_alpha.
Conclusione su Come fare il Finetuning di Llama2 e Mistral con Python
Comprendi ciò che stai facendo. Ci sono ottimi strumenti di addestramento come axolotl che ti permettono di concentrarti sulla creazione del dataset anziché sulla scrittura della tua funzione di padding. Tuttavia, una solida comprensione dei meccanismi sottostanti è inestimabile.
Questa conoscenza ti consente di navigare nelle complessità e risolvere i problemi con sicurezza.
Approccio Incrementale: Inizia con un esempio di base utilizzando un piccolo dataset. Scala gradualmente e aggiusta i parametri incrementalmente per scoprire il loro impatto sulle prestazioni del modello.
Metti in Evidenza la Qualità dei Dati: Dati di alta qualità sono il fondamento di un addestramento efficace. Sii innovativo e diligente nell'assemblare il tuo dataset.
Il fine-tuning degli LLMs come Llama 2 e Mistral è un processo gratificante, specialmente quando si dispone del dataset e dei parametri di addestramento giusti. Ricorda sempre di monitorare le prestazioni del modello e sii pronto a iterare ed adattarti. Desideri sfruttare al massimo il potenziale dell'Intelligenza Artificiale per il tuo progetto? Siamo qui per aiutarti a ottimizzare al massimo le tue soluzioni. Prenota una consulenza gratuita con noi per scoprire come implementare l'AI in modo concreto nel tuo settore. Non perdere l'opportunità di far crescere il tuo business con l'AI! Prenota ora la tua consulenza gratuita qui: Consulenza Gratuita sull'Applicazione Concreta dell'AI
Grazie per le info dell'articolo ..... siete GRANDI
Altre persone avrebbero venduto il contenuto di questo articolo per migliaia di euro con un corsetto , per me siete folli a regalare tutto questo valore.
però grazie ora mi metto a giocare con questi modelli vediamo che esce