* 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 8: POS tagging & sentiment analysis

PSYC 51.17: Models of language and communication

Jeremy R. Manning
Dartmouth College
Winter 2026

Learning objectives

  1. Understand part-of-speech (POS) tagging and its applications
  2. Explore how neural networks learn grammatical structure
  3. Apply sentiment analysis to real-world text
  4. Fine-tune pre-trained models for domain-specific tasks
  5. Critically evaluate whether models "understand" language
  • Can statistical patterns capture grammatical knowledge?
  • What does it mean for a model to "understand" emotion?

Part-of-speech (POS) tagging

  • Assigning grammatical category to each word
  • Categories: noun, verb, adjective, adverb, pronoun, preposition, etc.
  • A fundamental NLP task

1The   cat   sat   on   the   mat
2DET   NOUN  VERB  ADP  DET   NOUN
  • Disambiguation: "book" as noun vs. verb
  • Syntax parsing and understanding
  • Information extraction, machine translation

POS tagsets: universal vs. fine-grained

Universal POS (17 tags):

  • ADJ, ADV, ADP, AUX
  • CONJ, DET, NOUN, NUM
  • PRON, PROPN, VERB, ...

Penn Treebank (45+ tags):

  • NN/NNS/NNP/NNPS (nouns)
  • VB/VBD/VBG/VBN/VBP/VBZ (verbs)
  • Much finer distinctions!

Simplicity vs. linguistic detail

Context matters: ambiguous words

Sentence Word POS Explanation
"I read a book" book NOUN Object being read
"Please book a table" book VERB Action of reserving
"She runs fast" fast ADV Modifies "runs"
"I will fast today" fast VERB Action of not eating
"Please close the door" close VERB Action
"Stay close to me" close ADV Modifies position

Context determines POS! Models must look at surrounding words.

spaCy resolves ambiguity using context


1import spacy
2nlp = spacy.load("en_core_web_sm")
3
4sentences = [
5    "I need to book a flight",      # book = VERB
6    "I'm reading a great book",     # book = NOUN
7    "The record was broken",        # record = NOUN
8    "Please record the meeting",    # record = VERB
9]
10
11for sent in sentences:
12    doc = nlp(sent)
13    for token in doc:
14        if token.text.lower() in ["book", "record"]:
15            print(f'"{sent}"')
16            print(f'  "{token.text}" -> {token.pos_} ({spacy.explain(token.pos_)})\n')

Examples: "to book" vs "a book". Preceeding words tell us whether "book" is a verb or noun.

What methods are used for POS tagging?

Traditional approaches (pre-neural network)

  • Rule-based: hand-crafted grammar rules
  • Hidden Markov Models (HMMs): probabilistic sequences
  • Conditional Random Fields (CRFs): structured prediction

Modern neural network approaches

  • Recurrent Neural Networks (RNNs/LSTMs): process sequences
  • Transformers (BERT, etc.): bidirectional context
  • Fine-tune pre-trained models on POS data
  • Learn patterns from data (no hand-crafted rules)
  • Capture long-range dependencies
  • Handle ambiguity through context
  • State-of-the-art accuracy (>97% on English!)

POS tagging is tricky!

Linzen, Dupoux, & Goldberg (2016, TACL). Assessing the ability of LSTMs to learn syntax-sensitive dependencies.

The key to the cabinets is/are? ...
Challenge: distractor nouns between subject and verb
  • Model must identify "key" (singular) as subject
  • Ignore "cabinets" (plural distractor)
  • Predict correct verb form "is" (not "are")

Token classification with HuggingFace


1from transformers import pipeline
2pos_tagger = pipeline("token-classification",
3    model="vblagoje/bert-english-uncased-finetuned-pos",
4    aggregation_strategy="simple")
5
6sentence = "Apple Inc. is looking at buying a UK startup"
7results = pos_tagger(sentence)
8
9for result in results:
10    print(f"{result['word']:<15} {result['entity_group']:<8} ({result['score']:.3f})")
11# Apple           PROPN    (0.998)
12# Inc.            PROPN    (0.995)
13# is              AUX      (0.999)
14# looking         VERB     (0.997)

So what? Why is POS tagging important?

Applications

  • Syntax parsing
  • Named entity recognition
  • Information extraction
  • Machine translation
  • Sentiment analysis (e.g., adjectives)

Linguistic insights

  • Study grammatical structure
  • Analyze language acquisition
  • Explore cross-linguistic patterns
  • Understand model "knowledge" of syntax
  • Stylometric analysis

Consider how simple rule-based models like ELIZA parse user inputs versus what it means to explicitly model and identify parts of speech. Does POS tagging represent a deeper "understanding" of text? Or is it just another statistical pattern?

Check out the POS tagging demo to explore POS tagging interactively.

Sentiment analysis overview

  • Identifying emotional tone in text
  • Classifying as positive, negative, neutral
  • Extracting subjective information

1"I love this product!"           → Positive
2"This is the worst experience."  → Negative
3"It's okay, nothing special."    → Neutral

Sentiment is often linked to adjectives/adverbs (e.g., "great", "terrible"). POS tagging can help identify sentiment-bearing words. (Caveat: modern models often learn this implicitly!)

Sentiment analysis identifies emotional "tone"

Applications

  • Social media monitoring
  • Customer feedback analysis
  • Market research
  • Review analysis
  • Political tracking
  • Psychological studies (mood/emotion)
  • Chatbots and virtual assistants

Granularity levels

  • Binary: positive/negative
  • Ternary: positive/negative/neutral
  • Fine-grained: 1-5 stars
  • Continuous: sentiment score
  • Aspect-based: per-feature
  • Emotion detection: joy, anger, sadness, etc.

Sentiment analysis challenges

  • "Oh great, another meeting" (negative, despite "great")
  • "This is the best movie I've ever fallen asleep to" (negative!)
  • "This movie is sick!" (positive in slang, negative literally)
  • "The book was long" (neutral? negative?)
  • "Great food but terrible service" (both positive and negative)
  • Aspect-based sentiment: food=positive, service=negative
  • "not good" vs. "good"
  • "I don't dislike it" (double negative = positive?)
  • "Explosive growth" (positive in business, negative in safety)
  • Different domains have different sentiment patterns
  • "It's okay, I guess" (neutral or slightly negative?)
  • Subjective interpretations vary

Sentiment analysis with HuggingFace


1from transformers import pipeline
2sentiment = pipeline("sentiment-analysis")
3
4tricky_cases = [
5    ("Oh great, another Monday meeting", "NEGATIVE"),    # Sarcasm
6    ("This movie is not bad at all", "POSITIVE"),        # Negation
7    ("I don't dislike this product", "POSITIVE"),        # Double negative
8    ("Great camera but terrible battery life", "MIXED"), # Mixed sentiment
9]
10
11for text, expected in tricky_cases:
12    result = sentiment(text)[0]
13    print(f"{text}")
14    print(f"  Model: {result['label']} ({result['score']:.3f}) | Expected: {expected}\n")

Even modern models still struggle with sarcasm and complex negation!

There are essentially two types of sentiment analysis approaches

  • Lexicon-based methods: predefined word sentiment scores
  • Machine learning methods: statistical models (traditional or neural)

Sentiment lexicons: sum up words with predefined sentiment scores

Popular lexicons:

  • AFINN: -5 to +5 ratings
  • SentiWordNet: pos/neg/neutral
  • VADER: Social media focused

Limitations: Ignores context, word order, sarcasm

Example (AFINN):

Word Score
great +3
good +3
hate -3
terrible -3
awful -3
  • Simple and interpretable
  • Fast!
  • No training data needed; "just works" out of the box

Neural network approaches for sentiment analysis

  • Learn context-dependent representations
  • Capture word order and negation
  • Handle sarcasm better (though still imperfect)
  • Transfer learning: pre-train on large corpus, fine-tune for sentiment
Input Text Pre-trained Model (BERT) Classification Head Sentiment Label
Typical neural sentiment analysis architecture

The pre-trained model learns general language patterns, and the classification head is trained specifically for sentiment analysis using labeled sentiment data (IMDb reviews, Amazon reviews, restaurant reviews, etc.).

Discussion: understanding emotion or "just" patterns?

  1. Can models "feel" sentiment?
    • They predict labels, but do they understand emotion?
  2. Is sentiment objective or subjective?
    • Different annotators may disagree on sentiment
    • How do models handle ambiguity?
  3. Cultural and linguistic variation:
    • Sentiment expressions vary across cultures
    • Can models capture these nuances?
  4. Ethical considerations:
    • Automated sentiment analysis in hiring, lending...
    • Risks of bias and discrimination
    • Should we trust model judgments?

Looking back: week 2

Data Cleaning Tokenization POS Tagging Sentiment Analysis
The NLP pipeline (so far!)
  • Data cleaning: reduces noise by removing unwanted elements
  • Tokenization: break text into units to enable more efficient processing
  • POS tagging: reveals grammatical structure
  • Sentiment analysis: extracts emotional valence and tone

Assignment 2: SPAM email classifier

Apply last week's concepts

  • Data cleaning: remove HTML tags and special characters, normalize text and formatting
  • Tokenization: try different tokenizers (word, subword)
  • Features: extract useful signals (POS patterns, sentiment, keywords, etc.)
  • Classification: train a model to identify spam vs. legitimate emails

Think about

  • Intuitively, what makes spam different from legitimate emails? Can you codify these differences?
  • How does preprocessing affect accuracy?
  • Can you create compute new features that improve performance?
  • Can you leverage POS patterns? (e.g., does spam tend to have more imperative verbs?)
  • Which model architectures work best for this task?

Key takeaways

  • POS tagging assigns grammatical categories to words using context
  • Neural networks (RNNs, Transformers) excel at POS tagging
  • Sentiment analysis identifies emotional tone in text
  • Lexicon-based and neural network methods have different strengths
  • Challenges include sarcasm, negation, and context-dependence
  • Critical thinking about model "understanding" is essential

Looking ahead: the rest of this week

  • Classic embeddings: LSA, LDA, topic modeling
  • Word embeddings: Word2Vec, GloVe, FastText
  • Distributional semantics and dimensionality reduction

"You shall know a word by the company it keeps"

How can we represent word meaning computationally?

  • Finishing Assignment 2 (due Monday!)
  • Thinking about what "meaning" is. How might you define it?

Questions? Want to chat more?

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

We'll diving into embeddings during our X-hour!