NLP Course documentation

การใช้งานตัวตัดคำแบบเร็ว (Fast tokenizers) ใน QA pipeline

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

การใช้งานตัวตัดคำแบบเร็ว (Fast tokenizers) ใน QA pipeline

Ask a Question Open In Colab Open In Studio Lab

ในบทนี้ เราจะเรียนเกี่ยวกับการใช้งาน pipeline เพื่อทำ question-answering และดูว่าเราจะสามารถใช้ข้อมูลจาก offset เพื่อเอาไว้หาคำตอบให้กับคำถาม input จากบริบทรอบๆ (context) ได้อย่างไร ขั้นตอนนี้จะคล้ายๆกับตอนที่เราใช้ offset เพื่อรวมรวม entity ประเภทเดียวกันเข้าด้วยกัน ในบทที่แล้ว จากนั้น เราจะมาดูกันว่าเราจะจัดการกับ context ที่ยาวมากๆ จนบางส่วนต้องถูกตัดทอนออกได้อย่างไร คุณสามารถข้ามส่วนนี้หากคุณไม่สนใจ question answering

การใช้ question-answering pipeline

อย่างที่คุณได้เรียนในบทที่ 1 เราสามารถใช้ question-answering pipeline เพื่อคำนวณคำตอบของคำถาม input ได้ :

from transformers import pipeline

question_answerer = pipeline("question-answering")
context = """
🤗 Transformers is backed by the three most popular deep learning libraries — Jax, PyTorch, and TensorFlow — with a seamless integration
between them. It's straightforward to train your models with one before loading them for inference with the other.
"""
question = "Which deep learning libraries back 🤗 Transformers?"
question_answerer(question=question, context=context)
{'score': 0.97773,
 'start': 78,
 'end': 105,
 'answer': 'Jax, PyTorch and TensorFlow'}

ใน pipeline อื่นๆ เราไม่สามารถตัดทอนและแยกข้อความที่ยาวเกินกว่าความยาวสูงสุดที่โมเดลกำหนดได้ (และอาจพลาดตัดข้อมูลที่ส่วนท้ายของเอกสารได้ด้วย) แต่ pipeline ที่เราจะเรียนกันนี้ สามารถจัดการกับ context ที่ยาวมากได้ และจะ return คำตอบให้กับคำถาม แม้ว่าจะอยู่ในตอนท้าย:

long_context = """
🤗 Transformers: State of the Art NLP

🤗 Transformers provides thousands of pretrained models to perform tasks on texts such as classification, information extraction,
question answering, summarization, translation, text generation and more in over 100 languages.
Its aim is to make cutting-edge NLP easier to use for everyone.

🤗 Transformers provides APIs to quickly download and use those pretrained models on a given text, fine-tune them on your own datasets and
then share them with the community on our model hub. At the same time, each python module defining an architecture is fully standalone and
can be modified to enable quick research experiments.

Why should I use transformers?

1. Easy-to-use state-of-the-art models:
  - High performance on NLU and NLG tasks.
  - Low barrier to entry for educators and practitioners.
  - Few user-facing abstractions with just three classes to learn.
  - A unified API for using all our pretrained models.
  - Lower compute costs, smaller carbon footprint:

2. Researchers can share trained models instead of always retraining.
  - Practitioners can reduce compute time and production costs.
  - Dozens of architectures with over 10,000 pretrained models, some in more than 100 languages.

3. Choose the right framework for every part of a model's lifetime:
  - Train state-of-the-art models in 3 lines of code.
  - Move a single model between TF2.0/PyTorch frameworks at will.
  - Seamlessly pick the right framework for training, evaluation and production.

4. Easily customize a model or an example to your needs:
  - We provide examples for each architecture to reproduce the results published by its original authors.
  - Model internals are exposed as consistently as possible.
  - Model files can be used independently of the library for quick experiments.

🤗 Transformers is backed by the three most popular deep learning libraries — Jax, PyTorch and TensorFlow — with a seamless integration
between them. It's straightforward to train your models with one before loading them for inference with the other.
"""
question_answerer(question=question, context=long_context)
{'score': 0.97149,
 'start': 1892,
 'end': 1919,
 'answer': 'Jax, PyTorch and TensorFlow'}

เรามาดูกันว่ามันทำงานอย่างไร!

การใช้งานโมเดลสำหรับงาน question answering

เช่นเดียวกับ pipeline อื่นๆ เราจะเริ่มต้นด้วยการ tokenize ข้อความ input ของเรา แล้วส่งผลลัพธ์ที่ได้ต่อไปยังตัวโมเดล ค่าเริ่มต้นของ checkpoint สำหรับ question-answering pipeline คือ distilbert-base-cased-distilled-squad (คำว่า “squad” มาจากชื่อของชุดข้อมูลที่โมเดลใช้เพื่อ fine-tune ซึ่งก็คือ SQuAD dataset เราจะพูดถึงชุดข้อมูลนี้เพิ่มเติมในบทที่ 7):

from transformers import AutoTokenizer, AutoModelForQuestionAnswering

model_checkpoint = "distilbert-base-cased-distilled-squad"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)
model = AutoModelForQuestionAnswering.from_pretrained(model_checkpoint)

inputs = tokenizer(question, context, return_tensors="pt")
outputs = model(**inputs)

เราจะทำการตัดคำให้กับส่วนที่เป็นคำถามและส่วน context ไปด้วยกัน โดยจะตัดคำให้กับส่วนคำถามก่อน

An example of tokenization of question and context

โมเดลสำหรับ question answering นั้นทำงานแตกต่างไปจากโมเดลอื่น ที่คุณเคยเห็นมาแล้วเล็กน้อย จากภาพด้านบน โมเดลจะถูกการเทรนให้ทำนาย index ของ token ที่เป็นจุดเริ่มต้นของข้อความคำตอบ (ซึ่งก็คือ index ที่ 21) และ index ของ token สุดท้ายของข้อความคำตอบ (ซึ่งก็คือ index ที่ 24) นี่คือสาเหตุที่โมเดลเหล่านั้นไม่ return tensor ของ logit หนึ่งตัว แต่สองตัว: tensor แรก คือ logits สำหรับ token เริ่มต้นของคำตอบ และอีก tensor เป็น logits สำหรับ token สุดท้ายของคำตอบ เนื่องจากในกรณีนี้ เรามีเพียง input เดียว ซึ่งมี 66 token เราจะได้ผลลัพธ์ดังนี้:

start_logits = outputs.start_logits
end_logits = outputs.end_logits
print(start_logits.shape, end_logits.shape)
torch.Size([1, 66]) torch.Size([1, 66])

ในการแปลงค่า logits ให้เป็นค่าความน่าจะเป็น (probabilities) เราจะใช้ฟังก์ชัน softmax แต่ก่อนอื่น เราจะต้องทำการปกปิด (mask) index ที่ไม่ได้เป็นส่วนหนึ่งของ context ก่อน อินพุตของเราคือ [CLS] question [SEP] context [SEP] ดังนั้น เราจะ mask แต่ละ token ในส่วนที่เป็นคำถาม รวมถึง token [SEP] ด้วย อย่างไรก็ตาม เราจะเก็บ [CLS] ไว้ เนื่องจากโมเดลบางตัวอาจจะใช้มัน เพื่อระบุว่าคำตอบไม่อยู่ใน context เนื่องจากเราจะใช้ softmax ในภายหลัง เราจึงเพียงแค่ต้องแทนที่ค่า logits ที่เราต้องการ mask ด้วยตัวเลขติดลบจำนวนมาก ในตัวอย่างนี้ เราใช้ -10000:

import torch

sequence_ids = inputs.sequence_ids()
# Mask everything apart from the tokens of the context
mask = [i != 1 for i in sequence_ids]
# Unmask the [CLS] token
mask[0] = False
mask = torch.tensor(mask)[None]

start_logits[mask] = -10000
end_logits[mask] = -10000

หลังจากที่ เราได้ mask ค่า logits ตำแหน่งที่เราไม่ต้องการจะทำนาย เรียบร้อยแล้ว ตอนนี้เราก็สามารถคำนวณ softmax ได้:

start_probabilities = torch.nn.functional.softmax(start_logits, dim=-1)[0]
end_probabilities = torch.nn.functional.softmax(end_logits, dim=-1)[0]

ตอนนี้ เราสามารถหาค่า argmax ของ probabilities ของจุดเริ่มต้นและจุดสิ้นสุดได้แล้ว แต่ปัญหาที่อาจจะเกิดขึ้นก็คือ index ของจุดเริ่มต้น นั้นอยู่เกิน index ของจุดสิ้นสุด ดังนั้นเราจึงต้องหาวิธีจัดการปัญหานี้ เราจะคำนวณ probabilities ของ start_index และ end_index ที่เป็นไปได้จริง ซึ่งหมายถึง start_index <= end_index จากนั้นเราจะเลือกใช้แค่ tuple (start_index, end_index) ที่มีความเป็นไปได้สูงสุด สมมติว่า เหตุการณ์ที่ “คำตอบเริ่มต้นที่ start_index” และ “คำตอบสิ้นสุดที่ end_index” ไม่มีความเกี่ยวข้องกัน (independent) ความน่าจะเป็นที่ คำตอบจะเริ่มต้นที่ start_index และสิ้นสุดที่ end_index คือ: start_probabilities[start_index]×end_probabilities[end_index]\mathrm{start\_probabilities}[\mathrm{start\_index}] \times \mathrm{end\_probabilities}[\mathrm{end\_index}]

การคำนวณ score ทำได้โดยคำนวณผลคูณทั้งหมดของstart_probabilities[start_index]×end_probabilities[end_index]\mathrm{start\_probabilities}[\mathrm{start\_index}] \times \mathrm{end\_probabilities}[\mathrm{end\_index}] โดยที่ start_index <= end_index

ขั้นแรก เราจะคำนวณผลคูณที่เป็นไปได้ทั้งหมด:

scores = start_probabilities[:, None] * end_probabilities[None, :]

จากนั้นเราจะ mask ค่าตรงที่ start_index > end_index ให้เป็น 0 (ค่า probabilities อื่นๆ เป็นจำนวนบวกทั้งหมด) ฟังก์ชัน torch.triu() จะคำนวณ ส่วนสามเหลี่ยมบนของ tensor 2 มิติ ที่เราใส่ไปเป็น argument ดังนั้นมันจะทำการ mask ให้เรา:

scores = torch.triu(scores)

ตอนนี้เราแค่ต้องหา index ที่มีค่า probability สูงสุด เนื่องจาก PyTorch จะ return ค่าในรูป flattened tensor เราจึงต้องใช้การหารแล้วปัดลง (floor division) // และโมดูลัส % เพื่อคำนวณ start_index และ end_index:

max_index = scores.argmax().item()
start_index = max_index // scores.shape[1]
end_index = max_index % scores.shape[1]
print(scores[start_index, end_index])

ตอนนี้ เราก็ได้ score ที่ถูกต้องสำหรับคำตอบแล้ว (คุณสามารถตรวจสอบได้โดยเปรียบเทียบกับผลลัพธ์แรกในส่วนก่อนหน้า):

0.97773

✏️ ลองทำดู! คำนวณ index เริ่มต้นและสิ้นสุด เพื่อหาคำตอบที่น่าจะเป็นไปได้มากที่สุด 5 คำตอบ

เรามี start_index และ end_index ของ token ที่จะเอามาเป็นคำตอบได้แล้ว ดังนั้นตอนนี้เราเพียงแค่ต้องแปลงเป็น index ของตัวอักษร ใน context เท่านั้น นี่คือจุดที่ offsets จะมีประโยชน์มาก เราสามารถใช้งานมันได้เหมือนที่เราทำใน token classification:

inputs_with_offsets = tokenizer(question, context, return_offsets_mapping=True)
offsets = inputs_with_offsets["offset_mapping"]

start_char, _ = offsets[start_index]
_, end_char = offsets[end_index]
answer = context[start_char:end_char]

ตอนนี้ เราแค่ต้องฟอร์แมตทุกอย่างเพื่อให้ได้ผลลัพธ์ที่ต้องการ:

result = {
    "answer": answer,
    "start": start_char,
    "end": end_char,
    "score": scores[start_index, end_index],
}
print(result)
{'answer': 'Jax, PyTorch and TensorFlow',
 'start': 78,
 'end': 105,
 'score': 0.97773}

ยอดเยี่ยม! เราได้คำตอบเหมือนกับในตัวอย่างแรกของเรา!

✏️ ลองดูสิ! ใช้คะแนนที่ดีที่สุดที่คุณคำนวณไว้ก่อนหน้านี้ เพื่อคำนวณคำตอบที่น่าจะเป็นไปได้มากที่สุดห้าลำดับ ในการตรวจสอบผลลัพธ์ของคุณ ให้กลับไปที่ pipeline แรกแล้วตั้งค่า top_k=5 ตอนที่รัน pipeline

การจัดการกับบริบทยาว (long contexts)

หากคุณต้องการ tokenize คำถามและบริบทที่ค่อยข้างยาว ที่เราใช้เป็นตัวอย่างก่อนหน้านี้ คุณจะได้ token ที่มีความยาวสูงกว่าความยาวสูงสุดที่จำกัดไว้ใน pipeline question-answering (ซึ่งคือ 384):

inputs = tokenizer(question, long_context)
print(len(inputs["input_ids"]))
461

ดังนั้น เราจึงจำเป็นจะต้องตัดทอน input ของเราให้ความยาวเท่ากับความยาวสูงสุด มีหลายวิธีที่เราสามารถทำได้ อย่างไรก็ตาม เราไม่ต้องการตัดคำถามให้สั้นลง เราต้องการตัดเฉพาะตัวบริบทเท่านั้น เนื่องจากบริบทอยู่ในตำแหน่งของประโยคที่สอง เราจะใช้กลยุทธ์การตัดทอนที่เรียกว่า "only_second" อย่างไรก็ตาม ปัญหาหนึ่งที่อาจจะเกิดขึ้นก็คือ คำตอบของคำถามอาจจะอยู่ในส่วนที่ถูกตัดออกไป เช่นในตัวอย่างข้างบน เราได้เลือกคำถามที่คำตอบอยู่ในตอนท้ายของบริบท และเมื่อเราตัดทอนส่วนท้ายของบริบทออกไป คำตอบที่เราต้องการก็จะหายไปด้วย:

inputs = tokenizer(question, long_context, max_length=384, truncation="only_second")
print(tokenizer.decode(inputs["input_ids"]))
"""
[CLS] Which deep learning libraries back [UNK] Transformers? [SEP] [UNK] Transformers : State of the Art NLP

[UNK] Transformers provides thousands of pretrained models to perform tasks on texts such as classification, information extraction,
question answering, summarization, translation, text generation and more in over 100 languages.
Its aim is to make cutting-edge NLP easier to use for everyone.

[UNK] Transformers provides APIs to quickly download and use those pretrained models on a given text, fine-tune them on your own datasets and
then share them with the community on our model hub. At the same time, each python module defining an architecture is fully standalone and
can be modified to enable quick research experiments.

Why should I use transformers?

1. Easy-to-use state-of-the-art models:
  - High performance on NLU and NLG tasks.
  - Low barrier to entry for educators and practitioners.
  - Few user-facing abstractions with just three classes to learn.
  - A unified API for using all our pretrained models.
  - Lower compute costs, smaller carbon footprint:

2. Researchers can share trained models instead of always retraining.
  - Practitioners can reduce compute time and production costs.
  - Dozens of architectures with over 10,000 pretrained models, some in more than 100 languages.

3. Choose the right framework for every part of a model's lifetime:
  - Train state-of-the-art models in 3 lines of code.
  - Move a single model between TF2.0/PyTorch frameworks at will.
  - Seamlessly pick the right framework for training, evaluation and production.

4. Easily customize a model or an example to your needs:
  - We provide examples for each architecture to reproduce the results published by its original authors.
  - Model internal [SEP]
"""

ซึ่งหมายความว่า มันจะยากมากที่โมเดลของเราจะเลือกคำตอบได้ถูกต้อง เพื่อแก้ไขปัญหานี้ ไปป์ไลน์ question-answering จะแบ่งบริบทออกเป็นส่วนย่อยๆ ที่ไม่ยาวเกินความยาวสูงสุด

เพื่อให้แน่ใจว่า เราจะไม่แบ่งบริบทผิดตำแหน่งจนโมเดลไม่สามารถค้นหาคำตอบได้ เราจะแบ่งโดย ให้บริบทย่อยแต่ละส่วนมีส่วนที่ทับซ้อนกันด้วย เราสามารถใช้ tokenizer (ทั้งแบบเร็วและช้า) ทำสิ่งนี้ให้เราได้ โดยคุณจะต้องตั้งค่า return_overflowing_tokens=True นอกจากนั้น เพื่อกำหนดว่าเราจะให้ข้อความทับซ้อนกันมากแค่ไหน เราจำกำหนดค่าให้กับ argument stride

ดูตัวอย่างข้างล่างนี้ :

sentence = "This sentence is not too long but we are going to split it anyway."
inputs = tokenizer(
    sentence, truncation=True, return_overflowing_tokens=True, max_length=6, stride=2
)

for ids in inputs["input_ids"]:
    print(tokenizer.decode(ids))
'[CLS] This sentence is not [SEP]'
'[CLS] is not too long [SEP]'
'[CLS] too long but we [SEP]'
'[CLS] but we are going [SEP]'
'[CLS] are going to split [SEP]'
'[CLS] to split it anyway [SEP]'
'[CLS] it anyway. [SEP]'

คุณจะเห็นว่า ตอนนี้ประโยคถูกแบ่งออกเป็นส่วนๆ โดยแต่ละส่วนจะมีไม่เกิน 6 token และมี token ที่ทับซ้อนกัน 2 token (สังเกตว่า ประโยคสุดท้ายมีเพียง 4 token ในกรณี เราจะต้องเพิ่ม padding token ทีหลังเพื่อให้มันยาวเท่ากับส่วนอื่นๆ)

มาดูผลลัพธ์ของการ tokenization อย่างละเอียดยิ่งขึ้น:

print(inputs.keys())
dict_keys(['input_ids', 'attention_mask', 'overflow_to_sample_mapping'])

ผลลัพธ์จากการแบ่งประโยคนี้ คือ input_ids และ attention_mask ส่วนคีย์สุดท้าย overflow_to_sample_mapping เป็น map ที่บอกเราว่าแต่ละประโยคย่อยมาจากประโยค input ตำแหน่งที่เท่าไร ในตัวอย่างของเรา เราใช้แค่ประโยคเดียวเป็น input และเราได้ 7 ประโยคย่อยเป็น output แปลว่าทุกประโยคย่อยก็จะถูก map ไปหาประโยคหลักที่มี ID เดียวกัน :

print(inputs["overflow_to_sample_mapping"])
[0, 0, 0, 0, 0, 0, 0]

feature นี้จะมีประโยชน์เมื่อเราใช้ประโยคหลายเป็น input ตัวอย่างเช่น:

sentences = [
    "This sentence is not too long but we are going to split it anyway.",
    "This sentence is shorter but will still get split.",
]
inputs = tokenizer(
    sentences, truncation=True, return_overflowing_tokens=True, max_length=6, stride=2
)

print(inputs["overflow_to_sample_mapping"])

เราจะได้ :

[0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1]

ซึ่งหมายความว่า ประโยคแรกถูกแบ่งออกเป็น 7 ส่วน และ ประโยคที่สองถูกแบ่งออกเป็น 4 ส่วน

กลับมาดูกันว่า เราจะจัดการกับบริบทยาวๆได้อย่างไร ไปป์ไลน์ question-answering จำกัดความยาวสูงสุดไว้ที่ 384 และค่า stride ถูกตั้งไว้ที่ 128 ซึ่งสอดคล้องกับค่าที่ใช้ตอนที่โมเดลถูก fine-tune (คุณสามารถปรับ parameters เหล่านั้นได้ โดยตั้งค่า max_seq_len และ stride เมื่อเรียกไปป์ไลน์) เราจะใช้ค่าเริ่มต้นพวกนี้ในการแบ่งบริบทเป็นส่วนย่อยๆ นอกจากนี้ เราจะตั้งค่า padding (เพื่อให้มีแต่ละส่วนที่มีความยาวเท่ากัน และเพื่อที่เราจะได้นำมันไปสร้าง tensor ได้) และค่า offsets ด้วย:

inputs = tokenizer(
    question,
    long_context,
    stride=128,
    max_length=384,
    padding="longest",
    truncation="only_second",
    return_overflowing_tokens=True,
    return_offsets_mapping=True,
)

inputs เหล่านั้นจะมี input ID และ attention masks เช่นเดียวกับ offsets และ overflow_to_sample_mapping ที่เราเพิ่งพูดถึง เนื่องจากทั้งสองอย่างหลังนี้ไม่ใช่ parameters ที่ใช้โดยโมเดล เราจะเอามันออกจาก inputs ก่อนที่จะแปลง inputs เป็น tensor:

_ = inputs.pop("overflow_to_sample_mapping")
offsets = inputs.pop("offset_mapping")

inputs = inputs.convert_to_tensors("pt")
print(inputs["input_ids"].shape)
torch.Size([2, 384])

บริบทแบบยาวของเรา ตอนนี้ถูกแบ่งออกเป็นสองส่วน ซึ่งหมายความว่า หลังจากเราใส่มันเข้าไปในโมเดลแล้ว เราจะได้ค่า start logits และ end logits อย่างละ 2 เซ็ต :

outputs = model(**inputs)

start_logits = outputs.start_logits
end_logits = outputs.end_logits
print(start_logits.shape, end_logits.shape)
torch.Size([2, 384]) torch.Size([2, 384])

เช่นเดียวกับตัวอย่างก่อน ก่อนอื่นเราจะปิด(mask) token ที่ไม่ได้เป็นส่วนหนึ่งของบริบท ก่อนที่จะใช้ softmax นอกจากนี้เราจะปิด padding tokens ทั้งหมดด้วย (ตามการตั้งค่าใน attention mask):

sequence_ids = inputs.sequence_ids()
# Mask everything apart from the tokens of the context
mask = [i != 1 for i in sequence_ids]
# Unmask the [CLS] token
mask[0] = False
# Mask all the [PAD] tokens
mask = torch.logical_or(torch.tensor(mask)[None], (inputs["attention_mask"] == 0))

start_logits[mask] = -10000
end_logits[mask] = -10000

จากนั้นเราจะใช้ softmax เพื่อแปลง logits เป็นความน่าจะเป็น:

start_probabilities = torch.nn.functional.softmax(start_logits, dim=-1)
end_probabilities = torch.nn.functional.softmax(end_logits, dim=-1)

ขั้นตอนต่อไปนั้น คล้ายกับสิ่งที่เราทำกับบริบทแบบสั้นก่อนหน้านี้ เราจะรัน process เดียวกันนี้กับประโยคย่อยทั้งสองส่วนที่เราได้มา จากนั้น เราจะแจกจ่าย score ไปให้กับทุกๆ span ของคำตอบที่เป็นไปได้ และสุดท้ายเราจะเลือก span ที่มี score สูงที่สุด

candidates = []
for start_probs, end_probs in zip(start_probabilities, end_probabilities):
    scores = start_probs[:, None] * end_probs[None, :]
    idx = torch.triu(scores).argmax().item()

    start_idx = idx // scores.shape[1]
    end_idx = idx % scores.shape[1]
    score = scores[start_idx, end_idx].item()
    candidates.append((start_idx, end_idx, score))

print(candidates)
[(0, 18, 0.33867), (173, 184, 0.97149)]

output ที่เราได้คือ span คำตอบที่ดีที่สุดของแต่ละประโยคย่อย ที่โมเดลคำนวณได้ เราจะเห็นว่าโมเดลให้ค่าความมั่นใจที่สูงมากๆกับ span คำตอบในประโยคที่สองมากกว่าประโยคแรก (ซึ่งเป็นสัญญาณที่ดี!) สิ่งที่เราต้องทำหลังจากนี้ก็คือ map ค่า span ไปสู่ตัวอักษร เพื่อดูว่า คำตอบที่โมเดลคำนวณได้คืออะไร

✏️ ลองดูสิ! ปรับโค้ดด้านบนเพื่อให้มัน return score และ span ของคำตอบที่น่าจะเป็นไปได้มากที่สุด 5 ลำดับ (โดยเปรียบเทียบ score ของทุกประโยคย่อย)

ค่า offsets ที่เราใช้ก่อนหน้านี้ เป็น list ของ offsets โดยที่แต่ละประโยคย่อยจะมีหนึ่ง list :

for candidate, offset in zip(candidates, offsets):
    start_token, end_token, score = candidate
    start_char, _ = offset[start_token]
    _, end_char = offset[end_token]
    answer = long_context[start_char:end_char]
    result = {"answer": answer, "start": start_char, "end": end_char, "score": score}
    print(result)
{'answer': '\n🤗 Transformers: State of the Art NLP', 'start': 0, 'end': 37, 'score': 0.33867}
{'answer': 'Jax, PyTorch and TensorFlow', 'start': 1892, 'end': 1919, 'score': 0.97149}

ถ้าไม่นับผลลัพธ์แรกที่เรา print ออกมาด้วย เราก็จะได้ผลลัพธ์เดียวกันกับผลลัพธ์จากไปป์ไลน์ — เย้!

✏️ ลองดูสิ! ใช้ score ที่ดีที่สุดที่คุณคำนวณได้ก่อนหน้านี้ เพื่อแสดงคำตอบที่น่าจะเป็นไปได้มากที่สุด 5 ลำดับ (สำหรับบริบททั้งหมด ไม่ใช่แต่ละส่วน) เพื่อตรวจสอบผลลัพธ์ของคุณ ให้กลับไปที่ไปป์ไลน์แรกแล้วตั้งค่า top_k=5 เวลารัน

บทนี้ถือว่าเป็น การสรุปจบการเรียนรู้ความสามารถของ tokenizer แบบละเอียด ในบทต่อไปคุณจะได้ใช้ความรู้ที่เรียนมานี้ เพื่อฝึกฝนอีก โดยคุณจะได้ฝึก fine-tune โมเดลเพื่อ task ทั่วๆไป ของ NLP