Knowledge Representation

인화·2025년 10월 18일

인공지능

목록 보기
4/6

1. 지식 표현 개요

 지식 표현(Knowledge Representation, KR)은 현실 세계의 사실/규칙/관계를 기호적으로 기술하여 추론이 가능하도록 하는 방식을 의미한다. 다시 말해, 인간이나 컴퓨터가 지식을 어떻게 표현하는지, 그 방법에 관한 내용을 다룬다.

 지식 표현의 대표적인 방법에는 규칙, 의미망, 프레임, 시맨틱 웹/온톨로지가 있다.

  • 규칙 (Rules) : IF <전제> THEN <결론>과 같은 형태를 지니며, 작업 메모리(현재의 사실들)에 적용해 새로운 사실을 도출함.
  • 의미망 (Semantic Network) : 개념 간의 관계를 방향 그래프로 표현하며, 이때 노드(개념/객체)와 간선(관계)로 의미 구조를 표현함.
  • 프레임 (Frame) : 객체/개념을 슬롯(slot : 속성)과 값(value)으로 조직화하며, 슬롯에 프로시저(메소드)를 부착할 수 있음.
    • 이때, 프레임을 객체로, 슬롯을 field(속성)으로, 프로시저를 method(함수)로 볼 수 있음.
  • 시맨틱 웹/온톨로지 (OWL/RDF) : 웹 상에서 공유 가능한 의미/관계를 형식적으로 서술함.

2. 규칙 기반 표현과 점화 (Forward Chaining)

  규칙은 전제(Premise)와 결론(Conclusion)으로 구성되며, 규칙의 전제 조건을 만족하면 결론이 실행된다. 이를 규칙의 전제 조건이 일치하는 경우, 규칙은 점화되고 결론은 실행된다는 말로 표현하기도 한다.

 규칙을 사용하는 시스템에는 작업 메모리라고도 하는 데이터베이스가 포함되며, 작업 메모리(Working Memory)에는 현재 관측된 사실(Observed focus), 상태(State), 지식(Knowledge)이 저장된다.

 'IF 오늘은 휴강이다 THEN 기분이 좋다'와 같이 IF-THEN 구조로 표현되는 것을 규칙이라고 한다.

# 러닝 환경 준비
!pip -q install sympy networkx # Symbolic 수학 라이브러리, 그래프 분석 라이브러리 설치

import itertools
from typing import List, Set, Tuple, Dict, Any
import sympy as sp
import networkx as nx
import matplotlib.pyplot as plt

# 전방향 추론기(forward-chaining) 예시
from dataclasses import dataclass, field

# 데이터들을 담는 클래스 - __init__(), __repr__(), __eq__()과 같은 메소드들 자동으로 추가해 줌!
# __repr__ : 객체를 출력할 때 사람이 읽기 쉽게 보여주는 문자열 반환
@dataclass
class Rule:
  premises : List[str] # 전제들의 리스트 (모두 참이어야 함; OR는 별도 규칙으로 분해)
  conclusion : str

# 코드 비워져 있을 때 채울 수 있는지 요기
@dataclass
class RuleEngine:
  rules : List[Rule] # 규칙들의 리스트
  # facts를 facts: Set[str] = set()와 같은 형태로 쓰지 않는 이유는
  # list, set, dict과 같은 변경 가능한 자료형을 위와 같은 형태로 사용하면
  # 객체가 같은 기본값을 공유하는 문제가 생김. (다시 말해, 모든 객체가 같은 set 인스턴스를 가리킴)
  # 따라서 아래와 같은 형식을 통해 인스턴스마다 set()을 실행시켜 각 인스턴스마다 서로 다른 집합 가질 수 있게 함
  facts : Set[str] = field(default_factory=set) # 사실들의 집합 / field는 기본 값 공유 문제를 해결하기 위해 dataclasses에서 제공하는 함수임.

  # 전방향 추론 (forward chaining)
  def infer(self, max_steps: int = 50) -> Set[str]: # max_steps - 규칙이 새로운 사실을 계속 만들어 내면 무한루프 돌 수 있으므로
    """단순 전방향 추론 : 적용 가능한 규칙의 결론을 사실에 추가"""
    changed = True
    steps = 0
    while changed and steps < max_steps:
      changed = False
      steps += 1
      for r in self.rules:
        if all(p in self.facts for p in r.premises): # 모든 전제가 충족되고
          if r.conclusion not in self.facts: # 사실이 결론에 포함되어 있지 않으면
            self.facts.add(r.conclusion) # 결론을 사실 집합에 추가
            changed = True # 규칙에 새로운 사실이 추가됐으므로 True
    return self.facts # 사실 집합 반환

# 규칙과 사실 예시
rules = [
    Rule(["비가온다"], "우산을가져간다"),
    Rule(["버그가없다"], "프로그램은 올바르게 동작한다."),
    # (A OR B) -> C 는 두 규칙으로 분해 -> IF 습도가 높다 OR 온도>=30 THEN 에어컨가동
    # A OR B → C는 (A → C) ∧ (B → C)와 논리적 동치를 이루므로 두 규칙으로 나누면 동일한 의미가 됨.
    Rule(["습도가 높다"], "에어컨가동"),
    Rule(["온도>=30"], "에어컨가동"),
]

engine = RuleEngine(rules=rules, facts={"비가온다", "온도>=30"})
# 비가 온다 -> 우산을 가져간다
# 온도 >= 30 -> 에어컨 가동
engine.infer()
engine.facts

3. 의미망 (Semantic Network)

 의미망은 방향 그래프(Directed Graph)를 통해 개념 간의 관계를 나타내는 방법이며, 그래프는 노드와 간선으로 이뤄진다. 이때, 개념/객체를 노드로, 관계를 간선(ex. is-a, has, inst-of 등)으로 표현한다.

 의미망은 매우 복잡한 개념이나 인과 관계를 잘 표현할 수 있지만, 지식의 양이 커지면 너무 복잡해져 조작이 어렵다는 단점이 존재한다.

# 동물-조류-펭귄의 부분 계층과 속성
G = nx.DiGraph()
# 노드
G.add_nodes_from(["Animal", "Bird", "Penguin", "Wings", "Antarctica"]) # 노드 정의
# 관계 (라벨 포함)
G.add_edge("Penguin", "Bird", relation="is-a") # Penguin is a Bird
G.add_edge("Bird", "Animal", relation="is-a") # Bird is a Animal
G.add_edge("Bird", "Wings", relation="has") # Bird has Wings
G.add_edge("Penguin", "Antarctica", relation="habitat") # Penguin habitat Antarctica

pos = nx.spring_layout(G, seed=42) # 그래프 모양 + 위치 담은 변수
plt.figure(figsize=(6,4))
nx.draw(G, pos, with_labels=True) # 노드와 엣지를 그림
# pos는 노드들의 좌표 정보이고,
# G는 무엇이 연결되어 있는지(엣지 정보)를 담은 그래프 구조
# G.edges(data=True) : 모든 엣지 + 속성 반환
# u 시작 노드 / v 끝 노드 / d는 relation들 (속성) 반환함
edge_labels = {(u, v) : d["relation"] for u,v,d in G.edges(data=True)}
nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels) # relation label 그림
plt.title("Semantic Network Example")
plt.show()

프레임 (Frame) - 슬롯/프로시저

 프레임은 슬롯과 값으로 구성되며, 슬롯에 이벤트 기반 프로시저를 추가할 수 있다.

  • if-added : 새 값이 추가될 때 실행
  • if-deleted : 값이 삭제될 때 실행
  • if-needed : 값이 필요하지만 비어 있을 때 호출 (지연 계산)

 인공지능 분야에서는 프레임으로 '객체'를 나타내고, 프레임의 슬롯으로 객체의 '필드(속성)'을 나타내며, 슬롯에 붙은 프로시저로 객체의 '메소드(함수)'를 나타낸다. 또한, 프레임을 인스턴스 프레임과 클래스 프레임으로 나눌 수도 있다.

 이러한 프레임은 개념을 구조화해 표현할 수 있기에 개념과 개념 간 관계, 속성, 기능 등을 명확하게 표현할 수 있고, 지식의 재사용성이 높아 이미 만들어진 프레임을 조합해 새로운 프레임을 만들어 지식을 쉽게 확장 가능하며, 지식의 일관성을 유지할 수 있다는 장점이 존재한다.

class Frame: # 프레임 - 객체
  def __init__(self, name):
    self.name = name # 프레임 이름
    self.slots = {} # 슬롯 - attribute 저장하는 딕셔너리
    self.procs = {"if-added": {}, "if-deleted" : {}, "if-needed" : {}} # 프로시저 - method / 프로시저는 슬롯에 붙음.

  def set_slot(self, slot, value):
    # 슬롯에 값을 넣음
    self.slots[slot] = value
    # 프로시저가 if-added
    # (ex) 슬롯에 값이 추가될 때, on_color_added 함수가 자동으로 호출되게 함.
    # penguin = Frame("펭귄")
    # penguin.procs["if-added"]["color"] = on_color_added
    if slot in self.procs["if-added"]:
      self.procs["if-added"][slot](self, value) # 함수 실행

  def get_slot(self, slot):
    if slot not in self.slots and slot in self.procs["if-needed"]: # 슬롯이 비어있고 프로시저가 if-needed
      # 해당 슬롯이 아직 존재하지 않을 때, 필요할 때만 계산하는 함수를 실행해 값을 생성함.
      # 지연 계산 (값이 정말 필요할 때까지 계산을 미룸)
      self.slots[slot] = self.procs["if-needed"][slot](self)
    # dict.get(key, default)
    return self.slots.get(slot, None) # 저장된 값 반환

  def del_slot(self, slot):
    if slot in self.slots: # 슬롯이 존재하면
      val = self.slots.pop(slot) # 값 꺼내고 슬롯에서 제거
      # 슬롯에 삭제될 때 실행할 프로시저가 등록되어 있으면 실행
      if slot in self.procs["if-deleted"]: # 슬롯에 if-delete 프로시저 존재하면
        self.procs["if-deleted"][slot](self, val) # 함수 실행

  def attach_proc(self, kind, slot, func):
    # kind가 "if-added", "if-deleted", "if-needed" 중 하나인지 검증 (논리적 가정 검증)
    assert kind in self.procs # assert는 kind가 True가 아니면 오류 발생시킴
    self.procs[kind][slot] = func # 슬롯에 프로시저 함수 연결 (slot 이름에 함수 매핑) "if-added": { "age": <lambda함수> } < 요 느낌으로 저장됨

# 예시 : 사람 프레임
person = Frame("Kim")
# 프로시저 함수 붙임
person.attach_proc("if-added", "age", lambda self, v : print(f"[if-added] {self.name}.age := {v}"))
person.attach_proc("if-needed", "is_adult", lambda self: (self.slots.get("age", 0) >= 19))
# age 슬롯에 20 저장
person.set_slot("age", 20)
# 나이가 19살 이상이면 is_adult = True
print("is_adult? -> ", person.get_slot("is_adult"))

정리 - 규칙, 의미망, 프레임 비교 및 장단점

  • 규칙은 전제와 결론으로 구성된 지식 표현 방식을 의미하며, IF <전제> THEN <결론>의 구조를 지닌다. 이때, 전제 조건을 만족하면 결론이 실행되는데, 이를 점화되었다고도 표현한다. (ex) IF 오늘은 축제이다 THEN 교수님이 수업을 일찍 마치신다
    • 규칙을 사용해 지식을 표현하면 직관적이고 단순하게 지식을 표현할 수 있다는 장점이 존재한다. 또한, 어떤 규칙에 따라 이러한 결과가 도출되었는지 파악하기 용이하고, 규칙을 추가하거나 수정하기도 용이하다는 장점이 있다.
    • 그러나, 지식이 많아질 수록 전제와 결론의 수가 많아져 관리가 어렵고, 복잡한 관계를 표현하기 비효율적이며, 현실 세계의 모든 가능한 상황들을 규칙으로 표현하기엔 한계가 있다는 단점이 존재한다.
  • 의미망은 방향 그래프를 이용해 개념 간의 관계를 나타내는 지식 표현 방식이며, 각 노드는 개념을, 엣지는 개념 간 관계를 나타낸다.
    • 이러한 의미망은 객체 간 관계를 직관적으로 표현하기 쉽고, 'is-a', 'has' 등 객체 간 인과관계를 표현할 수도 있고, 새로운 개념이나 관계를 노드와 엣지를 통해 추가하는 것도 어렵지 않다는 장점이 있다.
    • 그러나, 의미망은 표준 지침이 없어 시스템에 따라 의미망의 형태가 달라 시스템마다 의미망의 형태가 다를 수 있고, 규모가 커지면 복잡해져 조작이 어려울 수 있고, 각 노드나 관계가 무엇을 의미하는지 명확히 밝힐 수 있는 의미 체계가 없다는 단점이 있다.
  • 프레임은 특정 객체와 그 속성을 묶어 하나로 조직화하는 지식 표현 방식으로, 객체지향 프로그래밍에서의 객체와 유사한 특성을 지닌다.
    • 이러한 프레임은 개념을 구조화해 표현할 수 있고, 유사한 개념을 쉽게 만들 수 있으므로 지식 재사용성이 높고, 개념을 구조화해 표현하기 때문에 지식의 일관성을 유지한다는 장점이 있다.
    • 그러나, 슬롯과 값에 대한 표준이 없고, 다른 지식 표현 방법들에 비해 표현 방법이 어려우며, 사전에 정의되지 않은 새로운 상황이나 속성을 쉽게 반영할 수 없다는 단점이 있다.

참고 - 논리

 전통적인 논리에는 명제 논리, 1차 술어 논리, 2차 술어 논리가 있다.

  • 명제 논리(Propositional Logic)는 명제와 명제 간의 논리적 관계를 다루며,
    AND, OR 등의 논리 연산자를 이용해 명제를 결합하거나 변형한다.
  • 1차 술어 논리(First-Order Predicate Logic) 는 술어(predicate)와 변수(variable)를 사용하여 대상을 좀 더 정교하게 표현한다. 예를 들어, 사람(x)이나 학생(x)처럼 “x가 사람이다”, “x가 학생이다”라는 형태로 표현한다.
  • 2차 술어 논리(Second-Order Predicate Logic) 는 1차 술어 논리를 확장하여 술어 자체를 객체로 다루는 논리이다. 즉, 술어들 사이의 관계나 속성을 표현할 수 있어 더 높은 수준의 구조적 관계를 표현할 수 있다.

 이러한 술어 논리는 수학적 근거를 바탕으로 논리 개념을 자연스럽게 표현 가능하고, 지식의 정형화 영역에 적합(ex. 정리 증명 기법 사용)하며, 지식의 첨가와 삭제가 용이하고, 비교적 단순하다는 장점이 있다. 하지만, 절차적인 지식 표현이 어렵고 사실의 구성 법칙이 부족하므로 실세계의 복잡한 구조를 표현하기 어렵다는 단점이 존재한다.

5. 명제 논리 (Propositional Logic)

  • 기호 논리학에서 명제는 참 혹은 거짓을 판별할 수 있는 문장이다.
  • 명제 논리는 주어와 술어를 구분하지 않고, 전체를 하나의 명제로 처리해 참 또는 거짓을 판별한다.
  • 단순 명제를 논리 연산자를 통해 결합하여 복합 명제를 구성할 수 있다.
  • 명제 논리의 장점은 명제 간 결합이 단순하고 체계적이라는 것이다. 그러나,
    명제가 최소 단위이기에 내부 구조에 대한 분석이 어렵고, 지식 표현을 일반화하기 어렵다는 단점이 존재한다.
  • 명제 : P, Q, R, ...는 참(T) 또는 거짓(F).
  • 연산자 : ¬,∧,∨,→,↔ 등

5.1 함축 (Implication)

  • 두 명제 사이의 조건부 관계를 표현하는 논리 연산자 (IF-THEN)

    C = 오늘은 휴일이다.
    D = 오늘은 수업이 없다.
    E = C -> D

5.2 모더스 포넌스, 부정 논법, 삼단 논법

 모더스 포넌스(Modus Ponens, affirming method)는 A -> B라는 규칙이 있고, A가 사실이면 B가 결론이 되는 구조를 의미한다. 예를 들어, 만약 "인화가 놀고 있다면 -> 수업을 짼 것이다" 에서 인화가 놀고 있다를 A, 수업을 짼 것이다를 B로 보고, 인화가 놀고 있다는 명제가 참이라면, 수업을 짼 것이라는 결론도 참이 된다.

 따라서 아래와 같은 규칙을 지닌다고 할 수 있다.

규칙 | A -> B
사실 | A
---------------------|
결론 | B

 부정 논법(Modus Tollens, denying method)은 A -> B라는 규칙이 있고, B가 사실이 아니면 A는 사실이 아니라는 결론을 도출해 내는 것을 의미한다. 예를 들어, "인화가 자퇴를 한다면 -> 인화는 새 진로를 찾은 것이다"에서 인화가 자퇴를 한다를 A, 인화는 새 진로를 찾은 것이다를 B로 보고, 인화가 새 진로를 찾은 것이라는 B가 거짓이면, 인화가 자퇴를 한다는 A도 거짓이 되어 인화는 자퇴를 하지 않는다는 결론이 나오는 것이다.

 따라서 아래와 같은 규칙을 지닌다고 할 수 있다.

규칙 | A -> B
사실 | NOT B
---------------------|
결론 | NOT A

 삼단 논법(syllogism, Based on two propositions)은 A->B이고 B->C이면 A->라는 결론을 도출해 내는 것을 의미한다. 예를 들어, "소크라테스는 인간이다"라는 규칙이 있고, "인간은 모두 죽는다"라는 사실이 있을 때, "소크라테스는 죽는다"라는 결론이 나오는 것과 같은 것이 삼단 논법의 예시이다.

 따라서 아래와 같은 규칙을 지닌다고 할 수 있다.

규칙 | A -> B
사실 | B -> C
---------------------|
결론 | A -> C
# SymPy 논리 심볼과 식
P, Q, R = sp.symbols('P Q R') # 명제를 나타내는 심볼 생성
expr = sp.Implies(P & sp.Not(Q), sp.Or(sp.Not(P), Q)) # expr = (P ∧ ¬Q) → (¬P ∨ Q)

def truth_table(expr, symbols): # 진리표 만들기
  rows = []
  # itertools.product()는 데카르트 곱(Cartesian product), 즉 모든 가능한 조합을 만들어주는 함수임.
  # itertools.product(iterable, repeat=n)
  # iterable: 조합을 만들 값들의 집합 (예: [False, True])
  # repeat=n: 몇 번 반복할지
  for vals in itertools.product([False, True], repeat=len(symbols)): # 모든 True/False 조합 만들고
    env = dict(zip(symbols, vals)) # 각 심볼에 True/False 매핑하는 dict 생성
    # expr.subs(env) : expr((P ∧ ¬Q) → (¬P ∨ Q))에 해당 값들 대입해서 결과를 계산함.
    rows.append((vals, bool(expr.subs(env)))) # 결과 저장
  return rows

tt = truth_table(expr, [P, Q]) # P, Q의 모든 값에 따른 진리표 생성
for env, val in tt: # env -> (P, Q) 값, val -> expr 결과 값 출력
  print(env, "=>", val)

5.3 추론 규칙 예시

# 모더스 포넌스는
# 규칙 A-> B
# 사실 A
# ------------
# 결론 B

def modus_ponens(A, B, facts: Set[str]) -> bool:
  """A->B와 A가 facts에 있으면 B를 추가"""
  changed = False
  if (f"{A}->{B}" in facts) and (A in facts) and (B not in facts): # A->B가 사실이고, A가 사실에 포함되어 있고, B가 포함되어 있지 않으면
    facts.add(B); changed = True # B를 사실에 추가
  return changed

facts = {"A->B", "A"} # 조건 충족 -> True
changed = modus_ponens("A", "B", facts)
facts, changed

6. 술어 논리 (Predicate Logic)

 명제 논리는 전체 명제가 참이냐 거짓이냐만 판단할 수 있다. 따라서 복잡한 문제를 해결하기엔 어려움이 존재한다.

 이에 반해, 술어 논리는 하나의 명제를 술어와 그 술어의 수식을 받는 객체로 분리하여 "술어(객체)"의 형태로 표현한 것으로, 명제 논리보다 더 구체적으로 지식을 표현한다. 이때, 객체는 상수 기호로, 관계는 술어 기호로 나타내며, 술어 논리에서는 하나의 명제를 변수와 한정자를 사용해 표현할 수 있다.

  • 전칭 한정자 ∀(All)는 어떤 명제가 모든 대상에 대해 참임을 나타내며, x[Human(x)>Die(x)]∀x[Human(x) -> Die(x)]와 같은 예시가 이에 해당한다.
  • 존재 한정자 ∃(Exist)는 어떤 명제가 적어도 하나 이상의 대상에 대해 참임을 의미하며, x[undergraduate(x)skipclass(x,inhwa)]∃x[undergraduate(x) ∧ skipclass(x, inhwa)]와 같은 예시가 이에 해당한다.

 술어 논리에서의 추론을 위한 첫 번째 방법은 술어 논리식을 명제 논리식으로 변환한 후, 명제 논리의 추론 기법을 적용하는 것이고, 두 번째는 논리 융합이다.

 논리 융합(Logical Inference)을 사용하기 위해서는 모든 지식이 정형식(formal expression)으로 표현되어야 하며, 정형식은 기본적인 명제나 개념들로 구성된 논리식이다. 이를 논리 연산자(logical operator)나 한정사(quantifier)를 통해 확장함으로써 더 복잡한 지식 구조를 표현할 수 있다.

A, B, C = sp.symbols('A B C')
formula = sp.Implies(A & sp.Implies(B, C), C)  # (A ∧ (B→C)) → C
# CNF 과정
# 1. 함축 기호 제거 - P → Q는 ¬P ∨ Q 와 동치
# ¬(A ∧ (¬B ∨ C)) ∨ C
# 2. 부정 기호를 드모르간 법칙을 통해 기초 공식 안으로 이동
# (¬A ∨ (B ∧ ¬C)) ∨ C
# 3. 분배 법칙 - P∨(Q∧R)≡(P∨Q)∧(P∨R)
# ((¬A∨C∨B)∧(¬A∨C∨¬C)) - 이때 (C ∨ ¬C)는 항상 참이므로
# (¬A∨B∨C)
cnf_form = sp.to_cnf(formula, simplify=True)
print("Original : ", formula)
print("CNF      : ", cnf_form)
profile
얼렁뚱땅 바보 학부생...

0개의 댓글