개체명 인식(Named Entity Recognition; NER)

이름을 뽑아내는 일입니다. 경계가 애매한 표현, 중첩된 개체(nested entities), 도메인 전문 용어(domain jargon)를 마주하기 전까지는 쉬워 보입니다.

유형: Build 언어: Python 선수 지식: Phase 5 · 02 (BoW + TF-IDF), Phase 5 · 03 (Word Embeddings) 예상 시간: 약 75분

학습 목표

  • 개체명 인식(NER)을 시퀀스 라벨링(sequence labeling) 문제로 표현합니다.
  • BIO/BILOU 태깅 스킴(tagging scheme)과 개체 단위 F1(entity-level F1)을 이해합니다.
  • 규칙 기반(rule-based), HMM, CRF, BiLSTM-CRF, 트랜스포머(transformer) 기반 NER의 차이를 설명합니다.
  • 중첩 개체(nested entity), 도메인 변화(domain shift), 희소 타입(sparse type) 같은 실제 운영 환경의 실패 양상을 식별합니다.

문제

Apple sued Google over its iPhone search deal in the US.라는 문장을 봅시다. 여기에는 다섯 개의 개체(entity)가 있습니다. Apple(ORG), Google(ORG), iPhone(PRODUCT), search deal(애매함), US(GPE)입니다. 좋은 NER 시스템은 이 개체들을 모두 올바른 타입과 함께 추출합니다. 나쁜 시스템은 iPhone을 놓치거나, 과일 Apple과 회사 Apple을 혼동하거나, US를 PERSON으로 잘못 라벨링합니다.

NER은 모든 구조화 추출(structured extraction) 파이프라인의 바닥에서 묵묵히 일하는 일꾼(workhorse)입니다. 이력서 파싱(resume parsing), 컴플라이언스 로그 스캔(compliance log scanning), 의료 기록 익명화(medical record anonymization), 검색 질의 이해(search query understanding), 챗봇 응답 근거 제공(grounding for chatbot responses), 법률 계약서 추출(legal contract extraction) 모두가 NER에 기댑니다. 평소에는 잘 보이지 않지만 거의 언제나 의존하고 있습니다.

이 강의는 규칙 기반, HMM, CRF로 이어지는 고전적인 경로(classical path)에서 시작해 BiLSTM-CRF, 그리고 트랜스포머로 이어지는 현대적 경로(modern path)까지 따라갑니다. 각 단계는 이전 단계의 구체적인 한계를 하나씩 해결합니다. 이 진화의 패턴 자체가 이 강의의 핵심입니다.

사전 테스트

2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요

1.NER이 문서 분류(document classification)가 아닌 시퀀스 라벨링(sequence labeling) 문제로 표현되는 이유는 무엇이며, 이 구분이 개체 추출에 왜 중요한가요?

2.BIO 태깅에서 B-ORG와 I-ORG 태그를 구분하는 것이, 개체 토큰에 단순히 ORG만 붙이는 것과 비교해 어떤 역할을 하나요?

0/2 답변 완료

개념

NER tagging: BIO schema + CRF+BiLSTM pipeline

BIO 태깅(BIO tagging)(또는 BILOU)은 개체 추출(entity extraction)을 시퀀스 라벨링 문제로 바꿉니다. 각 토큰(token)에 B-TYPE(개체 시작), I-TYPE(개체 내부), O(어떤 개체에도 속하지 않음)을 붙입니다.

Apple    B-ORG
sued     O
Google   B-ORG
over     O
its      O
iPhone   B-PRODUCT
search   O
deal     O
in       O
the      O
US       B-GPE
.        O

여러 토큰으로 이루어진 개체는 New B-GPE, York I-GPE, City I-GPE처럼 이어 붙입니다. BIO를 이해하는 모델은 임의 길이의 스팬(span)을 추출할 수 있습니다.

아키텍처의 발전 단계는 다음과 같습니다.

  • 규칙 기반(Rule-based). 정규표현식(regex)과 지명사전(gazetteer) 조회를 사용합니다. 이미 알고 있는 개체에 대해서는 정밀도(precision)가 매우 높지만, 새로운 개체에 대한 커버리지(coverage)는 사실상 없습니다.
  • HMM(Hidden Markov Model; 은닉 마르코프 모델). 태그(tag)가 주어졌을 때 토큰의 방출 확률(emission probability)과 태그 간 전이 확률(transition probability)을 사용하고, 비터비 알고리즘(Viterbi decode)으로 라벨 시퀀스를 찾습니다. 라벨이 붙은 데이터로 학습합니다.
  • CRF(Conditional Random Field; 조건부 무작위장). HMM과 비슷하게 시퀀스를 다루지만 판별 모델(discriminative model)이라 단어 모양(word shape), 대소문자(capitalization), 주변 단어 같은 임의의 특성(feature)을 자유롭게 섞어 쓸 수 있습니다. 2026년에도 자원이 부족한 환경(low-resource deployment)에서는 여전히 고전적인 운영용 일꾼으로 자리 잡고 있습니다.
  • BiLSTM-CRF. 수작업으로 만든 특성(hand-crafted features) 대신 신경망이 학습한 특성(neural features)을 사용합니다. LSTM이 문장을 양방향으로 읽고, 그 위에 올린 CRF 레이어가 일관된 태그 시퀀스를 강제합니다.
  • 트랜스포머 기반(Transformer-based). BERT에 토큰 분류 헤드(token-classification head)를 붙여 미세 조정(fine-tune)합니다. 정확도는 가장 높지만 연산량(compute)도 가장 큽니다.

직접 만들기

Step 1: BIO 태깅 헬퍼

def spans_to_bio(tokens, spans):
    labels = ["O"] * len(tokens)
    for start, end, label in spans:
        labels[start] = f"B-{label}"
        for i in range(start + 1, end):
            labels[i] = f"I-{label}"
    return labels


def bio_to_spans(tokens, labels):
    spans = []
    current = None
    for i, label in enumerate(labels):
        if label.startswith("B-"):
            if current:
                spans.append(current)
            current = (i, i + 1, label[2:])
        elif label.startswith("I-") and current and current[2] == label[2:]:
            current = (current[0], i + 1, current[2])
        else:
            if current:
                spans.append(current)
                current = None
    if current:
        spans.append(current)
    return spans
>>> tokens = ["Apple", "sued", "Google", "over", "iPhone", "sales", "."]
>>> labels = ["B-ORG", "O", "B-ORG", "O", "B-PRODUCT", "O", "O"]
>>> bio_to_spans(tokens, labels)
[(0, 1, 'ORG'), (2, 3, 'ORG'), (4, 5, 'PRODUCT')]

Step 2: 수작업으로 만든 특성(hand-crafted features)

신경망을 쓰지 않는 고전적인 NER에서는 특성 설계가 곧 승부수입니다. 유용한 특성의 예시는 다음과 같습니다.

def token_features(token, prev_token, next_token):
    return {
        "lower": token.lower(),
        "is_upper": token.isupper(),
        "is_title": token.istitle(),
        "has_digit": any(c.isdigit() for c in token),
        "suffix_3": token[-3:].lower(),
        "shape": word_shape(token),
        "prev_lower": prev_token.lower() if prev_token else "<BOS>",
        "next_lower": next_token.lower() if next_token else "<EOS>",
    }


def word_shape(word):
    out = []
    for c in word:
        if c.isupper():
            out.append("X")
        elif c.islower():
            out.append("x")
        elif c.isdigit():
            out.append("d")
        else:
            out.append(c)
    return "".join(out)

word_shape("iPhone")xXxxxx를 반환하고, word_shape("USA-2024")XXX-dddd를 반환합니다. 대소문자 패턴은 고유명사(proper noun)를 식별하는 데 강한 신호가 됩니다.

Step 3: 단순한 규칙 기반 + 사전 베이스라인

ORG_GAZETTEER = {"Apple", "Google", "Microsoft", "OpenAI", "Meta", "Amazon", "Netflix"}
GPE_GAZETTEER = {"US", "USA", "UK", "India", "Germany", "France"}
PRODUCT_GAZETTEER = {"iPhone", "Android", "Windows", "ChatGPT", "Claude"}


def rule_based_ner(tokens):
    labels = []
    for token in tokens:
        if token in ORG_GAZETTEER:
            labels.append("B-ORG")
        elif token in GPE_GAZETTEER:
            labels.append("B-GPE")
        elif token in PRODUCT_GAZETTEER:
            labels.append("B-PRODUCT")
        else:
            labels.append("O")
    return labels

운영 환경에서 쓰는 지명사전은 위키피디아(Wikipedia)와 디비피디아(DBpedia)에서 수집한 수백만 개의 항목을 가지고 있습니다. 커버리지는 훌륭하지만, 모호성 해소(disambiguation)는 형편없습니다. 회사 Apple과 과일 apple을 구분하기 어렵습니다. 바로 이런 이유로 통계적 모델(statistical model)이 결국 승리한 것입니다.

Step 4: CRF 단계 (전체 구현이 아닌 스케치)

CRF를 처음부터 50줄로 작성하는 일은 확률 이론(probability theory)에 대한 기반이 없으면 그다지 깨달음을 주지 못합니다. 대신 sklearn-crfsuite를 사용합니다.

import sklearn_crfsuite

def to_features(tokens):
    out = []
    for i, tok in enumerate(tokens):
        prev = tokens[i - 1] if i > 0 else ""
        nxt = tokens[i + 1] if i + 1 < len(tokens) else ""
        out.append({
            "word.lower()": tok.lower(),
            "word.isupper()": tok.isupper(),
            "word.istitle()": tok.istitle(),
            "word.isdigit()": tok.isdigit(),
            "word.suffix3": tok[-3:].lower(),
            "word.shape": word_shape(tok),
            "prev.word.lower()": prev.lower(),
            "next.word.lower()": nxt.lower(),
            "BOS": i == 0,
            "EOS": i == len(tokens) - 1,
        })
    return out


crf = sklearn_crfsuite.CRF(algorithm="lbfgs", c1=0.1, c2=0.1, max_iterations=100, all_possible_transitions=True)
X_train = [to_features(s) for s in sentences_tokenized]
crf.fit(X_train, bio_labels_train)

c1c2는 각각 L1, L2 정규화(regularization) 계수입니다. all_possible_transitions=TrueO 뒤에 I-ORG가 오는 것처럼 잘못된 시퀀스가 낮은 확률을 가지도록 모델이 학습할 수 있게 합니다. 이런 식으로 BIO의 일관성을 직접 제약으로 적지 않아도 CRF가 알아서 지키도록 만들 수 있습니다.

Step 5: BiLSTM-CRF가 더해 주는 것

특성이 손으로 만든 것에서 학습된 것(learned features)으로 바뀝니다. 입력은 토큰 임베딩(token embedding)이며, GloVe나 fastText 같은 사전 학습 임베딩을 씁니다. LSTM이 문장을 왼쪽에서 오른쪽으로, 그리고 오른쪽에서 왼쪽으로 읽습니다. 두 방향의 은닉 상태(hidden state)를 연결(concatenate)해 CRF 출력 레이어로 흘려보냅니다. CRF는 여전히 태그 시퀀스의 일관성을 강제하고, LSTM은 수작업 특성을 학습된 표현으로 대체합니다.

import torch
import torch.nn as nn


class BiLSTM_CRF_Head(nn.Module):
    def __init__(self, vocab_size, embed_dim, hidden_dim, n_labels):
        super().__init__()
        self.embed = nn.Embedding(vocab_size, embed_dim)
        self.lstm = nn.LSTM(embed_dim, hidden_dim, bidirectional=True, batch_first=True)
        self.fc = nn.Linear(hidden_dim * 2, n_labels)

    def forward(self, token_ids):
        e = self.embed(token_ids)
        h, _ = self.lstm(e)
        emissions = self.fc(h)
        return emissions

CRF 레이어 자체는 torchcrf.CRF를 사용합니다(pip install pytorch-crf). 수작업 특성 기반 CRF 대비 성능 향상(gain)은 측정 가능한 수준이지만, 라벨이 붙은 문장이 수만 개 이상 없다면 기대만큼 크지는 않습니다.

사용해보기

spaCy는 운영 수준의 NER을 기본으로 제공합니다.

import spacy

nlp = spacy.load("en_core_web_sm")
doc = nlp("Apple sued Google over its iPhone search deal in the US.")
for ent in doc.ents:
    print(f"{ent.text:20s} {ent.label_}")
Apple                ORG
Google               ORG
iPhone               ORG
US                   GPE

여기서 iPhone이 PRODUCT가 아니라 ORG로 라벨링된 점을 주목하세요. spaCy의 소형 모델은 제품 개체(product entity)에 대한 커버리지가 약합니다. 대형 모델인 en_core_web_lg가 더 잘하고, 트랜스포머 모델인 en_core_web_trf는 그보다도 더 잘합니다.

Hugging Face의 BERT 기반 NER 사용 예는 다음과 같습니다.

from transformers import pipeline

ner = pipeline("ner", model="dslim/bert-base-NER", aggregation_strategy="simple")
print(ner("Apple sued Google over its iPhone in the US."))
[{'entity_group': 'ORG', 'word': 'Apple', ...},
 {'entity_group': 'ORG', 'word': 'Google', ...},
 {'entity_group': 'MISC', 'word': 'iPhone', ...},
 {'entity_group': 'LOC', 'word': 'US', ...}]

aggregation_strategy="simple"은 인접한 B-X, I-X 토큰을 하나의 스팬으로 병합합니다. 이 옵션이 없으면 토큰 단위 라벨이 그대로 나오고, 사용자가 직접 병합해야 합니다.

LLM 기반 NER (2026년의 선택지)

2026년 기준 영샷(zero-shot)과 퓨샷(few-shot) LLM NER은 많은 도메인에서 미세 조정된 모델과 경쟁 가능한 수준이며, 라벨링된 데이터가 부족할 때는 특히 뛰어납니다.

  • 영샷 프롬프팅(Zero-shot prompting). LLM에게 개체 타입 목록과 예시 스키마를 주고, JSON 형식의 출력을 요청합니다. 별다른 준비 없이도 동작하지만, 처음 보는 도메인에서는 정확도가 보통 수준입니다.
  • ZeroTuneBio 방식의 프롬프팅. 작업을 후보 추출(candidate extraction), 의미 설명(meaning explanation), 판단(judgment), 재검토(re-check)로 분해합니다. 단일 호출이 아니라 다단계 프롬프트(multi-stage prompt)를 쓰면 생의학(biomedical) NER에서 정확도가 크게 올라갑니다. 동일한 패턴이 법률, 금융, 과학 도메인에도 적용됩니다.
  • RAG 기반 동적 프롬프팅(Dynamic prompting with RAG). 작은 라벨링된 시드(seed) 데이터셋에서 가장 유사한 예시를 검색해, 추론 호출마다 즉석에서 퓨샷 프롬프트를 구성합니다. 2026년 벤치마크에서 이 방식은 정적 프롬프팅 대비 GPT-4의 생의학 NER F1을 11~12% 끌어올렸습니다.
  • 개체 타입별 분해(Per-entity-type decomposition). 긴 문서에서는 모든 타입을 한 번에 추출하려는 단일 호출이 길어질수록 재현율(recall)을 잃습니다. 타입별로 한 번씩 추출 패스를 돌립니다. 추론 비용은 늘지만 정확도는 크게 올라갑니다. 임상 노트(clinical notes)와 법률 계약서 처리의 표준 패턴입니다.

2026년 기준 운영 권장 사항은, 학습 데이터를 수집하기 전에 먼저 LLM 영샷 베이스라인부터 만들어 보는 것입니다. 많은 경우 미세 조정이 필요 없을 정도로 F1이 충분히 잘 나옵니다.

고전 NER이 여전히 이기는 곳

LLM이 있어도, 다음 상황에서는 고전 NER이 이깁니다.

  • 지연 시간 예산(latency budget)이 50ms 이하입니다.
  • 라벨링된 예시가 수천 개 있고 98%+ F1이 필요합니다.
  • 도메인 온톨로지(ontology)가 안정적이라 사전 학습된 CRF나 BiLSTM이 잘 전이(transfer)됩니다.
  • 규제(regulatory) 제약 때문에 사내 설치(on-prem)이면서 비생성형(non-generative) 모델이 필요합니다.

무너지는 지점

  • 도메인 변화(Domain shift). CoNLL로 학습한 NER을 법률 계약서에 적용하면 지명사전보다도 성능이 떨어집니다. 자신의 도메인 데이터로 미세 조정하세요.
  • 중첩 개체(Nested entities). "Bank of America Tower"는 동시에 ORG이자 FACILITY입니다. 표준 BIO로는 겹치는 스팬을 표현할 수 없습니다. 중첩 NER(nested NER)이 필요하며, 다중 패스(multi-pass) 모델이나 스팬 기반(span-based) 모델을 사용해야 합니다.
  • 긴 개체(Long entities). "United States Federal Deposit Insurance Corporation." 같은 긴 기관명을 토큰 단위 모델이 가끔 쪼개 버립니다. aggregation_strategy를 사용하거나 후처리(post-process)로 보완하세요.
  • 희소 타입(Sparse types). DRUG_BRAND, ADVERSE_EVENT, DOSE 같은 의료 NER 라벨은 범용 모델이 거의 모릅니다. scispaCy와 BioBERT가 출발점입니다.

산출물 만들기

outputs/skill-ner-picker.md로 저장합니다.

---
name: ner-picker
description: Pick the right NER approach for a given extraction task.
version: 1.0.0
phase: 5
lesson: 06
tags: [nlp, ner, extraction]
---

Given a task description (domain, label set, language, latency, data volume), output:

Guide the student in Korean.

1. Approach. Rule-based + gazetteer, CRF, BiLSTM-CRF, or transformer fine-tune.
2. Starting model. Name it (spaCy model ID, Hugging Face checkpoint ID, or "custom, trained from scratch").
3. Labeling strategy. BIO, BILOU, or span-based. Justify in one sentence.
4. Evaluation. Use `seqeval`. Always report entity-level F1 (not token-level).

Refuse to recommend fine-tuning a transformer for under 500 labeled examples unless the user already has a pretrained domain model. Flag nested entities as needing span-based or multi-pass models. Require a gazetteer audit if the user mentions "production scale" and labels are unchanged from CoNLL-2003.

연습문제

  1. 쉬움. spans_to_bio의 역함수(inverse)인 bio_to_spans를 구현하고, 10개 문장에서 왕복(round-trip) 일관성을 검증합니다.
  2. 중간. 위의 sklearn-crfsuite CRF를 CoNLL-2003 영어 NER 데이터셋으로 학습합니다. seqeval을 사용해 개체별 F1을 보고합니다. 일반적인 결과는 약 84 F1 수준입니다.
  3. 어려움. 의료, 법률, 금융 같은 도메인 특화 NER 데이터셋에서 distilbert-base-cased를 미세 조정합니다. spaCy 소형 모델과 비교하고, 데이터 누수(data leakage) 점검 절차와 예상과 달랐던 점을 문서로 정리합니다.

핵심 용어

용어흔한 설명실제 의미
NER(Named Entity Recognition; 개체명 인식)이름을 추출하는 일토큰 스팬에 PERSON, ORG, GPE, DATE 같은 타입을 라벨링합니다.
BIO태깅 스킴B-X는 시작, I-X는 이어짐, O는 어떤 개체에도 속하지 않음을 뜻합니다.
BILOU더 세밀한 BIOL-X(last)와 U-X(unit)를 추가해 개체 경계를 더 깔끔하게 표현합니다.
CRF(Conditional Random Field; 조건부 무작위장)구조화 분류기(structured classifier)방출 확률뿐 아니라 라벨 간 전이(transition)를 모델링해 유효한 시퀀스를 강제합니다.
중첩 NER(Nested NER)겹치는 개체어떤 스팬과 그 부분 스팬(sub-span)이 서로 다른 개체일 수 있습니다. BIO만으로는 표현할 수 없습니다.
개체 단위 F1(Entity-level F1)올바른 NER 평가 지표예측한 스팬이 정답 스팬과 정확히 일치해야 합니다. 토큰 단위 F1은 정확도를 과대평가합니다.

더 읽을거리

실습 코드

이 강의의 실습 코드 1개

main
Code

산출물

이 강의에서 생성된 프롬프트, 스킬, 코드 산출물 1개

ner-picker

Pick the right NER approach for a given extraction task.

Skill

확인 문제

3문제 · 모두 맞추면 완료 표시가 가능합니다

1.지명사전(gazetteer)을 사용하는 규칙 기반 NER 시스템이 정밀도(precision) 95%이지만 재현율(recall) 30%입니다. CRF를 추가하면 정밀도를 크게 떨어뜨리지 않으면서 재현율이 향상되는 이유는 무엇인가요?

2.CoNLL-2003 뉴스 데이터셋으로 학습한 NER 모델을 법률 계약서에 배포했더니 개체 단위 F1(entity-level F1)이 90%에서 45%로 떨어졌습니다. 가장 가능성 높은 원인과 적절한 해결책은 무엇인가요?

3.'Bank of America Tower'는 동시에 ORG이자 FACILITY입니다. 표준 BIO 태깅으로 이를 표현할 수 없는 이유와 필요한 접근법은 무엇인가요?

0/3 답변 완료

추가 문제 풀기

AI가 강의 내용을 바탕으로 새로운 문제를 생성합니다