CSE 10124 — Building ChatGPT: Lab 02¶
From Text to Model Inputs (10 points)¶
- Name:
- NETID:
- Due: Sunday, September 27, 2026 at 11:59 PM Eastern
Overview¶
In Lab 01, each MNIST image already arrived as numerical pixel values. Text starts as a string. In this lab, you will build the bridge from text to bytes, token IDs, and embedding vectors that a neural network can process.
From concept to implementation¶
Build on Homework 03 by implementing byte-pair encoding (BPE), comparing tokens learned from different texts, and connecting the tokenizer to a PyTorch embedding layer.
- Tokenization and byte representations → Task 01
- Learning and applying BPE merges → Task 02
- Controlling token boundaries with regex → Task 03
- Token IDs and embedding lookup → Task 04
What you will build¶
You will write pair-counting and merging functions, complete a simple BPE tokenizer, add regex-based text splitting, and look up an embedding vector for each token ID.
Learning objectives¶
By the end of this lab, you should be able to:
- Represent text as a sequence of UTF-8 bytes.
- Count adjacent pairs and combine them into new tokens.
- Explain why different training texts produce different vocabularies.
- Explain how regex splitting limits where BPE can merge.
- Convert integer token IDs into a tensor of embedding vectors.
| Task ID | Description | Points |
|---|---|---|
| 01 | Represent text and merge byte pairs | 2.0 |
| 02 | Complete and compare simple BPE tokenizers | 3.0 |
| 03 | Complete and compare regex BPE tokenizers | 3.0 |
| 04 | Look up embedding vectors | 2.0 |
| 05 | Generate submission | 0 |
| Total | 10.0 |
Work from top to bottom. Complete each TODO and keep the requested variable names. Line counts are guides, not grading requirements. Run each completed cell and check its output.
Key Terms¶
- Byte — an integer from 0 through 255. UTF-8 represents a character using one or more bytes.
- Token — a unit of text represented by the tokenizer. In this lab, tokens begin as individual bytes and can grow through merges.
- Token ID — an integer identifying a token; its numerical size does not describe the token's meaning.
- Vocabulary — the mapping between token IDs and the text or bytes they represent.
- Byte-pair encoding (BPE) — a procedure that repeatedly merges a frequent adjacent pair into a new token.
- Merge rule — a learned instruction to replace a particular pair with its combined token ID.
- Encoding / decoding — converting text into token IDs / reconstructing text from those IDs.
- Regular expression (regex) — a pattern used here to split text into chunks before BPE is applied.
- Special token — a reserved marker such as
<|sos|>or<|eos|>. - Embedding — a vector associated with a token ID. An embedding layer stores one vector per vocabulary entry.
Task 00: Setup (0 pts.)¶
Run the cell below in Google Colab to download the course repository and import irishGPT.utilities for reading the training files.
Sanity check: the cell should finish with the utilities module imported. Repository download messages may vary. Resolve any import error before continuing.
import os
try:
import google.colab
REPO_URL = "https://github.com/wtheisen/nd-cse-10124-lectures.git"
REPO_NAME = "/content/nd-cse-10124-lectures"
L_PATH = "nd-cse-10124-lectures"
%cd /content/
!rm -r {REPO_NAME}
# Clone repo
if not os.path.exists(REPO_NAME):
!git clone {REPO_URL}
# cd into the data folder
%cd {L_PATH}
!pwd
except ImportError:
print("Unable to download repo, either:")
print("\tA.) You're not on colab")
print("\tB.) It has already been cloned")
import irishGPT.utilities as uts
Task 01: Represent Text and Merge Byte Pairs (2 pts.)¶
As in Homework 03, begin with a consistent numerical representation: UTF-8 bytes.
Task 01A: Inspect the byte representation¶
Store rug pug hug pun bun hugs run gun bug in a variable named prompt. Print the string and the list produced by encoding it as UTF-8 bytes. These characters each use one byte; that is not true for every Unicode character.
Sanity check: the list should contain 36 integers, including 32 for each space.
Grading (2.0 total for Task 01): byte representation (0.5); pair-counting and merging functions (1.0); merge demonstration (0.5).
LINES: about 3 for this cell
# TODO: add the string to a variable called "prompt"
# TODO: Print the variable
# TODO: print the variables tokens
Task 01A: Expected Output (0 pts.)¶
rug pug hug pun bun hugs run gun bug
[114, 117, 103, 32, 112, 117, 103, 32, 104, 117, 103, 32, 112, 117, 110, 32, 98, 117, 110, 32, 104, 117, 103, 115, 32, 114, 117, 110, 32, 103, 117, 110, 32, 98, 117, 103]
The string and byte values should match exactly. Each 32 represents a space.
Task 01B: Count and merge adjacent pairs¶
The pairs (117, 103) and (117, 110) represent ug and un. BPE counts adjacent pairs and replaces a frequent pair with a new token ID.
Complete count_pairs(tokens) so it returns a Counter of every adjacent pair. Complete merge_pair(tokens, target_pair, combined_id) so it scans left to right, replaces each non-overlapping occurrence of the target pair, and preserves all other tokens in order.
Sanity check: counting [1, 2, 1, 2] should give two occurrences of (1, 2) and one of (2, 1). Merging (1, 2) into ID 3 should produce [3, 3].
LINES: about 2 for counting and 8 for merging
from collections import Counter
def count_pairs(tokens):
pair_counts = Counter()
# TODO: Finish the count_pairs function
# LINES: 2
return pair_counts
def merge_pair(tokens, target_pair, combined_id):
merged_tokens = []
# TODO: Finish the merge_pair function
# LINES: 8
return merged_tokens
Task 01C: Apply one BPE merge¶
Use your functions on the byte representation of prompt. Store the pair counts in pairs_counts. The supplied code selects the most frequent pair; merge it using ID 257 and store the result in merged_tokens.
Here, 257 is a demonstration ID. The tokenizer classes below reserve 256 and 257 for special tokens, so their learned merges start at 258.
Sanity check: the sequence should shrink from 36 IDs to 31. The underlying text is unchanged; one ID now represents each occurrence of the selected pair.
LINES: about 2
prompt = 'rug pug hug pun bun hugs run gun bug'
print('Length of prompt before merging:', len(list(prompt.encode('utf-8'))))
print('Unmerged Prompt:', list(prompt.encode('utf-8')))
# TODO: Count the pairs of our prompt
most_common_pair = pairs_counts.most_common()[0][0]
# TODO: Merge the most common pair in our prompt, using token id 257 as the new token
print('Length of prompt after merging:', len(merged_tokens))
print('Merged Prompt:', merged_tokens)
Task 01C: Expected Output (0 pts.)¶
Length of prompt before merging: 36
Unmerged Prompt: [114, 117, 103, 32, 112, 117, 103, 32, 104, 117, 103, 32, 112, 117, 110, 32, 98, 117, 110, 32, 104, 117, 103, 115, 32, 114, 117, 110, 32, 103, 117, 110, 32, 98, 117, 103]
Length of prompt after merging: 31
Merged Prompt: [114, 257, 32, 112, 257, 32, 104, 257, 32, 112, 117, 110, 32, 98, 117, 110, 32, 104, 257, 115, 32, 114, 117, 110, 32, 103, 117, 110, 32, 98, 257]
The lengths should match exactly: five occurrences of ug become ID 257, reducing 36 IDs to 31. Decoding would still give the same text.
Task 02: Complete and Compare Simple BPE Tokenizers (3 pts.)¶
Task 02A: Complete the tokenizer¶
Simple_Tokenizer repeats Task 01’s merge process to build a vocabulary and merge rules.
Complete the pair-counting and merging methods using the same logic as Task 01. In train, count pairs, assign the next available ID, merge the selected pair, and record the result in both self.merges and self.vocab. Vocabulary entries for learned tokens should contain the concatenated bytes of their two components.
Read the supplied methods: encode applies learned merges in order; decode joins the stored bytes to reconstruct text.
Sanity check: learned IDs must start after the 256 byte IDs and two reserved IDs. For the ordinary prompt used here, decoding its encoding should recover the original text.
Grading (3.0 total for Task 02): working tokenizer implementation (2.0); training and comparing all three datasets (1.0).
LINES: about 2 for counting, 8 for merging, and 5 in train
"""
Minimal (byte-level) Byte Pair Encoding tokenizer.
Algorithmically follows along the GPT tokenizer:
https://github.com/openai/gpt-2/blob/master/src/encoder.py
"""
class Simple_Tokenizer():
def __init__(self):
self.merges = {} # (int, int) -> int
self.special_tokens = {'<|sos|>': 256, '<|eos|>': 257}
self.vocab = {idx: bytes([idx]) for idx in range(256)}
self.vocab[256] = '<|sos|>'
self.vocab[257] = '<|eos|>'
def _count_pairs(self, tokens, counts=None):
pair_counts = Counter()
# TODO: Finish the _count_pairs function
# LINES: 2
return pair_counts
def _merge_pair(self, tokens, target_pair, combined_id):
merged_tokens = []
# TODO: Finish the _merge_pair function
# LINES: 8
return merged_tokens
def train(self, text, max_vocab_size=1024):
vocab_size = len(self.vocab)
num_merges = max_vocab_size - vocab_size
text_bytes = text.encode('utf-8')
tokens = list(text_bytes)
for i in range(num_merges):
# TODO: Count the pairs
# This code will handle us having no more pairs to merge
try:
target_pair = pairs.most_common()[0][0]
except:
break
# TODO: Calculate the id for our new pair
# TODO: Merge the pair with the new id
# TODO: Save the id for the new pair in self.merges {pair -> id}
# TODO: Save what the id represents in self.vocab {id -> str}
def decode(self, tokens):
text_bytes = b''.join(self.vocab[token] for token in tokens)
return text_bytes.decode('utf-8')
def encode(self, text):
text_bytes = text.encode('utf-8')
tokens = list(text_bytes)
while len(tokens) >= 2:
pairs = self._count_pairs(tokens)
earliest_pair = min(pairs, key=lambda p: self.merges.get(p, float("inf")))
if earliest_pair not in self.merges:
break
pair_token_id = self.merges[earliest_pair]
tokens = self._merge_pair(tokens, earliest_pair, pair_token_id)
return tokens
Task 02B: Compare training texts¶
Create three separate Simple_Tokenizer() instances named jabber_tokenizer, zoomer_tokenizer, and shakes_tokenizer. Train each with max_vocab_size=512 on its corresponding supplied training string:
Datasets/jabberwocky.txtDatasets/zoomer.txtDatasets/shakespeare.txt
Use Simple_Tokenizer for all three instances in this task. Regex_Tokenizer is introduced in Task 03.
Print each tokenizer’s encoding of the same prompt and compare sequence lengths. Different training texts produce different merge rules; IDs from separate vocabularies do not have a shared meaning.
Sanity check: each tokenizer should encode the prompt successfully. Compare the sequences with the reference below, and check that decoding each sequence recovers prompt.
The Shakespeare training run may take roughly 10 minutes. Let it finish before continuing.
LINES: about 6
prompt = 'rug pug hug pun bun hugs run gun bug'
print(list(prompt.encode('utf-8')))
jabberwocky_training_str = uts.get_file_as_string('Datasets/jabberwocky.txt')
zoomer_training_str = uts.get_file_as_string('Datasets/zoomer.txt')
shakespeare_training_str = uts.get_file_as_string('Datasets/shakespeare.txt')
# TODO: Create a Simple_Tokenizer()
# TODO: Train it on the jabberwocky_training_str with a vocab of 512
print('Jabberwocky Tokenization of Prompt:', jabber_tokenizer.encode(prompt))
# TODO: Create a Simple_Tokenizer()
# TODO: Train it on the zoomer_training_str with a vocab of 512
print('Zoomer Tokenization of Prompt:', zoomer_tokenizer.encode(prompt))
# TODO: Create a Simple_Tokenizer()
# TODO: Train it on the shakespeare_training_str with a vocab of 512
print('Shakespearean Tokenization of Prompt:', shakes_tokenizer.encode(prompt))
Task 02B: Expected Output (0 pts.)¶
[114, 117, 103, 32, 112, 117, 103, 32, 104, 117, 103, 32, 112, 117, 110, 32, 98, 117, 110, 32, 104, 117, 103, 115, 32, 114, 117, 110, 32, 103, 117, 110, 32, 98, 117, 103]
Jabberwocky Tokenization of Prompt: [114, 117, 409, 112, 117, 409, 104, 117, 409, 112, 117, 110, 312, 117, 110, 32, 104, 117, 103, 261, 114, 117, 110, 32, 103, 117, 110, 312, 117, 103]
Zoomer Tokenization of Prompt: [114, 117, 103, 32, 112, 117, 103, 32, 104, 117, 103, 32, 112, 117, 373, 348, 373, 104, 117, 103, 262, 114, 117, 373, 103, 117, 373, 348, 103]
Shakespearean Tokenization of Prompt: [409, 103, 32, 112, 117, 103, 299, 117, 103, 32, 112, 334, 32, 98, 334, 299, 117, 103, 261, 114, 334, 32, 103, 334, 32, 98, 117, 103]
These are reference sequences for the supplied datasets. Compare sequence lengths and confirm that each sequence decodes to prompt. Exact IDs depend on the learned merge order; if they differ, check pair counts, tie handling, and ID assignment.
Task 03: Complete and Compare Regex BPE Tokenizers (3 pts.)¶
Task 03A: Inspect text chunks¶
A regex tokenizer splits text into chunks before learning and applying BPE merges. Unlike the simple tokenizer, it cannot merge across chunk boundaries.
Run the supplied pattern on prompt; you do not need to write or memorize the regex. Notice how leading spaces stay with the following words.
Sanity check: you should see nine chunks. Joining them in order should reconstruct the original prompt.
Grading (3.0 total for Task 03): complete chunk-based training and encoding (2.0); train and compare all three tokenizers (1.0).
import regex as re
GPT4_SPLIT_PATTERN = r"""'(?i:[sdmt]|ll|ve|re)|[^\r\n\p{L}\p{N}]?+\p{L}+|\p{N}{1,3}| ?[^\s\p{L}\p{N}]++[\r\n]*|\s*[\r\n]|\s+(?!\S)|\s+"""
regex = re.compile(GPT4_SPLIT_PATTERN)
prompt = 'rug pug hug pun bun hugs run gun bug'
print(prompt)
text_chunks = re.findall(regex, prompt)
print(text_chunks)
Task 03A: Expected Output (0 pts.)¶
rug pug hug pun bun hugs run gun bug
['rug', ' pug', ' hug', ' pun', ' bun', ' hugs', ' run', ' gun', ' bug']
The nine chunks should match exactly. Leading spaces are part of the chunks; joining the chunks should recover prompt.
Task 03B: Complete the regex tokenizer¶
Complete the two marked sections in Regex_Tokenizer.
In train, accumulate adjacent-pair counts across all chunks into the shared pairs counter. Count pairs within each chunk; do not count a pair spanning two chunks.
In encode, convert each text chunk to UTF-8 bytes, call _encode_chunk, and append the resulting IDs to tokens. The supplied code handles the special-token markers used in this lab's prompt.
Sanity check: training should build one shared vocabulary while keeping merges inside chunk boundaries. The marked prompt in the next task should start with ID 256 and end with ID 257.
LINES: about 2 in train and 4 in encode
class Regex_Tokenizer():
GPT4_SPLIT_PATTERN = r"""'(?i:[sdmt]|ll|ve|re)|[^\r\n\p{L}\p{N}]?+\p{L}+|\p{N}{1,3}| ?[^\s\p{L}\p{N}]++[\r\n]*|\s*[\r\n]|\s+(?!\S)|\s+"""
def __init__(self):
self.merges = {} # (int, int) -> int
self.special_tokens = {'<|sos|>': 256, '<|eos|>': 257} # str -> int, e.g. {'<|endoftext|>': 100257}
self.vocab = {idx: bytes([idx]) for idx in range(256)} | {idx: special.encode("utf-8") for special, idx in self.special_tokens.items()}
self.pattern = self.GPT4_SPLIT_PATTERN
self.compiled_pattern = re.compile(self.pattern)
def _count_pairs(self, tokens, pair_counts=None):
for pair in zip(tokens, tokens[1:]):
pair_counts[pair] += 1
return pair_counts
def _merge_pairs(self, tokens, target_pair, combined_id):
merged_tokens = []
i = 0
while i < len(tokens):
if i < len(tokens) - 1 and (tokens[i], tokens[i+1]) == target_pair:
merged_tokens.append(combined_id)
i += 2
else:
merged_tokens.append(tokens[i])
i += 1
return merged_tokens
def chunk(self, text):
return re.findall(self.compiled_pattern, text)
def train(self, text, max_vocab_size, verbose=False):
pretrain_vocab_size = len(self.vocab)
num_merges = max_vocab_size - pretrain_vocab_size
text_chunks = re.findall(self.compiled_pattern, text)
ids = [list(ch.encode("utf-8")) for ch in text_chunks]
for i in range(num_merges):
pairs = Counter()
# TODO: for each chunk, count the number of times every consecutive pair appears
# LINES: 2
pair = pairs.most_common()[0][0]
idx = pretrain_vocab_size + i
ids = [self._merge_pairs(chunk_ids, pair, idx) for chunk_ids in ids]
self.merges[pair] = idx
self.vocab[idx] = self.vocab[pair[0]] + self.vocab[pair[1]]
def decode(self, ids):
# given ids (list of integers), return Python string
part_bytes = []
for idx in ids:
if idx in self.vocab:
part_bytes.append(self.vocab[idx])
else:
part_bytes.append('<|unk|>')
text_bytes = b"".join(part_bytes)
text = text_bytes.decode("utf-8", errors="replace")
return text
def _encode_chunk(self, text_bytes):
# return the token ids
ids = list(text_bytes)
while len(ids) >= 2:
# find the pair with the lowest merge index
stats = self._count_pairs(ids, Counter())
pair = min(stats, key=lambda p: self.merges.get(p, float("inf")))
if pair not in self.merges:
break # nothing else can be merged anymore
idx = self.merges[pair]
ids = self._merge_pairs(ids, pair, idx)
return ids
def encode(self, text):
special_pattern = "(" + "|".join(re.escape(k) for k in self.special_tokens) + ")"
special_chunks = re.split(special_pattern, text)
tokens = []
for part in special_chunks[1:-1]:
if part in self.special_tokens:
tokens.append(self.special_tokens[part])
else:
text_chunks = self.chunk(part)
# TODO: encode each chunk of text and add the tokens to the list
# LINES: 4
# NOTE: You can use the _encode_chunk function above to help with this!
return tokens
Task 03C: Compare regex tokenizers¶
Create three separate Regex_Tokenizer() instances using the same variable names as Task 02. Train each on its corresponding dataset with a vocabulary size of 512, then print its encoding of the supplied prompt.
Keep the supplied <|sos|> and <|eos|> markers in the prompt.
Compare with Task 02: which differences reflect training text, and which reflect regex boundaries?
Sanity check: all three sequences should begin with 256 and end with 257. Decode each sequence to check that the marked prompt is preserved.
The Shakespeare training run may again take roughly 10 minutes.
LINES: about 6
prompt = '<|sos|>rug pug hug pun bun hugs run gun bug<|eos|>'
print(list(prompt.encode('utf-8')))
jabberwocky_training_str = uts.get_file_as_string('Datasets/jabberwocky.txt')
zoomer_training_str = uts.get_file_as_string('Datasets/zoomer.txt')
shakespeare_training_str = uts.get_file_as_string('Datasets/shakespeare.txt')
# TODO: Create a Regex_Tokenizer()
# TODO: Train it on the jabberwocky_training_str with a vocab of 512
print('Jabberwocky Tokenization of Prompt:', jabber_tokenizer.encode(prompt))
# TODO: Create a Regex_Tokenizer()
# TODO: Train it on the zoomer_training_str with a vocab of 512
print('Zoomer Tokenization of Prompt:', zoomer_tokenizer.encode(prompt))
# TODO: Create a Regex_Tokenizer()
# TODO: Train it on the shakespeare_training_str with a vocab of 512
print('Shakespearean Tokenization of Prompt:', shakes_tokenizer.encode(prompt))
Task 03C: Expected Output (0 pts.)¶
[60, 124, 115, 111, 115, 124, 62, 114, 117, 103, 32, 112, 117, 103, 32, 104, 117, 103, 32, 112, 117, 110, 32, 98, 117, 110, 32, 104, 117, 103, 115, 32, 114, 117, 110, 32, 103, 117, 110, 32, 98, 117, 103, 60, 124, 101, 111, 115, 124, 62]
Jabberwocky Tokenization of Prompt: [256, 114, 117, 103, 32, 112, 117, 103, 310, 117, 103, 32, 112, 117, 110, 503, 110, 310, 117, 103, 115, 314, 117, 110, 285, 117, 110, 503, 103, 257]
Zoomer Tokenization of Prompt: [256, 114, 117, 103, 293, 117, 103, 296, 117, 103, 293, 431, 276, 431, 296, 117, 103, 115, 355, 431, 286, 431, 276, 117, 103, 257]
Shakespearean Tokenization of Prompt: [256, 425, 103, 292, 117, 103, 290, 117, 103, 292, 117, 110, 270, 117, 110, 290, 117, 103, 115, 32, 425, 110, 306, 117, 110, 270, 117, 103, 257]
Each sequence should begin with 256 and end with 257, the reserved boundary markers. Check that decoding recovers the complete marked prompt. As in Task 02, interior IDs depend on the learned merge order.
Task 04: Look Up Embedding Vectors (2 pts.)¶
A token ID identifies a vocabulary entry. An embedding layer uses that ID to select a vector that a neural network can process.
Use the trained shakes_tokenizer from Task 03 to encode the marked prompt. Convert the IDs into a PyTorch LongTensor. Create an nn.Embedding table with one row per vocabulary entry and 8 values per row, then look up the vectors. Print the ID tensor's shape and the embedding tensor's shape.
The table is randomly initialized; this exercise performs lookup without learning meaning. Language-model training would update these vectors along with other model parameters.
Sanity check: for n tokens, the ID tensor should have shape (n,) and the embedding tensor should have shape (n, 8). Every ID must be a valid row index. The random vector values need not match anyone else's.
Finally, answer the reflection below in 1–2 sentences.
Grading (2.0): token-ID tensor and embedding lookup (1.0); correct shapes (0.5); explanation (0.5).
LINES: about 7, plus 1–2 sentences
import torch
from torch import nn
# TODO: encode prompt with shakes_tokenizer and make a LongTensor of token IDs.
# TODO: compute the vocabulary size and create nn.Embedding(vocab_size, 8).
# TODO: look up and print the ID and embedding shapes.
# LINES: ~7
Task 04: Expected Output (0 pts.)¶
For the 29-token Shakespeare reference sequence in Task 03C:
torch.Size([29])
torch.Size([29, 8])
Your first dimension must equal the number of IDs your tokenizer produced; the embedding dimension must be 8. The vector values are random and need not match a reference.
Task 04 Reflection¶
Why does a language model receive embedding vectors rather than raw strings? What role do token IDs play in selecting those vectors?
TODO: Write 1–2 sentences here.
Task 05: Generate Submission (0 pts.)¶
First run every cell above and confirm that your outputs are visible. Then replace YOUR_NETID in the code cell below, uncomment the final function call, and run the cell. Colab will convert the current executed notebook into lab02_NETID.html and download it to your computer.
Submit the generated HTML file on Canvas. The HTML preserves your code, written responses, and outputs in one file that the grader can open without rerunning the notebook.
import json
import subprocess
from pathlib import Path
NETID = "YOUR_NETID" # Replace with your Notre Dame NetID.
def export_notebook(netid):
netid = netid.strip()
if not netid or netid == "YOUR_NETID" or not netid.isalnum():
raise ValueError("Replace YOUR_NETID with your Notre Dame NetID before exporting.")
try:
from google.colab import _message, files
except ImportError as exc:
raise RuntimeError("Run this export cell in Google Colab.") from exc
base_name = f"lab02_{netid}"
ipynb_path = Path("/content") / f"{base_name}.ipynb"
html_path = ipynb_path.with_suffix(".html")
notebook = _message.blocking_request("get_ipynb", timeout_sec=10)["ipynb"]
with ipynb_path.open("w", encoding="utf-8") as f:
json.dump(notebook, f)
subprocess.run(
["jupyter", "nbconvert", "--to", "html", str(ipynb_path)],
check=True,
)
files.download(str(html_path))
# Uncomment this line after replacing YOUR_NETID above.
# export_notebook(NETID)
Task 05: Expected Output (0 pts.)¶
Colab should begin downloading a file named lab02_NETID.html, with your actual NetID in place of NETID. The conversion log may mention nbconvert; its exact wording and file size can vary.
Submission Checklist¶
- Enter your name and NETID at the top of the notebook.
- Complete every coding
TODOand the Task 04 reflection. - In Colab, choose Runtime → Run all and confirm that every graded cell finishes without an error.
- Keep the printed outputs for the merge demonstration, all three simple tokenizers, all three regex tokenizers, and the embedding shapes.
- Confirm that your tokenizers preserve the example text when encoding and decoding.
- In Task 05, replace
YOUR_NETID, uncommentexport_notebook(NETID), and run that cell. - Confirm that Colab downloads
lab02_NETID.htmlwith your actual NetID in the filename. - Upload that
.htmlfile to the Lab 02 assignment on Canvas. Do not submit the.ipynbfile or a PDF.