Bagian 4: Hasil Pelatihan MLM Bert

Home Forums Dayu Febri Transformer Bagian 4: Hasil Pelatihan MLM Bert

  • This topic is empty.
Viewing 1 post (of 1 total)
  • Author
    Posts
  • #10733
    mulkan syarif
    Keymaster

    Kode sudah saya lampirkan di https://drive.google.com/drive/folders/1wFhoQe9PR5bK1Wk9AamqnDuZHrzYp-Cc?usp=share_link yang telah saya bagikan sebelumnya.  Saya bagi menjadi 4 tahapan dan semuanya menggunakan Jupyter karena biar sekalian bisa akses google colab

    1. Perayapan Satua Bali.ipynb

    Digunakan untuk me scraping isi website, yang pernah saya bahas di https://softscients.com/forums/topic/bagian-13-pembuatan-dataset-kumpulan-daftar-contoh-satua-bali/ hasilnya nanti akan disimpan di satua bali1.csv

    import urllib.parse as parse
    import requests
    import re
    from bs4 import BeautifulSoup as BS
    
    host = ["https://msatuabali.blogspot.com/",
            "https://msatuabali.blogspot.com/p/tentang-kami.html",
            "https://msatuabali.blogspot.com/p/kontak-kami.html",
            "https://msatuabali.blogspot.com/p/privacy-policy.html",
            "https://msatuabali.blogspot.com/p/disclaimer.html"]
    
    title = [
        "Kumpulan Daftar Contoh Satua Bali - Msatua Bali",
        "Kumpulan Daftar Contoh Satua Bali - Msatua Bali",
        "Kumpulan Daftar Contoh Cerpen Bahasa Bali - Msatua Bali",
        "Kumpulan Daftar Contoh Pidato Bahasa Bali - Msatua Bali",
        "Pengertian Puisi Bali Anyar - Msatua Bali",
        "Kumpulan Contoh - Contoh Puisi Bali Anyar - Msatua Bali",
        "Pengertian Puisi Bali Purwa - Msatua Bali",
        "Kumpulan Contoh - Contoh Puisi Bali Purwa - Msatua Bali",
        "Kumpulan Contoh - Contoh Sekar Rare - Puisi Bali Purwa - Msatua Bali",
        "Kumpulan Contoh - Contoh Sekar Alit (Sekar Macapat / Pupuh) - Puisi Bali Purwa - Msatua Bali",
        "Kumpulan Contoh - Contoh Sekar Madya - Puisi Bali Purwa - Msatua Bali",
        "Kumpulan Contoh - Contoh Sekar Agung ( Kekawin atau Wirama ) - Puisi Bali Purwa - Msatua Bali",
        "Sitemap - Msatua Bali"
        ]
    
    
    req = requests.get("https://msatuabali.blogspot.com/p/satua-bali-i-siap-selem-msatuabali.html")
    document = BS(req.text,"html.parser")
    i = 1
    final_result = list()
    for links in document.find_all('a'):
        link = links.get("href")
        if (link not in host) and link.find(".html")>0:
            # print(link)
            try:
                req = requests.get(link)
                document = BS(req.text,"html.parser")
                if document.title.string not in title:
                    print(document.title.string)
                    try:
                        artikel = document.find_all("div", {"class": "main-content"})[0].text
                        artikel = re.sub(r"\s+", " ", artikel)
                        buffer = artikel.split(". ")
                        # print(len(buffer))
                        for j, isi in enumerate(buffer):
                            if j!=0 and j<=len(buffer)-11: #baris pertama diabaikan
                                buffer2 = {"content":isi,"title":document.title.string}
                                final_result.append(buffer2)
                        i+=1
                    except:
                        print("ada error")
                        pass
            except:
                pass
    import pandas as pd
    
    pd.DataFrame(final_result).to_csv("satua bali1.csv")
    print("selesai....")

     

    2. Pembuatan Dataset dan Vocab-Token.ipynb

    Berdasarkan satua bali1.csv,  kita akan membuat dataset dan dilanjut dengan token/vocab. Kita akan buat function untuk memecah kalimat jika ukurannya cukup besar menjad kalimat-kalimat kecil.

    import pandas as pd
    import os
    from tokenizers import BertWordPieceTokenizer
    from pathlib import Path
    import math
    from tokenizers.implementations import ByteLevelBPETokenizer, BertWordPieceTokenizer
    from tokenizers.processors import BertProcessing
    from transformers import BertTokenizerFast,BertForMaskedLM, pipeline, BertConfig
    from transformers import LineByLineTextDataset, TextDataset
    from transformers import DataCollatorForLanguageModeling
    
    
    folder_corpus = 'model1'
    
    
    def pecah_kalimat(kalimat):
        n = len(kalimat.split(" "))
        delta = 60
    
        panjang = math.ceil(n/delta)
        token = kalimat.split(" ")
        a = 0
        b = a+delta
        result = []
        for i in range(0,panjang):
            if b>n:
                b = n
                # print(a,b)
                A = a-1
                B = b
            else:
                # print(a,b)
                A = a
                B = b
            buffer = token[A:B]
            strs = ""
            for j in buffer:
                strs+=j+" "
            result.append(strs)
    
            a = b+1
            b = a+delta
        return result

    kita gunakan function diatas untuk membuat file dataset.txt

    # buat folder
    if os.path.exists(folder_corpus)==False:
        os.mkdir(folder_corpus)
    # pembuatan dataset
    persiapan_dataset = True
    if persiapan_dataset:
        df = pd.read_csv('satua bali1.csv')
        artikel = df['content']
        with open('dataset.txt', 'w', encoding='utf-8') as fp:
            for i, kalimat in enumerate(artikel):
                # cari kalimat yang agak panjang, jangan terlalu sedikit
                if len(kalimat.split(" "))>=5 and len(kalimat.split(" "))<=60:
                    # print(kalimat)
                    fp.write("\n"+kalimat)
                else:
                    banyak_kalimat = pecah_kalimat(kalimat)
                    for j in banyak_kalimat:
                        fp.write("\n"+j)
                if i==10:
                    pass
                    #break
    print("selesai...")

    Selanjutnya berdasarkan dataset.txt akan dibuat token dan vocab

    # pembuatan vocab /tokenizer
    persiapan_dataset = True
    if persiapan_dataset:
        tokenizer = BertWordPieceTokenizer()
        # Bert
        special_tokens_dict = {'unk_token': '[UNK]',
                            'sep_token':'[SEP]',
                            'pad_token':'[PAD]',
                            'cls_token':'[CLS]',
                            'mask_token':'[MASK]'
                            }
        tokenizer.train(files='dataset.txt',
                        vocab_size=32_000,
                        min_frequency=1,
                        special_tokens=[
                                    "[UNK]",
                                    "[SEP]",
                                    "[PAD]",
                                    "[CLS]",
                                    "[MASK]"])
    
        if os.path.exists(folder_corpus)==False:
            os.mkdir(folder_corpus)
        tokenizer.save_model(folder_corpus)
    
        print("selesai disimpan")

    hasilnya akan disimpan dalam sebuah folder dengan nama model1/vocab.txt.  Kalau dibuka dengan notepad akan terdapat sebanyak 26.380 vocab yang nantinya digunakan sebagai dasar tokenizer.

    3. Pelatihan.ipynb

    Pada tahap ini yang sangat membutuhkan waktu puluhan jam beroperasi untuk melakukan training. Kita loading dulu beberapa library dan folder yang kita gunakan

    import pandas as pd
    import os
    from tokenizers import BertWordPieceTokenizer
    from pathlib import Path
    import math
    from tokenizers.implementations import ByteLevelBPETokenizer, BertWordPieceTokenizer
    from tokenizers.processors import BertProcessing
    from transformers import BertTokenizerFast,BertForMaskedLM, pipeline, BertConfig,AutoTokenizer,AutoModel
    from transformers import LineByLineTextDataset, TextDataset
    from transformers import DataCollatorForLanguageModeling
    import pandas as pd
    from torch.utils.data import Dataset, DataLoader
    import numpy as np
    
    folder_corpus = './model1'

    kita loading dulu tokenizer dan potong token dengan panjang 256 karakter saja. Karena kalau pakai 512 kehabisan memory

    tokenizer = BertTokenizerFast.from_pretrained(folder_corpus, max_len=256)

    mari kita coba library token nya apakah bisa digunakan untuk khusus bahasa bali?

    idx_token = tokenizer.encode("Dening keto Ida Anake Agung lantas ngambil rabi")
    print(idx_token)
    token = tokenizer.decode(idx_token)
    print(token)
    

    hasilnya sudah sesuai

    [3, 514, 163, 144, 237, 386, 140, 1717, 1595, 1]
    [CLS] dening keto ida anake agung lantas ngambil rabi [SEP]

    Loading data menggunakan pandas dengan cara membaca file csv

    from datasets import Dataset
    df = pd.read_csv('satua bali1.csv',usecols=['content'])
    dataset = Dataset.from_pandas(df.rename(columns={"content":'text'}))
    print(dataset

    kita bisa record nya yaitu 11 ribuan

    Dataset({
        features: ['text'],
        num_rows: 11009
    })

    kita akan memanfaatkan class Dataset dari pytorch untuk menyiapakan datanya dalam bentuk token ID nya

    import torch
    from torch.utils.data import Dataset
    from accelerate import Accelerator, DistributedType
    
    class LineByLineTextDataset(Dataset):
        def __init__(self, tokenizer, raw_datasets, max_length: int):
            self.padding = "max_length"
            self.text_column_name = 'text'
            self.max_length = max_length
            self.accelerator = Accelerator(gradient_accumulation_steps=1)
            self.tokenizer = tokenizer
    
            with self.accelerator.main_process_first():
                self.tokenized_datasets = raw_datasets.map(
                    self.tokenize_function,
                    batched=True,
                    num_proc=1,
                    remove_columns=[self.text_column_name],
                    desc="Running tokenizer on dataset line_by_line",
                )
                # self.tokenized_datasets.set_format('torch',columns=['input_ids'],dtype=torch.long)
                
        def tokenize_function(self,examples):
            examples[self.text_column_name] = [
                line for line in examples[self.text_column_name] if len(line[0]) > 0 and not line[0].isspace()
            ]
            return self.tokenizer(
                examples[self.text_column_name],
                padding=self.padding,
                truncation=True,
                max_length=self.max_length,
                return_special_tokens_mask=True,
            )
        def __len__(self):
            return len(self.tokenized_datasets)
    
        def __getitem__(self, i):
            return self.tokenized_datasets[i]

    kita panggil saja

    tokenized_dataset_train = LineByLineTextDataset(
        tokenizer= tokenizer,
        raw_datasets = dataset,
        max_length=256,
    )

    mari kita panggil tokenizer_dataset_train apakah sudah muncul token ID nya?

    # jika ingin melihat dataset nya!
    for i, batch in enumerate(tokenized_dataset_train):
        print(batch)
        break

    hasilnya

    {'input_ids': [3, 514, 2491, 258, 144, 1116, 2377, 10, 140, 207, 144, 1717, 1595, 207, 2435, 10, 416, 193, 629, 223, 1116, 2377, 12, 449, 497, 3887, 144, 1116, 1595, 10, 629, 193, 199, 174, 223, 1116, 2377, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2], 
    
    'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 
    
    'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 
    'special_tokens_mask': [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}

    ok sudah muncul bisa dilihat input_ids yang berisi token ID nya

    kita juga akan membuat data_collator untuk mensetting mask nya

    # mlm_probability` denotes
    # probability with which we mask the input tokens in a sequence.
    # pengaturan [MASK] sebanyak 0.15% dari kalimat yang ada didalam token akan di masking
    data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=True, mlm_probability=0.10)

    artinya jika ada 1 kalimat dengan jumlah token 100, maka 10% akan di masking

    • saya lagi makan  donat dan es teh di restoran kota semarang

    panjang token diatas yaitu 11, maka akan ada 1 token yg masking secara random

    • saya lagi makan  donat dan es teh di restoran kota [MASK]

    lalu kita atur BERT Config modelnya

    config = BertConfig(
        hidden_size = 384,
        vocab_size= tokenizer.vocab_size,
        num_hidden_layers = 6,
        num_attention_heads = 6,
        intermediate_size = 1024,
        max_position_embeddings = 256
    )
    
    model = BertForMaskedLM(config=config)

    kita atur jumlah epoch, save tensor per berapa kali, dan serta logging nya

    from transformers import Trainer, TrainingArguments
    
    training_args = TrainingArguments(
        output_dir=folder_corpus,
        overwrite_output_dir=True,
        push_to_hub=False,
        hub_model_id="bahasa-bali",
        num_train_epochs=100,
        per_device_train_batch_size=4,
        save_steps=5_000,
        logging_steps = 500,
        save_total_limit=2,
        use_mps_device = True, # disable this if you're running non-mac env
        hub_private_repo = False, # please set true if you want to save model privetly
        save_safetensors= True,
        learning_rate = 1e-4,
        #report_to='wandb'
    )
    
    trainer = Trainer(
        model=model,
        args=training_args,
        data_collator=data_collator,
        train_dataset=tokenized_dataset_train
    )

    jika sudah siap, maka lakukan training

    trainer.train()

    nanti akan muncul informasi

     

    jika sudah selesai, akan dilakukan penyimpanan

    trainer.save_model(folder_corpus)
    print("selsai simpan")

    kita bisa juga ploting loss nya tiap iterasi

    import pandas as pd
    from matplotlib import pyplot as plt
    loss = pd.DataFrame(trainer.state.log_history)
    plt.figure()
    plt.plot(loss['loss'])
    plt.show()

    pelatihan diatas butuh waktu sekitar 26 jam beroperasi terus menerus untuk selesaikan training

    4. pengujian.ipynb

    Model yang telah disimpan, bisa kita gunakan untuk melakukan inference

    from transformers import BertForMaskedLM, pipeline,AutoTokenizer
    import os
    folder_corpus = './model1'
    
    
    tokenizer = AutoTokenizer.from_pretrained(folder_corpus)
    model = BertForMaskedLM.from_pretrained(folder_corpus)
    recognizer = pipeline("fill-mask", model=model, tokenizer=tokenizer)

    kita akan lakukan  masking dengan contoh berikut

    target = "Uli joh koné ia suba ningalin ada sembé di pondokné"
    teks = "Uli joh koné ia suba [MASK] ada sembé di pondokné"
    recognizer(teks)

    hasilnya

    [{'score': 0.2525024712085724,
      'token': 197,
      'token_str': 'kone',
      'sequence': 'uli joh kone ia suba kone ada sembe di pondokne'},
     {'score': 0.1863546222448349,
      'token': 1203,
      'token_str': 'ningalin',
      'sequence': 'uli joh kone ia suba ningalin ada sembe di pondokne'},
     {'score': 0.0720268115401268,
      'token': 515,
      'token_str': 'liu',
      'sequence': 'uli joh kone ia suba liu ada sembe di pondokne'},
     {'score': 0.0452091321349144,
      'token': 1344,
      'token_str': 'joh',
      'sequence': 'uli joh kone ia suba joh ada sembe di pondokne'},
     {'score': 0.03558509796857834,
      'token': 847,
      'token_str': 'taen',
      'sequence': 'uli joh kone ia suba taen ada sembe di pondokne'}]

    walaupun belum sempurna hasilnya masih salah

    Catatan

    Setelah saya pelajari lebih lanjut melalui beragam trial and error, dengan dataset yang lebih kecil, diambil dari sebuah puisi dari https://www.brainacademy.id/blog/contoh-puisi

    Harus menggunakan setting min_frequency = 1 pada tokenizer

    tokenizer = BertWordPieceTokenizer()
    # Bert
    special_tokens_dict = {'unk_token': '[UNK]',
                           'sep_token':'[SEP]',
                           'pad_token':'[PAD]',
                           'cls_token':'[CLS]',
                           'mask_token':'[MASK]'
                           }
    tokenizer.train(files='dataset.txt', 
                    vocab_size=32_000, 
                    min_frequency=1, 
                    special_tokens=[
                                "[UNK]",
                                "[SEP]",
                                "[PAD]",
                                "[CLS]",
                                "[MASK]"])
    
    tokenizer.save_model(folder_model)

    contoh hasilnya sebagai berikut

    belajar filsafat, sastra, teknologi, ilmu kedokteran
    hasil prediksi:
    BELAJAR filsafat, SASTRA, TEKNOLOGI, ILMU KEDOKTERAN
    BELAJAR menyangkut, SASTRA, TEKNOLOGI, ILMU KEDOKTERAN
    BELAJAR pandangan, SASTRA, TEKNOLOGI, ILMU KEDOKTERAN
    BELAJAR ada, SASTRA, TEKNOLOGI, ILMU KEDOKTERAN
    BELAJAR hanya, SASTRA, TEKNOLOGI, ILMU KEDOKTERAN
    BELAJAR sastra, SASTRA, TEKNOLOGI, ILMU KEDOKTERAN
    BELAJAR belajar, SASTRA, TEKNOLOGI, ILMU KEDOKTERAN
    BELAJAR kedokteran, SASTRA, TEKNOLOGI, ILMU KEDOKTERAN
    BELAJAR ilmu, SASTRA, TEKNOLOGI, ILMU KEDOKTERAN
    BELAJAR ia, SASTRA, TEKNOLOGI, ILMU KEDOKTERAN

     

    catatan untuk loss function

    Untuk loss function diatas masih secara default, belum aku lengkapi dengan referensi berikut: https://discuss.huggingface.co/t/getting-the-mlm-accuracy-for-the-bert-model-i-am-training-from-scratch/6795

    https://discuss.huggingface.co/t/how-to-correctly-evaluate-a-masked-language-model/9634/2

    ref:

    https://mccormickml.com/2019/07/22/BERT-fine-tuning/

     

     

Viewing 1 post (of 1 total)
  • You must be logged in to reply to this topic.