* 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.07: 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_)})')
continued...

spaCy resolves ambiguity using context


17            print()
...continued

"to book" vs "a book" — context is everything!

POS tagging with spaCy


1import spacy
2nlp = spacy.load("en_core_web_sm")
3
4sentence = "She will book the meeting room tomorrow"
5doc = nlp(sentence)
6
7print(f"{'Word':<12} {'POS':<8} {'Tag':<8} {'Explanation'}")
8for token in doc:
9    print(f"{token.text:<12} {token.pos_:<8} {token.tag_:<8} {spacy.explain(token.pos_)}")
10
11# Output:
12# She          PRON     PRP      pronoun, personal
13# will         AUX      MD       verb, modal auxiliary
14# book         VERB     VB       verb, base form
15# the          DET      DT       determiner
16# meeting      NOUN     NN       noun, singular or mass

"book" correctly identified as VERB!

How do POS taggers work?

Traditional approaches (pre-neural):

  • Rule-based: Hand-crafted grammar rules
  • Hidden Markov Models (HMMs): Probabilistic sequences
  • Conditional Random Fields (CRFs): Structured prediction

Modern neural 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!)

Neural networks and grammar

Classic test: Subject-verb agreement (Linzen et al., 2016)

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")

LSTMs can learn this! But they struggle with complex cases.

Reference: Linzen, Dupoux, & Goldberg (2016). TACL.

BLiMP: Testing linguistic knowledge

  • 67,000 minimal pairs across 67 paradigms
  • Tests syntax, semantics, morphology
  • Task: Model assigns higher P to acceptable sentence

Transformers (BERT, GPT-2) score 70-85%, but not perfect!

Acceptable Unacceptable
"Who did you see?" "Who did you saw?"
"I think that she left" "I think that she leave"

Reference: Warstadt et al. (2020). TACL.

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)

Discussion: Do models "understand" grammar?

  1. Chomsky's view: Grammar requires innate, symbolic rules
    • Can statistical patterns truly capture grammatical knowledge?
  2. Emergentist view: Grammar emerges from usage patterns
    • Maybe neural networks learn similarly to humans?
  3. Functional perspective: If it works, does it matter?
    • Models perform well on tasks—is that "understanding"?
  4. Limitations: Models still fail on edge cases
    • What does this tell us about their knowledge?

Is pattern matching sufficient for grammatical competence?

Sentiment analysis determines emotional tone of text

Applications:

  • Social media monitoring
  • Customer feedback analysis
  • Market research
  • Review analysis
  • Political tracking

Granularity levels:

  • Binary: positive/negative
  • Ternary: positive/negative/neutral
  • Fine-grained: 1-5 stars
  • Continuous: sentiment score
  • Aspect-based: per-feature

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

Sentiment challenges: Code examples


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")

Many models still struggle with sarcasm and complex negation!

Sentiment lexicons: 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

Lexicon-based sentiment analysis


1from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
2analyzer = SentimentIntensityAnalyzer()
3
4texts = ["I love this product! It's amazing!", "This is the worst experience ever.",
5         "It's okay, nothing special.", "Great food but terrible service!"]
6
7for text in texts:
8    scores = analyzer.polarity_scores(text)
9    print(f"{text}")
10    print(f"  Pos: {scores['pos']:.2f}, Neg: {scores['neg']:.2f}, Compound: {scores['compound']:.3f}\n")
11# VADER handles emoji and punctuation! "Great!!!" scores higher than "Great"

Modern approach: Pre-trained models

  • 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

Fine-tune on labeled sentiment data (IMDb, Amazon reviews, etc.)

VADER vs Neural: Head-to-head comparison


1from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
2from transformers import pipeline
3
4vader = SentimentIntensityAnalyzer()
5neural = pipeline("sentiment-analysis")
6
7test_cases = [
8    "I absolutely love this product!",
9    "This is not what I expected, but in a good way",
10    "The movie was so bad it was actually hilarious",
11]
12
13for text in test_cases:
14    v_score = vader.polarity_scores(text)['compound']
15    n_result = neural(text)[0]
continued...

VADER vs Neural: Head-to-head comparison


16    print(f"Text: {text}")
17    print(f"  VADER: {v_score:+.3f} ({'POS' if v_score > 0 else 'NEG'})")
18    print(f"  Neural: {n_result['label']} ({n_result['score']:.3f})\n")
...continued

Neural models handle nuance better, but VADER is faster and interpretable.

Sentiment analysis with HuggingFace


1from transformers import pipeline
2sentiment_analyzer = pipeline("sentiment-analysis")
3
4texts = ["I love this product! It's amazing!", "This is the worst experience ever.",
5         "It's okay, nothing special.", "I don't hate it, but I don't love it either."]
6
7for text in texts:
8    result = sentiment_analyzer(text)[0]
9    print(f"{text}")
10    print(f"  {result['label']}, Confidence: {result['score']:.3f}\n")
11# "I love this product!" → POSITIVE (0.999)

Domain-specific sentiment models

General models miss domain-specific language

Fine-tune on domain-specific data!

Why it works: Domain-specific vocabulary ("bullish" in finance = positive), different sentiment expressions, adapted conventions.

Domain Model Data
Medical BioBERT Patient feedback
Twitter TwitterBERT Social posts
Products RoBERTa Amazon reviews
Movies BERT IMDb reviews

Comparing general vs. domain-specific


1from transformers import pipeline
2general = pipeline("sentiment-analysis")
3financial = pipeline("sentiment-analysis", model="ProsusAI/finbert")
4
5texts = ["The company's earnings exceeded expectations",
6         "Revenue declined but margins improved", "Stock prices plummeted"]
7
8for text in texts:
9    gen = general(text)[0]
10    fin = financial(text)[0]
11    print(f"{text}")
12    print(f"  General: {gen['label']} ({gen['score']:.3f}) | "
13          f"Financial: {fin['label']} ({fin['score']:.3f})\n")
14# Financial model often more accurate for finance text!

Fine-tuning for sentiment analysis

  1. Start with pre-trained model (BERT, RoBERTa) — already knows language
  2. Prepare labeled dataset — text + sentiment labels (pos/neg/neutral)
  3. Add classification head — dense layer outputting class probabilities
  4. Fine-tune on sentiment data — much faster than training from scratch!
  5. Evaluate — accuracy, precision, recall, F1-score

Fine-tuning example (simplified)


1from transformers import AutoModelForSequenceClassification, Trainer, TrainingArguments
2
3# 1. Load pre-trained model
4model = AutoModelForSequenceClassification.from_pretrained("bert-base-uncased", num_labels=2)
5
6# 2. Define training arguments
7training_args = TrainingArguments(output_dir="./results", num_train_epochs=3,
8    per_device_train_batch_size=16, evaluation_strategy="epoch")
9
10# 3. Create Trainer and train (train_dataset, eval_dataset prepared separately)
11trainer = Trainer(model=model, args=training_args,
12    train_dataset=train_dataset, eval_dataset=eval_dataset)
13trainer.train()

Evaluation metrics for sentiment analysis

  • Precision: Of predicted positives, how many are truly positive?

    Precision=TPTP+FP\text{Precision} = \frac{TP}{TP + FP}

  • Recall: Of actual positives, how many did we catch?

    Recall=TPTP+FN\text{Recall} = \frac{TP}{TP + FN}

  • F1-Score: Harmonic mean of precision and recall

    F1=2×Precision×RecallPrecision+RecallF1 = 2 \times \frac{\text{Precision} \times \text{Recall}}{\text{Precision} + \text{Recall}}

  • Imbalanced datasets (e.g., 90% positive reviews)
  • Different costs for false positives vs. false negatives
  • F1 gives balanced view of model performance

Confusion matrix example

Predicted Positive Predicted Negative
Actual Positive TP = 90 FN = 10
Actual Negative FP = 5 TN = 95
  • Accuracy = (90 + 95) / 200 = 92%
  • Precision = 90 / 95 = 95%
  • Recall = 90 / 100 = 90%
  • F1 = 2 × (0.95 × 0.90) / (0.95 + 0.90) = 92%

Aspect-based sentiment analysis

Reviews often mention multiple aspects with different sentiments

"The food was delicious but the service was terrible. The atmosphere was okay."

  • Food: Positive
  • Service: Negative
  • Atmosphere: Neutral

Applications:

  • Restaurant reviews: food, service, ambiance, price
  • Product reviews: quality, price, shipping, customer service
  • Hotel reviews: room, location, staff, cleanliness

More nuanced than overall sentiment! Provides actionable insights.

Aspect-based sentiment: Worked example


1review = """The pasta was incredible - best I've had!
2However, we waited 45 min which was frustrating.
3Ambiance was nice but loud. Prices reasonable."""
4
5aspects = {
6    "food": ["pasta", "incredible"],    # POSITIVE
7    "service": ["waited", "frustrating"], # NEGATIVE
8    "ambiance": ["nice", "loud"],        # MIXED
9    "price": ["reasonable"]              # POSITIVE
10}
Aspect Sentiment
Food POSITIVE
Service NEGATIVE
Ambiance MIXED
Price POSITIVE

Focus on improving wait times!

Real-world application: Product review analysis

  • Identify product strengths and weaknesses
  • Track sentiment trends over time
  • Compare against competitors
  • Prioritize product improvements
Collect Reviews Extract Aspects Analyze Sentiment Generate Insights
Product review analysis pipeline
  • Product A: Stable positive sentiment
  • Product B: Declining sentiment → investigate quality issues!

Hands-on exercise

  1. Collect data:
    • Scrape product reviews (Amazon, Yelp)
    • Or use public dataset (IMDb, Twitter)
  2. Compare approaches:
    • Lexicon-based (VADER)
    • General pre-trained model (HuggingFace pipeline)
    • Domain-specific model (if available)
  3. Analyze results:
    • Where do models disagree?
    • Which handles sarcasm better?
    • Which is most accurate for your domain?
  4. Bonus: Fine-tune a model on your specific dataset!

Discussion: Understanding emotion

  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?

Week 2: Each step builds on the previous one

Data Cleaning Tokenization POS Tagging Sentiment Analysis
The NLP pipeline
  • Data cleaning: Remove noise
  • Tokenization: Break into units
  • POS tagging: Grammar structure
  • Sentiment: Emotional meaning

Assignment 2: SPAM email classifier

Build a classifier to detect spam emails

Apply this week's concepts:

  • Data cleaning: Remove HTML tags, normalize text
  • Tokenization: Try different tokenizers (word, subword)
  • Features: Extract useful signals (POS patterns, sentiment?)
  • Classification: Train a model to identify spam

Think about:

  • What makes spam different from legitimate emails?
  • How does preprocessing affect accuracy?
  • Can you use sentiment as a feature?
  • What about POS patterns? (e.g., spam has more imperatives?)

Key takeaways

  1. POS tagging reveals grammatical structure — Neural nets learn syntax, but do they "understand" it?
  2. Sentiment analysis extracts emotional meaning — Lexicons → neural networks; domain fine-tuning helps
  3. Statistical learning powers modern NLP — Models learn patterns without explicit rules
  4. Context is crucial — Words are ambiguous; Transformers excel at capturing context
  5. Critical thinking matters — Question what "understanding" means; be aware of biases

Primary references

POS tagging and syntax:

  • Linzen, Dupoux, & Goldberg (2016). Assessing the Ability of LSTMs to Learn Syntax-Sensitive Dependencies. TACL.
  • Warstadt et al. (2020). BLiMP: The Benchmark of Linguistic Minimal Pairs. TACL.

Sentiment analysis:

  • Pang & Lee (2008). Opinion Mining and Sentiment Analysis. Foundations and Trends in IR.
  • Hutto & Gilbert (2014). VADER: A Parsimonious Rule-based Model for Sentiment Analysis. ICWSM.

Looking ahead: Weeks 3-4

  • Dimensionality reduction (PCA, UMAP)
  • Bag-of-words and TF-IDF
  • Word embeddings (Word2Vec, GloVe)
  • Distributional semantics

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

How can we represent word meaning computationally?

  • Completing Assignment 2
  • Thinking about: What is "meaning"? How would you define it?
  • Exploring: Vector representations and semantic similarity

Additional resources

Tools and libraries:

Datasets:

HuggingFace Spaces: Try models in your browser! https://huggingface.co/spaces

Questions? Want to chat more?

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

Week 2 complete! See you in Week 3!