Buckets:
| # การประมวลผลข้อมูล | |
| {#if fw === 'pt'} | |
| <CourseFloatingBanner chapter={3} | |
| classNames="absolute z-10 right-0 top-0" | |
| notebooks={[ | |
| {label: "Google Colab", value: "https://colab.research.google.com/github/huggingface/notebooks/blob/master/course/th/chapter3/section2_pt.ipynb"}, | |
| {label: "Aws Studio", value: "https://studiolab.sagemaker.aws/import/github/huggingface/notebooks/blob/master/course/th/chapter3/section2_pt.ipynb"}, | |
| ]} /> | |
| {:else} | |
| <CourseFloatingBanner chapter={3} | |
| classNames="absolute z-10 right-0 top-0" | |
| notebooks={[ | |
| {label: "Google Colab", value: "https://colab.research.google.com/github/huggingface/notebooks/blob/master/course/th/chapter3/section2_tf.ipynb"}, | |
| {label: "Aws Studio", value: "https://studiolab.sagemaker.aws/import/github/huggingface/notebooks/blob/master/course/th/chapter3/section2_tf.ipynb"}, | |
| ]} /> | |
| {/if} | |
| {#if fw === 'pt'} | |
| เราจะยังคงใช้ตัวอย่างจากบทที่แล้ว [previous chapter](/course/chapter2) โค้ดข้างล่างนี้คือวิธีการเทรนโมเดลสำหรับจำแนกลำดับ (sequence classifier) โดยใช้ข้อมูล 1 batch ใน Pytorch: | |
| ```python | |
| import torch | |
| from torch.optim import AdamW | |
| from transformers import AutoTokenizer, AutoModelForSequenceClassification | |
| # Same as before | |
| checkpoint = "bert-base-uncased" | |
| tokenizer = AutoTokenizer.from_pretrained(checkpoint) | |
| model = AutoModelForSequenceClassification.from_pretrained(checkpoint) | |
| sequences = [ | |
| "I've been waiting for a HuggingFace course my whole life.", | |
| "This course is amazing!", | |
| ] | |
| batch = tokenizer(sequences, padding=True, truncation=True, return_tensors="pt") | |
| # This is new | |
| batch["labels"] = torch.tensor([1, 1]) | |
| optimizer = AdamW(model.parameters()) | |
| loss = model(**batch).loss | |
| loss.backward() | |
| optimizer.step() | |
| ``` | |
| {:else} | |
| เราจะยังคงใช้ตัวอย่างจากบทที่แล้ว [previous chapter](/course/chapter2) โค้ดข้างล่างนี้คือวิธีการเทรนโมเดลสำหรับจำแนกลำดับ (sequence classifier) โดยใช้ข้อมูล 1 batch ใน TensorFlow: | |
| ```python | |
| import tensorflow as tf | |
| import numpy as np | |
| from transformers import AutoTokenizer, TFAutoModelForSequenceClassification | |
| # Same as before | |
| checkpoint = "bert-base-uncased" | |
| tokenizer = AutoTokenizer.from_pretrained(checkpoint) | |
| model = TFAutoModelForSequenceClassification.from_pretrained(checkpoint) | |
| sequences = [ | |
| "I've been waiting for a HuggingFace course my whole life.", | |
| "This course is amazing!", | |
| ] | |
| batch = dict(tokenizer(sequences, padding=True, truncation=True, return_tensors="tf")) | |
| # This is new | |
| model.compile(optimizer="adam", loss="sparse_categorical_crossentropy") | |
| labels = tf.convert_to_tensor([1, 1]) | |
| model.train_on_batch(batch, labels) | |
| ``` | |
| {/if} | |
| เป็นที่แน่นอนว่า ถ้าเราเทรนโมเดลโดยใช้ข้อมูลเพียง 2 ประโยคก็คงไม่ได้ผลลัพธ์ที่ดีเท่าไรนัก ถ้าคุณต้องการผลลัพธ์ที่ดีขึ้น คุณจะต้องเตรียมชุดข้อมูล (dataset) ที่มีขนาดใหญ่ขึ้น | |
| ใน section นี้ เราจะใช้ชุดข้อมูล MRPC (Microsoft Research Paraphrase Corpus) มารันให้ดูเป็นตัวอย่าง ชุดข้อมูลนี้มีการนำเสนอใน [paper](https://www.aclweb.org/anthology/I05-5002.pdf) โดย William B. Dolan and Chris Brockett โดยชุดข้อมูลนี้ประกอบด้วยคู่ของประโยคจำนวน 5,801 คู่ โดยมีข้อมูล label บ่งบอกว่าประโยคแต่ละคู่เกิดจากการถอความ (paraphrase) หรือไม่ (ประโยคคู่นี้มีความหมายเดียวกันหรือไม่) เหตุผลที่เราเลือกชุดข้อมูลนี้ เนื่องจากมันเป็นชุดข้อมูลที่มีขนาดเล็ก จึงง่ายต่อการนำไปทดลองเทรนโมเดล | |
| ### วิธีการโหลดชุดข้อมูลจาก Hub | |
| {#if fw === 'pt'} | |
| <Youtube id="_BZearw7f0w"/> | |
| {:else} | |
| <Youtube id="W_gMJF0xomE"/> | |
| {/if} | |
| Hub นั้นไม่ได้เก็บเพียงแค่โมเดล แต่ยังเก็บชุดข้อมูลในหลากหลายภาษาไว้เป็นจำนวนมาก คุณสามารถเลือกดูชุดข้อมูลต่าง ๆ ได้ที่ [here](https://huggingface.co/datasets) และเราขอแนะนำให้คุณลองโหลดและประมวลผลชุดข้อมูลชุดใหม่หลังจากที่คุณเรียน section นี้จบแล้ว (ดูเอกสารข้อมูลทั่วไปได้ที่ [here](https://huggingface.co/docs/datasets/loading)) แต่ตอนนี้เรามาสนใจกับชุดข้อมูล MRPC กันก่อนนะ! ชุดข้อมูลนี้เป็นหนึ่งในสิบของชุดข้อมูลที่ใช้วัดผลใน [GLUE benchmark](https://gluebenchmark.com/) ซึ่งเป็นตัววัดผลทางวิชาการ (academic benchmark) ที่ใช้วัดประสิทธิภาพของโมเดล ML โดยให้โมเดลทำงานจำแนกข้อความแบบต่าง ๆ กัน รวม 10 งาน | |
| ไลบรารี่ 🤗 Datasets library มีคำสั่งที่ใช้งานได้ง่ายมากในการดาวโหลดและ cache ชุดข้อมูลที่อยู่บน Hub เราสามารถดาวโหลดชุดข้อมูล MRPC ได้ดังนี้: | |
| > [!TIP] | |
| > ⚠️ **คำเตือน** ตรวจสอบให้แน่ใจว่า `datasets` ได้ถูกติดตั้งโดยการรัน `pip install datasets` ก่อน จากนั้นโหลดชุดข้อมูล MRPC และพิมพ์เพื่อดูว่ามีอะไรบ้าง | |
| ```py | |
| from datasets import load_dataset | |
| raw_datasets = load_dataset("glue", "mrpc") | |
| raw_datasets | |
| ``` | |
| ```python out | |
| DatasetDict({ | |
| train: Dataset({ | |
| features: ['sentence1', 'sentence2', 'label', 'idx'], | |
| num_rows: 3668 | |
| }) | |
| validation: Dataset({ | |
| features: ['sentence1', 'sentence2', 'label', 'idx'], | |
| num_rows: 408 | |
| }) | |
| test: Dataset({ | |
| features: ['sentence1', 'sentence2', 'label', 'idx'], | |
| num_rows: 1725 | |
| }) | |
| }) | |
| ``` | |
| คุณจะเห็นว่า เราจะได้อ็อบเจกต์ `DatasetDict` ซึ่งเก็บข้อมูลของ training set (ชุดข้อมูลที่ใช้เทรน) validation set (ชุดข้อมูลที่ใช้ตรวจสอบ) และ test set (ชุดข้อมูลที่ใช้ทดสอบ) ซึ่งในแต่ละชุดก็ประกอบด้วยหลายคอลัมน์ (`sentence1`, `sentence2`, `label`, and `idx`) และมีตัวแปร num_rows เก็บจำนวนข้อมูลของแต่ละชุด (ใน training set มีคู่ประโยคจำนวน 3,668 คู่ ส่วนใน validation set มี 408 คู่ และใน test set มี 1,725 คู่) | |
| คำสั่งนี้จะดาวโหลดและเก็บ cache ของชุดข้อมูลไว้ โดยค่าเริ่มต้น (by default) จะเก็บ cache ไว้ที่ *~/.cache/huggingface/dataset* โดยใน Chapter 2 เราได้บอกวิธีไว้แล้วว่า คุณสามารถเปลี่ยนโฟลเดอร์ที่จะเก็บ cache ได้โดยการตั้งค่าตัวแปร environment ที่ชื่อ `HF_HOME` | |
| เราสามารถเข้าถึงข้อมูลประโยคแต่ละคู่ในอ็อบเจกต์ `raw_datasets` ของเราได้โดยการใช้ indexing แบบเดียวกับที่ใช้กับ dictionary: | |
| ```py | |
| raw_train_dataset = raw_datasets["train"] | |
| raw_train_dataset[0] | |
| ``` | |
| ```python out | |
| {'idx': 0, | |
| 'label': 1, | |
| 'sentence1': 'Amrozi accused his brother , whom he called " the witness " , of deliberately distorting his evidence .', | |
| 'sentence2': 'Referring to him as only " the witness " , Amrozi accused his brother of deliberately distorting his evidence .'} | |
| ``` | |
| เราจะเห็นได้ว่าข้อมูล labels นั้นอยู่ในรูป integers อยู่แล้ว จึงไม่ได้ต้องทำการประมวลผลใด ๆ เพิ่มเติมกับ label ถ้าอยากรู้ว่า integer ตัวไหนตรงกับ label ตัวไหน เราสามารถเข้าไปดูได้ที่ `features` ของอ็อพเจกต์ `raw_train_dataset` ของเรา ซึ่งจะบอกชนิดของข้อมูลในแต่ละคอลัมน์: | |
| ```py | |
| raw_train_dataset.features | |
| ``` | |
| ```python out | |
| {'sentence1': Value(dtype='string', id=None), | |
| 'sentence2': Value(dtype='string', id=None), | |
| 'label': ClassLabel(num_classes=2, names=['not_equivalent', 'equivalent'], names_file=None, id=None), | |
| 'idx': Value(dtype='int32', id=None)} | |
| ``` | |
| เราจะเห็นเบื้องหลังของ `label` ว่าเป็นข้อมูลชนิด `ClassLabel` โดยข้อมูลการ mapping integers เข้ากับชื่อ label นั้นเก็บอยู่ในโฟลเดอร์ *names* โดย `0` จะตรงกับ `not_equivalent` และ `1` ตรงกับ `equivalent` | |
| > [!TIP] | |
| > ✏️ **ลองเลย!** ลองดูที่ element 15 ของ training set และ element 87 ของ validation set ว่ามี label เป็นอะไร? | |
| ### การประมวลผลชุดข้อมูล | |
| {#if fw === 'pt'} | |
| <Youtube id="0u3ioSwev3s"/> | |
| {:else} | |
| <Youtube id="P-rZWqcB6CE"/> | |
| {/if} | |
| ในขั้นตอนการประมวลผลชุดข้อมูล เราจะต้องแปลงตัวอักษรให้กลายเป็นตัวเลข เพื่อให้โมเดลสามารถทำความเข้าใจได้ ดังที่คุณได้เห็นแล้วใน [previous chapter](/course/chapter2) ขั้นตอนการแปลงนี้สามารถทำได้โดยใช้ tokenizer โดยเราสามารถป้อนข้อมูลเข้า tokenizer เพียงแค่หนึ่งประโยค หรือจะป้อนข้อมูลเป็น list ของประโยคทั้งหมดเลยก็ได้ เราสามารถ tokenize ทั้งประโยคแรกและประโยคที่สองในแต่ละคู่ประโยคทุกคู่ได้ดังนี้: | |
| ```py | |
| from transformers import AutoTokenizer | |
| checkpoint = "bert-base-uncased" | |
| tokenizer = AutoTokenizer.from_pretrained(checkpoint) | |
| tokenized_sentences_1 = tokenizer(raw_datasets["train"]["sentence1"]) | |
| tokenized_sentences_2 = tokenizer(raw_datasets["train"]["sentence2"]) | |
| ``` | |
| อย่างไรก็ตาม การส่งเพียงข้อมูลสองลำดับ (sequences) ในลักษณะนี้เข้าไปยังไม่เพียงพอที่จะทำให้โมเดลสามารถเรียนรู้และทำนายว่าประโยคทั้งสองนี้เป็นประโยคที่เกิดจากการถอดความ (paraphrase) หรือไม่ เราจะต้องจัดการให้ประโยคทั้งสองเป็นคู่กันก่อนแล้วค่อยทำการประมวลผลให้เหมาะสม ซึ่งโชคดีมากที่ tokenizer สามารถรับข้อมูลคู่ของลำดับแล้วเตรียมข้อมูลให้อยู่ในรูปแบบที่เหมาะสมกับการป้อนเข้าโมเดล BERT ของเรา: | |
| ```py | |
| inputs = tokenizer("This is the first sentence.", "This is the second one.") | |
| inputs | |
| ``` | |
| ```python out | |
| { | |
| 'input_ids': [101, 2023, 2003, 1996, 2034, 6251, 1012, 102, 2023, 2003, 1996, 2117, 2028, 1012, 102], | |
| 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1], | |
| 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] | |
| } | |
| ``` | |
| เราได้อธิบายเกี่ยวกับ keys ที่ชื่อ `input_ids` และ `attention_mask` ไปแล้วใน [Chapter 2](/course/chapter2) แต่เรายังไม่ได้พูดถึง `token_type_ids` ซึ่งในตัวอย่างนี้ ตัว token_type_ids นี่เองที่เป็นตัวบอกโมเดลว่าส่วนไหนของ input ที่เป็นประโยคแรก และส่วนไหนที่เป็นประโยคที่สอง | |
| > [!TIP] | |
| > ✏️ **ลองเลย!** ลองเลือก element 15 ของ training set มาลอง tokenize ประโยคทั้งสองแยกกันทีละประโยค และลอง tokenize เป็นคู่มาเทียบกันดู การ tokenize สองแบบนี้ให้ผลลัพธ์ที่ต่างกันอย่างไร? | |
| ถ้าเรา decode ข้อมูล IDs ที่อยู่ใน `input_ids` กลับไปเป็นคำ: | |
| ```py | |
| tokenizer.convert_ids_to_tokens(inputs["input_ids"]) | |
| ``` | |
| เราจะได้ผลลัพธ์: | |
| ```python out | |
| ['[CLS]', 'this', 'is', 'the', 'first', 'sentence', '.', '[SEP]', 'this', 'is', 'the', 'second', 'one', '.', '[SEP]'] | |
| ``` | |
| เราจะเห็นได้ว่าถ้าเราจะป้อนข้อมูลเข้าไปทีละสองประโยค โมเดลจะต้องการรับข้อมูลในรูปของ `[CLS] ประโยคที่หนึ่ง [SEP] ประโยคที่สอง [SEP]` ซึ่งถ้าเราไปเรียงให้ตรงกับ `token_type_ids` เราจะได้: | |
| ```python out | |
| ['[CLS]', 'this', 'is', 'the', 'first', 'sentence', '.', '[SEP]', 'this', 'is', 'the', 'second', 'one', '.', '[SEP]'] | |
| [ 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1] | |
| ``` | |
| คุณจะเห็นได้ว่า input ในส่วนที่ตรงกับ `[CLS] ประโยคที่หนึ่ง [SEP]` จะมี token type ID มีค่าเป็น 0 ทั้งหมด ในขณะที่ input ส่วนที่เหลือซึ่งตรงกับ `ประโยคที่สอง [SEP]` จะมี token type ID มีค่าเป็น 1 ทั้งหมด | |
| ควรระวังไว้ว่า ถ้าคุณเลือก checkpoint อื่น ผลลัพธ์จากการ tokenize อาจจะไม่มี token_type_ids อยู่ด้วยก็ได้ (ยกตัวอย่างเช่น ถ้าคุณเลือกโมเดล DistilBERT ผลลัพธ์จากการ tokenize จะไม่มี token_type_ids) การ tokenize จะให้ token_type_ids ออกมาก็ต่อเมื่อโมเดลนั้นรู้ว่าต้องจัดการกับมันอย่างไร เพราะโมเดลเคยเห็นข้อมูลนี้มาแล้วในช่วง pretraining | |
| ในตัวอย่างนี้ โมเดล BERT ผ่านการ pretrain มาด้วย token type IDs แล้ว และนอกเหนือไปจากเป้าหมายในการเทรนให้โมเดลสามารถเติมคำที่ถูกปิดไว้ (masked langauge modeling objective) ที่เราได้คุยกันใน [Chapter 1](/course/chapter1) โมเดล BERT ยังมีอีกเป้าหมายหนึ่งที่เรียกว่า _next sentence prediction_ (การทำนายประโยคถัดไป) โดยมีเป้าหมายในการทำแบบจำลองความสัมพันธ์ระหว่างคู่ของประโยคต่าง ๆ | |
| ในการทำให้โมเดลสามารถบรรลุเป้าหมายการทำนายประโยคถัดไป ได้มีการป้อนคู่ของประโยคที่ถูกปิดไว้อย่างสุ่มจำนวนมาก (pairs of sentences with randomly masked tokens) เข้าไปในโมเดล แล้วให้โมเดลทำนายว่าประโยคที่สองเป็นประโยคที่ตามหลังประโยคแรกหรือไม่ เพื่อไม่ให้โมเดลเรียนรู้เฉพาะประโยคที่เรียงตามกันเพียงอย่างเดียว จึงมีการแบ่งข้อมูลให้ครึ่งหนึ่งของคู่ประโยคทั้งหมด เป็นประโยคที่เรียงตามกันจริง ๆ เหมือนในเอกสารต้นฉบับ และอีกครึ่งหนึ่งเป็นคู่ประโยคที่เกิดจากสองประโยคที่มาจากเอกสารคนละชิ้นกัน | |
| โดยทั่วไปแล้ว คุณไม่ต้องกังวลว่าจะมีข้อมูล `token_type_ids` ในผลลัพธ์จากการ toknize หรือไม่ ตราบเท่าที่คุณเลือกให้ tokenizer และโมเดลใช้ checkpoint ตัวเดียวกัน เพราะถ้า tokenizer รู้ว่าต้องป้อนข้อมูลอะไรเข้าโมเดล ก็จะไม่เกิดปัญหาใด ๆ | |
| ตอนนี้เราก็ได้เห็นแล้วว่า tokenizer ของเราสามารถรับข้อมูลคู่ประโยคเพียงคู่เดียวก็ได้ หรือสามารถ tokenize คู่ประโยคทั้งหมดที่มีอยู่ในชุดข้อมูลของเราเลยก็ได้: เหมือนกับที่เราทำใน [previous chapter](/course/chapter2) เราสามารถป้อนข้อมูลเป็น list ของคู่ประโยคต่าง ๆ เข้าไปใน tokenizer ได้ โดยป้อนข้อมูล list ของประโยคแรก แล้วตามด้วย list ของประโยคที่สอง และยังสามารถทำการเติมและตัด (padding and truncation) เหมือนกับที่เราทำใน [Chapter 2](/course/chapter2) ได้ เราอาจจะเขียนคำสั่งในการประมวลผลชุดข้อมูล training set ได้ดังนี้: | |
| ```py | |
| tokenized_dataset = tokenizer( | |
| raw_datasets["train"]["sentence1"], | |
| raw_datasets["train"]["sentence2"], | |
| padding=True, | |
| truncation=True, | |
| ) | |
| ``` | |
| ซึ่งการเขียนคำสั่งแบบนี้ก็ได้ผลลัพธ์ที่ถูกต้อง แต่จะมีจุดด้อยคือการทำแบบนี้จะได้ผลลัพธ์ออกมาเป็น dictionary (โดยมี keys ต่าง ๆ คือ `input_ids`, `attention_mask` และ `token_type_ids` และมี values เป็น lists ของ lists) วิธีการนี้จะใช้การได้ก็ต่อเมื่อคอมพิวเตอร์ของคุณมี RAM เพียงพอที่จะเก็บข้อมูลของทั้งชุดข้อมูลในตอนที่ทำการ tokenize ชุดข้อมูลทั้งหมด (ในขณะที่ dataset ที่ได้จากไลบรารี่ 🤗 Datasets จะเป็นไฟล์ [Apache Arrow](https://arrow.apache.org/) ซึ่งจะเก็บข้อมูลไว้ใน disk คุณจึงสามารถโหลดเฉพาะข้อมูลที่ต้องการมาเก็บไว้ใน memory ได้) | |
| เพื่อให้ข้อมูลของเรายังเป็นข้อมูลชนิด dataset เราจะใช้เมธอด [`Dataset.map()`](https://huggingface.co/docs/datasets/package_reference/main_classes#datasets.Dataset.map) ซึ่งจะช่วยให้เราเลือกได้ว่าเราจะทำการประมวลผลอื่น ๆ นอกเหนือจากการ tokenize หรือไม่ โดยเมธอด `map()` ทำงานโดยการเรียกใช้ฟังก์ชั่นกับแต่ละ element ของชุดข้อมูล เรามาสร้างฟังก์ชั่นสำหรับ tokenize ข้อมูลของเรากันก่อน: | |
| ```py | |
| def tokenize_function(example): | |
| return tokenizer(example["sentence1"], example["sentence2"], truncation=True) | |
| ``` | |
| ฟังก์ชั่นนี้จะรับ dictionary (เหมือนกับแต่ละ item ของชุดข้อมูลของเรา) และให้ผลลัพธ์เป็น dictionary ตัวใหม่ที่มี keys เป็น `input_ids`, `attention_mask` และ `token_type_ids` ควรสังเกตว่าถึงแม้ `example` dictionary จะประกอบไปด้วยข้อมูลหลายชุด (แต่ละ key เป็น list ของประโยคต่าง ๆ ) ฟังก์ชั่นนี้ก็ยังทำงานได้ เนื่องจาก `tokenizer` สามารถรับข้อมูลเป็น list ของคู่ประโยคต่าง ๆ ได้ดังที่ได้เห็นแล้วข้างต้น และการเขียนฟังก์ชั่นแบบนี้ยังทำให้เราสามารถใช้ตัวเลือก `batched=True` ตอนที่เราเรียกใช้เมธอด `map()` ได้อีกด้วย ซึ่งจะช่วยให้การ tokenize เร็วขึ้นอย่างมาก เนื่องจาก tokenizer ในไลบรารี่ [🤗 Tokenizers](https://github.com/huggingface/tokenizers) นั้นเขียนโดยใช้ภาษา Rust ซึ่งจะทำงานได้รวดเร็วมากหากคุณป้อนข้อมูลเข้าไปจำนวนมากพร้อม ๆ กัน | |
| ควรสังเกตว่าเรายังไม่ได้ใส่อากิวเมนต์ `padding` เข้ามาในฟังก์ชั่น tokenize ของเราตอนนี้ เนื่องจากการเติม (padding) ข้อมูลทุก ๆ ตัวอย่างให้มีความยาวเท่ากับประโยคที่มีความยาวมากสุดนั้นไม่ค่อยมีประสิทธิภาพเท่าไรนัก วิธีการที่ดีกว่าคือให้เราเติม (pad) ข้อมูลเมื่อเรากำลังสร้าง batch ขึ้นมา ซึ่งเราก็จะต้องเติมให้ข้อมูลมีความยาวเท่ากับประโยคที่ยาวที่สุดใน batch นั้น ๆ ก็พอ ไม่จำเป็นต้องเติมให้ยาวเท่ากับประโยคที่ยาวที่สุดในทั้งชุดข้อมูล การทำเช่นนี้จะช่วยประหยัดเวลาและพลังในการประมวลผลได้อย่างมาก แม้ input ของเราจะมีความยาวที่แตกต่างกันมากก็ตาม! | |
| ต่อไปนี้คือวิธีการใช้ฟังก์ชั่น tokenize ให้ทำงานกับข้อมูลใน dataset ทุกชุดของเราในคราวเดียว โดยเราจะใส่ `batched=True` ตอนที่ call เมธอด `map` เพื่อให้ฟังก์ชั่นทำงานกับ elements หลาย ๆ ตัวใน dataset ของเราในคราวเดียว (ไม่ได้ทำทีละ element แยกกัน) ซึ่งการทำเช่นนี้จะช่วยให้เราประมวลผลข้อมูลได้เร็วขึ้นมาก | |
| ```py | |
| tokenized_datasets = raw_datasets.map(tokenize_function, batched=True) | |
| tokenized_datasets | |
| ``` | |
| ไลบรารี่ 🤗 Datasets จะทำการประมวลผลนี้โดยการเพิ่ม fields ใหม่เข้าไปยัง datasets ของเรา โดยเพิ่ม field ให้กับแต่ละ key ของ dictionary ที่ได้ออกมาจากฟังก์ชั่นประมวลผลของเรา | |
| ```python out | |
| DatasetDict({ | |
| train: Dataset({ | |
| features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'], | |
| num_rows: 3668 | |
| }) | |
| validation: Dataset({ | |
| features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'], | |
| num_rows: 408 | |
| }) | |
| test: Dataset({ | |
| features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'], | |
| num_rows: 1725 | |
| }) | |
| }) | |
| ``` | |
| นอกจากนี้คุณยังสามารถใช้ multiprocessing ตอนที่คุณใช้ฟังก์ชั่น preprocess ของคุณกับ `map()` ได้โดยการใส่อากิวเมนต์ `num_proc` แต่ที่เราไม่ได้ทำให้ดูตรงนี้ เนื่องจากไลบรารี่ 🤗 Tokenizers นั้นมีการใช้ multiple threads เพื่อให้การ tokenize ตัวอย่างของเราเร็วขึ้นอยู่แล้ว แต่ถ้าคุณไม่ได้ใช้ fast tokenizer ที่เขียนไว้ในไลบรารี่นี้ การใช้ multiprocessing ก็อาจจะช่วยให้การประมวลผลชุดข้อมูลของคุณเร็วขึ้นได้ | |
| `tokenize_function` ของเราให้ผลลัพธ์เป็น dictionary โดยมี keys ต่าง ๆ ได้แก่ `input_ids`, `attention_mask` และ `token_type_ids` เพื่อให้ทั้งสาม field นี้ถูกเพิ่มเข้าไปใน dataset ทั้งสาม split คุณควรจำไว้ว่าเราอาจจะเปลี่ยน filed ที่มีอยู่แล้วก็ได้ ถ้าหากว่าคุณเลือกเขียนฟังก์ชั่นให้เปลี่ยนค่าใน key เดิมใน dataset ที่เราจะทำการ map และให้ฟังก์ชั่น return ค่าใหม่ออกมา | |
| ขั้นตอนสุดท้ายที่ต้องทำก็คือการเติมชุดข้อมูลตัวอย่างของเราให้มีความยาวเท่ากับข้อมูลตัวที่มีความยาวมากที่สุดใน batch ซึ่งเทคนิคเราจะเรียกว่า *dynamic padding* (การเติมแบบพลวัต) | |
| ### Dynamic padding (การเติมแบบพลวัต) | |
| <Youtube id="7q5NyFT8REg"/> | |
| {#if fw === 'pt'} | |
| ฟังก์ชั่นที่ทำหน้าที่เก็บข้อมูลตัวอย่างเข้ามาทำเป็น batch เรียกว่า *collate function* ซึ่งเป็นอากิวเมนต์ที่คุณสามารถใส่เพิ่มได้เมื่อคุณสร้าง `DataLoader` โดยการตั้งค่าเริ่มต้นจะเป็นฟังก์ชั่นที่ทำหน้าที่เพียงแปลงข้อมูลตัวอย่างของคุณให้เป็น Pytorch tensors และนำมา concatenate ต่อกัน (แบบ recursive ถ้าหากคุณป้อนข้อมูลเป็น lists, tuples หรือ dictionaries) ซึ่งในกรณีตัวอย่างของเรานี้จะทำแบบนั้นไม่ได้ เนื่องจากข้อมูลป้อนเข้าแต่ละตัวของเรามีขนาดไม่เท่ากัน ซึ่งเราก็ได้จงใจที่ยังไม่ทำการเติม (padding) มาจนถึงตอนนี้ เพื่อที่จะทำการเติมเท่าที่จำเป็นต้องทำในแต่ละ batch เพื่อหลีกเลี่ยงการเติมข้อมูลให้มีความยาวเกินจำเป็น ซึ่งการทำแบบนี้จะช่วยให้การ training เร็วขึ้นค่อนข้างมาก แต่ควรระวังไว้ว่าถ้าคุณ train บน TPU การทำแบบนี้อาจสร้างปัญหาได้ เนื่องจาก TPUs นั้นชอบข้อมูลที่มี shape คงที่มากกว่า แม้ว่าจะต้องเติมข้อมูลให้ยาวมากก็ตาม | |
| {:else} | |
| ฟังก์ชั่นที่ทำหน้าที่เก็บข้อมูลตัวอย่างเข้ามาทำเป็น batch เรียกว่า *collate function* ซึ่งมีการตั้งค่าเริ่มต้นเป็นฟังก์ชั่นที่ทำหน้าที่เพียงแปลงข้อมูลตัวอย่างของคุณให้เป็น tf.Tensor และนำมา concatenate ต่อกัน (แบบ recursive ถ้าหากคุณป้อนข้อมูลเป็น lists, tuples หรือ dictionaries) ซึ่งในกรณีตัวอย่างของเรานี้จะทำแบบนั้นไม่ได้ เนื่องจากข้อมูลป้อนเข้าแต่ละตัวของเรามีขนาดไม่เท่ากัน ซึ่งเราก็ได้จงใจที่ยังไม่ทำการเติม (padding) มาจนถึงตอนนี้ เพื่อที่จะทำการเติมเท่าที่จำเป็นต้องทำในแต่ละ batch เพื่อหลีกเลี่ยงการเติมข้อมูลให้มีความยาวเกินจำเป็น ซึ่งการทำแบบนี้จะช่วยให้การ training เร็วขึ้นค่อนข้างมาก แต่ควรระวังไว้ว่าถ้าคุณ train บน TPU การทำแบบนี้อาจสร้างปัญหาได้ เนื่องจาก TPUs นั้นชอบข้อมูลที่มี shape คงที่มากกว่า แม้ว่าจะต้องเติมข้อมูลให้ยาวมากก็ตาม | |
| {/if} | |
| ในทางปฏิบัติแล้ว เราจะต้องสร้างฟังก์ชั่น collate ที่จะทำการเติมข้อมูลในแต่ละ batch ของ dataset ด้วยจำนวนที่ถูกต้อง ซึ่งโชคดีที่ไลบรารี่ 🤗 Transformers ได้เตรียมฟังก์ชั่นนี้ไว้ให้แล้วในโมดูล `DataCollatorWithPadding` โดยจะรับข้อมูลเป็น tokenier (เพื่อให้รู้ว่าจะต้องเติมด้วย paddin token อะไร และเพื่อให้รู้ว่าโมเดลคาดหวังว่าจะต้องเติมไปทางซ้ายหรือทางขวามือของข้อมูล) และจะทำขั้นตอนทุกอย่างที่คุณต้องการ: | |
| {#if fw === 'pt'} | |
| ```py | |
| from transformers import DataCollatorWithPadding | |
| data_collator = DataCollatorWithPadding(tokenizer=tokenizer) | |
| ``` | |
| {:else} | |
| ```py | |
| from transformers import DataCollatorWithPadding | |
| data_collator = DataCollatorWithPadding(tokenizer=tokenizer, return_tensors="tf") | |
| ``` | |
| {/if} | |
| เพื่อจะทดสอบของเล่นชิ้นใหม่นี้ ลองเลือกข้อมูลบางส่วนจากชุดข้อมูล training ของเรามาทดลองสร้างเป็น batch ซึ่งตรงนี้เราจะเอาคอลัมน์ idx, sentence1 และ sentence2 ออกไปเนื่องจากเราไม่จำเป็นต้องใช้ อีกทั้งคอลัมน์เหล่านี้ยังมี strings (ซึ่งเราไม่สามารถใช้ strings ในการสร้าง tensor ได้) แล้วลองดูความยาวของข้อมูลแต่ละตัวใน batch ของเรา: | |
| ```py | |
| samples = tokenized_datasets["train"][:8] | |
| samples = {k: v for k, v in samples.items() if k not in ["idx", "sentence1", "sentence2"]} | |
| [len(x) for x in samples["input_ids"]] | |
| ``` | |
| ```python out | |
| [50, 59, 47, 67, 59, 50, 62, 32] | |
| ``` | |
| เราเลือกได้ข้อมูลที่มีความยาวต่าง ๆ กัน ตั้งแต่ 32 ไปถึง 67 (ซึ่งก็ไม่น่าประหลาดใจอะไร) การทำ Dynamic padding ควรที่จะเติมข้อมูลทุกตัวใน batch นี้ให้มีความยาวเท่ากันเท่ากับ 67 (ซึ่งเป็นความยาวของข้อมูลที่ยาวที่สุดใน batch นี้) ถ้าไม่มีการทำ dynamic padding เราก็จะต้องเติมข้อมูลให้ยาวเท่ากับข้อมูลที่ยาวที่สุดใน dataset หรือไม่ก็เท่ากับความยาวสูงสุดที่โมเดลจะรับได้ ลองมาตรวจสอบกันดูว่า `data_collator` ของเรานั้นได้ทำการเติมแบบพลวัตให้กับข้อมูลใน batch ของเราอย่างถูกต้องเหมาะสม: | |
| ```py | |
| batch = data_collator(samples) | |
| {k: v.shape for k, v in batch.items()} | |
| ``` | |
| {#if fw === 'tf'} | |
| ```python out | |
| {'attention_mask': TensorShape([8, 67]), | |
| 'input_ids': TensorShape([8, 67]), | |
| 'token_type_ids': TensorShape([8, 67]), | |
| 'labels': TensorShape([8])} | |
| ``` | |
| {:else} | |
| ```python out | |
| {'attention_mask': torch.Size([8, 67]), | |
| 'input_ids': torch.Size([8, 67]), | |
| 'token_type_ids': torch.Size([8, 67]), | |
| 'labels': torch.Size([8])} | |
| ``` | |
| ผลลัพธ์ออกมาดูดีเลย! ตอนนี้เราก็จัดการข้อมูลจาก raw text ให้เป็นชุดของ batch ที่โมเดลทำความเข้าใจได้แล้ว เราพร้อมที่จะ fine-tune แล้ว! | |
| {/if} | |
| > [!TIP] | |
| > ✏️ **ลองเลย!** ลองทำการประมวลผลแบบนี้กับชุดข้อมูล GLUE SST-2 ดู มันจะต่างจากตัวอย่างนี้เล็กน้อย เนื่องจากชุดข้อมูลนั้นประกอบไปด้วยประโยคเดียวแทนที่จะเป็นคู่ประโยค แต่ส่วนที่เหลือก็เหมือนกัน ถ้าอยากลองความท้าทายที่ยากขึ้นไปอีก ให้ลองเขียนฟังก์ชั่นประมวลผลที่ใช้กับ GLUE tasks ได้ทุก task ดูสิ | |
| {#if fw === 'tf'} | |
| ตอนนี้เราก็ได้ dataset และ data collator แล้ว เราจะต้องนำมันมาต่อเข้าด้วยกัน โดยเราอาจจะโหลด batch และ collate มันแบบ manual ก็ได้ แต่นั่นเป็นงานที่หนักมากและไม่ค่อยมีประสิทธิภาพนัก เราอาจเลือกใช้เมธอด `to_tf_dataset()` ในการแก้ปัญหานี้อย่างมีประสิทธิภาพ โดยเมธอดนี้จะ wrap `tf.data.Dataset` เข้ากับ dataset ของคุณและคุณสามารถใส่ collation function เข้าไปด้วยได้ โดย `tf.data.Dataset` นั้นเป็น native TensorFlow format ซึ่ง Keras สามารถใช้ร่วมกับ `model.fit()` ได้ ดังนั้นเมธอดนี้จะแปลง 🤗 Dataset ให้เป็น format ที่พร้อมสำหรับการ training แล้ว ลองมาดูการเทรนโมเดลด้วย dataset ของเรากันเลย! | |
| ```py | |
| tf_train_dataset = tokenized_datasets["train"].to_tf_dataset( | |
| columns=["attention_mask", "input_ids", "token_type_ids"], | |
| label_cols=["labels"], | |
| shuffle=True, | |
| collate_fn=data_collator, | |
| batch_size=8, | |
| ) | |
| tf_validation_dataset = tokenized_datasets["validation"].to_tf_dataset( | |
| columns=["attention_mask", "input_ids", "token_type_ids"], | |
| label_cols=["labels"], | |
| shuffle=False, | |
| collate_fn=data_collator, | |
| batch_size=8, | |
| ) | |
| ``` | |
| เสร็จเรียบร้อย! เราสามารถนำ datasets พวกนี้ไปใช้ในบทเรียนต่อไปของเราได้เลย โดยการ training นั้นค่อนข้างตรงไปตรงมาไม่ซับซ้อนหลังจากที่เราทำงานอย่างหนักไปกกับการประมวลผลข้อมูลแล้ว | |
| {/if} | |
| <EditOnGithub source="https://github.com/huggingface/course/blob/main/chapters/th/chapter3/2.mdx" /> |
Xet Storage Details
- Size:
- 42.6 kB
- Xet hash:
- 193f079fb54f869f078ff9a43884faf347f74b73e9be2788893d5afe4aa6375b
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.