품사 태깅과 구문 분석(POS Tagging and Syntactic Parsing)
한동안 문법은 유행이 아니었습니다. 그러다 모든 LLM 파이프라인이 구조화 추출(structured extraction)을 검증해야 하게 되면서 다시 돌아왔습니다.
유형: Build
언어: Python
선수 지식: Phase 5 · 01 (Text Processing), Phase 2 · 14 (Naive Bayes)
예상 시간: 약 45분
학습 목표
- 품사 태깅(POS tagging)과 구문 분석(syntactic parsing)의 역할을 설명합니다.
- 펜 트리뱅크(Penn Treebank; PTB)와 유니버설 디펜던시(Universal Dependencies; UD) 태그셋(tagset)의 차이를 이해합니다.
- 최빈 태그 기준선(most-frequent-tag baseline)과 바이그램 은닉 마르코프 모델(bigram HMM) 태거를 구현합니다.
- 의존 구문 분석(dependency parse)이 표제어 추출(lemmatization), 구조화 추출(structured extraction), 질의 이해(query understanding)에 어떻게 쓰이는지 파악합니다.
문제
Lesson 01에서는 표제어 추출(lemmatization)에 품사 태그(part-of-speech tag)가 필요하다고 했습니다. running이 동사라는 사실을 모르면 표제어 추출기(lemmatizer)는 이를 run으로 줄일 수 없습니다. better가 형용사라는 사실을 모르면 good으로 돌릴 수 없습니다.
이 약속 뒤에는 하나의 큰 하위 분야가 있습니다. 품사 태깅은 단어에 문법 범주(grammatical category)를 붙입니다. 구문 분석은 문장의 트리 구조(tree structure)를 복원합니다. 어떤 단어가 어떤 단어를 수식하는지, 어떤 동사가 어떤 논항(argument)을 지배하는지를 찾습니다.
딥러닝(deep learning) 이후 연구의 중심은 사전학습 트랜스포머(pretrained transformer) 위에서 동작하는 토큰 분류(token classification) 과제로 옮겨갔지만, 응용 자연어 처리(applied NLP)에서는 여전히 품사와 의존 트리(dependency tree)가 중요합니다. LLM이 생성한 JSON을 문법 제약(grammatical constraint)으로 검증하고, 질의응답 시스템(question-answering system)은 의존 구문 분석으로 질의를 분해하며, 기계 번역(machine translation) 평가기는 파스 트리(parse tree)의 정렬을 확인합니다.
개념

품사 태깅은 각 토큰에 문법 범주를 붙입니다. 펜 트리뱅크(Penn Treebank; PTB) 태그셋은 영어의 기본 태그셋입니다. NN 단수 명사, NNS 복수 명사, NNP 고유 명사, VBD 과거형 동사처럼 세밀합니다. 유니버설 디펜던시(Universal Dependencies; UD) 태그셋은 더 거칠지만 언어에 종속되지 않으며(language-agnostic) 다국어(cross-lingual) 작업에서 기본값이 되었습니다.
The/DET cats/NOUN were/AUX running/VERB at/ADP 3pm/NOUN ./PUNCT
구문 분석은 트리를 만듭니다. 두 가지 큰 방식이 있습니다.
- 구성 구문 분석(Constituency parsing). 명사구(noun phrase), 동사구(verb phrase), 전치사구(prepositional phrase)가 서로 중첩됩니다. 출력은 비단말 범주(NP, VP, PP)로 이루어진 트리이며, 단어가 잎(leaf)에 위치합니다.
- 의존 구문 분석(Dependency parsing). 각 단어는 하나의 머리 단어(head word)에 의존하며, 간선(edge)에는 문법 관계(grammatical relation) 레이블이 붙습니다. 출력은 모든 간선이 (머리, 의존어, 관계) 삼중쌍인 트리입니다.
의존 구문 분석은 어순(word order)이 자유로운 언어에도 비교적 잘 일반화되어 2010년대 이후 널리 쓰였습니다.
running is ROOT
cats is nsubj of running
were is aux of running
at is prep of running
3pm is pobj of at
직접 만들기
Step 1: 최빈 태그 기준선(most-frequent-tag baseline)
가장 단순하지만 작동하는 품사 태거(POS tagger)입니다. 각 단어에 대해 학습 데이터에서 가장 자주 나타났던 태그를 예측합니다.
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]
브라운 코퍼스(Brown corpus)에서 이 기준선은 약 85%의 정확도(accuracy)에 도달합니다. 훌륭한 수치는 아니지만, 진지한 모델이라면 반드시 넘어야 할 하한선입니다.
Step 2: 바이그램 은닉 마르코프 모델(bigram HMM) 태거
태그 시퀀스(tag sequence)의 결합 확률(joint probability)을 다음과 같이 모델링합니다.
P(tags, words) = prod P(tag_i | tag_{i-1}) * P(word_i | tag_i)
필요한 표는 두 개입니다. 이전 태그가 주어졌을 때 다음 태그가 나올 전이 확률(transition probability), 태그가 주어졌을 때 단어가 나올 방출 확률(emission probability)입니다. 두 확률 모두 빈도 수에 라플라스 평활(Laplace smoothing)을 적용해 추정하고, 비터비(Viterbi) 알고리즘(태그 격자에 대한 동적 계획법; dynamic programming)으로 가장 가능성 높은 태그 시퀀스를 찾습니다.
바이그램 HMM은 브라운 코퍼스에서 약 93%의 정확도를 냅니다. 85%에서 93%로 뛰는 이유는 대부분 전이 확률에 있습니다. 모델이 DET NOUN은 흔하고 NOUN DET은 드물다는 사실을 배우기 때문입니다.
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)]
Step 3: 현대 태거가 이 모델을 앞서는 이유
전이 확률과 방출 확률은 지역적(local)입니다. I bought a saw에서 saw는 명사이고 I saw the movie에서는 동사라는 점을 충분히 포착하기 어렵습니다. 접미사(suffix), 단어 형태(word shape), 앞뒤 단어, 단어 자체 같은 임의의 자질(feature)을 사용하는 CRF는 약 97%에 도달합니다. BiLSTM-CRF나 트랜스포머(transformer)는 98% 이상까지 갑니다.
다만 이 과제의 상한선(ceiling)은 어노테이터 간 불일치(annotator disagreement)로 정해집니다. 펜 트리뱅크에서 사람 어노테이터의 일치율(agreement)이 약 97%라면, 98%를 넘는 모델은 테스트 셋(test set)에 과적합(overfit)하고 있을 가능성도 있습니다.
Step 4: 의존 구문 분석 개관(dependency parsing sketch)
의존 구문 분석을 처음부터 구현하는 일은 이 강의 범위를 벗어납니다. 표준 교과서로는 저러프스키와 마틴(Jurafsky and Martin)의 정통 교재가 있습니다. 알아둘 고전적 계열은 두 가지입니다.
- 전이 기반 파서(transition-based parser). arc-eager, arc-standard 같은 방식이 있으며, 쉬프트-리듀스 파서(shift-reduce parser)처럼 토큰을 읽어 스택(stack)에 올리고 호(arc)를 만드는 리듀스(reduce) 동작을 적용합니다. 그리디 디코딩(greedy decoding)이 빠릅니다. 고전 구현으로는 MaltParser가 있고, 현대 신경망 버전으로는 Chen과 Manning의 전이 기반 파서가 있습니다.
- 그래프 기반 파서(graph-based parser). Eisner 알고리즘, Dozat-Manning biaffine 등이 있으며, 가능한 모든 머리-의존어(head-dependent) 간선을 점수화한 뒤 최대 신장 트리(maximum spanning tree)를 고릅니다. 느리지만 더 정확한 편입니다.
대부분의 응용 작업에서는 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
dep 열을 아래에서 위로 읽으면 문장의 문법 구조가 드러납니다.
사용하기
상용 자연어 처리 라이브러리는 대부분 품사 태거와 의존 구문 분석기를 표준 파이프라인으로 제공합니다.
- spaCy (
en_core_web_sm / md / lg / trf). 빠르고 정확하며 토크나이즈(tokenization), 개체명 인식(NER), 표제어 추출(lemmatization)과 통합되어 있습니다. token.tag_는 펜 트리뱅크, token.pos_는 유니버설 디펜던시, token.dep_는 의존 관계입니다.
- Stanford NLP(stanza). CoreNLP의 후속이며 60개 이상의 언어에서 최신 수준(state-of-the-art) 성능을 보입니다.
- trankit. 트랜스포머 기반이며 유니버설 디펜던시 정확도가 좋습니다.
- NLTK.
pos_tag 함수를 제공합니다. 사용 가능하지만 느리고 오래된 편이며 교육용으로는 충분합니다.
2026년에도 중요한 이유
- 표제어 추출(lemmatization). Lesson 01에서 본 것처럼, 정확한 표제어 추출을 위해서는 품사가 반드시 필요합니다.
- LLM 출력의 구조화 검증. 생성된 문장이 주어-동사 일치(subject-verb agreement)나 필수 수식어 같은 문법 제약을 만족하는지 확인합니다.
- 속성 기반 감성 분석(aspect-based sentiment). 의존 구문 분석으로 어떤 형용사가 어떤 명사를 수식하는지 찾습니다.
- 질의 이해(query understanding). "movies directed by Wes Anderson starring Bill Murray" 같은 자연어 질의를 파스 결과를 통해 구조화 제약(structured constraint)으로 분해합니다.
- 다국어 전이(cross-lingual transfer). 유니버설 디펜던시 태그와 의존 관계는 언어에 종속되지 않아 새로운 언어에 대해 제로샷(zero-shot) 구조 분석이 가능합니다.
- 저비용 파이프라인(low-compute pipeline). 트랜스포머를 배포할 수 없을 때 품사 + 의존 구문 분석 + 가제티어(gazetteer) 조합이 의외로 멀리까지 갑니다.
산출물 만들기
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:
Guide the student in Korean.
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.
연습문제
- 쉬움. 작은 태그 부착 코퍼스(예: NLTK의 Brown 일부)에서 최빈 태그 기준선을 실행하고, 보류한 문장(held-out sentence)에 대한 정확도를 측정합니다. 약 85% 결과를 확인합니다.
- 중간. 위 바이그램 HMM을 학습하고 태그별 정밀도(precision)와 재현율(recall)을 보고합니다. 어떤 태그가 가장 많이 혼동되는지 확인합니다.
- 어려움. spaCy 의존 구문 분석을 사용해 1000개 문장에서 주어-동사-목적어(subject-verb-object) 삼중쌍을 추출합니다. 50개 삼중쌍을 직접 라벨링해 평가하고, 수동태(passive), 등위 접속(coordination), 생략된 주어(elided subject) 같은 실패 사례를 문서화합니다.
핵심 용어
| 용어 | 흔한 설명 | 실제 의미 |
|---|
| 품사 태그(POS tag) | 단어의 종류 | 문법 범주를 가리킵니다. PTB는 36개, UD는 17개입니다. |
| 펜 트리뱅크(Penn Treebank) | 표준 태그셋 | 영어 특화 태그셋입니다. 동사 시제와 명사 수를 세밀히 구분합니다. |
| 유니버설 디펜던시(Universal Dependencies) | 다국어 태그셋 | PTB보다 거칠지만 언어 중립적이며 다국어 작업의 기본값입니다. |
| 의존 구문 분석(Dependency parse) | 문장 트리 | 각 단어가 하나의 머리(head)를 갖고, 각 간선에 문법 관계가 붙습니다. |
| 비터비(Viterbi) | 동적 계획법 | 방출 확률과 전이 확률이 주어졌을 때 가장 확률 높은 태그 시퀀스를 찾습니다. |
더 읽을거리