튜토리얼
Week 4: 지식 추출

Week 4. 지식 추출 파이프라인

NER, 관계 추출, Entity Resolution, LLM 통합


학습 목표

이 튜토리얼을 완료하면 다음을 할 수 있습니다:

  • Named Entity Recognition (NER) 구현
  • Relation Extraction (RE) 수행
  • Entity Resolution (서로 다른 데이터에서 같은 대상 식별)
  1. LLM을 활용한 지식 추출 파이프라인 구축

핵심 개념

1. 지식 추출 개요

비정형 데이터의 구조화: "문서를 데이터로"

세상 지식의 대부분은 비정형 텍스트(PDF, 이메일, 뉴스 기사 등)에 잠겨 있습니다. 지식 그래프는 PDF를 직접 "읽을" 수 없습니다.

지식 추출(Knowledge Extraction) 은 자유 형식의 텍스트를 구조화된 트리플(Subject-Predicate-Object)로 변환하는 파이프라인입니다.

  • 원시 텍스트: "애플은 스티브 잡스에 의해 설립되었다."
  • 구조화된 데이터: (애플, 설립자, 스티브 잡스)

즉, "읽기"를 "데이터베이스 기록"으로 바꾸는 과정입니다.

원시 텍스트 → NER → Entity Linking → 관계 추출 → 지식 그래프
    │           │          │              │
    │           ↓          ↓              ↓
    │      [개체들]     [URIs]       [트리플]
    │       "놀란"   ex:Nolan   (Nolan, directed, Inception)

2. Named Entity Recognition (NER)

spaCy 기반 NER

import spacy
 
nlp = spacy.load("ko_core_news_lg")  # 한국어 모델
 
text = "크리스토퍼 놀란이 2010년에 인셉션을 감독했다."
doc = nlp(text)
 
for ent in doc.ents:
    print(f"{ent.text} ({ent.label_})")
# 크리스토퍼 놀란 (PERSON)
# 2010년 (DATE)
# 인셉션 (WORK_OF_ART)

LLM 기반 NER

from openai import OpenAI
 
client = OpenAI()
 
def extract_entities(text):
    response = client.chat.completions.create(
        model="gpt-4",
        messages=[{
            "role": "system",
            "content": """텍스트에서 개체를 추출하세요.
            JSON 형식: [{"text": "", "type": "", "description": ""}]
            유형: PERSON, MOVIE, ORGANIZATION, DATE, LOCATION"""
        }, {
            "role": "user",
            "content": text
        }]
    )
    return json.loads(response.choices[0].message.content)
 
entities = extract_entities("크리스토퍼 놀란이 워너 브라더스에서 인셉션을 감독했다.")
# [
#   {"text": "크리스토퍼 놀란", "type": "PERSON", "description": "감독"},
#   {"text": "인셉션", "type": "MOVIE", "description": "2010년 영화"},
#   {"text": "워너 브라더스", "type": "ORGANIZATION", "description": "제작사"}
# ]

3. 관계 추출

프롬프트 기반 추출

def extract_relations(text, entities):
    entity_list = ", ".join([e["text"] for e in entities])
 
    response = client.chat.completions.create(
        model="gpt-4",
        messages=[{
            "role": "system",
            "content": f"""주어진 개체: {entity_list}
            텍스트에서 관계를 추출하세요.
            JSON 형식: [{{"subject": "", "predicate": "", "object": ""}}]"""
        }, {
            "role": "user",
            "content": text
        }]
    )
    return json.loads(response.choices[0].message.content)
 
# 결과:
# [{"subject": "크리스토퍼 놀란", "predicate": "감독하다", "object": "인셉션"}]

Fine-tuned 모델 추출

from transformers import pipeline
 
# REBEL 모델 사용 (Relation Extraction By End-to-end Language generation)
triplet_extractor = pipeline(
    "text2text-generation",
    model="Babelscape/rebel-large",
    tokenizer="Babelscape/rebel-large"
)
 
text = "Christopher Nolan directed Inception."
output = triplet_extractor(text, return_tensors=True, max_length=256)
# 출력 파싱하여 트리플 추출

4. Entity Resolution

문제

소스 A: "크리스 놀란"
소스 B: "크리스토퍼 놀란"
소스 C: "C. Nolan"
→ 모두 같은 사람!

해결책

from rapidfuzz import fuzz
 
def find_similar_entities(new_entity, existing_entities, threshold=85):
    matches = []
    for existing in existing_entities:
        score = fuzz.ratio(new_entity.lower(), existing.lower())
        if score >= threshold:
            matches.append((existing, score))
    return sorted(matches, key=lambda x: x[1], reverse=True)
 
# 임베딩 기반 접근
from sentence_transformers import SentenceTransformer
 
model = SentenceTransformer('all-MiniLM-L6-v2')
 
def entity_similarity(entity1, entity2):
    emb1 = model.encode(entity1)
    emb2 = model.encode(entity2)
    return cosine_similarity([emb1], [emb2])[0][0]

5. 완전한 파이프라인

class KnowledgeExtractor:
    def __init__(self, llm_client, entity_store):
        self.llm = llm_client
        self.store = entity_store
 
    def extract(self, text):
        # 1. NER
        entities = self.extract_entities(text)
 
        # 2. Entity Resolution
        resolved_entities = []
        for entity in entities:
            existing = self.store.find_similar(entity["text"])
            if existing:
                entity["uri"] = existing.uri
            else:
                entity["uri"] = self.store.create_new(entity)
            resolved_entities.append(entity)
 
        # 3. 관계 추출
        relations = self.extract_relations(text, resolved_entities)
 
        # 4. 트리플 구성
        triples = []
        for rel in relations:
            subj_uri = self.get_uri(rel["subject"], resolved_entities)
            obj_uri = self.get_uri(rel["object"], resolved_entities)
            triples.append((subj_uri, rel["predicate"], obj_uri))
 
        return triples

면접 질문 맛보기

Q: 지식 추출 품질을 어떻게 보장하나요?

  • 멀티 모델 앙상블: spaCy + LLM 결과 결합
  • Human-in-the-Loop: 샘플링하여 추출 결과 리뷰
  • 신뢰도 임계값: 높은 신뢰도만 수용
  • 검증 규칙: 도메인 특화 비즈니스 규칙 적용

더 많은 면접 질문은 Premium Interviews (opens in a new tab)에서 확인하세요.


🎬 프로젝트: 영화 추천 지식그래프

진행 현황

Week주제프로젝트 마일스톤
1온톨로지 입문✅ 영화 도메인 설계 완료
2RDF & RDFS✅ 영화 10편 RDF 변환 완료
3OWL & 추론✅ 추론 규칙 적용 완료
4지식 추출위키피디아에서 영화 정보 수집
5Neo4j그래프 DB에 저장 및 쿼리
6GraphRAG자연어 질의
7온톨로지 에이전트새 영화 자동 업데이트
8도메인 확장의료/법률/금융 케이스
9서비스 배포API + 대시보드

Week 4 마일스톤: 위키피디아에서 영화 정보 자동 추출

수동으로 10편 입력하던 것을 자동화합니다. 위키피디아와 IMDB에서 영화 정보를 추출합니다.

추출 파이프라인:

위키피디아 텍스트
    ↓ NER (개체명 인식)
[Christopher Nolan] [Inception] [2010]
    ↓ Relation Extraction (관계 추출)
(Nolan) --directed--> (Inception)
    ↓ Entity Resolution (개체 연결)
Nolan = dbpedia:Christopher_Nolan
    ↓ RDF 트리플 생성

추출할 정보:

  • 영화: 제목, 개봉일, 장르, 러닝타임
  • 인물: 이름, 생년월일, 역할(감독/배우)
  • 관계: directed, actedIn, hasGenre

목표: 100편의 영화 데이터를 자동 수집

프로젝트 노트북에서 위키피디아로부터 영화 데이터를 자동 수집합니다.

프로젝트 노트북에서는 다음을 직접 구현합니다:

  • Wikipedia API로 영화 100편 정보 크롤링
  • spaCy NER로 감독/배우 이름 자동 추출
  • LLM으로 "directed", "actedIn" 관계 추출
  • Entity Resolution: "크리스 놀란" = "크리스토퍼 놀란" 병합

9주 후 완성되는 것: "놀란 감독 스타일의 SF 영화 추천해줘"라고 물으면, 지식그래프에서 감독-장르-평점 관계를 추론하고 자연어로 답변하는 AI 에이전트


실습 노트북

이론을 더 깊이 탐구하고 싶다면:

실습 노트북에서는 추가로 다음 내용을 다룹니다:

  • spaCy 커스텀 NER 모델 Fine-tuning
  • GPT-4 프롬프트 엔지니어링 기법
  • 임베딩 기반 Entity Resolution
  • 멀티소스 데이터 통합 전략

다음: Week 5: Neo4j