Phase 05 - Lesson 07

Marcação de Classes Gramaticais (POS) e Análise Sintática

A gramática ficou fora de moda por um tempo. Depois, todo pipeline de LLM precisou validar extração estruturada, e ela voltou.

Tipo: Build Linguagens: Python Pré-requisitos: Fase 5 · 01 (Processamento de Texto), Fase 2 · 14 (Naive Bayes) Tempo: ~45 minutos

O Problema

A lição 01 prometeu que a lematização precisa de uma marcação de classe gramatical. Sem saber que running é um verbo, um lematizador não consegue reduzi-lo a run. Sem saber que better é um adjetivo, ele não consegue reduzi-lo a good.

Essa promessa escondia um subcampo inteiro. A marcação de classes gramaticais atribui categorias gramaticais. A análise sintática recupera a estrutura em árvore da frase: qual palavra modifica qual, qual verbo governa quais argumentos. O PLN clássico passou vinte anos refinando ambas. Então o aprendizado profundo as colapsou em uma tarefa de classificação de tokens sobre um transformer pré-treinado, e a comunidade de pesquisa seguiu em frente.

A comunidade aplicada não. Todo pipeline de extração estruturada ainda usa POS e árvores de dependência por baixo dos panos. JSON gerado por LLM é validado contra restrições gramaticais. Sistemas de perguntas e respostas decompõem consultas usando análises de dependência. Avaliadores de qualidade de tradução automática verificam o alinhamento de árvores de análise.

Vale a pena conhecer. Esta lição apresenta os conjuntos de tags, as baselines e o ponto em que você para de implementar do zero e chama o spaCy.

O Conceito

Exemplo de tag POS + análise de dependência

Marcação POS rotula cada token com uma categoria gramatical. O conjunto de tags do Penn Treebank (PTB) é o padrão para o inglês. 36 tags com distinções que o leitor casual acha minuciosas: NN substantivo singular, NNS substantivo plural, NNP substantivo próprio singular, VBD verbo no passado, VBZ verbo na 3ª pessoa do singular do presente, e assim por diante. O conjunto de tags das Universal Dependencies (UD) é mais grosseiro (17 tags) e independente de idioma; tornou-se o padrão para trabalho multilíngue.

The/DET cats/NOUN were/AUX running/VERB at/ADP 3pm/NOUN ./PUNCT

Análise sintática produz uma árvore. Dois estilos principais:

  • Análise de constituintes. Sintagmas nominais, sintagmas verbais e sintagmas preposicionais aninham-se uns dentro dos outros. A saída é uma árvore de categorias não terminais (NP, VP, PP) com as palavras como folhas.
  • Análise de dependência. Cada palavra tem uma única palavra-cabeça da qual depende, rotulada com uma relação gramatical. A saída é uma árvore em que cada aresta é uma tripla (cabeça, dependente, relação).

A análise de dependência venceu nos anos 2010 porque generaliza de forma limpa entre idiomas, especialmente os de ordem de palavras livre.

running is ROOT
cats is nsubj of running
were is aux of running
at is prep of running
3pm is pobj of at

Construa

Passo 1: baseline da tag mais frequente

O marcador POS mais simplório que funciona. Para cada palavra, preveja a tag que ela teve com mais frequência no treinamento.

from collections import Counter, defaultdict


def train_mft(train_examples):
    word_tag_counts = defaultdict(Counter)
    all_tags = Counter()
    for tokens, tags in train_examples:
        for token, tag in zip(tokens, tags):
            word_tag_counts[token.lower()][tag] += 1
            all_tags[tag] += 1
    word_best = {w: c.most_common(1)[0][0] for w, c in word_tag_counts.items()}
    default_tag = all_tags.most_common(1)[0][0]
    return word_best, default_tag


def predict_mft(tokens, word_best, default_tag):
    return [word_best.get(t.lower(), default_tag) for t in tokens]

No corpus Brown, essa baseline atinge ~85% de acurácia. Não é boa, mas é o piso abaixo do qual nenhum modelo sério deveria cair.

Passo 2: marcador HMM de bigramas

Modele a probabilidade conjunta da sequência:

P(tags, words) = prod P(tag_i | tag_{i-1}) * P(word_i | tag_i)

Duas tabelas: probabilidades de transição (tag dada a tag anterior), probabilidades de emissão (palavra dada a tag). Estime ambas a partir de contagens com suavização de Laplace. Decodifique com Viterbi (programação dinâmica sobre o reticulado de tags).

import math


def train_hmm(train_examples, alpha=0.01):
    transitions = defaultdict(Counter)
    emissions = defaultdict(Counter)
    tags = set()
    vocab = set()

    for tokens, ts in train_examples:
        prev = "<BOS>"
        for token, tag in zip(tokens, ts):
            transitions[prev][tag] += 1
            emissions[tag][token.lower()] += 1
            tags.add(tag)
            vocab.add(token.lower())
            prev = tag
        transitions[prev]["<EOS>"] += 1

    return transitions, emissions, tags, vocab


def log_prob(table, given, key, smooth_denom, alpha):
    return math.log((table[given].get(key, 0) + alpha) / smooth_denom)


def viterbi(tokens, transitions, emissions, tags, vocab, alpha=0.01):
    tags_list = list(tags)
    n = len(tokens)
    V = [[0.0] * len(tags_list) for _ in range(n)]
    back = [[0] * len(tags_list) for _ in range(n)]

    for j, tag in enumerate(tags_list):
        em_denom = sum(emissions[tag].values()) + alpha * (len(vocab) + 1)
        tr_denom = sum(transitions["<BOS>"].values()) + alpha * (len(tags_list) + 1)
        tr = log_prob(transitions, "<BOS>", tag, tr_denom, alpha)
        em = log_prob(emissions, tag, tokens[0].lower(), em_denom, alpha)
        V[0][j] = tr + em
        back[0][j] = 0

    for i in range(1, n):
        for j, tag in enumerate(tags_list):
            em_denom = sum(emissions[tag].values()) + alpha * (len(vocab) + 1)
            em = log_prob(emissions, tag, tokens[i].lower(), em_denom, alpha)
            best_prev = 0
            best_score = -1e30
            for k, prev_tag in enumerate(tags_list):
                tr_denom = sum(transitions[prev_tag].values()) + alpha * (len(tags_list) + 1)
                tr = log_prob(transitions, prev_tag, tag, tr_denom, alpha)
                score = V[i - 1][k] + tr + em
                if score > best_score:
                    best_score = score
                    best_prev = k
            V[i][j] = best_score
            back[i][j] = best_prev

    last_best = max(range(len(tags_list)), key=lambda j: V[n - 1][j])
    path = [last_best]
    for i in range(n - 1, 0, -1):
        path.append(back[i][path[-1]])
    return [tags_list[j] for j in reversed(path)]

O HMM de bigramas no Brown atinge ~93% de acurácia. O salto de 85% para 93% vem principalmente das probabilidades de transição — o modelo aprende que DET NOUN é comum e NOUN DET é raro.

Passo 3: por que os marcadores modernos superam isso

As probabilidades de transição + emissão são locais. Elas não conseguem capturar que saw é um substantivo em "I bought a saw" mas um verbo em "I saw the movie". Um CRF com features arbitrárias (sufixo, formato da palavra, palavra anterior e posterior, a própria palavra) atinge ~97%. Um BiLSTM-CRF ou transformer atinge ~98%+.

O teto desta tarefa é definido pela discordância entre anotadores. Anotadores humanos concordam cerca de 97% das vezes no Penn Treebank. Modelos acima de 98% provavelmente estão sofrendo overfitting no conjunto de teste.

Passo 4: esboço da análise de dependência

A análise de dependência completa a partir do zero está fora de escopo; o tratamento canônico de livro-texto está em Jurafsky e Martin. Duas famílias clássicas para conhecer:

  • Parsers baseados em transições (arc-eager, arc-standard) agem como um parser shift-reduce: leem tokens, empilham-nos numa pilha e aplicam ações de redução que criam arcos. A decodificação gulosa é rápida. A implementação clássica é o MaltParser. Versão neural moderna: o parser baseado em transições de Chen e Manning.
  • Parsers baseados em grafos (algoritmo de Eisner, biafim de Dozat-Manning) pontuam cada possível aresta cabeça-dependente e escolhem a árvore geradora máxima. Mais lentos, mas mais precisos.

Para a maior parte do trabalho aplicado, chame o spaCy:

import spacy

nlp = spacy.load("en_core_web_sm")
doc = nlp("The cats were running at 3pm.")
for token in doc:
    print(f"{token.text:10s} tag={token.tag_:5s} pos={token.pos_:6s} dep={token.dep_:10s} head={token.head.text}")
The        tag=DT    pos=DET    dep=det        head=cats
cats       tag=NNS   pos=NOUN   dep=nsubj      head=running
were       tag=VBD   pos=AUX    dep=aux        head=running
running    tag=VBG   pos=VERB   dep=ROOT       head=running
at         tag=IN    pos=ADP    dep=prep       head=running
3pm        tag=NN    pos=NOUN   dep=pobj       head=at
.          tag=.     pos=PUNCT  dep=punct      head=running

Leia a coluna dep de baixo para cima e a estrutura gramatical da frase se revela.

Use

Toda biblioteca de PLN de produção entrega parsers de POS e dependência como parte de um pipeline padrão.

  • spaCy (en_core_web_sm / md / lg / trf). Rápido, preciso, integrado com tokenização + NER + lematização. token.tag_ (Penn), token.pos_ (UD), token.dep_ (relação de dependência).
  • Stanford NLP (stanza). Sucessor do CoreNLP da Stanford. Estado da arte em mais de 60 idiomas.
  • trankit. Baseado em transformers, boa acurácia em UD.
  • NLTK. pos_tag. Utilizável, lento, mais antigo. Adequado para ensino.

Onde isto ainda importa em 2026

  • Lematização. A lição 01 precisa de POS para lematizar corretamente. Sempre.
  • Extração estruturada de saídas de LLM. Valide que uma frase gerada respeita restrições gramaticais (por exemplo, concordância sujeito-verbo, modificadores obrigatórios).
  • Análise de sentimento baseada em aspectos. As análises de dependência dizem qual adjetivo modifica qual substantivo.
  • Compreensão de consultas. "filmes dirigidos por Wes Anderson estrelando Bill Murray" se decompõe em restrições estruturadas via a análise.
  • Transferência multilíngue. As tags UD e relações de dependência são independentes de idioma, permitindo análise estruturada zero-shot de novos idiomas.
  • Pipelines de baixo poder computacional. Se você não puder enviar um transformer, POS + análise de dependência + gazetteer leva você surpreendentemente longe.

Entregue

Salve como outputs/skill-grammar-pipeline.md:

---
name: grammar-pipeline
description: Design a classical POS + dependency pipeline for a downstream NLP task.
version: 1.0.0
phase: 5
lesson: 07
tags: [nlp, pos, parsing]
---

Given a downstream task (information extraction, rewrite validation, query decomposition, lemmatization), you output:

1. Tagset to use. Penn Treebank for English-only legacy pipelines, Universal Dependencies for multilingual or cross-lingual.
2. Library. spaCy for most production, stanza for academic-grade multilingual, trankit for highest UD accuracy. Name the specific model ID.
3. Integration pattern. Show the 3-5 lines that call the library and consume the needed attributes (`.pos_`, `.dep_`, `.head`).
4. Failure mode to test. Noun-verb ambiguity (`saw`, `book`, `can`) and PP-attachment ambiguity are the classical traps. Sample 20 outputs and eyeball.

Refuse to recommend rolling your own parser. Building parsers from scratch is a research project, not an application task. Flag any pipeline that consumes POS tags without handling lowercase/uppercase variants as fragile.

Exercícios

  1. Fácil. Usando a baseline da tag mais frequente em um pequeno corpus marcado (por exemplo, o subconjunto Brown do NLTK), meça a acurácia em frases reservadas para teste. Verifique o resultado de ~85%.
  2. Médio. Treine o HMM de bigramas acima e reporte precisão/recall por tag. Quais tags o HMM mais confunde?
  3. Difícil. Use a análise de dependência do spaCy para extrair triplas sujeito-verbo-objeto de uma amostra de 1000 frases. Avalie em 50 triplas rotuladas manualmente. Documente onde a extração falha (frequentemente em voz passiva, coordenações e sujeitos elípticos).

Termos-Chave

Termo O que as pessoas dizem O que realmente significa
Tag POS Tipo da palavra Categoria gramatical. O PTB tem 36; o UD tem 17.
Penn Treebank Conjunto de tags padrão Específico do inglês. Tempos verbais e número de substantivos de granularidade fina.
Universal Dependencies Conjunto de tags multilíngue Mais grosseiro que o PTB; neutro em relação ao idioma; padrão para trabalho multilíngue.
Análise de dependência Árvore da frase Cada palavra tem uma cabeça, cada aresta tem uma relação gramatical.
Viterbi Programação dinâmica Encontra a sequência de tags de maior probabilidade dadas emissões e transições.

Leitura Adicional

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).