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¶

In [ ]:
# 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))
In [ ]:
!pip install -q label-studio scikit-learn statsmodels albumentations nlpaug audiomentations librosa soundfile
In [ ]:
!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.

In [ ]:
OMDB_KEY = "PASTE_YOUR_KEY_HERE"
OMDB_URL = "https://www.omdbapi.com/"
In [ ]:
# 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¶

In [ ]:
# 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¶

In [ ]:
# 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¶

  1. For semantic segmentation of a poster, build a NumPy array of shape (4, 4) where 0 = background, 1 = person, 2 = title text. Any sensible pattern is fine.
  2. For NER, take a real plot string from movies["plot"] and write the entity spans as (start, end, type) tuples, using PERSON, LOCATION, and DATE. Check every span by slicing the string and printing the result.
In [ ]:
# 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.

In [ ]:
# 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.

In [ ]:
# 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¶

In [ ]:
# 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 means Dark, above 160 means Uplifting, otherwise still None.

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.

In [ ]:
# YOUR CODE HERE
def rule_v2(genre, poster_path):
    pass

Question 2.3 (Solved): Pseudo-labelling, and its danger¶

In [ ]:
# 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.

In [ ]:
# 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.

  1. You have 50 movies and need perfect labels for a test set.
  2. You have 100,000 movies and a model that is already 85% accurate.
  3. You need to label whether a poster file is bigger than 100 KB.
  4. You have 2,000 movies, one week, and one annotator.
In [ ]:
# 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.

In [ ]:
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:

In [ ]:
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>
In [ ]:
### Question 3.2: Build the task file

Label Studio reads a JSON list of tasks. Build one from movies, where each task looks like:

In [ ]:
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.

In [ ]:
# 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¶

In [ ]:
# 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.

In [ ]:
# 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.

  1. Drawing boxes around every car in 500 traffic photos.
  2. Marking company names inside 1,000 news articles.
  3. Labelling posters, plot text, and trailer audio in one project.
In [ ]:
# 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¶

In [ ]:
# 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.

In [ ]:
# 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.

In [ ]:
# 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?

In [ ]:
# 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.

In [ ]:
# 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¶

In [ ]:
# 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¶

In [ ]:
# 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:

  1. Print the percent agreement and Cohen's Kappa.
  2. Print the confusion matrix with labels=TONES.
  3. Say which pair of tones was mixed up most often.
  4. List the movies where you and your partner disagreed.
In [ ]:
# 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.

In [ ]:
# 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?

In [ ]:
# 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.

In [ ]:
# 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.

In [ ]:
# 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¶

In [ ]:
# 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.

  1. Build TF-IDF vectors from the plot column (sklearn.feature_extraction.text.TfidfVectorizer).
  2. Given 3 seed Romantic movies, use NearestNeighbors to find the 20 closest unlabelled movies. Return their titles - these are your next candidates.
In [ ]:
# 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.

In [ ]:
# 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.

In [ ]:
# YOUR ANSWER HERE
answer_6_4 = "..."

Section 7: Active Learning¶

Labelling random items wastes effort on easy ones. Active learning trains a small model on what you have, then asks you to label the items it is least sure about. A human stays in the loop.

Question 7.1 (Solved): Three ways to measure "unsure"¶

In [ ]:
# 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¶

In [ ]:
# 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.

  1. Start with 10 labelled movies; the rest form the pool.
  2. Each round: train LogisticRegression, then pick 10 more movies (a) by entropy, and (b) at random. Add them, retrain, record test accuracy.
  3. 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.

In [ ]:
# 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%".

In [ ]:
# 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.

In [ ]:
# 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.

In [ ]:
# YOUR ANSWER HERE
answer_7_6 = "..."

Section 8: Weak Supervision¶

What if you write code instead of clicking? A labelling function is a small rule that votes for a label, or abstains. Many weak rules combined can produce usable labels with no hand labelling at all.

Question 8.1 (Solved): Two labelling functions¶

In [ ]:
# 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¶

  1. Write lf_poster_dark(row): vote "Dark" when the poster's average brightness is below 70, otherwise abstain.
  2. Run all three rules over every movie and build a vote table.
  3. Combine votes by majority. On a tie, or when all three abstain, output None.
In [ ]:
# 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.
In [ ]:
# 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.

In [ ]:
# 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:

  1. More data without more labelling.
  2. Better generalisation - the model copes with new, unseen inputs.
  3. Less overfitting - the model cannot simply memorise the images.
  4. It eases class imbalance - augment the rare class more.

Question 9.1 (Solved): Show the imbalance¶

In [ ]:
# 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?

In [ ]:
# YOUR CODE HERE
extra_needed = {}
answer_9_2 = "..."

Section 10: Image Augmentation¶

Question 10.1 (Solved): Basic transforms with torchvision¶

In [ ]:
# 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:

  1. Rotation (up to 20 degrees)
  2. Translation (shift sideways and up/down)
  3. Scaling / zoom
  4. Random crop
  5. Brightness and contrast change
  6. Noise injection (add random values to the pixel array)
  7. Blur

Show all seven in one matplotlib grid, with the technique name as each title.

In [ ]:
# YOUR CODE HERE

Question 10.3 (Solved): The same job with Albumentations¶

In [ ]:
# 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.

In [ ]:
# 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.

In [ ]:
# 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.

In [ ]:
# YOUR ANSWER HERE
answer_10_6 = "..."

Section 11: Text Augmentation¶

Now augment the plot column.

Question 11.1 (Solved): Two simple text edits¶

In [ ]:
# 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:

  1. synonym_replacement(text, n=2) - replace n words with a synonym. Use nlpaug's SynonymAug, or nltk WordNet.
  2. random_insertion(text, n=1) - insert n synonyms of existing words at random positions.

Run all four functions on five plots and print before and after.

In [ ]:
# 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.

In [ ]:
# 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.

In [ ]:
# YOUR CODE HERE

Section 12: Audio Augmentation¶

Our movie data has no audio, so we make our own. The techniques are identical whatever the source.

Question 12.1 (Solved): Build a clip and add noise¶

In [ ]:
# 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:

  1. Time shift - roll the wave forward by 0.5 seconds.
  2. Pitch shift - up 2 semitones (librosa.effects.pitch_shift).
  3. Speed change - 1.2× faster (librosa.effects.time_stretch).
  4. Volume change - 50% quieter.
In [ ]:
# YOUR CODE HERE

Question 12.3 (Solved): A pipeline with audiomentations¶

In [ ]:
# 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)
In [ ]:
### 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.
In [ ]:
# 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.

In [ ]:
# YOUR ANSWER HERE
answer_12_5 = "..."

Section 13: Best Practices¶

Question 13.1 (Solved): Online vs offline augmentation¶

In [ ]:
# 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:

  1. no augmentation,
  2. light augmentation (flip only),
  3. 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.

In [ ]:
# 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.

In [ ]:
# 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?

In [ ]:
# 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:

  1. Tell apart classification, regression, detection, segmentation, and NER labels.
  2. Choose between manual, rule-based, model-assisted, and pseudo-labelling.
  3. Run Label Studio: config, import, label, export, and parse.
  4. Write annotation guidelines and repair them when people disagree.
  5. Measure agreement with Cohen's and Fleiss' Kappa, read the score, and state its limits.
  6. Pick what to label using random, stratified, and snowball sampling.
  7. Save labelling effort with active learning.
  8. Write labelling functions for weak supervision.
  9. Augment images, text, and audio with the standard libraries.
  10. 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¶

  1. This notebook with all cells run.
  2. Your Label Studio export files (export_A.json, export_B.json).
  3. Your augmented sample grid (one image is enough).
  4. 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).