단원 홈
1단원 · 10차시

사람의 지식을 적어 넣기
사실과 규칙이라는 두 벌의 문법

9차시까지 우리는 길 찾는 법을 짰습니다. 그런데 그 지도는 누가 그려 주었을까요? 오늘은 알고리즘이 아니라 지식 그 자체를 적습니다. 사람이 아는 것을 컴퓨터가 읽을 수 있는 글로 옮기는 데는 문법이 딱 두 벌 필요합니다.

성취기준 12인기01-05
지식 표현사실규칙 지식 베이스IF-THEN지식 획득의 병목
🎯 학습 목표
  • 사실과 규칙의 차이를 정의로 말하고, 주어진 문장을 둘로 갈라 적을 수 있다.
  • 규칙 하나를 (조건 목록, 결론) 짝으로 코드에 적고, 규칙 여럿을 모아 지식 베이스를 만들 수 있다.
  • 규칙이 발동하는 조건 두 가지를 짚고, 둘째 검사를 지웠을 때 무엇이 망가지는지 실행으로 확인할 수 있다.
🤔

여는 장면 — 지도는 누가 그려 주었나

3차시부터 9차시까지 일곱 시간 동안 우리는 찾는 법을 짰습니다. 물병을 상태로 적었고, 큐를 스택으로 바꿔 보았고, 어림 h를 곱해 보았고, 마지막에는 지형마다 다른 비용이 있는 지도 위에서 A*를 돌렸지요.

그런데 한 가지 짚고 넘어가지 않은 것이 있습니다. 미로 MAZE는 누가 적었나요? 7행 12열의 벽 배치, 지형별 비용 {".": 1, "s": 3, "w": 5} — 전부 사람이 손으로 적어 넣은 것입니다. 알고리즘은 그 위를 달렸을 뿐이에요. 컴퓨터가 아는 세상은 우리가 적어 준 만큼입니다.

이제 지도가 아니라 판단을 적을 차례입니다. 예를 들어 이런 것입니다.

📖 진료실의 3초

아이가 진료실에 들어옵니다. 의사는 3초 만에 판단합니다. "열이 나고 기침을 하네. 목도 부었고. 감기로 봅시다."
그 3초 안에는 배운 것과 겪은 것이 뒤섞여 있습니다. 1980년대 사람들은 이 3초를 컴퓨터에 옮겨 담으려 했어요. 그러려면 먼저 이 물음에 답해야 합니다.

💭 오늘의 물음

사람의 머릿속에 있는 판단을 컴퓨터가 읽을 수 있는 글로 옮기려면, 어떤 모양으로 적어야 하는가?

"열이 난다"와 "열이 나고 기침을 하면 감기를 의심한다"는 결이 다른 문장입니다. 앞의 것은 지금 눈으로 확인한 것이고, 뒤의 것은 언제 어디서든 통하는 판단의 다리예요. 오늘 배울 것은 이 두 결을 갈라 적는 문법입니다. 그런데 필요한 문법은 두 벌뿐이고, 그 둘로 적어 놓은 것이 곧 컴퓨터가 스스로 결론을 내는 재료가 됩니다.

박물관 진열대 위의 Symbolics 리스프 워크스테이션 — 흰 모니터와 키보드, 마우스, 옆에 선 푸른 본체와 설명 팻말
박물관에 전시된 Symbolics 리스프 워크스테이션 — 1980년대에 인공지능 연구용 언어 리스프(Lisp)를 빠르게 돌리려고 만든 전용 컴퓨터입니다. 이 시대 사람들이 컴퓨터에 넣으려 한 것은 데이터가 아니라 사람의 판단이었고, 그 판단을 적는 문법이 오늘 배울 사실과 규칙입니다. 출처: leighklotz, Wikimedia Commons (CC BY 2.0)
1

사실 — 세상을 짧은 문장으로 잘라 적는다

사실(fact)은 지금 참이라고 아는 것입니다. "깃털이 있다", "열이 난다", "3학년이다" — 눈으로 확인했거나 누가 알려 주어서 이미 참이 된 것들이에요.

사실을 코드에 적는 방법은 김빠질 만큼 간단합니다. 문자열 하나가 사실 하나입니다. 그리고 지금 아는 사실을 전부 모아 집합(set)에 담습니다.

# 지금 아는 사실
facts = {"깃털이 있다", "헤엄친다"}

왜 하필 집합일까요? 이유가 셋 있고, 셋 다 나중에 코드 한 줄씩으로 되돌아옵니다.

  • 중복이 없다. "깃털이 있다"를 두 번 적어도 집합에는 하나만 남습니다. 같은 것을 두 번 안다고 해서 더 아는 것이 아니니까요.
  • 순서가 없다. 무엇을 먼저 알았는지는 판단에 아무 영향을 주지 않습니다. "열이 난다"를 먼저 알았든 "기침을 한다"를 먼저 알았든 감기 의심은 똑같이 나옵니다.
  • 있는지 없는지만 빠르게 묻는다. 파이썬에서 "깃털이 있다" in facts는 사실이 몇 개든 거의 같은 시간에 답이 나옵니다. 오늘 코드는 이 물음을 수백 번 던집니다.

여기서 한 가지를 분명히 해 둡시다. 컴퓨터는 "깃털이 있다"라는 문자열이 무슨 뜻인지 모릅니다. 그 안에 '깃'이라는 글자가 있다는 것, 그리고 그 문자열이 집합 안에 있는지 없는지만 압니다. 1차시에서 ELIZA가 "3 곱하기 17은?"에 51이라는 답의 존재조차 모른 채 "그건 당신은 어떻게 생각하나요?"라고 되물었던 것과 같은 사정이에요. 뜻은 사람이 읽을 때만 생깁니다.

⚠️ 그래서 사실은 '한 조각'으로 잘라 적어야 한다

"열이 나고 기침을 한다"를 사실 하나로 적으면 안 됩니다. 컴퓨터는 그 문자열 통째로만 알아보므로, "열이 난다"만 아는 상황과 짝지어지지 않아요. "열이 난다"와 "기침을 한다", 사실 두 개로 잘라 적습니다. 기준은 하나입니다 — 따로 관찰될 수 있으면 따로 적는다.

2

규칙 — IF와 THEN 사이에 다리를 놓는다

사실만으로는 아무 데도 못 갑니다. 아는 것에서 모르던 것으로 건너가려면 다리가 필요해요. 그 다리가 규칙(rule)입니다.

IF 조건1 AND 조건2 AND …  THEN  결론

사람 말로는 "깃털이 있으면 새다"입니다. 이것을 코드에 적으려면 조건들과 결론을 따로 담을 그릇이 필요해요. 조건은 여럿일 수 있으니 목록에, 결론은 하나뿐이니 문자열에 담고, 둘을 짝(튜플)으로 묶습니다.

사람이 하는 말 “깃털이 있으면 새다” 적어 넣기 ( ["깃털이 있다"] , "새다" ) 조건 목록 결론 조건이 둘일 때 “새인데 헤엄치면 펭귄” ( ["새다", "헤엄친다"] , "펭귄이다" ) 목록에 나란히 = AND
규칙 하나는 (조건 목록, 결론) 짝 하나입니다. 목록에 나란히 놓인 조건들은 전부 참일 때만 다리가 열립니다 — 곧 AND입니다.

이렇게 적은 규칙을 여러 개 모아 목록에 담은 것이 지식 베이스입니다. 오늘 우리가 쓸 지식 베이스는 규칙 6개짜리입니다. 이 여섯 줄은 11차시와 12차시에서도 글자 그대로 다시 쓰므로 눈에 익혀 두세요.

RULES = [
    (["깃털이 있다"],               "새다"),
    (["젖을 먹인다"],               "포유류다"),
    (["새다", "빠르게 난다"],        "매다"),
    (["새다", "헤엄친다"],           "펭귄이다"),
    (["포유류다", "헤엄친다"],       "돌고래다"),
    (["포유류다", "초음파를 쓴다"],   "박쥐다"),
]

여섯 줄을 눈으로 훑으면 두 층이 보입니다. 위의 두 규칙은 관찰한 것에서 바로 나오는 결론이고, 아래 네 규칙은 그 결론을 다시 조건으로 씁니다. "새다"는 1번 규칙의 결론이면서 3·4번 규칙의 조건이에요. 이 이어짐이 다음 시간의 주제입니다.

모래톱 앞 바다에서 물을 튀기며 공중으로 뛰어오른 병코돌고래 한 마리, 뒤쪽 모래밭에 갈매기 몇 마리
병코돌고래 — {젖을 먹인다, 헤엄친다}로 판정되는 쪽입니다. '헤엄친다'라는 같은 사실이 펭귄 규칙과 돌고래 규칙 양쪽 조건에 들어 있습니다. 둘을 갈라놓는 것은 앞 층의 결론(새다 / 포유류다)이에요. 출처: Giles Laurent, Wikimedia Commons (CC BY-SA 4.0)
맨땅에 내려앉아 옆을 보고 있는 송골매 수컷
송골매 — {깃털이 있다, 빠르게 난다}로 판정되는 쪽입니다. 사진처럼 땅에 앉아 있을 때도 '빠르게 난다'는 이 새에 대한 사실로 그대로 적어 둡니다. 사실은 '지금 보이는 모습'이 아니라 우리가 참이라고 아는 것이니까요. 출처: Sumeet Moghe, Wikimedia Commons (CC BY-SA 4.0)
ℹ️ OR은 어디 갔나

이 문법에는 AND만 있고 OR이 없습니다. 빠뜨린 것이 아니라 필요가 없어서입니다. "A이거나 B이면 C다"는 규칙을 두 줄로 나눠 적으면 됩니다 — (["A"], "C")와 (["B"], "C"). 어느 쪽이든 열리면 같은 결론에 닿지요. 문법을 늘리는 대신 규칙을 늘린 것인데, 이 선택 덕분에 규칙을 검사하는 코드가 한 줄로 끝납니다. NOT은 사정이 다릅니다 — '없다'를 조건으로 쓰면 사실이 늘어날 때 예전 결론이 뒤집힐 수 있어 훨씬 까다로워집니다. 그래서 오늘 문법에서는 뺐습니다.

사람이 하는 말결코드에 적는 모양
이 새는 깃털이 있다사실 "깃털이 있다"
깃털이 있으면 새다규칙 (["깃털이 있다"], "새다")
지금 열이 38.5도다사실 "열이 난다"
열이 나고 기침을 하면 감기를 의심한다규칙 (["열이 난다", "기침을 한다"], "감기를 의심한다")
이 학생은 3학년이다사실 "3학년이다"

가르는 잣대는 하나입니다. 지금 이 하나에 대한 이야기면 사실, "~이면 ~이다" 꼴로 언제든 통하면 규칙입니다. "이 새는 깃털이 있다"는 이 새 이야기지만, "깃털이 있으면 새다"는 세상 모든 것에 대한 이야기예요.

3

규칙은 if 문이 아니다 — 지식과 엔진을 갈라 두는 값어치

여기까지 읽고 이렇게 생각한 사람이 있을 겁니다. "그냥 if 문 아닌가요?" 모양은 확실히 닮았습니다. 하지만 결정적으로 다른 점이 하나 있고, 그 하나가 이 방식 전체를 떠받칩니다.

① if 문으로 적으면

# 판단이 코드 안에 박힌다
if "깃털이 있다" in facts:
    kind = "새"
    if "헤엄친다" in facts:
        kind = "펭귄"
    elif "빠르게 난다" in facts:
        kind = "매"
elif "젖을 먹인다" in facts:
    ...

② 규칙으로 적으면

# 판단이 데이터로 빠져나온다
RULES = [
    (["깃털이 있다"], "새다"),
    (["새다", "헤엄친다"], "펭귄이다"),
    (["새다", "빠르게 난다"], "매다"),
    ...
]
# 이 목록을 읽는 코드는 따로 있다

①은 사람이 정한 순서대로 실행됩니다. 위에서 아래로 한 번 훑고 끝이에요. ②는 순서 없이 쌓아 둔 규칙 더미이고, 조건이 맞는 것이 스스로 발동합니다. 차이가 어디서 드러나는지는 규칙을 하나 더 얹을 때 보입니다.

상황if 문으로 적었다면규칙으로 적었다면
"밤에 활동하는 새는 올빼미다"를 새로 넣는다 조건 가지 어디에 끼울지 사람이 정해야 한다. 코드를 고친다. 목록 맨 뒤에 한 줄 더한다. 코드는 안 건드린다.
전문가가 판단 하나를 바꾸라고 한다 프로그래머를 불러야 한다 전문가가 직접 목록을 고칠 수 있다
"왜 그렇게 판단했나" 물으면 실행 흐름을 사람이 되짚어야 한다 발동한 규칙을 차례로 적어 두면 그것이 설명이다
규칙 순서를 통째로 뒤집으면 결과가 달라진다 — elif는 순서가 뜻이다 발동 순서는 바뀌지만 최종 결론 집합은 같다

마지막 줄이 오늘 시뮬레이터로 직접 확인할 성질입니다. 규칙 목록을 거꾸로 뒤집어 돌려 보면 로그에 찍히는 순서는 달라지는데 결국 알아낸 사실의 집합은 똑같습니다. 이것이 "순서가 뜻을 가지지 않는다"의 뜻이에요.

지식 · RULES 규칙 6줄 = 그냥 데이터 전문가가 늘리고 고친다 자주 바뀐다 ↻ 먹인다 엔진 · fires / apply_once 규칙을 읽고 발동시키는 코드 동물도 병도 급식도 이것 하나로 한 글자도 안 바뀐다 새 사실 = 결론 분야를 바꾸려면 왼쪽만 갈아 끼운다 — 오른쪽은 그대로다.
지식과 엔진의 분리. 왼쪽(규칙 목록)만 갈아 끼우면 같은 코드로 동물도, 병도, 급식 메뉴도 판정합니다. 11차시에서 이 분리를 다시 확인하게 됩니다 — 규칙을 늘려도 엔진은 한 글자도 고치지 않습니다.

3단원에서 배울 if 문이 나쁘다는 말이 아닙니다. 판단이 열 개쯤이면 if 가 낫습니다. 짧고 빠르니까요. 그런데 판단이 수백 개가 되고, 그것을 프로그래머가 아니라 의사나 정비 기술자가 관리해야 하는 순간 이야기가 달라집니다. 1980년대 전문가 시스템은 그 순간을 겨냥한 물건이었어요.

4

'발동한다'는 것 — 두 가지를 함께 묻는다

규칙을 쌓아 두었으니 이제 어느 규칙이 지금 발동하는가를 판정할 코드가 필요합니다. 직관적으로는 하나만 물으면 될 것 같습니다 — "조건이 전부 아는 사실인가?" 그런데 실제로 필요한 물음은 둘입니다.

def fires(rule, facts):
    conds, concl = rule
    return (all(c in facts for c in conds)   # ① 조건이 전부 이미 아는 사실인가
            and concl not in facts)          # ② 결론이 아직 모르는 것인가

①은 예상대로입니다. all()은 안에 든 검사가 전부 참일 때만 참을 돌려줍니다. 조건 목록에 나란히 놓은 것이 AND였던 이유가 여기서 코드 한 줄로 드러나요. 조건이 하나든 셋이든 이 줄은 그대로입니다.

②가 낯설 겁니다. "결론이 아직 사실에 없어야 한다"는 조건을 왜 붙일까요? 말로 하면 이렇습니다 — 이미 아는 것을 또 알아내는 것은 발동이 아니다. 그런데 이 한 줄을 지우면 단순히 쓸데없는 일이 늘어나는 정도가 아닙니다.

⚠️ ②를 지우면 프로그램이 멈추지 않는다

관찰이 {"깃털이 있다", "헤엄친다"}일 때 ②를 지우고 돌려 보면 1번 규칙이 여섯 번 내리 다시 발동합니다. "새다"를 이미 알고 있는데도 조건(깃털이 있다)은 여전히 참이라 계속 열리는 거예요. 여섯 번을 돌리고 나서 아는 사실은 하나도 늘지 않았습니다.

여섯 번에서 멈춘 것은 코드에 range(6)이라는 상한을 걸어 두었기 때문입니다. 상한을 빼고 '더 나올 것이 없을 때까지' 돌리면 2,000,000회(0.9초)를 돌고도 끝나지 않습니다. 이 교과서의 파이썬은 브라우저 안에서, 화면을 그리는 것과 같은 자리에서 돌기 때문에 무한 루프는 곧 탭이 멈추는 것입니다. 오늘 실습에 상한이 들어 있는 까닭이에요.

이 둘을 통과한 규칙을 하나 골라 결론을 사실에 더하는 것이 apply_once()입니다. '한 번'이라는 이름 그대로 발동하는 규칙을 위에서부터 찾아 딱 하나만 적용해요. 두 개를 한꺼번에 처리하지 않는 것은 한 걸음씩 눈으로 보기 위해서입니다.

def apply_once(facts, rules):
    for r in rules:
        if fires(r, facts):
            print(f"   규칙 발동:  IF {' AND '.join(r[0])}  ->  {r[1]}")
            return facts | {r[1]}          # 집합에 결론을 더한 새 집합
    print("   발동할 규칙이 없다.")
    return facts

관찰이 {"깃털이 있다", "헤엄친다"}일 때 이 함수를 세 번 부르면 이렇게 됩니다. 실행하기 전에 셋째 칸에 무엇이 찍힐지 먼저 예측해 보세요.

호출발동한 규칙아는 사실개수
시작— 깃털이 있다, 헤엄친다2
1번째IF 깃털이 있다 → 새다 깃털이 있다, 새다, 헤엄친다3
2번째IF 새다 AND 헤엄친다 → 펭귄이다 깃털이 있다, 새다, 펭귄이다, 헤엄친다4
3번째발동할 규칙이 없다 (그대로)4

사실 개수가 2 → 3 → 4 → 4로 움직입니다. 새로 알아낸 것은 2개 — "새다"와 "펭귄이다"예요. 셋째 호출에서 멈춘 것이 이 방식의 중요한 성질입니다. 사실은 늘기만 하고 줄지 않으므로, 언젠가 더 늘 것이 없어지는 순간이 반드시 옵니다. 2번째 호출에서 얻은 "새다"가 다음 규칙의 조건이 되어 "펭귄이다"까지 이어졌다는 점도 눈여겨 두세요 — 이 이어짐에 이름을 붙이고 기계에 맡기는 것이 다음 시간의 일입니다.

초록빛 물속에서 날개를 저으며 헤엄치는 젠투펭귄 한 마리, 한쪽 날개에 식별 띠가 감겨 있다
물속을 헤엄치는 젠투펭귄 — {깃털이 있다, 헤엄친다}로 판정되는 쪽입니다. 날개가 있어도 하늘을 날지는 못하고, 그 날개를 노처럼 저어 물속을 나아갑니다. '새인데 날지 못한다'는 예외를 규칙 문법은 어떻게 다룰까요? 조건에 '난다'를 안 넣으면 그만입니다. 출처: Debs from England, Wikimedia Commons (CC BY 2.0)
파란 하늘을 배경으로 날개를 활짝 펴고 나는 인도날여우박쥐 두 마리와 오른쪽의 나뭇가지
인도날여우박쥐 — 젖을 먹이는 포유류인데 하늘을 납니다. "날면 새다"라는 그럴듯한 규칙 하나면 이 박쥐가 새가 됩니다. 그래서 규칙에는 '난다'가 한 번도 안 들어갑니다. 다만 이 과일박쥐 무리는 초음파가 아니라 눈과 코로 먹이를 찾으므로, 우리 규칙표의 초음파를 쓴다로는 박쥐로 판정되지 않습니다 — 규칙은 적어 준 조건만큼만 압니다. 출처: Vinod.Kumar.IFS, Wikimedia Commons (CC BY-SA 4.0)

사진 두 장이 이 문법의 성질과 한계를 함께 보여 줍니다. 규칙은 적어 준 대로만 따집니다. 우리가 "난다"를 조건에서 뺐기 때문에 박쥐가 새가 되지 않은 것이지, 컴퓨터가 박쥐를 알아본 것이 아니에요. 그리고 반대 방향의 사고도 일어납니다 — 관찰을 {깃털이 있다, 젖을 먹인다, 헤엄친다}로 적으면 규칙이 네 번 발동해 펭귄이면서 동시에 돌고래인 것이 만들어집니다. 말이 되는지는 아무도 안 따집니다.

📖 적히지 않는 지식 — 지식 획득의 병목

규칙으로 적기 쉬운 지식이 있고, 아무리 애써도 안 되는 지식이 있습니다. 의사에게 "어떻게 아셨어요?"라고 물으면 이런 답이 돌아옵니다 — "얼굴이 안 좋아 보여서요." 은행 창구 직원은 "이 손님은 좀 수상했어요."라고 하고, 선배 정비공은 "소리가 평소랑 달랐어요."라고 합니다.

이 문장들을 (조건 목록, 결론) 짝으로 적어 보려 하면 곧바로 막힙니다. '얼굴이 안 좋다'를 무엇으로 잘라 적을까요? 눈 밑이 어둡다? 입꼬리가 처졌다? 어느 것도 그 판단의 전부가 아니고, 전문가 자신도 목록을 다 대지 못합니다. 전문가가 실제로 아는 것과, 그가 말로 옮길 수 있는 것 사이의 이 간격을 지식 획득의 병목이라고 부릅니다.

이 병목이 1980년대 전문가 시스템이 부딪힌 벽이고, "그러면 규칙을 사람이 적지 말고 데이터에서 뽑아내면 어떨까"라는 생각으로 이어집니다. 그 생각이 Ⅱ단원의 기계학습이에요. 오늘 여러분이 규칙으로 적으려다 실패한 지식을 하나 적어 내는 것이 12차시 한계 논의의 재료가 됩니다.

💻

손으로 ① — 지식 베이스 판을 만져 본다

아래 판에는 방금 본 규칙 6개가 그대로 들어 있습니다. 왼쪽에서 관찰한 사실을 켜고 끄면 오른쪽 규칙 가운데 발동할 수 있는 것에 불이 켜지고, 한 걸음 나아갈 때마다 결론이 새 사실로 떨어집니다. 아래 칸에 규칙을 직접 적어 넣을 수도 있어요 — 적어 넣은 규칙에 새 조건이 있으면 왼쪽에 켤 수 있는 사실이 자동으로 하나 늘어납니다.

🧠 지식 베이스 판 — 사실을 켜면 규칙이 발동한다 INTERACTIVE

왼쪽 동그란 칩을 눌러 관찰한 사실을 켭니다. 켜는 순간 판이 처음부터 다시 돌면서 발동하는 규칙이 하나씩 노랗게 켜졌다가 초록으로 바뀝니다. ▶ 한 걸음은 apply_once()를 딱 한 번 부르는 것이고, ⏩ 끝까지는 더 나올 사실이 없어질 때까지 이어서 부르는 것입니다. 🔄 초기화는 알아낸 사실만 지우고 켠 사실과 규칙은 그대로 두며, ↺ 처음 규칙 6개로는 넣은 규칙까지 전부 되돌립니다.

① 관찰한 사실 — 켜고 끄기
③ 내 규칙 적어 넣기

② 지식 베이스 — 규칙 6개

    버튼만 눌러 보고 끝내면 5분이면 끝납니다. 아래 다섯 과제를 공책에 적으면서 하세요. 숫자를 적어 두어야 확인 문제에서 쓸 수 있습니다.

    • 표 채우기. 아래 표의 네 가지 관찰을 차례로 켜 가며 ⏩ 끝까지를 누르고, 발동 횟수와 새로 알아낸 사실 수를 적으세요. 켜는 사실을 하나 늘리면 발동도 반드시 느는지도 함께 보세요.
    • 반례 만들기 ㉮. 아무 규칙도 발동하지 않는 관찰을 두 가지 찾으세요. 사실을 하나도 안 켠 것은 답으로 치지 않습니다. 켤 수 있는 사실이 5개니 관찰 조합은 모두 32가지인데, 그중 발동이 0인 것은 몇 가지일까요? (판에서 하나씩 세어 봐도 되고, 규칙 6줄을 보고 따져도 됩니다.)
    • 반례 만들기 ㉯. 한 마리가 펭귄이면서 동시에 돌고래가 되는 관찰을 만드세요. 몇 번 발동하나요? 그리고 이것이 왜 버그가 아닌지 한 줄로 적으세요.
    • 내 규칙 넣기. 초음파를 쓴다 → 새다를 넣고 {초음파를 쓴다, 젖을 먹인다}를 켜 보세요. 무엇이 되나요? 그다음 순환하는 규칙 두 개(새다 → 날개가 있다, 날개가 있다 → 새다)를 넣고 ⏩ 끝까지를 눌러 보세요. 멈추나요, 안 멈추나요? 왜 그럴까요?
    • 순서 뒤집기. ↕ 규칙 순서 뒤집기를 누른 뒤 같은 관찰을 다시 돌리세요. 로그에 찍히는 순서와 마지막에 알아낸 사실의 집합 중 무엇이 바뀌고 무엇이 그대로인가요?
    켠 사실(관찰)발동 횟수새로 알아낸 사실 수마지막에 나온 결론
    깃털이 있다, 헤엄친다
    젖을 먹인다, 초음파를 쓴다
    깃털이 있다, 젖을 먹인다, 헤엄친다
    다섯 개 전부 켜기

    위에서부터 2번·2개 / 2번·2개 / 4번·4개 / 6번·6개입니다. 마지막에 나온 결론은 각각 펭귄이다, 박쥐다, 돌고래다, 박쥐다예요. 발동 횟수와 새로 알아낸 사실 수가 네 줄 모두 같다는 것도 눈여겨보세요 — 한 번 발동할 때마다 새 사실이 정확히 하나씩 붙기 때문입니다.

    "켜는 사실이 늘면 발동도 반드시 느는가"의 답은 아니오입니다. 줄어드는 일은 없지만, 그대로인 일은 흔합니다. 예를 들어 {깃털이 있다}는 1번 발동인데 여기에 초음파를 쓴다를 더해도 여전히 1번입니다 — 초음파 규칙은 '포유류다'를 요구하는데 그것이 없으니까요. 32가지 조합에서 사실을 하나 더 켜 보는 80가지 경우를 전부 세어 보면 발동이 준 경우는 0가지, 그대로인 경우가 20가지입니다.

    셋째 줄이 오늘의 반례입니다. {깃털이 있다, 젖을 먹인다, 헤엄친다}에서는 새다 → 포유류다 → 펭귄이다 → 돌고래다 순으로 네 번 발동해 펭귄이면서 돌고래인 것이 만들어집니다. 규칙은 적어 준 조건만 따지고 '말이 되는가'는 따지지 않기 때문이에요. 이것을 막고 싶다면 규칙을 더 적는 수밖에 없습니다 — 예를 들어 '깃털이 있으면 젖을 먹이지 않는다' 같은 것인데, 그러려면 지금 문법에 없는 NOT이 필요해집니다.

    ㉮의 답: 켤 수 있는 사실 5개로 만들 수 있는 관찰은 32가지이고, 그중 아무 규칙도 발동하지 않는 것은 8가지입니다(하나도 안 켠 것 포함). 모두 깃털이 있다와 젖을 먹인다가 둘 다 없는 관찰이에요. 예를 들어 {헤엄친다}, {빠르게 난다, 초음파를 쓴다}. 규칙 6개 중 이 둘 없이 열리는 문이 하나도 없기 때문입니다 — 첫 단추가 없으면 뒤의 규칙은 전부 잠겨 있습니다.

    순환 규칙을 넣어도 멈춥니다. 새다 → 날개가 있다가 발동한 뒤 날개가 있다 → 새다는 결론 '새다'가 이미 사실이라 검사 ②에 걸려 발동 자격을 잃어요. 개념 4의 그 한 줄이 여기서 일합니다.

    💻

    손으로 ② — fires()를 직접 채우고, 일부러 부순다

    판에서 본 것을 이제 코드로 옮깁니다. 아래 코드에는 빈칸이 네 곳 있어요. ①②③은 개념 4에 그대로 적혀 있고, ④는 ①에 적은 줄을 그대로 옮겨 적는 자리입니다. 다 채우고 실행하면 개념 4의 표가 여러분 화면에 찍혀야 합니다.

    • 빈칸 ①②③을 채운다. 실행해서 [A]가 새다 → 펭귄이다 → 발동할 규칙이 없다 순으로 나오면 성공입니다. 아는 사실 개수가 2 → 3 → 4 → 4로 움직이는지 확인하세요.
    • 빈칸 ④를 채워 일부러 부순다. fires_bad는 검사 ②가 빠진 판입니다. 몇 번 발동하고, 사실은 몇 개 늘었는지 세어 공책에 적으세요. range(6)을 지우지 마세요. 지우면 브라우저 탭이 멈춥니다.
    • [C] 표를 읽는다. 여덟 줄 가운데 발동이 (없음)인 줄이 둘 있습니다. 둘의 공통점을 한 줄로 적으세요. 그리고 맨 아래 줄에서 무슨 일이 벌어졌는지도요.
    • 본 과제 — 내 분야의 규칙 5개. RULES 목록 아래에 자기 분야의 규칙을 5개 더 적어 넣고 다시 실행하세요. 급식 메뉴 추천, 동아리 배정, 진로 추천, 우리 반 지각 판정 — 무엇이든 좋습니다. 관찰들 목록에도 자기 관찰을 한 줄 넣어야 [C] 표에 나옵니다.
    • 본 과제 — 실패한 규칙 1개. 규칙으로 적으려다 실패한 지식을 반드시 하나 적어 내세요. 무엇을 적으려 했고, 어디서 막혔는지 한 줄씩. 이 문장이 12차시 수업의 재료가 됩니다.
    💡 막혔을 때

    빈칸 ①은 all(로 시작하는 한 줄입니다 — "conds 안의 모든 c가 facts 안에 있는가". ②는 concl과 facts와 not in 세 조각으로 됩니다. ③은 집합에 원소 하나를 더한 새 집합이고, 파이썬에서 집합 합치기는 |입니다 ({1,2} | {3} → {1,2,3}). 개념 4의 코드 상자를 그대로 보고 옮겨도 됩니다.

    같은 규칙(IF 깃털이 있다 → 새다)이 여섯 번 내리 발동하고, 여섯 번을 돌린 뒤 아는 사실은 ['깃털이 있다', '새다', '헤엄친다'] — 하나도 늘지 않았습니다.

    왜 1번 규칙만 계속 나올까요? apply_once는 발동하는 규칙을 위에서부터 찾아 첫 번째 하나만 적용합니다. 검사 ②가 없으면 1번 규칙은 조건이 계속 참이므로 영원히 첫 번째 자리를 차지해요. 뒤의 규칙에는 차례가 영영 오지 않습니다. '펭귄이다'는 만들어질 수 없습니다.

    여기서 멈춘 것은 range(6) 덕분입니다. 이 상한을 빼고 '더 나올 것이 없을 때까지' 돌리면 2,000,000회를 돌고도(0.9초) 끝나지 않았습니다 — 실제로 재 본 값입니다. 검사 ②는 군더더기가 아니라 이 프로그램이 끝난다는 것을 보장하는 줄이에요.

    [C] 표에서 발동이 (없음)인 두 줄은 {헤엄친다}와 {빠르게 난다, 초음파를 쓴다}입니다. 공통점은 '깃털이 있다'도 '젖을 먹인다'도 없다는 것 — 첫 단추가 없으면 아무 문도 안 열립니다. 맨 아래 줄에서는 네 번 발동해 펭귄이면서 돌고래가 만들어집니다.

    ⚠️ 조용히 틀리는 자리 — 조건을 목록이 아니라 문자열로 적으면

    규칙을 적을 때 가장 흔한 실수입니다. (["깃털이 있다"], "새다")에서 대괄호를 빼고 ("깃털이 있다", "새다")라고 적어도 오류가 나지 않습니다. 대신 그 규칙은 한 번도 발동하지 않습니다. all(c in facts for c in conds)가 문자열을 한 글자씩 훑어 '깃', '털', '이'… 가 사실인지 묻기 때문이에요.

    어디서 빠뜨렸느냐에 따라 겉으로 보이는 것이 다릅니다. 규칙을 새로 하나 더할 때 빠뜨리면 그 규칙 하나만 죽고 나머지 6개는 그대로 돌아가므로 발동 목록은 멀쩡해 보입니다 — 알아채기가 훨씬 어렵지요. 반대로 1번 규칙에서 빠뜨리면 {깃털이 있다, 헤엄친다}의 발동 목록이 통째로 []가 됩니다. '새다'가 만들어지지 않으니 그 뒤 규칙이 전부 잠기기 때문이에요. 조건이 하나여도 ["깃털이 있다"]처럼 대괄호를 씌웁니다. 오류 메시지가 없으니 스스로 알아채는 수밖에 없습니다.

    📖

    정리 — 지식이 데이터가 되었다

    오늘 한 일을 한 문장으로 줄이면 이렇습니다. 머릿속에 있던 판단을 목록 하나로 옮겨 적었다. 그 순간 지식은 프로그램이 아니라 데이터가 되었고, 데이터가 되었기 때문에 늘리고, 고치고, 순서를 뒤집고, 통째로 갈아 끼울 수 있게 되었습니다.

    문법무엇코드에 적는 모양모아 두는 곳
    사실지금 참이라고 아는 것 하나 "깃털이 있다"집합 facts
    규칙조건이 전부 참이면 결론을 얹는 다리 (["새다", "헤엄친다"], "펭귄이다")목록 RULES

    그리고 발동의 조건이 둘이라는 것을 배웠습니다. ① 조건이 전부 이미 아는 사실이고, ② 결론이 아직 모르는 것일 때만 규칙이 열립니다. ②를 지우면 답이 틀리는 게 아니라 끝나지 않습니다 — 이것을 여섯 번의 재발동으로 직접 보았어요.

    남은 아쉬움도 분명합니다. 오늘 우리는 apply_once()를 손으로 세 번 불렀습니다. 규칙이 100개라면 몇 번을 불러야 할까요? 그리고 언제 그만 불러야 할지는 누가 정할까요? 다음 시간에는 이 되풀이를 기계에 맡기고, 더 나올 것이 없는 그 순간에 고정점이라는 이름을 붙입니다. 오늘 못박은 RULES 6줄은 11차시와 12차시에서 글자 그대로 다시 쓰입니다.

    🔁 되돌아보기

    오늘 사람의 판단을 사실과 규칙이라는 두 벌의 문법으로 적어 넣고, 규칙이 발동하는 조건 두 가지를 코드에서 직접 채워 확인했다. 그리고 규칙으로 적히지 않는 지식이 있다는 것도 손으로 겪었다. 다음 시간에는 이 한 걸음을 되풀이하게 만들어, 결론이 또 조건이 되는 연쇄가 어디서 멈추는지 본다.

    ✅

    확인 문제

    ✍️ 문제마다 답을 쓰고 제출하기를 누르세요. 제출하면 모범 답안이 열리고, 제출한 답은 선생님께 전달됩니다.

    1. 사실과 규칙의 차이를 정의로 쓰시오. 그리고 아래 다섯 문장을 둘로 나누시오.
    ㉠ 이 동물은 젖을 먹인다   ㉡ 젖을 먹이면 포유류다   ㉢ 오늘 급식은 카레다   ㉣ 비가 오면 운동장 조회를 안 한다   ㉤ 지금 기온이 31도다
    📖 모범 답안

    사실은 지금 참이라고 아는 것 하나로, 이 하나의 대상·상황에 대한 이야기다. 규칙은 "조건들이 전부 참이면 결론이 참이다"라는 다리로, 어느 대상에나 언제든 통하는 이야기다.

    사실: ㉠ · ㉢ · ㉤ / 규칙: ㉡ · ㉣. 가르는 잣대는 "지금 이것 하나에 대한 말인가, 아니면 '~이면 ~이다' 꼴로 언제든 통하는 말인가"다. ㉢은 오늘의 급식이라는 지금 사실이지만, "수요일이면 카레다"라고 적으면 규칙이 된다. 같은 내용도 어떻게 적느냐에 따라 결이 달라진다는 점이 중요하다.

    2. "열이 나고 기침을 하면 감기를 의심한다"를 (조건 목록, 결론) 짝으로 코드에 적으시오. 그리고 "열이 나거나 기침을 하면 진료를 받는다"는 어떻게 적어야 하는지도 쓰시오.
    📖 모범 답안

    앞의 것은 규칙 하나다.
    (["열이 난다", "기침을 한다"], "감기를 의심한다")

    뒤의 것은 OR이므로 규칙 두 줄로 나눠 적는다.
    (["열이 난다"], "진료를 받는다")
    (["기침을 한다"], "진료를 받는다")

    이 문법에는 AND만 있고 OR이 없다. 목록에 나란히 놓으면 AND이고, OR은 같은 결론을 갖는 규칙을 여럿 두는 것으로 대신한다. 문법을 하나 줄인 대신 규칙 줄 수가 늘어나는 맞바꿈이다.

    3. [오늘 실행한 결과] fires()의 빈칸 ①②를 채워 [A]를 돌렸을 때 아는 사실의 개수가 어떻게 움직였는지 네 숫자로 쓰고, 세 번째 호출에서 "발동할 규칙이 없다"가 나온 까닭을 설명하시오.
    📖 모범 답안

    2 → 3 → 4 → 4. 시작이 2개(깃털이 있다, 헤엄친다), 1번째 호출로 '새다'가 붙어 3개, 2번째 호출로 '펭귄이다'가 붙어 4개, 3번째 호출에서는 늘지 않아 그대로 4개다. 새로 알아낸 것은 2개다.

    세 번째에서 멈춘 까닭은 규칙 6개 가운데 두 검사를 함께 통과하는 것이 하나도 없기 때문이다. 1번(깃털→새다)과 4번(새다,헤엄친다→펭귄이다)은 조건은 여전히 참이지만 결론이 이미 사실에 있어서 검사 ②에 걸린다. 2·3·5·6번은 조건 가운데 아직 모르는 것이 있어 검사 ①에 걸린다 — 2번은 '젖을 먹인다'가, 3번은 '빠르게 난다'가, 5·6번은 '포유류다'가 사실에 없다. 사실은 늘기만 하므로 언젠가 이런 순간이 반드시 오고, 그것이 다음 시간에 배울 고정점이다.

    4. [오늘 실행한 결과] 검사 ②(concl not in facts)를 지운 fires_bad로 여섯 번 돌렸을 때 화면에 무엇이 몇 번 찍혔고, 사실은 몇 개 늘었는지 쓰시오. 그리고 range(6)이라는 상한이 왜 코드에 들어 있는지 설명하시오.
    📖 모범 답안

    규칙 발동: IF 깃털이 있다 -> 새다가 여섯 번 찍히고, 아는 사실은 하나도 늘지 않았다(3개 그대로). apply_once는 발동하는 첫 규칙 하나만 적용하는데, 검사 ②가 없으면 1번 규칙이 계속 첫 자리를 차지해 뒤 규칙에 차례가 오지 않기 때문이다.

    상한이 있는 까닭: 상한을 빼고 '더 나올 것이 없을 때까지' 돌리면 2,000,000회(0.9초)를 돌고도 끝나지 않는다. 이 교과서의 파이썬은 브라우저가 화면을 그리는 것과 같은 자리에서 돌기 때문에, 무한 루프는 곧 탭이 멈추는 것이다. 무한히 돌 수 있는 코드를 학생이 만나게 될 자리에는 상한을 두어 망가지더라도 말은 하고 망가지게 만드는 것이 옳다.

    5. [오늘 만진 판] 지식 베이스 판에서 {깃털이 있다, 젖을 먹인다, 헤엄친다}를 켰을 때 발동 횟수와 마지막 결론을 쓰고, 이 결과가 왜 프로그램의 버그가 아닌지 설명하시오. 이런 일을 막으려면 무엇이 더 필요한가?
    📖 모범 답안

    네 번 발동하고 새다 → 포유류다 → 펭귄이다 → 돌고래다 순으로 이어져 마지막 결론이 '돌고래다'다. 곧 펭귄이면서 동시에 돌고래인 것이 만들어진다.

    버그가 아니다. 코드는 적어 준 규칙을 정확히 그대로 따랐다. 잘못은 지식 쪽에 있다 — '깃털이 있으면서 젖을 먹이는 동물은 없다'는 것을 아무도 적어 주지 않았기 때문이다. 규칙은 조건이 맞는지만 보고, 결론들이 서로 말이 되는지는 보지 않는다.

    막으려면 규칙을 더 적어야 한다. 그런데 '깃털이 있으면 젖을 먹이지 않는다'는 '없음'을 다루는 문법(NOT)을 요구한다. 오늘 문법에는 그것이 없다. 세상의 상식을 빠짐없이 적어 넣는 일이 얼마나 끝없는지를 보여 주는 자리이고, 이것이 12차시에서 다시 만날 한계다.

    6. 규칙 기반으로 적는 것이 3단원에서 배울 if 문과 어떻게 다른지, "규칙을 하나 더 얹을 때"를 예로 들어 설명하시오. 그리고 여러분이 규칙으로 적으려다 실패한 지식을 하나 들고, 왜 이 문법에 담기지 않았는지 쓰시오.
    📖 모범 답안

    다른 점: if·elif는 사람이 정한 순서대로 실행되므로, 판단을 하나 더 넣으려면 어느 가지에 끼울지 사람이 정하고 코드를 고쳐야 한다. 규칙은 순서 없이 쌓아 두고 조건이 맞는 것이 스스로 발동하므로, "밤에 활동하는 새는 올빼미다"를 넣고 싶으면 목록 맨 뒤에 한 줄 더하면 끝이고 규칙을 읽는 코드는 한 글자도 안 바뀐다. 판에서 순서를 뒤집어 보면 발동 순서는 달라지지만 최종 결론 집합은 같다는 것으로 이 성질을 확인할 수 있다. 덕분에 프로그래머가 아닌 전문가가 직접 지식을 관리할 수 있다.

    실패한 지식은 사람마다 다르다. 채점 기준은 두 가지다 — ① 적으려던 판단을 구체적으로 적었는가, ② 어디서 막혔는지를 문법의 말로 설명했는가. 좋은 답의 예: "'이 문장은 어색하다'를 적으려 했는데, 조건으로 쓸 사실을 잘라 낼 수가 없었다. '어색하다'를 만드는 것이 낱말인지 순서인지 상황인지 나 자신도 대지 못했다." 이런 간격이 지식 획득의 병목이고, 규칙을 사람이 적는 대신 데이터에서 뽑아내자는 생각(Ⅱ단원 기계학습)이 여기서 나온다.

    🔎

    더 알아보기

    사실과 규칙으로 세상을 적으려 할 때 — 끝없는 상식, 집합의 속도, 오래된 검색표

    블라인드가 내려진 창가에서 웃고 있는 더글러스 레나트, 체크무늬 셔츠에 짙은 재킷 차림
    역사

    상식을 전부 적어 넣으려 한 사람 — Cyc 프로젝트

    확인 문제 5에서 규칙 여섯 줄이 펭귄이면서 돌고래인 것을 만들어 냈지요. 막으려면 '새와 포유류는 겹치지 않는다' 같은 말을 누군가 적어 주어야 합니다. 사람에게는 너무 당연해서 아무도 입 밖에 내지 않는 이런 지식을 상식이라고 부릅니다.

    이 상식을 정면으로 적어 넣으려 한 사람이 사진의 더글러스 레나트(Douglas Lenat)입니다. 그는 전문가 시스템이 자기 분야를 조금만 벗어나도 엉뚱한 답을 내는 까닭이 상식이 없어서라고 보았고, 1984년 미국 텍사스 오스틴의 연구 컨소시엄 MCC에서 백과사전(encyclopedia)에서 이름을 딴 Cyc(사이크)를 시작했습니다. "물건을 놓으면 떨어진다", "사람은 한 번에 한 곳에만 있다" 같은 말을 사실과 규칙으로 하나하나 적는 일이었지요.

    프로젝트는 1994년 회사 Cycorp로 독립했고, 레나트는 2023년 세상을 떠날 때까지 이 일을 이끌었습니다. 사람이 손으로 적은 단언은 수백만 개로 불어났지만 "이제 다 적었다"고 말할 날은 오지 않았습니다. 오늘 겪은 지식 획득의 병목이 가장 큰 규모로 드러난 사례이고, Ⅱ단원의 기계학습은 정반대 방향 — 사람이 적는 대신 데이터에서 뽑아내는 길을 택합니다.

    사진: Cycorp 사무실의 더글러스 레나트 · 출처: LordRedthorn, Wikimedia Commons (CC BY-SA 4.0)

    목록(list) — 앞에서부터 하나씩 견준다 새다 깃털 젖 난다 초음파 헤엄 1 2 3 4 5 6번째 찾는 말이 맨 끝이면 여섯 번 모두 견준다 — 많을수록 느리다 집합(set) — 해시로 칸 번호를 곧장 계산한다 "헤엄친다" hash( ) 5번 칸 0 1 2 3 4 헤엄 6 7 원소가 여섯 개든 육만 개든, 그 칸 하나만 들여다본다
    원리 더 깊이

    사실을 왜 집합에 담았나 — 해시라는 지름길

    개념 1에서 사실을 집합에 담는 까닭 셋째로 "있는지 없는지만 빠르게 묻는다"를 들었습니다. 만약 사실을 목록(list)에 담았다면 "헤엄친다" in facts는 앞에서부터 하나씩 견주어야 합니다. 찾는 말이 맨 끝에 있거나 아예 없으면 원소 수만큼 견주게 되지요. fires()는 이 물음을 규칙마다, 걸음마다 던지므로 차이가 쌓입니다.

    파이썬의 집합은 해시 표로 만들어져 있습니다. 원소를 넣을 때 hash()로 문자열을 정수로 바꾸고, 그 수로 들어갈 칸을 정해 둡니다. 물을 때도 같은 계산을 해서 그 칸만 들여다보면 됩니다. 가끔 두 원소가 같은 칸을 가리키면 몇 칸 더 살피지만, 평균으로 보면 원소가 여섯 개든 육만 개든 걸리는 시간이 거의 같습니다.

    이 지름길에는 조건이 붙습니다. 해시값이 바뀌면 칸을 잃어버리므로 내용이 바뀔 수 있는 것은 집합에 넣을 수 없습니다. {["깃털이 있다"]}라고 쓰면 TypeError: unhashable type: 'list'가 납니다. 그래서 사실은 문자열로 집합에 담고, 규칙의 조건은 목록으로 RULES에 담았습니다. 또 문자열의 해시값은 실행할 때마다 달라질 수 있어서 집합을 그대로 찍으면 순서가 바뀌기도 합니다 — 오늘 코드가 사실을 sorted()로 정렬해 찍은 까닭입니다.

    깃털이 있나? 예 아니오 헤엄치나? 헤엄치나? 예 아니오 예 아니오 펭귄 매 돌고래 박쥐 묻는 차례가 정해져 있고, 끝까지 가면 반드시 이름 하나 — 모양은 if · elif 와 같다
    생각할 거리

    생물학자의 오래된 규칙표 — 검색표와 규칙 더미는 무엇이 다른가

    생물학자들은 처음 보는 생물의 이름을 검색표(이분 검색표)로 찾습니다. "잎이 마주 나는가, 어긋나는가?"처럼 두 갈래 물음에 차례로 답하다 보면 이름 하나에 닿는 표예요. 프랑스의 박물학자 라마르크가 1778년 『프랑스 식물지』에 이 방식을 써서 널리 퍼뜨렸습니다. 그림은 오늘의 네 동물을 검색표로 짜 본 것입니다. 갈래마다 IF가 있으니 규칙과 꼭 닮았지요.

    그런데 결정적인 차이가 둘 있습니다. 첫째, 검색표는 묻는 차례가 정해져 있습니다. 개념 3의 if·elif와 같은 모양이라, 올빼미를 새로 넣으려면 어느 갈래에 끼울지 사람이 정하고 표를 다시 짜야 합니다. 규칙 더미는 목록 끝에 한 줄을 더하면 끝났지요.

    둘째, 검색표는 끝까지 가면 반드시 이름 하나를 내놓습니다. 이 표에서는 깃털도 없고 헤엄도 치지 않으면 무엇이든 '박쥐'가 됩니다 — 고양이를 넣어도요. 오늘의 규칙 더미는 맞는 규칙이 없으면 '(없음)'이라고 말하고, 조건이 겹치면 펭귄과 돌고래를 둘 다 내놓기도 했습니다. 검색표는 '찾는 대상이 이 목록 안에 있다'는 것이 확실할 때 빠르고 깔끔하며, 규칙 더미는 지식이 계속 늘고 바뀌는 곳에서 힘을 냅니다.