The Hugging Face Ecosystem (Essentials)
Classroom material
- Time
- ~47 min
- Sections
- 8
- Code cells
- 25
- Figures
- 5
Before you start
What does the Hugging Face ecosystem give us, and what exactly are we downloading when we load a model by name?
- You will be able to
- describe how the Hub, the local cache and the three core libraries fit together;
- read a model repository's file list and say what each file is for, and which ones are actually downloaded;
- read a model card's metadata, evaluation claims and limitations, and test a claim yourself;
- use Datasets to load, inspect, filter, tokenise and batch a dataset;
- explain what the Tokenizers library does, and show why a model and its tokeniser must be used together;
- check a model's licence and access restrictions before relying on it.
In Lesson 7.1 we downloaded two pretrained models by name, and the library quietly did a lot for us. It found the model on a website, decided which files to fetch, stored them on our disk, rebuilt the architecture from a configuration, loaded the weights and prepared a tokeniser. To use pretrained models confidently, and to work out what went wrong when something fails, you need to know what each of those pieces is and where it lives.
Almost all of that machinery comes from one company and its open-source projects: Hugging Face. Its website, the Hub, is where most openly shared models and datasets are published, and its Python libraries are the standard way to load and use them. This lesson is a guided tour of that ecosystem: the essentials you need for the rest of Modules 7 and 8, not every feature.
Key question: What does the Hugging Face ecosystem give us, and what exactly are we downloading when we load a model by name?
Prerequisites
read Lesson 7.1 first. You will need the idea of a configuration plus weights (7.1, Section 6), tokens and subword tokenisation (Lessons 5.3 and 6.2), and PyTorch's DataLoader from Module 2.
Setup — imports, versions and device detection used by the notebook (1 cells)
The setup cell imports the libraries and prints their versions. Hugging Face's libraries change quickly, so when you compare your results with someone else's, versions are the first thing to check. The same harmless warnings as in Lesson 7.1 appear here (the TqdmWarning about IProgress, the unauthenticated-request message, and on a Mac a few W… lines when the libraries load).
Environment and reference results. Use Python 3.12 and requirements.txt (also supplied at the root of the Module 7/8 notebook bundle). Install packages into a separate environment, restart the kernel, and run from the beginning. The saved outputs are the author's CPU reference run, with approved explanatory corrections. Seeds and artifact revisions reduce variation; numbers and timings can differ across hardware. No account or paid API is required for the public teaching models. Initial model/data downloads require internet access.
import osimport numpy as npimport torchimport matplotlib.pyplot as pltimport transformers, datasets, tokenizers, huggingface_hubfrom huggingface_hub import HfApi, ModelCard, hf_hub_download, scan_cache_dirfrom datasets import load_datasetfrom transformers import AutoConfig, AutoTokenizer, AutoModelForSequenceClassification, DataCollatorWithPaddingtorch.manual_seed(0)np.random.seed(0)device = "cuda" if torch.cuda.is_available() else "cpu"print("device:", device)for lib in (transformers, datasets, tokenizers, huggingface_hub): print(f"{lib.__name__:<16} {lib.__version__}")# Freeze the public artifacts used by the recorded experiments.MODEL_REVISIONS = {'openai/clip-vit-base-patch32': '3d74acf9a28c67741b2f4f2ea7635f0aaf6f0268', 'distilbert/distilbert-base-uncased-finetuned-sst-2-english': '714eb0fa89d2f80546fda750413ed43d93601a13', 'sentence-transformers/all-MiniLM-L6-v2': '1110a243fdf4706b3f48f1d95db1a4f5529b4d41', 'HuggingFaceTB/SmolLM2-135M': '93efa2f097d58c2a74874c7e644dbc9b0cee75a2', 'distilbert/distilgpt2': '2290a62682d06624634c1f46a6ad5be0f47f38aa', 'HuggingFaceTB/SmolLM2-135M-Instruct': '12fd25f77366fa6b3b4b768ec3050bf629380bac', 'HuggingFaceTB/SmolLM2-360M-Instruct': 'a10cc1512eabd3dde888204e902eca88bddb4951', 'Qwen/Qwen2.5-0.5B': '060db6499f32faf8b98477b0a26969ef7d8b9987', 'Qwen/Qwen2.5-7B': 'd149729398750b98c0af14eb82c78cfe92750796', 'google-bert/bert-large-uncased': '6da4b6a26a1877e173fca3225479512db81a5e5b', 'distilbert/distilbert-base-uncased': '12040accade4e8a0f71eabdb258fecc2e7e948be', 'google-bert/bert-base-uncased': '86b5e0934494bd15c9632b12f734a8a67f723594', 'cardiffnlp/twitter-roberta-base-sentiment-latest': '3216a57f2a0d9c45a2e6c20157c20c49fb4bf9c7', 'distilbert/distilbert-base-cased-distilled-squad': '564e9b582944a57a3e586bbb98fd6f0a4118db7f', 'google/vit-base-patch16-224': '3f49326eb077187dfe1c2a2bb15fbd74e6ab91e3', 'sshleifer/distilbart-cnn-12-6': 'a4f8f3ea906ed274767e9906dbaede7531d660ff', 'textattack/distilbert-base-uncased-imdb': '5b0f46c2fc4b86bf21f0ec0409bed77ee142b332', 'google/gemma-2-2b': 'c5ebcd40d208330abc697524c919956e692655cf', 'meta-llama/Llama-3.2-1B': '4e20de362430cd3b72f300e6b0f18e50e7166e08'}DATASET_REVISIONS = {'stanfordnlp/imdb': 'e6281661ce1c48d982bc483cf8a173c1bbeb5d31', 'stanfordnlp/sst2': '8d51e7e4887a4caaa95b3fbebbf53c0490b58bbb'}def benchmark_clock(): """Read elapsed time after synchronising any queued CUDA work.""" if torch.cuda.is_available(): torch.cuda.synchronize() import time return time.perf_counter()def ordered_fingerprint(texts, labels): """Hash ordered text/label pairs to verify alignment for paired comparisons. Arguments: texts: Text strings in the order expected by the caller. labels: Integer targets aligned with the input examples. """ import json, hashlib rows = [[str(t), int(y)] for t, y in zip(texts, labels)] return hashlib.sha256(json.dumps(rows, ensure_ascii=False, separators=(',', ':')).encode()).hexdigest()
/Users/solomonpromise/Documents/Simplilearn Classes/Deep Learning/_work78/.venv/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html from .autonotebook import tqdm as notebook_tqdm
device: cpu transformers 5.20.1 datasets 5.1.0 tokenizers 0.23.3 huggingface_hub 1.33.0
The Big Picture: Where Hugging Face Fits in AI Engineering
Let's begin with the whole workflow in three short cells. First, we ask for a model by its repository name, an owner and a model name separated by a slash, just like a GitHub project.
SST2 = "distilbert/distilbert-base-uncased-finetuned-sst-2-english" # a repository name on the Hubtokenizer = AutoTokenizer.from_pretrained(SST2, revision=MODEL_REVISIONS.get(SST2)) # Tokenizers library, via Transformersmodel = AutoModelForSequenceClassification.from_pretrained(SST2, revision=MODEL_REVISIONS.get(SST2)).to(device).eval() # Transformers library
W1017 12:52:03.618000 72510 site-packages/torch/distributed/elastic/multiprocessing/redirects.py:35] NOTE: Redirects are currently not supported in MacOs. W1017 12:52:03.715000 72510 site-packages/torch/utils/_pytree.py:630] <enum 'KernelPreference'> is an Enum subclass and is now natively supported by torch.compile as an opaque value type. Calling register_constant() on Enum subclasses is deprecated and will be an error in a future release. W1017 12:52:03.735000 72510 site-packages/torch/utils/_pytree.py:630] <enum 'ScaleCalculationMode'> is an Enum subclass and is now natively supported by torch.compile as an opaque value type. Calling register_constant() on Enum subclasses is deprecated and will be an error in a future release. Loading weights: 0%| | 0/104 [00:00<?, ?it/s]Loading weights: 100%|██████████| 104/104 [00:00<00:00, 12484.83it/s]
Two lines, two libraries. AutoTokenizer gives us the tokeniser; its fast text-splitting engine comes from the Tokenizers library. AutoModelForSequenceClassification comes from Transformers, and so does the code that builds the network. Both looked up the name on the Hub, downloaded what they needed, and stored it. Now we use them:
enc = tokenizer("A gripping, beautifully shot film.", return_tensors="pt").to(device) # text -> token IDswith torch.no_grad(): probs = model(**enc).logits.softmax(dim=-1)[0] # (2,) class probabilitiesprint({model.config.id2label[i]: round(p.item(), 3) for i, p in enumerate(probs)})
{'NEGATIVE': 0.0, 'POSITIVE': 1.0}
The model is sure this one is positive: 1.0 against 0.0, rounded. Where did the downloaded files go?
cache = huggingface_hub.constants.HF_HUB_CACHE # where downloads are storedprint("local cache folder ends in:", os.path.join(*cache.split(os.sep)[-2:]))
local cache folder ends in: huggingface/hub
Every download is stored in a local cache, a folder that by default is ~/.cache/huggingface/hub in your home directory. (You can move it by setting the HF_HOME environment variable, which is what was done when these lessons were built.) The second time you load the same model, nothing is downloaded: the library finds the files in the cache.
That is the whole ecosystem in one picture:
- the Hub stores and publishes models and datasets, with their documentation;
- the libraries (Transformers, Datasets and Tokenizers, with a helper library called
huggingface_hubunderneath) fetch them and turn them into working Python objects; - the cache keeps local copies so you download each file only once;
- your code sits on top and decides what to do with the model.

Figure 7.2.1. The Hub and local cache supply compatible artifacts. Tokenizers, Datasets and Transformers serve different stages of the application; cards supply evidence to inspect.
Key terms
- Hugging Face Hub: a website hosting openly shared models, datasets and demos, each in its own repository.
- Repository name: the
owner/nameidentifier of a Hub repository, such asdistilbert/distilbert-base-uncased-finetuned-sst-2-english.- Local cache: the folder where downloaded files are kept so they are not downloaded again.
- Transformers, Datasets, Tokenizers: Hugging Face's main libraries, for models, data and text splitting respectively.
The next section looks at the top layer: what is actually stored in a repository.
What connects a Hub name to a working model?
Repository files, a local cache, and compatible libraries.
The Hub, Model Repositories and Model Cards
2.1 What is in a repository?
A model repository is a folder of files with a history, much like a code repository on GitHub. The huggingface_hub library can list a repository's files and their sizes without downloading anything.
api = HfApi()info = api.model_info(SST2, files_metadata=True, revision=MODEL_REVISIONS.get(SST2)) # metadata only, no downloadsprint(f"task: {info.pipeline_tag} | library: {info.library_name}")print(f"{len(info.siblings)} files in the repository:")for f in info.siblings: print(f" {f.rfilename:<28} {f.size / 1e6:9.3f} MB")
task: text-classification | library: transformers 17 files in the repository: .gitattributes 0.000 MB README.md 0.011 MB config.json 0.001 MB map.jpeg 0.082 MB model.safetensors 267.833 MB onnx/added_tokens.json 0.000 MB onnx/config.json 0.001 MB onnx/model.onnx 267.956 MB onnx/special_tokens_map.json 0.000 MB onnx/tokenizer.json 0.711 MB onnx/tokenizer_config.json 0.001 MB onnx/vocab.txt 0.232 MB pytorch_model.bin 267.844 MB rust_model.ot 267.846 MB tf_model.h5 267.950 MB tokenizer_config.json 0.000 MB vocab.txt 0.232 MB
The repository has 17 files, and they fall into four groups.
- The configuration:
config.json, the architecture blueprint you read in Lesson 7.1. - The weights, five times over:
model.safetensors,pytorch_model.bin,tf_model.h5,rust_model.otandonnx/model.onnxare each about 268 MB (267.833 MB, 267.844 MB, 267.950 MB, 267.846 MB and 267.956 MB). They hold the same learned numbers, saved in formats for different software: the safe format we used in 7.1, the older PyTorch format, TensorFlow's format, a Rust library's format, and ONNX, a format for running models outside Python. Theonnx/folder also has its own copy of the tokeniser files, so that version works without the rest of the repository. - The tokeniser:
vocab.txtlists the vocabulary, one token per line, andtokenizer_config.jsonholds tokeniser settings. - Documentation:
README.mdis the model card, andmap.jpegis a picture the card displays..gitattributesis a bookkeeping file for the version-control system.
Transformers does not download all of this. It picks only the files it needs for PyTorch, and prefers .safetensors when a repository offers it. We'll check exactly what landed on our disk at the end of this section.
2.2 The model card
The model card is the repository's README.md: a document describing what the model is, how it was trained, what it is for and where it fails. The top of the file holds machine-readable metadata, and the rest is written for people.
card = ModelCard.load(SST2) # downloads README.md, the model cardmeta = card.data.to_dict()print("metadata:", {k: v for k, v in meta.items() if k != "model-index"})print("\nsection headings:")for line in card.text.splitlines(): if line.startswith("#"): print(" " + line)
metadata: {'datasets': ['sst2', 'glue'], 'language': 'en', 'license': 'apache-2.0'}
section headings:
# DistilBERT base uncased finetuned SST-2
## Table of Contents
## Model Details
## How to Get Started With the Model
## Uses
#### Direct Use
#### Misuse and Out-of-scope Use
## Risks, Limitations and Biases
# Training
#### Training Data
#### Training Procedure
###### Fine-tuning hyper-parameters
The metadata says the model was trained on sst2 (the dataset is also part of a benchmark collection called glue), works on English (en), and is released under the apache-2.0 licence (Section 4). The headings show a well-organised card: details, how to use it, intended uses and misuses, risks, and how it was trained.
Cards can also declare evaluation results in a structured block called model-index. Here are its accuracy entries:
for result in meta["model-index"][0]["results"]: # evaluation results declared in the card ds = result["dataset"] for m in result["metrics"]: if m.get("type") == "accuracy": print(f"dataset: {ds.get('name')} | split: {ds.get('split')} | accuracy {m.get('value'):.4f}")
dataset: glue | split: validation | accuracy 0.9106 dataset: sst2 | split: train | accuracy 0.9886
Two numbers, and only one of them is useful. On the glue validation split, a set of SST-2 sentences the model did not train on, the card reports an accuracy of 0.9106. It also reports 0.9886 on its own sst2 training split. A model's score on the data it was trained on says almost nothing about new data, as you saw when diagnosing overfitting in Module 3, so ignore that number. Note too that both numbers are on SST-2's short sentences, not on reviews like IMDb's. Lesson 7.3 measures what that means in practice.
The most valuable part of a good card is often its honest list of problems:
text = card.textstart = text.index("## Risks, Limitations and Biases")print(text[start:start + 1100])
## Risks, Limitations and Biases Based on a few experimentations, we observed that this model could produce biased predictions that target underrepresented populations. For instance, for sentences like `This film was filmed in COUNTRY`, this binary classification model will give radically different probabilities for the positive label depending on the country (0.89 if the country is France, but 0.08 if the country is Afghanistan) when nothing in the input indicates such a strong semantic shift. In this [colab](https://colab.research.google.com/gist/ageron/fb2f64fb145b4bc7c49efc97e5f114d3/biasmap.ipynb), [Aurélien Géron](https://twitter.com/aureliengeron) made an interesting map plotting these probabilities for each country. <img src="https://huggingface.co/distilbert-base-uncased-finetuned-sst-2-english/resolve/main/map.jpeg" alt="Map of positive probabilities per country." width="500"/> We strongly advise users to thoroughly probe these aspects on their use-cases in order to evaluate the risks of this model. We recommend looking at the following bias evaluation datasets as a pla
The authors report that the model treats sentences about different countries very differently: 0.89 probability of positive for "This film was filmed in France", but 0.08 for Afghanistan. Nothing in such a sentence says anything good or bad about the film. We will test this claim ourselves in Section 4.
2.3 What landed on your computer
Now let's look in the cache for this repository and see which of the 17 files we actually have.
repo = next(r for r in scan_cache_dir().repos if r.repo_id == SST2)local = sorted(f.file_name for rev in repo.revisions for f in rev.files)print("files downloaded so far:", local)print(f"size on disk: {repo.size_on_disk / 1e6:.1f} MB")path = hf_hub_download(SST2, "config.json", revision=MODEL_REVISIONS.get(SST2))print("one file's path inside the cache:", path[path.index("models--"):])
files downloaded so far: ['README.md', 'config.json', 'model.safetensors', 'tokenizer_config.json', 'vocab.txt'] size on disk: 268.1 MB one file's path inside the cache: models--distilbert--distilbert-base-uncased-finetuned-sst-2-english/snapshots/714eb0fa89d2f80546fda750413ed43d93601a13/config.json
We have 5 of the 17 files: the configuration, the safetensors weights, the two tokeniser files and the card (fetched just now, when we read it). That's 268.1 MB rather than more than a gigabyte for all five weight formats. The path shows how the cache is organised: one folder per repository (models--owner--name), then a snapshots folder with one subfolder per commit (the 714eb0fa… ID we pinned in Lesson 7.1), then the files.

Figure 7.2.2. A repository can contain documentation, configuration, weights and tokenizer artifacts in several formats. Download only the compatible files required by your framework.
Key terms
- Model repository: a Hub folder holding a model's configuration, weights, tokeniser files and documentation, with version history.
- Model card: the repository's
README.md, describing the model, its training, intended uses, limitations and evaluation.- Card metadata: the machine-readable header of a model card, with fields such as
license,language,datasetsandmodel-index.- ONNX: a file format for running models outside the framework that trained them.
Common mistake: quoting a model card's benchmark number as if it applied to your data. A card's numbers describe its evaluation data; this card's are for short SST-2 sentences. Measure on your own data before relying on them.
-
The repository holds five copies of the weights, but loading the model downloaded only one. Why is that a good design, and which file would it have chosen if
model.safetensorswere missing?Downloading all five would waste over a gigabyte of disk and bandwidth on formats your framework can't use; each format exists so that users of different software can get exactly one. Transformers prefers
model.safetensorsbecause it is safe to load. Without it, a PyTorch user would getpytorch_model.bin, the older PyTorch format, which works but, like any pickle file, could in principle run code when loaded, so it should only come from sources you trust.
-
The card reports an accuracy of 0.9886 on
sst2train and 0.9106 ongluevalidation. Which would you put in a report to your manager, and why?The validation figure, 0.9106. The training figure, 0.9886, measures the model on sentences it learned from, so it mostly shows how well it memorised them. Even the validation figure should come with a caveat: it is for short SST-2 sentences, and your data may behave differently. Best of all is a figure measured on your own data.
With the Hub covered, the next section turns to the libraries that use it.
What should a benchmark score on a card tell you?
Performance under its stated evaluation protocol.
The Core Libraries: Transformers, Datasets, Tokenizers
3.1 Transformers: one interface for many architectures
The Transformers library contains code for hundreds of model architectures. The Auto classes (AutoConfig, AutoTokenizer, AutoModel…) save you from knowing which class each repository needs: they read the repository's config.json, look at its model_type, and build the matching class. Downloading only the configuration (a few kilobytes) is enough to see this:
repos = [SST2, "distilbert/distilgpt2", "sentence-transformers/all-MiniLM-L6-v2", "openai/clip-vit-base-patch32"]for r in repos: cfg = AutoConfig.from_pretrained(r, revision=MODEL_REVISIONS.get(r)) # downloads config.json only print(f"{r:<58} model_type: {cfg.model_type:<11} architectures: {cfg.architectures}")
distilbert/distilbert-base-uncased-finetuned-sst-2-english model_type: distilbert architectures: ['DistilBertForSequenceClassification'] distilbert/distilgpt2 model_type: gpt2 architectures: ['GPT2LMHeadModel'] sentence-transformers/all-MiniLM-L6-v2 model_type: bert architectures: ['BertModel'] openai/clip-vit-base-patch32 model_type: clip architectures: ['CLIPModel']
Four repositories, four different architectures: DistilBERT, GPT-2, BERT and CLIP. Yet the same line of code, AutoConfig.from_pretrained(name), handled all of them. The architectures field records which class the weights were saved from: a sequence classifier, a language model with a word-prediction head (GPT2LMHeadModel), a bare BERT body, and a combined image–text model. The rest of Module 7 loads all four. Lesson 7.4 covers the Auto classes for loading models in detail.
3.2 Datasets: data that doesn't fill your memory
The Datasets library does for data what Transformers does for models: it downloads datasets from the Hub by name, and gives them a common interface. We'll load IMDb's official train and test files, as in Lesson 7.1.
imdb = load_dataset("stanfordnlp/imdb", cache_dir="data/imdb", data_files={"train": "plain_text/train-*.parquet", "test": "plain_text/test-*.parquet"}, revision=DATASET_REVISIONS.get("stanfordnlp/imdb"))print(imdb)
Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.
DatasetDict({
train: Dataset({
features: ['text', 'label'],
num_rows: 25000
})
test: Dataset({
features: ['text', 'label'],
num_rows: 25000
})
})
The result is a DatasetDict: a dictionary of splits, each a Dataset with named columns (features) and a number of rows. Each split has 25000 reviews. Let's look inside the training split:
train = imdb["train"]print("features:", train.features)print("labels:", np.bincount(list(train["label"])))row = train[0] # one row is a plain dictionaryprint(row["label"], "|", row["text"][:240])
features: {'text': Value('string'), 'label': ClassLabel(names=['neg', 'pos'])}
labels: [12500 12500]
0 | I rented I AM CURIOUS-YELLOW from my video store because of all the controversy that surrounded it when it was first released in 1967. I also heard that at first it was seized by U.S. customs if it ever tried to enter this country, therefor
The label column is a ClassLabel with names neg and pos, stored as 0 and 1, and the split is exactly balanced: 12500 of each. Indexing a row gives a plain Python dictionary. (This first review shows the official files' quirks: no cleaning, and the IMDb split from Lesson 7.1, with duplicates removed, is built on top of this raw version.)
How does a library handle datasets far larger than memory? It doesn't load them into memory at all:
print("table type:", type(train.data).__name__)for f in train.cache_files: print("backed by:", os.path.basename(f["filename"]), f"| {os.path.getsize(f['filename']) / 1e6:.1f} MB on disk")
table type: MemoryMappedTable backed by: imdb-train.arrow | 33.4 MB on disk
The table is a MemoryMappedTable, backed by a 33.4 MB file on disk in the Apache Arrow format, a column-based layout designed for fast reading. Memory mapping means the operating system reads parts of the file only when they are needed, so a dataset bigger than your RAM still opens instantly. This is also why the library writes cache files: every operation produces a new Arrow table on disk.
Datasets works best through a few operations that each produce a new dataset. filter keeps the rows that pass a test:
short = train.filter(lambda r: len(r["text"].split()) < 30) # keep reviews under 30 wordsprint(len(short), "reviews under 30 words, for example:")for r in short.select(range(3)): print(f" {r['label']} | {r['text']}")
49 reviews under 30 words, for example: 0 | Unwatchable. You can't even make it past the first three minutes. And this is coming from a huge Adam Sandler fan!!1 0 | You'd better choose Paul Verhoeven's even if you have watched it. 0 | Long, boring, blasphemous. Never have I been so glad to see ending credits roll.
Only 49 of the 25000 training reviews are under 30 words. You'll see "Filter" progress bars as these operations run.
map applies a function to every row, or with batched=True, to batches of rows at a time, which is much faster. The most common use is tokenising. Here we count how many DistilBERT tokens each review needs, without cutting any off. Expect one warning, "Token indices sequence length is longer than the specified maximum sequence length…": it is the tokeniser pointing out that some reviews are longer than the model can accept. That's true, and exactly what we want to measure.
lengths = train.map(lambda b: {"n_tokens": [len(ids) for ids in tokenizer(b["text"])["input_ids"]]}, batched=True, remove_columns=["text"]) # tokens per review, no truncationn = np.array(lengths["n_tokens"])print(f"tokens per review: median {np.median(n):.0f} | longest {n.max()} | over 256: {(n > 256).mean():.1%} | over 512: {(n > 512).mean():.1%}")
tokens per review: median 233 | longest 3127 | over 256: 44.3% | over 512: 14.8%
The median review is 233 tokens long, the longest is 3127, and 44.3% of reviews are over 256 tokens, with 14.8% over 512, the most DistilBERT can read at once. (These are raw reviews; the <br /> tags still in them add a few tokens each.) The histogram shows the long tail:
plt.figure(figsize=(6.5, 3.6))plt.hist(n, bins=np.arange(0, 1201, 25), color="tab:green")for limit, style in [(256, "--"), (512, "-")]: plt.axvline(limit, color="black", ls=style, label=f"{limit} tokens")plt.xlabel("DistilBERT tokens per review (reviews over 1,200 not shown)"); plt.ylabel("number of reviews")plt.title("How long are IMDb reviews?"); plt.legend(); plt.show()

Most reviews are a few hundred tokens long, but a long tail stretches far past the 512-token limit. Every model in this module has to decide what to do with that tail: cut it off, which is called truncation, or split the review into pieces. Lesson 7.4 covers the choice; the 256-token setting used in Lesson 7.1 cuts off a large share of reviews.
To prepare data for a model, we tokenise with truncation and remove the raw text column. We'll use the first 1,000 reviews to keep this quick:
def tokenize(batch): """Tokenise a text batch under the explicitly declared truncation policy. Arguments: batch: Dictionary or list of aligned batch fields. """ return tokenizer(batch["text"], truncation=True, max_length=256) # cut to at most 256 tokenssmall = train.select(range(1000)).map(tokenize, batched=True, remove_columns=["text"])print(small)
Dataset({
features: ['label', 'input_ids', 'token_type_ids', 'attention_mask'],
num_rows: 1000
})
Each row now holds input_ids (the token IDs), an attention_mask (Lesson 7.4) and a token_type_ids column we'll explain in a moment. Finally, set_format("torch") makes rows come out as PyTorch tensors, and a DataLoader batches them, just as in your own training loops. Because reviews have different lengths, the batches need padding: a data collator is a function that assembles a batch, and DataCollatorWithPadding pads each batch to its longest member.
small.set_format("torch") # rows now come out as tensorsloader = torch.utils.data.DataLoader(small, batch_size=8, shuffle=True, collate_fn=DataCollatorWithPadding(tokenizer), # pads each batch generator=torch.Generator().manual_seed(0))batch = next(iter(loader))print({k: tuple(v.shape) for k, v in batch.items()})
{'input_ids': (8, 256), 'token_type_ids': (8, 256), 'attention_mask': (8, 256), 'labels': (8,)}
A batch of 8 reviews, each padded to 256 tokens (the length of the longest review in the batch, which hit the truncation limit), plus 8 labels. The collator also renamed label to labels, the name Transformers models expect when they compute a loss. This dataset is ready for training, which is exactly where Module 8 picks it up.

Figure 7.2.3. Inspect examples, transform them under a declared policy, then dynamically pad and mask each batch. Learned preprocessing must be fitted on training data only.
Key terms
- Dataset / DatasetDict: a table of examples with named columns / a dictionary of such tables, one per split.
- Features: the named, typed columns of a dataset, such as
text(a string) andlabel(a class label).- Apache Arrow: a column-based file format for fast reading of large tables.
- Memory mapping: reading a file on disk as if it were in memory, fetching only the parts that are used.
mapandfilter: Datasets operations that transform every row, or keep only some rows, producing a new dataset.- Data collator: a function that combines individual examples into a batch, for example by padding them to equal length.
-
A colleague tries to load a 200 GB text dataset on a laptop with 16 GB of RAM using Datasets, and expects it to crash. Why might it not?
Datasets stores data as memory-mapped Arrow files. The operating system reads only the parts being used into memory, so opening and iterating over a dataset much larger than RAM works, a batch at a time. It would still need 200 GB of disk space, and operations like
mapwrite new files to disk.
3.3 Tokenizers: fast, and specific to each model
Which tokeniser did AutoTokenizer give us? The answer is a small surprise.
print("tokenizer class:", type(tokenizer).__name__, "| it returns:", tokenizer.model_input_names)with torch.no_grad(): with_tt = model(**{k: v.to(device) for k, v in batch.items() if k != "labels"}).logits without = model(input_ids=batch["input_ids"].to(device), attention_mask=batch["attention_mask"].to(device)).logitsprint("same outputs with and without token_type_ids:", torch.equal(with_tt, without))
tokenizer class: BertTokenizer | it returns: ['input_ids', 'token_type_ids', 'attention_mask'] same outputs with and without token_type_ids: True
We asked for DistilBERT's tokeniser and got a BertTokenizer. Recall from Section 2.1 that this repository's tokenizer_config.json is tiny: it doesn't name a tokeniser class, so the library fell back to the general BERT tokeniser, which works because DistilBERT uses BERT's vocabulary. The BERT tokeniser also produces token_type_ids, which BERT uses to tell two sentences in one input apart. DistilBERT doesn't use them, and the check above shows its outputs are identical with or without them. Repositories are not all equally tidy, and knowing what each file does helps you spot why a tokeniser behaves unexpectedly.
The Tokenizers library does the actual splitting, in fast compiled code written in Rust. Its most important lesson is one you met in Lesson 6.2: a model and its tokeniser are a pair. Let's tokenise the same sentence with the tokenisers of two different models.
gpt2_tok = AutoTokenizer.from_pretrained("distilbert/distilgpt2", revision=MODEL_REVISIONS.get("distilbert/distilgpt2"))sample = "Tokenisers don't agree: unbelievably GOOD!"for name, t in [("DistilBERT", tokenizer), ("distilgpt2", gpt2_tok)]: ids = t(sample)["input_ids"] print(f"{name:<11} vocabulary {t.vocab_size:,} | {len(ids)} IDs: {ids}") print(f"{'':<11} tokens: {t.convert_ids_to_tokens(ids)}")print("ID 2204 is", repr(tokenizer.decode([2204])), "to DistilBERT and", repr(gpt2_tok.decode([2204])), "to distilgpt2")
DistilBERT vocabulary 30,522 | 17 IDs: [101, 19204, 17288, 2015, 2123, 1005, 1056, 5993, 1024, 4895, 8671, 2666, 3567, 6321, 2204, 999, 102]
tokens: ['[CLS]', 'token', '##iser', '##s', 'don', "'", 't', 'agree', ':', 'un', '##bel', '##ie', '##va', '##bly', 'good', '!', '[SEP]']
distilgpt2 vocabulary 50,257 | 9 IDs: [30642, 21572, 836, 470, 4236, 25, 48943, 21090, 0]
tokens: ['Token', 'isers', 'Ġdon', "'t", 'Ġagree', ':', 'Ġunbelievably', 'ĠGOOD', '!']
ID 2204 is 'good' to DistilBERT and ' benef' to distilgpt2
The same 42-character sentence becomes 17 IDs for DistilBERT and 9 for distilgpt2, and the IDs share nothing. DistilBERT's vocabulary has 30,522 entries and distilgpt2's 50,257. The splits differ too:
- DistilBERT lowercases everything (
GOODbecamegood), marks the start and end with[CLS]and[SEP], and marks word-continuation pieces with##: "unbelievably" became five pieces,un ##bel ##ie ##va ##bly. - distilgpt2 keeps capitals, adds no special tokens, and marks a piece that starts with a space with
Ġ. "unbelievably" is a single token in its larger vocabulary.
The last line makes the point directly: ID 2204 is good to DistilBERT but a completely different piece of text to distilgpt2. Feeding one model's IDs to the other may not raise any error. It silently produces nonsense.
The difference in algorithm shows when we look inside:
for name, t in [("DistilBERT", tokenizer), ("distilgpt2", gpt2_tok)]: backend = t.backend_tokenizer # the Rust object from the Tokenizers library print(f"{name:<11} fast: {t.is_fast} | algorithm: {type(backend.model).__name__}")print("DistilBERT normaliser:", repr(tokenizer.backend_tokenizer.normalizer.normalize_str("Café FÊTE")))
DistilBERT fast: True | algorithm: WordPiece distilgpt2 fast: True | algorithm: BPE DistilBERT normaliser: 'cafe fete'
Both are "fast" tokenisers, backed by the Tokenizers library. DistilBERT uses WordPiece and distilgpt2 uses BPE (byte-pair encoding, which you built a version of in Lesson 6.2). Before splitting, DistilBERT's tokeniser runs a normaliser that lowercases and strips accents: "Café FÊTE" becomes 'cafe fete'. This is what "uncased" means in the model's name.
One more feature of fast tokenisers is invaluable when you build applications: offset mapping, which records which characters of the original text each token came from.
enc = tokenizer(sample, return_offsets_mapping=True)for tok, (a, b) in zip(enc.tokens(), enc["offset_mapping"]): print(f"{tok:<10} characters {a:>2}-{b:<2} {sample[a:b]!r}")
[CLS] characters 0-0 '' token characters 0-5 'Token' ##iser characters 5-9 'iser' ##s characters 9-10 's' don characters 11-14 'don' ' characters 14-15 "'" t characters 15-16 't' agree characters 17-22 'agree' : characters 22-23 ':' un characters 24-26 'un' ##bel characters 26-29 'bel' ##ie characters 29-31 'ie' ##va characters 31-33 'va' ##bly characters 33-36 'bly' good characters 37-41 'GOOD' ! characters 41-42 '!' [SEP] characters 0-0 ''
Each token knows its source characters, even after lowercasing: the token good maps back to characters 37–41, which read 'GOOD' in the original. Special tokens like [CLS] map to nothing (0–0). Offsets let you highlight words in the original text, for example to show a user which part of a review a model reacted to.
Key terms
- Auto classes: Transformers classes (
AutoConfig,AutoTokenizer,AutoModel…) that read a repository's configuration and build the matching class.- Fast tokeniser: a tokeniser backed by the compiled Tokenizers library.
- WordPiece and BPE: two subword algorithms; WordPiece marks continuation pieces with
##, GPT-2's BPE marks pieces that begin with a space withĠ.- Normaliser: the step before splitting that cleans text, for example by lowercasing and removing accents.
- Offset mapping: for each token, the start and end positions of the characters it came from.
Common mistake: loading a tokeniser from one repository and a model from another. The shapes may match, so nothing crashes, but the IDs mean different things to each, and the predictions are meaningless. Load both from the same repository name.
-
You tokenise a review with distilgpt2's tokeniser and pass the IDs to the SST-2 DistilBERT model. Will you get an error? If not, what will the predictions be worth?
It depends on the IDs. DistilBERT's embedding table has 30,522 rows, so any ID of 30,522 or more causes an index error. Our sample sentence's distilgpt2 IDs include 30642 and 48943, so it would crash, which is at least a warning. If every ID happens to be smaller, there is no error at all: the model looks the IDs up, but each one means a different token to DistilBERT than it did to distilgpt2. The model sees a meaningless jumble and its predictions are worthless, with nothing to tell you so.
Knowing how to load a model is half the job. The other half is knowing whether you're allowed to, and whether you should.
Why keep a model with its matching tokenizer?
Token IDs and preprocessing must match the learned interface.
Licences, Usage Restrictions and Responsible Selection
4.1 Licences
Every model is someone's work, released under some terms. A licence states what you may do with it: use it commercially, change it, redistribute it, and under what conditions. On the Hub, the licence is usually declared in the card's metadata, so we can check it in code.
candidates = [SST2, "distilbert/distilbert-base-uncased", "sentence-transformers/all-MiniLM-L6-v2", "distilbert/distilgpt2", "openai/clip-vit-base-patch32", "meta-llama/Llama-3.2-1B", "google/gemma-2-2b"]for r in candidates: i = api.model_info(r, revision=MODEL_REVISIONS.get(r)) # metadata only licence = i.card_data.get("license") if i.card_data else None print(f"{r:<42} licence: {str(licence):<11} gated: {i.gated}")
distilbert/distilbert-base-uncased-finetuned-sst-2-english licence: apache-2.0 gated: False distilbert/distilbert-base-uncased licence: apache-2.0 gated: False sentence-transformers/all-MiniLM-L6-v2 licence: apache-2.0 gated: False distilbert/distilgpt2 licence: apache-2.0 gated: False openai/clip-vit-base-patch32 licence: None gated: False meta-llama/Llama-3.2-1B licence: llama3.2 gated: manual google/gemma-2-2b licence: gemma gated: manual
Three different situations appear in this table.
- A standard open licence. Four models use
apache-2.0, a widely used permissive licence: you may use, change and redistribute the model, including commercially, as long as you keep the licence and copyright notices. - A custom licence with gated access.
meta-llama/Llama-3.2-1Bandgoogle/gemma-2-2bdeclare their own licences,llama3.2andgemma. These are custom terms written by the model's publisher, with their own conditions, for example on how the model may be used. Both are gated (manual): you must sign in, read and accept the terms on the Hub page, and wait for approval before downloading. - No declared licence.
openai/clip-vit-base-patch32showsNone.
Let's see what access looks like for a gated model without an account:
try: hf_hub_download("meta-llama/Llama-3.2-1B", "config.json", revision=MODEL_REVISIONS.get("meta-llama/Llama-3.2-1B")) # a gated repository, without a tokenexcept Exception as e: print("download refused:", type(e).__name__, "| HTTP 401 (not authorised):", "401" in str(e))
download refused: GatedRepoError | HTTP 401 (not authorised): True
The Hub refuses: a GatedRepoError with HTTP status 401, "not authorised". The metadata was public (we read the licence above), but the files are not until you accept the terms. To use such a model, you would log in with an access token (hf auth login) after accepting its terms on the website. Nothing in Modules 7 and 8 needs one.
What about CLIP's missing licence?
clip_card = ModelCard.load("openai/clip-vit-base-patch32")print("metadata:", clip_card.data.to_dict())print("lines mentioning a licence:", [l for l in clip_card.text.splitlines() if "licen" in l.lower()])print("section headings:", [l.strip("# ") for l in clip_card.text.splitlines() if l.startswith("#")])
metadata: {'tags': ['vision'], 'widget': [{'src': 'https://huggingface.co/datasets/mishig/sample_images/resolve/main/cat-dog-music.png', 'candidate_labels': 'playing music, playing sports', 'example_title': 'Cat & Dog'}]}
lines mentioning a licence: []
section headings: ['Model Card: CLIP', 'Model Details', 'Model Date', 'Model Type', 'Documents', 'Use with Transformers', 'Model Use', 'Intended Use', 'Primary intended uses', 'Out-of-Scope Use Cases', 'Data', 'Data Mission Statement', 'Performance and Limitations', 'Performance', 'Limitations', 'Bias and Fairness', 'Feedback', 'Where to send questions or comments about the model']
The card's metadata contains only a tag and a demo-widget setting, and no line of the card mentions a licence at all. That doesn't mean the model is free to use. In most countries, work published without a licence gives you no automatic right to reuse it. The card does say a lot about intended use: its headings include "Primary intended uses" and "Out-of-Scope Use Cases". What to do in practice is to look for the licence where the model was originally published (the authors' own code repository), use the model only for the purposes its creators describe, and get advice before building a product on it. Lesson 7.7 makes this part of a model-selection checklist. In these lessons we use CLIP for learning, which is within its stated research purpose.
4.2 Responsible selection: test the card's warnings
A licence tells you what you may do; the card's limitations tell you what you should worry about. Section 2.2's card warned that sentences about different countries get very different sentiment. Let's test that claim with six countries, chosen before running the cell.
The missing Hub metadata field is not proof that the original project has no licence. The original CLIP repository includes an MIT licence; verify the terms and provenance applicable to the exact weights and distribution you intend to use. Licence terms and intended-use recommendations are different questions.
countries = ["France", "Afghanistan", "United States", "Nigeria", "India", "Brazil"] # chosen before runningsentences = [f"This film was filmed in {c}." for c in countries]enc = tokenizer(sentences, padding=True, return_tensors="pt").to(device)with torch.no_grad(): p_pos = model(**enc).logits.softmax(dim=-1)[:, 1] # (6,) probability of POSITIVEfor c, p in zip(countries, p_pos): print(f"P(positive) = {p.item():.3f} This film was filmed in {c}.")
P(positive) = 0.965 This film was filmed in France. P(positive) = 0.165 This film was filmed in Afghanistan. P(positive) = 0.963 This film was filmed in United States. P(positive) = 0.736 This film was filmed in Nigeria. P(positive) = 0.986 This film was filmed in India. P(positive) = 0.969 This film was filmed in Brazil.
The warning holds on today's model. "This film was filmed in Afghanistan." gets a positive probability of 0.165, against 0.965 for France and 0.963 for the United States. Nigeria gets 0.736. The exact values differ from the card's (0.89 and 0.08), which is a good reason to measure rather than copy, but the pattern is the same: the model's judgement of a neutral sentence depends on the country named in it. If this model screened reviews written about films from around the world, some would be judged more harshly purely because of where they were made.
Responsible selection means doing this kind of check as a matter of routine:
- read the licence and any usage restrictions;
- read the card's intended uses, out-of-scope uses and limitations;
- test the warnings that matter for your application, on inputs like your users' inputs;
- record what you found, and decide whether the model is acceptable for the use you have in mind.
Key terms
- Licence: the legal terms under which a model may be used, changed and shared.
- Permissive licence: a licence such as Apache 2.0 that allows broad use, including commercial use, with few conditions.
- Custom licence: terms written by a model's publisher for that model, often with specific use restrictions.
- Gated model: a model whose files can be downloaded only after you sign in and accept its terms.
- Intended use / out-of-scope use: what a model's creators designed it for, and uses they advise against.
Common mistake: treating "downloadable" as "allowed". A model can be public on the Hub and still carry a licence that forbids your use, or carry no licence at all. Check the terms before you build on a model, not after.
-
You find a model on the Hub that scores well on your data, but its card has no licence. Your product is commercial. What do you do?
Don't build on it yet. Look for a licence where the model was originally published, such as the authors' paper or code repository, or contact the authors. If none can be found, treat the model as not licensed for your use, get legal advice, or choose a model with a clear licence. Lesson 7.7 includes this step in its selection checklist.
-
You plan to use the SST-2 model to flag negative product reviews from customers in many countries. Based on Section 4.2, describe one test you would run before deploying it.
For example: write a set of neutral and mildly positive review sentences that differ only in the country, city or nationality mentioned (or in names typical of different regions), run them through the model, and compare the positive probabilities. Large differences for otherwise identical sentences, like the 0.965 and 0.165 measured here, would mean customers from some places get systematically harsher judgements. You'd then decide whether to mitigate it, monitor it, or choose a different model.
What does missing licence metadata establish?
The inspected field supplies no licence evidence; check publisher terms.
Putting It All Together
Let's reassemble the ecosystem.
When you write AutoModelForSequenceClassification.from_pretrained("owner/name"), you are working with every layer at once. The Hub stores the model's repository: 17 files for the model we studied, including its configuration, the same weights in five formats, its tokeniser files and its model card. The huggingface_hub library downloads only what your framework needs, 5 files and 268.1 MB here, into the local cache, where they are kept by repository and commit. Transformers reads the configuration and its Auto classes build the right architecture, whether that is DistilBERT, GPT-2, BERT or CLIP. Tokenizers turns text into token IDs, and every model needs its own tokeniser: the same sentence was 17 IDs for DistilBERT and 9 for distilgpt2. Datasets loads data in memory-mapped Arrow tables and turns it into model-ready batches through filter, map, set_format and a data collator. Along the way we measured that 14.8% of IMDb reviews are longer than DistilBERT's 512-token limit.
Around the code sit two questions that no library answers for you. Is the model good enough for my data? The card's 0.9106 accuracy is on SST-2 sentences, not your data. Am I allowed to use it, and should I? Licences ranged from permissive (apache-2.0) to custom and gated (llama3.2, gemma) to missing (CLIP). The card's bias warning held when we tested it: 0.965 for France against 0.165 for Afghanistan, for the same neutral sentence.

Figure 7.2.4. The repository name resolves to artifacts; the tokenizer prepares IDs and masks, and compatible model code loads the architecture and weights before inference.
Summary
- Hugging Face provides the Hub, where models and datasets are published in repositories, and the libraries that use them: Transformers (models), Datasets (data) and Tokenizers (text splitting), with
huggingface_hubfor downloads. - Downloads go to a local cache (by default
~/.cache/huggingface/hub), organised by repository and commit, so each file is downloaded once. - A model repository can hold much more than you need: the SST-2 repository has 17 files, including the same weights in five formats, but Transformers downloaded 5 files, 268.1 MB.
- A model card has machine-readable metadata (licence, language, datasets, evaluation results) and human-readable sections on uses and limitations. Its numbers describe its own evaluation data, here 0.9106 accuracy on SST-2 validation sentences, not yours.
- Auto classes read a repository's configuration and build the right architecture, so one line of code works for DistilBERT, GPT-2, BERT and CLIP.
- Datasets stores data in memory-mapped Arrow tables, and prepares it with
filter,map,set_formatand a data collator. 14.8% of IMDb training reviews exceed 512 DistilBERT tokens. - A model and its tokeniser are a pair: the same sentence gave 17 IDs with DistilBERT's WordPiece tokeniser and 9 completely different IDs with distilgpt2's BPE tokeniser.
- Licences vary: permissive (
apache-2.0), custom and gated (llama3.2,gemma), or missing (CLIP). Downloadable doesn't mean allowed. - Test a card's warnings yourself: the SST-2 model's country bias held, with 0.965 for France and 0.165 for Afghanistan.
Glossary
| Term | Meaning |
|---|---|
| Hugging Face Hub | A website hosting shared models, datasets and demos in repositories. |
| Repository name | The owner/name identifier of a Hub repository. |
| Model repository | A Hub folder with a model's configuration, weights, tokeniser files and card, plus history. |
| Local cache | The folder where downloaded Hub files are stored for reuse. |
| Transformers | Hugging Face's library of model architectures and loading code. |
| Datasets | Hugging Face's library for loading and processing datasets. |
| Tokenizers | Hugging Face's fast, compiled library for splitting text into tokens. |
| huggingface_hub | The helper library that talks to the Hub: listing, downloading, caching. |
| Model card | A repository's README.md describing a model, its uses and limitations. |
| Card metadata | Machine-readable fields at the top of a model card, such as license and model-index. |
| ONNX | A file format for running models outside the framework that trained them. |
| Auto classes | Classes that read a configuration and build the matching architecture. |
| Dataset / DatasetDict | A table of examples / a dictionary of tables, one per split. |
| Features | The named, typed columns of a dataset. |
| Apache Arrow | A column-based file format for fast reading of large tables. |
| Memory mapping | Reading a file on disk as if it were in memory, fetching parts only when used. |
| map / filter | Datasets operations that transform every row / keep rows that pass a test. |
| Padding | Adding filler tokens so sequences in a batch have equal length. |
| Data collator | A function that combines examples into a batch, for example by padding them. |
| Fast tokeniser | A tokeniser backed by the compiled Tokenizers library. |
| WordPiece | A subword algorithm that marks continuation pieces with ##; used by BERT and DistilBERT. |
| BPE (byte-pair encoding) | A subword algorithm that merges frequent character pairs; used by GPT-2. |
| Normaliser | The tokeniser step that cleans text before splitting, such as lowercasing. |
| Offset mapping | The character positions in the original text that each token came from. |
| Licence | The legal terms for using, changing and sharing a model. |
| Permissive licence | A licence allowing broad use, including commercial, with few conditions. |
| Custom licence | Terms written by a model's publisher specifically for that model. |
| Gated model | A model whose files require signing in and accepting terms before download. |
| Intended use | What a model's creators designed it to be used for. |
Wrap-up
Quick check
What connects a Hub name to a working model?
Repository files, a local cache, and compatible libraries.
Why keep a model with its matching tokenizer?
Token IDs and preprocessing must match the learned interface.
What does missing licence metadata establish?
The inspected field supplies no licence evidence; check publisher terms.
Questions and discussion
Ask about this lesson, share what you found in a lab, or help someone else. Posts are public on the course's GitHub Discussions: anyone can read them, and you need a GitHub account to post.
Opening it loads giscus from giscus.app.