Lab 2: Data Labelling and Data Augmentation¶
A label is the answer you want a model to learn. Nobody gives it to you for free. Somebody has to decide it, write it down, and check that other people agree.
This lab has two halves:
- Part A (Sections 1–8): create labels, and learn to trust them.
- Part B (Sections 9–13): turn a small labelled set into a big one.
Goal: label each movie by its tone - Dark, Uplifting, Tense,
Comedic, Romantic - using its plot summary and its poster.
Why tone and not genre? Genre already comes from the API. Tone is a matter of
opinion. Opinions are where labelling gets hard, and hard is what we want to
study. A horror comedy is Tense to one person and Comedic to another, and
that disagreement is the most useful thing in this lab.
Before you start: in Kaggle, open the right-hand panel and switch Internet to On, and set Accelerator to GPU. Section 13 trains a small CNN and will be slow without it.
Setup¶
# This Python 3 environment comes with many helpful analytics libraries installed
# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python
import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)
import os
for dirname, _, filenames in os.walk('/kaggle/input'):
for filename in filenames:
print(os.path.join(dirname, filename))
!pip install -q label-studio scikit-learn statsmodels albumentations nlpaug audiomentations librosa soundfile
!pip install -q label-studio scikit-learn statsmodels albumentations nlpaug audiomentations librosa soundfile
import json, time, random, requests
from pathlib import Path
DATA_DIR = Path("data"); DATA_DIR.mkdir(exist_ok=True)
POSTER_DIR = DATA_DIR / "posters"; POSTER_DIR.mkdir(exist_ok=True)
TONES = ["Dark", "Uplifting", "Tense", "Comedic", "Romantic"]
random.seed(42)
np.random.seed(42)
print("Tones:", TONES)
Getting the movies (same source as Lab 1)¶
Paste the OMDb key you used in Lab 1. Get a free one at
https://www.omdbapi.com/apikey.aspx if you need another.
OMDB_KEY = "PASTE_YOUR_KEY_HERE"
OMDB_URL = "https://www.omdbapi.com/"
# SOLVED - read this, you will use `movies` in every section
TITLES = [
"Inception","The Matrix","Titanic","Parasite","Toy Story","Joker","La La Land",
"Get Out","Interstellar","Frozen","The Godfather","Alien","Amelie","Se7en",
"Up","The Shining","Notting Hill","Mad Max Fury Road","Her","Whiplash",
"The Notebook","Hereditary","Paddington","Gravity","Moonlight","Coco",
"The Big Lebowski","No Country for Old Men","Pride and Prejudice","Arrival",
"Superbad","The Conjuring","Before Sunrise","Blade Runner","Groundhog Day",
"A Quiet Place","Call Me by Your Name","Dunkirk","Shrek","The Revenant",
"Little Miss Sunshine","It Follows","Casablanca","Gladiator","Ratatouille",
"Black Swan","Love Actually","Sicario","The Grand Budapest Hotel","Room",
"Zombieland","Eternal Sunshine of the Spotless Mind","Prisoners","Booksmart",
"The Witch","500 Days of Summer","Nightcrawler","Finding Nemo","Oldboy",
"Bridesmaids","Midsommar","Brooklyn","Dune","Inside Out","The Departed",
"Silver Linings Playbook","Train to Busan","Knives Out","Carol","Uncut Gems",
"Spirited Away","The Truman Show","Jaws","Juno","Requiem for a Dream",
"About Time","Green Book","The Babadook","Wall-E","Fight Club","Roma",
"Crazy Rich Asians","Us","Lion","Baby Driver","The Lighthouse","Soul",
"Manchester by the Sea","Kill Bill","Legally Blonde","Nomadland","Gone Girl",
"Toy Story 3","The Farewell","Split","Chef","Marriage Story","Hot Fuzz",
"The Sixth Sense","Boyhood","Lady Bird","Doctor Strange","Rocky",
]
def fetch_movie(title):
r = requests.get(OMDB_URL, params={"t": title, "apikey": OMDB_KEY}, timeout=10)
return r.json()
rows = []
for t in TITLES:
d = fetch_movie(t)
if d.get("Response") == "False":
continue
rows.append({
"imdb_id": d.get("imdbID"),
"title": d.get("Title"),
"year": d.get("Year"),
"genre": d.get("Genre", ""),
"plot": d.get("Plot", ""),
"poster": d.get("Poster", ""),
})
time.sleep(0.2) # be polite to the server
movies = pd.DataFrame(rows)
movies = movies[movies["poster"] != "N/A"].reset_index(drop=True)
movies.to_csv(DATA_DIR / "movies.csv", index=False)
print(movies.shape)
movies.head()
Downloading the posters¶
# SOLVED
from PIL import Image
from io import BytesIO
def download_poster(row):
path = POSTER_DIR / f"{row['imdb_id']}.jpg"
if path.exists():
return path
try:
r = requests.get(row["poster"], timeout=10)
Image.open(BytesIO(r.content)).convert("RGB").save(path)
return path
except Exception:
return None
movies["poster_path"] = movies.apply(download_poster, axis=1)
movies = movies[movies["poster_path"].notna()].reset_index(drop=True)
print("Posters downloaded:", len(movies))
PART A - LABELLING DATA¶
Section 1: Types of Label¶
Different tasks need different label shapes. The same movie can carry many kinds of label.
| Task | What the label looks like |
|---|---|
| Classification | one class name ("Dark") |
| Regression | one number (7.8) |
| Object Detection | boxes: [x, y, width, height, class] |
| Semantic Segmentation | one class per pixel (a mask) |
| NER | text spans: (start, end, entity_type) |
Question 1.1 (Solved): One movie, three label types¶
# SOLVED EXAMPLE
example = {
"classification": "Dark", # tone of the film
"regression": 7.8, # predicted rating
"detection": [{"box": [30, 40, 120, 200], "class": "face"},
{"box": [10, 260, 300, 40], "class": "title_text"}],
}
for k, v in example.items():
print(k, "->", v)
Question 1.2: Write a segmentation label and an NER label¶
- For semantic segmentation of a poster, build a NumPy array of shape
(4, 4)where0= background,1= person,2= title text. Any sensible pattern is fine. - For NER, take a real plot string from
movies["plot"]and write the entity spans as(start, end, type)tuples, usingPERSON,LOCATION, andDATE. Check every span by slicing the string and printing the result.
# YOUR CODE HERE
Question 1.3: Cost of labelling¶
Rank the five label types from cheapest to most expensive to produce by hand. Then write one sentence explaining why the most expensive one is so slow.
# YOUR ANSWER HERE
answer_1_3 = "..."
Question 1.4: Name three challenges¶
Name three difficulties you expect when labelling 1,000 movies by tone. Think about cost, tiredness, differing opinions, and rare classes.
# YOUR ANSWER HERE
challenges = ["...", "...", "..."]
Section 2: Manual vs Automatic Labelling¶
There is a range from "human does everything" to "machine does everything".
| Method | Who labels | Speed | Quality |
|---|---|---|---|
| Manual | human only | slow | high |
| Rule-based | code rules | fast | depends on rules |
| Model-assisted | model suggests, human fixes | medium | high |
| Semi-automatic | model does easy ones, human does hard ones | medium | good |
| Pseudo-labelling | model labels unlabelled data, used as truth | very fast | risky |
Question 2.1 (Solved): A rule-based labeller¶
# SOLVED EXAMPLE
def rule_from_genre(genre):
"""Guess tone from the genre string. Return None if unsure."""
g = str(genre).lower()
if "horror" in g or "thriller" in g:
return "Tense"
if "romance" in g:
return "Romantic"
if "comedy" in g:
return "Comedic"
if "crime" in g or "war" in g:
return "Dark"
return None # rule refuses to guess
movies["rule_tone"] = movies["genre"].apply(rule_from_genre)
print(movies["rule_tone"].value_counts(dropna=False))
Question 2.2: Add a poster-brightness rule and measure coverage¶
Write rule_v2(genre, poster_path):
- First try
rule_from_genre. - If that returns
None, fall back to the poster's average brightness: below 70 meansDark, above 160 meansUplifting, otherwise stillNone.
Run it on all movies. Report coverage - the percentage that received any label at all.
Tip: Image.open(path).convert("L") gives a greyscale image; take .mean() of
the array.
# YOUR CODE HERE
def rule_v2(genre, poster_path):
pass
Question 2.3 (Solved): Pseudo-labelling, and its danger¶
# SOLVED EXAMPLE
# A model labels unlabelled data, and we treat those labels as if they were real.
# We keep only the predictions the model is very confident about.
def pseudo_label(model, X_unlabelled, threshold=0.95):
proba = model.predict_proba(X_unlabelled)
confident = proba.max(axis=1) >= threshold
labels = model.classes_[proba.argmax(axis=1)]
return confident, labels # train on labels[confident] only
Question 2.4: Why pseudo-labelling can go wrong¶
The model is confident, but confident is not the same as correct. Explain in 2–3 sentences what happens to the model when we train on its own wrong guesses.
This failure has a name - confirmation bias, or error reinforcement.
# YOUR ANSWER HERE
answer_2_4 = "..."
Question 2.5: Pick a method for each situation¶
Choose one method (manual / rule-based / model-assisted / pseudo-labelling) and give a one-line reason.
- You have 50 movies and need perfect labels for a test set.
- You have 100,000 movies and a model that is already 85% accurate.
- You need to label whether a poster file is bigger than 100 KB.
- You have 2,000 movies, one week, and one annotator.
# YOUR ANSWERS HERE
answer_2_5 = {1: "...", 2: "...", 3: "...", 4: "..."}
Section 3: Annotation Tools¶
You cannot label well in a spreadsheet. A real tool gives you a good screen, keeps your progress, and exports clean files.
| Tool | Best for |
|---|---|
| Label Studio | many data types (image, text, audio) - we use this |
| CVAT | video and images, especially boxes and masks |
| LabelImg | simple: bounding boxes on images only |
| Doccano | text only: classification and NER |
Question 3.1 (Solved): Start Label Studio¶
Label Studio runs as a web app. On Kaggle it is easiest to run it on your own laptop; the export file is all you need to bring back here.
bash
label-studio start movie-tone --init
# open http://localhost:8080
Create a project and paste this labelling config under Settings → Labeling Interface → Code. It shows the poster and the plot, so annotators judge both:
xml
<View>
<Image name="poster" value="$image"/>
<Text name="plot" value="$plot"/>
<Choices name="tone" toName="poster" choice="single-radio">
<Choice value="Dark"/>
<Choice value="Uplifting"/>
<Choice value="Tense"/>
<Choice value="Comedic"/>
<Choice value="Romantic"/>
</Choices>
</View>
### Question 3.2: Build the task file
Label Studio reads a JSON list of tasks. Build one from movies, where each
task looks like:
json
{"data": {"image": "<poster url>", "plot": "<plot text>",
"imdb_id": "tt1375666", "title": "Inception"}}
Save it as ls_tasks.json and import it into your project.
# YOUR CODE HERE
Question 3.3: Label 30 movies (work with a partner)¶
Pick 30 movies. You and a lab partner label the same 30, separately and without talking. Export both results (Export → JSON).
No partner? Label the 30 twice, with at least a 30 minute gap, and do not look at your first answers. That measures agreement with yourself, which is also a real and useful number.
Question 3.4 (Solved): Read the export file¶
# SOLVED EXAMPLE
def parse_export(path):
"""Turn a Label Studio JSON export into a DataFrame[imdb_id, tone]."""
raw = json.loads(Path(path).read_text())
rows = []
for task in raw:
result = task["annotations"][0]["result"]
if not result: # task was skipped
continue
rows.append({"imdb_id": task["data"]["imdb_id"],
"tone": result[0]["value"]["choices"][0]})
return pd.DataFrame(rows)
# ann_a = parse_export("export_A.json")
# ann_b = parse_export("export_B.json")
Question 3.5: Join the two annotators¶
Merge the two tables on imdb_id so each row holds both tones. Print how many of
the 30 movies both people actually finished.
# YOUR CODE HERE
# merged = ann_a.merge(ann_b, on="imdb_id", suffixes=("_a", "_b"))
Question 3.6: Choose the right tool¶
Name the best tool from the table above for each job, and why in one line.
- Drawing boxes around every car in 500 traffic photos.
- Marking company names inside 1,000 news articles.
- Labelling posters, plot text, and trailer audio in one project.
# YOUR ANSWERS HERE
answer_3_6 = {1: "...", 2: "...", 3: "..."}
Section 4: Annotation Guidelines¶
A guideline is a written rule book. If two people read it and still label differently, the rule book is at fault - not the people.
Question 4.1 (Solved): A first rule book¶
# SOLVED EXAMPLE
GUIDELINES_V1 = {
"Dark": "Sad, violent, or hopeless story. Poster is mostly black or grey.",
"Uplifting": "The story ends in hope or success. Bright warm poster.",
"Tense": "The viewer is meant to feel fear or suspense. Not funny.",
"Comedic": "The film is meant to make you laugh.",
"Romantic": "A love story is the main plot, not a side plot.",
}
for k, v in GUIDELINES_V1.items():
print(f"{k:10s} : {v}")
Question 4.2: Find hard cases and fix the rules¶
Find three movies in your set where these rules do not give a clear answer. For each, write: the movie, why it is confusing, and a new rule that settles it.
Good places to look: a sad love story, a funny horror film, a hopeful film about a terrible event.
# YOUR ANSWERS HERE
hard_cases = [
# {"movie": "...", "why_confusing": "...", "new_rule": "..."},
]
Question 4.3: Add an "I cannot decide" option¶
Find three movies in your set where these rules do not give a clear answer. For each, write: the movie, why it is confusing, and a new rule that settles it.
Good places to look: a sad love story, a funny horror film, a hopeful film about a terrible event.
# YOUR ANSWERS HERE
hard_cases = [
# {"movie": "...", "why_confusing": "...", "new_rule": "..."},
]
Question 4.3: Add an "I cannot decide" option¶
Right now an annotator must pick one of five tones even when unsure. Add a sixth
option, "Unclear", and write the rule for when to use it.
Then answer in two sentences: why is allowing "Unclear" better than forcing a
guess?
# YOUR ANSWER HERE
answer_4_3 = "..."
Question 4.4: Design a review step¶
Quality control means somebody checks the labels. Write a short plan (4–5 lines) covering: who reviews, what share of labels get reviewed, what happens when the reviewer disagrees, and when you would stop and rewrite the guidelines.
# YOUR ANSWER HERE
review_plan = "..."
Section 5: Inter-Annotator Agreement¶
If two people label the same items, how much do they agree? Plain "percent agreement" is not enough, because two people guessing at random still agree sometimes. Cohen's Kappa removes that lucky agreement.
Kappa = 1 means perfect agreement. Kappa = 0 means no better than guessing.
Question 5.1 (Solved): Percent agreement vs Cohen's Kappa¶
# SOLVED EXAMPLE
from sklearn.metrics import cohen_kappa_score, confusion_matrix
a = ["Dark","Dark","Tense","Comedic","Romantic","Dark","Tense","Uplifting"]
b = ["Dark","Tense","Tense","Comedic","Romantic","Dark","Dark","Uplifting"]
percent = np.mean([x == y for x, y in zip(a, b)])
kappa = cohen_kappa_score(a, b)
print(f"Percent agreement: {percent:.2f}")
print(f"Cohen's Kappa: {kappa:.2f}")
Question 5.2 (Solved): How to read a Kappa score¶
# SOLVED EXAMPLE
def read_kappa(k):
if k < 0.00: return "Poor (worse than guessing)"
if k <= 0.20: return "Slight"
if k <= 0.40: return "Fair"
if k <= 0.60: return "Moderate"
if k <= 0.80: return "Substantial"
return "Almost perfect"
print(read_kappa(kappa))
Question 5.3: Compute Kappa on your own labels¶
Using merged from Question 3.5:
- Print the percent agreement and Cohen's Kappa.
- Print the confusion matrix with
labels=TONES. - Say which pair of tones was mixed up most often.
- List the movies where you and your partner disagreed.
# YOUR CODE HERE
Question 5.4: Fix the guidelines and measure again¶
Use the disagreements from 5.3 to write GUIDELINES_V2. Re-label only the
disagreed movies with the new rules, then compute Kappa again.
Did it improve? This loop - measure, fix the rules, measure again - is the main skill of this section.
# YOUR CODE HERE
Question 5.5: Name the sources of disagreement¶
Sort each of your disagreements into one bucket:
- the guidelines were unclear,
- the annotator slipped (tired, misclick),
- the movie is truly ambiguous (both answers are fair).
Which bucket is biggest? Which one can never be fixed by better rules?
# YOUR ANSWER HERE
answer_5_5 = "..."
Question 5.6: Fleiss' Kappa for three or more people¶
Cohen's Kappa handles exactly two annotators. For three or more, use Fleiss' Kappa. It needs a counts table: one row per item, one column per class, each cell holding how many annotators chose that class.
Get a third person to label the same 30 movies, then compute it.
# YOUR CODE HERE
from statsmodels.stats.inter_rater import fleiss_kappa, aggregate_raters
# labels_matrix: shape (30, 3), one column per annotator, cells are tone names
# table, _ = aggregate_raters(labels_matrix) # -> counts, shape (30, 5)
# print(fleiss_kappa(table))
Question 5.7: A limit of Cohen's Kappa¶
Build two label lists where 95% of items are "Dark" and the two annotators
agree on nearly everything. Compute percent agreement and Kappa.
You will see high agreement but a surprisingly low Kappa. Explain in two sentences why. This is the imbalanced class problem of Kappa, and it is the reason you never report Kappa without also reporting the class distribution.
# YOUR CODE HERE
Section 6: Sampling - Choosing What to Label¶
You cannot label everything. Which items you pick changes what the model learns.
| Strategy | How it picks | Watch out for |
|---|---|---|
| Random | every item equally likely | rare classes get missed |
| Stratified | a fixed share from each group | you must know the groups |
| Snowball | start from a few, add similar ones | not representative |
| Convenience | whatever is easiest to reach | strong bias |
Question 6.1 (Solved): Random and stratified sampling¶
# SOLVED EXAMPLE
movies["decade"] = pd.to_numeric(movies["year"].str[:4], errors="coerce") // 10 * 10
random_sample = movies.sample(n=30, random_state=0)
strat_sample = movies.groupby("decade", group_keys=False).apply(
lambda g: g.sample(n=min(5, len(g)), random_state=0)
)
print(random_sample["decade"].value_counts().sort_index())
print(strat_sample["decade"].value_counts().sort_index())
Question 6.2: Snowball sampling for a rare tone¶
Suppose Romantic is rare in your labelled set. Snowball sampling starts from a
few known examples (seeds) and pulls in the most similar unlabelled items.
- Build TF-IDF vectors from the
plotcolumn (sklearn.feature_extraction.text.TfidfVectorizer). - Given 3 seed
Romanticmovies, useNearestNeighborsto find the 20 closest unlabelled movies. Return their titles - these are your next candidates.
# YOUR CODE HERE
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.neighbors import NearestNeighbors
Question 6.3: The hidden cost of snowball sampling¶
A snowball sample is not random. Explain in 2–3 sentences why testing your model on a snowball-grown set makes its accuracy look better than it really is, and what you must keep aside to avoid this.
# YOUR ANSWER HERE
answer_6_3 = "..."
Question 6.4: Spot the convenience sample¶
Your friend labels only the first 100 rows of the CSV because they are at the top of the file - and the CSV is sorted by year. Name two ways this hurts the model.
# YOUR ANSWER HERE
answer_6_4 = "..."
# SOLVED EXAMPLE
def least_confidence(proba): # 1 minus the top probability
return 1.0 - proba.max(axis=1)
def margin_sampling(proba): # small gap between top two classes = unsure
top2 = np.sort(proba, axis=1)[:, -2:]
return 1.0 - (top2[:, 1] - top2[:, 0])
def entropy_sampling(proba): # spread-out probabilities = unsure
p = np.clip(proba, 1e-9, 1.0)
return -(p * np.log(p)).sum(axis=1)
# In all three, a HIGHER score means "label this one next".
Question 7.2 (Solved): One round of pool-based active learning¶
# SOLVED EXAMPLE
from sklearn.linear_model import LogisticRegression
def query_next(X_pool, model, strategy=entropy_sampling, k=10):
"""Indices of the k most uncertain items in the pool."""
proba = model.predict_proba(X_pool)
return np.argsort(strategy(proba))[-k:]
# clf = LogisticRegression(max_iter=1000).fit(X_labelled, y_labelled)
# to_label = query_next(X_pool, clf)
Question 7.3: Active learning vs random - draw the curves¶
Use your labelled movies as a stand-in for a human, so you do not have to keep clicking. Features: the TF-IDF plot vectors from Question 6.2.
- Start with 10 labelled movies; the rest form the pool.
- Each round: train
LogisticRegression, then pick 10 more movies (a) by entropy, and (b) at random. Add them, retrain, record test accuracy. - Plot both curves - labels used on the x-axis, accuracy on the y-axis.
The gap between the curves is the labelling effort you saved.
# YOUR CODE HERE
import matplotlib.pyplot as plt
Question 7.4: Report the saving¶
Find how many labels each method needed to first reach 60% accuracy. Report the ratio, for example: "active learning needed 60 labels, random needed 100, so we saved 40%".
# YOUR CODE HERE
Question 7.5: Query-by-Committee (short answer)¶
In Query-by-Committee you train several different models and ask for labels where the models disagree most.
Write 2–3 sentences giving one advantage over plain uncertainty sampling, and one cost.
# YOUR ANSWER HERE
answer_7_5 = "..."
Question 7.6: The diversity problem¶
Plain uncertainty sampling often picks 10 nearly identical movies. Explain why that wastes the budget, and describe in a line or two how you would add diversity to the selection.
# YOUR ANSWER HERE
answer_7_6 = "..."
# SOLVED EXAMPLE
ABSTAIN = None
def lf_genre(row):
"""Vote from the genre string."""
return rule_from_genre(row["genre"])
def lf_plot_keywords(row):
"""Vote from words in the plot summary."""
p = str(row["plot"]).lower()
if any(w in p for w in ["murder", "kill", "death", "war"]):
return "Dark"
if any(w in p for w in ["love", "romance", "wedding"]):
return "Romantic"
return ABSTAIN
Question 8.2: Write a third rule and combine the votes¶
- Write
lf_poster_dark(row): vote"Dark"when the poster's average brightness is below 70, otherwise abstain. - Run all three rules over every movie and build a vote table.
- Combine votes by majority. On a tie, or when all three abstain, output
None.
# YOUR CODE HERE
Question 8.3: Score your weak labels¶
Compare the weak labels against your 30 hand labels from Section 3. Report:
- Coverage: the share of items that received any label.
- Accuracy: of those, the share that matched the human.
# YOUR CODE HERE
Question 8.4: When is weak supervision worth it?¶
Using your actual coverage and accuracy numbers - not a general feeling - argue in 3–4 sentences whether you would train on these weak labels.
# YOUR ANSWER HERE
answer_8_4 = "..."
PART B - DATA AUGMENTATION¶
Section 9: Why Augment?¶
Augmentation makes new training samples from old ones by changing them slightly. A mirrored poster still has the same tone, so you get a free extra sample.
It helps in four ways:
- More data without more labelling.
- Better generalisation - the model copes with new, unseen inputs.
- Less overfitting - the model cannot simply memorise the images.
- It eases class imbalance - augment the rare class more.
Question 9.1 (Solved): Show the imbalance¶
# SOLVED EXAMPLE
counts = pd.Series({"Dark": 120, "Tense": 90, "Comedic": 60,
"Uplifting": 45, "Romantic": 12})
print(counts)
print("\nBiggest : smallest =", round(counts.max() / counts.min(), 1))
Question 9.2: Plan an augmentation budget¶
Using the counts above, decide how many extra samples to create per class so every class lands near 120. Write it as a dictionary.
Then answer: what is the risk of making 108 new Romantic samples from just 12
real ones?
# YOUR CODE HERE
extra_needed = {}
answer_9_2 = "..."
# SOLVED EXAMPLE
from torchvision import transforms
img = Image.open(movies.loc[0, "poster_path"]).convert("RGB")
basic = transforms.Compose([
transforms.Resize((224, 224)),
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomRotation(degrees=10),
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),
])
basic(img).save("aug_example.jpg")
print("Saved one augmented poster")
Question 10.2: Build one transform per technique¶
Write one transform for each, and save the output so you can look at it:
- Rotation (up to 20 degrees)
- Translation (shift sideways and up/down)
- Scaling / zoom
- Random crop
- Brightness and contrast change
- Noise injection (add random values to the pixel array)
- Blur
Show all seven in one matplotlib grid, with the technique name as each title.
# YOUR CODE HERE
Question 10.3 (Solved): The same job with Albumentations¶
# SOLVED EXAMPLE
import albumentations as A
import cv2
pipeline = A.Compose([
A.HorizontalFlip(p=0.5),
A.RandomBrightnessContrast(p=0.5),
A.GaussNoise(p=0.3),
A.CoarseDropout(max_holes=4, max_height=32, max_width=32, p=0.3), # Cutout
])
arr = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
print("Output shape:", pipeline(image=arr)["image"].shape)
Question 10.4: Compare the two libraries¶
Run the same three transforms (flip, brightness, noise) 200 times with torchvision and 200 times with Albumentations. Time both. Which is faster? Add one line on what else would make you choose one over the other.
# YOUR CODE HERE
Question 10.5: MixUp¶
MixUp blends two images and their labels:
new = 0.7 * A + 0.3 * B, and the label becomes 70% class A, 30% class B.
CutMix instead pastes a rectangle of B onto A, and the label share follows
the patch area.
Implement MixUp for two posters and show the result. Then say in one sentence why the label must be mixed too, not just the picture.
# YOUR CODE HERE
Question 10.6: Find the augmentation that breaks the label¶
Some transforms destroy the meaning. For tone labels on posters, name one transform from Question 10.2 that could change the true label, and explain why.
Hint: think about what a very strong brightness increase does to a Dark poster.
# YOUR ANSWER HERE
answer_10_6 = "..."
# SOLVED EXAMPLE
def random_deletion(text, p=0.1):
words = text.split()
kept = [w for w in words if random.random() > p]
return " ".join(kept) if kept else text
def random_swap(text, n=1):
words = text.split()
for _ in range(n):
i, j = random.sample(range(len(words)), 2)
words[i], words[j] = words[j], words[i]
return " ".join(words)
plot = movies.loc[0, "plot"]
print("original :", plot)
print("deletion :", random_deletion(plot))
print("swap :", random_swap(plot))
Question 11.2: Synonym replacement and random insertion¶
Write two more functions:
synonym_replacement(text, n=2)- replacenwords with a synonym. Usenlpaug'sSynonymAug, ornltkWordNet.random_insertion(text, n=1)- insertnsynonyms of existing words at random positions.
Run all four functions on five plots and print before and after.
# YOUR CODE HERE
Question 11.3: Back translation¶
Back translation means English → another language → back to English. The meaning survives, the wording changes.
Use nlpaug's BackTranslationAug to back-translate three plots through German.
Print the original and the result.
Then answer: name one thing back translation does better than synonym replacement.
# YOUR CODE HERE
Question 11.4: Check the meaning survived¶
Text augmentation can silently change meaning. Read 10 augmented plots and mark each "meaning kept" or "meaning broken". Report the percentage that survived, and show the worst example you found.
# YOUR CODE HERE
# SOLVED EXAMPLE
import librosa, soundfile as sf
sr = 16000
t = np.linspace(0, 3, sr * 3)
# a simple three-note tone, standing in for a trailer clip
clip = (0.4*np.sin(2*np.pi*220*t) + 0.3*np.sin(2*np.pi*330*t)
+ 0.2*np.sin(2*np.pi*440*t)).astype(np.float32)
sf.write("clip.wav", clip, sr)
noisy = clip + np.random.normal(0, 0.01, len(clip)).astype(np.float32)
sf.write("clip_noisy.wav", noisy, sr)
print("Length in seconds:", len(clip) / sr)
You may instead record 5 seconds of your own voice and load it with
librosa.load("your.wav", sr=16000).
Question 12.2: Four more audio transforms¶
Write each and save a .wav you can listen to:
- Time shift - roll the wave forward by 0.5 seconds.
- Pitch shift - up 2 semitones (
librosa.effects.pitch_shift). - Speed change - 1.2× faster (
librosa.effects.time_stretch). - Volume change - 50% quieter.
# YOUR CODE HERE
Question 12.3 (Solved): A pipeline with audiomentations¶
# SOLVED EXAMPLE
from audiomentations import Compose, AddGaussianNoise, TimeStretch, PitchShift, Gain
augment = Compose([
AddGaussianNoise(min_amplitude=0.001, max_amplitude=0.01, p=0.5),
TimeStretch(min_rate=0.9, max_rate=1.1, p=0.5),
PitchShift(min_semitones=-2, max_semitones=2, p=0.5),
Gain(min_gain_db=-6, max_gain_db=6, p=0.5),
])
aug_clip = augment(samples=clip, sample_rate=sr)
### Question 12.4: Look at the spectrogram
Draw the **mel spectrogram** of the original and of one augmented clip, side by
side (`librosa.feature.melspectrogram`, `librosa.display.specshow`).
Then explain: **time masking** hides some time steps, **frequency masking** hides
some frequency rows. Point to where each would sit on your picture.
# YOUR CODE HERE
Question 12.5: An unrealistic audio augmentation¶
Give one audio augmentation that would be wrong for a speech recognition task, and say why.
# YOUR ANSWER HERE
answer_12_5 = "..."
# SOLVED EXAMPLE
# OFFLINE: build the new files once and save them.
# + fast at training time, easy to inspect
# - uses disk, the same fixed set every epoch
# ONLINE: augment inside the DataLoader, fresh every epoch.
# + endless variety, no extra disk
# - costs CPU time on every step
from torch.utils.data import DataLoader
# train_ds = PosterDataset(paths, labels, transform=basic) # online
# loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=2)
Question 13.2: Does augmentation actually help? Measure it.¶
This is the most important question in Part B. Fine-tune the same small CNN
(torchvision.models.resnet18(pretrained=True), final layer resized to 5 tones)
three times, and compare validation accuracy:
- no augmentation,
- light augmentation (flip only),
- heavy augmentation (flip + rotate + colour jitter + noise + cutout).
Put the three results in one table. Which won?
Make sure the GPU is switched on. Each run should take only a few minutes.
# YOUR CODE HERE
import torch, torchvision
print("GPU available:", torch.cuda.is_available())
Question 13.3: When augmentation hurts¶
Now build a bad pipeline on purpose - rotate posters by 180 degrees and raise brightness by 90%. Train with it and record the accuracy.
Explain why it hurt, and connect your answer to unrealistic augmentation: the new samples no longer resemble anything in the test set.
# YOUR CODE HERE
Question 13.4: Never augment the test set¶
Explain in two sentences why you augment the training set but not the validation or test set. What would your accuracy number mean if you augmented the test set too?
# YOUR ANSWER HERE
answer_13_4 = "..."
Section 14: Challenge Problems (Optional)¶
Challenge 14.1: Krippendorff's Alpha¶
Fleiss' Kappa needs every annotator to label every item. Krippendorff's Alpha tolerates missing annotations. Compute it on your data, compare with Fleiss', and say when you would prefer Alpha.
Challenge 14.2: Active learning with diversity¶
Fix the problem from Question 7.6. Take the 50 most uncertain movies, cluster them into 10 groups with k-means, and label one per cluster. Show it beats plain uncertainty sampling on the learning curve.
Challenge 14.3: Augmentation search¶
Instead of guessing which augmentations to use, search. Try 8 random pipelines, train a model on each, keep the best. This is a simple AutoAugment.
Challenge 14.4: Test-time augmentation¶
At prediction time, augment each test poster 5 ways, predict on all 5, and average the results. Does accuracy improve? What does it cost?
Summary¶
You can now:
- Tell apart classification, regression, detection, segmentation, and NER labels.
- Choose between manual, rule-based, model-assisted, and pseudo-labelling.
- Run Label Studio: config, import, label, export, and parse.
- Write annotation guidelines and repair them when people disagree.
- Measure agreement with Cohen's and Fleiss' Kappa, read the score, and state its limits.
- Pick what to label using random, stratified, and snowball sampling.
- Save labelling effort with active learning.
- Write labelling functions for weak supervision.
- Augment images, text, and audio with the standard libraries.
- Prove whether your augmentation helped - and recognise when it hurts.
Next Week¶
Lab 3: Reproducibility and Hyperparameter Search. You have labels and a bigger dataset. Next you will train models so that any result can be repeated exactly, and search for the best settings instead of guessing.
Submission¶
- This notebook with all cells run.
- Your Label Studio export files (
export_A.json,export_B.json). - Your augmented sample grid (one image is enough).
- A short report (half a page) with:
- your Kappa before and after fixing the guidelines (Q5.3, Q5.4),
- your active learning saving ratio (Q7.4),
- your augmentation comparison table (Q13.2).