* 2. Include this script: * 3. Create charts with minimal configuration - colors are auto-applied! */ (function() { 'use strict'; // ========================================================================== // READ COLORS FROM CSS CUSTOM PROPERTIES // This ensures chart colors stay in sync with the theme // ========================================================================== /** * Get a CSS custom property value from :root */ function getCSSVar(name, fallback = '') { if (typeof getComputedStyle === 'undefined') return fallback; const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); return value || fallback; } /** * Build palette from CSS custom properties (with fallbacks) */ function buildPaletteFromCSS() { return { // Primary brand colors dartmouthGreen: getCSSVar('--dartmouth-green', '#00693e'), textPrimary: getCSSVar('--text-primary', '#0a2518'), textSecondary: getCSSVar('--text-secondary', '#0a3d23'), // Chart colors (from CSS --chart-color-N variables) chartColors: [ getCSSVar('--chart-color-1', '#00693e'), getCSSVar('--chart-color-2', '#267aba'), getCSSVar('--chart-color-3', '#ffa00f'), getCSSVar('--chart-color-4', '#9d162e'), getCSSVar('--chart-color-5', '#8a6996'), getCSSVar('--chart-color-6', '#a5d75f'), getCSSVar('--chart-color-7', '#003c73'), getCSSVar('--chart-color-8', '#d94415'), getCSSVar('--chart-color-9', '#643c20'), getCSSVar('--chart-color-10', '#c4dd88'), getCSSVar('--chart-color-11', '#f5dc69'), getCSSVar('--chart-color-12', '#424141'), ], // Background colors (semi-transparent versions) chartBgColors: [ getCSSVar('--chart-bg-1', 'rgba(0, 105, 62, 0.5)'), getCSSVar('--chart-bg-2', 'rgba(38, 122, 186, 0.5)'), getCSSVar('--chart-bg-3', 'rgba(255, 160, 15, 0.5)'), getCSSVar('--chart-bg-4', 'rgba(157, 22, 46, 0.5)'), getCSSVar('--chart-bg-5', 'rgba(138, 105, 150, 0.5)'), getCSSVar('--chart-bg-6', 'rgba(165, 215, 95, 0.5)'), ], // Semantic colors positive: getCSSVar('--chart-positive', '#00693e'), negative: getCSSVar('--chart-negative', '#9d162e'), neutral: getCSSVar('--chart-neutral', '#424141'), highlight: getCSSVar('--chart-highlight', '#ffa00f'), // Grid and axis colors gridLight: getCSSVar('--chart-grid-light', 'rgba(0, 105, 62, 0.1)'), gridMedium: getCSSVar('--chart-grid-medium', 'rgba(0, 105, 62, 0.15)'), gridDark: getCSSVar('--chart-grid-dark', 'rgba(0, 105, 62, 0.2)'), axisColor: getCSSVar('--chart-axis-color', '#0a2518'), // Font fontFamily: getCSSVar('--chart-font-family', "'Avenir LT Std', 'Avenir', 'Avenir Next', -apple-system, BlinkMacSystemFont, sans-serif"), }; } // Initialize palette (will be populated when DOM is ready) let CDL_PALETTE = null; // For convenience, expose primary chart colors array let CHART_COLORS = null; // ========================================================================== // FONT CONFIGURATION // Responsive font sizes based on typical Marp slide dimensions (1280x720) // ========================================================================== const FONT_CONFIG = { sizes: { title: 22, // Chart title subtitle: 18, // Subtitle legend: 16, // Legend labels axisTitle: 18, // Axis titles axisTicks: 16, // Axis tick labels tooltip: 14, // Tooltip text dataLabels: 14, // Data labels on charts }, weight: { normal: 400, medium: 500, bold: 600, }, }; // ========================================================================== // HELPER FUNCTIONS // ========================================================================== /** * Ensure palette is initialized */ function ensurePalette() { if (!CDL_PALETTE) { CDL_PALETTE = buildPaletteFromCSS(); CHART_COLORS = CDL_PALETTE.chartColors; } return CDL_PALETTE; } /** * Get color for a dataset at given index * Cycles through palette if more datasets than colors */ function getColor(index) { ensurePalette(); return CHART_COLORS[index % CHART_COLORS.length]; } /** * Get color with alpha transparency */ function getColorWithAlpha(color, alpha) { // Handle hex colors if (color.startsWith('#')) { const r = parseInt(color.slice(1, 3), 16); const g = parseInt(color.slice(3, 5), 16); const b = parseInt(color.slice(5, 7), 16); return `rgba(${r}, ${g}, ${b}, ${alpha})`; } // Handle rgba colors if (color.startsWith('rgba')) { return color.replace(/[\d.]+\)$/, `${alpha})`); } return color; } /** * Generate colors for all datasets in chart data * Automatically assigns colors if not specified */ function autoAssignColors(data, chartType) { if (!data || !data.datasets) return data; data.datasets.forEach((dataset, index) => { const baseColor = getColor(index); // Only assign colors if not already specified switch (chartType) { case 'bar': case 'horizontalBar': if (!dataset.backgroundColor) { dataset.backgroundColor = baseColor; } if (!dataset.borderColor) { dataset.borderColor = baseColor; } if (dataset.borderWidth === undefined) { dataset.borderWidth = 2; } break; case 'line': if (!dataset.borderColor) { dataset.borderColor = baseColor; } if (!dataset.backgroundColor) { dataset.backgroundColor = getColorWithAlpha(baseColor, 0.1); } if (dataset.borderWidth === undefined) { dataset.borderWidth = 3; } if (dataset.pointRadius === undefined) { dataset.pointRadius = 6; } if (!dataset.pointBackgroundColor) { dataset.pointBackgroundColor = baseColor; } if (dataset.tension === undefined) { dataset.tension = 0.3; } break; case 'scatter': case 'bubble': if (!dataset.backgroundColor) { dataset.backgroundColor = baseColor; } if (!dataset.borderColor) { dataset.borderColor = baseColor; } if (dataset.pointRadius === undefined) { dataset.pointRadius = 15; } if (dataset.pointHoverRadius === undefined) { dataset.pointHoverRadius = 18; } break; case 'pie': case 'doughnut': case 'polarArea': // For pie charts, we need multiple colors for one dataset if (!dataset.backgroundColor) { const numItems = dataset.data ? dataset.data.length : 6; dataset.backgroundColor = []; for (let i = 0; i < numItems; i++) { dataset.backgroundColor.push(getColor(i)); } } if (!dataset.borderColor) { dataset.borderColor = '#d8d8d8'; // Slide background } if (dataset.borderWidth === undefined) { dataset.borderWidth = 2; } break; case 'radar': if (!dataset.borderColor) { dataset.borderColor = baseColor; } if (!dataset.backgroundColor) { dataset.backgroundColor = getColorWithAlpha(baseColor, 0.2); } if (dataset.borderWidth === undefined) { dataset.borderWidth = 2; } if (dataset.pointRadius === undefined) { dataset.pointRadius = 4; } if (!dataset.pointBackgroundColor) { dataset.pointBackgroundColor = baseColor; } break; default: // Generic color assignment if (!dataset.backgroundColor) { dataset.backgroundColor = baseColor; } if (!dataset.borderColor) { dataset.borderColor = baseColor; } } }); return data; } // ========================================================================== // CHART.JS GLOBAL DEFAULTS // ========================================================================== function applyGlobalDefaults() { if (typeof Chart === 'undefined') { console.warn('Chart.js not loaded. chart-defaults.js requires Chart.js to be loaded first.'); return false; } // Ensure palette is loaded from CSS const palette = ensurePalette(); // Font defaults Chart.defaults.font.family = palette.fontFamily; Chart.defaults.font.size = FONT_CONFIG.sizes.axisTicks; Chart.defaults.color = palette.textPrimary; // Responsive defaults Chart.defaults.responsive = true; Chart.defaults.maintainAspectRatio = false; // Animation (subtle) Chart.defaults.animation.duration = 400; // Plugin defaults // Legend Chart.defaults.plugins.legend.labels.font = { family: palette.fontFamily, size: FONT_CONFIG.sizes.legend, weight: FONT_CONFIG.weight.normal, }; Chart.defaults.plugins.legend.labels.color = palette.textPrimary; Chart.defaults.plugins.legend.labels.usePointStyle = true; Chart.defaults.plugins.legend.labels.padding = 20; // Title Chart.defaults.plugins.title.font = { family: palette.fontFamily, size: FONT_CONFIG.sizes.title, weight: FONT_CONFIG.weight.medium, }; Chart.defaults.plugins.title.color = palette.textPrimary; // Tooltip Chart.defaults.plugins.tooltip.backgroundColor = palette.textPrimary; Chart.defaults.plugins.tooltip.titleFont = { family: palette.fontFamily, size: FONT_CONFIG.sizes.tooltip, weight: FONT_CONFIG.weight.medium, }; Chart.defaults.plugins.tooltip.bodyFont = { family: palette.fontFamily, size: FONT_CONFIG.sizes.tooltip, }; Chart.defaults.plugins.tooltip.cornerRadius = 4; Chart.defaults.plugins.tooltip.padding = 10; // Scale defaults (for cartesian charts) // These need to be applied per-scale type const scaleDefaults = { grid: { color: palette.gridLight, lineWidth: 1, }, border: { color: palette.gridDark, width: 1, }, ticks: { font: { family: palette.fontFamily, size: FONT_CONFIG.sizes.axisTicks, }, color: palette.textPrimary, }, title: { font: { family: palette.fontFamily, size: FONT_CONFIG.sizes.axisTitle, weight: FONT_CONFIG.weight.normal, }, color: palette.textPrimary, }, }; // Apply scale defaults to linear scale if (Chart.defaults.scales && Chart.defaults.scales.linear) { if (Chart.defaults.scales.linear.grid) Object.assign(Chart.defaults.scales.linear.grid, scaleDefaults.grid); if (Chart.defaults.scales.linear.border) Object.assign(Chart.defaults.scales.linear.border, scaleDefaults.border); if (Chart.defaults.scales.linear.ticks) Object.assign(Chart.defaults.scales.linear.ticks, scaleDefaults.ticks); if (Chart.defaults.scales.linear.title) Object.assign(Chart.defaults.scales.linear.title, scaleDefaults.title); } // Apply scale defaults to category scale if (Chart.defaults.scales && Chart.defaults.scales.category) { if (Chart.defaults.scales.category.grid) Object.assign(Chart.defaults.scales.category.grid, scaleDefaults.grid); if (Chart.defaults.scales.category.border) Object.assign(Chart.defaults.scales.category.border, scaleDefaults.border); if (Chart.defaults.scales.category.ticks) Object.assign(Chart.defaults.scales.category.ticks, scaleDefaults.ticks); if (Chart.defaults.scales.category.title) Object.assign(Chart.defaults.scales.category.title, scaleDefaults.title); } // Apply scale defaults to logarithmic scale if (Chart.defaults.scales && Chart.defaults.scales.logarithmic) { if (Chart.defaults.scales.logarithmic.grid) Object.assign(Chart.defaults.scales.logarithmic.grid, scaleDefaults.grid); if (Chart.defaults.scales.logarithmic.border) Object.assign(Chart.defaults.scales.logarithmic.border, scaleDefaults.border); if (Chart.defaults.scales.logarithmic.ticks) Object.assign(Chart.defaults.scales.logarithmic.ticks, scaleDefaults.ticks); if (Chart.defaults.scales.logarithmic.title) Object.assign(Chart.defaults.scales.logarithmic.title, scaleDefaults.title); } // Apply scale defaults to radial scale (for radar charts) if (Chart.defaults.scales && Chart.defaults.scales.radialLinear) { if (Chart.defaults.scales.radialLinear.grid) Chart.defaults.scales.radialLinear.grid.color = palette.gridLight; if (Chart.defaults.scales.radialLinear.angleLines) Chart.defaults.scales.radialLinear.angleLines.color = palette.gridMedium; if (Chart.defaults.scales.radialLinear.pointLabels) { Chart.defaults.scales.radialLinear.pointLabels.font = { family: palette.fontFamily, size: FONT_CONFIG.sizes.axisTicks, }; Chart.defaults.scales.radialLinear.pointLabels.color = palette.textPrimary; } } return true; } // ========================================================================== // CHART WRAPPER FOR AUTO-STYLING // ========================================================================== /** * Wrap the Chart constructor to automatically apply CDL styling */ function wrapChartConstructor() { if (typeof Chart === 'undefined') return; const OriginalChart = Chart; // Create a wrapper that auto-applies colors window.Chart = function(ctx, config) { // Auto-assign colors if not specified if (config && config.data) { config.data = autoAssignColors(config.data, config.type); } // Merge default options for specific chart types if (config && config.options) { config.options = applyChartTypeDefaults(config.type, config.options); } // Call original constructor return new OriginalChart(ctx, config); }; // Copy static properties and methods Object.setPrototypeOf(window.Chart, OriginalChart); Object.assign(window.Chart, OriginalChart); // Preserve the prototype chain window.Chart.prototype = OriginalChart.prototype; } /** * Apply chart-type specific defaults */ function applyChartTypeDefaults(chartType, userOptions) { const options = { ...userOptions }; switch (chartType) { case 'bar': case 'horizontalBar': // Bar chart defaults if (!options.scales) options.scales = {}; if (!options.scales.x) options.scales.x = {}; if (!options.scales.y) options.scales.y = {}; // Hide x-axis grid for cleaner look if (options.scales.x.grid === undefined) { options.scales.x.grid = { display: false }; } break; case 'line': // Line chart defaults if (!options.interaction) { options.interaction = { intersect: false, mode: 'index' }; } break; case 'pie': case 'doughnut': // Pie/doughnut defaults if (!options.plugins) options.plugins = {}; if (options.plugins.legend === undefined) { const palette = ensurePalette(); options.plugins.legend = { position: 'right', labels: { font: { family: palette.fontFamily, size: FONT_CONFIG.sizes.legend, }, color: palette.textPrimary, padding: 15, }, }; } break; case 'radar': // Radar chart defaults - keep as-is, scale defaults applied globally break; case 'scatter': case 'bubble': // Scatter/bubble defaults if (!options.scales) options.scales = {}; if (!options.scales.x) options.scales.x = {}; if (!options.scales.y) options.scales.y = {}; break; } return options; } // ========================================================================== // CONVENIENCE FUNCTIONS FOR USERS // Exposed on window.CDLChart for easy access // ========================================================================== window.CDLChart = { // Color palette access (getters to ensure lazy initialization) get colors() { return ensurePalette().chartColors; }, get palette() { return ensurePalette(); }, // Get specific color by index getColor: getColor, // Get color with transparency getColorWithAlpha: getColorWithAlpha, // Get array of colors for a specific count getColors: function(count) { ensurePalette(); const result = []; for (let i = 0; i < count; i++) { result.push(getColor(i)); } return result; }, // Font configuration fonts: FONT_CONFIG, // Quick chart creation helpers // These create minimal config that auto-applies all styling /** * Create a simple bar chart * @param {string} canvasId - Canvas element ID * @param {string[]} labels - X-axis labels * @param {number[]} data - Data values * @param {object} options - Optional overrides */ bar: function(canvasId, labels, data, options = {}) { return new Chart(document.getElementById(canvasId), { type: 'bar', data: { labels: labels, datasets: [{ data: data }], }, options: { plugins: { legend: { display: false } }, ...options, }, }); }, /** * Create a simple line chart * @param {string} canvasId - Canvas element ID * @param {string[]} labels - X-axis labels * @param {Array} datasets - Array of {label, data} objects * @param {object} options - Optional overrides */ line: function(canvasId, labels, datasets, options = {}) { return new Chart(document.getElementById(canvasId), { type: 'line', data: { labels: labels, datasets: datasets.map(ds => ({ label: ds.label, data: ds.data, fill: ds.fill !== undefined ? ds.fill : true, })), }, options: options, }); }, /** * Create a simple pie chart * @param {string} canvasId - Canvas element ID * @param {string[]} labels - Slice labels * @param {number[]} data - Data values * @param {object} options - Optional overrides */ pie: function(canvasId, labels, data, options = {}) { return new Chart(document.getElementById(canvasId), { type: 'pie', data: { labels: labels, datasets: [{ data: data }], }, options: options, }); }, /** * Create a simple scatter chart * @param {string} canvasId - Canvas element ID * @param {Array} datasets - Array of {label, data: [{x, y}]} objects * @param {object} options - Optional overrides */ scatter: function(canvasId, datasets, options = {}) { return new Chart(document.getElementById(canvasId), { type: 'scatter', data: { datasets: datasets.map(ds => ({ label: ds.label, data: ds.data, })), }, options: options, }); }, /** * Create a doughnut chart * @param {string} canvasId - Canvas element ID * @param {string[]} labels - Slice labels * @param {number[]} data - Data values * @param {object} options - Optional overrides */ doughnut: function(canvasId, labels, data, options = {}) { return new Chart(document.getElementById(canvasId), { type: 'doughnut', data: { labels: labels, datasets: [{ data: data }], }, options: options, }); }, /** * Create a radar chart * @param {string} canvasId - Canvas element ID * @param {string[]} labels - Axis labels * @param {Array} datasets - Array of {label, data} objects * @param {object} options - Optional overrides */ radar: function(canvasId, labels, datasets, options = {}) { return new Chart(document.getElementById(canvasId), { type: 'radar', data: { labels: labels, datasets: datasets.map(ds => ({ label: ds.label, data: ds.data, })), }, options: options, }); }, }; // ========================================================================== // INITIALIZATION // ========================================================================== function initialize() { // Wait for Chart.js to be available if (typeof Chart !== 'undefined') { applyGlobalDefaults(); wrapChartConstructor(); console.log('CDL Chart defaults applied successfully.'); return true; } else { // Chart.js not yet loaded - wait and retry let retries = 0; const maxRetries = 50; // 5 seconds max wait const checkInterval = setInterval(function() { retries++; if (typeof Chart !== 'undefined') { clearInterval(checkInterval); applyGlobalDefaults(); wrapChartConstructor(); console.log('CDL Chart defaults applied successfully (after waiting for Chart.js).'); } else if (retries >= maxRetries) { clearInterval(checkInterval); console.warn('Chart.js not found after waiting. CDL Chart defaults not applied.'); } }, 100); return false; } } // Initialize IMMEDIATELY - this must run BEFORE any chart creation scripts // Chart.js CDN should be loaded before this script initialize(); })();

Lecture 7: Text classification workshop

PSYC 51.17: Models of language and communication

Jeremy R. Manning
Dartmouth College
Winter 2026

Learning Objectives

  1. Build text classifiers from scratch using scikit-learn
  2. Understand different text representation methods (BoW, TF-IDF, embeddings)
  3. Compare Naive Bayes, Logistic Regression, and Neural Network classifiers
  4. Evaluate classifier performance using appropriate metrics
  5. Debug common issues in text classification pipelines

Hands-on coding with the 20 Newsgroups dataset

Workshop Overview

  1. Part 1: Loading and exploring real data
  2. Part 2: Feature engineering for text (BoW, TF-IDF)
  3. Part 3: Building classifiers (Naive Bayes, Logistic Regression, Neural Networks)
  4. Part 4: Model comparison and analysis
  5. Part 5: Error analysis and improvements

xhour_classification_demo.ipynb (click to open in Colab!)

Let's dive in using the notebook instead of slides. After class, you can review these slides for reference.

The 20 Newsgroups Dataset

  • Posts from 20 different newsgroups
  • ~20,000 documents total
  • Good for learning classification fundamentals
  • sci.space — Science discussions about space
  • rec.sport.hockey — Sports discussions about hockey
  • misc.forsale — Random items for sale
  • comp.graphics — Computer graphics

Relatively distinct topics for easier learning.

Loading the Data


1from sklearn.datasets import fetch_20newsgroups
2
3categories = [
4    'sci.space',
5    'rec.sport.hockey',
6    'misc.forsale',
7    'comp.graphics'
8]
9
10train_data = fetch_20newsgroups(
11    subset='train',
12    categories=categories,
13    shuffle=True,
14    random_state=42,
15    remove=('headers', 'footers', 'quotes')  # Remove metadata
16)
17
18print(f"Loaded {len(train_data.data)} training documents")

Always explore your data before building models

  1. How many documents per category?
  2. What do the documents look like?
  3. What words/phrases might be good indicators?
  4. Are there categories that might be hard to distinguish?

Exploring the data: document counts and samples


1# Check class distribution
2print("Documents per category:")
3for i, name in enumerate(train_data.target_names):
4    count = (train_data.target == i).sum()
5    print(f"  {name}: {count}")
6# sci.space: 593, misc.forsale: 585, comp.graphics: 584, rec.sport.hockey: 600
7
8# Look at a sample document
9print(train_data.data[1].strip())
10# Didn't one of the early jet fighters have these?...

In our example dataset, the classes are roughly balanced (~600 documents each). Why is this good for training classifiers? What challenges might arise if one class had 10,000 documents and another only 100?

Convert text to numbers for machine learning

  1. Bag of Words (BoW): Count word frequencies
  2. TF-IDF: Weight by document frequency

Remember yesterday's lecture on tokenization? How might the choice of tokens (words, n-grams, subwords, characters) affect these representations? For today, we'll use the word "word" to mean "token". But keep in mind that other tokenization strategies could be used!

Bag of Words: count how many times each word appears in each document


1from sklearn.feature_extraction.text import CountVectorizer
2
3bow_vectorizer = CountVectorizer(
4    max_features=5000,   # Keep only top 5000 words
5    min_df=2,            # Word must appear in at least 2 docs
6    max_df=0.8,          # Word must appear in <80% of docs
7    stop_words='english' # Remove common words
8)
9
10X_train_bow = bow_vectorizer.fit_transform(train_data.data)
11# Result: Sparse matrix of word counts

Bag of Words: observations, strengths, and weaknesses

  • Simple and effective for many tasks
  • Captures word presence and frequency
  • Ignores word order and context
  • Not all words are equally informative
  • Usually high-dimensional and sparse

Term Frequency-Inverse Document Frequency

TF-IDF(t,d)=TF(t,d)×IDF(t)\text{TF-IDF}(t, d) = \text{TF}(t, d) \times \text{IDF}(t)

where:

  • tt = term (word or token)
  • dd = document
  • NN = total number of documents
  • df(t)\text{df}(t) = number of documents containing term tt
  • TF(t,d)\text{TF}(t, d) = frequency of term tt in document dd
  • IDF(t)=log⁡Ndf(t)\text{IDF}(t) = \log\frac{N}{\text{df}(t)} = (log) inverse document frequency

Downweight common words, upweight rare and informative words!

TF-IDF in practice


1from sklearn.feature_extraction.text import TfidfVectorizer
2
3tfidf_vectorizer = TfidfVectorizer(
4    max_features=5000,
5    min_df=2,
6    max_df=0.8,
7    stop_words='english',
8    use_idf=True,
9    sublinear_tf=True  # Use log scaling for term frequency
10)
11
12X_train_tfidf = tfidf_vectorizer.fit_transform(train_data.data)
13# Result: Sparse matrix of TF-IDF scores

Three classifier approaches to compare

  1. Naive Bayes: Fast, probabilistic, good baseline
  2. Logistic Regression: Linear, interpretable, performs well
  3. Neural Network: Flexible, can learn complex patterns, can also overfit

Naive Bayes classifier

P(y∣x)=P(y)P(x∣y)P(x)P(y|x) = \frac{P(y) P(x|y) }{P(x)}

P(y∣x)∝P(y)∏i=1nP(xi∣y)P(y|x) \propto P(y) \prod_{i=1}^n P(x_i|y)

  • Why "naive"? Assumes features are independent (they're not!)
  • Why does it work? Despite the wrong assumption, it often performs well for text.

1from sklearn.naive_bayes import MultinomialNB
2
3nb = MultinomialNB()
4nb.fit(X_train_tfidf, train_data.target)

Logistic regression classifier

P(y=k∣x)=ewkTx∑jewjTxP(y=k|x) = \frac{e^{w_k^T x}}{\sum_{j} e^{w_j^T x}}

where

  • wkw_k = weight vector for class kk
  • xx = input feature vector (e.g., TF-IDF scores)
  • Interpretable weights: which words matter?
  • Fast training and prediction

1from sklearn.linear_model import LogisticRegression
2
3lr = LogisticRegression(max_iter=1000, C=1.0)
4lr.fit(X_train_tfidf, train_data.target)

Neural network classifier

Input (TF-IDF) Hidden Layer 1 (256) Hidden Layer 2 (128) Output (4 classes)
A (simple) feedforward architecture

1import torch.nn as nn
2
3class TextClassifier(nn.Module):
4    def __init__(self, input_dim, hidden_dim, output_dim):
5        super().__init__()
6        self.fc1 = nn.Linear(input_dim, hidden_dim)
7        self.fc2 = nn.Linear(hidden_dim, hidden_dim // 2)
8        self.fc3 = nn.Linear(hidden_dim // 2, output_dim)
9        self.dropout = nn.Dropout(0.3)
10        self.relu = nn.ReLU()
11
12nn_model = TextClassifier(input_dim=X_train_tfidf.shape[1], hidden_dim=256, output_dim=4)

Evaluating classifier performance

  • Accuracy: overall correctness: TP+TNTP+TN+FP+FN\frac{TP + TN}{TP + TN + FP + FN}
  • Precision: of predicted positives, how many are truly positive?
  • Recall: of actual positives, how many did we catch?
  • F1-Score: harmonic mean: F1=2×Precision×RecallPrecision+RecallF1 = 2 \times \frac{Precision \times Recall}{Precision + Recall} (Intuition: balances precision and recall)
  • Confusion Matrix: detailed breakdown of predicted versus actual labels

Confusion matrix

Predicted A Predicted B Predicted C Predicted D
Actual A 85 2 3 0
Actual B 1 92 2 5
Actual C 4 3 88 5
Actual D 0 8 2 90

Diagonal = correct predictions. Off-diagonal = errors.

Error analysis: critical but often skipped

  1. Find misclassified examples
  2. Look for patterns
  3. Understand why the model failed
  4. Use insights to improve

Key Takeaways

  1. Good features matter more than complex models (for many tasks)
  2. TF-IDF usually beats raw BoW for text classification
  3. Linear models are competitive with neural networks on bag-of-words
  4. Always examine errors to understand model behavior

Putting this week's lectures into context

  1. Monday: Data cleaning (Lecture 5)
  2. Wednesday: Tokenization (Lecture 6)
  3. Today: Classification (Lecture 7)
  4. Friday: POS Tagging & Sentiment Analysis (Lecture 8)

Part-of-speech tagging and sentiment analysis: how do these building blocks combine for real NLP applications?

Hands-on exercise

  1. Load the 20 Newsgroups dataset
  2. Experiment with different vectorizers
  3. Train multiple classifiers
  4. Analyze errors and improve
  5. Try your own text examples!

Build intuition for text classification

Questions? Want to chat more?

📧 Email me
💬 Join our Discord
💁 Come to office hours

ELIZA assignment is due tomorrow at 11:59pm!